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
102 changes: 102 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
linux:
# Everything, including the live tests, against a PostgreSQL service container.
name: Linux (Swift ${{ matrix.swift }}, ${{ matrix.configuration }})
runs-on: ubuntu-24.04
container: swift:${{ matrix.swift }}-noble
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# The oldest supported Swift (swift-tools-version 6.2) and the newest.
swift: ["6.2", "6.4"]
configuration: [debug, release]
services:
postgres:
image: postgres:17
env:
POSTGRES_HOST_AUTH_METHOD: trust
options: >-
--health-cmd "pg_isready -U postgres" --health-interval 5s --health-timeout 5s --health-retries 20
env:
PG_TESTS: "1"
PGUSER: postgres
steps:
- uses: actions/checkout@v7

- name: Install client libraries
run: |
apt-get update
apt-get install -y --no-install-recommends libpq-dev pkg-config

# The tests connect with "host=localhost", which PGHOST can't override. libpq's
# PGHOSTADDR can: the connection goes to hostaddr, and host is only used for
# authentication and TLS.
- name: Point libpq at the service
run: |
addr=$(getent hosts postgres | awk '{ print $1; exit }')
if [ -z "$addr" ]; then
echo "Could not resolve the postgres service"
exit 1
fi
echo "PGHOSTADDR=$addr" >> "$GITHUB_ENV"

- name: Toolchain
run: swift --version

- name: Build (warnings are errors)
run: |
flags="-c ${{ matrix.configuration }} -Xswiftc -warnings-as-errors"
# @testable imports need testing enabled in an optimized build.
[ "${{ matrix.configuration }}" = release ] && flags="$flags -Xswiftc -enable-testing"
echo "FLAGS=$flags" >> "$GITHUB_ENV"
swift build --build-tests $flags

- name: Test
run: swift test $FLAGS

macos:
# GitHub's macOS runners have no PostgreSQL server: this builds everything and runs the
# tests that don't need one (the live tests are skipped without PG_TESTS=1).
name: macOS (Xcode ${{ matrix.xcode }}, ${{ matrix.configuration }})
runs-on: macos-26
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# The oldest Xcode with Swift 6.2, and the runner's default.
xcode: ["26.0.1", "26.6"]
configuration: [debug, release]
steps:
- uses: actions/checkout@v7

- name: Select Xcode
run: |
sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
swift --version

- name: Install client libraries
run: |
brew install libpq
# libpq is keg-only, so pkg-config doesn't find it without this.
echo "PKG_CONFIG_PATH=$(brew --prefix libpq)/lib/pkgconfig" >> "$GITHUB_ENV"

- name: Build (warnings are errors)
run: |
flags="-c ${{ matrix.configuration }} -Xswiftc -warnings-as-errors"
# @testable imports need testing enabled in an optimized build.
[ "${{ matrix.configuration }}" = release ] && flags="$flags -Xswiftc -enable-testing"
echo "FLAGS=$flags" >> "$GITHUB_ENV"
swift build --build-tests $flags

- name: Test
run: swift test $FLAGS
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let package = Package(
.library(name: "PerfectPostgreSQL", targets: ["PerfectPostgreSQL"]),
],
dependencies: [
.package(url: "https://github.com/PerfectlySoft/Perfect-CRUD.git", branch: "main"),
.package(url: "https://github.com/PerfectlySoft/Perfect-CRUD.git", from: "3.0.1"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
],
targets: [
Expand Down
177 changes: 125 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,86 +1,159 @@
# Perfect - PostgreSQL Connector

<p align="center">
<a href="https://developer.apple.com/swift/" target="_blank">
<img src="https://img.shields.io/badge/Swift-6.2-orange.svg?style=flat" alt="Swift 6.2">
</a>
<a href="https://developer.apple.com/swift/" target="_blank">
<img src="https://img.shields.io/badge/Platforms-macOS%2012%2B-lightgray.svg?style=flat" alt="Platforms macOS 12+">
</a>
<a href="LICENSE" target="_blank">
<img src="https://img.shields.io/badge/License-Apache-lightgrey.svg?style=flat" alt="License Apache">
</a>
<img src="https://img.shields.io/badge/Swift-6.2-orange.svg?style=flat" alt="Swift 6.2">
<img src="https://img.shields.io/badge/Platforms-macOS%2012%2B%20%7C%20Linux-lightgray.svg?style=flat" alt="Platforms macOS 12+ | Linux">
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-lightgrey.svg?style=flat" alt="License Apache 2.0"></a>
</p>

This package has been modernized for Swift 6.
A PostgreSQL connector for Swift, built on the `libpq` client library. It has two layers:

It provides a Swift wrapper around the `libpq` client library for connecting to PostgreSQL servers, **and** a full [Perfect-CRUD](https://github.com/PerfectlySoft/Perfect-CRUD) SQL provider backend for PostgreSQL. `Sources/PerfectPostgreSQL/PostgresCRUD.swift` implements Perfect-CRUD's `SQLGenDelegate`, `SQLExeDelegate`, and `DatabaseConfigurationProtocol`, so this is no longer a bare, stand-alone libpq connector — it's a CRUD driver that happens to also expose the lower-level `PGConnection`/`PGResult` libpq wrapper (`Sources/PerfectPostgreSQL/PerfectPostgreSQL.swift`, using `swift-log` for diagnostics rather than `print`).
- a [Perfect-CRUD](https://github.com/PerfectlySoft/Perfect-CRUD) backend, `PostgresDatabaseConfiguration`, so
`Codable` types can be created, inserted, queried, joined, updated and deleted through CRUD, plus
PostgreSQL-only `returning(...)` variants of insert and update;
- the lower-level `PGConnection` / `PGResult` wrapper around libpq.

**Usage status:** this driver is real, tested, and consumed today — [Perfect-Session](https://github.com/PerfectlySoft/Perfect-Session)'s `PostgreSQLSessionDriver` imports `PerfectPostgreSQL` directly and is one of Perfect-Session's four supported backend drivers.
[Perfect-Session](https://github.com/PerfectlySoft/Perfect-Session)'s PostgreSQL session driver is built on it.

The pre-Swift-6 version of this package is preserved on the [`legacy`](../../tree/legacy) branch.
The pre-Swift-6 version of this package (4.0.0 and earlier) is preserved on the [`legacy`](../../tree/legacy) branch.

Both `libpq` wrapper types (`PGConnection`, `PGResult`) are synchronous/blocking and marked `@unchecked Sendable`; there is no async/await API in this package.
**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.

This package builds with Swift Package Manager. Ensure you have installed and activated a Swift 6.2 (or later) tool chain.

## Requirements

- swift-tools-version: 6.2, built with `swiftLanguageMode(.v6)` (strict concurrency)
- Platform: `platforms: [.macOS(.v12)]` — macOS 12 or later (no other Apple platforms declared)
- Linux is buildable via SwiftPM/systemLibrary (the `libpq` system-library target declares an `.apt(["libpq-dev"])` provider) but is not asserted in the `platforms` array, so Linux support is implicit, not a guaranteed contract of this manifest.

## Dependencies

Declared in `Package.swift`:

- [`Perfect-CRUD`](https://github.com/PerfectlySoft/Perfect-CRUD) (`.package(url:, branch: "main")`), product `PerfectCRUD`.
- [`swift-log`](https://github.com/apple/swift-log.git) `from: 1.5.0`, product `Logging`.
- A `.systemLibrary` target `libpq` (via `pkgConfig: "libpq"`) providing the C libpq bindings.
```swift
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")
])
]
```

## macOS Build Notes
## Installing libpq

This package requires the `libpq` client library, available via Homebrew:
On macOS, install it with Homebrew:

```
brew install libpq
```

## Linux Build Notes

Ensure that you have installed `libpq-dev`.
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:

```
sudo apt-get install libpq-dev
export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig"
```

## Building
On Linux (Debian/Ubuntu):

```
sudo apt-get install libpq-dev pkg-config
```

Add this project as a dependency in your `Package.swift` file:
## Usage

```swift
dependencies: [
// No tagged releases exist yet, so pin a branch rather than a version:
.package(url: "https://github.com/PerfectlySoft/Perfect-PostgreSQL.git", branch: "main"),
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "PerfectPostgreSQL", package: "Perfect-PostgreSQL"),
]
),
]
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()
```

## Testing
`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](https://github.com/PerfectlySoft/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 does `execute(_: DynamicSQL)` for a statement that returns rows. 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, so retrying it inserts the row again. `mutate(_: DynamicMutation)`
and row-less `DynamicSQL` work, but the result's `affectedRows` is always 0 and its `insertedID` is nil.
`DynamicSQL` with `allowsMultipleStatements: true` throws (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 `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.

A test target is included. Run it with:
## Testing

```
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.

## Migrating from 4.x

5.0.0 is a major release. The [release notes](../../releases/tag/5.0.0) 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](https://github.com/PerfectlySoft/Perfect-CRUD#migrating-from-2x).
* **`Sendable`.** Closures passed to `PGConnection.setReceiver` and `setProcessor` must be `@Sendable`.
* **Shared connections are serialized.** `PGConnection` calls from different threads now wait for each other, and
`doWithTransaction` blocks 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 `.dropTable` drops every table in the
set before creating any. `.reconcileTable` now 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 as `host: "localhost port=5433"`), pass the plain value and use the matching parameter or a connection
string with `PostgresDatabaseConfiguration(_:)` instead.
* **Logging.** The connector's one diagnostic of its own (an unhandled result status) goes through
[swift-log](https://github.com/apple/swift-log) (label `perfect.postgresql`) instead of `print`. Errors and queries
logged by the CRUD layer still go through Perfect-CRUD's `CRUDLogging`.

## License

Apache 2.0 — see [LICENSE](LICENSE).
Apache 2.0. See [LICENSE](LICENSE).
26 changes: 21 additions & 5 deletions Sources/PerfectPostgreSQL/PostgresCRUD.swift
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ class PostgresExeDelegate: SQLExeDelegate, @unchecked Sendable {
}
}

public struct PostgresDatabaseConfiguration: DatabaseConfigurationProtocol {
public struct PostgresDatabaseConfiguration: DatabaseConfigurationProtocol, Sendable {
let connection: PGConnection

public init(url: String?,
Expand All @@ -606,17 +606,33 @@ public struct PostgresDatabaseConfiguration: DatabaseConfigurationProtocol {
}

public init(database: String, host: String, port: Int? = nil, username: String? = nil, password: String? = nil) throws {
var s = "host=\(host) dbname=\(database)"
try self.init(Self.connectionInfo(database: database, host: host, port: port, username: username, password: password))
}
// Each value is single-quoted, with `\` and `'` backslash-escaped, as libpq's conninfo
// syntax requires. Unquoted, a password containing a space failed to parse, and a value
// like "x sslmode=disable" added a connection option of its own.
static func connectionInfo(database: String, host: String, port: Int?, username: String?, password: String?) -> String {
func quoted(_ value: String) -> String {
var escaped = String.UnicodeScalarView()
for scalar in value.unicodeScalars {
if scalar == "\\" || scalar == "'" {
escaped.append("\\")
}
escaped.append(scalar)
}
return "'\(String(escaped))'"
}
var s = "host=\(quoted(host)) dbname=\(quoted(database))"
if let p = port {
s += " port=\(p)"
}
if let u = username {
s += " user=\(u)"
s += " user=\(quoted(u))"
}
if let p = password {
s += " password=\(p)"
s += " password=\(quoted(p))"
}
try self.init(s)
return s
}
public init(_ connectionInfo: String) throws {
let con = PGConnection()
Expand Down
Loading
Loading