From 88ba04280a19da34f8b700c5f68f884cc8c58426 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 16 Mar 2026 21:18:58 +0700 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20self-describing=20plugin=20syst?= =?UTF-8?q?em=20=E2=80=94=20zero=20core=20changes=20for=20new=20drivers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TablePro/AppDelegate+ConnectionHandler.swift | 4 +- TablePro/AppDelegate+FileOpen.swift | 2 +- TablePro/Core/Database/DatabaseDriver.swift | 8 +- TablePro/Core/Database/DatabaseManager.swift | 2 +- TablePro/Core/Plugins/PluginManager.swift | 22 +- .../Core/Plugins/PluginMetadataRegistry.swift | 480 ++++-------------- .../Plugins/Registry/RegistryModels.swift | 96 ++++ .../Infrastructure/DeeplinkHandler.swift | 6 +- .../Connection/ConnectionURLFormatter.swift | 6 +- .../Connection/ConnectionURLParser.swift | 20 +- .../Connection/DatabaseConnection.swift | 42 +- .../ViewModels/QuickSwitcherViewModel.swift | 2 +- TablePro/Views/Editor/HistoryPanelView.swift | 2 +- .../Views/Results/TableRowViewWithMenu.swift | 2 +- .../Views/Structure/TableStructureView.swift | 4 +- .../DataChangeManagerClickHouseTests.swift | 6 +- .../SQLStatementGeneratorMSSQLTests.swift | 4 +- .../SQLStatementGeneratorNoPKTests.swift | 4 +- ...LStatementGeneratorPKRegressionTests.swift | 6 +- ...tatementGeneratorParameterStyleTests.swift | 4 +- .../ClickHouse/ClickHouseDialectTests.swift | 2 +- .../FilterSQLGeneratorMSSQLTests.swift | 2 +- .../Core/Database/MSSQLDriverTests.swift | 2 +- .../Core/Services/SafeModeGuardTests.swift | 8 +- .../TableQueryBuilderFilterTests.swift | 2 +- .../TableQueryBuilderMSSQLTests.swift | 4 +- ...nnectionStorageAdditionalFieldsTests.swift | 16 +- .../ConnectionURLFormatterTests.swift | 2 +- .../ConnectionURLParserMSSQLTests.swift | 12 +- .../Utilities/ConnectionURLParserTests.swift | 18 +- .../Utilities/DatabaseURLSchemeTests.swift | 16 +- .../SQLRowToStatementConverterTests.swift | 6 +- TableProTests/Helpers/TestFixtures.swift | 7 +- ...abaseConnectionAdditionalFieldsTests.swift | 56 +- .../Models/DatabaseTypeCassandraTests.swift | 36 +- .../Models/DatabaseTypeMSSQLTests.swift | 18 +- .../Models/DatabaseTypeRedisTests.swift | 18 +- TableProTests/Models/DatabaseTypeTests.swift | 30 +- .../TableOperationDialogLogicTests.swift | 2 +- .../AIChatViewModelActionTests.swift | 4 +- .../Main/CoordinatorSidebarActionsTests.swift | 6 +- 41 files changed, 409 insertions(+), 580 deletions(-) diff --git a/TablePro/AppDelegate+ConnectionHandler.swift b/TablePro/AppDelegate+ConnectionHandler.swift index 0fe711d1e..03d44cd49 100644 --- a/TablePro/AppDelegate+ConnectionHandler.swift +++ b/TablePro/AppDelegate+ConnectionHandler.swift @@ -141,7 +141,7 @@ extension AppDelegate { let connectionName = url.deletingPathExtension().lastPathComponent for (sessionId, session) in DatabaseManager.shared.activeSessions { - if session.connection.type == .duckdb + if session.connection.type == DatabaseType(rawValue: "DuckDB") && session.connection.database == filePath && session.driver != nil { bringConnectionWindowToFront(sessionId) @@ -155,7 +155,7 @@ extension AppDelegate { port: 0, database: filePath, username: "", - type: .duckdb + type: DatabaseType(rawValue: "DuckDB") ) openNewConnectionWindow(for: connection) diff --git a/TablePro/AppDelegate+FileOpen.swift b/TablePro/AppDelegate+FileOpen.swift index 25c66fdbc..c2e8253b7 100644 --- a/TablePro/AppDelegate+FileOpen.swift +++ b/TablePro/AppDelegate+FileOpen.swift @@ -66,7 +66,7 @@ extension AppDelegate { switch dbType { case .sqlite: self.handleSQLiteFile(url) - case .duckdb: + case DatabaseType(rawValue: "DuckDB"): self.handleDuckDBFile(url) default: self.handleGenericDatabaseFile(url, type: dbType) diff --git a/TablePro/Core/Database/DatabaseDriver.swift b/TablePro/Core/Database/DatabaseDriver.swift index b06e781e1..69dda2b1a 100644 --- a/TablePro/Core/Database/DatabaseDriver.swift +++ b/TablePro/Core/Database/DatabaseDriver.swift @@ -362,15 +362,15 @@ enum DatabaseDriverFactory { } switch connection.type { - case .mongodb: + case DatabaseType(rawValue: "MongoDB"): fields["sslCACertPath"] = ssl.caCertificatePath fields["mongoReadPreference"] = connection.mongoReadPreference ?? "" fields["mongoWriteConcern"] = connection.mongoWriteConcern ?? "" - case .redis: + case DatabaseType(rawValue: "Redis"): fields["redisDatabase"] = String(connection.redisDatabase ?? 0) - case .mssql: + case DatabaseType(rawValue: "SQL Server"): fields["mssqlSchema"] = connection.mssqlSchema ?? "dbo" - case .oracle: + case DatabaseType(rawValue: "Oracle"): fields["oracleServiceName"] = connection.oracleServiceName ?? "" default: break diff --git a/TablePro/Core/Database/DatabaseManager.swift b/TablePro/Core/Database/DatabaseManager.swift index 530c3c794..ca402a81c 100644 --- a/TablePro/Core/Database/DatabaseManager.swift +++ b/TablePro/Core/Database/DatabaseManager.swift @@ -803,7 +803,7 @@ final class DatabaseManager { driver: DatabaseDriver ) async -> String? { // Only needed for PostgreSQL PK modifications - guard databaseType == .postgresql || databaseType == .redshift || databaseType == .duckdb else { return nil } + guard databaseType == .postgresql || databaseType == .redshift || databaseType == DatabaseType(rawValue: "DuckDB") else { return nil } guard changes.contains(where: { if case .modifyPrimaryKey = $0 { return true } diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index ecf3b6d2c..35c4340fd 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -251,15 +251,27 @@ 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 + let driverInstance = driver.createDriver(config: DriverConnectionConfig( + host: "", port: 0, username: "", password: "", database: "" + )) + let snapshot = PluginMetadataRegistry.shared.buildMetadataSnapshot( + from: driverType, + isDownloadable: driverType.isDownloadable, + parameterStyle: driverInstance.parameterStyle + ) + 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..85eea8346 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry.swift @@ -125,386 +125,27 @@ 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() } - // swiftlint:disable function_body_length private func registerBuiltInDefaults() { - let mysqlDialect = SQLDialectDescriptor( - identifierQuote: "`", - keywords: [ - "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", - "ON", "USING", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN", "AS", "ALIAS", - "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", - "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", - "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "DATABASE", "SCHEMA", - "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", - "ADD", "MODIFY", "CHANGE", "COLUMN", "RENAME", - "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", "ANY", "SOME", - "CASE", "WHEN", "THEN", "ELSE", "END", "IF", "IFNULL", "COALESCE", - "UNION", "INTERSECT", "EXCEPT", - "FORCE", "USE", "IGNORE", "STRAIGHT_JOIN", "DUAL", - "SHOW", "DESCRIBE", "EXPLAIN" - ], - functions: [ - "COUNT", "SUM", "AVG", "MAX", "MIN", "GROUP_CONCAT", - "CONCAT", "SUBSTRING", "LEFT", "RIGHT", "LENGTH", "LOWER", "UPPER", - "TRIM", "LTRIM", "RTRIM", "REPLACE", - "NOW", "CURDATE", "CURTIME", "DATE", "TIME", "YEAR", "MONTH", "DAY", - "DATE_ADD", "DATE_SUB", "DATEDIFF", "TIMESTAMPDIFF", - "ROUND", "CEIL", "FLOOR", "ABS", "MOD", "POW", "SQRT", - "CAST", "CONVERT" - ], - dataTypes: [ - "INT", "INTEGER", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", - "DECIMAL", "NUMERIC", "FLOAT", "DOUBLE", "REAL", - "CHAR", "VARCHAR", "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", - "BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", - "DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR", - "ENUM", "SET", "JSON", "BOOL", "BOOLEAN" - ], - tableOptions: [ - "ENGINE=InnoDB", "DEFAULT CHARSET=utf8mb4", "COLLATE=utf8mb4_unicode_ci", - "AUTO_INCREMENT=", "COMMENT=", "ROW_FORMAT=" - ], - regexSyntax: .regexp, - booleanLiteralStyle: .numeric, - likeEscapeStyle: .implicit, - paginationStyle: .limit, - requiresBackslashEscaping: true - ) - - let mysqlColumnTypes: [String: [String]] = [ - "Integer": ["TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT"], - "Float": ["FLOAT", "DOUBLE", "DECIMAL", "NUMERIC", "REAL"], - "String": ["CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT", "ENUM", "SET"], - "Date": ["DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR"], - "Binary": ["BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB", "BIT"], - "Boolean": ["BOOLEAN", "BOOL"], - "JSON": ["JSON"], - "Spatial": ["GEOMETRY", "POINT", "LINESTRING", "POLYGON"] - ] - - let postgresqlDialect = SQLDialectDescriptor( - identifierQuote: "\"", - keywords: [ - "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", "FULL", - "ON", "USING", "AND", "OR", "NOT", "IN", "LIKE", "ILIKE", "BETWEEN", "AS", - "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", "FETCH", "FIRST", "ROWS", "ONLY", - "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", - "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "DATABASE", "SCHEMA", - "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", - "ADD", "MODIFY", "COLUMN", "RENAME", - "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", "ANY", "SOME", - "CASE", "WHEN", "THEN", "ELSE", "END", "COALESCE", "NULLIF", - "UNION", "INTERSECT", "EXCEPT", - "RETURNING", "WITH", "RECURSIVE", "MATERIALIZED", - "EXPLAIN", "ANALYZE", "VERBOSE", - "WINDOW", "OVER", "PARTITION", - "LATERAL", "ORDINALITY" - ], - functions: [ - "COUNT", "SUM", "AVG", "MAX", "MIN", "STRING_AGG", "ARRAY_AGG", - "CONCAT", "SUBSTRING", "LEFT", "RIGHT", "LENGTH", "LOWER", "UPPER", - "TRIM", "LTRIM", "RTRIM", "REPLACE", "SPLIT_PART", - "NOW", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", - "DATE_TRUNC", "EXTRACT", "AGE", "TO_CHAR", "TO_DATE", - "ROUND", "CEIL", "CEILING", "FLOOR", "ABS", "MOD", "POW", "POWER", "SQRT", - "CAST", "TO_NUMBER", "TO_TIMESTAMP", - "JSON_BUILD_OBJECT", "JSON_AGG", "JSONB_BUILD_OBJECT" - ], - dataTypes: [ - "INTEGER", "INT", "SMALLINT", "BIGINT", "SERIAL", "BIGSERIAL", "SMALLSERIAL", - "DECIMAL", "NUMERIC", "REAL", "DOUBLE", "PRECISION", - "CHAR", "CHARACTER", "VARCHAR", "TEXT", - "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL", - "BOOLEAN", "BOOL", "JSON", "JSONB", "UUID", "BYTEA", "ARRAY" - ], - tableOptions: [ - "INHERITS", "PARTITION BY", "TABLESPACE", "WITH", "WITHOUT OIDS" - ], - regexSyntax: .tilde, - booleanLiteralStyle: .truefalse, - likeEscapeStyle: .explicit, - paginationStyle: .limit - ) - - let postgresqlColumnTypes: [String: [String]] = [ - "Integer": ["SMALLINT", "INTEGER", "BIGINT", "SERIAL", "BIGSERIAL", "SMALLSERIAL"], - "Float": ["REAL", "DOUBLE PRECISION", "NUMERIC", "DECIMAL", "MONEY"], - "String": ["CHARACTER VARYING", "VARCHAR", "CHARACTER", "CHAR", "TEXT", "NAME"], - "Date": [ - "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL", - "TIME WITH TIME ZONE", "TIMESTAMP WITH TIME ZONE" - ], - "Binary": ["BYTEA"], - "Boolean": ["BOOLEAN"], - "JSON": ["JSON", "JSONB"], - "UUID": ["UUID"], - "Array": ["ARRAY"], - "Network": ["INET", "CIDR", "MACADDR", "MACADDR8"], - "Geometric": ["POINT", "LINE", "LSEG", "BOX", "PATH", "POLYGON", "CIRCLE"], - "Range": ["INT4RANGE", "INT8RANGE", "NUMRANGE", "TSRANGE", "TSTZRANGE", "DATERANGE"], - "Text Search": ["TSVECTOR", "TSQUERY"], - "XML": ["XML"] - ] - - let sqliteDialect = SQLDialectDescriptor( - identifierQuote: "`", - keywords: [ - "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", - "ON", "AND", "OR", "NOT", "IN", "LIKE", "GLOB", "BETWEEN", "AS", - "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", - "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", - "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "TRIGGER", - "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", - "ADD", "COLUMN", "RENAME", - "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", - "CASE", "WHEN", "THEN", "ELSE", "END", "COALESCE", "IFNULL", "NULLIF", - "UNION", "INTERSECT", "EXCEPT", - "AUTOINCREMENT", "WITHOUT", "ROWID", "PRAGMA", - "REPLACE", "ABORT", "FAIL", "IGNORE", "ROLLBACK", - "TEMP", "TEMPORARY", "VACUUM", "EXPLAIN", "QUERY", "PLAN" - ], - functions: [ - "COUNT", "SUM", "AVG", "MAX", "MIN", "GROUP_CONCAT", "TOTAL", - "LENGTH", "SUBSTR", "SUBSTRING", "LOWER", "UPPER", "TRIM", "LTRIM", "RTRIM", - "REPLACE", "INSTR", "PRINTF", - "DATE", "TIME", "DATETIME", "JULIANDAY", "STRFTIME", - "ABS", "ROUND", "RANDOM", - "CAST", "TYPEOF", - "COALESCE", "IFNULL", "NULLIF", "HEX", "QUOTE" - ], - dataTypes: [ - "INTEGER", "REAL", "TEXT", "BLOB", "NUMERIC", - "INT", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", - "UNSIGNED", "BIG", "INT2", "INT8", - "CHARACTER", "VARCHAR", "VARYING", "NCHAR", "NATIVE", - "NVARCHAR", "CLOB", - "DOUBLE", "PRECISION", "FLOAT", - "DECIMAL", "BOOLEAN", "DATE", "DATETIME" - ], - tableOptions: [ - "WITHOUT ROWID", "STRICT" - ], - regexSyntax: .unsupported, - booleanLiteralStyle: .numeric, - likeEscapeStyle: .explicit, - paginationStyle: .limit - ) - - let sqliteColumnTypes: [String: [String]] = [ - "Integer": ["INTEGER", "INT", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT"], - "Float": ["REAL", "DOUBLE", "FLOAT", "NUMERIC", "DECIMAL"], - "String": ["TEXT", "VARCHAR", "CHARACTER", "CHAR", "CLOB", "NVARCHAR", "NCHAR"], - "Date": ["DATE", "TIME", "DATETIME", "TIMESTAMP"], - "Binary": ["BLOB"], - "Boolean": ["BOOLEAN"] - ] - - let pgpassField = ConnectionField( - id: "usePgpass", - label: String(localized: "Use ~/.pgpass"), - defaultValue: "false", - fieldType: .toggle, - section: .authentication, - hidesPassword: true - ) - - let defaults: [(typeId: String, snapshot: PluginMetadataSnapshot)] = [ - ("MySQL", PluginMetadataSnapshot( - displayName: "MySQL", iconName: "mysql-icon", defaultPort: 3_306, - requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true, - isDownloadable: false, primaryUrlScheme: "mysql", parameterStyle: .questionMark, - navigationModel: .standard, explainVariants: [], pathFieldRole: .database, - supportsHealthMonitor: true, urlSchemes: ["mysql"], postConnectActions: [], - brandColorHex: "#FF9500", - queryLanguageName: "SQL", editorLanguage: .sql, - connectionMode: .network, supportsDatabaseSwitching: true, - capabilities: .defaults, - schema: PluginMetadataSnapshot.SchemaInfo( - defaultSchemaName: "public", - defaultGroupName: "main", - tableEntityName: "Tables", - defaultPrimaryKeyColumn: nil, - immutableColumns: [], - systemDatabaseNames: ["information_schema", "mysql", "performance_schema", "sys"], - systemSchemaNames: [], - fileExtensions: [], - databaseGroupingStrategy: .byDatabase, - structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] - ), - editor: PluginMetadataSnapshot.EditorConfig( - sqlDialect: mysqlDialect, - statementCompletions: [], - columnTypesByCategory: mysqlColumnTypes - ), - connection: .defaults - )), - ("MariaDB", PluginMetadataSnapshot( - displayName: "MariaDB", iconName: "mariadb-icon", defaultPort: 3_306, - requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true, - isDownloadable: false, primaryUrlScheme: "mariadb", parameterStyle: .questionMark, - navigationModel: .standard, explainVariants: [], pathFieldRole: .database, - supportsHealthMonitor: true, urlSchemes: ["mariadb"], postConnectActions: [], - brandColorHex: "#00B4D8", - queryLanguageName: "SQL", editorLanguage: .sql, - connectionMode: .network, supportsDatabaseSwitching: true, - capabilities: .defaults, - schema: PluginMetadataSnapshot.SchemaInfo( - defaultSchemaName: "public", - defaultGroupName: "main", - tableEntityName: "Tables", - defaultPrimaryKeyColumn: nil, - immutableColumns: [], - systemDatabaseNames: ["information_schema", "mysql", "performance_schema", "sys"], - systemSchemaNames: [], - fileExtensions: [], - databaseGroupingStrategy: .byDatabase, - structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] - ), - editor: PluginMetadataSnapshot.EditorConfig( - sqlDialect: mysqlDialect, - statementCompletions: [], - columnTypesByCategory: mysqlColumnTypes - ), - connection: .defaults - )), - ("PostgreSQL", PluginMetadataSnapshot( - displayName: "PostgreSQL", iconName: "postgresql-icon", defaultPort: 5_432, - requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true, - isDownloadable: false, primaryUrlScheme: "postgresql", parameterStyle: .dollar, - navigationModel: .standard, explainVariants: [], pathFieldRole: .database, - supportsHealthMonitor: true, urlSchemes: ["postgresql", "postgres"], postConnectActions: [], - brandColorHex: "#336791", - queryLanguageName: "SQL", editorLanguage: .sql, - connectionMode: .network, supportsDatabaseSwitching: true, - capabilities: PluginMetadataSnapshot.CapabilityFlags( - supportsSchemaSwitching: true, - supportsImport: true, - supportsExport: true, - supportsSSH: true, - supportsSSL: true, - supportsCascadeDrop: true, - supportsForeignKeyDisable: false, - supportsReadOnlyMode: true, - supportsQueryProgress: false, - requiresReconnectForDatabaseSwitch: true - ), - schema: PluginMetadataSnapshot.SchemaInfo( - defaultSchemaName: "public", - defaultGroupName: "main", - tableEntityName: "Tables", - defaultPrimaryKeyColumn: nil, - immutableColumns: [], - systemDatabaseNames: ["postgres", "template0", "template1"], - systemSchemaNames: [], - fileExtensions: [], - databaseGroupingStrategy: .bySchema, - structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] - ), - editor: PluginMetadataSnapshot.EditorConfig( - sqlDialect: postgresqlDialect, - statementCompletions: [], - columnTypesByCategory: postgresqlColumnTypes - ), - connection: PluginMetadataSnapshot.ConnectionConfig( - additionalConnectionFields: [pgpassField] - ) - )), - ("Redshift", PluginMetadataSnapshot( - displayName: "Redshift", iconName: "redshift-icon", defaultPort: 5_439, - requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: false, - isDownloadable: false, primaryUrlScheme: "redshift", parameterStyle: .dollar, - navigationModel: .standard, explainVariants: [], pathFieldRole: .database, - supportsHealthMonitor: true, urlSchemes: ["redshift"], postConnectActions: [], - brandColorHex: "#205B8E", - queryLanguageName: "SQL", editorLanguage: .sql, - connectionMode: .network, supportsDatabaseSwitching: true, - capabilities: PluginMetadataSnapshot.CapabilityFlags( - supportsSchemaSwitching: true, - supportsImport: true, - supportsExport: true, - supportsSSH: true, - supportsSSL: true, - supportsCascadeDrop: true, - supportsForeignKeyDisable: false, - supportsReadOnlyMode: true, - supportsQueryProgress: false, - requiresReconnectForDatabaseSwitch: true - ), - schema: PluginMetadataSnapshot.SchemaInfo( - defaultSchemaName: "public", - defaultGroupName: "main", - tableEntityName: "Tables", - defaultPrimaryKeyColumn: nil, - immutableColumns: [], - systemDatabaseNames: ["postgres", "template0", "template1"], - systemSchemaNames: [], - fileExtensions: [], - databaseGroupingStrategy: .bySchema, - structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] - ), - editor: PluginMetadataSnapshot.EditorConfig( - sqlDialect: postgresqlDialect, - statementCompletions: [], - columnTypesByCategory: postgresqlColumnTypes - ), - connection: PluginMetadataSnapshot.ConnectionConfig( - additionalConnectionFields: [pgpassField] - ) - )), - ("SQLite", PluginMetadataSnapshot( - displayName: "SQLite", iconName: "sqlite-icon", defaultPort: 0, - requiresAuthentication: false, supportsForeignKeys: true, supportsSchemaEditing: true, - isDownloadable: false, primaryUrlScheme: "sqlite", parameterStyle: .questionMark, - navigationModel: .standard, explainVariants: [], pathFieldRole: .filePath, - supportsHealthMonitor: false, urlSchemes: ["sqlite"], postConnectActions: [], - brandColorHex: "#003B57", - queryLanguageName: "SQL", editorLanguage: .sql, - connectionMode: .fileBased, supportsDatabaseSwitching: false, - capabilities: PluginMetadataSnapshot.CapabilityFlags( - supportsSchemaSwitching: false, - supportsImport: true, - supportsExport: true, - supportsSSH: false, - supportsSSL: false, - supportsCascadeDrop: false, - supportsForeignKeyDisable: true, - supportsReadOnlyMode: true, - supportsQueryProgress: false, - requiresReconnectForDatabaseSwitch: false - ), - schema: PluginMetadataSnapshot.SchemaInfo( - defaultSchemaName: "public", - defaultGroupName: "main", - tableEntityName: "Tables", - defaultPrimaryKeyColumn: nil, - immutableColumns: [], - systemDatabaseNames: [], - systemSchemaNames: [], - fileExtensions: ["db", "sqlite", "sqlite3"], - databaseGroupingStrategy: .flat, - structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] - ), - editor: PluginMetadataSnapshot.EditorConfig( - sqlDialect: sqliteDialect, - statementCompletions: [], - columnTypesByCategory: sqliteColumnTypes - ), - connection: .defaults - )) - ] - // swiftlint:enable function_body_length - let allDefaults = defaults + registryPluginDefaults() - for entry in allDefaults { + // Built-in plugins (MySQL, MariaDB, PostgreSQL, Redshift, SQLite) self-register + // their metadata at load time via buildMetadataSnapshot() in PluginManager.registerCapabilities(). + // Only registry plugin defaults (for downloadable plugins not yet installed) are pre-populated here. + for entry in registryPluginDefaults() { snapshots[entry.typeId] = entry.snapshot for scheme in entry.snapshot.urlSchemes { 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) { @@ -538,6 +179,72 @@ final class PluginMetadataRegistry: @unchecked Sendable { return schemeIndex[scheme.lowercased()] } + // MARK: - Snapshot Builder + + 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 databaseType(forUrlScheme scheme: String) -> DatabaseType? { guard let typeId = typeId(forUrlScheme: scheme) else { return nil } return DatabaseType(rawValue: typeId) @@ -563,4 +270,39 @@ final class PluginMetadataRegistry: @unchecked Sendable { defer { lock.unlock() } return schemeIndex } + + // MARK: - Type Registration Helpers + + /// 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() + reverseTypeIndex[aliasTypeId] = primaryTypeId + lock.unlock() + } + + /// Returns all registered type IDs. + func allRegisteredTypeIds() -> [String] { + lock.lock() + defer { lock.unlock() } + return Array(snapshots.keys) + } + + /// Resolves a database type raw value to its plugin type ID. + /// For multi-type plugins (MySQL serves MariaDB), maps the alias to the primary. + func pluginTypeId(for rawValue: String) -> String { + lock.lock() + defer { lock.unlock() } + if snapshots[rawValue] != nil { + return reverseTypeIndex[rawValue] ?? rawValue + } + return reverseTypeIndex[rawValue] ?? rawValue + } + + /// Returns whether the given type ID is registered. + func hasType(_ typeId: String) -> Bool { + lock.lock() + defer { lock.unlock() } + return snapshots[typeId] != nil + } } 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/Core/Utilities/Connection/ConnectionURLFormatter.swift b/TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift index b1ebac97d..3ee78be10 100644 --- a/TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift +++ b/TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift @@ -13,7 +13,7 @@ struct ConnectionURLFormatter { return formatSQLite(connection.database) } - if connection.type == .duckdb { + if connection.type == DatabaseType(rawValue: "DuckDB") { return formatDuckDB(connection.database) } @@ -76,7 +76,7 @@ struct ConnectionURLFormatter { result += ":\(connection.port)" } - let sshPathComponent = connection.type == .oracle + let sshPathComponent = connection.type == DatabaseType(rawValue: "Oracle") ? (connection.oracleServiceName ?? connection.database) : connection.database result += "/\(sshPathComponent)" @@ -109,7 +109,7 @@ struct ConnectionURLFormatter { result += ":\(connection.port)" } - let pathComponent = connection.type == .oracle + let pathComponent = connection.type == DatabaseType(rawValue: "Oracle") ? (connection.oracleServiceName ?? connection.database) : connection.database result += "/\(pathComponent)" diff --git a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift index 2857b58f1..ba0a92381 100644 --- a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift +++ b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift @@ -101,19 +101,19 @@ struct ConnectionURLParser { case "sqlite": dbType = .sqlite case "mongodb", "mongodb+srv": - dbType = .mongodb + dbType = DatabaseType(rawValue: "MongoDB") case "redis", "rediss": - dbType = .redis + dbType = DatabaseType(rawValue: "Redis") case "sqlserver", "mssql", "jdbc:sqlserver": - dbType = .mssql + dbType = DatabaseType(rawValue: "SQL Server") case "oracle", "jdbc:oracle:thin": - dbType = .oracle + dbType = DatabaseType(rawValue: "Oracle") case "clickhouse", "ch": - dbType = .clickhouse + dbType = DatabaseType(rawValue: "ClickHouse") case "cassandra", "cql": - dbType = .cassandra + dbType = DatabaseType(rawValue: "Cassandra") case "scylladb", "scylla": - dbType = .scylladb + dbType = DatabaseType(rawValue: "ScyllaDB") default: if let resolvedType = PluginMetadataRegistry.shared.databaseType(forUrlScheme: scheme) { dbType = resolvedType @@ -186,7 +186,7 @@ struct ConnectionURLParser { var sslMode = ext.sslMode // Redis-specific: parse database index from path and handle TLS scheme var redisDatabase: Int? - if dbType == .redis { + if dbType == DatabaseType(rawValue: "Redis") { if !database.isEmpty { redisDatabase = Int(database) database = "" @@ -198,7 +198,7 @@ struct ConnectionURLParser { // Oracle-specific: path component is the service name, not the database name var oracleServiceName: String? - if dbType == .oracle && !database.isEmpty { + if dbType == DatabaseType(rawValue: "Oracle") && !database.isEmpty { oracleServiceName = database database = "" } @@ -329,7 +329,7 @@ struct ConnectionURLParser { // Oracle-specific: path component is the service name, not the database name var oracleServiceName: String? - if dbType == .oracle && !database.isEmpty { + if dbType == DatabaseType(rawValue: "Oracle") && !database.isEmpty { oracleServiceName = database database = "" } diff --git a/TablePro/Models/Connection/DatabaseConnection.swift b/TablePro/Models/Connection/DatabaseConnection.swift index 0402069f0..a02cbe5d8 100644 --- a/TablePro/Models/Connection/DatabaseConnection.swift +++ b/TablePro/Models/Connection/DatabaseConnection.swift @@ -219,20 +219,12 @@ 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") - static let mongodb = DatabaseType(rawValue: "MongoDB") - static let redis = DatabaseType(rawValue: "Redis") - static let mssql = DatabaseType(rawValue: "SQL Server") - static let oracle = DatabaseType(rawValue: "Oracle") - static let clickhouse = DatabaseType(rawValue: "ClickHouse") - static let duckdb = DatabaseType(rawValue: "DuckDB") - static let cassandra = DatabaseType(rawValue: "Cassandra") - static let scylladb = DatabaseType(rawValue: "ScyllaDB") - static let etcd = DatabaseType(rawValue: "etcd") } extension DatabaseType: Codable { @@ -248,29 +240,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 +286,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/ViewModels/QuickSwitcherViewModel.swift b/TablePro/ViewModels/QuickSwitcherViewModel.swift index e5e0cc844..53e4acee6 100644 --- a/TablePro/ViewModels/QuickSwitcherViewModel.swift +++ b/TablePro/ViewModels/QuickSwitcherViewModel.swift @@ -87,7 +87,7 @@ internal final class QuickSwitcherViewModel { } // Schemas (only for databases that support them) - let supportsSchemas = [DatabaseType.postgresql, .redshift, .oracle, .mssql] + let supportsSchemas = [DatabaseType.postgresql, .redshift, DatabaseType(rawValue: "Oracle"), DatabaseType(rawValue: "SQL Server")] if supportsSchemas.contains(databaseType) { do { let schemas = try await driver.fetchSchemas() diff --git a/TablePro/Views/Editor/HistoryPanelView.swift b/TablePro/Views/Editor/HistoryPanelView.swift index 062b9d0b5..cde6b894f 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.") ? DatabaseType(rawValue: "MongoDB") : .mysql ) .background(Color(nsColor: ThemeEngine.shared.colors.editor.background)) diff --git a/TablePro/Views/Results/TableRowViewWithMenu.swift b/TablePro/Views/Results/TableRowViewWithMenu.swift index f5a7f2517..9fb630818 100644 --- a/TablePro/Views/Results/TableRowViewWithMenu.swift +++ b/TablePro/Views/Results/TableRowViewWithMenu.swift @@ -69,7 +69,7 @@ final class TableRowViewWithMenu: NSTableRowView { copyAsMenu.addItem(jsonItem) if let dbType = coordinator.databaseType, - dbType != .mongodb && dbType != .redis, + dbType != DatabaseType(rawValue: "MongoDB") && dbType != DatabaseType(rawValue: "Redis"), coordinator.tableName != nil { copyAsMenu.addItem(NSMenuItem.separator()) diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index 96d842d84..ca795d520 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -108,7 +108,7 @@ struct TableStructureView: View { if !connection.type.supportsForeignKeys { tabs = tabs.filter { $0 != .foreignKeys } } - if connection.type != .clickhouse { + if connection.type != DatabaseType(rawValue: "ClickHouse") { tabs = tabs.filter { $0 != .parts } } return tabs @@ -223,7 +223,7 @@ struct TableStructureView: View { } private func updateColumn(_ column: inout EditableColumnDefinition, at index: Int, with value: String) { - if connection.type == .clickhouse { + if connection.type == DatabaseType(rawValue: "ClickHouse") { // ClickHouse: Name(0), Type(1), Nullable(2), Default(3), Comment(4) — no Auto Inc switch index { case 0: column.name = value diff --git a/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift b/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift index 0ee3df4ee..d72efaabb 100644 --- a/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift +++ b/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift @@ -20,7 +20,7 @@ struct DataChangeManagerClickHouseTests { tableName: "users", columns: ["id", "name"], primaryKeyColumn: "id", - databaseType: .clickhouse + databaseType: DatabaseType(rawValue: "ClickHouse") ) manager.recordCellChange( @@ -49,7 +49,7 @@ struct DataChangeManagerClickHouseTests { tableName: "events", columns: ["id", "status"], primaryKeyColumn: "id", - databaseType: .clickhouse + databaseType: DatabaseType(rawValue: "ClickHouse") ) manager.recordCellChange( @@ -100,7 +100,7 @@ struct DataChangeManagerClickHouseTests { tableName: "logs", columns: ["timestamp", "message"], primaryKeyColumn: nil, - databaseType: .clickhouse + databaseType: DatabaseType(rawValue: "ClickHouse") ) manager.recordCellChange( diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift index 73994106c..fa048fc57 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift @@ -2,7 +2,7 @@ // SQLStatementGeneratorMSSQLTests.swift // TableProTests // -// Tests for SQLStatementGenerator with databaseType: .mssql +// Tests for SQLStatementGenerator with databaseType: DatabaseType(rawValue: "SQL Server") // import Foundation @@ -22,7 +22,7 @@ struct SQLStatementGeneratorMSSQLTests { tableName: tableName, columns: columns, primaryKeyColumn: primaryKeyColumn, - databaseType: .mssql + databaseType: DatabaseType(rawValue: "SQL Server") ) } diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift index 0a1f689eb..e65abf948 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift @@ -156,7 +156,7 @@ struct SQLStatementGeneratorNoPKTests { @Test("Update without PK — MSSQL uses UPDATE TOP (1)") func testUpdateNoPKMSSQLTop() { - let generator = makeGenerator(databaseType: .mssql) + let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) let changes: [RowChange] = [ RowChange( rowIndex: 0, @@ -266,7 +266,7 @@ struct SQLStatementGeneratorNoPKTests { @Test("Delete without PK — MSSQL uses DELETE TOP (1)") func testDeleteNoPKMSSQLTop() { - let generator = makeGenerator(databaseType: .mssql) + let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) let changes: [RowChange] = [ RowChange( rowIndex: 0, diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift index 1ecee048b..1bc4cdd38 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift @@ -103,7 +103,7 @@ struct SQLStatementGeneratorPKRegressionTests { @Test("MSSQL delete with PK uses ? placeholder and PK-only WHERE") func testMSSQLDeleteWithPK() { - let generator = makeGenerator(databaseType: .mssql) + let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) let changes = [makeDeleteChange(rowIndex: 0, originalRow: ["1", "John", "john@test.com"])] let statements = generator.generateStatements( @@ -127,7 +127,7 @@ struct SQLStatementGeneratorPKRegressionTests { @Test("ClickHouse delete with PK uses ALTER TABLE DELETE") func testClickHouseDeleteWithPK() { - let generator = makeGenerator(databaseType: .clickhouse) + let generator = makeGenerator(databaseType: DatabaseType(rawValue: "ClickHouse")) let changes = [makeDeleteChange(rowIndex: 0, originalRow: ["1", "John", "john@test.com"])] let statements = generator.generateStatements( @@ -173,7 +173,7 @@ struct SQLStatementGeneratorPKRegressionTests { @Test("MSSQL update with PK uses PK-only WHERE") func testMSSQLUpdateWithPK() { - let generator = makeGenerator(databaseType: .mssql) + let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) let changes = [makeUpdateChange( rowIndex: 0, columnIndex: 1, columnName: "name", oldValue: "John", newValue: "Jane", originalRow: ["1", "John", "john@test.com"] diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift index 1f8c4364d..d2f17d776 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift @@ -72,7 +72,7 @@ struct SQLStatementGeneratorParameterStyleTests { @Test("DuckDB defaults to dollar style") func testDuckDBDefaultsDollar() { - let generator = makeGenerator(databaseType: .duckdb) + let generator = makeGenerator(databaseType: DatabaseType(rawValue: "DuckDB")) let insertedRowData: [Int: [String?]] = [0: ["1", "John", "john@example.com"]] let changes: [RowChange] = [ RowChange(rowIndex: 0, type: .insert, cellChanges: [], originalRow: nil) @@ -125,7 +125,7 @@ struct SQLStatementGeneratorParameterStyleTests { @Test("MSSQL defaults to questionMark style") func testMSSQLDefaultsQuestionMark() { - let generator = makeGenerator(databaseType: .mssql) + let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) let insertedRowData: [Int: [String?]] = [0: ["1", "John", "john@example.com"]] let changes: [RowChange] = [ RowChange(rowIndex: 0, type: .insert, cellChanges: [], originalRow: nil) diff --git a/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift b/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift index ec37f1938..a7b90484c 100644 --- a/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift +++ b/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift @@ -32,7 +32,7 @@ struct ClickHouseDialectTests { @Test("Factory returns empty dialect when plugin not loaded") @MainActor func testFactoryFallbackWithoutPlugin() { - let dialect = SQLDialectFactory.createDialect(for: .clickhouse) + let dialect = SQLDialectFactory.createDialect(for: DatabaseType(rawValue: "ClickHouse")) // Without plugin loaded, factory returns empty fallback #expect(dialect.keywords.isEmpty) } diff --git a/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift b/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift index e5b73df35..cdc775823 100644 --- a/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift +++ b/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift @@ -2,7 +2,7 @@ // FilterSQLGeneratorMSSQLTests.swift // TableProTests // -// Tests for FilterSQLGenerator with databaseType: .mssql +// Tests for FilterSQLGenerator with databaseType: DatabaseType(rawValue: "SQL Server") // import Foundation diff --git a/TableProTests/Core/Database/MSSQLDriverTests.swift b/TableProTests/Core/Database/MSSQLDriverTests.swift index 3da327f94..4d48a9af0 100644 --- a/TableProTests/Core/Database/MSSQLDriverTests.swift +++ b/TableProTests/Core/Database/MSSQLDriverTests.swift @@ -75,7 +75,7 @@ struct MSSQLDriverTests { // MARK: - Helpers private func makeConnection(mssqlSchema: String? = nil) -> DatabaseConnection { - var conn = TestFixtures.makeConnection(type: .mssql) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "SQL Server")) conn.mssqlSchema = mssqlSchema return conn } diff --git a/TableProTests/Core/Services/SafeModeGuardTests.swift b/TableProTests/Core/Services/SafeModeGuardTests.swift index 603929b41..550dcafa6 100644 --- a/TableProTests/Core/Services/SafeModeGuardTests.swift +++ b/TableProTests/Core/Services/SafeModeGuardTests.swift @@ -70,7 +70,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .readOnly, isWriteOperation: false, sql: "db.users.find({})", operationDescription: "Find", - window: nil, databaseType: .mongodb + window: nil, databaseType: DatabaseType(rawValue: "MongoDB") ) guard case let .blocked(message) = result else { Issue.record("Expected .blocked for MongoDB but got .allowed") @@ -84,7 +84,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .readOnly, isWriteOperation: false, sql: "GET key", operationDescription: "Get", - window: nil, databaseType: .redis + window: nil, databaseType: DatabaseType(rawValue: "Redis") ) guard case let .blocked(message) = result else { Issue.record("Expected .blocked for Redis but got .allowed") @@ -98,7 +98,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .silent, isWriteOperation: false, sql: "db.users.find({})", operationDescription: "Find", - window: nil, databaseType: .mongodb + window: nil, databaseType: DatabaseType(rawValue: "MongoDB") ) if case .blocked = result { Issue.record("Expected .allowed for MongoDB in silent mode but got .blocked") @@ -110,7 +110,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .silent, isWriteOperation: false, sql: "GET key", operationDescription: "Get", - window: nil, databaseType: .redis + window: nil, databaseType: DatabaseType(rawValue: "Redis") ) if case .blocked = result { Issue.record("Expected .allowed for Redis in silent mode but got .blocked") diff --git a/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift b/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift index 806666755..cdfcef8a5 100644 --- a/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift +++ b/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift @@ -157,7 +157,7 @@ struct TableQueryBuilderPostgreSQLQuickSearchTests { @Suite("Table Query Builder - NoSQL Nil Dialect Fallback") struct TableQueryBuilderNoSQLTests { // MongoDB has no SQL dialect — should produce bare SELECT without WHERE - private let builder = TableQueryBuilder(databaseType: .mongodb) + private let builder = TableQueryBuilder(databaseType: DatabaseType(rawValue: "MongoDB")) @Test("NoSQL type produces no WHERE for filtered query") func noSqlFilteredQueryNoWhere() { diff --git a/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift b/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift index 03181e88f..175b2c289 100644 --- a/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift +++ b/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift @@ -2,7 +2,7 @@ // TableQueryBuilderMSSQLTests.swift // TableProTests // -// Tests for TableQueryBuilder with databaseType: .mssql +// Tests for TableQueryBuilder with databaseType: DatabaseType(rawValue: "SQL Server") // import Foundation @@ -11,7 +11,7 @@ import Testing @Suite("Table Query Builder MSSQL") struct TableQueryBuilderMSSQLTests { - private let builder = TableQueryBuilder(databaseType: .mssql) + private let builder = TableQueryBuilder(databaseType: DatabaseType(rawValue: "SQL Server")) // MARK: - Base Query Tests diff --git a/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift b/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift index c502f826e..f3d418788 100644 --- a/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift +++ b/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift @@ -19,7 +19,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test Mongo", host: "localhost", port: 27_017, - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "admin", mongoReadPreference: "secondary", mongoWriteConcern: "majority" @@ -42,7 +42,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test MSSQL", host: "localhost", port: 1_433, - type: .mssql, + type: DatabaseType(rawValue: "SQL Server"), mssqlSchema: "custom_schema" ) @@ -61,7 +61,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test Oracle", host: "localhost", port: 1_521, - type: .oracle, + type: DatabaseType(rawValue: "Oracle"), oracleServiceName: "ORCL" ) @@ -80,7 +80,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test Redis", host: "localhost", port: 6_379, - type: .redis, + type: DatabaseType(rawValue: "Redis"), redisDatabase: 5 ) @@ -146,7 +146,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Cache Test", host: "localhost", port: 27_017, - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "testdb" ) @@ -172,7 +172,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Mongo", host: "localhost", port: 27_017, - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "admin" ) @@ -182,7 +182,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Redis", host: "localhost", port: 6_379, - type: .redis, + type: DatabaseType(rawValue: "Redis"), redisDatabase: 3 ) @@ -192,7 +192,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "MSSQL", host: "localhost", port: 1_433, - type: .mssql, + type: DatabaseType(rawValue: "SQL Server"), mssqlSchema: "dbo" ) diff --git a/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift b/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift index 5fe77b28c..f4197a293 100644 --- a/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift +++ b/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift @@ -69,7 +69,7 @@ struct ConnectionURLFormatterTests { func testDefaultPortOmittedMongoDB() { let conn = DatabaseConnection( name: "", host: "host", port: 27_017, database: "db", - username: "user", type: .mongodb + username: "user", type: DatabaseType(rawValue: "MongoDB") ) let url = ConnectionURLFormatter.format(conn, password: "pass", sshPassword: nil) #expect(!url.contains(":27017")) diff --git a/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift b/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift index 87638f8c3..50f1731f2 100644 --- a/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift +++ b/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift @@ -16,7 +16,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) #expect(parsed.host == "host") #expect(parsed.port == nil) #expect(parsed.database == "mydb") @@ -30,7 +30,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) #expect(parsed.host == "host") #expect(parsed.database == "db") #expect(parsed.username == "user") @@ -43,7 +43,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) #expect(parsed.host == "host") #expect(parsed.username == "user") } @@ -54,7 +54,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) #expect(parsed.host == "host") #expect(parsed.database == "db") #expect(parsed.username == "") @@ -67,7 +67,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) #expect(parsed.port == 1434) #expect(parsed.host == "host") #expect(parsed.database == "db") @@ -79,7 +79,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mongodb) + #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) #expect(parsed.host == "cluster.net") #expect(parsed.database == "db") } diff --git a/TableProTests/Core/Utilities/ConnectionURLParserTests.swift b/TableProTests/Core/Utilities/ConnectionURLParserTests.swift index 3551510a5..bd9752339 100644 --- a/TableProTests/Core/Utilities/ConnectionURLParserTests.swift +++ b/TableProTests/Core/Utilities/ConnectionURLParserTests.swift @@ -275,7 +275,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mongodb) + #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) #expect(parsed.host == "mongo.example.com") #expect(parsed.port == nil) #expect(parsed.database == "mydb") @@ -289,7 +289,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mongodb) + #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) #expect(parsed.port == nil) } @@ -575,7 +575,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.host == "localhost") #expect(parsed.port == nil) #expect(parsed.redisDatabase == nil) @@ -587,7 +587,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.redisDatabase == 3) #expect(parsed.database == "") } @@ -598,7 +598,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.host == "localhost") #expect(parsed.port == nil) } @@ -609,7 +609,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.sslMode == .required) } @@ -619,7 +619,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.password == "password") #expect(parsed.host == "localhost") } @@ -630,7 +630,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.username == "user") #expect(parsed.password == "pass") #expect(parsed.host == "localhost") @@ -645,7 +645,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.redisDatabase == 0) #expect(parsed.database == "") } diff --git a/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift b/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift index 44030fabf..30da1f5c8 100644 --- a/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift +++ b/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift @@ -66,7 +66,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mongodb) + #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) } @Test("MongoDB+SRV scheme parses and maps to mongodb type") @@ -75,7 +75,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mongodb) + #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) } @Test("Redis scheme parses successfully") @@ -84,7 +84,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.sslMode == nil) } @@ -94,7 +94,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .redis) + #expect(parsed.type == DatabaseType(rawValue: "Redis")) #expect(parsed.sslMode == .required) } @@ -113,7 +113,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) } @Test("SQL Server scheme parses successfully") @@ -122,7 +122,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) } // MARK: - SSH Variants @@ -201,7 +201,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success, got: \(result)"); return } - #expect(parsed.type == .cassandra) + #expect(parsed.type == DatabaseType(rawValue: "Cassandra")) #expect(parsed.host == "host") #expect(parsed.port == nil) // 9042 is the default port, so parser normalizes to nil #expect(parsed.database == "keyspace") @@ -235,7 +235,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == .mssql) + #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) } @Test("Mixed case scheme parses correctly") diff --git a/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift b/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift index 57516383b..0bc2ab560 100644 --- a/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift +++ b/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift @@ -146,14 +146,14 @@ struct SQLRowToStatementConverterTests { @Test("ClickHouse fallback uses standard UPDATE syntax (plugin handles ALTER TABLE at runtime)") func clickhouseFallbackUsesStandardUpdate() { - let converter = makeConverter(databaseType: .clickhouse, dialect: Self.clickhouseDialect) + let converter = makeConverter(databaseType: DatabaseType(rawValue: "ClickHouse"), dialect: Self.clickhouseDialect) let result = converter.generateUpdates(rows: [["1", "Alice", "alice@example.com"]]) #expect(result == "UPDATE `users` SET `name` = 'Alice', `email` = 'alice@example.com' WHERE `id` = '1';") } @Test("MSSQL uses bracket quoting") func mssqlUsesBracketQuoting() { - let converter = makeConverter(databaseType: .mssql, dialect: Self.mssqlDialect) + let converter = makeConverter(databaseType: DatabaseType(rawValue: "SQL Server"), dialect: Self.mssqlDialect) let result = converter.generateInserts(rows: [["1", "Alice", "alice@example.com"]]) #expect(result == "INSERT INTO [users] ([id], [name], [email]) VALUES ('1', 'Alice', 'alice@example.com');") } @@ -174,7 +174,7 @@ struct SQLRowToStatementConverterTests { @Test("DuckDB uses double-quote quoting and standard UPDATE syntax") func duckdbUsesDoubleQuoteAndStandardUpdate() { - let converter = makeConverter(databaseType: .duckdb, dialect: Self.duckdbDialect) + let converter = makeConverter(databaseType: DatabaseType(rawValue: "DuckDB"), dialect: Self.duckdbDialect) let insert = converter.generateInserts(rows: [["1", "Alice", "alice@example.com"]]) #expect(insert == "INSERT INTO \"users\" (\"id\", \"name\", \"email\") VALUES ('1', 'Alice', 'alice@example.com');") let update = converter.generateUpdates(rows: [["1", "Alice", "alice@example.com"]]) diff --git a/TableProTests/Helpers/TestFixtures.swift b/TableProTests/Helpers/TestFixtures.swift index 63e7afe4a..f0f98ba4a 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, + DatabaseType(rawValue: "MongoDB"), DatabaseType(rawValue: "Redis"), DatabaseType(rawValue: "ClickHouse") + ] // MARK: - ClickHouse Connection Fixture @@ -22,7 +25,7 @@ enum TestFixtures { port: 8_123, database: "default", username: "default", - type: .clickhouse + type: DatabaseType(rawValue: "ClickHouse") ) // MARK: - Factory Methods diff --git a/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift b/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift index 12bf1f8bc..700751ae4 100644 --- a/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift +++ b/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift @@ -15,37 +15,37 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("mongoAuthSource defaults to nil") func mongoAuthSourceDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: .mongodb) + let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) #expect(conn.mongoAuthSource == nil) } @Test("mongoReadPreference defaults to nil") func mongoReadPreferenceDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: .mongodb) + let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) #expect(conn.mongoReadPreference == nil) } @Test("mongoWriteConcern defaults to nil") func mongoWriteConcernDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: .mongodb) + let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) #expect(conn.mongoWriteConcern == nil) } @Test("mssqlSchema defaults to nil") func mssqlSchemaDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: .mssql) + let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "SQL Server")) #expect(conn.mssqlSchema == nil) } @Test("oracleServiceName defaults to nil") func oracleServiceNameDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: .oracle) + let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Oracle")) #expect(conn.oracleServiceName == nil) } @Test("redisDatabase defaults to nil") func redisDatabaseDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: .redis) + let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Redis")) #expect(conn.redisDatabase == nil) } @@ -53,42 +53,42 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("mongoAuthSource is readable and writable") func mongoAuthSourceReadWrite() { - var conn = TestFixtures.makeConnection(type: .mongodb) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) conn.mongoAuthSource = "admin" #expect(conn.mongoAuthSource == "admin") } @Test("mongoReadPreference is readable and writable") func mongoReadPreferenceReadWrite() { - var conn = TestFixtures.makeConnection(type: .mongodb) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) conn.mongoReadPreference = "secondary" #expect(conn.mongoReadPreference == "secondary") } @Test("mongoWriteConcern is readable and writable") func mongoWriteConcernReadWrite() { - var conn = TestFixtures.makeConnection(type: .mongodb) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) conn.mongoWriteConcern = "majority" #expect(conn.mongoWriteConcern == "majority") } @Test("mssqlSchema is readable and writable") func mssqlSchemaReadWrite() { - var conn = TestFixtures.makeConnection(type: .mssql) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "SQL Server")) conn.mssqlSchema = "dbo" #expect(conn.mssqlSchema == "dbo") } @Test("oracleServiceName is readable and writable") func oracleServiceNameReadWrite() { - var conn = TestFixtures.makeConnection(type: .oracle) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Oracle")) conn.oracleServiceName = "ORCL" #expect(conn.oracleServiceName == "ORCL") } @Test("redisDatabase is readable and writable") func redisDatabaseReadWrite() { - var conn = TestFixtures.makeConnection(type: .redis) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Redis")) conn.redisDatabase = 3 #expect(conn.redisDatabase == 3) } @@ -97,7 +97,7 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("Setting mongoAuthSource writes to additionalFields dict") func mongoAuthSourceWritesToDict() { - var conn = TestFixtures.makeConnection(type: .mongodb) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) conn.mongoAuthSource = "admin" #expect(conn.additionalFields["mongoAuthSource"] == "admin") } @@ -106,7 +106,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initWithDictPopulatesAliases() { let conn = DatabaseConnection( name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), additionalFields: [ "mongoAuthSource": "admin", "mongoReadPreference": "primary", @@ -122,7 +122,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func emptyStringReturnsNil() { let conn = DatabaseConnection( name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), additionalFields: ["mongoAuthSource": ""] ) #expect(conn.mongoAuthSource == nil) @@ -130,7 +130,7 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("Setting nil via computed alias writes empty string to dict") func settingNilWritesEmptyString() { - var conn = TestFixtures.makeConnection(type: .mongodb) + var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) conn.mongoAuthSource = "admin" conn.mongoAuthSource = nil #expect(conn.additionalFields["mongoAuthSource"] == "") @@ -142,7 +142,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initPopulatesMongoAuthSource() { let conn = DatabaseConnection( name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "admin" ) #expect(conn.mongoAuthSource == "admin") @@ -153,7 +153,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initPopulatesMssqlSchema() { let conn = DatabaseConnection( name: "Test", - type: .mssql, + type: DatabaseType(rawValue: "SQL Server"), mssqlSchema: "dbo" ) #expect(conn.mssqlSchema == "dbo") @@ -164,7 +164,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initPopulatesOracleServiceName() { let conn = DatabaseConnection( name: "Test", - type: .oracle, + type: DatabaseType(rawValue: "Oracle"), oracleServiceName: "ORCL" ) #expect(conn.oracleServiceName == "ORCL") @@ -175,7 +175,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initDictOverridesNamedParams() { let conn = DatabaseConnection( name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "fromParam", additionalFields: ["mongoAuthSource": "fromDict"] ) @@ -190,13 +190,13 @@ struct DatabaseConnectionAdditionalFieldsTests { let a = DatabaseConnection( id: id, name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "admin" ) let b = DatabaseConnection( id: id, name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "admin" ) #expect(a == b) @@ -208,13 +208,13 @@ struct DatabaseConnectionAdditionalFieldsTests { let a = DatabaseConnection( id: id, name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "admin" ) let b = DatabaseConnection( id: id, name: "Test", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "other" ) #expect(a != b) @@ -226,7 +226,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func codableRoundTripMongo() throws { let original = DatabaseConnection( name: "Mongo", - type: .mongodb, + type: DatabaseType(rawValue: "MongoDB"), mongoAuthSource: "admin", mongoReadPreference: "secondary", mongoWriteConcern: "majority" @@ -242,7 +242,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func codableRoundTripMssql() throws { let original = DatabaseConnection( name: "MSSQL", - type: .mssql, + type: DatabaseType(rawValue: "SQL Server"), mssqlSchema: "dbo" ) let data = try JSONEncoder().encode(original) @@ -254,7 +254,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func codableRoundTripOracle() throws { let original = DatabaseConnection( name: "Oracle", - type: .oracle, + type: DatabaseType(rawValue: "Oracle"), oracleServiceName: "ORCL" ) let data = try JSONEncoder().encode(original) @@ -264,7 +264,7 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("Codable round-trip preserves nil additional fields") func codableRoundTripNils() throws { - let original = TestFixtures.makeConnection(type: .mongodb) + let original = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) let data = try JSONEncoder().encode(original) let decoded = try JSONDecoder().decode(DatabaseConnection.self, from: data) #expect(decoded.mongoAuthSource == nil) diff --git a/TableProTests/Models/DatabaseTypeCassandraTests.swift b/TableProTests/Models/DatabaseTypeCassandraTests.swift index 3bba70abf..b144b3b5a 100644 --- a/TableProTests/Models/DatabaseTypeCassandraTests.swift +++ b/TableProTests/Models/DatabaseTypeCassandraTests.swift @@ -5,91 +5,91 @@ import Testing struct DatabaseTypeCassandraTests { @Test("Cassandra raw value is Cassandra") func cassandraRawValue() { - #expect(DatabaseType.cassandra.rawValue == "Cassandra") + #expect(DatabaseType(rawValue: "Cassandra").rawValue == "Cassandra") } @Test("ScyllaDB raw value is ScyllaDB") func scylladbRawValue() { - #expect(DatabaseType.scylladb.rawValue == "ScyllaDB") + #expect(DatabaseType(rawValue: "ScyllaDB").rawValue == "ScyllaDB") } @Test("Cassandra pluginTypeId is Cassandra") func cassandraPluginTypeId() { - #expect(DatabaseType.cassandra.pluginTypeId == "Cassandra") + #expect(DatabaseType(rawValue: "Cassandra").pluginTypeId == "Cassandra") } @Test("ScyllaDB pluginTypeId is Cassandra") func scylladbPluginTypeId() { - #expect(DatabaseType.scylladb.pluginTypeId == "Cassandra") + #expect(DatabaseType(rawValue: "ScyllaDB").pluginTypeId == "Cassandra") } @Test("Cassandra default port is 9042") func cassandraDefaultPort() { - #expect(DatabaseType.cassandra.defaultPort == 9_042) + #expect(DatabaseType(rawValue: "Cassandra").defaultPort == 9_042) } @Test("ScyllaDB default port is 9042") func scylladbDefaultPort() { - #expect(DatabaseType.scylladb.defaultPort == 9_042) + #expect(DatabaseType(rawValue: "ScyllaDB").defaultPort == 9_042) } @Test("Cassandra does not require authentication") func cassandraRequiresAuthentication() { - #expect(DatabaseType.cassandra.requiresAuthentication == false) + #expect(DatabaseType(rawValue: "Cassandra").requiresAuthentication == false) } @Test("ScyllaDB does not require authentication") func scylladbRequiresAuthentication() { - #expect(DatabaseType.scylladb.requiresAuthentication == false) + #expect(DatabaseType(rawValue: "ScyllaDB").requiresAuthentication == false) } @Test("Cassandra does not support foreign keys") func cassandraSupportsForeignKeys() { - #expect(DatabaseType.cassandra.supportsForeignKeys == false) + #expect(DatabaseType(rawValue: "Cassandra").supportsForeignKeys == false) } @Test("ScyllaDB does not support foreign keys") func scylladbSupportsForeignKeys() { - #expect(DatabaseType.scylladb.supportsForeignKeys == false) + #expect(DatabaseType(rawValue: "ScyllaDB").supportsForeignKeys == false) } @Test("Cassandra supports schema editing") func cassandraSupportsSchemaEditing() { - #expect(DatabaseType.cassandra.supportsSchemaEditing == true) + #expect(DatabaseType(rawValue: "Cassandra").supportsSchemaEditing == true) } @Test("ScyllaDB supports schema editing") func scylladbSupportsSchemaEditing() { - #expect(DatabaseType.scylladb.supportsSchemaEditing == true) + #expect(DatabaseType(rawValue: "ScyllaDB").supportsSchemaEditing == true) } @Test("Cassandra icon name is cassandra-icon") func cassandraIconName() { - #expect(DatabaseType.cassandra.iconName == "cassandra-icon") + #expect(DatabaseType(rawValue: "Cassandra").iconName == "cassandra-icon") } @Test("ScyllaDB icon name is scylladb-icon") func scylladbIconName() { - #expect(DatabaseType.scylladb.iconName == "scylladb-icon") + #expect(DatabaseType(rawValue: "ScyllaDB").iconName == "scylladb-icon") } @Test("Cassandra is a downloadable plugin") func cassandraIsDownloadablePlugin() { - #expect(DatabaseType.cassandra.isDownloadablePlugin == true) + #expect(DatabaseType(rawValue: "Cassandra").isDownloadablePlugin == true) } @Test("ScyllaDB is a downloadable plugin") func scylladbIsDownloadablePlugin() { - #expect(DatabaseType.scylladb.isDownloadablePlugin == true) + #expect(DatabaseType(rawValue: "ScyllaDB").isDownloadablePlugin == true) } @Test("Cassandra included in allCases") func cassandraIncludedInAllCases() { - #expect(DatabaseType.allCases.contains(.cassandra)) + #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "Cassandra"))) } @Test("ScyllaDB included in allCases") func scylladbIncludedInAllCases() { - #expect(DatabaseType.allCases.contains(.scylladb)) + #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "ScyllaDB"))) } } diff --git a/TableProTests/Models/DatabaseTypeMSSQLTests.swift b/TableProTests/Models/DatabaseTypeMSSQLTests.swift index acd5abbd8..20edb3742 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 DatabaseType(rawValue: "SQL Server") properties and methods. // import Foundation @@ -15,43 +15,43 @@ struct DatabaseTypeMSSQLTests { @Test("defaultPort is 1433") func defaultPort() { - #expect(DatabaseType.mssql.defaultPort == 1_433) + #expect(DatabaseType(rawValue: "SQL Server").defaultPort == 1_433) } @Test("rawValue is SQL Server") func rawValue() { - #expect(DatabaseType.mssql.rawValue == "SQL Server") + #expect(DatabaseType(rawValue: "SQL Server").rawValue == "SQL Server") } @Test("requiresAuthentication is true") func requiresAuthentication() { - #expect(DatabaseType.mssql.requiresAuthentication == true) + #expect(DatabaseType(rawValue: "SQL Server").requiresAuthentication == true) } @Test("supportsForeignKeys is true") func supportsForeignKeys() { - #expect(DatabaseType.mssql.supportsForeignKeys == true) + #expect(DatabaseType(rawValue: "SQL Server").supportsForeignKeys == true) } @Test("supportsSchemaEditing is true") func supportsSchemaEditing() { - #expect(DatabaseType.mssql.supportsSchemaEditing == true) + #expect(DatabaseType(rawValue: "SQL Server").supportsSchemaEditing == true) } @Test("iconName is mssql-icon") func iconName() { - #expect(DatabaseType.mssql.iconName == "mssql-icon") + #expect(DatabaseType(rawValue: "SQL Server").iconName == "mssql-icon") } // MARK: - allKnownTypes Tests @Test("allKnownTypes contains mssql") func allKnownTypesContainsMSSql() { - #expect(DatabaseType.allKnownTypes.contains(.mssql)) + #expect(DatabaseType.allKnownTypes.contains(DatabaseType(rawValue: "SQL Server"))) } @Test("allCases shim contains mssql") func allCasesContainsMSSql() { - #expect(DatabaseType.allCases.contains(.mssql)) + #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "SQL Server"))) } } diff --git a/TableProTests/Models/DatabaseTypeRedisTests.swift b/TableProTests/Models/DatabaseTypeRedisTests.swift index 5ace3b5b6..bd647a3b2 100644 --- a/TableProTests/Models/DatabaseTypeRedisTests.swift +++ b/TableProTests/Models/DatabaseTypeRedisTests.swift @@ -5,46 +5,46 @@ import Testing struct DatabaseTypeRedisTests { @Test("Default port is 6379") func defaultPort() { - #expect(DatabaseType.redis.defaultPort == 6_379) + #expect(DatabaseType(rawValue: "Redis").defaultPort == 6_379) } @Test("Icon name is redis-icon") func iconName() { - #expect(DatabaseType.redis.iconName == "redis-icon") + #expect(DatabaseType(rawValue: "Redis").iconName == "redis-icon") } @Test("Does not require authentication") func requiresAuthentication() { - #expect(DatabaseType.redis.requiresAuthentication == false) + #expect(DatabaseType(rawValue: "Redis").requiresAuthentication == false) } @Test("Does not support foreign keys") func supportsForeignKeys() { - #expect(DatabaseType.redis.supportsForeignKeys == false) + #expect(DatabaseType(rawValue: "Redis").supportsForeignKeys == false) } @Test("Does not support schema editing") func supportsSchemaEditing() { - #expect(DatabaseType.redis.supportsSchemaEditing == false) + #expect(DatabaseType(rawValue: "Redis").supportsSchemaEditing == false) } @Test("Raw value is Redis") func rawValue() { - #expect(DatabaseType.redis.rawValue == "Redis") + #expect(DatabaseType(rawValue: "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(DatabaseType(rawValue: "Redis").themeColor == PluginManager.shared.brandColor(for: DatabaseType(rawValue: "Redis"))) } @Test("Included in allKnownTypes") func includedInAllKnownTypes() { - #expect(DatabaseType.allKnownTypes.contains(.redis)) + #expect(DatabaseType.allKnownTypes.contains(DatabaseType(rawValue: "Redis"))) } @Test("Included in allCases shim") func includedInAllCases() { - #expect(DatabaseType.allCases.contains(.redis)) + #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "Redis"))) } } diff --git a/TableProTests/Models/DatabaseTypeTests.swift b/TableProTests/Models/DatabaseTypeTests.swift index 57054253e..c8fc8868d 100644 --- a/TableProTests/Models/DatabaseTypeTests.swift +++ b/TableProTests/Models/DatabaseTypeTests.swift @@ -34,7 +34,7 @@ struct DatabaseTypeTests { @Test("MongoDB default port is 27017") func testMongoDBDefaultPort() { - #expect(DatabaseType.mongodb.defaultPort == 27_017) + #expect(DatabaseType(rawValue: "MongoDB").defaultPort == 27_017) } @Test("allKnownTypes count is 13") @@ -52,15 +52,15 @@ struct DatabaseTypeTests { (DatabaseType.mariadb, "MariaDB"), (DatabaseType.postgresql, "PostgreSQL"), (DatabaseType.sqlite, "SQLite"), - (DatabaseType.mongodb, "MongoDB"), - (DatabaseType.redis, "Redis"), + (DatabaseType(rawValue: "MongoDB"), "MongoDB"), + (DatabaseType(rawValue: "Redis"), "Redis"), (DatabaseType.redshift, "Redshift"), - (DatabaseType.mssql, "SQL Server"), - (DatabaseType.oracle, "Oracle"), - (DatabaseType.clickhouse, "ClickHouse"), - (DatabaseType.duckdb, "DuckDB"), - (DatabaseType.cassandra, "Cassandra"), - (DatabaseType.scylladb, "ScyllaDB") + (DatabaseType(rawValue: "SQL Server"), "SQL Server"), + (DatabaseType(rawValue: "Oracle"), "Oracle"), + (DatabaseType(rawValue: "ClickHouse"), "ClickHouse"), + (DatabaseType(rawValue: "DuckDB"), "DuckDB"), + (DatabaseType(rawValue: "Cassandra"), "Cassandra"), + (DatabaseType(rawValue: "ScyllaDB"), "ScyllaDB") ]) func testRawValueMatchesDisplayName(dbType: DatabaseType, expectedRawValue: String) { #expect(dbType.rawValue == expectedRawValue) @@ -70,27 +70,27 @@ struct DatabaseTypeTests { @Test("ClickHouse default port is 8123") func testClickHouseDefaultPort() { - #expect(DatabaseType.clickhouse.defaultPort == 8_123) + #expect(DatabaseType(rawValue: "ClickHouse").defaultPort == 8_123) } @Test("ClickHouse requires authentication") func testClickHouseRequiresAuth() { - #expect(DatabaseType.clickhouse.requiresAuthentication == true) + #expect(DatabaseType(rawValue: "ClickHouse").requiresAuthentication == true) } @Test("ClickHouse does not support foreign keys") func testClickHouseSupportsForeignKeys() { - #expect(DatabaseType.clickhouse.supportsForeignKeys == false) + #expect(DatabaseType(rawValue: "ClickHouse").supportsForeignKeys == false) } @Test("ClickHouse supports schema editing") func testClickHouseSupportsSchemaEditing() { - #expect(DatabaseType.clickhouse.supportsSchemaEditing == true) + #expect(DatabaseType(rawValue: "ClickHouse").supportsSchemaEditing == true) } @Test("ClickHouse icon name is clickhouse-icon") func testClickHouseIconName() { - #expect(DatabaseType.clickhouse.iconName == "clickhouse-icon") + #expect(DatabaseType(rawValue: "ClickHouse").iconName == "clickhouse-icon") } // MARK: - Plugin Type ID Alias Tests @@ -154,6 +154,6 @@ struct DatabaseTypeTests { let types: Set = [.mysql, .postgresql, .sqlite] #expect(types.contains(.mysql)) #expect(types.contains(.postgresql)) - #expect(!types.contains(.redis)) + #expect(!types.contains(DatabaseType(rawValue: "Redis"))) } } diff --git a/TableProTests/Models/TableOperationDialogLogicTests.swift b/TableProTests/Models/TableOperationDialogLogicTests.swift index fff6feecd..c5c48f58f 100644 --- a/TableProTests/Models/TableOperationDialogLogicTests.swift +++ b/TableProTests/Models/TableOperationDialogLogicTests.swift @@ -134,7 +134,7 @@ struct TableOperationDialogLogicTests { @Test("MongoDB does not support cascade") func testMongoDBCascadeNotSupported() { - #expect(DialogLogic.cascadeSupported(databaseType: .mongodb) == false) + #expect(DialogLogic.cascadeSupported(databaseType: DatabaseType(rawValue: "MongoDB")) == false) } // MARK: - cascadeDisabled diff --git a/TableProTests/ViewModels/AIChatViewModelActionTests.swift b/TableProTests/ViewModels/AIChatViewModelActionTests.swift index d2705c208..c997e601f 100644 --- a/TableProTests/ViewModels/AIChatViewModelActionTests.swift +++ b/TableProTests/ViewModels/AIChatViewModelActionTests.swift @@ -33,7 +33,7 @@ struct AIChatViewModelActionTests { @Test("handleFixError with MongoDB connection uses JavaScript language") func fixErrorMongoDBConnection() { let vm = AIChatViewModel() - vm.connection = TestFixtures.makeConnection(type: .mongodb) + vm.connection = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) vm.handleFixError(query: "db.users.find({})", error: "SyntaxError") @@ -46,7 +46,7 @@ struct AIChatViewModelActionTests { @Test("handleFixError with Redis connection uses bash language") func fixErrorRedisConnection() { let vm = AIChatViewModel() - vm.connection = TestFixtures.makeConnection(type: .redis) + vm.connection = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Redis")) vm.handleFixError(query: "GET mykey", error: "WRONGTYPE") diff --git a/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift b/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift index 62e41262b..6571b46f1 100644 --- a/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift +++ b/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift @@ -51,7 +51,7 @@ struct CoordinatorSidebarActionsTests { @Test("createView does not crash for each database type", arguments: [ DatabaseType.mysql, .mariadb, .postgresql, .sqlite, .redshift, - .clickhouse, .mssql, .oracle, .mongodb, .redis, .duckdb, + DatabaseType(rawValue: "ClickHouse"), DatabaseType(rawValue: "SQL Server"), DatabaseType(rawValue: "Oracle"), DatabaseType(rawValue: "MongoDB"), DatabaseType(rawValue: "Redis"), DatabaseType(rawValue: "DuckDB"), ]) @MainActor func createViewDoesNotCrash(type: DatabaseType) { @@ -77,7 +77,7 @@ struct CoordinatorSidebarActionsTests { @Test("openImportDialog with MongoDB returns early at type guard") @MainActor func openImportDialogBlockedForMongoDB() { - let (coordinator, _) = makeCoordinator(type: .mongodb) + let (coordinator, _) = makeCoordinator(type: DatabaseType(rawValue: "MongoDB")) defer { coordinator.teardown() } // Hits the MongoDB/Redis guard; shows an alert as side effect @@ -88,7 +88,7 @@ struct CoordinatorSidebarActionsTests { @Test("openImportDialog with Redis returns early at type guard") @MainActor func openImportDialogBlockedForRedis() { - let (coordinator, _) = makeCoordinator(type: .redis) + let (coordinator, _) = makeCoordinator(type: DatabaseType(rawValue: "Redis")) defer { coordinator.teardown() } coordinator.openImportDialog() From c347c271993c38ee52b23de4b9dd73329f8891e7 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 16 Mar 2026 21:42:43 +0700 Subject: [PATCH 2/3] fix: address all 6 PR review issues for self-describing plugin system --- TablePro/AppDelegate+ConnectionHandler.swift | 4 +- TablePro/AppDelegate+FileOpen.swift | 2 +- TablePro/Core/Database/DatabaseDriver.swift | 8 +- TablePro/Core/Database/DatabaseManager.swift | 2 +- TablePro/Core/Plugins/PluginManager.swift | 9 +- .../Core/Plugins/PluginMetadataRegistry.swift | 453 ++++++++++++++++-- .../Connection/ConnectionURLFormatter.swift | 6 +- .../Connection/ConnectionURLParser.swift | 20 +- .../Connection/DatabaseConnection.swift | 11 + .../ViewModels/QuickSwitcherViewModel.swift | 2 +- TablePro/Views/Editor/HistoryPanelView.swift | 2 +- .../Views/Results/TableRowViewWithMenu.swift | 2 +- .../Views/Structure/TableStructureView.swift | 4 +- .../DataChangeManagerClickHouseTests.swift | 6 +- .../SQLStatementGeneratorMSSQLTests.swift | 4 +- .../SQLStatementGeneratorNoPKTests.swift | 4 +- ...LStatementGeneratorPKRegressionTests.swift | 6 +- ...tatementGeneratorParameterStyleTests.swift | 4 +- .../ClickHouse/ClickHouseDialectTests.swift | 2 +- .../FilterSQLGeneratorMSSQLTests.swift | 2 +- .../Core/Database/MSSQLDriverTests.swift | 2 +- .../Core/Services/SafeModeGuardTests.swift | 8 +- .../TableQueryBuilderFilterTests.swift | 2 +- .../TableQueryBuilderMSSQLTests.swift | 4 +- ...nnectionStorageAdditionalFieldsTests.swift | 16 +- .../ConnectionURLFormatterTests.swift | 2 +- .../ConnectionURLParserMSSQLTests.swift | 12 +- .../Utilities/ConnectionURLParserTests.swift | 18 +- .../Utilities/DatabaseURLSchemeTests.swift | 16 +- .../SQLRowToStatementConverterTests.swift | 6 +- TableProTests/Helpers/TestFixtures.swift | 4 +- ...abaseConnectionAdditionalFieldsTests.swift | 56 +-- .../Models/DatabaseTypeCassandraTests.swift | 36 +- .../Models/DatabaseTypeMSSQLTests.swift | 18 +- .../Models/DatabaseTypeRedisTests.swift | 18 +- TableProTests/Models/DatabaseTypeTests.swift | 30 +- .../TableOperationDialogLogicTests.swift | 2 +- .../AIChatViewModelActionTests.swift | 4 +- .../Main/CoordinatorSidebarActionsTests.swift | 6 +- 39 files changed, 593 insertions(+), 220 deletions(-) diff --git a/TablePro/AppDelegate+ConnectionHandler.swift b/TablePro/AppDelegate+ConnectionHandler.swift index 03d44cd49..0fe711d1e 100644 --- a/TablePro/AppDelegate+ConnectionHandler.swift +++ b/TablePro/AppDelegate+ConnectionHandler.swift @@ -141,7 +141,7 @@ extension AppDelegate { let connectionName = url.deletingPathExtension().lastPathComponent for (sessionId, session) in DatabaseManager.shared.activeSessions { - if session.connection.type == DatabaseType(rawValue: "DuckDB") + if session.connection.type == .duckdb && session.connection.database == filePath && session.driver != nil { bringConnectionWindowToFront(sessionId) @@ -155,7 +155,7 @@ extension AppDelegate { port: 0, database: filePath, username: "", - type: DatabaseType(rawValue: "DuckDB") + type: .duckdb ) openNewConnectionWindow(for: connection) diff --git a/TablePro/AppDelegate+FileOpen.swift b/TablePro/AppDelegate+FileOpen.swift index c2e8253b7..25c66fdbc 100644 --- a/TablePro/AppDelegate+FileOpen.swift +++ b/TablePro/AppDelegate+FileOpen.swift @@ -66,7 +66,7 @@ extension AppDelegate { switch dbType { case .sqlite: self.handleSQLiteFile(url) - case DatabaseType(rawValue: "DuckDB"): + case .duckdb: self.handleDuckDBFile(url) default: self.handleGenericDatabaseFile(url, type: dbType) diff --git a/TablePro/Core/Database/DatabaseDriver.swift b/TablePro/Core/Database/DatabaseDriver.swift index 69dda2b1a..b06e781e1 100644 --- a/TablePro/Core/Database/DatabaseDriver.swift +++ b/TablePro/Core/Database/DatabaseDriver.swift @@ -362,15 +362,15 @@ enum DatabaseDriverFactory { } switch connection.type { - case DatabaseType(rawValue: "MongoDB"): + case .mongodb: fields["sslCACertPath"] = ssl.caCertificatePath fields["mongoReadPreference"] = connection.mongoReadPreference ?? "" fields["mongoWriteConcern"] = connection.mongoWriteConcern ?? "" - case DatabaseType(rawValue: "Redis"): + case .redis: fields["redisDatabase"] = String(connection.redisDatabase ?? 0) - case DatabaseType(rawValue: "SQL Server"): + case .mssql: fields["mssqlSchema"] = connection.mssqlSchema ?? "dbo" - case DatabaseType(rawValue: "Oracle"): + case .oracle: fields["oracleServiceName"] = connection.oracleServiceName ?? "" default: break diff --git a/TablePro/Core/Database/DatabaseManager.swift b/TablePro/Core/Database/DatabaseManager.swift index ca402a81c..530c3c794 100644 --- a/TablePro/Core/Database/DatabaseManager.swift +++ b/TablePro/Core/Database/DatabaseManager.swift @@ -803,7 +803,7 @@ final class DatabaseManager { driver: DatabaseDriver ) async -> String? { // Only needed for PostgreSQL PK modifications - guard databaseType == .postgresql || databaseType == .redshift || databaseType == DatabaseType(rawValue: "DuckDB") else { return nil } + guard databaseType == .postgresql || databaseType == .redshift || databaseType == .duckdb else { return nil } guard changes.contains(where: { if case .modifyPrimaryKey = $0 { return true } diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 35c4340fd..7d194ec37 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -258,14 +258,11 @@ final class PluginManager { driverPlugins[additionalId] = driver } - // Self-register plugin metadata from the DriverPlugin protocol - let driverInstance = driver.createDriver(config: DriverConnectionConfig( - host: "", port: 0, username: "", password: "", database: "" - )) + // 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, - parameterStyle: driverInstance.parameterStyle + isDownloadable: driverType.isDownloadable ) PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: typeId) for additionalId in driverType.additionalDatabaseTypeIds { diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry.swift b/TablePro/Core/Plugins/PluginMetadataRegistry.swift index 85eea8346..a35d8214e 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry.swift @@ -131,11 +131,376 @@ final class PluginMetadataRegistry: @unchecked Sendable { registerBuiltInDefaults() } + // swiftlint:disable function_body_length private func registerBuiltInDefaults() { - // Built-in plugins (MySQL, MariaDB, PostgreSQL, Redshift, SQLite) self-register - // their metadata at load time via buildMetadataSnapshot() in PluginManager.registerCapabilities(). - // Only registry plugin defaults (for downloadable plugins not yet installed) are pre-populated here. - for entry in registryPluginDefaults() { + let mysqlDialect = SQLDialectDescriptor( + identifierQuote: "`", + keywords: [ + "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", + "ON", "USING", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN", "AS", "ALIAS", + "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", + "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", + "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "DATABASE", "SCHEMA", + "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", + "ADD", "MODIFY", "CHANGE", "COLUMN", "RENAME", + "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", "ANY", "SOME", + "CASE", "WHEN", "THEN", "ELSE", "END", "IF", "IFNULL", "COALESCE", + "UNION", "INTERSECT", "EXCEPT", + "FORCE", "USE", "IGNORE", "STRAIGHT_JOIN", "DUAL", + "SHOW", "DESCRIBE", "EXPLAIN" + ], + functions: [ + "COUNT", "SUM", "AVG", "MAX", "MIN", "GROUP_CONCAT", + "CONCAT", "SUBSTRING", "LEFT", "RIGHT", "LENGTH", "LOWER", "UPPER", + "TRIM", "LTRIM", "RTRIM", "REPLACE", + "NOW", "CURDATE", "CURTIME", "DATE", "TIME", "YEAR", "MONTH", "DAY", + "DATE_ADD", "DATE_SUB", "DATEDIFF", "TIMESTAMPDIFF", + "ROUND", "CEIL", "FLOOR", "ABS", "MOD", "POW", "SQRT", + "CAST", "CONVERT" + ], + dataTypes: [ + "INT", "INTEGER", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", + "DECIMAL", "NUMERIC", "FLOAT", "DOUBLE", "REAL", + "CHAR", "VARCHAR", "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", + "BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", + "DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR", + "ENUM", "SET", "JSON", "BOOL", "BOOLEAN" + ], + tableOptions: [ + "ENGINE=InnoDB", "DEFAULT CHARSET=utf8mb4", "COLLATE=utf8mb4_unicode_ci", + "AUTO_INCREMENT=", "COMMENT=", "ROW_FORMAT=" + ], + regexSyntax: .regexp, + booleanLiteralStyle: .numeric, + likeEscapeStyle: .implicit, + paginationStyle: .limit, + requiresBackslashEscaping: true + ) + + let mysqlColumnTypes: [String: [String]] = [ + "Integer": ["TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT"], + "Float": ["FLOAT", "DOUBLE", "DECIMAL", "NUMERIC", "REAL"], + "String": ["CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT", "ENUM", "SET"], + "Date": ["DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR"], + "Binary": ["BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB", "BIT"], + "Boolean": ["BOOLEAN", "BOOL"], + "JSON": ["JSON"], + "Spatial": ["GEOMETRY", "POINT", "LINESTRING", "POLYGON"] + ] + + let postgresqlDialect = SQLDialectDescriptor( + identifierQuote: "\"", + keywords: [ + "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", "FULL", + "ON", "USING", "AND", "OR", "NOT", "IN", "LIKE", "ILIKE", "BETWEEN", "AS", + "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", "FETCH", "FIRST", "ROWS", "ONLY", + "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", + "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "DATABASE", "SCHEMA", + "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", + "ADD", "MODIFY", "COLUMN", "RENAME", + "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", "ANY", "SOME", + "CASE", "WHEN", "THEN", "ELSE", "END", "COALESCE", "NULLIF", + "UNION", "INTERSECT", "EXCEPT", + "RETURNING", "WITH", "RECURSIVE", "MATERIALIZED", + "EXPLAIN", "ANALYZE", "VERBOSE", + "WINDOW", "OVER", "PARTITION", + "LATERAL", "ORDINALITY" + ], + functions: [ + "COUNT", "SUM", "AVG", "MAX", "MIN", "STRING_AGG", "ARRAY_AGG", + "CONCAT", "SUBSTRING", "LEFT", "RIGHT", "LENGTH", "LOWER", "UPPER", + "TRIM", "LTRIM", "RTRIM", "REPLACE", "SPLIT_PART", + "NOW", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", + "DATE_TRUNC", "EXTRACT", "AGE", "TO_CHAR", "TO_DATE", + "ROUND", "CEIL", "CEILING", "FLOOR", "ABS", "MOD", "POW", "POWER", "SQRT", + "CAST", "TO_NUMBER", "TO_TIMESTAMP", + "JSON_BUILD_OBJECT", "JSON_AGG", "JSONB_BUILD_OBJECT" + ], + dataTypes: [ + "INTEGER", "INT", "SMALLINT", "BIGINT", "SERIAL", "BIGSERIAL", "SMALLSERIAL", + "DECIMAL", "NUMERIC", "REAL", "DOUBLE", "PRECISION", + "CHAR", "CHARACTER", "VARCHAR", "TEXT", + "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL", + "BOOLEAN", "BOOL", "JSON", "JSONB", "UUID", "BYTEA", "ARRAY" + ], + tableOptions: [ + "INHERITS", "PARTITION BY", "TABLESPACE", "WITH", "WITHOUT OIDS" + ], + regexSyntax: .tilde, + booleanLiteralStyle: .truefalse, + likeEscapeStyle: .explicit, + paginationStyle: .limit + ) + + let postgresqlColumnTypes: [String: [String]] = [ + "Integer": ["SMALLINT", "INTEGER", "BIGINT", "SERIAL", "BIGSERIAL", "SMALLSERIAL"], + "Float": ["REAL", "DOUBLE PRECISION", "NUMERIC", "DECIMAL", "MONEY"], + "String": ["CHARACTER VARYING", "VARCHAR", "CHARACTER", "CHAR", "TEXT", "NAME"], + "Date": [ + "DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "INTERVAL", + "TIME WITH TIME ZONE", "TIMESTAMP WITH TIME ZONE" + ], + "Binary": ["BYTEA"], + "Boolean": ["BOOLEAN"], + "JSON": ["JSON", "JSONB"], + "UUID": ["UUID"], + "Array": ["ARRAY"], + "Network": ["INET", "CIDR", "MACADDR", "MACADDR8"], + "Geometric": ["POINT", "LINE", "LSEG", "BOX", "PATH", "POLYGON", "CIRCLE"], + "Range": ["INT4RANGE", "INT8RANGE", "NUMRANGE", "TSRANGE", "TSTZRANGE", "DATERANGE"], + "Text Search": ["TSVECTOR", "TSQUERY"], + "XML": ["XML"] + ] + + let sqliteDialect = SQLDialectDescriptor( + identifierQuote: "`", + keywords: [ + "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", + "ON", "AND", "OR", "NOT", "IN", "LIKE", "GLOB", "BETWEEN", "AS", + "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", + "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", + "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "TRIGGER", + "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", + "ADD", "COLUMN", "RENAME", + "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", + "CASE", "WHEN", "THEN", "ELSE", "END", "COALESCE", "IFNULL", "NULLIF", + "UNION", "INTERSECT", "EXCEPT", + "AUTOINCREMENT", "WITHOUT", "ROWID", "PRAGMA", + "REPLACE", "ABORT", "FAIL", "IGNORE", "ROLLBACK", + "TEMP", "TEMPORARY", "VACUUM", "EXPLAIN", "QUERY", "PLAN" + ], + functions: [ + "COUNT", "SUM", "AVG", "MAX", "MIN", "GROUP_CONCAT", "TOTAL", + "LENGTH", "SUBSTR", "SUBSTRING", "LOWER", "UPPER", "TRIM", "LTRIM", "RTRIM", + "REPLACE", "INSTR", "PRINTF", + "DATE", "TIME", "DATETIME", "JULIANDAY", "STRFTIME", + "ABS", "ROUND", "RANDOM", + "CAST", "TYPEOF", + "COALESCE", "IFNULL", "NULLIF", "HEX", "QUOTE" + ], + dataTypes: [ + "INTEGER", "REAL", "TEXT", "BLOB", "NUMERIC", + "INT", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", + "UNSIGNED", "BIG", "INT2", "INT8", + "CHARACTER", "VARCHAR", "VARYING", "NCHAR", "NATIVE", + "NVARCHAR", "CLOB", + "DOUBLE", "PRECISION", "FLOAT", + "DECIMAL", "BOOLEAN", "DATE", "DATETIME" + ], + tableOptions: [ + "WITHOUT ROWID", "STRICT" + ], + regexSyntax: .unsupported, + booleanLiteralStyle: .numeric, + likeEscapeStyle: .explicit, + paginationStyle: .limit + ) + + let sqliteColumnTypes: [String: [String]] = [ + "Integer": ["INTEGER", "INT", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT"], + "Float": ["REAL", "DOUBLE", "FLOAT", "NUMERIC", "DECIMAL"], + "String": ["TEXT", "VARCHAR", "CHARACTER", "CHAR", "CLOB", "NVARCHAR", "NCHAR"], + "Date": ["DATE", "TIME", "DATETIME", "TIMESTAMP"], + "Binary": ["BLOB"], + "Boolean": ["BOOLEAN"] + ] + + let pgpassField = ConnectionField( + id: "usePgpass", + label: String(localized: "Use ~/.pgpass"), + defaultValue: "false", + fieldType: .toggle, + section: .authentication, + hidesPassword: true + ) + + let defaults: [(typeId: String, snapshot: PluginMetadataSnapshot)] = [ + ("MySQL", PluginMetadataSnapshot( + displayName: "MySQL", iconName: "mysql-icon", defaultPort: 3_306, + requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true, + isDownloadable: false, primaryUrlScheme: "mysql", parameterStyle: .questionMark, + navigationModel: .standard, explainVariants: [], pathFieldRole: .database, + supportsHealthMonitor: true, urlSchemes: ["mysql"], postConnectActions: [], + brandColorHex: "#FF9500", + queryLanguageName: "SQL", editorLanguage: .sql, + connectionMode: .network, supportsDatabaseSwitching: true, + capabilities: .defaults, + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: "public", + defaultGroupName: "main", + tableEntityName: "Tables", + defaultPrimaryKeyColumn: nil, + immutableColumns: [], + systemDatabaseNames: ["information_schema", "mysql", "performance_schema", "sys"], + systemSchemaNames: [], + fileExtensions: [], + databaseGroupingStrategy: .byDatabase, + structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: mysqlDialect, + statementCompletions: [], + columnTypesByCategory: mysqlColumnTypes + ), + connection: .defaults + )), + ("MariaDB", PluginMetadataSnapshot( + displayName: "MariaDB", iconName: "mariadb-icon", defaultPort: 3_306, + requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true, + isDownloadable: false, primaryUrlScheme: "mariadb", parameterStyle: .questionMark, + navigationModel: .standard, explainVariants: [], pathFieldRole: .database, + supportsHealthMonitor: true, urlSchemes: ["mariadb"], postConnectActions: [], + brandColorHex: "#00B4D8", + queryLanguageName: "SQL", editorLanguage: .sql, + connectionMode: .network, supportsDatabaseSwitching: true, + capabilities: .defaults, + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: "public", + defaultGroupName: "main", + tableEntityName: "Tables", + defaultPrimaryKeyColumn: nil, + immutableColumns: [], + systemDatabaseNames: ["information_schema", "mysql", "performance_schema", "sys"], + systemSchemaNames: [], + fileExtensions: [], + databaseGroupingStrategy: .byDatabase, + structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: mysqlDialect, + statementCompletions: [], + columnTypesByCategory: mysqlColumnTypes + ), + connection: .defaults + )), + ("PostgreSQL", PluginMetadataSnapshot( + displayName: "PostgreSQL", iconName: "postgresql-icon", defaultPort: 5_432, + requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true, + isDownloadable: false, primaryUrlScheme: "postgresql", parameterStyle: .dollar, + navigationModel: .standard, explainVariants: [], pathFieldRole: .database, + supportsHealthMonitor: true, urlSchemes: ["postgresql", "postgres"], postConnectActions: [], + brandColorHex: "#336791", + queryLanguageName: "SQL", editorLanguage: .sql, + connectionMode: .network, supportsDatabaseSwitching: true, + capabilities: PluginMetadataSnapshot.CapabilityFlags( + supportsSchemaSwitching: true, + supportsImport: true, + supportsExport: true, + supportsSSH: true, + supportsSSL: true, + supportsCascadeDrop: true, + supportsForeignKeyDisable: false, + supportsReadOnlyMode: true, + supportsQueryProgress: false, + requiresReconnectForDatabaseSwitch: true + ), + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: "public", + defaultGroupName: "main", + tableEntityName: "Tables", + defaultPrimaryKeyColumn: nil, + immutableColumns: [], + systemDatabaseNames: ["postgres", "template0", "template1"], + systemSchemaNames: [], + fileExtensions: [], + databaseGroupingStrategy: .bySchema, + structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: postgresqlDialect, + statementCompletions: [], + columnTypesByCategory: postgresqlColumnTypes + ), + connection: PluginMetadataSnapshot.ConnectionConfig( + additionalConnectionFields: [pgpassField] + ) + )), + ("Redshift", PluginMetadataSnapshot( + displayName: "Redshift", iconName: "redshift-icon", defaultPort: 5_439, + requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: false, + isDownloadable: false, primaryUrlScheme: "redshift", parameterStyle: .dollar, + navigationModel: .standard, explainVariants: [], pathFieldRole: .database, + supportsHealthMonitor: true, urlSchemes: ["redshift"], postConnectActions: [], + brandColorHex: "#205B8E", + queryLanguageName: "SQL", editorLanguage: .sql, + connectionMode: .network, supportsDatabaseSwitching: true, + capabilities: PluginMetadataSnapshot.CapabilityFlags( + supportsSchemaSwitching: true, + supportsImport: true, + supportsExport: true, + supportsSSH: true, + supportsSSL: true, + supportsCascadeDrop: true, + supportsForeignKeyDisable: false, + supportsReadOnlyMode: true, + supportsQueryProgress: false, + requiresReconnectForDatabaseSwitch: true + ), + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: "public", + defaultGroupName: "main", + tableEntityName: "Tables", + defaultPrimaryKeyColumn: nil, + immutableColumns: [], + systemDatabaseNames: ["postgres", "template0", "template1"], + systemSchemaNames: [], + fileExtensions: [], + databaseGroupingStrategy: .bySchema, + structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: postgresqlDialect, + statementCompletions: [], + columnTypesByCategory: postgresqlColumnTypes + ), + connection: PluginMetadataSnapshot.ConnectionConfig( + additionalConnectionFields: [pgpassField] + ) + )), + ("SQLite", PluginMetadataSnapshot( + displayName: "SQLite", iconName: "sqlite-icon", defaultPort: 0, + requiresAuthentication: false, supportsForeignKeys: true, supportsSchemaEditing: true, + isDownloadable: false, primaryUrlScheme: "sqlite", parameterStyle: .questionMark, + navigationModel: .standard, explainVariants: [], pathFieldRole: .filePath, + supportsHealthMonitor: false, urlSchemes: ["sqlite"], postConnectActions: [], + brandColorHex: "#003B57", + queryLanguageName: "SQL", editorLanguage: .sql, + connectionMode: .fileBased, supportsDatabaseSwitching: false, + capabilities: PluginMetadataSnapshot.CapabilityFlags( + supportsSchemaSwitching: false, + supportsImport: true, + supportsExport: true, + supportsSSH: false, + supportsSSL: false, + supportsCascadeDrop: false, + supportsForeignKeyDisable: true, + supportsReadOnlyMode: true, + supportsQueryProgress: false, + requiresReconnectForDatabaseSwitch: false + ), + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: "public", + defaultGroupName: "main", + tableEntityName: "Tables", + defaultPrimaryKeyColumn: nil, + immutableColumns: [], + systemDatabaseNames: [], + systemSchemaNames: [], + fileExtensions: ["db", "sqlite", "sqlite3"], + databaseGroupingStrategy: .flat, + structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: sqliteDialect, + statementCompletions: [], + columnTypesByCategory: sqliteColumnTypes + ), + connection: .defaults + )) + ] + // swiftlint:enable function_body_length + let allDefaults = defaults + registryPluginDefaults() + for entry in allDefaults { snapshots[entry.typeId] = entry.snapshot for scheme in entry.snapshot.urlSchemes { schemeIndex[scheme.lowercased()] = entry.typeId @@ -179,8 +544,48 @@ final class PluginMetadataRegistry: @unchecked Sendable { return schemeIndex[scheme.lowercased()] } + func databaseType(forUrlScheme scheme: String) -> DatabaseType? { + guard let typeId = typeId(forUrlScheme: scheme) else { return nil } + 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() + reverseTypeIndex[aliasTypeId] = primaryTypeId + lock.unlock() + } + + /// 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, @@ -245,11 +650,6 @@ final class PluginMetadataRegistry: @unchecked Sendable { ) } - func databaseType(forUrlScheme scheme: String) -> DatabaseType? { - guard let typeId = typeId(forUrlScheme: scheme) else { return nil } - return DatabaseType(rawValue: typeId) - } - func allFileExtensions() -> [String: String] { lock.lock() defer { lock.unlock() } @@ -270,39 +670,4 @@ final class PluginMetadataRegistry: @unchecked Sendable { defer { lock.unlock() } return schemeIndex } - - // MARK: - Type Registration Helpers - - /// 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() - reverseTypeIndex[aliasTypeId] = primaryTypeId - lock.unlock() - } - - /// Returns all registered type IDs. - func allRegisteredTypeIds() -> [String] { - lock.lock() - defer { lock.unlock() } - return Array(snapshots.keys) - } - - /// Resolves a database type raw value to its plugin type ID. - /// For multi-type plugins (MySQL serves MariaDB), maps the alias to the primary. - func pluginTypeId(for rawValue: String) -> String { - lock.lock() - defer { lock.unlock() } - if snapshots[rawValue] != nil { - return reverseTypeIndex[rawValue] ?? rawValue - } - return reverseTypeIndex[rawValue] ?? rawValue - } - - /// Returns whether the given type ID is registered. - func hasType(_ typeId: String) -> Bool { - lock.lock() - defer { lock.unlock() } - return snapshots[typeId] != nil - } } diff --git a/TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift b/TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift index 3ee78be10..b1ebac97d 100644 --- a/TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift +++ b/TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift @@ -13,7 +13,7 @@ struct ConnectionURLFormatter { return formatSQLite(connection.database) } - if connection.type == DatabaseType(rawValue: "DuckDB") { + if connection.type == .duckdb { return formatDuckDB(connection.database) } @@ -76,7 +76,7 @@ struct ConnectionURLFormatter { result += ":\(connection.port)" } - let sshPathComponent = connection.type == DatabaseType(rawValue: "Oracle") + let sshPathComponent = connection.type == .oracle ? (connection.oracleServiceName ?? connection.database) : connection.database result += "/\(sshPathComponent)" @@ -109,7 +109,7 @@ struct ConnectionURLFormatter { result += ":\(connection.port)" } - let pathComponent = connection.type == DatabaseType(rawValue: "Oracle") + let pathComponent = connection.type == .oracle ? (connection.oracleServiceName ?? connection.database) : connection.database result += "/\(pathComponent)" diff --git a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift index ba0a92381..2857b58f1 100644 --- a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift +++ b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift @@ -101,19 +101,19 @@ struct ConnectionURLParser { case "sqlite": dbType = .sqlite case "mongodb", "mongodb+srv": - dbType = DatabaseType(rawValue: "MongoDB") + dbType = .mongodb case "redis", "rediss": - dbType = DatabaseType(rawValue: "Redis") + dbType = .redis case "sqlserver", "mssql", "jdbc:sqlserver": - dbType = DatabaseType(rawValue: "SQL Server") + dbType = .mssql case "oracle", "jdbc:oracle:thin": - dbType = DatabaseType(rawValue: "Oracle") + dbType = .oracle case "clickhouse", "ch": - dbType = DatabaseType(rawValue: "ClickHouse") + dbType = .clickhouse case "cassandra", "cql": - dbType = DatabaseType(rawValue: "Cassandra") + dbType = .cassandra case "scylladb", "scylla": - dbType = DatabaseType(rawValue: "ScyllaDB") + dbType = .scylladb default: if let resolvedType = PluginMetadataRegistry.shared.databaseType(forUrlScheme: scheme) { dbType = resolvedType @@ -186,7 +186,7 @@ struct ConnectionURLParser { var sslMode = ext.sslMode // Redis-specific: parse database index from path and handle TLS scheme var redisDatabase: Int? - if dbType == DatabaseType(rawValue: "Redis") { + if dbType == .redis { if !database.isEmpty { redisDatabase = Int(database) database = "" @@ -198,7 +198,7 @@ struct ConnectionURLParser { // Oracle-specific: path component is the service name, not the database name var oracleServiceName: String? - if dbType == DatabaseType(rawValue: "Oracle") && !database.isEmpty { + if dbType == .oracle && !database.isEmpty { oracleServiceName = database database = "" } @@ -329,7 +329,7 @@ struct ConnectionURLParser { // Oracle-specific: path component is the service name, not the database name var oracleServiceName: String? - if dbType == DatabaseType(rawValue: "Oracle") && !database.isEmpty { + if dbType == .oracle && !database.isEmpty { oracleServiceName = database database = "" } diff --git a/TablePro/Models/Connection/DatabaseConnection.swift b/TablePro/Models/Connection/DatabaseConnection.swift index a02cbe5d8..d6b61ac86 100644 --- a/TablePro/Models/Connection/DatabaseConnection.swift +++ b/TablePro/Models/Connection/DatabaseConnection.swift @@ -225,6 +225,17 @@ extension DatabaseType { 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") + static let oracle = DatabaseType(rawValue: "Oracle") + static let clickhouse = DatabaseType(rawValue: "ClickHouse") + static let duckdb = DatabaseType(rawValue: "DuckDB") + static let cassandra = DatabaseType(rawValue: "Cassandra") + static let scylladb = DatabaseType(rawValue: "ScyllaDB") + static let etcd = DatabaseType(rawValue: "etcd") } extension DatabaseType: Codable { diff --git a/TablePro/ViewModels/QuickSwitcherViewModel.swift b/TablePro/ViewModels/QuickSwitcherViewModel.swift index 53e4acee6..e5e0cc844 100644 --- a/TablePro/ViewModels/QuickSwitcherViewModel.swift +++ b/TablePro/ViewModels/QuickSwitcherViewModel.swift @@ -87,7 +87,7 @@ internal final class QuickSwitcherViewModel { } // Schemas (only for databases that support them) - let supportsSchemas = [DatabaseType.postgresql, .redshift, DatabaseType(rawValue: "Oracle"), DatabaseType(rawValue: "SQL Server")] + let supportsSchemas = [DatabaseType.postgresql, .redshift, .oracle, .mssql] if supportsSchemas.contains(databaseType) { do { let schemas = try await driver.fetchSchemas() diff --git a/TablePro/Views/Editor/HistoryPanelView.swift b/TablePro/Views/Editor/HistoryPanelView.swift index cde6b894f..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.") ? DatabaseType(rawValue: "MongoDB") : .mysql + .hasPrefix("db.") ? .mongodb : .mysql ) .background(Color(nsColor: ThemeEngine.shared.colors.editor.background)) diff --git a/TablePro/Views/Results/TableRowViewWithMenu.swift b/TablePro/Views/Results/TableRowViewWithMenu.swift index 9fb630818..f5a7f2517 100644 --- a/TablePro/Views/Results/TableRowViewWithMenu.swift +++ b/TablePro/Views/Results/TableRowViewWithMenu.swift @@ -69,7 +69,7 @@ final class TableRowViewWithMenu: NSTableRowView { copyAsMenu.addItem(jsonItem) if let dbType = coordinator.databaseType, - dbType != DatabaseType(rawValue: "MongoDB") && dbType != DatabaseType(rawValue: "Redis"), + dbType != .mongodb && dbType != .redis, coordinator.tableName != nil { copyAsMenu.addItem(NSMenuItem.separator()) diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index ca795d520..96d842d84 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -108,7 +108,7 @@ struct TableStructureView: View { if !connection.type.supportsForeignKeys { tabs = tabs.filter { $0 != .foreignKeys } } - if connection.type != DatabaseType(rawValue: "ClickHouse") { + if connection.type != .clickhouse { tabs = tabs.filter { $0 != .parts } } return tabs @@ -223,7 +223,7 @@ struct TableStructureView: View { } private func updateColumn(_ column: inout EditableColumnDefinition, at index: Int, with value: String) { - if connection.type == DatabaseType(rawValue: "ClickHouse") { + if connection.type == .clickhouse { // ClickHouse: Name(0), Type(1), Nullable(2), Default(3), Comment(4) — no Auto Inc switch index { case 0: column.name = value diff --git a/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift b/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift index d72efaabb..0ee3df4ee 100644 --- a/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift +++ b/TableProTests/Core/ChangeTracking/DataChangeManagerClickHouseTests.swift @@ -20,7 +20,7 @@ struct DataChangeManagerClickHouseTests { tableName: "users", columns: ["id", "name"], primaryKeyColumn: "id", - databaseType: DatabaseType(rawValue: "ClickHouse") + databaseType: .clickhouse ) manager.recordCellChange( @@ -49,7 +49,7 @@ struct DataChangeManagerClickHouseTests { tableName: "events", columns: ["id", "status"], primaryKeyColumn: "id", - databaseType: DatabaseType(rawValue: "ClickHouse") + databaseType: .clickhouse ) manager.recordCellChange( @@ -100,7 +100,7 @@ struct DataChangeManagerClickHouseTests { tableName: "logs", columns: ["timestamp", "message"], primaryKeyColumn: nil, - databaseType: DatabaseType(rawValue: "ClickHouse") + databaseType: .clickhouse ) manager.recordCellChange( diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift index fa048fc57..73994106c 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift @@ -2,7 +2,7 @@ // SQLStatementGeneratorMSSQLTests.swift // TableProTests // -// Tests for SQLStatementGenerator with databaseType: DatabaseType(rawValue: "SQL Server") +// Tests for SQLStatementGenerator with databaseType: .mssql // import Foundation @@ -22,7 +22,7 @@ struct SQLStatementGeneratorMSSQLTests { tableName: tableName, columns: columns, primaryKeyColumn: primaryKeyColumn, - databaseType: DatabaseType(rawValue: "SQL Server") + databaseType: .mssql ) } diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift index e65abf948..0a1f689eb 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift @@ -156,7 +156,7 @@ struct SQLStatementGeneratorNoPKTests { @Test("Update without PK — MSSQL uses UPDATE TOP (1)") func testUpdateNoPKMSSQLTop() { - let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) + let generator = makeGenerator(databaseType: .mssql) let changes: [RowChange] = [ RowChange( rowIndex: 0, @@ -266,7 +266,7 @@ struct SQLStatementGeneratorNoPKTests { @Test("Delete without PK — MSSQL uses DELETE TOP (1)") func testDeleteNoPKMSSQLTop() { - let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) + let generator = makeGenerator(databaseType: .mssql) let changes: [RowChange] = [ RowChange( rowIndex: 0, diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift index 1bc4cdd38..1ecee048b 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift @@ -103,7 +103,7 @@ struct SQLStatementGeneratorPKRegressionTests { @Test("MSSQL delete with PK uses ? placeholder and PK-only WHERE") func testMSSQLDeleteWithPK() { - let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) + let generator = makeGenerator(databaseType: .mssql) let changes = [makeDeleteChange(rowIndex: 0, originalRow: ["1", "John", "john@test.com"])] let statements = generator.generateStatements( @@ -127,7 +127,7 @@ struct SQLStatementGeneratorPKRegressionTests { @Test("ClickHouse delete with PK uses ALTER TABLE DELETE") func testClickHouseDeleteWithPK() { - let generator = makeGenerator(databaseType: DatabaseType(rawValue: "ClickHouse")) + let generator = makeGenerator(databaseType: .clickhouse) let changes = [makeDeleteChange(rowIndex: 0, originalRow: ["1", "John", "john@test.com"])] let statements = generator.generateStatements( @@ -173,7 +173,7 @@ struct SQLStatementGeneratorPKRegressionTests { @Test("MSSQL update with PK uses PK-only WHERE") func testMSSQLUpdateWithPK() { - let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) + let generator = makeGenerator(databaseType: .mssql) let changes = [makeUpdateChange( rowIndex: 0, columnIndex: 1, columnName: "name", oldValue: "John", newValue: "Jane", originalRow: ["1", "John", "john@test.com"] diff --git a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift index d2f17d776..1f8c4364d 100644 --- a/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift +++ b/TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift @@ -72,7 +72,7 @@ struct SQLStatementGeneratorParameterStyleTests { @Test("DuckDB defaults to dollar style") func testDuckDBDefaultsDollar() { - let generator = makeGenerator(databaseType: DatabaseType(rawValue: "DuckDB")) + let generator = makeGenerator(databaseType: .duckdb) let insertedRowData: [Int: [String?]] = [0: ["1", "John", "john@example.com"]] let changes: [RowChange] = [ RowChange(rowIndex: 0, type: .insert, cellChanges: [], originalRow: nil) @@ -125,7 +125,7 @@ struct SQLStatementGeneratorParameterStyleTests { @Test("MSSQL defaults to questionMark style") func testMSSQLDefaultsQuestionMark() { - let generator = makeGenerator(databaseType: DatabaseType(rawValue: "SQL Server")) + let generator = makeGenerator(databaseType: .mssql) let insertedRowData: [Int: [String?]] = [0: ["1", "John", "john@example.com"]] let changes: [RowChange] = [ RowChange(rowIndex: 0, type: .insert, cellChanges: [], originalRow: nil) diff --git a/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift b/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift index a7b90484c..ec37f1938 100644 --- a/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift +++ b/TableProTests/Core/ClickHouse/ClickHouseDialectTests.swift @@ -32,7 +32,7 @@ struct ClickHouseDialectTests { @Test("Factory returns empty dialect when plugin not loaded") @MainActor func testFactoryFallbackWithoutPlugin() { - let dialect = SQLDialectFactory.createDialect(for: DatabaseType(rawValue: "ClickHouse")) + let dialect = SQLDialectFactory.createDialect(for: .clickhouse) // Without plugin loaded, factory returns empty fallback #expect(dialect.keywords.isEmpty) } diff --git a/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift b/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift index cdc775823..e5b73df35 100644 --- a/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift +++ b/TableProTests/Core/Database/FilterSQLGeneratorMSSQLTests.swift @@ -2,7 +2,7 @@ // FilterSQLGeneratorMSSQLTests.swift // TableProTests // -// Tests for FilterSQLGenerator with databaseType: DatabaseType(rawValue: "SQL Server") +// Tests for FilterSQLGenerator with databaseType: .mssql // import Foundation diff --git a/TableProTests/Core/Database/MSSQLDriverTests.swift b/TableProTests/Core/Database/MSSQLDriverTests.swift index 4d48a9af0..3da327f94 100644 --- a/TableProTests/Core/Database/MSSQLDriverTests.swift +++ b/TableProTests/Core/Database/MSSQLDriverTests.swift @@ -75,7 +75,7 @@ struct MSSQLDriverTests { // MARK: - Helpers private func makeConnection(mssqlSchema: String? = nil) -> DatabaseConnection { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "SQL Server")) + var conn = TestFixtures.makeConnection(type: .mssql) conn.mssqlSchema = mssqlSchema return conn } diff --git a/TableProTests/Core/Services/SafeModeGuardTests.swift b/TableProTests/Core/Services/SafeModeGuardTests.swift index 550dcafa6..603929b41 100644 --- a/TableProTests/Core/Services/SafeModeGuardTests.swift +++ b/TableProTests/Core/Services/SafeModeGuardTests.swift @@ -70,7 +70,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .readOnly, isWriteOperation: false, sql: "db.users.find({})", operationDescription: "Find", - window: nil, databaseType: DatabaseType(rawValue: "MongoDB") + window: nil, databaseType: .mongodb ) guard case let .blocked(message) = result else { Issue.record("Expected .blocked for MongoDB but got .allowed") @@ -84,7 +84,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .readOnly, isWriteOperation: false, sql: "GET key", operationDescription: "Get", - window: nil, databaseType: DatabaseType(rawValue: "Redis") + window: nil, databaseType: .redis ) guard case let .blocked(message) = result else { Issue.record("Expected .blocked for Redis but got .allowed") @@ -98,7 +98,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .silent, isWriteOperation: false, sql: "db.users.find({})", operationDescription: "Find", - window: nil, databaseType: DatabaseType(rawValue: "MongoDB") + window: nil, databaseType: .mongodb ) if case .blocked = result { Issue.record("Expected .allowed for MongoDB in silent mode but got .blocked") @@ -110,7 +110,7 @@ struct SafeModeGuardTests { let result = await SafeModeGuard.checkPermission( level: .silent, isWriteOperation: false, sql: "GET key", operationDescription: "Get", - window: nil, databaseType: DatabaseType(rawValue: "Redis") + window: nil, databaseType: .redis ) if case .blocked = result { Issue.record("Expected .allowed for Redis in silent mode but got .blocked") diff --git a/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift b/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift index cdfcef8a5..806666755 100644 --- a/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift +++ b/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift @@ -157,7 +157,7 @@ struct TableQueryBuilderPostgreSQLQuickSearchTests { @Suite("Table Query Builder - NoSQL Nil Dialect Fallback") struct TableQueryBuilderNoSQLTests { // MongoDB has no SQL dialect — should produce bare SELECT without WHERE - private let builder = TableQueryBuilder(databaseType: DatabaseType(rawValue: "MongoDB")) + private let builder = TableQueryBuilder(databaseType: .mongodb) @Test("NoSQL type produces no WHERE for filtered query") func noSqlFilteredQueryNoWhere() { diff --git a/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift b/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift index 175b2c289..03181e88f 100644 --- a/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift +++ b/TableProTests/Core/Services/TableQueryBuilderMSSQLTests.swift @@ -2,7 +2,7 @@ // TableQueryBuilderMSSQLTests.swift // TableProTests // -// Tests for TableQueryBuilder with databaseType: DatabaseType(rawValue: "SQL Server") +// Tests for TableQueryBuilder with databaseType: .mssql // import Foundation @@ -11,7 +11,7 @@ import Testing @Suite("Table Query Builder MSSQL") struct TableQueryBuilderMSSQLTests { - private let builder = TableQueryBuilder(databaseType: DatabaseType(rawValue: "SQL Server")) + private let builder = TableQueryBuilder(databaseType: .mssql) // MARK: - Base Query Tests diff --git a/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift b/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift index f3d418788..c502f826e 100644 --- a/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift +++ b/TableProTests/Core/Storage/ConnectionStorageAdditionalFieldsTests.swift @@ -19,7 +19,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test Mongo", host: "localhost", port: 27_017, - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "admin", mongoReadPreference: "secondary", mongoWriteConcern: "majority" @@ -42,7 +42,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test MSSQL", host: "localhost", port: 1_433, - type: DatabaseType(rawValue: "SQL Server"), + type: .mssql, mssqlSchema: "custom_schema" ) @@ -61,7 +61,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test Oracle", host: "localhost", port: 1_521, - type: DatabaseType(rawValue: "Oracle"), + type: .oracle, oracleServiceName: "ORCL" ) @@ -80,7 +80,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Test Redis", host: "localhost", port: 6_379, - type: DatabaseType(rawValue: "Redis"), + type: .redis, redisDatabase: 5 ) @@ -146,7 +146,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Cache Test", host: "localhost", port: 27_017, - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "testdb" ) @@ -172,7 +172,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Mongo", host: "localhost", port: 27_017, - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "admin" ) @@ -182,7 +182,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "Redis", host: "localhost", port: 6_379, - type: DatabaseType(rawValue: "Redis"), + type: .redis, redisDatabase: 3 ) @@ -192,7 +192,7 @@ struct ConnectionStorageAdditionalFieldsTests { name: "MSSQL", host: "localhost", port: 1_433, - type: DatabaseType(rawValue: "SQL Server"), + type: .mssql, mssqlSchema: "dbo" ) diff --git a/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift b/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift index f4197a293..5fe77b28c 100644 --- a/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift +++ b/TableProTests/Core/Utilities/ConnectionURLFormatterTests.swift @@ -69,7 +69,7 @@ struct ConnectionURLFormatterTests { func testDefaultPortOmittedMongoDB() { let conn = DatabaseConnection( name: "", host: "host", port: 27_017, database: "db", - username: "user", type: DatabaseType(rawValue: "MongoDB") + username: "user", type: .mongodb ) let url = ConnectionURLFormatter.format(conn, password: "pass", sshPassword: nil) #expect(!url.contains(":27017")) diff --git a/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift b/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift index 50f1731f2..87638f8c3 100644 --- a/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift +++ b/TableProTests/Core/Utilities/ConnectionURLParserMSSQLTests.swift @@ -16,7 +16,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) #expect(parsed.host == "host") #expect(parsed.port == nil) #expect(parsed.database == "mydb") @@ -30,7 +30,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) #expect(parsed.host == "host") #expect(parsed.database == "db") #expect(parsed.username == "user") @@ -43,7 +43,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) #expect(parsed.host == "host") #expect(parsed.username == "user") } @@ -54,7 +54,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) #expect(parsed.host == "host") #expect(parsed.database == "db") #expect(parsed.username == "") @@ -67,7 +67,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) #expect(parsed.port == 1434) #expect(parsed.host == "host") #expect(parsed.database == "db") @@ -79,7 +79,7 @@ struct ConnectionURLParserMSSQLTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) + #expect(parsed.type == .mongodb) #expect(parsed.host == "cluster.net") #expect(parsed.database == "db") } diff --git a/TableProTests/Core/Utilities/ConnectionURLParserTests.swift b/TableProTests/Core/Utilities/ConnectionURLParserTests.swift index bd9752339..3551510a5 100644 --- a/TableProTests/Core/Utilities/ConnectionURLParserTests.swift +++ b/TableProTests/Core/Utilities/ConnectionURLParserTests.swift @@ -275,7 +275,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) + #expect(parsed.type == .mongodb) #expect(parsed.host == "mongo.example.com") #expect(parsed.port == nil) #expect(parsed.database == "mydb") @@ -289,7 +289,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) + #expect(parsed.type == .mongodb) #expect(parsed.port == nil) } @@ -575,7 +575,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.host == "localhost") #expect(parsed.port == nil) #expect(parsed.redisDatabase == nil) @@ -587,7 +587,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.redisDatabase == 3) #expect(parsed.database == "") } @@ -598,7 +598,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.host == "localhost") #expect(parsed.port == nil) } @@ -609,7 +609,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.sslMode == .required) } @@ -619,7 +619,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.password == "password") #expect(parsed.host == "localhost") } @@ -630,7 +630,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.username == "user") #expect(parsed.password == "pass") #expect(parsed.host == "localhost") @@ -645,7 +645,7 @@ struct ConnectionURLParserTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.redisDatabase == 0) #expect(parsed.database == "") } diff --git a/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift b/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift index 30da1f5c8..44030fabf 100644 --- a/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift +++ b/TableProTests/Core/Utilities/DatabaseURLSchemeTests.swift @@ -66,7 +66,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) + #expect(parsed.type == .mongodb) } @Test("MongoDB+SRV scheme parses and maps to mongodb type") @@ -75,7 +75,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "MongoDB")) + #expect(parsed.type == .mongodb) } @Test("Redis scheme parses successfully") @@ -84,7 +84,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.sslMode == nil) } @@ -94,7 +94,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "Redis")) + #expect(parsed.type == .redis) #expect(parsed.sslMode == .required) } @@ -113,7 +113,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) } @Test("SQL Server scheme parses successfully") @@ -122,7 +122,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) } // MARK: - SSH Variants @@ -201,7 +201,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success, got: \(result)"); return } - #expect(parsed.type == DatabaseType(rawValue: "Cassandra")) + #expect(parsed.type == .cassandra) #expect(parsed.host == "host") #expect(parsed.port == nil) // 9042 is the default port, so parser normalizes to nil #expect(parsed.database == "keyspace") @@ -235,7 +235,7 @@ struct DatabaseURLSchemeTests { guard case .success(let parsed) = result else { Issue.record("Expected success"); return } - #expect(parsed.type == DatabaseType(rawValue: "SQL Server")) + #expect(parsed.type == .mssql) } @Test("Mixed case scheme parses correctly") diff --git a/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift b/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift index 0bc2ab560..57516383b 100644 --- a/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift +++ b/TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift @@ -146,14 +146,14 @@ struct SQLRowToStatementConverterTests { @Test("ClickHouse fallback uses standard UPDATE syntax (plugin handles ALTER TABLE at runtime)") func clickhouseFallbackUsesStandardUpdate() { - let converter = makeConverter(databaseType: DatabaseType(rawValue: "ClickHouse"), dialect: Self.clickhouseDialect) + let converter = makeConverter(databaseType: .clickhouse, dialect: Self.clickhouseDialect) let result = converter.generateUpdates(rows: [["1", "Alice", "alice@example.com"]]) #expect(result == "UPDATE `users` SET `name` = 'Alice', `email` = 'alice@example.com' WHERE `id` = '1';") } @Test("MSSQL uses bracket quoting") func mssqlUsesBracketQuoting() { - let converter = makeConverter(databaseType: DatabaseType(rawValue: "SQL Server"), dialect: Self.mssqlDialect) + let converter = makeConverter(databaseType: .mssql, dialect: Self.mssqlDialect) let result = converter.generateInserts(rows: [["1", "Alice", "alice@example.com"]]) #expect(result == "INSERT INTO [users] ([id], [name], [email]) VALUES ('1', 'Alice', 'alice@example.com');") } @@ -174,7 +174,7 @@ struct SQLRowToStatementConverterTests { @Test("DuckDB uses double-quote quoting and standard UPDATE syntax") func duckdbUsesDoubleQuoteAndStandardUpdate() { - let converter = makeConverter(databaseType: DatabaseType(rawValue: "DuckDB"), dialect: Self.duckdbDialect) + let converter = makeConverter(databaseType: .duckdb, dialect: Self.duckdbDialect) let insert = converter.generateInserts(rows: [["1", "Alice", "alice@example.com"]]) #expect(insert == "INSERT INTO \"users\" (\"id\", \"name\", \"email\") VALUES ('1', 'Alice', 'alice@example.com');") let update = converter.generateUpdates(rows: [["1", "Alice", "alice@example.com"]]) diff --git a/TableProTests/Helpers/TestFixtures.swift b/TableProTests/Helpers/TestFixtures.swift index f0f98ba4a..d7708f1fe 100644 --- a/TableProTests/Helpers/TestFixtures.swift +++ b/TableProTests/Helpers/TestFixtures.swift @@ -14,7 +14,7 @@ enum TestFixtures { static let allDatabaseTypes: [DatabaseType] = [ .mysql, .mariadb, .postgresql, .sqlite, .redshift, - DatabaseType(rawValue: "MongoDB"), DatabaseType(rawValue: "Redis"), DatabaseType(rawValue: "ClickHouse") + .mongodb, .redis, .clickhouse ] // MARK: - ClickHouse Connection Fixture @@ -25,7 +25,7 @@ enum TestFixtures { port: 8_123, database: "default", username: "default", - type: DatabaseType(rawValue: "ClickHouse") + type: .clickhouse ) // MARK: - Factory Methods diff --git a/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift b/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift index 700751ae4..12bf1f8bc 100644 --- a/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift +++ b/TableProTests/Models/DatabaseConnectionAdditionalFieldsTests.swift @@ -15,37 +15,37 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("mongoAuthSource defaults to nil") func mongoAuthSourceDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + let conn = TestFixtures.makeConnection(type: .mongodb) #expect(conn.mongoAuthSource == nil) } @Test("mongoReadPreference defaults to nil") func mongoReadPreferenceDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + let conn = TestFixtures.makeConnection(type: .mongodb) #expect(conn.mongoReadPreference == nil) } @Test("mongoWriteConcern defaults to nil") func mongoWriteConcernDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + let conn = TestFixtures.makeConnection(type: .mongodb) #expect(conn.mongoWriteConcern == nil) } @Test("mssqlSchema defaults to nil") func mssqlSchemaDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "SQL Server")) + let conn = TestFixtures.makeConnection(type: .mssql) #expect(conn.mssqlSchema == nil) } @Test("oracleServiceName defaults to nil") func oracleServiceNameDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Oracle")) + let conn = TestFixtures.makeConnection(type: .oracle) #expect(conn.oracleServiceName == nil) } @Test("redisDatabase defaults to nil") func redisDatabaseDefaultsToNil() { - let conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Redis")) + let conn = TestFixtures.makeConnection(type: .redis) #expect(conn.redisDatabase == nil) } @@ -53,42 +53,42 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("mongoAuthSource is readable and writable") func mongoAuthSourceReadWrite() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + var conn = TestFixtures.makeConnection(type: .mongodb) conn.mongoAuthSource = "admin" #expect(conn.mongoAuthSource == "admin") } @Test("mongoReadPreference is readable and writable") func mongoReadPreferenceReadWrite() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + var conn = TestFixtures.makeConnection(type: .mongodb) conn.mongoReadPreference = "secondary" #expect(conn.mongoReadPreference == "secondary") } @Test("mongoWriteConcern is readable and writable") func mongoWriteConcernReadWrite() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + var conn = TestFixtures.makeConnection(type: .mongodb) conn.mongoWriteConcern = "majority" #expect(conn.mongoWriteConcern == "majority") } @Test("mssqlSchema is readable and writable") func mssqlSchemaReadWrite() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "SQL Server")) + var conn = TestFixtures.makeConnection(type: .mssql) conn.mssqlSchema = "dbo" #expect(conn.mssqlSchema == "dbo") } @Test("oracleServiceName is readable and writable") func oracleServiceNameReadWrite() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Oracle")) + var conn = TestFixtures.makeConnection(type: .oracle) conn.oracleServiceName = "ORCL" #expect(conn.oracleServiceName == "ORCL") } @Test("redisDatabase is readable and writable") func redisDatabaseReadWrite() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Redis")) + var conn = TestFixtures.makeConnection(type: .redis) conn.redisDatabase = 3 #expect(conn.redisDatabase == 3) } @@ -97,7 +97,7 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("Setting mongoAuthSource writes to additionalFields dict") func mongoAuthSourceWritesToDict() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + var conn = TestFixtures.makeConnection(type: .mongodb) conn.mongoAuthSource = "admin" #expect(conn.additionalFields["mongoAuthSource"] == "admin") } @@ -106,7 +106,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initWithDictPopulatesAliases() { let conn = DatabaseConnection( name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, additionalFields: [ "mongoAuthSource": "admin", "mongoReadPreference": "primary", @@ -122,7 +122,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func emptyStringReturnsNil() { let conn = DatabaseConnection( name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, additionalFields: ["mongoAuthSource": ""] ) #expect(conn.mongoAuthSource == nil) @@ -130,7 +130,7 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("Setting nil via computed alias writes empty string to dict") func settingNilWritesEmptyString() { - var conn = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + var conn = TestFixtures.makeConnection(type: .mongodb) conn.mongoAuthSource = "admin" conn.mongoAuthSource = nil #expect(conn.additionalFields["mongoAuthSource"] == "") @@ -142,7 +142,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initPopulatesMongoAuthSource() { let conn = DatabaseConnection( name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "admin" ) #expect(conn.mongoAuthSource == "admin") @@ -153,7 +153,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initPopulatesMssqlSchema() { let conn = DatabaseConnection( name: "Test", - type: DatabaseType(rawValue: "SQL Server"), + type: .mssql, mssqlSchema: "dbo" ) #expect(conn.mssqlSchema == "dbo") @@ -164,7 +164,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initPopulatesOracleServiceName() { let conn = DatabaseConnection( name: "Test", - type: DatabaseType(rawValue: "Oracle"), + type: .oracle, oracleServiceName: "ORCL" ) #expect(conn.oracleServiceName == "ORCL") @@ -175,7 +175,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func initDictOverridesNamedParams() { let conn = DatabaseConnection( name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "fromParam", additionalFields: ["mongoAuthSource": "fromDict"] ) @@ -190,13 +190,13 @@ struct DatabaseConnectionAdditionalFieldsTests { let a = DatabaseConnection( id: id, name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "admin" ) let b = DatabaseConnection( id: id, name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "admin" ) #expect(a == b) @@ -208,13 +208,13 @@ struct DatabaseConnectionAdditionalFieldsTests { let a = DatabaseConnection( id: id, name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "admin" ) let b = DatabaseConnection( id: id, name: "Test", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "other" ) #expect(a != b) @@ -226,7 +226,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func codableRoundTripMongo() throws { let original = DatabaseConnection( name: "Mongo", - type: DatabaseType(rawValue: "MongoDB"), + type: .mongodb, mongoAuthSource: "admin", mongoReadPreference: "secondary", mongoWriteConcern: "majority" @@ -242,7 +242,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func codableRoundTripMssql() throws { let original = DatabaseConnection( name: "MSSQL", - type: DatabaseType(rawValue: "SQL Server"), + type: .mssql, mssqlSchema: "dbo" ) let data = try JSONEncoder().encode(original) @@ -254,7 +254,7 @@ struct DatabaseConnectionAdditionalFieldsTests { func codableRoundTripOracle() throws { let original = DatabaseConnection( name: "Oracle", - type: DatabaseType(rawValue: "Oracle"), + type: .oracle, oracleServiceName: "ORCL" ) let data = try JSONEncoder().encode(original) @@ -264,7 +264,7 @@ struct DatabaseConnectionAdditionalFieldsTests { @Test("Codable round-trip preserves nil additional fields") func codableRoundTripNils() throws { - let original = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + let original = TestFixtures.makeConnection(type: .mongodb) let data = try JSONEncoder().encode(original) let decoded = try JSONDecoder().decode(DatabaseConnection.self, from: data) #expect(decoded.mongoAuthSource == nil) diff --git a/TableProTests/Models/DatabaseTypeCassandraTests.swift b/TableProTests/Models/DatabaseTypeCassandraTests.swift index b144b3b5a..e9abb07e7 100644 --- a/TableProTests/Models/DatabaseTypeCassandraTests.swift +++ b/TableProTests/Models/DatabaseTypeCassandraTests.swift @@ -5,91 +5,91 @@ import Testing struct DatabaseTypeCassandraTests { @Test("Cassandra raw value is Cassandra") func cassandraRawValue() { - #expect(DatabaseType(rawValue: "Cassandra").rawValue == "Cassandra") + #expect(.cassandra.rawValue == "Cassandra") } @Test("ScyllaDB raw value is ScyllaDB") func scylladbRawValue() { - #expect(DatabaseType(rawValue: "ScyllaDB").rawValue == "ScyllaDB") + #expect(.scylladb.rawValue == "ScyllaDB") } @Test("Cassandra pluginTypeId is Cassandra") func cassandraPluginTypeId() { - #expect(DatabaseType(rawValue: "Cassandra").pluginTypeId == "Cassandra") + #expect(.cassandra.pluginTypeId == "Cassandra") } @Test("ScyllaDB pluginTypeId is Cassandra") func scylladbPluginTypeId() { - #expect(DatabaseType(rawValue: "ScyllaDB").pluginTypeId == "Cassandra") + #expect(.scylladb.pluginTypeId == "Cassandra") } @Test("Cassandra default port is 9042") func cassandraDefaultPort() { - #expect(DatabaseType(rawValue: "Cassandra").defaultPort == 9_042) + #expect(.cassandra.defaultPort == 9_042) } @Test("ScyllaDB default port is 9042") func scylladbDefaultPort() { - #expect(DatabaseType(rawValue: "ScyllaDB").defaultPort == 9_042) + #expect(.scylladb.defaultPort == 9_042) } @Test("Cassandra does not require authentication") func cassandraRequiresAuthentication() { - #expect(DatabaseType(rawValue: "Cassandra").requiresAuthentication == false) + #expect(.cassandra.requiresAuthentication == false) } @Test("ScyllaDB does not require authentication") func scylladbRequiresAuthentication() { - #expect(DatabaseType(rawValue: "ScyllaDB").requiresAuthentication == false) + #expect(.scylladb.requiresAuthentication == false) } @Test("Cassandra does not support foreign keys") func cassandraSupportsForeignKeys() { - #expect(DatabaseType(rawValue: "Cassandra").supportsForeignKeys == false) + #expect(.cassandra.supportsForeignKeys == false) } @Test("ScyllaDB does not support foreign keys") func scylladbSupportsForeignKeys() { - #expect(DatabaseType(rawValue: "ScyllaDB").supportsForeignKeys == false) + #expect(.scylladb.supportsForeignKeys == false) } @Test("Cassandra supports schema editing") func cassandraSupportsSchemaEditing() { - #expect(DatabaseType(rawValue: "Cassandra").supportsSchemaEditing == true) + #expect(.cassandra.supportsSchemaEditing == true) } @Test("ScyllaDB supports schema editing") func scylladbSupportsSchemaEditing() { - #expect(DatabaseType(rawValue: "ScyllaDB").supportsSchemaEditing == true) + #expect(.scylladb.supportsSchemaEditing == true) } @Test("Cassandra icon name is cassandra-icon") func cassandraIconName() { - #expect(DatabaseType(rawValue: "Cassandra").iconName == "cassandra-icon") + #expect(.cassandra.iconName == "cassandra-icon") } @Test("ScyllaDB icon name is scylladb-icon") func scylladbIconName() { - #expect(DatabaseType(rawValue: "ScyllaDB").iconName == "scylladb-icon") + #expect(.scylladb.iconName == "scylladb-icon") } @Test("Cassandra is a downloadable plugin") func cassandraIsDownloadablePlugin() { - #expect(DatabaseType(rawValue: "Cassandra").isDownloadablePlugin == true) + #expect(.cassandra.isDownloadablePlugin == true) } @Test("ScyllaDB is a downloadable plugin") func scylladbIsDownloadablePlugin() { - #expect(DatabaseType(rawValue: "ScyllaDB").isDownloadablePlugin == true) + #expect(.scylladb.isDownloadablePlugin == true) } @Test("Cassandra included in allCases") func cassandraIncludedInAllCases() { - #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "Cassandra"))) + #expect(DatabaseType.allCases.contains(.cassandra)) } @Test("ScyllaDB included in allCases") func scylladbIncludedInAllCases() { - #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "ScyllaDB"))) + #expect(DatabaseType.allCases.contains(.scylladb)) } } diff --git a/TableProTests/Models/DatabaseTypeMSSQLTests.swift b/TableProTests/Models/DatabaseTypeMSSQLTests.swift index 20edb3742..7692f2048 100644 --- a/TableProTests/Models/DatabaseTypeMSSQLTests.swift +++ b/TableProTests/Models/DatabaseTypeMSSQLTests.swift @@ -2,7 +2,7 @@ // DatabaseTypeMSSQLTests.swift // TableProTests // -// Tests for DatabaseType(rawValue: "SQL Server") properties and methods. +// Tests for .mssql properties and methods. // import Foundation @@ -15,43 +15,43 @@ struct DatabaseTypeMSSQLTests { @Test("defaultPort is 1433") func defaultPort() { - #expect(DatabaseType(rawValue: "SQL Server").defaultPort == 1_433) + #expect(.mssql.defaultPort == 1_433) } @Test("rawValue is SQL Server") func rawValue() { - #expect(DatabaseType(rawValue: "SQL Server").rawValue == "SQL Server") + #expect(.mssql.rawValue == "SQL Server") } @Test("requiresAuthentication is true") func requiresAuthentication() { - #expect(DatabaseType(rawValue: "SQL Server").requiresAuthentication == true) + #expect(.mssql.requiresAuthentication == true) } @Test("supportsForeignKeys is true") func supportsForeignKeys() { - #expect(DatabaseType(rawValue: "SQL Server").supportsForeignKeys == true) + #expect(.mssql.supportsForeignKeys == true) } @Test("supportsSchemaEditing is true") func supportsSchemaEditing() { - #expect(DatabaseType(rawValue: "SQL Server").supportsSchemaEditing == true) + #expect(.mssql.supportsSchemaEditing == true) } @Test("iconName is mssql-icon") func iconName() { - #expect(DatabaseType(rawValue: "SQL Server").iconName == "mssql-icon") + #expect(.mssql.iconName == "mssql-icon") } // MARK: - allKnownTypes Tests @Test("allKnownTypes contains mssql") func allKnownTypesContainsMSSql() { - #expect(DatabaseType.allKnownTypes.contains(DatabaseType(rawValue: "SQL Server"))) + #expect(DatabaseType.allKnownTypes.contains(.mssql)) } @Test("allCases shim contains mssql") func allCasesContainsMSSql() { - #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "SQL Server"))) + #expect(DatabaseType.allCases.contains(.mssql)) } } diff --git a/TableProTests/Models/DatabaseTypeRedisTests.swift b/TableProTests/Models/DatabaseTypeRedisTests.swift index bd647a3b2..d6e834883 100644 --- a/TableProTests/Models/DatabaseTypeRedisTests.swift +++ b/TableProTests/Models/DatabaseTypeRedisTests.swift @@ -5,46 +5,46 @@ import Testing struct DatabaseTypeRedisTests { @Test("Default port is 6379") func defaultPort() { - #expect(DatabaseType(rawValue: "Redis").defaultPort == 6_379) + #expect(.redis.defaultPort == 6_379) } @Test("Icon name is redis-icon") func iconName() { - #expect(DatabaseType(rawValue: "Redis").iconName == "redis-icon") + #expect(.redis.iconName == "redis-icon") } @Test("Does not require authentication") func requiresAuthentication() { - #expect(DatabaseType(rawValue: "Redis").requiresAuthentication == false) + #expect(.redis.requiresAuthentication == false) } @Test("Does not support foreign keys") func supportsForeignKeys() { - #expect(DatabaseType(rawValue: "Redis").supportsForeignKeys == false) + #expect(.redis.supportsForeignKeys == false) } @Test("Does not support schema editing") func supportsSchemaEditing() { - #expect(DatabaseType(rawValue: "Redis").supportsSchemaEditing == false) + #expect(.redis.supportsSchemaEditing == false) } @Test("Raw value is Redis") func rawValue() { - #expect(DatabaseType(rawValue: "Redis").rawValue == "Redis") + #expect(.redis.rawValue == "Redis") } @Test("Theme color is derived from plugin brand color") @MainActor func themeColor() { - #expect(DatabaseType(rawValue: "Redis").themeColor == PluginManager.shared.brandColor(for: DatabaseType(rawValue: "Redis"))) + #expect(.redis.themeColor == PluginManager.shared.brandColor(for: .redis)) } @Test("Included in allKnownTypes") func includedInAllKnownTypes() { - #expect(DatabaseType.allKnownTypes.contains(DatabaseType(rawValue: "Redis"))) + #expect(DatabaseType.allKnownTypes.contains(.redis)) } @Test("Included in allCases shim") func includedInAllCases() { - #expect(DatabaseType.allCases.contains(DatabaseType(rawValue: "Redis"))) + #expect(DatabaseType.allCases.contains(.redis)) } } diff --git a/TableProTests/Models/DatabaseTypeTests.swift b/TableProTests/Models/DatabaseTypeTests.swift index c8fc8868d..c4e2f275d 100644 --- a/TableProTests/Models/DatabaseTypeTests.swift +++ b/TableProTests/Models/DatabaseTypeTests.swift @@ -34,7 +34,7 @@ struct DatabaseTypeTests { @Test("MongoDB default port is 27017") func testMongoDBDefaultPort() { - #expect(DatabaseType(rawValue: "MongoDB").defaultPort == 27_017) + #expect(.mongodb.defaultPort == 27_017) } @Test("allKnownTypes count is 13") @@ -52,15 +52,15 @@ struct DatabaseTypeTests { (DatabaseType.mariadb, "MariaDB"), (DatabaseType.postgresql, "PostgreSQL"), (DatabaseType.sqlite, "SQLite"), - (DatabaseType(rawValue: "MongoDB"), "MongoDB"), - (DatabaseType(rawValue: "Redis"), "Redis"), + (.mongodb, "MongoDB"), + (.redis, "Redis"), (DatabaseType.redshift, "Redshift"), - (DatabaseType(rawValue: "SQL Server"), "SQL Server"), - (DatabaseType(rawValue: "Oracle"), "Oracle"), - (DatabaseType(rawValue: "ClickHouse"), "ClickHouse"), - (DatabaseType(rawValue: "DuckDB"), "DuckDB"), - (DatabaseType(rawValue: "Cassandra"), "Cassandra"), - (DatabaseType(rawValue: "ScyllaDB"), "ScyllaDB") + (.mssql, "SQL Server"), + (.oracle, "Oracle"), + (.clickhouse, "ClickHouse"), + (.duckdb, "DuckDB"), + (.cassandra, "Cassandra"), + (.scylladb, "ScyllaDB") ]) func testRawValueMatchesDisplayName(dbType: DatabaseType, expectedRawValue: String) { #expect(dbType.rawValue == expectedRawValue) @@ -70,27 +70,27 @@ struct DatabaseTypeTests { @Test("ClickHouse default port is 8123") func testClickHouseDefaultPort() { - #expect(DatabaseType(rawValue: "ClickHouse").defaultPort == 8_123) + #expect(.clickhouse.defaultPort == 8_123) } @Test("ClickHouse requires authentication") func testClickHouseRequiresAuth() { - #expect(DatabaseType(rawValue: "ClickHouse").requiresAuthentication == true) + #expect(.clickhouse.requiresAuthentication == true) } @Test("ClickHouse does not support foreign keys") func testClickHouseSupportsForeignKeys() { - #expect(DatabaseType(rawValue: "ClickHouse").supportsForeignKeys == false) + #expect(.clickhouse.supportsForeignKeys == false) } @Test("ClickHouse supports schema editing") func testClickHouseSupportsSchemaEditing() { - #expect(DatabaseType(rawValue: "ClickHouse").supportsSchemaEditing == true) + #expect(.clickhouse.supportsSchemaEditing == true) } @Test("ClickHouse icon name is clickhouse-icon") func testClickHouseIconName() { - #expect(DatabaseType(rawValue: "ClickHouse").iconName == "clickhouse-icon") + #expect(.clickhouse.iconName == "clickhouse-icon") } // MARK: - Plugin Type ID Alias Tests @@ -154,6 +154,6 @@ struct DatabaseTypeTests { let types: Set = [.mysql, .postgresql, .sqlite] #expect(types.contains(.mysql)) #expect(types.contains(.postgresql)) - #expect(!types.contains(DatabaseType(rawValue: "Redis"))) + #expect(!types.contains(.redis)) } } diff --git a/TableProTests/Models/TableOperationDialogLogicTests.swift b/TableProTests/Models/TableOperationDialogLogicTests.swift index c5c48f58f..fff6feecd 100644 --- a/TableProTests/Models/TableOperationDialogLogicTests.swift +++ b/TableProTests/Models/TableOperationDialogLogicTests.swift @@ -134,7 +134,7 @@ struct TableOperationDialogLogicTests { @Test("MongoDB does not support cascade") func testMongoDBCascadeNotSupported() { - #expect(DialogLogic.cascadeSupported(databaseType: DatabaseType(rawValue: "MongoDB")) == false) + #expect(DialogLogic.cascadeSupported(databaseType: .mongodb) == false) } // MARK: - cascadeDisabled diff --git a/TableProTests/ViewModels/AIChatViewModelActionTests.swift b/TableProTests/ViewModels/AIChatViewModelActionTests.swift index c997e601f..d2705c208 100644 --- a/TableProTests/ViewModels/AIChatViewModelActionTests.swift +++ b/TableProTests/ViewModels/AIChatViewModelActionTests.swift @@ -33,7 +33,7 @@ struct AIChatViewModelActionTests { @Test("handleFixError with MongoDB connection uses JavaScript language") func fixErrorMongoDBConnection() { let vm = AIChatViewModel() - vm.connection = TestFixtures.makeConnection(type: DatabaseType(rawValue: "MongoDB")) + vm.connection = TestFixtures.makeConnection(type: .mongodb) vm.handleFixError(query: "db.users.find({})", error: "SyntaxError") @@ -46,7 +46,7 @@ struct AIChatViewModelActionTests { @Test("handleFixError with Redis connection uses bash language") func fixErrorRedisConnection() { let vm = AIChatViewModel() - vm.connection = TestFixtures.makeConnection(type: DatabaseType(rawValue: "Redis")) + vm.connection = TestFixtures.makeConnection(type: .redis) vm.handleFixError(query: "GET mykey", error: "WRONGTYPE") diff --git a/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift b/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift index 6571b46f1..62e41262b 100644 --- a/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift +++ b/TableProTests/Views/Main/CoordinatorSidebarActionsTests.swift @@ -51,7 +51,7 @@ struct CoordinatorSidebarActionsTests { @Test("createView does not crash for each database type", arguments: [ DatabaseType.mysql, .mariadb, .postgresql, .sqlite, .redshift, - DatabaseType(rawValue: "ClickHouse"), DatabaseType(rawValue: "SQL Server"), DatabaseType(rawValue: "Oracle"), DatabaseType(rawValue: "MongoDB"), DatabaseType(rawValue: "Redis"), DatabaseType(rawValue: "DuckDB"), + .clickhouse, .mssql, .oracle, .mongodb, .redis, .duckdb, ]) @MainActor func createViewDoesNotCrash(type: DatabaseType) { @@ -77,7 +77,7 @@ struct CoordinatorSidebarActionsTests { @Test("openImportDialog with MongoDB returns early at type guard") @MainActor func openImportDialogBlockedForMongoDB() { - let (coordinator, _) = makeCoordinator(type: DatabaseType(rawValue: "MongoDB")) + let (coordinator, _) = makeCoordinator(type: .mongodb) defer { coordinator.teardown() } // Hits the MongoDB/Redis guard; shows an alert as side effect @@ -88,7 +88,7 @@ struct CoordinatorSidebarActionsTests { @Test("openImportDialog with Redis returns early at type guard") @MainActor func openImportDialogBlockedForRedis() { - let (coordinator, _) = makeCoordinator(type: DatabaseType(rawValue: "Redis")) + let (coordinator, _) = makeCoordinator(type: .redis) defer { coordinator.teardown() } coordinator.openImportDialog() From fc55067578c404c42b28be437d60f921947e0313 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 16 Mar 2026 21:49:31 +0700 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20PR=20review=20nitpicks=20?= =?UTF-8?q?=E2=80=94=20defer=20unlock,=20explicit=20type=20prefixes,=20fle?= =?UTF-8?q?xible=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Plugins/PluginMetadataRegistry.swift | 2 +- TableProTests/Models/DatabaseTypeTests.swift | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry.swift b/TablePro/Core/Plugins/PluginMetadataRegistry.swift index a35d8214e..f5051078b 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry.swift @@ -555,8 +555,8 @@ final class PluginMetadataRegistry: @unchecked Sendable { /// 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 - lock.unlock() } /// Returns all registered type IDs (sorted for deterministic UI ordering). diff --git a/TableProTests/Models/DatabaseTypeTests.swift b/TableProTests/Models/DatabaseTypeTests.swift index c4e2f275d..75a3625aa 100644 --- a/TableProTests/Models/DatabaseTypeTests.swift +++ b/TableProTests/Models/DatabaseTypeTests.swift @@ -34,12 +34,16 @@ struct DatabaseTypeTests { @Test("MongoDB default port is 27017") func testMongoDBDefaultPort() { - #expect(.mongodb.defaultPort == 27_017) + #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") @@ -52,15 +56,15 @@ struct DatabaseTypeTests { (DatabaseType.mariadb, "MariaDB"), (DatabaseType.postgresql, "PostgreSQL"), (DatabaseType.sqlite, "SQLite"), - (.mongodb, "MongoDB"), - (.redis, "Redis"), + (DatabaseType.mongodb, "MongoDB"), + (DatabaseType.redis, "Redis"), (DatabaseType.redshift, "Redshift"), - (.mssql, "SQL Server"), - (.oracle, "Oracle"), - (.clickhouse, "ClickHouse"), - (.duckdb, "DuckDB"), - (.cassandra, "Cassandra"), - (.scylladb, "ScyllaDB") + (DatabaseType.mssql, "SQL Server"), + (DatabaseType.oracle, "Oracle"), + (DatabaseType.clickhouse, "ClickHouse"), + (DatabaseType.duckdb, "DuckDB"), + (DatabaseType.cassandra, "Cassandra"), + (DatabaseType.scylladb, "ScyllaDB") ]) func testRawValueMatchesDisplayName(dbType: DatabaseType, expectedRawValue: String) { #expect(dbType.rawValue == expectedRawValue) @@ -70,27 +74,27 @@ struct DatabaseTypeTests { @Test("ClickHouse default port is 8123") func testClickHouseDefaultPort() { - #expect(.clickhouse.defaultPort == 8_123) + #expect(DatabaseType.clickhouse.defaultPort == 8_123) } @Test("ClickHouse requires authentication") func testClickHouseRequiresAuth() { - #expect(.clickhouse.requiresAuthentication == true) + #expect(DatabaseType.clickhouse.requiresAuthentication == true) } @Test("ClickHouse does not support foreign keys") func testClickHouseSupportsForeignKeys() { - #expect(.clickhouse.supportsForeignKeys == false) + #expect(DatabaseType.clickhouse.supportsForeignKeys == false) } @Test("ClickHouse supports schema editing") func testClickHouseSupportsSchemaEditing() { - #expect(.clickhouse.supportsSchemaEditing == true) + #expect(DatabaseType.clickhouse.supportsSchemaEditing == true) } @Test("ClickHouse icon name is clickhouse-icon") func testClickHouseIconName() { - #expect(.clickhouse.iconName == "clickhouse-icon") + #expect(DatabaseType.clickhouse.iconName == "clickhouse-icon") } // MARK: - Plugin Type ID Alias Tests