Skip to content

Commit 8e97f1b

Browse files
committed
feat: add TableProCore cross-platform Swift Package
1 parent 260ebdb commit 8e97f1b

File tree

64 files changed

+5050
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+5050
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "TableProCore",
7+
platforms: [
8+
.macOS(.v14),
9+
.iOS(.v17)
10+
],
11+
products: [
12+
.library(name: "TableProPluginKit", targets: ["TableProPluginKit"]),
13+
.library(name: "TableProModels", targets: ["TableProModels"]),
14+
.library(name: "TableProDatabase", targets: ["TableProDatabase"]),
15+
.library(name: "TableProQuery", targets: ["TableProQuery"])
16+
],
17+
targets: [
18+
.target(
19+
name: "TableProPluginKit",
20+
dependencies: [],
21+
path: "Sources/TableProPluginKit"
22+
),
23+
.target(
24+
name: "TableProModels",
25+
dependencies: ["TableProPluginKit"],
26+
path: "Sources/TableProModels"
27+
),
28+
.target(
29+
name: "TableProDatabase",
30+
dependencies: ["TableProModels", "TableProPluginKit"],
31+
path: "Sources/TableProDatabase"
32+
),
33+
.target(
34+
name: "TableProQuery",
35+
dependencies: ["TableProModels", "TableProPluginKit"],
36+
path: "Sources/TableProQuery"
37+
),
38+
.testTarget(
39+
name: "TableProModelsTests",
40+
dependencies: ["TableProModels", "TableProPluginKit"],
41+
path: "Tests/TableProModelsTests"
42+
),
43+
.testTarget(
44+
name: "TableProDatabaseTests",
45+
dependencies: ["TableProDatabase", "TableProModels", "TableProPluginKit"],
46+
path: "Tests/TableProDatabaseTests"
47+
),
48+
.testTarget(
49+
name: "TableProQueryTests",
50+
dependencies: ["TableProQuery", "TableProModels", "TableProPluginKit"],
51+
path: "Tests/TableProQueryTests"
52+
)
53+
]
54+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Foundation
2+
3+
public enum ConnectionError: Error, LocalizedError {
4+
case pluginNotFound(String)
5+
case notConnected
6+
case sshNotSupported
7+
8+
public var errorDescription: String? {
9+
switch self {
10+
case .pluginNotFound(let type):
11+
return "No driver plugin for database type: \(type)"
12+
case .notConnected:
13+
return "Not connected to database"
14+
case .sshNotSupported:
15+
return "SSH tunneling is not available on this platform"
16+
}
17+
}
18+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import Foundation
2+
import TableProModels
3+
import TableProPluginKit
4+
5+
public final class ConnectionManager: @unchecked Sendable {
6+
private let pluginLoader: PluginLoader
7+
private let secureStore: SecureStore
8+
private let sshProvider: SSHProvider?
9+
10+
private let lock = NSLock()
11+
private var sessions: [UUID: ConnectionSession] = [:]
12+
13+
public init(
14+
pluginLoader: PluginLoader,
15+
secureStore: SecureStore,
16+
sshProvider: SSHProvider? = nil
17+
) {
18+
self.pluginLoader = pluginLoader
19+
self.secureStore = secureStore
20+
self.sshProvider = sshProvider
21+
}
22+
23+
public func connect(_ connection: DatabaseConnection) async throws -> ConnectionSession {
24+
let password = try secureStore.retrieve(forKey: connection.id.uuidString)
25+
26+
var effectiveHost = connection.host
27+
var effectivePort = connection.port
28+
if connection.sshEnabled, let ssh = connection.sshConfiguration {
29+
guard let provider = sshProvider else {
30+
throw ConnectionError.sshNotSupported
31+
}
32+
let tunnel = try await provider.createTunnel(
33+
config: ssh,
34+
remoteHost: connection.host,
35+
remotePort: connection.port
36+
)
37+
effectiveHost = tunnel.localHost
38+
effectivePort = tunnel.localPort
39+
}
40+
41+
do {
42+
guard let plugin = pluginLoader.driverPlugin(for: connection.type.pluginTypeId) else {
43+
throw ConnectionError.pluginNotFound(connection.type.rawValue)
44+
}
45+
46+
let config = DriverConnectionConfig(
47+
host: effectiveHost,
48+
port: effectivePort,
49+
username: connection.username,
50+
password: password ?? "",
51+
database: connection.database,
52+
additionalFields: connection.additionalFields
53+
)
54+
let pluginDriver = plugin.createDriver(config: config)
55+
56+
let driver = PluginDriverAdapter(pluginDriver: pluginDriver)
57+
try await driver.connect()
58+
59+
let session = ConnectionSession(
60+
connectionId: connection.id,
61+
driver: driver,
62+
activeDatabase: connection.database,
63+
status: .connected
64+
)
65+
storeSession(session, for: connection.id)
66+
return session
67+
} catch {
68+
if connection.sshEnabled, let provider = sshProvider {
69+
try? await provider.closeTunnel(for: connection.id)
70+
}
71+
throw error
72+
}
73+
}
74+
75+
public func disconnect(_ connectionId: UUID) async {
76+
let session = removeSession(for: connectionId)
77+
78+
guard let session else { return }
79+
try? await session.driver.disconnect()
80+
81+
if let sshProvider {
82+
try? await sshProvider.closeTunnel(for: connectionId)
83+
}
84+
}
85+
86+
public func updateSession(_ connectionId: UUID, _ mutation: (inout ConnectionSession) -> Void) {
87+
lock.lock()
88+
defer { lock.unlock() }
89+
guard var session = sessions[connectionId] else { return }
90+
mutation(&session)
91+
sessions[connectionId] = session
92+
}
93+
94+
public func switchDatabase(_ connectionId: UUID, to database: String) async throws {
95+
guard let session = session(for: connectionId) else {
96+
throw ConnectionError.notConnected
97+
}
98+
try await session.driver.switchDatabase(to: database)
99+
updateSession(connectionId) { $0.activeDatabase = database }
100+
}
101+
102+
private func storeSession(_ session: ConnectionSession, for id: UUID) {
103+
lock.lock()
104+
sessions[id] = session
105+
lock.unlock()
106+
}
107+
108+
private func removeSession(for id: UUID) -> ConnectionSession? {
109+
lock.lock()
110+
let session = sessions.removeValue(forKey: id)
111+
lock.unlock()
112+
return session
113+
}
114+
115+
public func session(for connectionId: UUID) -> ConnectionSession? {
116+
lock.lock()
117+
defer { lock.unlock() }
118+
return sessions[connectionId]
119+
}
120+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
import TableProModels
3+
4+
public struct ConnectionSession: Sendable {
5+
public let connectionId: UUID
6+
public let driver: any DatabaseDriver
7+
public internal(set) var activeDatabase: String
8+
public internal(set) var currentSchema: String?
9+
public internal(set) var status: ConnectionStatus
10+
public internal(set) var tables: [TableInfo]
11+
12+
public init(
13+
connectionId: UUID,
14+
driver: any DatabaseDriver,
15+
activeDatabase: String,
16+
currentSchema: String? = nil,
17+
status: ConnectionStatus = .connected,
18+
tables: [TableInfo] = []
19+
) {
20+
self.connectionId = connectionId
21+
self.driver = driver
22+
self.activeDatabase = activeDatabase
23+
self.currentSchema = currentSchema
24+
self.status = status
25+
self.tables = tables
26+
}
27+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Foundation
2+
import TableProModels
3+
4+
public protocol DatabaseDriver: AnyObject, Sendable {
5+
func connect() async throws
6+
func disconnect() async throws
7+
func ping() async throws -> Bool
8+
9+
func execute(query: String) async throws -> QueryResult
10+
func cancelCurrentQuery() async throws
11+
12+
func fetchTables(schema: String?) async throws -> [TableInfo]
13+
func fetchColumns(table: String, schema: String?) async throws -> [ColumnInfo]
14+
func fetchIndexes(table: String, schema: String?) async throws -> [IndexInfo]
15+
func fetchForeignKeys(table: String, schema: String?) async throws -> [ForeignKeyInfo]
16+
func fetchDatabases() async throws -> [String]
17+
18+
func switchDatabase(to name: String) async throws
19+
var supportsSchemas: Bool { get }
20+
func switchSchema(to name: String) async throws
21+
var currentSchema: String? { get }
22+
23+
var supportsTransactions: Bool { get }
24+
func beginTransaction() async throws
25+
func commitTransaction() async throws
26+
func rollbackTransaction() async throws
27+
28+
var serverVersion: String? { get }
29+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import Foundation
2+
import TableProModels
3+
import TableProPluginKit
4+
5+
public final class PluginDriverAdapter: DatabaseDriver, @unchecked Sendable {
6+
private let pluginDriver: any PluginDatabaseDriver
7+
8+
public init(pluginDriver: any PluginDatabaseDriver) {
9+
self.pluginDriver = pluginDriver
10+
}
11+
12+
public func connect() async throws {
13+
try await pluginDriver.connect()
14+
}
15+
16+
// PluginDatabaseDriver.disconnect() is sync and non-throwing, while
17+
// DatabaseDriver.disconnect() is async throws. A non-throwing call
18+
// satisfies the throwing requirement, so no try is needed here.
19+
public func disconnect() async throws {
20+
pluginDriver.disconnect()
21+
}
22+
23+
public func ping() async throws -> Bool {
24+
do {
25+
try await pluginDriver.ping()
26+
return true
27+
} catch {
28+
return false
29+
}
30+
}
31+
32+
public func execute(query: String) async throws -> QueryResult {
33+
let pluginResult = try await pluginDriver.execute(query: query)
34+
return QueryResult(from: pluginResult)
35+
}
36+
37+
public func cancelCurrentQuery() async throws {
38+
try pluginDriver.cancelQuery()
39+
}
40+
41+
public func fetchTables(schema: String?) async throws -> [TableInfo] {
42+
let pluginTables = try await pluginDriver.fetchTables(schema: schema)
43+
return pluginTables.map { TableInfo(from: $0) }
44+
}
45+
46+
public func fetchColumns(table: String, schema: String?) async throws -> [ColumnInfo] {
47+
let pluginColumns = try await pluginDriver.fetchColumns(table: table, schema: schema)
48+
return pluginColumns.enumerated().map { index, col in
49+
ColumnInfo(from: col, ordinalPosition: index)
50+
}
51+
}
52+
53+
public func fetchIndexes(table: String, schema: String?) async throws -> [IndexInfo] {
54+
let pluginIndexes = try await pluginDriver.fetchIndexes(table: table, schema: schema)
55+
return pluginIndexes.map { IndexInfo(from: $0) }
56+
}
57+
58+
public func fetchForeignKeys(table: String, schema: String?) async throws -> [ForeignKeyInfo] {
59+
let pluginFKs = try await pluginDriver.fetchForeignKeys(table: table, schema: schema)
60+
return pluginFKs.map { ForeignKeyInfo(from: $0) }
61+
}
62+
63+
public func fetchDatabases() async throws -> [String] {
64+
try await pluginDriver.fetchDatabases()
65+
}
66+
67+
public func switchDatabase(to name: String) async throws {
68+
try await pluginDriver.switchDatabase(to: name)
69+
}
70+
71+
public var supportsSchemas: Bool {
72+
pluginDriver.supportsSchemas
73+
}
74+
75+
public func switchSchema(to name: String) async throws {
76+
try await pluginDriver.switchSchema(to: name)
77+
}
78+
79+
public var currentSchema: String? {
80+
pluginDriver.currentSchema
81+
}
82+
83+
public var supportsTransactions: Bool {
84+
pluginDriver.supportsTransactions
85+
}
86+
87+
public func beginTransaction() async throws {
88+
try await pluginDriver.beginTransaction()
89+
}
90+
91+
public func commitTransaction() async throws {
92+
try await pluginDriver.commitTransaction()
93+
}
94+
95+
public func rollbackTransaction() async throws {
96+
try await pluginDriver.rollbackTransaction()
97+
}
98+
99+
public var serverVersion: String? {
100+
pluginDriver.serverVersion
101+
}
102+
}

0 commit comments

Comments
 (0)