Skip to main content
Every database driver is a bundle the app loads at runtime. That decision shapes the rest: nothing switches on database type, no driver list is compiled in, and DatabaseType is a string-based struct rather than an enum, so a type from a plugin the app has never heard of still round-trips through Codable. The UI is SwiftUI with AppKit underneath it for windows, menus, and the data grid. Async work is Swift concurrency throughout. Database connectivity is native C libraries, linked as static .a files out of Libs/. CLAUDE.md in the repository root is the authority on the rules a change has to satisfy: the invariants that have caused real bugs, the ABI policy, and what has to happen before a commit lands. This page is the map of where things live, not the rulebook.

Plugin system

PluginManager (Core/Plugins/) discovers bundles, version-checks them, and loads them. Four pieces carry the whole system: Packages/TableProCore/Sources/TableProPluginKit is a symlink to the same files, so edit only the copy under Plugins/TableProPluginKit/. Five drivers ship inside the app and cover nine databases. Another 17 come from the registry and install on the first connection that needs them. See Plugins & Themes. A driver is one of four roles a bundle can take. ExportFormatPlugin, ImportFormatPlugin, and DocumentInspectorPlugin are the others, which is how the CSV, JSON, SQL, XLSX, and MQL formats and the CSV inspector ship.

Opt-in protocols

A driver adopts these on top of PluginDatabaseDriver to reach extra surfaces. The app finds them with a runtime cast, so skipping one costs nothing and no ABI bump is involved.

PluginKit ABI

TableProPluginKit builds with BUILD_LIBRARY_FOR_DISTRIBUTION = YES, so its public ABI is resilient: the Swift runtime fills any requirement an already-built plugin never implemented from the protocol’s default, and that plugin keeps loading under a newer app. Adding a requirement with a default is free. Removing one, or adding a parameter to an existing public initializer, breaks every shipped binary. Run scripts/check-pluginkit-abi.sh before merging anything under Plugins/TableProPluginKit/. Plugin Development has the additive-versus-breaking rules and what a breaking change obliges you to do.

Key services

DatabaseManager (Core/Database/DatabaseManager.swift) is the connection pool and the interface every view and coordinator goes through for connect, disconnect, reconnect, and session tracking. ConnectionHealthMonitor pings each live connection every 30 seconds and reconnects with exponential backoff. MainContentCoordinator is the central coordinator, one per connection session rather than per window, built by SessionStateFactory and split across extension files in Views/Main/Extensions/ (+Alerts, +Filtering, +Pagination, and the rest). New coordinator behavior goes in a new extension file rather than the main one. Autocomplete is three pieces. CompletionEngine ranks the suggestions and knows nothing about the editor framework; QueryCompletionAdapter bridges it to CodeEdit. SQLContextAnalyzer reads what the cursor sits in: a table reference, a column reference, a keyword. SQLSchemaProvider is an actor that caches schema and hands concurrent callers the same in-flight Task rather than firing a second fetchTables().

Change tracking

1

Edit

DataChangeManager records the cell edit as a pending change. Nothing has reached the server.
2

Save

SQLStatementGenerator turns the pending set into INSERT, UPDATE, and DELETE statements.
3

Undo

Grid edits register on the window’s UndoManager, which TabWindowController.windowWillReturnUndoManager resolves to the selected ConnectionWorkspace.undoManager. One window hosts several connections, so the undo stack is per connection, not per window. Structure edits use a private UndoManager inside StructureChangeManager.
AnyChangeManager wraps whichever concrete manager is active behind the ChangeManaging protocol, so callers do not care which one they have.

MCP server

Core/MCP/ holds a Model Context Protocol server bound to loopback, speaking plain HTTP through NWListener. Nothing in it is reachable off the machine: no TLS, no remote mode. MCPProtocolDispatcher is an actor that resolves the protocol era from the request, checks scopes, runs each handler in its own task under a 330-second deadline, and normalizes the result envelope. MCPServerManager owns the lifecycle on the main actor: port binding, the bridge credential, the handshake file. The wire contract, the tool catalog, and the version policy are documented for consumers: MCP Protocol, MCP Tools, Versioning.

Connecting

A cancelled connect is the hard case here, because Task.cancel() cannot interrupt a driver blocked in a C call. CLAUDE.md sets out what a driver owes the app on that path, written down after the same bug shipped four times.

State and storage

CLAUDE.md carries the full storage table, including which stores sync through iCloud and which stay device-local.

Dependencies

Plugins/DamengDriverPlugin links a Rust wire-protocol bridge built from Native/DamengBridge, not an SPM package. For the repository layout, see Project Structure.