A PostgreSQL connector for Swift, built on the libpq client library. It has two layers:
- a Perfect-CRUD backend,
PostgresDatabaseConfiguration, soCodabletypes can be created, inserted, queried, joined, updated and deleted through CRUD, plus PostgreSQL-onlyreturning(...)variants of insert and update; - the lower-level
PGConnection/PGResultwrapper around libpq.
Perfect-Session's PostgreSQL session driver is built on it.
The pre-Swift-6 version of this package (4.0.0 and earlier) is preserved on the legacy branch.
Requirements: Swift 6.2 or newer (Swift 6 language mode). macOS 12+ and Linux, with the libpq client library installed (see below). CI runs the tests on Linux (Swift 6.2 and 6.4, Ubuntu 24.04, against PostgreSQL 17) and macOS (Xcode 26.0.1 and 26.6) in debug and release.
dependencies: [
.package(url: "https://github.com/PerfectlySoft/Perfect-PostgreSQL.git", from: "5.0.0")
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "PerfectPostgreSQL", package: "Perfect-PostgreSQL")
])
]On macOS, install it with Homebrew:
brew install libpq
Homebrew's libpq is keg-only (not linked into /opt/homebrew). Swift 6.4's SwiftPM finds it anyway, but Swift 6.2's
doesn't, and the build fails with 'libpq-fe.h' file not found. Pointing pkg-config at it works with both:
export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig"
On Linux (Debian/Ubuntu):
sudo apt-get install libpq-dev pkg-config
import PerfectCRUD
import PerfectPostgreSQL
struct Person: Codable {
let id: Int
let name: String
}
let db = Database(configuration: try PostgresDatabaseConfiguration(
database: "mydb", host: "localhost", username: "me", password: "secret"))
try db.create(Person.self, policy: .reconcileTable)
let people = db.table(Person.self)
try people.insert(Person(id: 1, name: "Ada"))
let ada = try people.where(\Person.id == 1).first()PostgresDatabaseConfiguration("host=localhost dbname=mydb") takes a libpq connection string instead. libpq's
environment variables (PGPORT, PGUSER, PGPASSWORD and so on) fill in anything the connection string leaves out.
See Perfect-CRUD for the query API. What the PostgreSQL backend doesn't support:
- Dynamic Select:
select(_: DynamicQuery)throws "Dynamic rows are not supported by this connector" as soon as a row comes back, and so doesexecute(_: DynamicSQL)for a statement that returns rows. The error comes only after the statement has run (and, outside a transaction, been committed): anINSERT … RETURNINGthroughexecutehas inserted its row when it throws, so retrying it inserts the row again.mutate(_: DynamicMutation)and row-lessDynamicSQLwork, but the result'saffectedRowsis always 0 and itsinsertedIDis nil.DynamicSQLwithallowsMultipleStatements: truethrows (PostgreSQL's extended protocol takes one statement per call) and runs none of them. - Isolation levels:
transaction(isolation:)ignores the level and uses the server's default.
PGConnection and PGResult are synchronous (blocking) and @unchecked Sendable. Every PGConnection method holds
an internal lock, and doWithTransaction holds it for the whole transaction, so a connection can be shared between
tasks. Separate calls from different tasks can still interleave, so give each task its own connection (for example
through Perfect-CRUD's DatabaseConnectionPool) when that matters.
That lock does not cover Perfect-CRUD's Database.transaction / transactionAsync: they send BEGIN, the body's
statements and COMMIT or ROLLBACK as separate calls, so 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; take one connection per task from a DatabaseConnectionPool. PostgresDatabaseConfiguration is
Sendable, so the pool and CRUD's async entry points (transactionAsync, insertAsync, fetchAll and so on) work
with it.
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.
swift test
The tests that need a server only run with PG_TESTS=1. They connect with host=localhost and the
postgres and testing123 databases (the tests drop and recreate testing123 and
perfect_subtable_order_test), so use a throwaway server. libpq's environment variables set the rest, for example:
docker run -d --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:17
PG_TESTS=1 PGUSER=postgres swift test
Use PGPORT for a server on another port, and PGHOSTADDR for one that isn't on localhost.
5.0.0 is a major release. The release notes list every change; the ones most likely to need attention when upgrading:
- Toolchain and platforms. Swift tools 6.2 and the Swift 6 language mode are required, and the minimum macOS is now 12 (was 10.15).
- libpq comes from the system. The Perfect-libpq and Perfect-libpq-linux packages are no longer used: install libpq as above, and remove those packages from your own manifest if you added them.
- Perfect-CRUD 3.0.0. CRUD's own breaking changes apply; see its migration notes.
Sendable. Closures passed toPGConnection.setReceiverandsetProcessormust be@Sendable.- Shared connections are serialized.
PGConnectioncalls from different threads now wait for each other, anddoWithTransactionblocks other callers on that connection until it commits or rolls back. Code inside the transaction closure that waits for another thread using the same connection will deadlock. create()with sub-tables. Tables are created in foreign-key order, and.dropTabledrops every table in the set before creating any..reconcileTablenow reconciles existing sub-tables too, which drops columns their models no longer have. When it needs to alter a table the role doesn't own, it fails with "must be owner".- Identifiers containing
".quote(identifier:)now doubles an embedded", so such a name refers to the table or column of that exact name. Before, it ended the quoted identifier and let a Dynamic API name inject SQL. - Connection parameters are quoted.
PostgresDatabaseConfiguration(database:host:port:username:password:)now quotes each value, so a value is used exactly as given. If you escaped values yourself, or appended options inside one (such ashost: "localhost port=5433"), pass the plain value and use the matching parameter or a connection string withPostgresDatabaseConfiguration(_:)instead. - Logging. The connector's one diagnostic of its own (an unhandled result status) goes through
swift-log (label
perfect.postgresql) instead ofprint. Errors and queries logged by the CRUD layer still go through Perfect-CRUD'sCRUDLogging.
Apache 2.0. See LICENSE.