Skip to content

Commit 523449f

Browse files
committed
feat: ship Oracle and ClickHouse as downloadable plugins
1 parent f961a21 commit 523449f

File tree

16 files changed

+11265
-10830
lines changed

16 files changed

+11265
-10830
lines changed

.github/workflows/build-plugin.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Build Plugin
2+
3+
on:
4+
push:
5+
tags: ["plugin-*-v*"]
6+
7+
env:
8+
XCODE_PROJECT: TablePro.xcodeproj
9+
10+
jobs:
11+
build-plugin:
12+
name: Build Plugin
13+
runs-on: self-hosted
14+
timeout-minutes: 30
15+
16+
steps:
17+
- name: Extract plugin info from tag
18+
id: plugin-info
19+
run: |
20+
TAG="${GITHUB_REF#refs/tags/}"
21+
# Tag format: plugin-<name>-v<version>
22+
# e.g., plugin-oracle-v1.0.0 -> OracleDriverPlugin
23+
PLUGIN_NAME=$(echo "$TAG" | sed -E 's/^plugin-([a-z]+)-v.*$/\1/')
24+
VERSION=$(echo "$TAG" | sed -E 's/^plugin-[a-z]+-v(.*)$/\1/')
25+
26+
# Map short name to Xcode target
27+
case "$PLUGIN_NAME" in
28+
oracle) TARGET="OracleDriverPlugin" ;;
29+
clickhouse) TARGET="ClickHouseDriverPlugin" ;;
30+
*) echo "Unknown plugin: $PLUGIN_NAME"; exit 1 ;;
31+
esac
32+
33+
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
34+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
35+
echo "plugin_name=$PLUGIN_NAME" >> "$GITHUB_OUTPUT"
36+
echo "Building $TARGET v$VERSION"
37+
38+
- name: Install Git LFS
39+
run: brew list git-lfs &>/dev/null || brew install git-lfs; git lfs install
40+
41+
- name: Checkout code
42+
uses: actions/checkout@v4
43+
with:
44+
lfs: true
45+
46+
- name: Pull LFS files
47+
run: git lfs pull
48+
49+
- name: Build plugin (ARM64)
50+
run: ./scripts/build-plugin.sh "${{ steps.plugin-info.outputs.target }}" arm64
51+
52+
- name: Build plugin (x86_64)
53+
run: ./scripts/build-plugin.sh "${{ steps.plugin-info.outputs.target }}" x86_64
54+
55+
- name: Notarize
56+
if: env.NOTARIZE == 'true'
57+
env:
58+
NOTARIZE: "true"
59+
run: |
60+
for zip in build/Plugins/*.zip; do
61+
xcrun notarytool submit "$zip" \
62+
--apple-id "$APPLE_ID" \
63+
--team-id "D7HJ5TFYCU" \
64+
--keychain-profile "notarytool-profile" \
65+
--wait
66+
done
67+
68+
- name: Create GitHub Release
69+
uses: softprops/action-gh-release@v2
70+
with:
71+
name: "${{ steps.plugin-info.outputs.target }} v${{ steps.plugin-info.outputs.version }}"
72+
body: |
73+
## ${{ steps.plugin-info.outputs.target }} v${{ steps.plugin-info.outputs.version }}
74+
75+
Plugin release for TablePro.
76+
77+
### Installation
78+
Download the ZIP for your architecture and install via **Settings > Plugins > Install from File**.
79+
files: build/Plugins/*.zip
80+
draft: false
81+
prerelease: false

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Import plugin system: SQL import extracted into a `.tableplugin` bundle, matching the export plugin architecture
1313
- `ImportFormatPlugin` protocol in TableProPluginKit for building custom import format plugins
1414
- SQLImportPlugin as the first import format plugin (SQL files and .gz compressed SQL)
15+
- Oracle and ClickHouse shipped as downloadable plugins, reducing app bundle size for most users
16+
- Plugin install prompt when connecting to a database whose driver plugin is not installed
17+
- `databaseTypeIds` field on registry plugins for mapping registry entries to database types
18+
- `build-plugin.sh` script and `build-plugin.yml` CI workflow for building standalone plugin releases
1519

1620
## [0.16.1] - 2026-03-09
1721

TablePro.xcodeproj/project.pbxproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@
99
/* Begin PBXBuildFile section */
1010
5A860000A00000000 /* TableProPluginKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1111
5A861000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
12-
5A861000D00000000 /* OracleDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A861000100000000 /* OracleDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1312
5A862000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
1413
5A862000D00000000 /* SQLiteDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A862000100000000 /* SQLiteDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1514
5A863000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
16-
5A863000D00000000 /* ClickHouseDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A863000100000000 /* ClickHouseDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1715
5A864000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
1816
5A864000D00000000 /* MSSQLDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A864000100000000 /* MSSQLDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1917
5A865000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
@@ -166,9 +164,7 @@
166164
dstPath = "";
167165
dstSubfolderSpec = 13;
168166
files = (
169-
5A861000D00000000 /* OracleDriver.tableplugin in Copy Plug-Ins */,
170167
5A862000D00000000 /* SQLiteDriver.tableplugin in Copy Plug-Ins */,
171-
5A863000D00000000 /* ClickHouseDriver.tableplugin in Copy Plug-Ins */,
172168
5A864000D00000000 /* MSSQLDriver.tableplugin in Copy Plug-Ins */,
173169
5A865000D00000000 /* MySQLDriver.tableplugin in Copy Plug-Ins */,
174170
5A866000D00000000 /* MongoDBDriver.tableplugin in Copy Plug-Ins */,

TablePro/Core/Database/DatabaseDriver.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ enum DatabaseDriverFactory {
301301
PluginManager.shared.loadPendingPlugins()
302302
}
303303
guard let plugin = PluginManager.shared.driverPlugins[pluginId] else {
304+
if connection.type.isDownloadablePlugin {
305+
throw PluginError.pluginNotInstalled(connection.type.rawValue)
306+
}
304307
throw DatabaseError.connectionFailed(
305308
"\(pluginId) driver plugin not loaded. The plugin may be disabled or missing from the PlugIns directory."
306309
)

TablePro/Core/Plugins/PluginError.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum PluginError: LocalizedError {
1717
case pluginConflict(existingName: String)
1818
case appVersionTooOld(minimumRequired: String, currentApp: String)
1919
case downloadFailed(String)
20+
case pluginNotInstalled(String)
2021
case incompatibleWithCurrentApp(minimumRequired: String)
2122

2223
var errorDescription: String? {
@@ -43,6 +44,8 @@ enum PluginError: LocalizedError {
4344
return String(localized: "Plugin requires app version \(minimumRequired) or later, but current version is \(currentApp)")
4445
case .downloadFailed(let reason):
4546
return String(localized: "Plugin download failed: \(reason)")
47+
case .pluginNotInstalled(let databaseType):
48+
return String(localized: "The \(databaseType) plugin is not installed. You can download it from the plugin marketplace.")
4649
case .incompatibleWithCurrentApp(let minimumRequired):
4750
return String(localized: "This plugin requires TablePro \(minimumRequired) or later")
4851
}

TablePro/Core/Plugins/PluginManager.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,35 @@ final class PluginManager {
282282
}
283283
}
284284

285+
// MARK: - Driver Availability
286+
287+
func isDriverAvailable(for databaseType: DatabaseType) -> Bool {
288+
loadPendingPlugins()
289+
return driverPlugins[databaseType.pluginTypeId] != nil
290+
}
291+
292+
func installMissingPlugin(
293+
for databaseType: DatabaseType,
294+
progress: @escaping @MainActor @Sendable (Double) -> Void
295+
) async throws {
296+
let registryClient = RegistryClient.shared
297+
await registryClient.fetchManifest()
298+
299+
guard let manifest = registryClient.manifest else {
300+
throw PluginError.downloadFailed(String(localized: "Could not fetch plugin registry"))
301+
}
302+
303+
let pluginTypeId = databaseType.pluginTypeId
304+
guard let registryPlugin = manifest.plugins.first(where: { plugin in
305+
plugin.databaseTypeIds?.contains(pluginTypeId) == true
306+
}) else {
307+
throw PluginError.notFound
308+
}
309+
310+
let entry = try await installFromRegistry(registryPlugin, progress: progress)
311+
Self.logger.info("Installed missing plugin '\(entry.name)' for database type '\(databaseType.rawValue)'")
312+
}
313+
285314
// MARK: - Enable / Disable
286315

287316
func setEnabled(_ enabled: Bool, pluginId: String) {

TablePro/Core/Plugins/Registry/RegistryModels.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct RegistryPlugin: Codable, Sendable, Identifiable {
1818
let author: RegistryAuthor
1919
let homepage: String?
2020
let category: RegistryCategory
21+
let databaseTypeIds: [String]?
2122
let downloadURL: String
2223
let sha256: String
2324
let minAppVersion: String?

TablePro/Models/Connection/DatabaseConnection.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,13 @@ enum DatabaseType: String, CaseIterable, Identifiable, Codable {
226226
}
227227
}
228228

229+
var isDownloadablePlugin: Bool {
230+
switch self {
231+
case .oracle, .clickhouse: return true
232+
default: return false
233+
}
234+
}
235+
229236
/// Asset name for each database type icon
230237
var iconName: String {
231238
switch self {

0 commit comments

Comments
 (0)