Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Build Plugin

on:
push:
tags: ["plugin-*-v*"]

env:
XCODE_PROJECT: TablePro.xcodeproj

jobs:
build-plugin:
name: Build Plugin
runs-on: self-hosted
timeout-minutes: 30

steps:
- name: Extract plugin info from tag
id: plugin-info
run: |
TAG="${GITHUB_REF#refs/tags/}"
# Tag format: plugin-<name>-v<version>
# e.g., plugin-oracle-v1.0.0 -> OracleDriverPlugin
PLUGIN_NAME=$(echo "$TAG" | sed -E 's/^plugin-([a-z]+)-v.*$/\1/')
VERSION=$(echo "$TAG" | sed -E 's/^plugin-[a-z]+-v(.*)$/\1/')

# Map short name to Xcode target
case "$PLUGIN_NAME" in
oracle) TARGET="OracleDriverPlugin" ;;
clickhouse) TARGET="ClickHouseDriverPlugin" ;;
*) echo "Unknown plugin: $PLUGIN_NAME"; exit 1 ;;
esac

echo "target=$TARGET" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "plugin_name=$PLUGIN_NAME" >> "$GITHUB_OUTPUT"
echo "Building $TARGET v$VERSION"

- name: Install Git LFS
run: brew list git-lfs &>/dev/null || brew install git-lfs; git lfs install

- name: Checkout code
uses: actions/checkout@v4
with:
lfs: true

- name: Pull LFS files
run: git lfs pull

- name: Build plugin (ARM64)
run: ./scripts/build-plugin.sh "${{ steps.plugin-info.outputs.target }}" arm64

- name: Build plugin (x86_64)
run: ./scripts/build-plugin.sh "${{ steps.plugin-info.outputs.target }}" x86_64

- name: Notarize
if: env.NOTARIZE == 'true'
env:
NOTARIZE: "true"
run: |
for zip in build/Plugins/*.zip; do
xcrun notarytool submit "$zip" \
--apple-id "$APPLE_ID" \
--team-id "D7HJ5TFYCU" \
--keychain-profile "notarytool-profile" \
--wait
done

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "${{ steps.plugin-info.outputs.target }} v${{ steps.plugin-info.outputs.version }}"
body: |
## ${{ steps.plugin-info.outputs.target }} v${{ steps.plugin-info.outputs.version }}

Plugin release for TablePro.

### Installation
Download the ZIP for your architecture and install via **Settings > Plugins > Install from File**.
files: build/Plugins/*.zip
draft: false
prerelease: false
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Import plugin system: SQL import extracted into a `.tableplugin` bundle, matching the export plugin architecture
- `ImportFormatPlugin` protocol in TableProPluginKit for building custom import format plugins
- SQLImportPlugin as the first import format plugin (SQL files and .gz compressed SQL)
- Oracle and ClickHouse shipped as downloadable plugins, reducing app bundle size for most users
- Plugin install prompt when connecting to a database whose driver plugin is not installed
- `databaseTypeIds` field on registry plugins for mapping registry entries to database types
- `build-plugin.sh` script and `build-plugin.yml` CI workflow for building standalone plugin releases

## [0.16.1] - 2026-03-09

Expand Down
4 changes: 0 additions & 4 deletions TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@
/* Begin PBXBuildFile section */
5A860000A00000000 /* TableProPluginKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A861000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5A861000D00000000 /* OracleDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A861000100000000 /* OracleDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A862000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5A862000D00000000 /* SQLiteDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A862000100000000 /* SQLiteDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A863000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5A863000D00000000 /* ClickHouseDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A863000100000000 /* ClickHouseDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A864000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5A864000D00000000 /* MSSQLDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A864000100000000 /* MSSQLDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A865000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
Expand Down Expand Up @@ -166,9 +164,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
5A861000D00000000 /* OracleDriver.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 */,
5A865000D00000000 /* MySQLDriver.tableplugin in Copy Plug-Ins */,
5A866000D00000000 /* MongoDBDriver.tableplugin in Copy Plug-Ins */,
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Core/Database/DatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ enum DatabaseDriverFactory {
PluginManager.shared.loadPendingPlugins()
}
guard let plugin = PluginManager.shared.driverPlugins[pluginId] else {
if connection.type.isDownloadablePlugin {
throw PluginError.pluginNotInstalled(connection.type.rawValue)
}
throw DatabaseError.connectionFailed(
"\(pluginId) driver plugin not loaded. The plugin may be disabled or missing from the PlugIns directory."
)
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Core/Plugins/PluginError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ enum PluginError: LocalizedError {
case pluginConflict(existingName: String)
case appVersionTooOld(minimumRequired: String, currentApp: String)
case downloadFailed(String)
case pluginNotInstalled(String)
case incompatibleWithCurrentApp(minimumRequired: String)

var errorDescription: String? {
Expand All @@ -43,6 +44,8 @@ enum PluginError: LocalizedError {
return String(localized: "Plugin requires app version \(minimumRequired) or later, but current version is \(currentApp)")
case .downloadFailed(let reason):
return String(localized: "Plugin download failed: \(reason)")
case .pluginNotInstalled(let databaseType):
return String(localized: "The \(databaseType) plugin is not installed. You can download it from the plugin marketplace.")
case .incompatibleWithCurrentApp(let minimumRequired):
return String(localized: "This plugin requires TablePro \(minimumRequired) or later")
}
Expand Down
46 changes: 46 additions & 0 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,52 @@ final class PluginManager {
}
}

// MARK: - Driver Availability

func isDriverAvailable(for databaseType: DatabaseType) -> Bool {
loadPendingPlugins()
return driverPlugins[databaseType.pluginTypeId] != nil
}

func installMissingPlugin(
for databaseType: DatabaseType,
progress: @escaping @MainActor @Sendable (Double) -> Void
) async throws {
let pluginTypeId = databaseType.pluginTypeId

if let existingEntry = plugins.first(where: { entry in
entry.databaseTypeId == pluginTypeId || entry.additionalTypeIds.contains(pluginTypeId)
}) {
if !existingEntry.isEnabled {
setEnabled(true, pluginId: existingEntry.id)
}
if driverPlugins[pluginTypeId] != nil {
Self.logger.info("Re-enabled existing plugin '\(existingEntry.name)' for '\(databaseType.rawValue)'")
return
}
Self.logger.warning("Plugin '\(existingEntry.id)' exists but driver not registered, reinstalling")
if existingEntry.source == .userInstalled {
try? uninstallPlugin(id: existingEntry.id)
}
}

let registryClient = RegistryClient.shared
await registryClient.fetchManifest()

guard let manifest = registryClient.manifest else {
throw PluginError.downloadFailed(String(localized: "Could not fetch plugin registry"))
}

guard let registryPlugin = manifest.plugins.first(where: { plugin in
plugin.databaseTypeIds?.contains(pluginTypeId) == true
}) else {
throw PluginError.notFound
}

let entry = try await installFromRegistry(registryPlugin, progress: progress)
Self.logger.info("Installed missing plugin '\(entry.name)' for database type '\(databaseType.rawValue)'")
}

// MARK: - Enable / Disable

func setEnabled(_ enabled: Bool, pluginId: String) {
Expand Down
1 change: 1 addition & 0 deletions TablePro/Core/Plugins/Registry/RegistryModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct RegistryPlugin: Codable, Sendable, Identifiable {
let author: RegistryAuthor
let homepage: String?
let category: RegistryCategory
let databaseTypeIds: [String]?
let downloadURL: String
let sha256: String
let minAppVersion: String?
Expand Down
7 changes: 7 additions & 0 deletions TablePro/Models/Connection/DatabaseConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ enum DatabaseType: String, CaseIterable, Identifiable, Codable {
}
}

var isDownloadablePlugin: Bool {
switch self {
case .oracle, .clickhouse: return true
default: return false
}
}

/// Asset name for each database type icon
var iconName: String {
switch self {
Expand Down
Loading