Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
8e97f1b
feat: add TableProCore cross-platform Swift Package
datlechin Mar 31, 2026
827295c
feat: add TableProMobile iOS app skeleton with core views
datlechin Mar 31, 2026
f52d62a
refactor: remove plugin system from TableProDatabase, add DriverFacto…
datlechin Mar 31, 2026
b420a1b
feat: add SQLiteDriver and IOSDriverFactory, remove plugin system fro…
datlechin Mar 31, 2026
7f13dac
feat: add SQLite file picker, document picker, and password storage f…
datlechin Mar 31, 2026
f8215ed
fix: resolve Swift 6 concurrency errors in SQLiteDriver, add missing …
datlechin Mar 31, 2026
5439580
feat: add SQL query editor for iOS app
datlechin Mar 31, 2026
dd7f9b4
fix: move navigationDestination to NavigationStack level for connecti…
datlechin Mar 31, 2026
865e30a
fix: add Hashable to DatabaseConnection and SSH/SSL types for Navigat…
datlechin Mar 31, 2026
b63762d
fix: replace nested NavigationStack/TabView with segmented Picker in …
datlechin Mar 31, 2026
ddbed80
feat: persist connections to UserDefaults across app restarts
datlechin Mar 31, 2026
bb2dd20
fix: use ASCII keyboard for query editor to prevent smart quote subst…
datlechin Mar 31, 2026
9e14e50
refactor: polish iOS views — compact keyword bar, dynamic column widt…
datlechin Mar 31, 2026
da1f799
fix: disconnect only when leaving connection, not on push navigation
datlechin Mar 31, 2026
86a646e
fix: use Double.greatestFiniteMagnitude for task sleep
datlechin Mar 31, 2026
1b8acab
fix: use UInt64.max nanoseconds to avoid Duration overflow crash
datlechin Mar 31, 2026
9c421ff
fix: remove auto-disconnect from ConnectedView to prevent connection …
datlechin Mar 31, 2026
df269ca
fix: remove DragGesture from RowDetailView to avoid conflict with bac…
datlechin Mar 31, 2026
dbaeea3
feat: disconnect all database sessions when app enters background
datlechin Mar 31, 2026
eb76fd0
feat: add row editing — swipe-to-delete, edit mode, insert row for iO…
datlechin Mar 31, 2026
c8df3a0
fix: update local row data after successful edit in RowDetailView
datlechin Mar 31, 2026
32a9320
fix: only default PK columns to NULL in InsertRowView, not all nullab…
datlechin Mar 31, 2026
3b9e504
fix: reconnect on app active, fix 13 UI/UX issues across all iOS views
datlechin Mar 31, 2026
3e96c73
Merge branch 'main' into feat/tablepro-core-package
datlechin Apr 1, 2026
03ec9ca
Merge branch 'main' into feat/tablepro-core-package
datlechin Apr 1, 2026
722c28f
feat: add iOS cross-compile scripts for OpenSSL, hiredis, libpq, MariaDB
datlechin Apr 1, 2026
3935d3f
feat: iOS cross-compile scripts — OpenSSL, hiredis, libpq, MariaDB xc…
datlechin Apr 2, 2026
ca10b83
refactor: fix 13 code quality issues — proper SQL quoting, secure sto…
datlechin Apr 2, 2026
14a6650
fix: libpq iOS build — fix shell quoting, add missing pg_config defin…
datlechin Apr 2, 2026
0f19780
feat: add MySQL, PostgreSQL, Redis iOS drivers with C bridge modules
datlechin Apr 2, 2026
18981d9
fix: pass mutable pointers to redisCommandArgv in RedisDriver
datlechin Apr 2, 2026
4ba7b66
fix: add missing libpq symbols, fix Swift actor isolation and depreca…
datlechin Apr 2, 2026
43c5e4d
fix: add md5, pg_bitutils to libpq iOS build for remaining undefined …
datlechin Apr 2, 2026
de26009
fix: move pg_bitutils.c to correct src/port path in libpq iOS build
datlechin Apr 2, 2026
a99108e
feat: iPad split view — NavigationSplitView with sidebar + detail
datlechin Apr 2, 2026
549b1f1
feat: add SSH tunneling for iOS — libssh2 xcframework, actor-based tu…
datlechin Apr 2, 2026
fb73c3a
feat: add TableProSync module — CloudKit sync engine, record mapper, …
datlechin Apr 2, 2026
950b880
feat: integrate iCloud sync — IOSSyncCoordinator, AppState sync on ch…
datlechin Apr 3, 2026
31ffe51
fix: add Equatable to SyncStatus for comparison
datlechin Apr 3, 2026
3215a85
fix: lazy init CloudKitSyncEngine to prevent CKContainer crash on launch
datlechin Apr 3, 2026
cfb83df
fix: case-insensitive database type matching in IOSDriverFactory for …
datlechin Apr 3, 2026
a08a0f2
fix: align DatabaseType raw values with macOS for CloudKit sync compa…
datlechin Apr 3, 2026
77a0dfd
fix: align SSHConfiguration with macOS Codable format — tolerant deco…
datlechin Apr 3, 2026
a3406af
refactor: clean SSHAuthMethod — remove duplicate cases, move type mig…
datlechin Apr 3, 2026
da1464a
fix: derive sshEnabled from sshConfigJson for macOS CloudKit compatib…
datlechin Apr 3, 2026
1b0bec3
fix: add MariaDB to ConnectionFormView database type picker
datlechin Apr 3, 2026
9f3dbe2
fix: handle macOS capitalized SSH auth method raw values in decoder
datlechin Apr 3, 2026
5b21f19
fix: sync compatibility — SSL decoder, color field name, CKRecord pas…
datlechin Apr 3, 2026
1376513
feat: SSH private key — support both file import and paste key conten…
datlechin Apr 3, 2026
e3e4481
fix: final audit — 15 fixes including security (private key sync, SQL…
datlechin Apr 3, 2026
034d4c8
fix: replace NSLock with actor in IOSSSHProvider, move SyncError Equa…
datlechin Apr 3, 2026
9098e26
feat: shared iCloud Keychain — access group + sync for cross-device p…
datlechin Apr 3, 2026
3e55ad6
fix: align Keychain key patterns with macOS — com.TablePro.password/s…
datlechin Apr 3, 2026
d353ddf
fix: use tempId for SSH Keychain keys in testConnection, set pendingC…
datlechin Apr 3, 2026
778369f
fix: handle CloudKit deletions in sync — remove connections deleted o…
datlechin Apr 3, 2026
311cdd0
fix: 10 sync fixes — token expiry detection, pull-before-push, atomic…
datlechin Apr 3, 2026
c1405fb
fix: wrap NavigationSplitView detail in NavigationStack for push navi…
datlechin Apr 3, 2026
bebb7c3
fix: guard SSH channel close with isAlive check to prevent use-after-…
datlechin Apr 3, 2026
fc1104e
feat: add pull-to-refresh on connection list to trigger iCloud sync
datlechin Apr 3, 2026
1c217b2
fix: add Sync from iCloud button on empty connection list
datlechin Apr 3, 2026
2032bcf
refactor: native iOS sync UX — toolbar cloud button, loading state, p…
datlechin Apr 3, 2026
1093a74
fix: add missing TableProSync import in ConnectionListView
datlechin Apr 3, 2026
ecf5de5
fix: deduplicate CloudKit deletion IDs to prevent duplicate tombstone…
datlechin Apr 3, 2026
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
60 changes: 60 additions & 0 deletions Packages/TableProCore/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
name: "TableProCore",
platforms: [
.macOS(.v14),
.iOS(.v17)
],
products: [
.library(name: "TableProPluginKit", targets: ["TableProPluginKit"]),
.library(name: "TableProModels", targets: ["TableProModels"]),
.library(name: "TableProDatabase", targets: ["TableProDatabase"]),
.library(name: "TableProQuery", targets: ["TableProQuery"]),
.library(name: "TableProSync", targets: ["TableProSync"])
],
targets: [
.target(
name: "TableProPluginKit",
dependencies: [],
path: "Sources/TableProPluginKit"
),
.target(
name: "TableProModels",
dependencies: ["TableProPluginKit"],
path: "Sources/TableProModels"
),
.target(
name: "TableProDatabase",
dependencies: ["TableProModels"],
path: "Sources/TableProDatabase"
),
.target(
name: "TableProQuery",
dependencies: ["TableProModels", "TableProPluginKit"],
path: "Sources/TableProQuery"
),
.target(
name: "TableProSync",
dependencies: ["TableProModels"],
path: "Sources/TableProSync"
),
.testTarget(
name: "TableProModelsTests",
dependencies: ["TableProModels", "TableProPluginKit"],
path: "Tests/TableProModelsTests"
),
.testTarget(
name: "TableProDatabaseTests",
dependencies: ["TableProDatabase", "TableProModels"],
path: "Tests/TableProDatabaseTests"
),
.testTarget(
name: "TableProQueryTests",
dependencies: ["TableProQuery", "TableProModels", "TableProPluginKit"],
path: "Tests/TableProQueryTests"
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

public enum ConnectionError: Error, LocalizedError {
case driverNotFound(String)
case notConnected
case sshNotSupported

public var errorDescription: String? {
switch self {
case .driverNotFound(let type):
return "No driver available for database type: \(type)"
case .notConnected:
return "Not connected to database"
case .sshNotSupported:
return "SSH tunneling is not available on this platform"
}
}
}
132 changes: 132 additions & 0 deletions Packages/TableProCore/Sources/TableProDatabase/ConnectionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Foundation
import TableProModels

public final class ConnectionManager: @unchecked Sendable {
private let driverFactory: DriverFactory
private let secureStore: SecureStore
private let sshProvider: SSHProvider?

private let lock = NSLock()
private var sessions: [UUID: ConnectionSession] = [:]

public init(
driverFactory: DriverFactory,
secureStore: SecureStore,
sshProvider: SSHProvider? = nil
) {
self.driverFactory = driverFactory
self.secureStore = secureStore
self.sshProvider = sshProvider
}

public func connect(_ connection: DatabaseConnection) async throws -> ConnectionSession {
let password = try secureStore.retrieve(forKey: Self.passwordKey(for: connection.id))

var effectiveHost = connection.host
var effectivePort = connection.port
if connection.sshEnabled, let ssh = connection.sshConfiguration {
guard let provider = sshProvider else {
throw ConnectionError.sshNotSupported
}
let tunnel = try await provider.createTunnel(
config: ssh,
remoteHost: connection.host,
remotePort: connection.port
)
effectiveHost = tunnel.localHost
effectivePort = tunnel.localPort
}

do {
var effectiveConnection = connection
effectiveConnection.host = effectiveHost
effectiveConnection.port = effectivePort

let driver = try driverFactory.createDriver(for: effectiveConnection, password: password)
try await driver.connect()

let session = ConnectionSession(
connectionId: connection.id,
driver: driver,
activeDatabase: connection.database,
status: .connected
)
storeSession(session, for: connection.id)
return session
} catch {
if connection.sshEnabled, let provider = sshProvider {
try? await provider.closeTunnel(for: connection.id)
}
throw error
}
}

public func storePassword(_ password: String, for connectionId: UUID) throws {
try secureStore.store(password, forKey: Self.passwordKey(for: connectionId))
}

public func deletePassword(for connectionId: UUID) throws {
try secureStore.delete(forKey: Self.passwordKey(for: connectionId))
}

private static func passwordKey(for connectionId: UUID) -> String {
"com.TablePro.password.\(connectionId.uuidString)"
}

public func disconnect(_ connectionId: UUID) async {
let session = removeSession(for: connectionId)
guard let session else { return }
try? await session.driver.disconnect()
if let sshProvider {
try? await sshProvider.closeTunnel(for: connectionId)
}
}

public func disconnectAll() async {
let ids = allSessionIds()
for id in ids {
await disconnect(id)
}
}

private func allSessionIds() -> [UUID] {
lock.lock()
defer { lock.unlock() }
return Array(sessions.keys)
}

public func updateSession(_ connectionId: UUID, _ mutation: (inout ConnectionSession) -> Void) {
lock.lock()
defer { lock.unlock() }
guard var session = sessions[connectionId] else { return }
mutation(&session)
sessions[connectionId] = session
}

public func switchDatabase(_ connectionId: UUID, to database: String) async throws {
guard let session = session(for: connectionId) else {
throw ConnectionError.notConnected
}
try await session.driver.switchDatabase(to: database)
updateSession(connectionId) { $0.activeDatabase = database }
}

public func session(for connectionId: UUID) -> ConnectionSession? {
lock.lock()
defer { lock.unlock() }
return sessions[connectionId]
}

private func storeSession(_ session: ConnectionSession, for id: UUID) {
lock.lock()
sessions[id] = session
lock.unlock()
}

private func removeSession(for id: UUID) -> ConnectionSession? {
lock.lock()
let session = sessions.removeValue(forKey: id)
lock.unlock()
return session
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation
import TableProModels

/// Note: Views hold a snapshot of this struct. Mutable fields (activeDatabase, status)
/// are only updated through ConnectionManager.updateSession and should be re-fetched
/// from the manager when needed rather than read from a held copy.
public struct ConnectionSession: Sendable {
public let connectionId: UUID
public let driver: any DatabaseDriver
public internal(set) var activeDatabase: String
public internal(set) var currentSchema: String?
public internal(set) var status: ConnectionStatus
public internal(set) var tables: [TableInfo]

public init(
connectionId: UUID,
driver: any DatabaseDriver,
activeDatabase: String,
currentSchema: String? = nil,
status: ConnectionStatus = .connected,
tables: [TableInfo] = []
) {
self.connectionId = connectionId
self.driver = driver
self.activeDatabase = activeDatabase
self.currentSchema = currentSchema
self.status = status
self.tables = tables
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation
import TableProModels

public protocol DatabaseDriver: AnyObject, Sendable {
func connect() async throws
func disconnect() async throws
func ping() async throws -> Bool

func execute(query: String) async throws -> QueryResult
func cancelCurrentQuery() async throws

func fetchTables(schema: String?) async throws -> [TableInfo]
func fetchColumns(table: String, schema: String?) async throws -> [ColumnInfo]
func fetchIndexes(table: String, schema: String?) async throws -> [IndexInfo]
func fetchForeignKeys(table: String, schema: String?) async throws -> [ForeignKeyInfo]
func fetchDatabases() async throws -> [String]

func switchDatabase(to name: String) async throws
var supportsSchemas: Bool { get }
func switchSchema(to name: String) async throws
var currentSchema: String? { get }

var supportsTransactions: Bool { get }
func beginTransaction() async throws
func commitTransaction() async throws
func rollbackTransaction() async throws

var serverVersion: String? { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation
import TableProModels

/// Creates database drivers for a given connection.
/// macOS: plugin-based implementation. iOS: direct driver creation.
public protocol DriverFactory: Sendable {
func createDriver(for connection: DatabaseConnection, password: String?) throws -> any DatabaseDriver
func supportedTypes() -> [DatabaseType]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import TableProModels

public protocol SSHProvider: Sendable {
func createTunnel(
config: SSHConfiguration,
remoteHost: String,
remotePort: Int
) async throws -> SSHTunnel

func closeTunnel(for connectionId: UUID) async throws
}

public struct SSHTunnel: Sendable {
public let localHost: String
public let localPort: Int

public init(localHost: String, localPort: Int) {
self.localHost = localHost
self.localPort = localPort
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

public protocol SecureStore: Sendable {
func store(_ value: String, forKey key: String) throws
func retrieve(forKey key: String) throws -> String?
func delete(forKey key: String) throws
}
17 changes: 17 additions & 0 deletions Packages/TableProCore/Sources/TableProModels/ConnectionGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public struct ConnectionGroup: Identifiable, Codable, Sendable {
public var id: UUID
public var name: String
public var sortOrder: Int

public init(
id: UUID = UUID(),
name: String = "",
sortOrder: Int = 0
) {
self.id = id
self.name = name
self.sortOrder = sortOrder
}
}
17 changes: 17 additions & 0 deletions Packages/TableProCore/Sources/TableProModels/ConnectionTag.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public struct ConnectionTag: Identifiable, Codable, Sendable {
public var id: UUID
public var name: String
public var colorHex: String

public init(
id: UUID = UUID(),
name: String = "",
colorHex: String = "#808080"
) {
self.id = id
self.name = name
self.colorHex = colorHex
}
}
Loading
Loading