Skip to main content

Architecture

TablePro is built with:
  • SwiftUI for the UI
  • AppKit for low-level macOS integration
  • Swift Concurrency (async/await, actors) for concurrent operations
  • Native C libraries for database connectivity

Dependencies

SPM dependencies:
PackageVersionPurpose
CodeEditSourceEditor0.15.2+Tree-sitter-powered code editor for the SQL editor
Sparkle2.xAuto-update framework with EdDSA signing
CodeEditSourceEditor bundles a SwiftLint plugin that requires -skipPackagePluginValidation for CLI builds. See Building.

Directory Structure

TablePro
Views
Models
ViewModels
Extensions
Theme
Resources
Plugins
TableProPluginKit
MySQLDriverPlugin
PostgreSQLDriverPlugin
...
Libs
TableProTests
scripts

Design Patterns

MVVM Architecture

Models: Pure data structures (structs, enums)
struct DatabaseConnection: Codable, Identifiable {
    let id: UUID
    var name: String
    var host: String
    var port: Int
    var type: DatabaseType
}
ViewModels: Observable state containers
@MainActor
class DatabaseManager: ObservableObject {
    @Published var sessions: [DatabaseSession] = []
    @Published var activeSessionId: UUID?

    func connect(to connection: DatabaseConnection) async throws {
        // Business logic
    }
}
Views: Declarative SwiftUI
struct ConnectionFormView: View {
    @StateObject private var dbManager = DatabaseManager.shared

    var body: some View {
        Form {
            // UI elements
        }
    }
}

Protocol-Oriented Design

All database drivers conform to a single protocol:
protocol DatabaseDriver: AnyObject {
    var connection: DatabaseConnection { get }
    var status: ConnectionStatus { get }

    func connect() async throws
    func disconnect()
    func execute(query: String) async throws -> QueryResult
    func fetchTables() async throws -> [TableInfo]
    // ...
}
Database drivers are implemented as .tableplugin bundles loaded at runtime. Each plugin implements DriverPlugin and PluginDatabaseDriver from the shared TableProPluginKit framework. PluginDriverAdapter bridges PluginDatabaseDriver to the core DatabaseDriver protocol.

Actor Isolation

Concurrent operations use Swift actors:
actor SSHTunnelManager {
    static let shared = SSHTunnelManager()

    private var tunnels: [UUID: SSHTunnel] = [:]

    func createTunnel(
        connectionId: UUID,
        sshHost: String,
        // ...
    ) async throws -> Int {
        // Thread-safe tunnel management
    }
}

Plugin System

Driver creation uses a plugin-based factory. PluginManager discovers and loads .tableplugin bundles at runtime. DatabaseDriverFactory looks up plugins via DatabaseType.pluginTypeId and wraps them with PluginDriverAdapter to conform to the core DatabaseDriver protocol. No switch statement or hardcoded driver list is needed.

Key Components

DatabaseManager

Central manager for all database operations:
  • Manages active sessions
  • Coordinates connections/disconnections
  • Handles SSH tunnel lifecycle
  • Publishes state changes to UI
@MainActor
class DatabaseManager: ObservableObject {
    static let shared = DatabaseManager()

    @Published var sessions: [DatabaseSession] = []
    @Published var activeSessionId: UUID?

    func connectToSession(_ connection: DatabaseConnection) async throws
    func disconnectSession(_ id: UUID) async
    func executeQuery(_ query: String) async throws -> QueryResult
}

Database Driver Plugins

Each driver is a .tableplugin bundle under Plugins/. MySQL, PostgreSQL, and SQLite are built into the app bundle; the rest are distributed via the plugin registry and downloaded on demand.
PluginDatabase TypesC BridgeDistribution
MySQLDriverPluginMySQL, MariaDBCMariaDB (libmariadb)Built-in
PostgreSQLDriverPluginPostgreSQL, RedshiftCLibPQ (libpq)Built-in
SQLiteDriverPluginSQLiteFoundation sqlite3Built-in
ClickHouseDriverPluginClickHouseURLSession HTTPRegistry
MSSQLDriverPluginSQL ServerCFreeTDSRegistry
MongoDBDriverPluginMongoDBCLibMongocRegistry
RedisDriverPluginRedisCRedisRegistry
DuckDBDriverPluginDuckDBCDuckDBRegistry
OracleDriverPluginOracleOracleNIO (SPM)Registry

Autocomplete Engine

  • CompletionEngine: Main entry point
  • SQLContextAnalyzer: Parses query context
  • SQLSchemaProvider: Provides schema information
  • SQLKeywords: SQL keyword definitions

SSH Tunnel Manager

Actor-based SSH tunnel management:
actor SSHTunnelManager {
    private var tunnels: [UUID: SSHTunnel] = [:]

    func createTunnel(...) async throws -> Int
    func closeTunnel(connectionId: UUID) async throws
    func hasTunnel(connectionId: UUID) -> Bool
}
Features:
  • Port forwarding via system ssh
  • Password and key authentication
  • Health monitoring
  • Automatic cleanup

Data Flow

Connection Flow

Query Execution Flow

State Management

Published Properties

UI state uses @Published:
@MainActor
class DatabaseManager: ObservableObject {
    @Published var sessions: [DatabaseSession] = []
    @Published var activeSessionId: UUID?
    @Published var isConnecting = false
}

App Storage

Settings persist via @AppStorage:
@AppStorage("appearance.theme") var theme: AppTheme = .system
@AppStorage("editor.fontSize") var fontSize: Int = 13

Environment

Shared state via SwiftUI environment:
@main
struct TableProApp: App {
    @StateObject private var dbManager = DatabaseManager.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(dbManager)
        }
    }
}

Error Handling

Driver Errors

Each driver defines specific errors:
enum MySQLError: Error, LocalizedError {
    case connectionFailed(String)
    case queryFailed(String)
    case authenticationFailed

    var errorDescription: String? {
        switch self {
        case .connectionFailed(let msg): return "Connection failed: \(msg)"
        case .queryFailed(let msg): return "Query failed: \(msg)"
        case .authenticationFailed: return "Authentication failed"
        }
    }
}

Error Propagation

Errors propagate through async/await:
func executeQuery(_ query: String) async throws -> QueryResult {
    guard let driver = activeDriver else {
        throw DatabaseError.notConnected
    }
    return try await driver.execute(query: query)
}

Testing

Unit Tests

Tests live in TableProTests/:
final class MySQLDriverTests: XCTestCase {
    func testConnectionString() throws {
        let connection = DatabaseConnection(...)
        let driver = MySQLDriver(connection: connection)
        XCTAssertEqual(driver.connectionString, "expected")
    }
}

Integration Tests

func testExecuteQuery() async throws {
    let driver = MySQLDriver(connection: testConnection)
    try await driver.connect()
    defer { driver.disconnect() }

    let result = try await driver.execute(query: "SELECT 1")
    XCTAssertEqual(result.rowCount, 1)
}