From e5d5ead525f5e9ee8200c96c2c89893b2ec69bb5 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 11 Mar 2026 17:13:26 +0700 Subject: [PATCH 1/2] feat: extend DriverPlugin protocol with UI/capability metadata Each driver plugin now self-declares brand color, connection mode, supported features, column types, URL schemes, and grouping strategy. All 20 properties have sensible defaults so existing plugins compile without changes. This is Phase 1.1 of the plugin extensibility plan. --- CHANGELOG.md | 1 + .../ClickHousePlugin.swift | 24 ++ Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift | 26 ++ Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift | 18 ++ .../MongoDBDriverPlugin/MongoDBPlugin.swift | 22 ++ Plugins/MySQLDriverPlugin/MySQLPlugin.swift | 16 + Plugins/OracleDriverPlugin/OraclePlugin.swift | 17 ++ .../PostgreSQLPlugin.swift | 23 ++ Plugins/RedisDriverPlugin/RedisPlugin.swift | 25 ++ Plugins/SQLiteDriverPlugin/SQLitePlugin.swift | 18 ++ .../TableProPluginKit/ConnectionMode.swift | 9 + Plugins/TableProPluginKit/DriverPlugin.swift | 54 ++++ .../TableProPluginKit/EditorLanguage.swift | 48 +++ .../TableProPluginKit/GroupingStrategy.swift | 10 + .../Plugins/DriverPluginMetadataTests.swift | 288 ++++++++++++++++++ 15 files changed, 599 insertions(+) create mode 100644 Plugins/TableProPluginKit/ConnectionMode.swift create mode 100644 Plugins/TableProPluginKit/EditorLanguage.swift create mode 100644 Plugins/TableProPluginKit/GroupingStrategy.swift create mode 100644 TableProTests/Core/Plugins/DriverPluginMetadataTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a00fa2cc..b24dc080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Plugin UI/capability metadata: each driver plugin now self-declares brand color, connection mode, supported features, column types, URL schemes, and grouping strategy via the `DriverPlugin` protocol - Copy as INSERT/UPDATE SQL statements from data grid context menu - Plugin download count display in Browse Plugins — fetched from GitHub Releases API and cached for 1 hour - MSSQL query cancellation (`cancelQuery`) and lock timeout (`applyQueryTimeout`) support diff --git a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift index 32d6dfc0..378a9ba5 100644 --- a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift +++ b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift @@ -18,6 +18,30 @@ final class ClickHousePlugin: NSObject, TableProPlugin, DriverPlugin { static let iconName = "bolt.fill" static let defaultPort = 8123 + // MARK: - UI/Capability Metadata + + static let brandColorHex = "#FFD100" + static let supportsForeignKeys = false + static let systemDatabaseNames: [String] = ["information_schema", "INFORMATION_SCHEMA", "system"] + static let columnTypesByCategory: [String: [String]] = [ + "Integer": [ + "UInt8", "UInt16", "UInt32", "UInt64", "UInt128", "UInt256", + "Int8", "Int16", "Int32", "Int64", "Int128", "Int256" + ], + "Float": ["Float32", "Float64", "Decimal", "Decimal32", "Decimal64", "Decimal128", "Decimal256"], + "String": ["String", "FixedString", "Enum8", "Enum16"], + "Date": ["Date", "Date32", "DateTime", "DateTime64"], + "Binary": [], + "Boolean": ["Bool"], + "JSON": ["JSON"], + "UUID": ["UUID"], + "Array": ["Array"], + "Map": ["Map"], + "Tuple": ["Tuple"], + "IP": ["IPv4", "IPv6"], + "Geo": ["Point", "Ring", "Polygon", "MultiPolygon"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { ClickHousePluginDriver(config: config) } diff --git a/Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift b/Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift index af9bbc62..21cc35ad 100644 --- a/Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift +++ b/Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift @@ -19,6 +19,32 @@ final class DuckDBPlugin: NSObject, TableProPlugin, DriverPlugin { static let iconName = "duckdb-icon" static let defaultPort = 0 + // MARK: - UI/Capability Metadata + + static let requiresAuthentication = false + static let connectionMode: ConnectionMode = .fileBased + static let urlSchemes: [String] = ["duckdb"] + static let fileExtensions: [String] = ["duckdb", "db"] + static let brandColorHex = "#FFD900" + static let supportsDatabaseSwitching = false + static let systemDatabaseNames: [String] = ["information_schema", "pg_catalog"] + static let databaseGroupingStrategy: GroupingStrategy = .flat + static let columnTypesByCategory: [String: [String]] = [ + "Integer": ["TINYINT", "SMALLINT", "INTEGER", "BIGINT", "HUGEINT", "UTINYINT", "USMALLINT", "UINTEGER", "UBIGINT"], + "Float": ["FLOAT", "DOUBLE", "DECIMAL", "NUMERIC"], + "String": ["VARCHAR", "TEXT", "CHAR", "BPCHAR"], + "Date": ["DATE", "TIME", "TIMESTAMP", "TIMESTAMPTZ", "TIMESTAMP_S", "TIMESTAMP_MS", "TIMESTAMP_NS", "INTERVAL"], + "Binary": ["BLOB", "BYTEA", "BIT", "BITSTRING"], + "Boolean": ["BOOLEAN"], + "JSON": ["JSON"], + "UUID": ["UUID"], + "List": ["LIST"], + "Struct": ["STRUCT"], + "Map": ["MAP"], + "Union": ["UNION"], + "Enum": ["ENUM"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { DuckDBPluginDriver(config: config) } diff --git a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift index aded08ed..077e6896 100644 --- a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift +++ b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift @@ -22,6 +22,24 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin { ConnectionField(id: "mssqlSchema", label: "Schema", placeholder: "dbo") ] + // MARK: - UI/Capability Metadata + + static let brandColorHex = "#E34517" + static let systemDatabaseNames: [String] = ["master", "tempdb", "model", "msdb"] + static let databaseGroupingStrategy: GroupingStrategy = .bySchema + static let columnTypesByCategory: [String: [String]] = [ + "Integer": ["TINYINT", "SMALLINT", "INT", "BIGINT"], + "Float": ["FLOAT", "REAL", "DECIMAL", "NUMERIC", "MONEY", "SMALLMONEY"], + "String": ["CHAR", "VARCHAR", "TEXT", "NCHAR", "NVARCHAR", "NTEXT"], + "Date": ["DATE", "TIME", "DATETIME", "DATETIME2", "SMALLDATETIME", "DATETIMEOFFSET"], + "Binary": ["BINARY", "VARBINARY", "IMAGE"], + "Boolean": ["BIT"], + "XML": ["XML"], + "UUID": ["UNIQUEIDENTIFIER"], + "Spatial": ["GEOMETRY", "GEOGRAPHY"], + "Other": ["SQL_VARIANT", "TIMESTAMP", "ROWVERSION", "CURSOR", "TABLE", "HIERARCHYID"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { MSSQLPluginDriver(config: config) } diff --git a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift index 6cdc1934..9d85800a 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift @@ -22,6 +22,28 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin { ConnectionField(id: "mongoWriteConcern", label: "Write Concern", placeholder: "majority") ] + // MARK: - UI/Capability Metadata + + static let requiresAuthentication = false + static let urlSchemes: [String] = ["mongodb", "mongodb+srv"] + static let brandColorHex = "#00ED63" + static let queryLanguageName = "MQL" + static let editorLanguage: EditorLanguage = .javascript + static let supportsForeignKeys = false + static let supportsSchemaEditing = false + static let databaseGroupingStrategy: GroupingStrategy = .flat + static let columnTypesByCategory: [String: [String]] = [ + "String": ["string", "objectId", "regex"], + "Number": ["int", "long", "double", "decimal"], + "Date": ["date", "timestamp"], + "Binary": ["binData"], + "Boolean": ["bool"], + "Array": ["array"], + "Object": ["object"], + "Null": ["null"], + "Other": ["javascript", "minKey", "maxKey"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { MongoDBPluginDriver(config: config) } diff --git a/Plugins/MySQLDriverPlugin/MySQLPlugin.swift b/Plugins/MySQLDriverPlugin/MySQLPlugin.swift index bba21f04..912d9b08 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPlugin.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPlugin.swift @@ -25,6 +25,22 @@ final class MySQLPlugin: NSObject, TableProPlugin, DriverPlugin { static let additionalConnectionFields: [ConnectionField] = [] static let additionalDatabaseTypeIds: [String] = ["MariaDB"] + // MARK: - UI/Capability Metadata + + static let urlSchemes: [String] = ["mysql"] + static let brandColorHex = "#FF9500" + static let systemDatabaseNames: [String] = ["information_schema", "mysql", "performance_schema", "sys"] + static let columnTypesByCategory: [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"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { MySQLPluginDriver(config: config) } diff --git a/Plugins/OracleDriverPlugin/OraclePlugin.swift b/Plugins/OracleDriverPlugin/OraclePlugin.swift index 7e247910..142c6c5c 100644 --- a/Plugins/OracleDriverPlugin/OraclePlugin.swift +++ b/Plugins/OracleDriverPlugin/OraclePlugin.swift @@ -21,6 +21,23 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin { ConnectionField(id: "oracleServiceName", label: "Service Name", placeholder: "ORCL") ] + // MARK: - UI/Capability Metadata + + static let brandColorHex = "#C3160B" + static let systemDatabaseNames: [String] = ["SYS", "SYSTEM", "OUTLN", "DBSNMP", "APPQOSSYS", "WMSYS", "XDB"] + static let databaseGroupingStrategy: GroupingStrategy = .bySchema + static let columnTypesByCategory: [String: [String]] = [ + "Integer": ["NUMBER", "INTEGER", "INT", "SMALLINT"], + "Float": ["FLOAT", "BINARY_FLOAT", "BINARY_DOUBLE", "DECIMAL", "NUMERIC", "REAL", "DOUBLE PRECISION"], + "String": ["VARCHAR2", "NVARCHAR2", "CHAR", "NCHAR", "CLOB", "NCLOB", "LONG"], + "Date": ["DATE", "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE", "INTERVAL YEAR TO MONTH", "INTERVAL DAY TO SECOND"], + "Binary": ["RAW", "LONG RAW", "BLOB", "BFILE"], + "Boolean": [], + "XML": ["XMLTYPE"], + "Spatial": ["SDO_GEOMETRY"], + "Other": ["ROWID", "UROWID"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { OraclePluginDriver(config: config) } diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift index 063cad1e..6568a704 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift @@ -24,6 +24,29 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin { static let additionalConnectionFields: [ConnectionField] = [] static let additionalDatabaseTypeIds: [String] = ["Redshift"] + // MARK: - UI/Capability Metadata + + static let urlSchemes: [String] = ["postgresql", "postgres"] + static let brandColorHex = "#336791" + static let systemDatabaseNames: [String] = ["postgres", "template0", "template1"] + static let databaseGroupingStrategy: GroupingStrategy = .bySchema + static let columnTypesByCategory: [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"] + ] + static func driverVariant(for databaseTypeId: String) -> String? { switch databaseTypeId { case "PostgreSQL": return "PostgreSQL" diff --git a/Plugins/RedisDriverPlugin/RedisPlugin.swift b/Plugins/RedisDriverPlugin/RedisPlugin.swift index b2b6b68c..1f83993c 100644 --- a/Plugins/RedisDriverPlugin/RedisPlugin.swift +++ b/Plugins/RedisDriverPlugin/RedisPlugin.swift @@ -24,6 +24,31 @@ final class RedisPlugin: NSObject, TableProPlugin, DriverPlugin { static let additionalConnectionFields: [ConnectionField] = [] static let additionalDatabaseTypeIds: [String] = [] + // MARK: - UI/Capability Metadata + + static let requiresAuthentication = false + static let urlSchemes: [String] = ["redis"] + static let brandColorHex = "#DC382D" + static let queryLanguageName = "Redis CLI" + static let editorLanguage: EditorLanguage = .bash + static let supportsForeignKeys = false + static let supportsSchemaEditing = false + static let supportsDatabaseSwitching = false + static let supportsImport = false + static let databaseGroupingStrategy: GroupingStrategy = .flat + static let defaultGroupName = "db0" + static let columnTypesByCategory: [String: [String]] = [ + "String": ["string"], + "List": ["list"], + "Set": ["set"], + "Sorted Set": ["zset"], + "Hash": ["hash"], + "Stream": ["stream"], + "HyperLogLog": ["hyperloglog"], + "Bitmap": ["bitmap"], + "Geospatial": ["geo"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { RedisPluginDriver(config: config) } diff --git a/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift b/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift index adc0086b..3d0b7e6c 100644 --- a/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift +++ b/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift @@ -19,6 +19,24 @@ final class SQLitePlugin: NSObject, TableProPlugin, DriverPlugin { static let iconName = "doc.fill" static let defaultPort = 0 + // MARK: - UI/Capability Metadata + + static let requiresAuthentication = false + static let connectionMode: ConnectionMode = .fileBased + static let urlSchemes: [String] = ["sqlite"] + static let fileExtensions: [String] = ["db", "sqlite", "sqlite3"] + static let brandColorHex = "#003B57" + static let supportsDatabaseSwitching = false + static let databaseGroupingStrategy: GroupingStrategy = .flat + static let columnTypesByCategory: [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"] + ] + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { SQLitePluginDriver(config: config) } diff --git a/Plugins/TableProPluginKit/ConnectionMode.swift b/Plugins/TableProPluginKit/ConnectionMode.swift new file mode 100644 index 00000000..9fd3d1be --- /dev/null +++ b/Plugins/TableProPluginKit/ConnectionMode.swift @@ -0,0 +1,9 @@ +// +// ConnectionMode.swift +// TableProPluginKit +// + +public enum ConnectionMode: String, Codable, Sendable { + case network + case fileBased +} diff --git a/Plugins/TableProPluginKit/DriverPlugin.swift b/Plugins/TableProPluginKit/DriverPlugin.swift index 52574480..860b9075 100644 --- a/Plugins/TableProPluginKit/DriverPlugin.swift +++ b/Plugins/TableProPluginKit/DriverPlugin.swift @@ -11,10 +11,64 @@ public protocol DriverPlugin: TableProPlugin { static func driverVariant(for databaseTypeId: String) -> String? func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver + + // MARK: - UI/Capability Metadata + + static var requiresAuthentication: Bool { get } + static var connectionMode: ConnectionMode { get } + static var urlSchemes: [String] { get } + static var fileExtensions: [String] { get } + static var brandColorHex: String { get } + static var queryLanguageName: String { get } + static var editorLanguage: EditorLanguage { get } + static var supportsForeignKeys: Bool { get } + static var supportsSchemaEditing: Bool { get } + static var supportsDatabaseSwitching: Bool { get } + static var supportsSchemaSwitching: Bool { get } + static var supportsImport: Bool { get } + static var supportsExport: Bool { get } + static var supportsHealthMonitor: Bool { get } + static var systemDatabaseNames: [String] { get } + static var systemSchemaNames: [String] { get } + static var databaseGroupingStrategy: GroupingStrategy { get } + static var defaultGroupName: String { get } + static var columnTypesByCategory: [String: [String]] { get } } public extension DriverPlugin { static var additionalConnectionFields: [ConnectionField] { [] } static var additionalDatabaseTypeIds: [String] { [] } static func driverVariant(for databaseTypeId: String) -> String? { nil } + + // MARK: - UI/Capability Metadata Defaults + + static var requiresAuthentication: Bool { true } + static var connectionMode: ConnectionMode { .network } + static var urlSchemes: [String] { [] } + static var fileExtensions: [String] { [] } + static var brandColorHex: String { "#808080" } + static var queryLanguageName: String { "SQL" } + static var editorLanguage: EditorLanguage { .sql } + static var supportsForeignKeys: Bool { true } + static var supportsSchemaEditing: Bool { true } + static var supportsDatabaseSwitching: Bool { true } + static var supportsSchemaSwitching: Bool { false } + static var supportsImport: Bool { true } + static var supportsExport: Bool { true } + static var supportsHealthMonitor: Bool { true } + static var systemDatabaseNames: [String] { [] } + static var systemSchemaNames: [String] { [] } + static var databaseGroupingStrategy: GroupingStrategy { .byDatabase } + static var defaultGroupName: String { "main" } + static var columnTypesByCategory: [String: [String]] { + [ + "Integer": ["INTEGER", "INT", "SMALLINT", "BIGINT", "TINYINT"], + "Float": ["FLOAT", "DOUBLE", "DECIMAL", "NUMERIC", "REAL"], + "String": ["VARCHAR", "CHAR", "TEXT", "NVARCHAR", "NCHAR"], + "Date": ["DATE", "TIME", "DATETIME", "TIMESTAMP"], + "Binary": ["BLOB", "BINARY", "VARBINARY"], + "Boolean": ["BOOLEAN", "BOOL"], + "JSON": ["JSON"] + ] + } } diff --git a/Plugins/TableProPluginKit/EditorLanguage.swift b/Plugins/TableProPluginKit/EditorLanguage.swift new file mode 100644 index 00000000..cb990ef5 --- /dev/null +++ b/Plugins/TableProPluginKit/EditorLanguage.swift @@ -0,0 +1,48 @@ +// +// EditorLanguage.swift +// TableProPluginKit +// + +public enum EditorLanguage: Sendable, Equatable { + case sql + case javascript + case bash + case custom(String) +} + +extension EditorLanguage: Codable { + private enum CodingKeys: String, CodingKey { + case type + case value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "sql": self = .sql + case "javascript": self = .javascript + case "bash": self = .bash + case "custom": + let value = try container.decode(String.self, forKey: .value) + self = .custom(value) + default: + self = .custom(type) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .sql: + try container.encode("sql", forKey: .type) + case .javascript: + try container.encode("javascript", forKey: .type) + case .bash: + try container.encode("bash", forKey: .type) + case .custom(let value): + try container.encode("custom", forKey: .type) + try container.encode(value, forKey: .value) + } + } +} diff --git a/Plugins/TableProPluginKit/GroupingStrategy.swift b/Plugins/TableProPluginKit/GroupingStrategy.swift new file mode 100644 index 00000000..9b12538b --- /dev/null +++ b/Plugins/TableProPluginKit/GroupingStrategy.swift @@ -0,0 +1,10 @@ +// +// GroupingStrategy.swift +// TableProPluginKit +// + +public enum GroupingStrategy: String, Codable, Sendable { + case byDatabase + case bySchema + case flat +} diff --git a/TableProTests/Core/Plugins/DriverPluginMetadataTests.swift b/TableProTests/Core/Plugins/DriverPluginMetadataTests.swift new file mode 100644 index 00000000..6eb7a044 --- /dev/null +++ b/TableProTests/Core/Plugins/DriverPluginMetadataTests.swift @@ -0,0 +1,288 @@ +// +// DriverPluginMetadataTests.swift +// TableProTests +// + +import Foundation +import TableProPluginKit +import Testing +@testable import TablePro + +// MARK: - Mock Plugin for Default Verification + +private final class MockDefaultPlugin: NSObject, TableProPlugin, DriverPlugin { + static let pluginName = "Mock Default" + static let pluginVersion = "1.0.0" + static let pluginDescription = "Plugin with all defaults" + static let capabilities: [PluginCapability] = [.databaseDriver] + + static let databaseTypeId = "MockDB" + static let databaseDisplayName = "Mock Database" + static let iconName = "cylinder.fill" + static let defaultPort = 9999 + + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { + fatalError("Not used in tests") + } +} + +// MARK: - Mock Plugin with Custom Overrides + +private final class MockCustomPlugin: NSObject, TableProPlugin, DriverPlugin { + static let pluginName = "Mock Custom" + static let pluginVersion = "1.0.0" + static let pluginDescription = "Plugin with custom values" + static let capabilities: [PluginCapability] = [.databaseDriver] + + static let databaseTypeId = "CustomDB" + static let databaseDisplayName = "Custom Database" + static let iconName = "doc.fill" + static let defaultPort = 0 + + static let requiresAuthentication = false + static let connectionMode: ConnectionMode = .fileBased + static let urlSchemes: [String] = ["customdb"] + static let fileExtensions: [String] = ["cdb", "customdb"] + static let brandColorHex = "#FF0000" + static let queryLanguageName = "CQL" + static let editorLanguage: EditorLanguage = .custom("cypher") + static let supportsForeignKeys = false + static let supportsSchemaEditing = false + static let supportsDatabaseSwitching = false + static let supportsImport = false + static let supportsExport = false + static let databaseGroupingStrategy: GroupingStrategy = .flat + static let defaultGroupName = "default" + static let systemDatabaseNames: [String] = ["system", "internal"] + static let columnTypesByCategory: [String: [String]] = [ + "String": ["text"], + "Number": ["integer"], + "Binary": [] + ] + + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { + fatalError("Not used in tests") + } +} + +// MARK: - ConnectionMode Tests + +@Suite("ConnectionMode Enum") +struct ConnectionModeTests { + @Test("Raw values match expected strings") + func rawValues() { + #expect(ConnectionMode.network.rawValue == "network") + #expect(ConnectionMode.fileBased.rawValue == "fileBased") + } + + @Test("Codable round-trip") + func codable() throws { + for mode in [ConnectionMode.network, .fileBased] { + let data = try JSONEncoder().encode(mode) + let decoded = try JSONDecoder().decode(ConnectionMode.self, from: data) + #expect(decoded == mode) + } + } +} + +// MARK: - EditorLanguage Tests + +@Suite("EditorLanguage Enum") +struct EditorLanguageTests { + @Test("Equatable for known cases") + func equatable() { + #expect(EditorLanguage.sql == EditorLanguage.sql) + #expect(EditorLanguage.javascript == EditorLanguage.javascript) + #expect(EditorLanguage.bash == EditorLanguage.bash) + #expect(EditorLanguage.sql != EditorLanguage.javascript) + } + + @Test("Custom case with associated value") + func customCase() { + let lang = EditorLanguage.custom("graphql") + #expect(lang == EditorLanguage.custom("graphql")) + #expect(lang != EditorLanguage.custom("cypher")) + #expect(lang != EditorLanguage.sql) + } + + @Test("Codable round-trip for all cases") + func codable() throws { + let cases: [EditorLanguage] = [.sql, .javascript, .bash, .custom("graphql")] + for lang in cases { + let data = try JSONEncoder().encode(lang) + let decoded = try JSONDecoder().decode(EditorLanguage.self, from: data) + #expect(decoded == lang) + } + } +} + +// MARK: - GroupingStrategy Tests + +@Suite("GroupingStrategy Enum") +struct GroupingStrategyTests { + @Test("Raw values match expected strings") + func rawValues() { + #expect(GroupingStrategy.byDatabase.rawValue == "byDatabase") + #expect(GroupingStrategy.bySchema.rawValue == "bySchema") + #expect(GroupingStrategy.flat.rawValue == "flat") + } + + @Test("Codable round-trip") + func codable() throws { + for strategy in [GroupingStrategy.byDatabase, .bySchema, .flat] { + let data = try JSONEncoder().encode(strategy) + let decoded = try JSONDecoder().decode(GroupingStrategy.self, from: data) + #expect(decoded == strategy) + } + } +} + +// MARK: - DriverPlugin Protocol Defaults + +@Suite("DriverPlugin Protocol Defaults") +struct DriverPluginDefaultsTests { + @Test("Default requiresAuthentication is true") + func requiresAuthentication() { + #expect(MockDefaultPlugin.requiresAuthentication == true) + } + + @Test("Default connectionMode is .network") + func connectionMode() { + #expect(MockDefaultPlugin.connectionMode == .network) + } + + @Test("Default urlSchemes is empty") + func urlSchemes() { + #expect(MockDefaultPlugin.urlSchemes.isEmpty) + } + + @Test("Default fileExtensions is empty") + func fileExtensions() { + #expect(MockDefaultPlugin.fileExtensions.isEmpty) + } + + @Test("Default brandColorHex is gray") + func brandColorHex() { + #expect(MockDefaultPlugin.brandColorHex == "#808080") + } + + @Test("Default queryLanguageName is SQL") + func queryLanguageName() { + #expect(MockDefaultPlugin.queryLanguageName == "SQL") + } + + @Test("Default editorLanguage is .sql") + func editorLanguage() { + #expect(MockDefaultPlugin.editorLanguage == .sql) + } + + @Test("Default supportsForeignKeys is true") + func supportsForeignKeys() { + #expect(MockDefaultPlugin.supportsForeignKeys == true) + } + + @Test("Default supportsSchemaEditing is true") + func supportsSchemaEditing() { + #expect(MockDefaultPlugin.supportsSchemaEditing == true) + } + + @Test("Default supportsDatabaseSwitching is true") + func supportsDatabaseSwitching() { + #expect(MockDefaultPlugin.supportsDatabaseSwitching == true) + } + + @Test("Default supportsSchemaSwitching is false") + func supportsSchemaSwitching() { + #expect(MockDefaultPlugin.supportsSchemaSwitching == false) + } + + @Test("Default supportsImport is true") + func supportsImport() { + #expect(MockDefaultPlugin.supportsImport == true) + } + + @Test("Default supportsExport is true") + func supportsExport() { + #expect(MockDefaultPlugin.supportsExport == true) + } + + @Test("Default supportsHealthMonitor is true") + func supportsHealthMonitor() { + #expect(MockDefaultPlugin.supportsHealthMonitor == true) + } + + @Test("Default systemDatabaseNames is empty") + func systemDatabaseNames() { + #expect(MockDefaultPlugin.systemDatabaseNames.isEmpty) + } + + @Test("Default systemSchemaNames is empty") + func systemSchemaNames() { + #expect(MockDefaultPlugin.systemSchemaNames.isEmpty) + } + + @Test("Default databaseGroupingStrategy is .byDatabase") + func databaseGroupingStrategy() { + #expect(MockDefaultPlugin.databaseGroupingStrategy == .byDatabase) + } + + @Test("Default defaultGroupName is main") + func defaultGroupName() { + #expect(MockDefaultPlugin.defaultGroupName == "main") + } + + @Test("Default columnTypesByCategory contains standard SQL categories") + func columnTypesByCategory() { + let types = MockDefaultPlugin.columnTypesByCategory + #expect(types["Integer"] != nil) + #expect(types["Float"] != nil) + #expect(types["String"] != nil) + #expect(types["Date"] != nil) + #expect(types["Binary"] != nil) + #expect(types["Boolean"] != nil) + #expect(types["JSON"] != nil) + } +} + +// MARK: - Custom Override Verification + +@Suite("DriverPlugin Custom Overrides") +struct DriverPluginCustomOverridesTests { + @Test("Custom plugin overrides all defaults correctly") + func customOverrides() { + #expect(MockCustomPlugin.requiresAuthentication == false) + #expect(MockCustomPlugin.connectionMode == .fileBased) + #expect(MockCustomPlugin.urlSchemes == ["customdb"]) + #expect(MockCustomPlugin.fileExtensions == ["cdb", "customdb"]) + #expect(MockCustomPlugin.brandColorHex == "#FF0000") + #expect(MockCustomPlugin.queryLanguageName == "CQL") + #expect(MockCustomPlugin.editorLanguage == .custom("cypher")) + #expect(MockCustomPlugin.supportsForeignKeys == false) + #expect(MockCustomPlugin.supportsSchemaEditing == false) + #expect(MockCustomPlugin.supportsDatabaseSwitching == false) + #expect(MockCustomPlugin.supportsImport == false) + #expect(MockCustomPlugin.supportsExport == false) + #expect(MockCustomPlugin.databaseGroupingStrategy == .flat) + #expect(MockCustomPlugin.defaultGroupName == "default") + #expect(MockCustomPlugin.systemDatabaseNames == ["system", "internal"]) + } + + @Test("Empty arrays in columnTypesByCategory are preserved") + func emptyArraysPreserved() { + let types = MockCustomPlugin.columnTypesByCategory + #expect(types["Binary"] == []) + #expect(types["String"] == ["text"]) + #expect(types["Number"] == ["integer"]) + } + + @Test("Non-overridden values still use defaults") + func nonOverriddenDefaults() { + #expect(MockCustomPlugin.supportsSchemaSwitching == false) + #expect(MockCustomPlugin.supportsHealthMonitor == true) + #expect(MockCustomPlugin.systemSchemaNames.isEmpty) + } +} + +// NOTE: Per-plugin metadata tests (MySQL, PostgreSQL, etc.) cannot run in xcodebuild test +// because .tableplugin bundles are loaded at runtime by the main app, not the test runner. +// The protocol defaults and override mechanism are fully covered by the mock-based tests above. From c7137613e097fe9132eea9b03fb3fd90e7e1de95 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 11 Mar 2026 17:19:02 +0700 Subject: [PATCH 2/2] fix: add missing systemDatabaseNames to MongoDBPlugin --- Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift index 9d85800a..0da1dfbf 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift @@ -31,6 +31,7 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin { static let editorLanguage: EditorLanguage = .javascript static let supportsForeignKeys = false static let supportsSchemaEditing = false + static let systemDatabaseNames: [String] = ["admin", "local", "config"] static let databaseGroupingStrategy: GroupingStrategy = .flat static let columnTypesByCategory: [String: [String]] = [ "String": ["string", "objectId", "regex"],