Skip to content

Perfect-PostgreSQL 5.0.0

Latest

Choose a tag to compare

@taplin taplin released this 13 Oct 14:44
60fdd89

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 libpq on macOS, apt-get install libpq-dev pkg-config on Debian/Ubuntu. Homebrew's libpq is keg-only, and Swift 6.2's SwiftPM doesn't find it without export 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-log from: "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. PGConnection and PGResult are @unchecked Sendable and PostgreSQLError is Sendable.
  • One manifest, system libpq (#28). libpq is now an inline .systemLibrary target 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.
  • PGConnection can be shared between tasks (#29). Every method that touches the libpq connection holds an internal recursive lock, and doWithTransaction holds it for the whole transaction, so other callers can't slip statements between BEGIN and COMMIT.
  • 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). PostgresDatabaseConfiguration is now Sendable (#32), which DatabaseConnectionPool, transactionAsync, insertAsync, fetchAll and 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), and execute(_: 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: an INSERT … RETURNING through execute has inserted its row when it throws, and retrying it inserts the row again. mutate(_: DynamicMutation) and row-less DynamicSQL work, but affectedRows is always 0 and insertedID is always nil. DynamicSQL with allowsMultipleStatements: true throws ("cannot insert multiple commands into a prepared statement") and runs none of the statements. Unsigned values, up to UInt64.max, are bound exactly as decimal text.
    • Isolation levels: transaction(isolation:) ignores the level; the transaction runs at the server's default.
  • Logging through swift-log (#28). The connector's one diagnostic of its own (an unhandled result status) uses a Logger labelled perfect.postgresql instead of print. Errors and queries logged by the CRUD layer still go through Perfect-CRUD's CRUDLogging.

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_PATH on macOS with Swift 6.2), and remove Perfect-libpq / Perfect-libpq-linux from your own manifest if you added them. import libpq still works for the C types (Oid, PQnoticeReceiver).

  • Perfect-CRUD 3.0.0. CRUD's breaking changes apply (Expression is now CRUDExpression, Sendable closures, queries that were silently wrong now throw, and more). The ones that change what PostgreSQL stores or returns:

    • optional @ForeignKey / @PrimaryKey columns are now nullable columns of the wrapped type, not jsonb NOT NULL, and .reconcileTable won'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, "" or false.

    Upgrade: follow the CRUD 3.0.0 upgrade notes, and recreate or ALTER tables with optional @ForeignKey / @PrimaryKey columns created by older versions.

  • Notice handlers must be @Sendable (#28). PGConnection.ReceiverProc and ProcessorProc are now @Sendable closures. Upgrade: make the closures you pass to setReceiver / setProcessor capture only Sendable state.

  • Shared connections are serialized (#29). Calls on one PGConnection from different threads now wait for each other, and doWithTransaction blocks 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's DatabaseConnectionPool) when calls must not interleave. The lock doesn't cover CRUD's Database.transaction / transactionAsync, which send BEGIN, the body and COMMIT or ROLLBACK as separate calls: another task's statement on the same Database runs inside the transaction and is committed or rolled back with it. Don't share a Database that 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 .dropTable drops every table in the set (in reverse order) before creating any. .reconcileTable now 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 the search_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 example host: "localhost port=5433"), that whole string is now the host name: use the port: parameter, or pass a connection string to PostgresDatabaseConfiguration(_:), which is unchanged.

  • Diagnostics moved to swift-log (#28). The connector's one print (an unhandled result status) is now a warning on the perfect.postgresql logger; CRUD-layer errors are still logged through CRUDLogging. Upgrade: bootstrap LoggingSystem if 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 to PQexecParams through 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.
  • putCopyData byte length (#29). It passed the Character count 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 no Sendable annotations), and #28 then declared PGConnection @unchecked Sendable without 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 @ForeignKey to one of its sub-tables couldn't be created (relation "…" does not exist), and .dropTable over existing tables silently lost that constraint.
  • Reconcile and same-named tables (#30). .reconcileTable read columns from information_schema.columns by 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 "id turned 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 as x sslmode=disable added a connection option of its own.
  • Pool and async entry points with this connector (#32). PostgresDatabaseConfiguration wasn't Sendable, so CRUD 3.0.0's DatabaseConnectionPool and 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 generated INSERT INTO "t" () VALUES (), a syntax error; it now uses DEFAULT VALUES, as the key-path form already did.

Known limitations

  • A String value containing a NUL character is cut off at the NUL when it's sent.
  • [UInt8] / Data columns are decoded from PostgreSQL's default hex bytea output; with bytea_output = escape they decode as empty.
  • A NULL read into a non-optional Int or String property gives 0 or "" rather than an error.
  • Date values are stored to the millisecond.
  • A UInt or UInt64 value above Int64.max binds (Perfect-CRUD 3.0.1 sends it as unsigned), but a bigint column can't hold it, so the server rejects it with an error.

Upgrade notes

  1. 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.
  2. Install libpq: brew install libpq (plus PKG_CONFIG_PATH with Swift 6.2) or apt-get install libpq-dev pkg-config. Drop Perfect-libpq / Perfect-libpq-linux from your manifest.
  3. Make notice receiver and processor closures @Sendable.
  4. If you share a PGConnection between threads, check that no transaction closure waits on another thread that uses it. Don't share a Database that runs CRUD transactions between tasks; use DatabaseConnectionPool.
  5. If you reconcile parents with sub-tables, expect the sub-tables to be reconciled (and their removed columns dropped) on every run.
  6. Don't use Dynamic select, affectedRows, insertedID or transaction(isolation:) with this connector yet, and don't retry a Dynamic execute that threw after running a write.
  7. If you appended options to a PostgresDatabaseConfiguration parameter 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.