Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Connect to PGlite through its socket server (`pglite-server`), with schema browsing, SQL editing, and row editing like PostgreSQL. Defaults to a loopback host with no TLS, and treats the connection as single-use to match PGlite's one-connection limit. (#1911)
- In the CSV editor, right-click a column header to rename, insert, delete, or change the type of that column. (#1913)

### Fixed
Expand Down
8 changes: 6 additions & 2 deletions Plugins/PostgreSQLDriverPlugin/LibPQDriverCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TableProPluginKit
final class LibPQDriverCore: @unchecked Sendable {
private let config: DriverConnectionConfig
private let schemaFallbackQueries: [String]
private let singleConnectionMode: Bool
private var libpqConnection: LibPQPluginConnection?

var currentSchema: String = "public"
Expand All @@ -24,10 +25,12 @@ final class LibPQDriverCore: @unchecked Sendable {

init(
config: DriverConnectionConfig,
schemaFallbackQueries: [String] = PostgreSQLSchemaQueries.schemaFallbackQueries
schemaFallbackQueries: [String] = PostgreSQLSchemaQueries.schemaFallbackQueries,
singleConnectionMode: Bool = false
) {
self.config = config
self.schemaFallbackQueries = schemaFallbackQueries
self.singleConnectionMode = singleConnectionMode
}

// MARK: - Connection
Expand All @@ -40,7 +43,8 @@ final class LibPQDriverCore: @unchecked Sendable {
password: config.password.isEmpty ? nil : config.password,
database: config.database,
sslConfig: config.ssl,
options: config.additionalFields["connectionOptions"]
options: config.additionalFields["connectionOptions"],
suppressServerSideCancel: singleConnectionMode
)

try await pqConn.connect()
Expand Down
36 changes: 20 additions & 16 deletions Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
private let database: String
private let sslConfig: SSLConfiguration
private let options: String?
private let suppressServerSideCancel: Bool

private let stateLock = NSLock()
private var _isConnected: Bool = false
Expand Down Expand Up @@ -135,7 +136,8 @@
password: String?,
database: String,
sslConfig: SSLConfiguration = SSLConfiguration(),
options: String? = nil
options: String? = nil,
suppressServerSideCancel: Bool = false
) {
self.host = host
self.port = port
Expand All @@ -144,6 +146,7 @@
self.database = database
self.sslConfig = sslConfig
self.options = options
self.suppressServerSideCancel = suppressServerSideCancel
}

deinit {
Expand All @@ -160,9 +163,9 @@
// MARK: - Connection Management

func connect() async throws {
stateLock.lock()

Check warning on line 166 in Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

instance method 'lock' is unavailable from asynchronous contexts; Use async-safe scoped locking instead; this is an error in the Swift 6 language mode
_isConnectCancelled = false
stateLock.unlock()

Check warning on line 168 in Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

instance method 'unlock' is unavailable from asynchronous contexts; Use async-safe scoped locking instead; this is an error in the Swift 6 language mode

try await withTaskCancellationHandler {
try await pluginDispatchAsyncCancellable(
Expand Down Expand Up @@ -358,7 +361,7 @@
let currentConn = conn
stateLock.unlock()

guard let currentConn else { return }
guard let currentConn, !suppressServerSideCancel else { return }
let cancelObj = PQgetCancel(currentConn)
guard let cancelObj else { return }
defer { PQfreeCancel(cancelObj) }
Expand Down Expand Up @@ -555,9 +558,22 @@

// MARK: - Streaming Query

private static func cancelAndDrain(_ conn: OpaquePointer, suppressCancel: Bool) {
if !suppressCancel {
let cancelObj = PQgetCancel(conn)
if let cancelObj {
var errbuf = [CChar](repeating: 0, count: 256)
PQcancel(cancelObj, &errbuf, Int32(errbuf.count))
PQfreeCancel(cancelObj)
}
}
while let res = PQgetResult(conn) { PQclear(res) }
}

func streamQuery(_ query: String) -> AsyncThrowingStream<PluginStreamElement, Error> {
let queryToRun = String(query)
let queue = self.queue
let suppressCancel = suppressServerSideCancel

final class StreamState: @unchecked Sendable {
var conn: OpaquePointer?
Expand All @@ -583,13 +599,7 @@
streamState.drained = true
streamState.lock.unlock()
guard let conn, !alreadyDrained else { return }
let cancelObj = PQgetCancel(conn)
if let cancelObj {
var errbuf = [CChar](repeating: 0, count: 256)
PQcancel(cancelObj, &errbuf, Int32(errbuf.count))
PQfreeCancel(cancelObj)
}
while let res = PQgetResult(conn) { PQclear(res) }
Self.cancelAndDrain(conn, suppressCancel: suppressCancel)
}
}

Expand Down Expand Up @@ -703,13 +713,7 @@
if !batch.isEmpty {
continuation.yield(.rows(batch))
}
let cancelObj = PQgetCancel(conn)
if let cancelObj {
var errbuf = [CChar](repeating: 0, count: 256)
PQcancel(cancelObj, &errbuf, Int32(errbuf.count))
PQfreeCancel(cancelObj)
}
while let res = PQgetResult(conn) { PQclear(res) }
Self.cancelAndDrain(conn, suppressCancel: suppressCancel)
streamState.lock.lock()
streamState.drained = true
streamState.lock.unlock()
Expand Down
54 changes: 54 additions & 0 deletions Plugins/PostgreSQLDriverPlugin/PGlitePluginDriver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// PGlitePluginDriver.swift
// PostgreSQLDriverPlugin
//
// PGlite PluginDatabaseDriver implementation. PGlite is PostgreSQL 17 compiled to
// WASM, reached over its socket server (@electric-sql/pglite-socket), so it reuses
// PostgreSQLPluginDriver's introspection. It is single-connection: query cancellation
// is a protocol no-op and a second connection is refused, so this driver suppresses
// the wire-level cancel and drops the cancelQuery capability.
//

import Foundation
import TableProPluginKit

final class PGlitePluginDriver: PostgreSQLPluginDriver {

Check warning on line 15 in Plugins/PostgreSQLDriverPlugin/PGlitePluginDriver.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

class 'PGlitePluginDriver' must restate inherited '@unchecked Sendable' conformance

Check warning on line 15 in Plugins/PostgreSQLDriverPlugin/PGlitePluginDriver.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

class 'PGlitePluginDriver' must restate inherited '@unchecked Sendable' conformance
private let connectHost: String
private let connectPort: Int

init(config: DriverConnectionConfig) {
self.connectHost = config.host
self.connectPort = config.port
super.init(config: config, singleConnectionMode: true)
}

override var capabilities: PluginCapabilities {
super.capabilities.subtracting(.cancelQuery)
}

override func connect() async throws {
do {
try await super.connect()
} catch is CancellationError {
throw CancellationError()
} catch {
throw Self.connectError(underlying: error, host: connectHost, port: connectPort)
}
}

private static func connectError(underlying: Error, host: String, port: Int) -> Error {
let reason = (underlying as? LibPQPluginError)?.message ?? underlying.localizedDescription
let template = String(
localized: "Can't reach a PGlite socket server at %@:%d. Start it with 'npx @electric-sql/pglite-socket', then try again."
)
return PGliteConnectionError(
pluginErrorMessage: String(format: template, host, port),
pluginErrorDetail: reason.isEmpty ? nil : reason
)
}
}

struct PGliteConnectionError: PluginDriverError {
let pluginErrorMessage: String
let pluginErrorDetail: String?
}
8 changes: 5 additions & 3 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import TableProPluginKit
final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
static let pluginName = "PostgreSQL Driver"
static let pluginVersion = "1.0.0"
static let pluginDescription = "PostgreSQL, Redshift, and CockroachDB support via libpq"
static let pluginDescription = "PostgreSQL, Redshift, CockroachDB, and PGlite support via libpq"
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "PostgreSQL"
static let databaseDisplayName = "PostgreSQL"
static let iconName = "postgresql-icon"
static let defaultPort = 5432
static let defaultPort = 5_432
static let additionalConnectionFields: [ConnectionField] = [
ConnectionField(
id: "usePgpass",
Expand Down Expand Up @@ -88,7 +88,7 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
section: .advanced
)
]
static let additionalDatabaseTypeIds: [String] = ["Redshift", "CockroachDB"]
static let additionalDatabaseTypeIds: [String] = ["Redshift", "CockroachDB", "PGlite"]

// MARK: - UI/Capability Metadata

Expand Down Expand Up @@ -176,6 +176,7 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
case "PostgreSQL": return "PostgreSQL"
case "Redshift": return "Redshift"
case "CockroachDB": return "CockroachDB"
case "PGlite": return "PGlite"
default: return nil
}
}
Expand All @@ -185,6 +186,7 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
switch variant {
case "Redshift": return RedshiftPluginDriver(config: config)
case "CockroachDB": return CockroachPluginDriver(config: config)
case "PGlite": return PGlitePluginDriver(config: config)
default: return PostgreSQLPluginDriver(config: config)
}
}
Expand Down
6 changes: 3 additions & 3 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
import os
import TableProPluginKit

final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
let core: LibPQDriverCore

private static let logger = Logger(subsystem: "com.TablePro.PostgreSQLDriver", category: "PostgreSQLPluginDriver")
Expand Down Expand Up @@ -41,8 +41,8 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
]
}

init(config: DriverConnectionConfig) {
self.core = LibPQDriverCore(config: config)
init(config: DriverConnectionConfig, singleConnectionMode: Bool = false) {
self.core = LibPQDriverCore(config: config, singleConnectionMode: singleConnectionMode)
}

// MARK: - Connection
Expand Down
7 changes: 1 addition & 6 deletions TablePro/Core/Plugins/PluginManager+Registration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,7 @@ extension PluginManager {
}
PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: typeId, preserveIcon: true)
for additionalId in driverType.additionalDatabaseTypeIds {
var additionalSnapshot = snapshot
if let existingDefault = PluginMetadataRegistry.shared.snapshot(forTypeId: additionalId),
!existingDefault.explainVariants.isEmpty {
additionalSnapshot = snapshot.withExplainVariants(existingDefault.explainVariants)
}
PluginMetadataRegistry.shared.register(snapshot: additionalSnapshot, forTypeId: additionalId, preserveIcon: true)
PluginMetadataRegistry.shared.registerVariant(pluginSnapshot: snapshot, forTypeId: additionalId)
PluginMetadataRegistry.shared.registerTypeAlias(additionalId, primaryTypeId: typeId)
}

Expand Down
Loading
Loading