Repository navigation
The first release since 4.0.0 (January 2020). It brings the connector up to Swift 6 and Perfect-CRUD 3.0.0, takes libpq from the system on both macOS and Linux, makes PGConnection safe to share, and fixes bugs in parameter passing, COPY, table creation, reconcile and identifier quoting, one of which let Dynamic API names inject SQL. It is a major version with source-breaking changes. Read "Breaking and behaviour changes" before upgrading.
Requirements
- Swift tools 6.2 and the Swift 6 language mode (4.0.0 declared tools 5.1).
- Platforms: macOS 12+ (was 10.15+) and Linux. No iOS: libpq is a host system library.
- The libpq client library, found through pkg-config:
brew install libpqon macOS,apt-get install libpq-dev pkg-configon Debian/Ubuntu. Homebrew's libpq is keg-only, and Swift 6.2's SwiftPM doesn't find it withoutexport PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig"(Swift 6.4's does). - Dependencies: Perfect-CRUD
from: "3.0.1"(3.0.1 fixes four crashes in 3.0.0) and apple/swift-logfrom: "1.5.0".
.package(url: "https://github.com/PerfectlySoft/Perfect-PostgreSQL.git", from: "5.0.0")CI now runs on GitHub Actions for every push and pull request: Linux (Swift 6.2 and 6.4, Ubuntu 24.04) against a PostgreSQL 17 service, and macOS (Xcode 26.0.1 and 26.6) without a server, each in debug and release with warnings treated as errors.
Highlights
- Swift 6 (#28). The package builds in the Swift 6 language mode.
PGConnectionandPGResultare@unchecked SendableandPostgreSQLErrorisSendable. - One manifest, system libpq (#28). libpq is now an inline
.systemLibrarytarget found through pkg-config, with Homebrew and apt providers. 4.0.0's manifest pulled in the Perfect-libpq package on macOS and Perfect-libpq-linux on Linux. PGConnectioncan be shared between tasks (#29). Every method that touches the libpq connection holds an internal recursive lock, anddoWithTransactionholds it for the whole transaction, so other callers can't slip statements betweenBEGINandCOMMIT.- Perfect-CRUD 3.0.0. Everything in CRUD 3.0.0 is available, including connection pooling, async entry points, migrations and nested transactions (savepoints, checked against PostgreSQL 17 for this release).
PostgresDatabaseConfigurationis nowSendable(#32), whichDatabaseConnectionPool,transactionAsync,insertAsync,fetchAlland the other async entry points require; without it they didn't compile with this connector. Two parts need connector support that this release doesn't have:- Dynamic API:
select(_: DynamicQuery), andexecute(_: DynamicSQL)for a statement that returns rows, throw "Dynamic rows are not supported by this connector" once a row comes back (an empty result is returned empty). The error comes only after the statement has run and, outside a transaction, been committed: anINSERT … RETURNINGthroughexecutehas inserted its row when it throws, and retrying it inserts the row again.mutate(_: DynamicMutation)and row-lessDynamicSQLwork, butaffectedRowsis always 0 andinsertedIDis always nil.DynamicSQLwithallowsMultipleStatements: truethrows ("cannot insert multiple commands into a prepared statement") and runs none of the statements. Unsigned values, up toUInt64.max, are bound exactly as decimal text. - Isolation levels:
transaction(isolation:)ignores the level; the transaction runs at the server's default.
- Dynamic API:
- Logging through swift-log (#28). The connector's one diagnostic of its own (an unhandled result status) uses a
Loggerlabelledperfect.postgresqlinstead ofprint. Errors and queries logged by the CRUD layer still go through Perfect-CRUD'sCRUDLogging.
Breaking and behaviour changes
-
Toolchain and platforms. Swift tools 6.2 is required, and the minimum macOS is 12. Upgrade: use Swift 6.2 or newer and raise your deployment target to macOS 12.
-
libpq comes from the system (#28). Upgrade: install libpq as in Requirements (with
PKG_CONFIG_PATHon macOS with Swift 6.2), and remove Perfect-libpq / Perfect-libpq-linux from your own manifest if you added them.import libpqstill works for the C types (Oid,PQnoticeReceiver). -
Perfect-CRUD 3.0.0. CRUD's breaking changes apply (
Expressionis nowCRUDExpression,Sendableclosures, queries that were silently wrong now throw, and more). The ones that change what PostgreSQL stores or returns:- optional
@ForeignKey/@PrimaryKeycolumns are now nullable columns of the wrapped type, notjsonb NOT NULL, and.reconcileTablewon't change an existing column; - a query result missing an optional wrapper's column now reads nil instead of throwing;
- a pivot-joined row that no parent links to no longer attaches to a parent whose key is 0,
""orfalse.
Upgrade: follow the CRUD 3.0.0 upgrade notes, and recreate or
ALTERtables with optional@ForeignKey/@PrimaryKeycolumns created by older versions. - optional
-
Notice handlers must be
@Sendable(#28).PGConnection.ReceiverProcandProcessorProcare now@Sendableclosures. Upgrade: make the closures you pass tosetReceiver/setProcessorcapture onlySendablestate. -
Shared connections are serialized (#29). Calls on one
PGConnectionfrom different threads now wait for each other, anddoWithTransactionblocks every other caller on that connection until it commits or rolls back. Separate calls from different tasks can still interleave. Upgrade: don't wait, inside a transaction closure, for another thread that uses the same connection; that now deadlocks. Use one connection per task (for example CRUD'sDatabaseConnectionPool) when calls must not interleave. The lock doesn't cover CRUD'sDatabase.transaction/transactionAsync, which sendBEGIN, the body andCOMMITorROLLBACKas separate calls: another task's statement on the sameDatabaseruns inside the transaction and is committed or rolled back with it. Don't share aDatabasethat runs transactions between tasks; use the pool. -
create()with sub-tables (#30). Tables are created in foreign-key order, so a parent can reference one of its sub-tables, and.dropTabledrops every table in the set (in reverse order) before creating any..reconcileTablenow reconciles existing sub-tables as well as the parent, so columns a sub-table's model no longer has are dropped from it. Reconcile reads the table that the unqualified name resolves to on thesearch_path. When it needs to alter a table the role can see but doesn't own, it now fails with "must be owner" (that table used to be skipped). Upgrade: review reconciles of parents with sub-tables, and back up before running one against data you care about. -
Identifiers containing
"(#31).quote(identifier:)doubles an embedded". A table or column name containing"now refers to exactly that name; before, it ended the quoted identifier early. Upgrade: nothing, unless you relied on that, which only worked by accident. -
Connection parameters are quoted (#32).
PostgresDatabaseConfiguration(database:host:port:username:password:)now single-quotes each value and escapes\and'. Upgrade: pass values as they are. If you added your own conninfo quoting or escaping to these parameters, remove it. If you appended options inside a value (for examplehost: "localhost port=5433"), that whole string is now the host name: use theport:parameter, or pass a connection string toPostgresDatabaseConfiguration(_:), which is unchanged. -
Diagnostics moved to swift-log (#28). The connector's one
print(an unhandled result status) is now a warning on theperfect.postgresqllogger; CRUD-layer errors are still logged throughCRUDLogging. Upgrade: bootstrapLoggingSystemif you want to see it somewhere other than swift-log's default handler.
Fixes
- Parameter buffer lifetimes (#29, reported in #26).
exec(statement:params:)passed parameter values toPQexecParamsthrough pointers that Swift only guarantees for a single expression. That was undefined behaviour; it was not seen to misbehave, including in optimized builds and under Address Sanitizer. Each value is now copied into a buffer owned by the call. Thanks to @RockfordWei (#26) for the report. putCopyDatabyte length (#29). It passed theCharactercount as the byte length, so non-ASCII COPY data was cut off and the COPY failed.- Concurrent use of one
PGConnection(#29, raised in #22 by @natsuki14). 4.0.0 had no locking (and noSendableannotations), and #28 then declaredPGConnection@unchecked Sendablewithout adding any, so sharing one connection between threads was unsafe; with 50 tasks sharing a connection, queries failed and one run crashed. create()table order (#30). A parent with a@ForeignKeyto one of its sub-tables couldn't be created (relation "…" does not exist), and.dropTableover existing tables silently lost that constraint.- Reconcile and same-named tables (#30).
.reconcileTableread columns frominformation_schema.columnsby table name alone, so a same-named table in another schema contributed its columns: a column name in both crashed the process with "Duplicate values for key", and other columns were wrongly dropped or never added. - SQL injection through Dynamic API names (#31). Caller-supplied table and field names weren't escaped, so a predicate field like
id" = "id" OR "idturned a one-row delete into a delete of every row. Escaping is per Unicode scalar, so a"followed by a combining mark is doubled too. - Connection parameters with spaces or quotes (#32).
PostgresDatabaseConfiguration(database:host:…)pasted values into the conninfo string unquoted, so a password with a space failed to connect ("missing "=" after …"), and a value such asx sslmode=disableadded a connection option of its own. - Pool and async entry points with this connector (#32).
PostgresDatabaseConfigurationwasn'tSendable, so CRUD 3.0.0'sDatabaseConnectionPooland async methods couldn't be used with it. returning(insert: [])(#32). An empty array trapped with "Index out of range"; it now returns[].- Whole-row
returning(insert:)with every key ignored (#32). It generatedINSERT INTO "t" () VALUES (), a syntax error; it now usesDEFAULT VALUES, as the key-path form already did.
Known limitations
- A
Stringvalue containing a NUL character is cut off at the NUL when it's sent. [UInt8]/Datacolumns are decoded from PostgreSQL's default hexbyteaoutput; withbytea_output = escapethey decode as empty.- A NULL read into a non-optional
IntorStringproperty gives 0 or""rather than an error. Datevalues are stored to the millisecond.- A
UIntorUInt64value aboveInt64.maxbinds (Perfect-CRUD 3.0.1 sends it as unsigned), but abigintcolumn can't hold it, so the server rejects it with an error.
Upgrade notes
- Point your dependency at
from: "5.0.0", build with Swift 6.2 or newer, and raise your deployment target to macOS 12. Apply the Perfect-CRUD 3.0.0 upgrade notes too. - Install libpq:
brew install libpq(plusPKG_CONFIG_PATHwith Swift 6.2) orapt-get install libpq-dev pkg-config. Drop Perfect-libpq / Perfect-libpq-linux from your manifest. - Make notice receiver and processor closures
@Sendable. - If you share a
PGConnectionbetween threads, check that no transaction closure waits on another thread that uses it. Don't share aDatabasethat runs CRUD transactions between tasks; useDatabaseConnectionPool. - If you reconcile parents with sub-tables, expect the sub-tables to be reconciled (and their removed columns dropped) on every run.
- Don't use Dynamic
select,affectedRows,insertedIDortransaction(isolation:)with this connector yet, and don't retry a Dynamicexecutethat threw after running a write. - If you appended options to a
PostgresDatabaseConfigurationparameter value, move them to their own parameter or a connection string.
Tested for this release: all 64 tests pass against PostgreSQL 17 on macOS with Xcode 27 (Swift 6.4) and Xcode 26.3 (Swift 6.2.4), and on Linux with Swift 6.2.4 and 6.4 (Ubuntu 24.04), each in debug and release with warnings as errors. A scratch package depending on a local 5.0.0 tag resolved CRUD 3.0.0 and built with both macOS toolchains.
Legacy
The pre-Swift-6 code is preserved on the legacy branch, which points at the same commit as the 4.0.0 tag. Stay on 4.0.0 if you can't move to Swift 6.2 yet.