diff --git a/CHANGELOG.md b/CHANGELOG.md index a6954ecc1..6478585a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Cassandra: connection now fails fast with a clear "Cassandra 2.x is not supported" message instead of cryptic "table not found" errors during sidebar load. +- MongoDB: dropped the `nameOnly: true` flag on `listDatabases` for servers older than 3.4, which previously rejected the flag. +- ClickHouse: index sidebar no longer fails on ClickHouse older than 19.17 by skipping the `system.data_skipping_indices` lookup when the table doesn't exist. +- MSSQL: view templates fall back to `IF EXISTS DROP / CREATE VIEW` on SQL Server 2014 and earlier, which lack `CREATE OR ALTER VIEW`. +- MySQL: added a plain `EXPLAIN` variant alongside `EXPLAIN FORMAT=JSON` so MySQL 5.5 users can run query plans. - PostgreSQL: connecting to servers older than 9.3 no longer fails with "relation pg_matviews does not exist"; the driver feature-gates `pg_matviews`, `pg_foreign_table`, `pg_sequences`, `array_position`, `attidentity`, `attgenerated`, and ICU locale columns behind the detected server version (#1240). ## [0.40.2] - 2026-05-12 diff --git a/Plugins/CassandraDriverPlugin/CassandraCapabilities.swift b/Plugins/CassandraDriverPlugin/CassandraCapabilities.swift new file mode 100644 index 000000000..958f727a4 --- /dev/null +++ b/Plugins/CassandraDriverPlugin/CassandraCapabilities.swift @@ -0,0 +1,22 @@ +// +// CassandraCapabilities.swift +// CassandraDriverPlugin +// + +import Foundation + +struct CassandraCapabilities: Sendable, Equatable { + let releaseVersionMajor: Int + + static let unknown = CassandraCapabilities(releaseVersionMajor: 0) + + var hasSystemSchemaKeyspace: Bool { releaseVersionMajor >= 3 } + + static func parseMajorVersion(_ version: String?) -> Int { + guard let version, let majorString = version.split(separator: ".").first, + let major = Int(majorString) else { + return 0 + } + return major + } +} diff --git a/Plugins/CassandraDriverPlugin/CassandraPlugin.swift b/Plugins/CassandraDriverPlugin/CassandraPlugin.swift index 09f2d7050..ff7e2f22f 100644 --- a/Plugins/CassandraDriverPlugin/CassandraPlugin.swift +++ b/Plugins/CassandraDriverPlugin/CassandraPlugin.swift @@ -921,12 +921,21 @@ internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sen stateLock.unlock() } - // Cache server version if let version = try? await connectionActor.serverVersion() { stateLock.lock() _cachedVersion = version stateLock.unlock() } + + let caps = CassandraCapabilities( + releaseVersionMajor: CassandraCapabilities.parseMajorVersion(serverVersion) + ) + guard caps.hasSystemSchemaKeyspace else { + throw CassandraPluginError.connectionFailed(String( + format: String(localized: "Cassandra %@ is not supported. TablePro requires Cassandra 3.0 or later (the system_schema keyspace was introduced in 3.0)."), + serverVersion ?? "" + )) + } } func disconnect() { diff --git a/Plugins/ClickHouseDriverPlugin/ClickHouseCapabilities.swift b/Plugins/ClickHouseDriverPlugin/ClickHouseCapabilities.swift new file mode 100644 index 000000000..73e547270 --- /dev/null +++ b/Plugins/ClickHouseDriverPlugin/ClickHouseCapabilities.swift @@ -0,0 +1,25 @@ +// +// ClickHouseCapabilities.swift +// ClickHouseDriverPlugin +// + +import Foundation + +struct ClickHouseCapabilities: Sendable, Equatable { + let major: Int + let minor: Int + + static let unknown = ClickHouseCapabilities(major: 0, minor: 0) + + var hasDataSkippingIndicesTable: Bool { + major > 19 || (major == 19 && minor >= 17) + } + + static func parse(_ version: String?) -> ClickHouseCapabilities { + guard let version else { return .unknown } + let parts = version.split(separator: ".") + guard let major = parts.first.flatMap({ Int($0) }) else { return .unknown } + let minor = parts.count > 1 ? (Int(parts[1]) ?? 0) : 0 + return ClickHouseCapabilities(major: major, minor: minor) + } +} diff --git a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift index 597c6777f..629599dee 100644 --- a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift +++ b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift @@ -435,6 +435,8 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable { )) } + let caps = ClickHouseCapabilities.parse(serverVersion) + guard caps.hasDataSkippingIndicesTable else { return indexes } let skippingSql = """ SELECT name, expr FROM system.data_skipping_indices WHERE database = currentDatabase() AND table = '\(escapedTable)' diff --git a/Plugins/MSSQLDriverPlugin/MSSQLCapabilities.swift b/Plugins/MSSQLDriverPlugin/MSSQLCapabilities.swift new file mode 100644 index 000000000..269b5e5ec --- /dev/null +++ b/Plugins/MSSQLDriverPlugin/MSSQLCapabilities.swift @@ -0,0 +1,29 @@ +// +// MSSQLCapabilities.swift +// MSSQLDriverPlugin +// + +import Foundation + +struct MSSQLCapabilities: Sendable, Equatable { + let major: Int + + static let unknown = MSSQLCapabilities(major: 0) + + var hasCreateOrAlterView: Bool { major >= 13 } + + static func parse(_ versionString: String?) -> MSSQLCapabilities { + guard let versionString else { return .unknown } + let pattern = #"(\d+)\.\d+\.\d+"# + guard let regex = try? NSRegularExpression(pattern: pattern), + let match = regex.firstMatch( + in: versionString, + range: NSRange(versionString.startIndex..., in: versionString) + ), + let range = Range(match.range(at: 1), in: versionString), + let major = Int(versionString[range]) else { + return .unknown + } + return MSSQLCapabilities(major: major) + } +} diff --git a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift index b7e918388..af6a6556d 100644 --- a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift +++ b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift @@ -752,12 +752,18 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { // MARK: - View Templates func createViewTemplate() -> String? { - "CREATE OR ALTER VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;" + if MSSQLCapabilities.parse(serverVersion).hasCreateOrAlterView { + return "CREATE OR ALTER VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;" + } + return "CREATE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;" } func editViewFallbackTemplate(viewName: String) -> String? { let quoted = quoteIdentifier(viewName) - return "CREATE OR ALTER VIEW \(quoted) AS\nSELECT * FROM table_name;" + if MSSQLCapabilities.parse(serverVersion).hasCreateOrAlterView { + return "CREATE OR ALTER VIEW \(quoted) AS\nSELECT * FROM table_name;" + } + return "IF OBJECT_ID('\(viewName)', 'V') IS NOT NULL DROP VIEW \(quoted);\nCREATE VIEW \(quoted) AS\nSELECT * FROM table_name;" } func castColumnToText(_ column: String) -> String { diff --git a/Plugins/MongoDBDriverPlugin/MongoDBCapabilities.swift b/Plugins/MongoDBDriverPlugin/MongoDBCapabilities.swift new file mode 100644 index 000000000..945fc105d --- /dev/null +++ b/Plugins/MongoDBDriverPlugin/MongoDBCapabilities.swift @@ -0,0 +1,25 @@ +// +// MongoDBCapabilities.swift +// MongoDBDriverPlugin +// + +import Foundation + +struct MongoDBCapabilities: Sendable, Equatable { + let major: Int + let minor: Int + + static let unknown = MongoDBCapabilities(major: 0, minor: 0) + + var supportsListDatabasesNameOnly: Bool { + major > 3 || (major == 3 && minor >= 4) + } + + static func parse(_ version: String?) -> MongoDBCapabilities { + guard let version else { return .unknown } + let parts = version.split(separator: ".") + guard let major = parts.first.flatMap({ Int($0) }) else { return .unknown } + let minor = parts.count > 1 ? (Int(parts[1]) ?? 0) : 0 + return MongoDBCapabilities(major: major, minor: minor) + } +} diff --git a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift index 1e3dac335..b8b85e257 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift @@ -1044,7 +1044,11 @@ private extension MongoDBConnection { func listDatabasesSync(client: OpaquePointer) throws -> [String] { try checkCancelled() - guard let command = jsonToBson("{\"listDatabases\": 1, \"nameOnly\": true}") else { + let caps = MongoDBCapabilities.parse(serverVersion()) + let commandJSON = caps.supportsListDatabasesNameOnly + ? "{\"listDatabases\": 1, \"nameOnly\": true}" + : "{\"listDatabases\": 1}" + guard let command = jsonToBson(commandJSON) else { throw MongoDBError(code: 0, message: "Failed to create listDatabases command") } defer { bson_destroy(command) } diff --git a/Plugins/MySQLDriverPlugin/MySQLPlugin.swift b/Plugins/MySQLDriverPlugin/MySQLPlugin.swift index d240a1876..4a4cd9e8e 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPlugin.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPlugin.swift @@ -29,7 +29,8 @@ final class MySQLPlugin: NSObject, TableProPlugin, DriverPlugin { static let urlSchemes: [String] = ["mysql"] static let explainVariants: [ExplainVariant] = [ - ExplainVariant(id: "explain", label: "EXPLAIN", sqlPrefix: "EXPLAIN FORMAT=JSON"), + ExplainVariant(id: "explain", label: "EXPLAIN", sqlPrefix: "EXPLAIN"), + ExplainVariant(id: "explain-json", label: "EXPLAIN (JSON)", sqlPrefix: "EXPLAIN FORMAT=JSON"), ] static let brandColorHex = "#FF9500" static let postConnectActions: [PostConnectAction] = [.selectDatabaseFromLastSession]