> ## Documentation Index
> Fetch the complete documentation index at: https://ngquct-docs-fix-500-query-results.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

Start from a plugin that already works. `Plugins/SurrealDBDriverPlugin/` is the compact reference: no C bridge, an HTTP transport, the schema-aware query hooks filled in, and an editor language of its own.

Two protocols do the work. `DriverPlugin` describes the database (name, port, capabilities, SQL dialect) and creates driver instances; `PluginDatabaseDriver` is the connection itself. Implement both in a `.tableplugin` bundle linked against **TableProPluginKit**, and `PluginDriverAdapter` bridges your driver to the app's internal `DatabaseDriver`. A driver that connects and lists tables gets the whole UI without asking: connection form, sidebar, data grid, editor, import, export.

## Bundle layout

A plugin is a macOS loadable bundle target with `WRAPPER_EXTENSION = tableplugin` that links TableProPluginKit. Set `INFOPLIST_KEY_NSPrincipalClass` to your `DriverPlugin` class.

| Key                               | Type             | Required    | Purpose                                                                        |
| --------------------------------- | ---------------- | ----------- | ------------------------------------------------------------------------------ |
| `TableProPluginKitVersion`        | integer          | Yes         | The PluginKit ABI the plugin was built against. Current value: 19              |
| `TableProProvidesDatabaseTypeIds` | array of strings | Recommended | Database type IDs the plugin serves, which is what makes lazy loading possible |
| `CFBundleShortVersionString`      | string           | Yes         | Plugin version, read by registry update checks                                 |
| `TableProMinAppVersion`           | string           | No          | The loader rejects the plugin on an older app                                  |

Leave `TableProProvidesDatabaseTypeIds` out and the plugin loads eagerly at startup, blocking launch, and `PluginManager` logs a warning naming the key. With it, the app registers your metadata from `Info.plist` and loads the binary on first use.

## Implementing DriverPlugin

Your principal class conforms to `TableProPlugin` and `DriverPlugin`. Nine members have no default, and this is all of them:

```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 settings, and an `additionalFields` dictionary filled from whatever `additionalConnectionFields` you declared.

The remaining fifty-odd statics all have defaults: connection mode, URL schemes, brand color, editor language, `sqlDialect` (keywords, functions, completions), navigation model, system database names, and two dozen `supports*` capability flags. Override the ones that differ. `Plugins/TableProPluginKit/DriverPlugin.swift` is the full list.

## Implementing PluginDatabaseDriver

Twelve requirements have no default. Everything else on the protocol does.

| 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(_:)`                                                                                                                                                                                   |

Four defaults are worth a second look before you accept them:

* `ping()` runs `SELECT 1`, and the transaction methods run `BEGIN` / `COMMIT` / `ROLLBACK` through `execute(query:)`. An engine without those keywords overrides all four.
* `fetchAllColumns(schema:)` and `fetchAllForeignKeys(schema:)` loop one round-trip per table. Any SQL driver should replace them with a single catalog query.
* `quoteIdentifier`, `escapeStringLiteral`, `executeParameterized`, and `streamRows` assume generic SQL.
* A non-SQL database implements `buildBrowseQuery`, `buildFilteredQuery`, and `generateStatements` instead, which is what makes browsing and editing work without SQL. Implement the `schema:`-aware overloads if your database has schemas; the schema-less defaults throw the schema away.

[Testing a Custom Plugin](/development/testing-plugins) covers getting the built bundle into a running app and reading the failure if it does not load.

## ABI compatibility

TableProPluginKit builds with Swift Library Evolution, so a plugin built against an older PluginKit keeps loading under a newer app: the runtime fills requirements it never implemented from their defaults. Adding a requirement that has a default costs nothing.

Two changes look additive and are not. Both have shipped, and both surface the same way: every registry plugin fails to load with "Bundle failed to load executable".

* **Removing a published requirement, even one that defaulted to `nil`.** Library Evolution rescues a requirement added after a plugin was built, never one removed out from under it. Removing one deletes its method descriptor and its default-implementation symbol, and every shipped plugin hard-references both in its witness table. If the app stops using a requirement, leave it in place with its default. (0.58, #1917: MongoDB, Oracle, Cassandra, Elasticsearch.)
* **Adding a parameter to an existing public initializer or function, even with a default value.** It replaces the mangled symbol. Add a new overload for the new field and mark the old one `@_disfavoredOverload`, so new code gets the full initializer and old binaries keep theirs.

`CLAUDE.md` carries the full additive-versus-breaking list and the checklist a breaking bump obliges you to run, including re-releasing every registry plugin before the app ships.

```bash theme={null}
scripts/check-pluginkit-abi.sh [base-ref]
```

Run it before merging any change under `Plugins/TableProPluginKit/`. It builds the framework at your tree and at the base ref with one toolchain and diffs the two public interfaces, so a Swift version difference between machines can never fake a diff. The base ref defaults to `origin/main`; pass the merge base when reviewing a branch. Commit or stash first, since it refuses a dirty working tree.

## Building outside this repository

A plugin needs TableProPluginKit, not this repository. Every driver happens to live under `Plugins/` here. A new one does not have to.

```bash theme={null}
scripts/generate-project.sh
scripts/build-pluginkit-xcframework.sh
# build/pluginkit/TableProPluginKit.xcframework, plus a zip and its SHA-256
```

Run that, or download a published XCFramework from the `pluginkit-v<version>` release, and link it from your own Xcode project. Your plugin target then needs nothing else from this repository.

Link it as **Do Not Embed**. The app supplies TableProPluginKit at runtime, so a copy inside your bundle is a second, conflicting one. The script refuses to emit a framework with no `.swiftinterface`, which is what a build without Library Evolution looks like: it links today and breaks every consumer on the next release.

## Publishing

Two steps put a plugin in TablePro's own registry, and both happen in this repository:

<Steps>
  <Step title="Add a manifest entry">
    Add your plugin to `.github/plugin-registry.json`, keyed by the slug its tag will use. Without an entry the release workflow stops with `Unknown plugin`. `icon` and `databaseTypeIds` there have to match what your `DriverPlugin` class declares, and `scripts/ci/check-plugin-manifest.py` fails PR CI and the release workflow when they drift.
  </Step>

  <Step title="Push one tag">
    `plugin-<slug>-v<version>`. CI builds both architectures, signs, notarizes, and updates `plugins.json`.
  </Step>
</Steps>

CI signs with TablePro's certificate, so a plugin published there arrives through a pull request, not a tag of your own. The other route stays outside this repository: sign the bundle with your own Developer ID and serve it from a registry URL you host, and the app installs it once the user agrees to trust your team by name. [Plugin Registry](/development/plugin-registry) has the manifest format, the `metadata` block that renders your connection form before the download finishes, and how to point the app at a private registry.
