Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Claude Agent** provider, which runs AI chat through the Claude Code command line tool so you can use a Claude subscription instead of a metered API key. It answers from your schema when the MCP server is on.
- **Count exactly** next to an estimated row total, to replace the estimate with a real count when you want it.
- Reorder filter rows by dragging the grip on the left of each row, or with **Move Up** and **Move Down** in the row's right-click menu.
- An **On Update** column in the Structure tab for MySQL and MariaDB, so a timestamp column can be set to update itself without writing the SQL by hand. (#2005)

### Fixed

- Editing a MySQL or MariaDB column no longer drops its `ON UPDATE CURRENT_TIMESTAMP`. Saving a change to any other part of the column, even just its comment, silently removed the clause. (#2005)
- A MySQL or MariaDB timestamp column that keeps fractional seconds now saves. Its default was written as text instead of an expression, so the change was rejected. (#2005)

- Quitting no longer loses unsaved SQL editor tabs. A window with no tabs loaded yet, such as one still waiting on its connection, could erase the saved tabs for that connection, so nothing came back on relaunch. Editors are also saved about a second after you stop typing instead of waiting up to 30 seconds, and a query over 500KB is kept in full rather than restored empty. (#1997)
- Opening a large MongoDB collection no longer hangs. Row counts that back the pagination display now give up after 5 seconds and leave the estimate in place instead of holding the tab.
- Stop now cancels a running MongoDB query on the server, rather than leaving it running until it finishes on its own.
Expand Down
2 changes: 1 addition & 1 deletion Plugins/BeancountDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesDatabaseTypeIds</key>
<array>
<string>Beancount</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/BigQueryDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/CSVExportPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesExportFormatIds</key>
<array>
<string>csv</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/CSVImportPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesImportFormatIds</key>
<array>
<string>csv</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/CassandraDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>NSPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).CassandraPlugin</string>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/ClickHouseDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesDatabaseTypeIds</key>
<array>
<string>ClickHouse</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/CloudflareD1DriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/DuckDBDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/DynamoDBDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/ElasticsearchDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<key>TableProMinAppVersion</key>
<string>0.53.0</string>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/EtcdDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/JSONExportPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesExportFormatIds</key>
<array>
<string>json</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/JSONImportPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesImportFormatIds</key>
<array>
<string>json</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/LibSQLDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/MQLExportPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesExportFormatIds</key>
<array>
<string>mql</string>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/MSSQLDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/MongoDBDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/MySQLDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesDatabaseTypeIds</key>
<array>
<string>MySQL</string>
Expand Down
85 changes: 85 additions & 0 deletions Plugins/MySQLDriverPlugin/MySQLColumnDefinitionSQL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// MySQLColumnDefinitionSQL.swift
// MySQLDriverPlugin
//

import Foundation
import TableProPluginKit

internal func mysqlQuoteIdentifier(_ name: String) -> String {
let escaped = name.replacingOccurrences(of: "`", with: "``")
return "`\(escaped)`"
}

internal func mysqlEscapeStringLiteral(_ value: String) -> String {
var result = value
result = result.replacingOccurrences(of: "\\", with: "\\\\")
result = result.replacingOccurrences(of: "'", with: "''")
result = result.replacingOccurrences(of: "\n", with: "\\n")
result = result.replacingOccurrences(of: "\r", with: "\\r")
result = result.replacingOccurrences(of: "\t", with: "\\t")
result = result.replacingOccurrences(of: "\0", with: "\\0")
result = result.replacingOccurrences(of: "\u{08}", with: "\\b")
result = result.replacingOccurrences(of: "\u{0C}", with: "\\f")
result = result.replacingOccurrences(of: "\u{1A}", with: "\\Z")
return result
}

/// MySQL rejects a CURRENT_TIMESTAMP expression whose fractional-second precision differs
/// from the column's own, so the precision is always taken from the declared type.
internal func mysqlFractionalSecondsSuffix(forDataType dataType: String) -> String {
let upper = dataType.uppercased()
guard upper.hasPrefix("TIMESTAMP(") || upper.hasPrefix("DATETIME(") else { return "" }
guard let open = dataType.firstIndex(of: "("),
let close = dataType[open...].firstIndex(of: ")") else { return "" }
return String(dataType[open...close])
}

internal func mysqlCurrentTimestampExpression(_ value: String, dataType: String) -> String? {
let upper = value.uppercased()
guard upper == "CURRENT_TIMESTAMP" || upper == "CURRENT_TIMESTAMP()"
|| upper.hasPrefix("CURRENT_TIMESTAMP(") else { return nil }
return "CURRENT_TIMESTAMP" + mysqlFractionalSecondsSuffix(forDataType: dataType)
}

internal func mysqlDefaultValueLiteral(_ value: String, dataType: String) -> String {
if let expression = mysqlCurrentTimestampExpression(value, dataType: dataType) { return expression }
if value.uppercased() == "NULL" || value.hasPrefix("'") { return value }
if Int64(value) != nil || Double(value) != nil { return value }
return "'\(mysqlEscapeStringLiteral(value))'"
}

internal func mysqlColumnAttributesSQL(_ column: PluginColumnDefinition) -> String {
var def = ""

if column.unsigned {
def += " UNSIGNED"
}
if let charset = column.charset, !charset.isEmpty {
def += " CHARACTER SET \(charset)"
}
if let collation = column.collation, !collation.isEmpty {
def += " COLLATE \(collation)"
}
def += column.isNullable ? " NULL" : " NOT NULL"

if let defaultValue = column.defaultValue {
def += " DEFAULT \(mysqlDefaultValueLiteral(defaultValue, dataType: column.dataType))"
}
if column.autoIncrement {
def += " AUTO_INCREMENT"
}
if let onUpdate = column.onUpdate,
let expression = mysqlCurrentTimestampExpression(onUpdate, dataType: column.dataType) {
def += " ON UPDATE \(expression)"
}
if let comment = column.comment, !comment.isEmpty {
def += " COMMENT '\(mysqlEscapeStringLiteral(comment))'"
}

return def
}

internal func mysqlColumnDefinitionSQL(_ column: PluginColumnDefinition) -> String {
"\(mysqlQuoteIdentifier(column.name)) \(column.dataType)" + mysqlColumnAttributesSQL(column)
}
2 changes: 1 addition & 1 deletion Plugins/MySQLDriverPlugin/MySQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ final class MySQLPlugin: NSObject, TableProPlugin, DriverPlugin {
]

static let structureColumnFields: [StructureColumnField] = [
.name, .type, .nullable, .defaultValue, .autoIncrement, .comment, .charset, .collation
.name, .type, .nullable, .defaultValue, .onUpdate, .autoIncrement, .comment, .charset, .collation
]

static let sqlDialect: SQLDialectDescriptor? = SQLDialectDescriptor(
Expand Down
97 changes: 4 additions & 93 deletions Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,11 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
}

func quoteIdentifier(_ name: String) -> String {
let escaped = name.replacingOccurrences(of: "`", with: "``")
return "`\(escaped)`"
mysqlQuoteIdentifier(name)
}

func escapeStringLiteral(_ value: String) -> String {
var result = value
result = result.replacingOccurrences(of: "\\", with: "\\\\")
result = result.replacingOccurrences(of: "'", with: "''")
result = result.replacingOccurrences(of: "\n", with: "\\n")
result = result.replacingOccurrences(of: "\r", with: "\\r")
result = result.replacingOccurrences(of: "\t", with: "\\t")
result = result.replacingOccurrences(of: "\0", with: "\\0")
result = result.replacingOccurrences(of: "\u{08}", with: "\\b")
result = result.replacingOccurrences(of: "\u{0C}", with: "\\f")
result = result.replacingOccurrences(of: "\u{1A}", with: "\\Z")
return result
mysqlEscapeStringLiteral(value)
}

private static let tableNameRegex = try? NSRegularExpression(pattern: "(?i)\\bFROM\\s+[`\"']?([\\w]+)[`\"']?")
Expand Down Expand Up @@ -761,48 +750,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
}

private func buildColumnDefinitionSQL(_ column: PluginColumnDefinition) -> String {
var def = "\(quoteIdentifier(column.name)) \(column.dataType)"

if column.unsigned {
def += " UNSIGNED"
}
if let charset = column.charset, !charset.isEmpty {
def += " CHARACTER SET \(charset)"
}
if let collation = column.collation, !collation.isEmpty {
def += " COLLATE \(collation)"
}
if column.isNullable {
def += " NULL"
} else {
def += " NOT NULL"
}
if let defaultValue = column.defaultValue {
let upper = defaultValue.uppercased()
if upper == "NULL" || upper == "CURRENT_TIMESTAMP" || upper == "CURRENT_TIMESTAMP()"
|| defaultValue.hasPrefix("'") {
def += " DEFAULT \(defaultValue)"
} else if Int64(defaultValue) != nil || Double(defaultValue) != nil {
def += " DEFAULT \(defaultValue)"
} else {
def += " DEFAULT '\(escapeStringLiteral(defaultValue))'"
}
}
if column.autoIncrement {
def += " AUTO_INCREMENT"
}
if let onUpdate = column.onUpdate, !onUpdate.isEmpty {
let upper = onUpdate.uppercased()
if upper == "CURRENT_TIMESTAMP" || upper == "CURRENT_TIMESTAMP()"
|| upper.hasPrefix("CURRENT_TIMESTAMP(") {
def += " ON UPDATE \(onUpdate)"
}
}
if let comment = column.comment, !comment.isEmpty {
def += " COMMENT '\(escapeStringLiteral(comment))'"
}

return def
mysqlColumnDefinitionSQL(column)
}

private func buildIndexDefinitionSQL(_ index: PluginIndexDefinition) -> String {
Expand Down Expand Up @@ -927,44 +875,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
let tableName = quoteIdentifier(table)
let colName = quoteIdentifier(column.name)

var def = "\(column.dataType)"
if column.unsigned {
def += " UNSIGNED"
}
if let charset = column.charset, !charset.isEmpty {
def += " CHARACTER SET \(charset)"
}
if let collation = column.collation, !collation.isEmpty {
def += " COLLATE \(collation)"
}
if column.isNullable {
def += " NULL"
} else {
def += " NOT NULL"
}
if let defaultValue = column.defaultValue {
let upper = defaultValue.uppercased()
if upper == "NULL" || upper == "CURRENT_TIMESTAMP" || upper == "CURRENT_TIMESTAMP()"
|| defaultValue.hasPrefix("'") {
def += " DEFAULT \(defaultValue)"
} else if Int64(defaultValue) != nil || Double(defaultValue) != nil {
def += " DEFAULT \(defaultValue)"
} else {
def += " DEFAULT '\(escapeStringLiteral(defaultValue))'"
}
}
if column.autoIncrement {
def += " AUTO_INCREMENT"
}
if let onUpdate = column.onUpdate, !onUpdate.isEmpty {
let upper = onUpdate.uppercased()
if upper == "CURRENT_TIMESTAMP" || upper == "CURRENT_TIMESTAMP()" || upper.hasPrefix("CURRENT_TIMESTAMP(") {
def += " ON UPDATE \(onUpdate)"
}
}
if let comment = column.comment, !comment.isEmpty {
def += " COMMENT '\(escapeStringLiteral(comment))'"
}
let def = "\(column.dataType)" + mysqlColumnAttributesSQL(column)

let position: String
if let afterCol = afterColumn {
Expand Down
2 changes: 1 addition & 1 deletion Plugins/OracleDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Plugins/PostgreSQLDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>TableProPluginKitVersion</key>
<integer>18</integer>
<integer>19</integer>
<key>TableProProvidesDatabaseTypeIds</key>
<array>
<string>PostgreSQL</string>
Expand Down
Loading
Loading