Skip to content

Commit 48be4c7

Browse files
committed
Merge branch 'main' into feat/cassandra-support
2 parents 7593152 + 37a9a8e commit 48be4c7

133 files changed

Lines changed: 3646 additions & 1900 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/write-tests/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ private func makeCoordinator(database: String = "db_a", type: DatabaseType = .my
304304
tabManager: QueryTabManager(),
305305
changeManager: DataChangeManager(),
306306
filterStateManager: FilterStateManager(),
307+
columnVisibilityManager: ColumnVisibilityManager(),
307308
toolbarState: ConnectionToolbarState()
308309
)
309310
}

CHANGELOG.md

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

1010
### Changed
1111

12+
- Redesigned Plugins settings tab with HSplitView master-detail layout: plugin list on the left, detail pane on the right, matching macOS conventions
13+
- Replaced ~40 hardcoded `DatabaseType` switches across ~20 UI files with dynamic plugin property lookups via `PluginManager`, so third-party plugins get correct UI behavior (colors, labels, editor language, feature toggles) automatically
14+
- ConnectionFormView now fully dynamic: pgpass toggle, password visibility, and SSH/SSL tab visibility all driven by plugin metadata (`FieldSection`, `hidesPassword`, `supportsSSH`/`supportsSSL`) instead of hardcoded type checks
15+
- Replaced `AppState.isMongoDB`/`isRedis` booleans with `AppState.editorLanguage: EditorLanguage` for extensible editor language detection
16+
- Theme colors now derived from plugin `brandColorHex` instead of hardcoded `Theme.mysqlColor` etc.
17+
- Sidebar labels ("Tables"/"Collections"/"Keys"), toolbar preview labels, and AI prompt language detection now use plugin metadata
18+
- Connection form, database switcher, type picker, file open handler, and toolbar all use plugin lookups for connection mode, authentication, import support, and system database names
19+
- Converted `DatabaseType` from closed enum to string-based struct, enabling future plugin-defined database types
1220
- Moved string literal escaping into plugin drivers via `escapeStringLiteral` on `PluginDatabaseDriver` and `DatabaseDriver` protocols; `SQLEscaping.escapeStringLiteral` now uses ANSI SQL escaping only (doubles single quotes, strips null bytes)
1321
- SQL autocomplete data types and CREATE TABLE options now use plugin-provided dialect data instead of hardcoded per-database switches
1422
- `FilterSQLGenerator` now uses `SQLDialectDescriptor` data (regex syntax, boolean literals, LIKE escape style, pagination style) instead of `DatabaseType` switch statements
23+
- Moved identifier quoting, autocomplete statement completions, view templates, and FK disable/enable into plugin system
24+
- Removed `DatabaseType` switches from `FilterSQLGenerator`, `SQLCompletionProvider`, `ImportDataSinkAdapter`, and `MainContentCoordinator+SidebarActions`
25+
- Replaced hardcoded `DatabaseType` switches in ExportDialog, DataChangeManager, SafeModeGuard, ExportService, DataGridView, HighlightedSQLTextView, ForeignKeyPopoverContentView, QueryTab, SQLRowToStatementConverter, SessionStateFactory, ConnectionToolbarState, and DatabaseSwitcherSheet with dynamic plugin lookups (`databaseGroupingStrategy`, `immutableColumns`, `supportsReadOnlyMode`, `paginationStyle`, `editorLanguage`, `connectionMode`, `supportsSchemaSwitching`)
26+
- Replaced remaining ~40 hardcoded `DatabaseType` switches in MainContentCoordinator (main + 5 extensions) and StructureRowProvider with plugin metadata lookups (`requiresReconnectForDatabaseSwitch`, `structureColumnFields`, `defaultPrimaryKeyColumn`, `supportsQueryProgress`, `autoLimitStyle`, `allTablesMetadataSQL`)
1527

1628
### Added
1729

30+
- Column visibility: toggle individual columns on/off via "Columns" button in the status bar or right-click header context menu "Hide Column", with per-tab and per-table persistence
1831
- `SQLDialectDescriptor` in TableProPluginKit: plugins can now self-describe their SQL dialect (keywords, functions, data types, identifier quoting), with `SQLDialectFactory` preferring plugin-provided dialect info over built-in structs
1932
- DDL schema generation protocol in TableProPluginKit: plugins can now optionally provide database-specific ALTER TABLE syntax (ADD/MODIFY/DROP COLUMN, ADD/DROP INDEX, ADD/DROP FK, MODIFY PK) via `PluginDatabaseDriver`, with `SchemaStatementGenerator` trying plugin methods first before falling back to built-in logic
2033
- Plugin-provided table operations: `truncateTableStatements`, `dropObjectStatement`, `foreignKeyDisableStatements`, `foreignKeyEnableStatements` in `PluginDatabaseDriver` protocol, allowing plugins to override TRUNCATE, DROP, and FK handling SQL
34+
- `CompletionEntry` struct and `statementCompletions` on `DriverPlugin` for plugin-provided autocomplete entries (MongoDB MQL methods, Redis commands)
35+
- `offsetFetchOrderBy` property on `SQLDialectDescriptor` for plugin-controlled ORDER BY in OFFSET/FETCH pagination
36+
- `createViewTemplate()`, `editViewFallbackTemplate(viewName:)`, and `castColumnToText(_:)` on `PluginDatabaseDriver` for plugin-provided view DDL templates and column casting
2137
- `buildExplainQuery` method in `PluginDatabaseDriver` protocol: plugins can now provide database-specific EXPLAIN syntax, with coordinator falling back to built-in logic when plugin returns nil
2238
- `SettablePlugin` protocol in TableProPluginKit SDK: unified settings pattern for all plugins with automatic persistence via `loadSettings()`/`saveSettings()`, replacing duplicated boilerplate across export/import/driver plugins
2339
- 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
@@ -27,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2743
- SQL import options (wrap in transaction, disable FK checks) now persist across launches
2844
- `needsRestart` banner persists across app quit/relaunch after plugin uninstall
2945
- Copy as INSERT/UPDATE SQL statements from data grid context menu
46+
- Configurable font family and size for data grid (Settings > Data Grid > Font)
3047
- Plugin download count display in Browse Plugins — fetched from GitHub Releases API and cached for 1 hour
3148
- MSSQL query cancellation (`cancelQuery`) and lock timeout (`applyQueryTimeout`) support
3249
- `~/.pgpass` file support for PostgreSQL/Redshift connections with live validation in the connection form

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ When adding a new driver: create a new plugin bundle under `Plugins/`, implement
7070

7171
When adding a new method to the driver protocol: add to `PluginDatabaseDriver` (with default implementation), then update `PluginDriverAdapter` to bridge it to `DatabaseDriver`.
7272

73+
### DatabaseType (String-Based Struct)
74+
75+
`DatabaseType` is a string-based struct (not an enum). Key rules:
76+
- All `switch` statements on `DatabaseType` must include `default:` — the type is open
77+
- Use static constants (`.mysql`, `.postgresql`) for known types
78+
- Unknown types (from future plugins) are valid — they round-trip through Codable
79+
- Use `DatabaseType.allKnownTypes` (not `allCases`) for the canonical list of built-in types
80+
7381
### Editor Architecture (CodeEditSourceEditor)
7482

7583
- **`SQLEditorTheme`** — single source of truth for editor colors/fonts

Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ final class ClickHousePlugin: NSObject, TableProPlugin, DriverPlugin {
4242
"Geo": ["Point", "Ring", "Polygon", "MultiPolygon"]
4343
]
4444

45+
static let structureColumnFields: [StructureColumnField] = [.name, .type, .nullable, .defaultValue, .comment]
46+
static let supportsQueryProgress = true
47+
4548
static let sqlDialect: SQLDialectDescriptor? = SQLDialectDescriptor(
4649
identifierQuote: "`",
4750
keywords: [
@@ -91,7 +94,8 @@ final class ClickHousePlugin: NSObject, TableProPlugin, DriverPlugin {
9194
regexSyntax: .match,
9295
booleanLiteralStyle: .numeric,
9396
likeEscapeStyle: .implicit,
94-
paginationStyle: .limit
97+
paginationStyle: .limit,
98+
requiresBackslashEscaping: true
9599
)
96100

97101
func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver {
@@ -570,6 +574,23 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
570574
_ = try await execute(query: "CREATE DATABASE `\(escapedName)`")
571575
}
572576

577+
// MARK: - All Tables Metadata
578+
579+
func allTablesMetadataSQL(schema: String?) -> String? {
580+
"""
581+
SELECT
582+
database as `schema`,
583+
name,
584+
engine as kind,
585+
total_rows as estimated_rows,
586+
formatReadableSize(total_bytes) as total_size,
587+
comment
588+
FROM system.tables
589+
WHERE database = currentDatabase()
590+
ORDER BY name
591+
"""
592+
}
593+
573594
// MARK: - DML Statement Generation
574595

575596
func generateStatements(
@@ -724,6 +745,17 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
724745
"EXPLAIN \(sql)"
725746
}
726747

748+
// MARK: - View Templates
749+
750+
func createViewTemplate() -> String? {
751+
"CREATE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;"
752+
}
753+
754+
func editViewFallbackTemplate(viewName: String) -> String? {
755+
let quoted = quoteIdentifier(viewName)
756+
return "CREATE OR REPLACE VIEW \(quoted) AS\nSELECT * FROM table_name;"
757+
}
758+
727759
// MARK: - Kill Query
728760

729761
private func killQuery(queryId: String) {

Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class DuckDBPlugin: NSObject, TableProPlugin, DriverPlugin {
2424
static let requiresAuthentication = false
2525
static let connectionMode: ConnectionMode = .fileBased
2626
static let urlSchemes: [String] = ["duckdb"]
27-
static let fileExtensions: [String] = ["duckdb", "db"]
27+
static let fileExtensions: [String] = ["duckdb", "ddb"]
2828
static let brandColorHex = "#FFD900"
2929
static let supportsDatabaseSwitching = false
3030
static let systemDatabaseNames: [String] = ["information_schema", "pg_catalog"]
@@ -821,6 +821,32 @@ final class DuckDBPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
821821
"EXPLAIN \(sql)"
822822
}
823823

824+
// MARK: - View Templates
825+
826+
func createViewTemplate() -> String? {
827+
"CREATE OR REPLACE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;"
828+
}
829+
830+
func editViewFallbackTemplate(viewName: String) -> String? {
831+
let quoted = quoteIdentifier(viewName)
832+
return "CREATE OR REPLACE VIEW \(quoted) AS\nSELECT * FROM table_name;"
833+
}
834+
835+
// MARK: - All Tables Metadata
836+
837+
func allTablesMetadataSQL(schema: String?) -> String? {
838+
let s = schema ?? currentSchema ?? "main"
839+
return """
840+
SELECT
841+
table_schema as schema_name,
842+
table_name as name,
843+
table_type as kind
844+
FROM information_schema.tables
845+
WHERE table_schema = '\(s)'
846+
ORDER BY table_name
847+
"""
848+
}
849+
824850
// MARK: - Private Helpers
825851

826852
nonisolated private func setInterruptHandle(_ handle: duckdb_connection?) {

Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
2626

2727
static let brandColorHex = "#E34517"
2828
static let systemDatabaseNames: [String] = ["master", "tempdb", "model", "msdb"]
29+
static let defaultSchemaName = "dbo"
2930
static let databaseGroupingStrategy: GroupingStrategy = .bySchema
3031
static let columnTypesByCategory: [String: [String]] = [
3132
"Integer": ["TINYINT", "SMALLINT", "INT", "BIGINT"],
@@ -87,7 +88,8 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
8788
regexSyntax: .unsupported,
8889
booleanLiteralStyle: .numeric,
8990
likeEscapeStyle: .explicit,
90-
paginationStyle: .offsetFetch
91+
paginationStyle: .offsetFetch,
92+
autoLimitStyle: .top
9193
)
9294

9395
func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver {
@@ -428,6 +430,21 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
428430
return "[\(escaped)]"
429431
}
430432

433+
// MARK: - View Templates
434+
435+
func createViewTemplate() -> String? {
436+
"CREATE OR ALTER VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;"
437+
}
438+
439+
func editViewFallbackTemplate(viewName: String) -> String? {
440+
let quoted = quoteIdentifier(viewName)
441+
return "CREATE OR ALTER VIEW \(quoted) AS\nSELECT * FROM table_name;"
442+
}
443+
444+
func castColumnToText(_ column: String) -> String {
445+
"CAST(\(column) AS NVARCHAR(MAX))"
446+
}
447+
431448
init(config: DriverConnectionConfig) {
432449
self.config = config
433450
self._currentSchema = config.additionalFields["mssqlSchema"]?.isEmpty == false
@@ -1160,6 +1177,27 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
11601177
_ = try await execute(query: "CREATE DATABASE \(quotedName)")
11611178
}
11621179

1180+
// MARK: - All Tables Metadata
1181+
1182+
func allTablesMetadataSQL(schema: String?) -> String? {
1183+
"""
1184+
SELECT
1185+
s.name as schema_name,
1186+
t.name as name,
1187+
CASE WHEN v.object_id IS NOT NULL THEN 'VIEW' ELSE 'TABLE' END as kind,
1188+
p.rows as estimated_rows,
1189+
CAST(ROUND(SUM(a.total_pages) * 8 / 1024.0, 2) AS VARCHAR) + ' MB' as total_size
1190+
FROM sys.tables t
1191+
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
1192+
INNER JOIN sys.indexes i ON t.object_id = i.object_id AND i.index_id IN (0, 1)
1193+
INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
1194+
INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
1195+
LEFT JOIN sys.views v ON t.object_id = v.object_id
1196+
GROUP BY s.name, t.name, p.rows, v.object_id
1197+
ORDER BY t.name
1198+
"""
1199+
}
1200+
11631201
// MARK: - Query Building
11641202

11651203
func buildBrowseQuery(

Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin {
5353
static let supportsForeignKeys = false
5454
static let supportsSchemaEditing = false
5555
static let systemDatabaseNames: [String] = ["admin", "local", "config"]
56+
static let tableEntityName = "Collections"
57+
static let supportsForeignKeyDisable = false
58+
static let immutableColumns: [String] = ["_id"]
59+
static let supportsReadOnlyMode = false
5660
static let databaseGroupingStrategy: GroupingStrategy = .flat
5761
static let columnTypesByCategory: [String: [String]] = [
5862
"String": ["string", "objectId", "regex"],
@@ -66,8 +70,38 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin {
6670
"Other": ["javascript", "minKey", "maxKey"]
6771
]
6872

73+
static let structureColumnFields: [StructureColumnField] = [.name, .type, .nullable]
74+
static let defaultPrimaryKeyColumn: String? = "_id"
75+
6976
static let sqlDialect: SQLDialectDescriptor? = nil
7077

78+
static var statementCompletions: [CompletionEntry] {
79+
[
80+
CompletionEntry(label: "db.", insertText: "db."),
81+
CompletionEntry(label: "db.runCommand", insertText: "db.runCommand"),
82+
CompletionEntry(label: "db.adminCommand", insertText: "db.adminCommand"),
83+
CompletionEntry(label: "db.createView", insertText: "db.createView"),
84+
CompletionEntry(label: "db.createCollection", insertText: "db.createCollection"),
85+
CompletionEntry(label: "show dbs", insertText: "show dbs"),
86+
CompletionEntry(label: "show collections", insertText: "show collections"),
87+
CompletionEntry(label: ".find", insertText: ".find"),
88+
CompletionEntry(label: ".findOne", insertText: ".findOne"),
89+
CompletionEntry(label: ".aggregate", insertText: ".aggregate"),
90+
CompletionEntry(label: ".insertOne", insertText: ".insertOne"),
91+
CompletionEntry(label: ".insertMany", insertText: ".insertMany"),
92+
CompletionEntry(label: ".updateOne", insertText: ".updateOne"),
93+
CompletionEntry(label: ".updateMany", insertText: ".updateMany"),
94+
CompletionEntry(label: ".deleteOne", insertText: ".deleteOne"),
95+
CompletionEntry(label: ".deleteMany", insertText: ".deleteMany"),
96+
CompletionEntry(label: ".replaceOne", insertText: ".replaceOne"),
97+
CompletionEntry(label: ".findOneAndUpdate", insertText: ".findOneAndUpdate"),
98+
CompletionEntry(label: ".findOneAndReplace", insertText: ".findOneAndReplace"),
99+
CompletionEntry(label: ".findOneAndDelete", insertText: ".findOneAndDelete"),
100+
CompletionEntry(label: ".countDocuments", insertText: ".countDocuments"),
101+
CompletionEntry(label: ".createIndex", insertText: ".createIndex")
102+
]
103+
}
104+
71105
func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver {
72106
MongoDBPluginDriver(config: config)
73107
}

Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,17 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
499499
}
500500
}
501501

502+
// MARK: - View Templates
503+
504+
func createViewTemplate() -> String? {
505+
"db.createView(\"view_name\", \"source_collection\", [\n {\"$match\": {}},\n {\"$project\": {\"_id\": 1}}\n])"
506+
}
507+
508+
func editViewFallbackTemplate(viewName: String) -> String? {
509+
let escaped = viewName.replacingOccurrences(of: "\"", with: "\\\"")
510+
return "db.runCommand({\"collMod\": \"\(escaped)\", \"viewOn\": \"source_collection\", \"pipeline\": [{\"$match\": {}}]})"
511+
}
512+
502513
// MARK: - Query Building
503514

504515
func buildBrowseQuery(

Plugins/MySQLDriverPlugin/MySQLPlugin.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ final class MySQLPlugin: NSObject, TableProPlugin, DriverPlugin {
8181
regexSyntax: .regexp,
8282
booleanLiteralStyle: .numeric,
8383
likeEscapeStyle: .implicit,
84-
paginationStyle: .limit
84+
paginationStyle: .limit,
85+
requiresBackslashEscaping: true
8586
)
8687

8788
func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver {

Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,54 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
602602
"EXPLAIN \(sql)"
603603
}
604604

605+
// MARK: - View Templates
606+
607+
func createViewTemplate() -> String? {
608+
"CREATE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;"
609+
}
610+
611+
func editViewFallbackTemplate(viewName: String) -> String? {
612+
let quoted = quoteIdentifier(viewName)
613+
return "ALTER VIEW \(quoted) AS\nSELECT * FROM table_name;"
614+
}
615+
616+
func castColumnToText(_ column: String) -> String {
617+
"CAST(\(column) AS CHAR)"
618+
}
619+
620+
// MARK: - Foreign Key Checks
621+
622+
func foreignKeyDisableStatements() -> [String]? {
623+
["SET FOREIGN_KEY_CHECKS=0"]
624+
}
625+
626+
func foreignKeyEnableStatements() -> [String]? {
627+
["SET FOREIGN_KEY_CHECKS=1"]
628+
}
629+
630+
// MARK: - All Tables Metadata
631+
632+
func allTablesMetadataSQL(schema: String?) -> String? {
633+
"""
634+
SELECT
635+
TABLE_SCHEMA as `schema`,
636+
TABLE_NAME as name,
637+
TABLE_TYPE as kind,
638+
IFNULL(CCSA.CHARACTER_SET_NAME, '') as charset,
639+
TABLE_COLLATION as collation,
640+
TABLE_ROWS as estimated_rows,
641+
CONCAT(ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2), ' MB') as total_size,
642+
CONCAT(ROUND(DATA_LENGTH / 1024 / 1024, 2), ' MB') as data_size,
643+
CONCAT(ROUND(INDEX_LENGTH / 1024 / 1024, 2), ' MB') as index_size,
644+
TABLE_COMMENT as comment
645+
FROM information_schema.TABLES
646+
LEFT JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY CCSA
647+
ON TABLE_COLLATION = CCSA.COLLATION_NAME
648+
WHERE TABLE_SCHEMA = DATABASE()
649+
ORDER BY TABLE_NAME
650+
"""
651+
}
652+
605653
// MARK: - Private Helpers
606654

607655
private func extractTableName(from query: String) -> String? {

0 commit comments

Comments
 (0)