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
3 changes: 3 additions & 0 deletions .github/plugin-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"SpannerDriverPlugin": {
"name": "Spanner Driver"
},
"StarRocksDriverPlugin": {
"name": "StarRocks Driver"
},
"SurrealDBDriverPlugin": {
"name": "SurrealDB Driver"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable {
public static let tidb = DatabaseType(rawValue: "TiDB")
public static let databend = DatabaseType(rawValue: "Databend")
public static let oceanbase = DatabaseType(rawValue: "OceanBase")
public static let starrocks = DatabaseType(rawValue: "StarRocks")
public static let postgresql = DatabaseType(rawValue: "PostgreSQL")
public static let sqlite = DatabaseType(rawValue: "SQLite")
public static let redis = DatabaseType(rawValue: "Redis")
Expand Down Expand Up @@ -47,7 +48,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable {
public static let bigtable = DatabaseType(rawValue: "Bigtable")

public static let allKnownTypes: [DatabaseType] = [
.mysql, .mariadb, .tidb, .databend, .oceanbase, .postgresql, .sqlite, .redis, .mongodb,
.mysql, .mariadb, .tidb, .databend, .oceanbase, .starrocks, .postgresql, .sqlite, .redis, .mongodb,
.clickhouse, .mssql, .oracle, .dameng, .duckdb, .cassandra, .redshift,
.etcd, .cloudflareD1, .dynamodb, .bigquery, .spanner, .snowflake, .libsql, .beancount,
.surrealdb, .teradata, .trino, .kafka, .cloudflareR2SQL, .weaviate, .firestore, .firebaseRealtimeDatabase, .bigtable
Expand All @@ -61,6 +62,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable {
case .tidb: return "tidb-icon"
case .databend: return "databend-icon"
case .oceanbase: return "oceanbase-icon"
case .starrocks: return "starrocks-icon"
case .postgresql: return "postgresql-icon"
case .redshift: return "redshift-icon"
case .sqlite: return "sqlite-icon"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ public struct SQLLexicalProfile: Sendable, Hashable {
"MariaDB": mySQLFamily,
"TiDB": mySQLCompatible,
"OceanBase": mySQLCompatible,
"StarRocks": mySQLCompatible,
"Databend": SQLLexicalProfile(
grammar: databend,
undetermined: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public struct SQLEngineVocabulary: Sendable, Hashable {
"TiDB": mySQL,
"Databend": mySQL,
"OceanBase": mySQL,
"StarRocks": mySQL,
"PostgreSQL": postgreSQL,
"CockroachDB": postgreSQL,
"PGlite": postgreSQL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct DatabaseTypeTests {
#expect(DatabaseType.tidb.rawValue == "TiDB")
#expect(DatabaseType.databend.rawValue == "Databend")
#expect(DatabaseType.oceanbase.rawValue == "OceanBase")
#expect(DatabaseType.starrocks.rawValue == "StarRocks")
#expect(DatabaseType.postgresql.rawValue == "PostgreSQL")
#expect(DatabaseType.sqlite.rawValue == "SQLite")
#expect(DatabaseType.redis.rawValue == "Redis")
Expand All @@ -31,6 +32,7 @@ struct DatabaseTypeTests {
#expect(DatabaseType.tidb.pluginTypeId == "MySQL")
#expect(DatabaseType.databend.pluginTypeId == "MySQL")
#expect(DatabaseType.oceanbase.pluginTypeId == "MySQL")
#expect(DatabaseType.starrocks.pluginTypeId == "StarRocks")
#expect(DatabaseType.postgresql.pluginTypeId == "PostgreSQL")
#expect(DatabaseType.redshift.pluginTypeId == "PostgreSQL")
#expect(DatabaseType.sqlite.pluginTypeId == "SQLite")
Expand Down Expand Up @@ -61,11 +63,12 @@ struct DatabaseTypeTests {

@Test("allKnownTypes contains all expected types")
func allKnownTypesComplete() {
#expect(DatabaseType.allKnownTypes.count == 33)
#expect(DatabaseType.allKnownTypes.count == 34)
#expect(DatabaseType.allKnownTypes.contains(.mysql))
#expect(DatabaseType.allKnownTypes.contains(.tidb))
#expect(DatabaseType.allKnownTypes.contains(.databend))
#expect(DatabaseType.allKnownTypes.contains(.oceanbase))
#expect(DatabaseType.allKnownTypes.contains(.starrocks))
#expect(DatabaseType.allKnownTypes.contains(.bigquery))
#expect(DatabaseType.allKnownTypes.contains(.spanner))
#expect(DatabaseType.allKnownTypes.contains(.snowflake))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ struct SQLLexicalReadingsTests {
@Test("Every SQL engine TablePro ships has a curated grammar")
func curatedTableCoversShippedEngines() {
let shipped = [
"MySQL", "MariaDB", "TiDB", "Databend", "OceanBase", "PostgreSQL", "Redshift", "CockroachDB", "PGlite", "KingbaseES",
"MySQL", "MariaDB", "TiDB", "Databend", "OceanBase", "StarRocks", "PostgreSQL", "Redshift", "CockroachDB", "PGlite", "KingbaseES",
"SQLite", "libSQL", "Turso", "Cloudflare D1", "DuckDB", "Oracle", "Dameng", "SQL Server", "ClickHouse",
"Snowflake", "BigQuery", "Spanner", "Trino", "Presto", "Teradata", "SAP HANA", "Cassandra", "ScyllaDB", "DynamoDB",
"SurrealDB", "Redis", "MongoDB", "etcd", "Elasticsearch", "Typesense", "Weaviate", "Kafka",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct SQLEngineVocabularyTests {
func everyEngineHasVocabulary() {
let typeIds = SQLEngineVocabulary.curatedDatabaseTypeIds
#expect(typeIds == [
"MySQL", "MariaDB", "TiDB", "Databend", "OceanBase",
"MySQL", "MariaDB", "TiDB", "Databend", "OceanBase", "StarRocks",
"PostgreSQL", "CockroachDB", "PGlite", "KingbaseES", "Redshift",
"SQLite", "SQL Server", "Oracle", "DuckDB",
])
Expand All @@ -30,6 +30,7 @@ struct SQLEngineVocabularyTests {
("TiDB", "MySQL"),
("Databend", "MySQL"),
("OceanBase", "MySQL"),
("StarRocks", "MySQL"),
("CockroachDB", "PostgreSQL"),
("PGlite", "PostgreSQL"),
("KingbaseES", "PostgreSQL"),
Expand Down
5 changes: 3 additions & 2 deletions apple/Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -932,8 +932,9 @@ final class MariaDBPluginConnection: @unchecked Sendable {
}

private func binaryStringsAreText(in query: String, flavor: MySQLServerFlavor) -> Bool {
MySQLServerVersion.labelsShowTextAsBinary(banner: serverVersion(), flavor: flavor)
&& mysqlStatementListsServerMetadata(query)
flavor.labelsTextResultsAsBinary
|| (MySQLServerVersion.labelsShowTextAsBinary(banner: serverVersion(), flavor: flavor)
&& mysqlStatementListsServerMetadata(query))
}

private func runTextStatement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal extension MySQLServerVersion {
return isKnownBelow((5, 7, 6), banner: banner) ? .grantUsage : .alterUser
case .mariadb:
return isKnownBelow((10, 2, 0), banner: banner) ? .grantUsage : .alterUser
case .tidb, .oceanbase, .databend:
case .tidb, .oceanbase, .databend, .starrocks:
return .alterUser
}
}
Expand Down
6 changes: 4 additions & 2 deletions apple/Plugins/MySQLDriverPlugin/MySQLCheckConstraints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ internal enum MySQLCheckConstraints {
return .informationSchema
case .databend:
return .databendCatalog
case .starrocks:
return .unavailable
}
}

Expand Down Expand Up @@ -98,7 +100,7 @@ internal enum MySQLCheckConstraints {
return MySQLServerVersion.components(from: banner) != nil
case .tidb(let version), .oceanbase(let version):
return version != nil
case .databend:
case .databend, .starrocks:
return true
}
}
Expand All @@ -109,7 +111,7 @@ internal enum MySQLCheckConstraints {
case .mariadb: return "MariaDB 10.2.1"
case .tidb: return "TiDB 7.2"
case .oceanbase: return "OceanBase 4.0"
case .databend: return nil
case .databend, .starrocks: return nil
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal enum MySQLFunctionalKeyParts {
case .mysql:
guard MySQLServerVersion.isKnownBelow(firstRelease, banner: banner) else { return nil }
return String(localized: "Indexing an expression needs MySQL 8.0.13 or later.")
case .tidb, .oceanbase, .databend:
case .tidb, .oceanbase, .databend, .starrocks:
return nil
}
}
Expand Down
6 changes: 3 additions & 3 deletions apple/Plugins/MySQLDriverPlugin/MySQLKillLatch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ nonisolated internal struct MySQLKillLatch {
}

/// Only the two engines measured to hold an idle kill pay the absorbing round trip. TiDB drops
/// the session on `KILL QUERY` instead of flagging it, and Databend and OceanBase are unmeasured,
/// so neither gets a statement inserted into its session on a guess.
/// the session on `KILL QUERY` instead of flagging it, and Databend, OceanBase and StarRocks are
/// unmeasured, so none gets a statement inserted into its session on a guess.
internal static func absorbsLatchedKill(flavor: MySQLServerFlavor) -> Bool {
switch flavor {
case .mysql, .mariadb:
return true
case .tidb, .databend, .oceanbase:
case .tidb, .databend, .oceanbase, .starrocks:
return false
}
}
Expand Down
2 changes: 1 addition & 1 deletion apple/Plugins/MySQLDriverPlugin/MySQLKillTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal extension MySQLServerFlavor {
case .databend:
guard let session = connectionIdentifier, !session.isEmpty else { return .threadId }
return .databendSession(session)
case .mysql, .mariadb, .oceanbase:
case .mysql, .mariadb, .oceanbase, .starrocks:
return .threadId
}
}
Expand Down
2 changes: 2 additions & 0 deletions apple/Plugins/MySQLDriverPlugin/MySQLMaintenance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ internal extension MySQLServerFlavor {
case .oceanbase(let version):
guard let version, version >= Self.oceanbaseBareAnalyzeFloor else { return [] }
return [MySQLMaintenance.analyzeOperation]
case .starrocks:
return []
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension MySQLPluginDriver {
/// `SHOW INDEX` returns text, so both are cast rather than read through a text accessor that
/// would depend on how the driver rendered an integer cell.
func fetchAllIndexes(schema: String?) async throws -> [String: [PluginIndexInfo]] {
guard !flavor.isDatabend else { return [:] }
guard flavor.hasMySQLObjectCatalogs else { return [:] }
let database = routineSchema(schema)
return try await catalogOrShow(
database: database,
Expand Down Expand Up @@ -80,6 +80,7 @@ extension MySQLPluginDriver {
/// so a caller asking about another one is answered about the one it asked about.
func fetchAllTableMetadata(schema: String?) async throws -> [String: PluginTableMetadata] {
guard !flavor.isDatabend else { return try await databendAllTableMetadata(database: routineSchema(schema)) }
guard !flavor.isStarRocks else { return try await starrocksTableStatus(database: routineSchema(schema)) }
let result = try await execute(query: Self.tableStatusStatement(database: routineSchema(schema)))
let appendsInnoDBStatus = serverAppendsInnoDBStatus
var metadata: [String: PluginTableMetadata] = [:]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import TableProPluginKit
extension MySQLPluginDriver {
func createDatabaseFormSpec() async throws -> PluginCreateDatabaseFormSpec? {
guard !flavor.isDatabend else { return PluginCreateDatabaseFormSpec(fields: [], footnote: nil) }
// The StarRocks database level is its catalogs, which a charset form cannot describe.
guard !flavor.isStarRocks else { return nil }
let charsetDefaults = try await fetchCharsetDefaults()
let collations = try await fetchCollationCatalog()
let serverDefaults = await fetchServerCharsetDefaults()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import TableProPluginKit

internal extension MySQLPluginDriver {
func fetchRoutineCreationScope(_ routine: PluginRoutineInfo) async throws -> PluginStatementScope? {
guard !flavor.isDatabend, serverHasInformationSchema else { return nil }
guard flavor.hasMySQLObjectCatalogs, serverHasInformationSchema else { return nil }
let exactSchema = await schemaNamesAreCaseSensitive()
let query = MySQLObjectQueries.routineCreationSettings(
schema: routineSchema(routine.schema),
Expand All @@ -22,7 +22,7 @@ internal extension MySQLPluginDriver {
}

func fetchTriggerCreationScope(_ trigger: PluginTriggerInfo) async throws -> PluginStatementScope? {
guard !flavor.isDatabend, holdsForServer(MySQLServerVersion.hasTriggerCatalog(banner:flavor:)) else {
guard flavor.hasMySQLObjectCatalogs, holdsForServer(MySQLServerVersion.hasTriggerCatalog(banner:flavor:)) else {
return nil
}
let query = MySQLObjectQueries.triggerCreationSettings(schema: routineSchema(trigger.schema), name: trigger.name)
Expand All @@ -31,7 +31,7 @@ internal extension MySQLPluginDriver {
}

func fetchEventCreationScope(_ event: PluginEventInfo) async throws -> PluginStatementScope? {
guard !flavor.isDatabend, holdsForServer(MySQLServerVersion.hasPartitionAndEventCatalogs(banner:flavor:)) else {
guard flavor.hasMySQLObjectCatalogs, holdsForServer(MySQLServerVersion.hasPartitionAndEventCatalogs(banner:flavor:)) else {
return nil
}
let exactSchema = await schemaNamesAreCaseSensitive()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import TableProPluginKit
internal extension MySQLPluginDriver {
func fetchDatabaseMetadata(_ database: String) async throws -> PluginDatabaseMetadata {
let isSystem = systemDatabaseNamesForConnectionType.contains(database)
// A StarRocks catalog is sized only by scanning the lake behind it.
guard !flavor.isStarRocks else { return PluginDatabaseMetadata(name: database, isSystemDatabase: isSystem) }
guard serverHasInformationSchema else {
return try await tableStatusMetadata(of: database, isSystem: isSystem)
}
Expand All @@ -38,6 +40,11 @@ internal extension MySQLPluginDriver {

func fetchAllDatabaseMetadata() async throws -> [PluginDatabaseMetadata] {
let systemDatabases = systemDatabaseNamesForConnectionType
guard !flavor.isStarRocks else {
return try await fetchDatabases().map {
PluginDatabaseMetadata(name: $0, isSystemDatabase: systemDatabases.contains($0))
}
}
guard serverHasInformationSchema else {
var metadata: [PluginDatabaseMetadata] = []
for database in try await fetchDatabases() {
Expand Down Expand Up @@ -79,6 +86,7 @@ internal extension MySQLPluginDriver {
/// 10.10+ reports `utf8mb4_uca1400_ai_ci`, which no `COLLATION_NAME` matches.
func allTablesMetadataSQL(schema: String?) -> String? {
guard !flavor.isDatabend else { return DatabendCatalog.allTablesMetadataSQL }
guard !flavor.isStarRocks else { return nil }
guard serverHasInformationSchema else { return Self.tableStatusStatement(database: effectiveSchema(schema)) }
return literalSpelling.respelled("""
SELECT
Expand Down
16 changes: 16 additions & 0 deletions apple/Plugins/MySQLDriverPlugin/MySQLPluginDriver+Flavor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal struct MySQLFlavorMismatchError: Error, Equatable {
case databendNeedsItsOwnType
case notDatabend
case notOceanBase
case notStarRocks
}

let kind: Kind
Expand All @@ -27,6 +28,8 @@ extension MySQLFlavorMismatchError: PluginDriverError {
return String(localized: "This server did not identify as Databend. Check the host and port of its MySQL handler.")
case .notOceanBase:
return String(localized: "This server did not identify as OceanBase. Check the host and port of its MySQL mode tenant.")
case .notStarRocks:
return String(localized: "This server did not identify as StarRocks. Check the host and the MySQL port of its frontend.")
}
}
}
Expand All @@ -38,6 +41,8 @@ extension MySQLPluginDriver {
return .databend
case MySQLServerFlavor.oceanbaseVariant:
return .oceanbase(version: nil)
case MySQLServerFlavor.starrocksVariant:
return .starrocks
default:
return .mysql
}
Expand Down Expand Up @@ -68,6 +73,17 @@ extension MySQLPluginDriver {
throw MySQLFlavorMismatchError(kind: .databendNeedsItsOwnType)
}

if variant == MySQLServerFlavor.starrocksVariant {
guard try await probeSucceeds(
MySQLFlavorResolution.starrocksProbe,
on: connection,
deadline: deadline
) else {
throw MySQLFlavorMismatchError(kind: .notStarRocks)
}
return .starrocks
}

if variant == MySQLServerFlavor.oceanbaseVariant {
let identity = try await connection.executeConnectQuery(
MySQLFlavorResolution.oceanbaseProbe,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension MySQLPluginDriver {
var tableDDLIncludesForeignKeys: Bool { true }

func fetchForeignKeys(table: String, schema: String?) async throws -> [PluginForeignKeyInfo] {
guard !flavor.isDatabend else { return [] }
guard flavor.hasMySQLObjectCatalogs else { return [] }
let database = effectiveSchema(schema)
let identity = serverIdentity
let omittedAction = MySQLServerVersion.omittedForeignKeyAction(
Expand All @@ -41,7 +41,7 @@ extension MySQLPluginDriver {
}

func fetchAllForeignKeys(schema: String?) async throws -> [String: [PluginForeignKeyInfo]] {
guard !flavor.isDatabend else { return [:] }
guard flavor.hasMySQLObjectCatalogs else { return [:] }
let database = effectiveSchema(schema)
return try await catalogOrShow(
database: database,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import TableProPluginKit
extension MySQLPluginDriver {
/// The routines and their parameters are two reads; `MySQLRoutineSignatures` says why.
func fetchRoutines(schema: String?) async throws -> [PluginRoutineInfo] {
guard !flavor.isDatabend, serverHasInformationSchema else { return [] }
guard flavor.hasMySQLObjectCatalogs, serverHasInformationSchema else { return [] }
let resolvedSchema = routineSchema(schema)
let exactSchema = await schemaNamesAreCaseSensitive()
let query = MySQLObjectQueries.routineList(schema: resolvedSchema, exactSchema: exactSchema)
Expand Down Expand Up @@ -96,7 +96,7 @@ extension MySQLPluginDriver {
var providesBulkTriggerFetch: Bool { true }

func fetchAllTriggers(schema: String?) async throws -> [PluginTriggerInfo] {
guard !flavor.isDatabend else { return [] }
guard flavor.hasMySQLObjectCatalogs else { return [] }
return try await triggerList(schema: routineSchema(schema), table: nil)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ internal extension MySQLPluginDriver {
let columnsByTable = try await informationSchemaColumns(schema: schema, table: table)
return columnsByTable[table] ?? (columnsByTable.count == 1 ? columnsByTable.values.first ?? [] : [])
}
guard !flavor.isStarRocks else {
return try await starrocksColumns(database: effectiveSchema(schema), table: table)[table] ?? []
}
let result = try await execute(query: "SHOW FULL COLUMNS FROM \(qualifiedName(table, schema: schema))")
let catalogDetails = try await fetchCatalogColumnDetails(table: table, schema: schema)
let createTableDefaults = try await createTableDefaults(
Expand Down Expand Up @@ -323,7 +326,8 @@ internal extension MySQLPluginDriver {
}
}

var providesBulkColumnFetch: Bool { true }
// StarRocks answers the bulk read by loading every table's schema from the lake behind an external catalog.
var providesBulkColumnFetch: Bool { !flavor.isStarRocks }

/// `GENERATION_EXPRESSION` is projected here rather than looked up per table, because a caller
/// that takes the bulk list has to receive what `fetchColumns` would have given it. Without the
Expand All @@ -332,6 +336,7 @@ internal extension MySQLPluginDriver {
func fetchAllColumns(schema: String?) async throws -> [String: [PluginColumnInfo]] {
guard !flavor.isDatabend else { return try await databendAllColumns(schema: schema) }
let database = effectiveSchema(schema)
guard !flavor.isStarRocks else { return try await starrocksColumns(database: database, table: nil) }
return try await catalogOrShow(
database: database,
catalogExists: serverHasInformationSchema,
Expand Down
Loading