> ## Documentation Index
> Fetch the complete documentation index at: https://ngquct-feat-sql-code-folding.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin Development

> Write a database driver plugin, implement the PluginKit protocols, and package it as a .tableplugin bundle

A TablePro database driver is a `.tableplugin` bundle built against **TableProPluginKit** (`Plugins/TableProPluginKit/`). You implement two protocols: `DriverPlugin` describes the database (name, port, capabilities, SQL dialect) and creates driver instances; `PluginDatabaseDriver` is the connection itself (connect, query, fetch schema). The app bridges your driver to its internal `DatabaseDriver` protocol through `PluginDriverAdapter` and resolves it by database type ID, so a working plugin gets the full UI: connection form, sidebar, data grid, editor, import and export.

The best starting point is an existing plugin. `Plugins/SurrealDBDriverPlugin/` is a compact reference with no C bridge: it talks HTTP, implements the schema-aware query-building hooks, and ships a custom editor language.

## Bundle layout

A plugin is a macOS loadable bundle target with `WRAPPER_EXTENSION = tableplugin`, linking TableProPluginKit. The principal class (set via `INFOPLIST_KEY_NSPrincipalClass`) must be your `DriverPlugin` class. `Info.plist` keys:

| Key                               | Type             | Required    | Purpose                                                                         |
| --------------------------------- | ---------------- | ----------- | ------------------------------------------------------------------------------- |
| `TableProPluginKitVersion`        | integer          | Yes         | PluginKit ABI version the plugin was built against (18, unchanged since 0.48.0) |
| `TableProProvidesDatabaseTypeIds` | array of strings | Recommended | Database type IDs the plugin serves. Enables lazy loading.                      |
| `TableProMinAppVersion`           | string           | No          | Loader rejects the plugin on older app versions                                 |
| `CFBundleShortVersionString`      | string           | Yes         | Plugin version, used for registry update checks                                 |

Without `TableProProvidesDatabaseTypeIds`, the plugin loads eagerly at app startup and `PluginManager` logs a warning. With it, the app registers the metadata from `Info.plist` and loads the binary on first use.

## Implementing DriverPlugin

Your principal class conforms to `TableProPlugin` and `DriverPlugin`. Only a handful of members have no default:

```swift theme={null}
final class SurrealDBPlugin: NSObject, TableProPlugin, DriverPlugin {
    static let pluginName = "SurrealDB Driver"
    static let pluginVersion = "1.0.0"
    static let pluginDescription = "SurrealDB driver over the HTTP RPC protocol with SurrealQL"
    static let capabilities: [PluginCapability] = [.databaseDriver]

    static let databaseTypeId = "SurrealDB"
    static let databaseDisplayName = "SurrealDB"
    static let iconName = "surrealdb-icon"
    static let defaultPort = 8000

    func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver {
        SurrealDBPluginDriver(config: config)
    }
}
```

`DriverConnectionConfig` carries host, port, username, password, database, SSL configuration, and an `additionalFields` dictionary populated from your `additionalConnectionFields` declarations.

Everything else on `DriverPlugin` is an optional static with a default: connection mode, URL schemes, brand color, editor language, `sqlDialect` (keywords, functions, completions), capability flags (`supportsSSH`, `supportsSchemaEditing`, `supportsTriggers`, and about twenty more), navigation model, and system database names. Override what differs from the defaults; see `Plugins/TableProPluginKit/DriverPlugin.swift` for the full list.

## Implementing PluginDatabaseDriver

The driver protocol has around 80 requirements, but most have default implementations. You must implement:

| Group     | Methods                                                                                                                                                                                                                           |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lifecycle | `connect()`, `disconnect()`                                                                                                                                                                                                       |
| Queries   | `execute(query:)`                                                                                                                                                                                                                 |
| Schema    | `fetchTables(schema:)`, `fetchColumns(table:schema:)`, `fetchIndexes(table:schema:)`, `fetchForeignKeys(table:schema:)`, `fetchTableDDL(table:schema:)`, `fetchViewDefinition(view:schema:)`, `fetchTableMetadata(table:schema:)` |
| Databases | `fetchDatabases()`, `fetchDatabaseMetadata(_:)`                                                                                                                                                                                   |

Notable defaults you may want to override:

* `ping()` runs `SELECT 1`; transactions run `BEGIN` / `COMMIT` / `ROLLBACK` via `execute(query:)`.
* `fetchAllColumns(schema:)` and `fetchAllForeignKeys(schema:)` loop per table (N+1 round-trips). SQL drivers should replace them with one bulk query.
* `quoteIdentifier`, `escapeStringLiteral`, `executeParameterized`, and `streamRows` have generic SQL defaults.
* Non-SQL databases implement the query-building hooks (`buildBrowseQuery`, `buildFilteredQuery`, `generateStatements`) so browsing and editing work without SQL. Implement the `schema:`-aware overloads if your database has schemas; the schema-less defaults drop the schema.

## How loading works

`PluginManager` loads plugins from two places: the app's `PlugIns/` directory (built-in, no signature check) and `~/Library/Application Support/TablePro/Plugins` (user-installed, must be signed by TablePro's signing team).

Before loading any bundle, `validateBundleVersions` checks `TableProPluginKitVersion` against the app's compatible range, `[minimumCompatiblePluginKitVersion, currentPluginKitVersion]` in `PluginManager.swift`. A missing key, a version above the range, or a version below it all reject the plugin with a clear error instead of a crash. `TableProMinAppVersion` is checked next, then the bundle's principal class must conform to `TableProPlugin`.

## ABI compatibility

TableProPluginKit builds with Swift Library Evolution, so its public ABI is resilient: a plugin built against an older PluginKit keeps loading under a newer app, and the runtime fills protocol requirements the plugin does not implement from their defaults.

If you change PluginKit itself, the rules are:

* **Additive, no version bump**: a new protocol requirement with a default implementation, a new field on a non-frozen struct added through a new initializer overload, reordering requirements.
* **Breaking, bump required**: changing an existing requirement's signature, removing a requirement, adding a requirement without a default, adding a case to a `@frozen` enum, changing a `@frozen` type's layout. Bump `currentPluginKitVersion` and every plugin's `TableProPluginKitVersion`, then re-release all registry plugins.
* **Never remove a published requirement, even one that defaulted to `nil`.** Library Evolution fills in requirements added after a plugin was built, but it cannot rescue one removed out from under it. Removing a requirement deletes its method descriptor and its default-implementation symbol, and every shipped plugin hard-references both in its witness table, so it fails to load with "Bundle failed to load executable". That shipped on 0.58 and broke MongoDB, Oracle, Cassandra, and Elasticsearch (#1917). If the app stops using a requirement, leave it in place with its default.
* **Never add a parameter to an existing public initializer or function, even with a default value.** It replaces the mangled symbol and every already-built plugin fails to load. Add a new overload and mark the old one `@_disfavoredOverload`.

Run `scripts/check-pluginkit-abi.sh` before merging any change under `Plugins/TableProPluginKit/`. It builds the public interface at your tree and at the base ref and diffs them. Commit or stash first; the script exits if the working tree is dirty. The base ref is an optional argument and defaults to `origin/main`, so pass the merge base when you review a branch.

## Test locally

The app derives the required signing team from its own signature, so a plugin built by the same Xcode as your debug app passes verification. Build the plugin target in Xcode, then install it into the user plugins directory and relaunch TablePro:

```bash theme={null}
scripts/install-plugin-dev.sh MyDriverPlugin
```

The script copies the bundle from DerivedData, so build from Xcode, not from a CLI `xcodebuild` (that writes to `build/Debug` instead). Do not patch the copied `Info.plist`: any edit invalidates the signature and dyld refuses to load the executable. See [Testing a Custom Plugin](/development/testing-plugins) for the **Copy Plug-Ins** build phase and the unsigned-plugin escape hatch.

## Building outside this repository

A plugin has to link TableProPluginKit. Until recently the only way to get it was to check out this repository and build the whole app project, which is why every driver lives under `Plugins/` here. That was a packaging limit, not a rule of the plugin system.

`scripts/build-pluginkit-xcframework.sh` produces a distributable `TableProPluginKit.xcframework` covering arm64 and x86\_64. Run it here, or download a published one from the `pluginkit-v<version>` release, and link it from your own Xcode project. Your plugin target then needs nothing from this repository.

```bash theme={null}
scripts/build-pluginkit-xcframework.sh
# build/pluginkit/TableProPluginKit.xcframework
```

Two things the script enforces, because getting either wrong produces a framework that links today and breaks every consumer later:

* It builds with `BUILD_LIBRARY_FOR_DISTRIBUTION=YES`, so the framework is ABI-resilient and a plugin built against an older PluginKit keeps loading under a newer app.
* It fails if the result carries no `.swiftinterface`, which is what a build without Library Evolution looks like.

Link the XCFramework as **Do Not Embed**. The app supplies TableProPluginKit at runtime; a copy inside your bundle would be a second, conflicting one.

## Distribute

Publishing to TablePro's registry takes two steps in this repo:

1. Add a case for your plugin to the `case "$PLUGIN_NAME"` block in the `Resolve plugin info` step of `.github/workflows/build-plugin.yml`: target name, bundle ID, display name, summary, database type IDs, icon, category, homepage. Without it the workflow exits with `Unknown plugin name`.
2. Push the tag `plugin-<name>-v<version>`. CI builds both architectures, signs, notarizes, and updates `plugins.json`.

CI signs with TablePro's own certificate, so a third-party plugin ships through a pull request, not a tag of your own. See [Plugin Registry](/development/plugin-registry) for the manifest format, the self-describing `metadata` block, and the publishing flow.
