diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index ecf3b6d2c..7d194ec37 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -251,15 +251,24 @@ final class PluginManager { Self.logger.error("Plugin '\(pluginId)' driver rejected: \(error.localizedDescription)") } if !driverPlugins.keys.contains(type(of: driver).databaseTypeId) { - let typeId = type(of: driver).databaseTypeId + let driverType = type(of: driver) + let typeId = driverType.databaseTypeId driverPlugins[typeId] = driver - for additionalId in type(of: driver).additionalDatabaseTypeIds { + for additionalId in driverType.additionalDatabaseTypeIds { driverPlugins[additionalId] = driver } - // Built-in defaults are pre-populated in PluginMetadataRegistry.init(). - // Runtime-loaded plugins may be compiled against an older TableProPluginKit, - // so we don't read new protocol properties from them to avoid witness table crashes. + // Self-register plugin metadata from the DriverPlugin protocol. + // parameterStyle defaults to .questionMark; built-in defaults already have correct values. + let snapshot = PluginMetadataRegistry.shared.buildMetadataSnapshot( + from: driverType, + isDownloadable: driverType.isDownloadable + ) + PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: typeId) + for additionalId in driverType.additionalDatabaseTypeIds { + PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: additionalId) + PluginMetadataRegistry.shared.registerTypeAlias(additionalId, primaryTypeId: typeId) + } Self.logger.debug("Registered driver plugin '\(pluginId)' for database type '\(typeId)'") registeredAny = true diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry.swift b/TablePro/Core/Plugins/PluginMetadataRegistry.swift index 89949d204..f5051078b 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry.swift @@ -125,6 +125,7 @@ final class PluginMetadataRegistry: @unchecked Sendable { private let lock = NSLock() private var snapshots: [String: PluginMetadataSnapshot] = [:] private var schemeIndex: [String: String] = [:] + private var reverseTypeIndex: [String: String] = [:] private init() { registerBuiltInDefaults() @@ -505,6 +506,11 @@ final class PluginMetadataRegistry: @unchecked Sendable { schemeIndex[scheme.lowercased()] = entry.typeId } } + + // Built-in type aliases: multi-type plugins where an alias maps to a primary plugin type ID + reverseTypeIndex["MariaDB"] = "MySQL" + reverseTypeIndex["Redshift"] = "PostgreSQL" + reverseTypeIndex["ScyllaDB"] = "Cassandra" } func register(snapshot: PluginMetadataSnapshot, forTypeId typeId: String) { @@ -543,6 +549,107 @@ final class PluginMetadataRegistry: @unchecked Sendable { return DatabaseType(rawValue: typeId) } + // MARK: - Dynamic Type Registration + + /// Registers an alias type ID that maps to a primary type ID. + /// Used for multi-type plugins (e.g., MariaDB → MySQL, Redshift → PostgreSQL). + func registerTypeAlias(_ aliasTypeId: String, primaryTypeId: String) { + lock.lock() + defer { lock.unlock() } + reverseTypeIndex[aliasTypeId] = primaryTypeId + } + + /// Returns all registered type IDs (sorted for deterministic UI ordering). + func allRegisteredTypeIds() -> [String] { + lock.lock() + defer { lock.unlock() } + return Array(snapshots.keys).sorted() + } + + /// Resolves a database type raw value to its plugin type ID for driver lookup. + /// For multi-type plugins (MySQL serves MariaDB), maps the alias to the primary. + /// Does NOT remap for snapshot lookups — use snapshot(forTypeId:) directly. + func pluginTypeId(for rawValue: String) -> String { + lock.lock() + defer { lock.unlock() } + return reverseTypeIndex[rawValue] ?? rawValue + } + + /// Checks if a type ID is registered (has a snapshot). + func hasType(_ typeId: String) -> Bool { + lock.lock() + defer { lock.unlock() } + return snapshots[typeId] != nil + } + + // MARK: - Snapshot Builder + + /// Builds a PluginMetadataSnapshot from a DriverPlugin's protocol properties. + /// Used by PluginManager to self-register plugins at load time. + func buildMetadataSnapshot( + from driverType: any DriverPlugin.Type, + isDownloadable: Bool = false, + parameterStyle: ParameterStyle = .questionMark + ) -> PluginMetadataSnapshot { + let schemes = driverType.urlSchemes + let primaryScheme = schemes.first ?? driverType.databaseTypeId.lowercased() + + return PluginMetadataSnapshot( + displayName: driverType.databaseDisplayName, + iconName: driverType.iconName, + defaultPort: driverType.defaultPort, + requiresAuthentication: driverType.requiresAuthentication, + supportsForeignKeys: driverType.supportsForeignKeys, + supportsSchemaEditing: driverType.supportsSchemaEditing, + isDownloadable: isDownloadable, + primaryUrlScheme: primaryScheme, + parameterStyle: parameterStyle, + navigationModel: driverType.navigationModel, + explainVariants: driverType.explainVariants, + pathFieldRole: driverType.pathFieldRole, + supportsHealthMonitor: driverType.supportsHealthMonitor, + urlSchemes: schemes, + postConnectActions: driverType.postConnectActions, + brandColorHex: driverType.brandColorHex, + queryLanguageName: driverType.queryLanguageName, + editorLanguage: driverType.editorLanguage, + connectionMode: driverType.connectionMode, + supportsDatabaseSwitching: driverType.supportsDatabaseSwitching, + capabilities: PluginMetadataSnapshot.CapabilityFlags( + supportsSchemaSwitching: driverType.supportsSchemaSwitching, + supportsImport: driverType.supportsImport, + supportsExport: driverType.supportsExport, + supportsSSH: driverType.supportsSSH, + supportsSSL: driverType.supportsSSL, + supportsCascadeDrop: driverType.supportsCascadeDrop, + supportsForeignKeyDisable: driverType.supportsForeignKeyDisable, + supportsReadOnlyMode: driverType.supportsReadOnlyMode, + supportsQueryProgress: driverType.supportsQueryProgress, + requiresReconnectForDatabaseSwitch: driverType.requiresReconnectForDatabaseSwitch + ), + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: driverType.defaultSchemaName, + defaultGroupName: driverType.defaultGroupName, + tableEntityName: driverType.tableEntityName, + defaultPrimaryKeyColumn: driverType.defaultPrimaryKeyColumn, + immutableColumns: driverType.immutableColumns, + systemDatabaseNames: driverType.systemDatabaseNames, + systemSchemaNames: driverType.systemSchemaNames, + fileExtensions: driverType.fileExtensions, + databaseGroupingStrategy: driverType.databaseGroupingStrategy, + structureColumnFields: driverType.structureColumnFields + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: driverType.sqlDialect, + statementCompletions: driverType.statementCompletions, + columnTypesByCategory: driverType.columnTypesByCategory + ), + connection: PluginMetadataSnapshot.ConnectionConfig( + additionalConnectionFields: driverType.additionalConnectionFields + ) + ) + } + func allFileExtensions() -> [String: String] { lock.lock() defer { lock.unlock() } diff --git a/TablePro/Core/Plugins/Registry/RegistryModels.swift b/TablePro/Core/Plugins/Registry/RegistryModels.swift index a50cd7453..fb617458d 100644 --- a/TablePro/Core/Plugins/Registry/RegistryModels.swift +++ b/TablePro/Core/Plugins/Registry/RegistryModels.swift @@ -45,6 +45,7 @@ struct RegistryPlugin: Codable, Sendable, Identifiable { let minPluginKitVersion: Int? let iconName: String? let isVerified: Bool + let metadata: RegistryPluginMetadata? } extension RegistryPlugin { @@ -83,3 +84,98 @@ enum RegistryCategory: String, Codable, Sendable, CaseIterable, Identifiable { } } } + +// MARK: - Plugin Metadata (self-describing registry plugins) + +struct RegistryPluginMetadata: Codable, Sendable { + let displayName: String? + let iconName: String? + let defaultPort: Int? + let brandColorHex: String? + let connectionMode: String? + let editorLanguage: String? + let queryLanguageName: String? + let primaryUrlScheme: String? + let parameterStyle: String? + + let requiresAuthentication: Bool? + let supportsForeignKeys: Bool? + let supportsSchemaEditing: Bool? + let supportsDatabaseSwitching: Bool? + let supportsSchemaSwitching: Bool? + let supportsSSH: Bool? + let supportsSSL: Bool? + let supportsImport: Bool? + let supportsExport: Bool? + let supportsHealthMonitor: Bool? + let supportsCascadeDrop: Bool? + let supportsForeignKeyDisable: Bool? + let supportsReadOnlyMode: Bool? + let supportsQueryProgress: Bool? + let requiresReconnectForDatabaseSwitch: Bool? + + let urlSchemes: [String]? + let fileExtensions: [String]? + let systemDatabaseNames: [String]? + let systemSchemaNames: [String]? + let defaultSchemaName: String? + let defaultGroupName: String? + let tableEntityName: String? + let defaultPrimaryKeyColumn: String? + let immutableColumns: [String]? + + let navigationModel: String? + let pathFieldRole: String? + let databaseGroupingStrategy: String? + let structureColumnFields: [String]? + let postConnectActions: [RegistryPostConnectAction]? + let additionalConnectionFields: [RegistryConnectionField]? + let explainVariants: [RegistryExplainVariant]? + let sqlDialect: RegistrySqlDialect? + let statementCompletions: [RegistryCompletionEntry]? + let columnTypesByCategory: [String: [String]]? +} + +struct RegistryConnectionField: Codable, Sendable { + let id: String + let label: String + let placeholder: String? + let defaultValue: String? + let fieldType: String? + let section: String? + let options: [RegistryDropdownOption]? +} + +struct RegistryDropdownOption: Codable, Sendable { + let value: String + let label: String +} + +struct RegistryPostConnectAction: Codable, Sendable { + let type: String + let fieldId: String? +} + +struct RegistryExplainVariant: Codable, Sendable { + let name: String + let prefix: String +} + +struct RegistrySqlDialect: Codable, Sendable { + let identifierQuote: String? + let keywords: [String]? + let functions: [String]? + let dataTypes: [String]? + let tableOptions: [String]? + let regexSyntax: String? + let booleanLiteralStyle: String? + let likeEscapeStyle: String? + let paginationStyle: String? + let offsetFetchOrderBy: String? + let requiresBackslashEscaping: Bool? +} + +struct RegistryCompletionEntry: Codable, Sendable { + let label: String + let insertText: String +} diff --git a/TablePro/Core/Services/Infrastructure/DeeplinkHandler.swift b/TablePro/Core/Services/Infrastructure/DeeplinkHandler.swift index dd0d76709..4de1bbed5 100644 --- a/TablePro/Core/Services/Infrastructure/DeeplinkHandler.swift +++ b/TablePro/Core/Services/Infrastructure/DeeplinkHandler.swift @@ -88,9 +88,9 @@ enum DeeplinkHandler { let host = value("host"), !host.isEmpty, let typeStr = value("type"), let dbType = DatabaseType(validating: typeStr) - ?? DatabaseType.allKnownTypes.first(where: { - $0.rawValue.lowercased() == typeStr.lowercased() - }) + ?? PluginMetadataRegistry.shared.allRegisteredTypeIds() + .first(where: { $0.lowercased() == typeStr.lowercased() }) + .map({ DatabaseType(rawValue: $0) }) else { logger.warning("Import deep link missing required params") return nil diff --git a/TablePro/Models/Connection/DatabaseConnection.swift b/TablePro/Models/Connection/DatabaseConnection.swift index 0402069f0..d6b61ac86 100644 --- a/TablePro/Models/Connection/DatabaseConnection.swift +++ b/TablePro/Models/Connection/DatabaseConnection.swift @@ -219,11 +219,14 @@ struct DatabaseType: Hashable, Identifiable, Sendable { } extension DatabaseType { + // Built-in types (bundled plugins) static let mysql = DatabaseType(rawValue: "MySQL") static let mariadb = DatabaseType(rawValue: "MariaDB") static let postgresql = DatabaseType(rawValue: "PostgreSQL") static let sqlite = DatabaseType(rawValue: "SQLite") static let redshift = DatabaseType(rawValue: "Redshift") + + // Registry-distributed types (known plugins, downloadable separately) static let mongodb = DatabaseType(rawValue: "MongoDB") static let redis = DatabaseType(rawValue: "Redis") static let mssql = DatabaseType(rawValue: "SQL Server") @@ -248,29 +251,27 @@ extension DatabaseType: Codable { } extension DatabaseType { - /// All built-in database types. - static let allKnownTypes: [DatabaseType] = [ - .mysql, .mariadb, .postgresql, .sqlite, .redshift, - .mongodb, .redis, .mssql, .oracle, .clickhouse, .duckdb, - .cassandra, .scylladb, .etcd, - ] + /// All registered database types, derived dynamically from the plugin metadata registry. + static var allKnownTypes: [DatabaseType] { + PluginMetadataRegistry.shared.allRegisteredTypeIds().map { DatabaseType(rawValue: $0) } + } /// Compatibility shim for CaseIterable call sites. static var allCases: [DatabaseType] { allKnownTypes } } extension DatabaseType { - /// Returns nil if rawValue doesn't match any known type. + /// Returns nil if rawValue doesn't match any registered type. init?(validating rawValue: String) { - guard Self.allKnownTypes.contains(where: { $0.rawValue == rawValue }) else { return nil } + guard PluginMetadataRegistry.shared.hasType(rawValue) else { return nil } self.rawValue = rawValue } } extension DatabaseType { - /// Plugin type ID used for PluginManager lookup. + /// Plugin type ID used for PluginManager lookup, resolved via the registry. var pluginTypeId: String { - Self.pluginTypeIdMap[self] ?? rawValue + PluginMetadataRegistry.shared.pluginTypeId(for: rawValue) } var isDownloadablePlugin: Bool { @@ -296,20 +297,6 @@ extension DatabaseType { var supportsSchemaEditing: Bool { PluginMetadataRegistry.shared.snapshot(forTypeId: pluginTypeId)?.supportsSchemaEditing ?? true } - - private static let pluginTypeIdMap: [DatabaseType: String] = [ - .mysql: "MySQL", .mariadb: "MySQL", - .postgresql: "PostgreSQL", .redshift: "PostgreSQL", - .mssql: "SQL Server", - .sqlite: "SQLite", - .mongodb: "MongoDB", - .redis: "Redis", - .oracle: "Oracle", - .clickhouse: "ClickHouse", - .duckdb: "DuckDB", - .cassandra: "Cassandra", .scylladb: "Cassandra", - .etcd: "etcd", - ] } // MARK: - Connection Color diff --git a/TablePro/Views/Editor/HistoryPanelView.swift b/TablePro/Views/Editor/HistoryPanelView.swift index 062b9d0b5..54f8c1f98 100644 --- a/TablePro/Views/Editor/HistoryPanelView.swift +++ b/TablePro/Views/Editor/HistoryPanelView.swift @@ -223,7 +223,7 @@ private extension HistoryPanelView { HighlightedSQLTextView( sql: entry.query.hasSuffix(";") ? entry.query : entry.query + ";", databaseType: entry.query.trimmingCharacters(in: .whitespaces) - .hasPrefix("db.") ? .mongodb : .mysql // Redis commands use SQL patterns for highlighting + .hasPrefix("db.") ? .mongodb : .mysql ) .background(Color(nsColor: ThemeEngine.shared.colors.editor.background)) diff --git a/TableProTests/Helpers/TestFixtures.swift b/TableProTests/Helpers/TestFixtures.swift index 63e7afe4a..d7708f1fe 100644 --- a/TableProTests/Helpers/TestFixtures.swift +++ b/TableProTests/Helpers/TestFixtures.swift @@ -12,7 +12,10 @@ import Testing enum TestFixtures { // MARK: - Database Types - static let allDatabaseTypes: [DatabaseType] = [.mysql, .mariadb, .postgresql, .sqlite, .redshift, .mongodb, .redis, .clickhouse] + static let allDatabaseTypes: [DatabaseType] = [ + .mysql, .mariadb, .postgresql, .sqlite, .redshift, + .mongodb, .redis, .clickhouse + ] // MARK: - ClickHouse Connection Fixture diff --git a/TableProTests/Models/DatabaseTypeCassandraTests.swift b/TableProTests/Models/DatabaseTypeCassandraTests.swift index 3bba70abf..e9abb07e7 100644 --- a/TableProTests/Models/DatabaseTypeCassandraTests.swift +++ b/TableProTests/Models/DatabaseTypeCassandraTests.swift @@ -5,82 +5,82 @@ import Testing struct DatabaseTypeCassandraTests { @Test("Cassandra raw value is Cassandra") func cassandraRawValue() { - #expect(DatabaseType.cassandra.rawValue == "Cassandra") + #expect(.cassandra.rawValue == "Cassandra") } @Test("ScyllaDB raw value is ScyllaDB") func scylladbRawValue() { - #expect(DatabaseType.scylladb.rawValue == "ScyllaDB") + #expect(.scylladb.rawValue == "ScyllaDB") } @Test("Cassandra pluginTypeId is Cassandra") func cassandraPluginTypeId() { - #expect(DatabaseType.cassandra.pluginTypeId == "Cassandra") + #expect(.cassandra.pluginTypeId == "Cassandra") } @Test("ScyllaDB pluginTypeId is Cassandra") func scylladbPluginTypeId() { - #expect(DatabaseType.scylladb.pluginTypeId == "Cassandra") + #expect(.scylladb.pluginTypeId == "Cassandra") } @Test("Cassandra default port is 9042") func cassandraDefaultPort() { - #expect(DatabaseType.cassandra.defaultPort == 9_042) + #expect(.cassandra.defaultPort == 9_042) } @Test("ScyllaDB default port is 9042") func scylladbDefaultPort() { - #expect(DatabaseType.scylladb.defaultPort == 9_042) + #expect(.scylladb.defaultPort == 9_042) } @Test("Cassandra does not require authentication") func cassandraRequiresAuthentication() { - #expect(DatabaseType.cassandra.requiresAuthentication == false) + #expect(.cassandra.requiresAuthentication == false) } @Test("ScyllaDB does not require authentication") func scylladbRequiresAuthentication() { - #expect(DatabaseType.scylladb.requiresAuthentication == false) + #expect(.scylladb.requiresAuthentication == false) } @Test("Cassandra does not support foreign keys") func cassandraSupportsForeignKeys() { - #expect(DatabaseType.cassandra.supportsForeignKeys == false) + #expect(.cassandra.supportsForeignKeys == false) } @Test("ScyllaDB does not support foreign keys") func scylladbSupportsForeignKeys() { - #expect(DatabaseType.scylladb.supportsForeignKeys == false) + #expect(.scylladb.supportsForeignKeys == false) } @Test("Cassandra supports schema editing") func cassandraSupportsSchemaEditing() { - #expect(DatabaseType.cassandra.supportsSchemaEditing == true) + #expect(.cassandra.supportsSchemaEditing == true) } @Test("ScyllaDB supports schema editing") func scylladbSupportsSchemaEditing() { - #expect(DatabaseType.scylladb.supportsSchemaEditing == true) + #expect(.scylladb.supportsSchemaEditing == true) } @Test("Cassandra icon name is cassandra-icon") func cassandraIconName() { - #expect(DatabaseType.cassandra.iconName == "cassandra-icon") + #expect(.cassandra.iconName == "cassandra-icon") } @Test("ScyllaDB icon name is scylladb-icon") func scylladbIconName() { - #expect(DatabaseType.scylladb.iconName == "scylladb-icon") + #expect(.scylladb.iconName == "scylladb-icon") } @Test("Cassandra is a downloadable plugin") func cassandraIsDownloadablePlugin() { - #expect(DatabaseType.cassandra.isDownloadablePlugin == true) + #expect(.cassandra.isDownloadablePlugin == true) } @Test("ScyllaDB is a downloadable plugin") func scylladbIsDownloadablePlugin() { - #expect(DatabaseType.scylladb.isDownloadablePlugin == true) + #expect(.scylladb.isDownloadablePlugin == true) } @Test("Cassandra included in allCases") diff --git a/TableProTests/Models/DatabaseTypeMSSQLTests.swift b/TableProTests/Models/DatabaseTypeMSSQLTests.swift index acd5abbd8..7692f2048 100644 --- a/TableProTests/Models/DatabaseTypeMSSQLTests.swift +++ b/TableProTests/Models/DatabaseTypeMSSQLTests.swift @@ -2,7 +2,7 @@ // DatabaseTypeMSSQLTests.swift // TableProTests // -// Tests for DatabaseType.mssql properties and methods. +// Tests for .mssql properties and methods. // import Foundation @@ -15,32 +15,32 @@ struct DatabaseTypeMSSQLTests { @Test("defaultPort is 1433") func defaultPort() { - #expect(DatabaseType.mssql.defaultPort == 1_433) + #expect(.mssql.defaultPort == 1_433) } @Test("rawValue is SQL Server") func rawValue() { - #expect(DatabaseType.mssql.rawValue == "SQL Server") + #expect(.mssql.rawValue == "SQL Server") } @Test("requiresAuthentication is true") func requiresAuthentication() { - #expect(DatabaseType.mssql.requiresAuthentication == true) + #expect(.mssql.requiresAuthentication == true) } @Test("supportsForeignKeys is true") func supportsForeignKeys() { - #expect(DatabaseType.mssql.supportsForeignKeys == true) + #expect(.mssql.supportsForeignKeys == true) } @Test("supportsSchemaEditing is true") func supportsSchemaEditing() { - #expect(DatabaseType.mssql.supportsSchemaEditing == true) + #expect(.mssql.supportsSchemaEditing == true) } @Test("iconName is mssql-icon") func iconName() { - #expect(DatabaseType.mssql.iconName == "mssql-icon") + #expect(.mssql.iconName == "mssql-icon") } // MARK: - allKnownTypes Tests diff --git a/TableProTests/Models/DatabaseTypeRedisTests.swift b/TableProTests/Models/DatabaseTypeRedisTests.swift index 5ace3b5b6..d6e834883 100644 --- a/TableProTests/Models/DatabaseTypeRedisTests.swift +++ b/TableProTests/Models/DatabaseTypeRedisTests.swift @@ -5,37 +5,37 @@ import Testing struct DatabaseTypeRedisTests { @Test("Default port is 6379") func defaultPort() { - #expect(DatabaseType.redis.defaultPort == 6_379) + #expect(.redis.defaultPort == 6_379) } @Test("Icon name is redis-icon") func iconName() { - #expect(DatabaseType.redis.iconName == "redis-icon") + #expect(.redis.iconName == "redis-icon") } @Test("Does not require authentication") func requiresAuthentication() { - #expect(DatabaseType.redis.requiresAuthentication == false) + #expect(.redis.requiresAuthentication == false) } @Test("Does not support foreign keys") func supportsForeignKeys() { - #expect(DatabaseType.redis.supportsForeignKeys == false) + #expect(.redis.supportsForeignKeys == false) } @Test("Does not support schema editing") func supportsSchemaEditing() { - #expect(DatabaseType.redis.supportsSchemaEditing == false) + #expect(.redis.supportsSchemaEditing == false) } @Test("Raw value is Redis") func rawValue() { - #expect(DatabaseType.redis.rawValue == "Redis") + #expect(.redis.rawValue == "Redis") } @Test("Theme color is derived from plugin brand color") @MainActor func themeColor() { - #expect(DatabaseType.redis.themeColor == PluginManager.shared.brandColor(for: .redis)) + #expect(.redis.themeColor == PluginManager.shared.brandColor(for: .redis)) } @Test("Included in allKnownTypes") diff --git a/TableProTests/Models/DatabaseTypeTests.swift b/TableProTests/Models/DatabaseTypeTests.swift index 57054253e..75a3625aa 100644 --- a/TableProTests/Models/DatabaseTypeTests.swift +++ b/TableProTests/Models/DatabaseTypeTests.swift @@ -37,9 +37,13 @@ struct DatabaseTypeTests { #expect(DatabaseType.mongodb.defaultPort == 27_017) } - @Test("allKnownTypes count is 13") - func testAllKnownTypesCount() { - #expect(DatabaseType.allKnownTypes.count == 13) + @Test("allKnownTypes contains all built-in types") + func testAllKnownTypesContainsBuiltIns() { + let knownTypes = DatabaseType.allKnownTypes + #expect(knownTypes.contains(.mysql)) + #expect(knownTypes.contains(.postgresql)) + #expect(knownTypes.contains(.sqlite)) + #expect(knownTypes.count >= 5) } @Test("allCases shim matches allKnownTypes")