Skip to content

Commit 74eb1c9

Browse files
authored
fix(plugins): version-gate SQL across Cassandra, MongoDB, ClickHouse, MSSQL, MySQL (#1242)
Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent 9f14077 commit 74eb1c9

10 files changed

Lines changed: 133 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- 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.
13+
- MongoDB: dropped the `nameOnly: true` flag on `listDatabases` for servers older than 3.4, which previously rejected the flag.
14+
- 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.
15+
- MSSQL: view templates fall back to `IF EXISTS DROP / CREATE VIEW` on SQL Server 2014 and earlier, which lack `CREATE OR ALTER VIEW`.
16+
- MySQL: added a plain `EXPLAIN` variant alongside `EXPLAIN FORMAT=JSON` so MySQL 5.5 users can run query plans.
1217
- 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).
1318

1419
## [0.40.2] - 2026-05-12
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// CassandraCapabilities.swift
3+
// CassandraDriverPlugin
4+
//
5+
6+
import Foundation
7+
8+
struct CassandraCapabilities: Sendable, Equatable {
9+
let releaseVersionMajor: Int
10+
11+
static let unknown = CassandraCapabilities(releaseVersionMajor: 0)
12+
13+
var hasSystemSchemaKeyspace: Bool { releaseVersionMajor >= 3 }
14+
15+
static func parseMajorVersion(_ version: String?) -> Int {
16+
guard let version, let majorString = version.split(separator: ".").first,
17+
let major = Int(majorString) else {
18+
return 0
19+
}
20+
return major
21+
}
22+
}

Plugins/CassandraDriverPlugin/CassandraPlugin.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -921,12 +921,21 @@ internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sen
921921
stateLock.unlock()
922922
}
923923

924-
// Cache server version
925924
if let version = try? await connectionActor.serverVersion() {
926925
stateLock.lock()
927926
_cachedVersion = version
928927
stateLock.unlock()
929928
}
929+
930+
let caps = CassandraCapabilities(
931+
releaseVersionMajor: CassandraCapabilities.parseMajorVersion(serverVersion)
932+
)
933+
guard caps.hasSystemSchemaKeyspace else {
934+
throw CassandraPluginError.connectionFailed(String(
935+
format: String(localized: "Cassandra %@ is not supported. TablePro requires Cassandra 3.0 or later (the system_schema keyspace was introduced in 3.0)."),
936+
serverVersion ?? "<unknown>"
937+
))
938+
}
930939
}
931940

932941
func disconnect() {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// ClickHouseCapabilities.swift
3+
// ClickHouseDriverPlugin
4+
//
5+
6+
import Foundation
7+
8+
struct ClickHouseCapabilities: Sendable, Equatable {
9+
let major: Int
10+
let minor: Int
11+
12+
static let unknown = ClickHouseCapabilities(major: 0, minor: 0)
13+
14+
var hasDataSkippingIndicesTable: Bool {
15+
major > 19 || (major == 19 && minor >= 17)
16+
}
17+
18+
static func parse(_ version: String?) -> ClickHouseCapabilities {
19+
guard let version else { return .unknown }
20+
let parts = version.split(separator: ".")
21+
guard let major = parts.first.flatMap({ Int($0) }) else { return .unknown }
22+
let minor = parts.count > 1 ? (Int(parts[1]) ?? 0) : 0
23+
return ClickHouseCapabilities(major: major, minor: minor)
24+
}
25+
}

Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,8 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
435435
))
436436
}
437437

438+
let caps = ClickHouseCapabilities.parse(serverVersion)
439+
guard caps.hasDataSkippingIndicesTable else { return indexes }
438440
let skippingSql = """
439441
SELECT name, expr FROM system.data_skipping_indices
440442
WHERE database = currentDatabase() AND table = '\(escapedTable)'
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// MSSQLCapabilities.swift
3+
// MSSQLDriverPlugin
4+
//
5+
6+
import Foundation
7+
8+
struct MSSQLCapabilities: Sendable, Equatable {
9+
let major: Int
10+
11+
static let unknown = MSSQLCapabilities(major: 0)
12+
13+
var hasCreateOrAlterView: Bool { major >= 13 }
14+
15+
static func parse(_ versionString: String?) -> MSSQLCapabilities {
16+
guard let versionString else { return .unknown }
17+
let pattern = #"(\d+)\.\d+\.\d+"#
18+
guard let regex = try? NSRegularExpression(pattern: pattern),
19+
let match = regex.firstMatch(
20+
in: versionString,
21+
range: NSRange(versionString.startIndex..., in: versionString)
22+
),
23+
let range = Range(match.range(at: 1), in: versionString),
24+
let major = Int(versionString[range]) else {
25+
return .unknown
26+
}
27+
return MSSQLCapabilities(major: major)
28+
}
29+
}

Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -752,12 +752,18 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
752752
// MARK: - View Templates
753753

754754
func createViewTemplate() -> String? {
755-
"CREATE OR ALTER VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;"
755+
if MSSQLCapabilities.parse(serverVersion).hasCreateOrAlterView {
756+
return "CREATE OR ALTER VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;"
757+
}
758+
return "CREATE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;"
756759
}
757760

758761
func editViewFallbackTemplate(viewName: String) -> String? {
759762
let quoted = quoteIdentifier(viewName)
760-
return "CREATE OR ALTER VIEW \(quoted) AS\nSELECT * FROM table_name;"
763+
if MSSQLCapabilities.parse(serverVersion).hasCreateOrAlterView {
764+
return "CREATE OR ALTER VIEW \(quoted) AS\nSELECT * FROM table_name;"
765+
}
766+
return "IF OBJECT_ID('\(viewName)', 'V') IS NOT NULL DROP VIEW \(quoted);\nCREATE VIEW \(quoted) AS\nSELECT * FROM table_name;"
761767
}
762768

763769
func castColumnToText(_ column: String) -> String {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// MongoDBCapabilities.swift
3+
// MongoDBDriverPlugin
4+
//
5+
6+
import Foundation
7+
8+
struct MongoDBCapabilities: Sendable, Equatable {
9+
let major: Int
10+
let minor: Int
11+
12+
static let unknown = MongoDBCapabilities(major: 0, minor: 0)
13+
14+
var supportsListDatabasesNameOnly: Bool {
15+
major > 3 || (major == 3 && minor >= 4)
16+
}
17+
18+
static func parse(_ version: String?) -> MongoDBCapabilities {
19+
guard let version else { return .unknown }
20+
let parts = version.split(separator: ".")
21+
guard let major = parts.first.flatMap({ Int($0) }) else { return .unknown }
22+
let minor = parts.count > 1 ? (Int(parts[1]) ?? 0) : 0
23+
return MongoDBCapabilities(major: major, minor: minor)
24+
}
25+
}

Plugins/MongoDBDriverPlugin/MongoDBConnection.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,11 @@ private extension MongoDBConnection {
10441044
func listDatabasesSync(client: OpaquePointer) throws -> [String] {
10451045
try checkCancelled()
10461046

1047-
guard let command = jsonToBson("{\"listDatabases\": 1, \"nameOnly\": true}") else {
1047+
let caps = MongoDBCapabilities.parse(serverVersion())
1048+
let commandJSON = caps.supportsListDatabasesNameOnly
1049+
? "{\"listDatabases\": 1, \"nameOnly\": true}"
1050+
: "{\"listDatabases\": 1}"
1051+
guard let command = jsonToBson(commandJSON) else {
10481052
throw MongoDBError(code: 0, message: "Failed to create listDatabases command")
10491053
}
10501054
defer { bson_destroy(command) }

Plugins/MySQLDriverPlugin/MySQLPlugin.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ final class MySQLPlugin: NSObject, TableProPlugin, DriverPlugin {
2929

3030
static let urlSchemes: [String] = ["mysql"]
3131
static let explainVariants: [ExplainVariant] = [
32-
ExplainVariant(id: "explain", label: "EXPLAIN", sqlPrefix: "EXPLAIN FORMAT=JSON"),
32+
ExplainVariant(id: "explain", label: "EXPLAIN", sqlPrefix: "EXPLAIN"),
33+
ExplainVariant(id: "explain-json", label: "EXPLAIN (JSON)", sqlPrefix: "EXPLAIN FORMAT=JSON"),
3334
]
3435
static let brandColorHex = "#FF9500"
3536
static let postConnectActions: [PostConnectAction] = [.selectDatabaseFromLastSession]

0 commit comments

Comments
 (0)