From f919ab453cc922e7a29ff833e18515da31c833b3 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 24 Mar 2026 21:02:50 +0700 Subject: [PATCH] feat: bundle ClickHouse, MSSQL, Redis, XLSX, MQL, SQLImport plugins as built-in --- CLAUDE.md | 8 +-- TablePro.xcodeproj/project.pbxproj | 77 +++++++++++++++++++++++ TablePro/Core/Plugins/PluginManager.swift | 36 +++++++++++ 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7dd37b2a..457e0a2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co TablePro is a native macOS database client (SwiftUI + AppKit) — a fast, lightweight alternative to TablePlus. macOS 14.0+, Swift 5.9, Universal Binary (arm64 + x86_64). - **Source**: `TablePro/` — `Core/` (business logic, services), `Views/` (UI), `Models/` (data structures), `ViewModels/`, `Extensions/`, `Theme/` -- **Plugins**: `Plugins/` — `.tableplugin` bundles + `TableProPluginKit` shared framework. Built-in (bundled in app): MySQL, PostgreSQL, SQLite, CSV, JSON, SQL export. Separately distributed via plugin registry: ClickHouse, MSSQL, MongoDB, Redis, Oracle, DuckDB, XLSX, MQL, SQLImport +- **Plugins**: `Plugins/` — `.tableplugin` bundles + `TableProPluginKit` shared framework. Built-in (bundled in app): MySQL, PostgreSQL, SQLite, ClickHouse, MSSQL, Redis, CSV, JSON, SQL export, XLSX, MQL, SQLImport, DynamoDB. Separately distributed via plugin registry: MongoDB, Oracle, DuckDB, Cassandra, Etcd, CloudflareD1 - **C bridges**: Each plugin contains its own C bridge module (e.g., `Plugins/MySQLDriverPlugin/CMariaDB/`, `Plugins/PostgreSQLDriverPlugin/CLibPQ/`) - **Static libs**: `Libs/` — pre-built `libmariadb*.a`, `libpq*.a`, etc. Downloaded from GitHub Releases via `scripts/download-libs.sh` (not in git) - **SPM deps**: CodeEditSourceEditor (`main` branch, tree-sitter editor), Sparkle (2.8.1, auto-update), OracleNIO. Managed via Xcode, no `Package.swift`. @@ -79,10 +79,10 @@ Plugin bundles under `Plugins/`: | MySQLDriverPlugin | MySQL, MariaDB | CMariaDB | Built-in | | PostgreSQLDriverPlugin | PostgreSQL, Redshift | CLibPQ | Built-in | | SQLiteDriverPlugin | SQLite | (Foundation sqlite3) | Built-in | -| ClickHouseDriverPlugin | ClickHouse | (URLSession HTTP) | Registry | -| MSSQLDriverPlugin | SQL Server | CFreeTDS | Registry | +| ClickHouseDriverPlugin | ClickHouse | (URLSession HTTP) | Built-in | +| MSSQLDriverPlugin | SQL Server | CFreeTDS | Built-in | +| RedisDriverPlugin | Redis | CRedis | Built-in | | MongoDBDriverPlugin | MongoDB | CLibMongoc | Registry | -| RedisDriverPlugin | Redis | CRedis | Registry | | DuckDBDriverPlugin | DuckDB | CDuckDB | Registry | | OracleDriverPlugin | Oracle | OracleNIO (SPM) | Registry | diff --git a/TablePro.xcodeproj/project.pbxproj b/TablePro.xcodeproj/project.pbxproj index f18be40c..24bb2624 100644 --- a/TablePro.xcodeproj/project.pbxproj +++ b/TablePro.xcodeproj/project.pbxproj @@ -26,6 +26,12 @@ 5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86B000100000000 /* JSONExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5A86C000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; }; 5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86C000100000000 /* SQLExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5A863000D00000000 /* ClickHouseDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A863000100000000 /* ClickHouseDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5A864000D00000000 /* MSSQLDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A864000100000000 /* MSSQLDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5A867000D00000000 /* RedisDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A867000100000000 /* RedisDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5A86D000D00000000 /* XLSXExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86D000100000000 /* XLSXExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5A86E000D00000000 /* MQLExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86E000100000000 /* MQLExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5A86F000D00000000 /* SQLImport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86F000100000000 /* SQLImport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5A86D000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; }; 5A86E000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; }; 5A86F000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; }; @@ -126,6 +132,41 @@ remoteGlobalIDString = 5A86C000000000000; remoteInfo = SQLExport; }; + 5A864000B00000000 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5A864000000000000; + remoteInfo = MSSQLDriver; + }; + 5A867000B00000000 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5A867000000000000; + remoteInfo = RedisDriver; + }; + 5A86D000B00000000 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5A86D000000000000; + remoteInfo = XLSXExport; + }; + 5A86E000B00000000 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5A86E000000000000; + remoteInfo = MQLExport; + }; + 5A86F000B00000000 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5A86F000000000000; + remoteInfo = SQLImport; + }; 5ABCC5AB2F43856700EAF3FC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; @@ -152,9 +193,15 @@ 5A865000D00000000 /* MySQLDriver.tableplugin in Copy Plug-Ins */, 5A868000D00000000 /* PostgreSQLDriver.tableplugin in Copy Plug-Ins */, 5A862000D00000000 /* SQLiteDriver.tableplugin in Copy Plug-Ins */, + 5A863000D00000000 /* ClickHouseDriver.tableplugin in Copy Plug-Ins */, + 5A864000D00000000 /* MSSQLDriver.tableplugin in Copy Plug-Ins */, + 5A867000D00000000 /* RedisDriver.tableplugin in Copy Plug-Ins */, 5A86A000D00000000 /* CSVExport.tableplugin in Copy Plug-Ins */, 5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */, 5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */, + 5A86D000D00000000 /* XLSXExport.tableplugin in Copy Plug-Ins */, + 5A86E000D00000000 /* MQLExport.tableplugin in Copy Plug-Ins */, + 5A86F000D00000000 /* SQLImport.tableplugin in Copy Plug-Ins */, 5ADDB00000000000000000D0 /* DynamoDBDriverPlugin.tableplugin in Copy Plug-Ins */, ); name = "Copy Plug-Ins"; @@ -823,12 +870,17 @@ 5A861000C00000000 /* PBXTargetDependency */, 5A862000C00000000 /* PBXTargetDependency */, 5A863000C00000000 /* PBXTargetDependency */, + 5A864000C00000000 /* PBXTargetDependency */, 5A865000C00000000 /* PBXTargetDependency */, + 5A867000C00000000 /* PBXTargetDependency */, 5A868000C00000000 /* PBXTargetDependency */, 5A869000C00000000 /* PBXTargetDependency */, 5A86A000C00000000 /* PBXTargetDependency */, 5A86B000C00000000 /* PBXTargetDependency */, 5A86C000C00000000 /* PBXTargetDependency */, + 5A86D000C00000000 /* PBXTargetDependency */, + 5A86E000C00000000 /* PBXTargetDependency */, + 5A86F000C00000000 /* PBXTargetDependency */, 5ADDB00000000000000000C1 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -1771,6 +1823,31 @@ target = 5A86C000000000000 /* SQLExport */; targetProxy = 5A86C000B00000000 /* PBXContainerItemProxy */; }; + 5A864000C00000000 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5A864000000000000 /* MSSQLDriver */; + targetProxy = 5A864000B00000000 /* PBXContainerItemProxy */; + }; + 5A867000C00000000 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5A867000000000000 /* RedisDriver */; + targetProxy = 5A867000B00000000 /* PBXContainerItemProxy */; + }; + 5A86D000C00000000 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5A86D000000000000 /* XLSXExport */; + targetProxy = 5A86D000B00000000 /* PBXContainerItemProxy */; + }; + 5A86E000C00000000 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5A86E000000000000 /* MQLExport */; + targetProxy = 5A86E000B00000000 /* PBXContainerItemProxy */; + }; + 5A86F000C00000000 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5A86F000000000000 /* SQLImport */; + targetProxy = 5A86F000B00000000 /* PBXContainerItemProxy */; + }; 5ABCC5AC2F43856700EAF3FC /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5A1091C62EF17EDC0055EA7C /* TablePro */; diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 21deb5e2..ea69adf0 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -269,6 +269,7 @@ final class PluginManager { if let builtInDir = builtInPluginsDir { discoverPlugins(from: builtInDir, source: .builtIn) + removeUserInstalledDuplicates(builtInDir: builtInDir) } discoverPlugins(from: userPluginsDir, source: .userInstalled) @@ -320,6 +321,41 @@ final class PluginManager { } } + /// Remove user-installed plugins that now ship as built-in to avoid dead weight. + private func removeUserInstalledDuplicates(builtInDir: URL) { + let fm = FileManager.default + guard let builtInBundles = try? fm.contentsOfDirectory( + at: builtInDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) else { return } + + var builtInBundleIds = Set() + for url in builtInBundles where url.pathExtension == "tableplugin" { + if let bundle = Bundle(url: url), let id = bundle.bundleIdentifier { + builtInBundleIds.insert(id) + } + } + + guard let userPlugins = try? fm.contentsOfDirectory( + at: userPluginsDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) else { return } + + for url in userPlugins where url.pathExtension == "tableplugin" { + guard let bundle = Bundle(url: url), let id = bundle.bundleIdentifier else { continue } + if builtInBundleIds.contains(id) { + do { + try fm.removeItem(at: url) + Self.logger.info("Removed user-installed '\(id)' — now ships as built-in") + } catch { + Self.logger.warning("Failed to remove duplicate plugin '\(id)': \(error.localizedDescription)") + } + } + } + } + private func discoverPlugin(at url: URL, source: PluginSource) throws { guard let bundle = Bundle(url: url) else { throw PluginError.invalidBundle("Cannot create bundle from \(url.lastPathComponent)")