From 14e10b35dfec7df73c3b6c15dedc6b511f499ea6 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 23 Oct 2025 08:02:27 -0500
Subject: [PATCH 1/5] Bump Swift minimum to 6.0, update CI, convert tests to
SwiftTesting
---
.github/workflows/api-docs.yml | 3 +
.github/workflows/test.yml | 86 ++--
Package.swift | 12 +-
README.md | 4 +-
.../Docs.docc/theme-settings.json | 6 +-
.../PostgresError+Database.swift | 5 -
.../FluentPostgresDriverTests.swift | 392 ++++++++++--------
...luentPostgresTransactionControlTests.swift | 111 ++---
8 files changed, 314 insertions(+), 305 deletions(-)
diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml
index c6086d5..829c389 100644
--- a/.github/workflows/api-docs.yml
+++ b/.github/workflows/api-docs.yml
@@ -3,6 +3,9 @@ on:
push:
branches:
- main
+permissions:
+ contents: read
+ id-token: write
jobs:
build-and-deploy:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cd903c4..e2f5a40 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,22 +5,22 @@ concurrency:
on:
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
push: { branches: [ main ] }
-
+permissions:
+ contents: read
env:
LOG_LEVEL: info
- SWIFT_DETERMINISTIC_HASHING: 1
- POSTGRES_HOSTNAME: 'psql-a'
- POSTGRES_HOSTNAME_A: 'psql-a'
- POSTGRES_HOSTNAME_B: 'psql-b'
- POSTGRES_DB: 'test_database_a'
- POSTGRES_DB_A: 'test_database_a'
- POSTGRES_DB_B: 'test_database_b'
- POSTGRES_USER: 'test_username'
- POSTGRES_USER_A: 'test_username'
- POSTGRES_USER_B: 'test_username'
- POSTGRES_PASSWORD: 'test_password'
- POSTGRES_PASSWORD_A: 'test_password'
- POSTGRES_PASSWORD_B: 'test_password'
+ POSTGRES_HOSTNAME_A: &postgres_host_a 'psql-a'
+ POSTGRES_HOSTNAME_B: &postgres_host_b 'psql-b'
+ POSTGRES_HOSTNAME: *postgres_host_a
+ POSTGRES_DB_A: &postgres_db_a 'test_database_a'
+ POSTGRES_DB_B: &postgres_db_b 'test_database_b'
+ POSTGRES_DB: *postgres_db_a
+ POSTGRES_USER_A: &postgres_user_a 'test_username'
+ POSTGRES_USER_B: &postgres_user_b 'test_username'
+ POSTGRES_USER: *postgres_user_a
+ POSTGRES_PASSWORD_A: &postgres_pass_a 'test_password'
+ POSTGRES_PASSWORD_B: &postgres_pass_b 'test_password'
+ POSTGRES_PASSWORD: *postgres_pass_a
jobs:
api-breakage:
@@ -42,35 +42,35 @@ jobs:
fail-fast: false
matrix:
include:
- - postgres-image-a: 'postgres:12'
- postgres-image-b: 'postgres:13'
+ - postgres-image-a: 'postgres:13'
+ postgres-image-b: 'postgres:14'
postgres-auth: 'trust'
- swift-image: 'swift:5.10-jammy'
- - postgres-image-a: 'postgres:14'
- postgres-image-b: 'postgres:15'
- postgres-auth: 'md5'
swift-image: 'swift:6.0-noble'
- - postgres-image-a: 'postgres:16'
- postgres-image-b: 'postgres:17'
- postgres-auth: 'scram-sha-256'
+ - postgres-image-a: 'postgres:15'
+ postgres-image-b: 'postgres:16'
+ postgres-auth: 'md5'
swift-image: 'swift:6.1-noble'
+ - postgres-image-a: 'postgres:17'
+ postgres-image-b: 'postgres:18'
+ postgres-auth: 'scram-sha-256'
+ swift-image: 'swift:6.2-noble'
container: ${{ matrix.swift-image }}
runs-on: ubuntu-latest
services:
- psql-a:
+ *postgres_host_a:
image: ${{ matrix.postgres-image-a }}
env:
- POSTGRES_USER: 'test_username'
- POSTGRES_DB: 'test_database_a'
- POSTGRES_PASSWORD: 'test_password'
+ POSTGRES_USER: *postgres_user_a
+ POSTGRES_DB: *postgres_db_a
+ POSTGRES_PASSWORD: *postgres_pass_a
POSTGRES_HOST_AUTH_METHOD: ${{ matrix.postgres-auth }}
POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }}
- psql-b:
+ *postgres_host_b:
image: ${{ matrix.postgres-image-b }}
env:
- POSTGRES_USER: 'test_username'
- POSTGRES_DB: 'test_database_b'
- POSTGRES_PASSWORD: 'test_password'
+ POSTGRES_USER: *postgres_user_b
+ POSTGRES_DB: *postgres_db_b
+ POSTGRES_PASSWORD: *postgres_pass_b
POSTGRES_HOST_AUTH_METHOD: ${{ matrix.postgres-auth }}
POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }}
steps:
@@ -79,7 +79,7 @@ jobs:
- name: Check out package
uses: actions/checkout@v5
- name: Run all tests
- run: swift test --enable-code-coverage
+ run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable
- name: Submit coverage report to Codecov.io
uses: vapor/swift-codecov-action@v0.3
with:
@@ -93,6 +93,8 @@ jobs:
include:
- macos-version: macos-15
xcode-version: latest-stable
+ - macos-version: macos-26
+ xcode-version: latest-stable
runs-on: ${{ matrix.macos-version }}
env:
LOG_LEVEL: debug
@@ -108,7 +110,7 @@ jobs:
run: |
brew upgrade || true
export PATH="$(brew --prefix)/opt/postgresql@16/bin:$PATH" PGDATA=/tmp/vapor-postgres-test PGUSER="${POSTGRES_USER_A}"
- brew install postgresql@17 && brew link --force postgresql@17
+ brew install postgresql@18 && brew link --force postgresql@18
initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER_A}" --pwfile=<(echo "${POSTGRES_PASSWORD_A}")
pg_ctl start --wait
PGPASSWORD="${POSTGRES_PASSWORD_A}" createdb -w -O "${POSTGRES_USER_A}" "${POSTGRES_DB_A}"
@@ -119,4 +121,20 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
- name: Run all tests
- run: swift test
+ run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable
+ - name: Submit coverage report to Codecov.io
+ uses: vapor/swift-codecov-action@v0.3
+ with:
+ codecov_token: ${{ secrets.CODECOV_TOKEN }}
+
+ musl:
+ runs-on: ubuntu-latest
+ container: swift:6.2-noble
+ timeout-minutes: 30
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v5
+ - name: Install SDK
+ run: swift sdk install https://download.swift.org/swift-6.2-release/static-sdk/swift-6.2-RELEASE/swift-6.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum d2225840e592389ca517bbf71652f7003dbf45ac35d1e57d98b9250368769378
+ - name: Build
+ run: swift build --swift-sdk x86_64-swift-linux-musl
diff --git a/Package.swift b/Package.swift
index 0ea5ef3..93320b0 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.10
+// swift-tools-version:6.0
import PackageDescription
let package = Package(
@@ -14,7 +14,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.52.2"),
- .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.14.0"),
+ .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.14.1"),
.package(url: "https://github.com/vapor/async-kit.git", from: "1.21.0"),
],
targets: [
@@ -41,9 +41,9 @@ let package = Package(
var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
+ .enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
- .enableUpcomingFeature("ConciseMagicFile"),
- .enableUpcomingFeature("ForwardTrailingClosures"),
- .enableUpcomingFeature("DisableOutwardActorInference"),
- .enableExperimentalFeature("StrictConcurrency=complete"),
+ .enableUpcomingFeature("InferIsolatedConformances"),
+ //.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
+ .enableUpcomingFeature("ImmutableWeakCaptures"),
] }
diff --git a/README.md b/README.md
index 632b114..08a2ba1 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,8 @@
-
-
+
+
diff --git a/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json b/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json
index 4ce4f1c..c0625a0 100644
--- a/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json
+++ b/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json
@@ -1,6 +1,6 @@
{
"theme": {
- "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
+ "aside": { "border-radius": "16px", "border-width": "3px", "border-style": "double" },
"border-radius": "0",
"button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
"code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
@@ -8,9 +8,9 @@
"fluentpsqldriver": "#336791",
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentpsqldriver) 30%, #000 100%)",
"documentation-intro-accent": "var(--color-fluentpsqldriver)",
- "documentation-intro-eyebrow": "white",
+ "hero-eyebrow": "white",
"documentation-intro-figure": "white",
- "documentation-intro-title": "white",
+ "hero-title": "white",
"logo-base": { "dark": "#fff", "light": "#000" },
"logo-shape": { "dark": "#000", "light": "#fff" },
"fill": { "dark": "#000", "light": "#fff" }
diff --git a/Sources/FluentPostgresDriver/PostgresError+Database.swift b/Sources/FluentPostgresDriver/PostgresError+Database.swift
index cd6865e..595df63 100644
--- a/Sources/FluentPostgresDriver/PostgresError+Database.swift
+++ b/Sources/FluentPostgresDriver/PostgresError+Database.swift
@@ -107,10 +107,5 @@ extension PSQLError {
}
}
-#if compiler(<6)
-extension PostgresError: DatabaseError {}
-extension PSQLError: DatabaseError {}
-#else
extension PostgresError: @retroactive DatabaseError {}
extension PSQLError: @retroactive DatabaseError {}
-#endif
diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
index a45ebc7..ff6b07c 100644
--- a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
+++ b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
@@ -5,37 +5,60 @@ import FluentSQL
import Logging
import PostgresKit
import SQLKit
+import Testing
import XCTest
-func XCTAssertThrowsErrorAsync(
- _ expression: @autoclosure () async throws -> T,
- _ message: @autoclosure () -> String = "",
- file: StaticString = #filePath,
- line: UInt = #line,
- _ callback: (any Error) -> Void = { _ in }
-) async {
+func withDbs(_ closure: @escaping @Sendable (_ dbs: Databases, _ db: any Database) async throws -> Void) async throws {
+ let databases = Databases(threadPool: .singleton, on: MultiThreadedEventLoopGroup.singleton)
+
+ databases.use(.testPostgres(subconfig: "A"), as: .a)
+ databases.use(.testPostgres(subconfig: "B"), as: .b)
+
do {
- _ = try await expression()
- XCTAssertThrowsError({}(), message(), file: file, line: line, callback)
+ let a = databases.database(.a, logger: .init(label: "test.fluent.a"), on: databases.eventLoopGroup.any())!
+ _ = try await (a as! any SQLDatabase).raw("drop schema if exists public cascade").run()
+ _ = try await (a as! any SQLDatabase).raw("create schema public").run()
+
+ let b = databases.database(.b, logger: .init(label: "test.fluent.b"), on: databases.eventLoopGroup.any())!
+ _ = try await (b as! any SQLDatabase).raw("drop schema if exists public cascade").run()
+ _ = try await (b as! any SQLDatabase).raw("create schema public").run()
+
+ try await closure(databases, a)
+ await databases.shutdownAsync()
} catch {
- XCTAssertThrowsError(try { throw error }(), message(), file: file, line: line, callback)
+ print(String(reflecting: error))
+ await databases.shutdownAsync()
+ throw error
}
}
-func XCTAssertNoThrowAsync(
- _ expression: @autoclosure () async throws -> T,
- _ message: @autoclosure () -> String = "",
- file: StaticString = #filePath,
- line: UInt = #line
-) async {
- do {
- _ = try await expression()
- } catch {
- XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line)
+final class FluentBenchmarksTests: XCTestCase {
+ var benchmarker: FluentBenchmarker { .init(databases: self.dbs) }
+ var dbs: Databases!
+
+ override func setUp() async throws {
+ try await super.setUp()
+
+ XCTAssert(isLoggingConfigured)
+ self.dbs = Databases(threadPool: .singleton, on: MultiThreadedEventLoopGroup.singleton)
+
+ self.dbs.use(.testPostgres(subconfig: "A"), as: .a)
+ self.dbs.use(.testPostgres(subconfig: "B"), as: .b)
+
+ let a = self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.dbs.eventLoopGroup.any())
+ _ = try await (a as! any PostgresDatabase).query("drop schema public cascade").get()
+ _ = try await (a as! any PostgresDatabase).query("create schema public").get()
+
+ let b = self.dbs.database(.b, logger: .init(label: "test.fluent.b"), on: self.dbs.eventLoopGroup.any())
+ _ = try await (b as! any PostgresDatabase).query("drop schema public cascade").get()
+ _ = try await (b as! any PostgresDatabase).query("create schema public").get()
+ }
+
+ override func tearDown() async throws {
+ await self.dbs.shutdownAsync()
+ try await super.tearDown()
}
-}
-final class FluentPostgresDriverTests: XCTestCase {
func testAggregate() throws { try self.benchmarker.testAggregate() }
func testArray() throws { try self.benchmarker.testArray() }
func testBatch() throws { try self.benchmarker.testBatch() }
@@ -68,23 +91,35 @@ final class FluentPostgresDriverTests: XCTestCase {
func testTimestamp() throws { try self.benchmarker.testTimestamp() }
func testTransaction() throws { try self.benchmarker.testTransaction() }
func testUnique() throws { try self.benchmarker.testUnique() }
+}
- func testDatabaseError() async throws {
- let sql1 = (self.db as! any SQLDatabase)
- await XCTAssertThrowsErrorAsync(try await sql1.raw("asdf").run()) {
- XCTAssertTrue(($0 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: $0))")
- XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))")
- XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))")
- }
+@Suite(.serialized)
+struct AllSuites {}
- let sql2 = (self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.eventLoopGroup.any())!) as! any SQLDatabase
- try await sql2.drop(table: "foo").ifExists().run()
- try await sql2.create(table: "foo").column("name", type: .text, .unique).run()
- try await sql2.insert(into: "foo").columns("name").values("bar").run()
- await XCTAssertThrowsErrorAsync(try await sql2.insert(into: "foo").columns("name").values("bar").run()) {
- XCTAssertTrue(($0 as? any DatabaseError)?.isConstraintFailure ?? false, "\(String(reflecting: $0))")
- XCTAssertFalse(($0 as? any DatabaseError)?.isSyntaxError ?? true, "\(String(reflecting: $0))")
- XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))")
+extension AllSuites {
+@Suite
+struct FluentPostgresDriverTests {
+ init() {
+ #expect(isLoggingConfigured)
+ }
+
+ @Test
+ func databaseError() async throws {
+ try await withDbs { dbs, db in
+ let sql1 = (db as! any SQLDatabase)
+ let error = await #expect(throws: (any Error).self) { try await sql1.raw("asdf").run() }
+ #expect((error as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: error))")
+ #expect(!((error as? any DatabaseError)?.isConstraintFailure ?? true), "\(String(reflecting: error))")
+ #expect(!((error as? any DatabaseError)?.isConnectionClosed ?? true), "\(String(reflecting: error))")
+
+ let sql2 = (dbs.database(.a, logger: .init(label: "test.fluent.a"), on: dbs.eventLoopGroup.any())!) as! any SQLDatabase
+ try await sql2.drop(table: "foo").ifExists().run()
+ try await sql2.create(table: "foo").column("name", type: .text, .unique).run()
+ try await sql2.insert(into: "foo").columns("name").values("bar").run()
+ await #expect(throws: (any Error).self) { try await sql2.insert(into: "foo").columns("name").values("bar").run() }
+ #expect((error as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: error))")
+ #expect(!((error as? any DatabaseError)?.isConstraintFailure ?? true), "\(String(reflecting: error))")
+ #expect(!((error as? any DatabaseError)?.isConnectionClosed ?? true), "\(String(reflecting: error))")
}
// Disabled until we figure out why it hangs instead of throwing an error.
@@ -100,7 +135,8 @@ final class FluentPostgresDriverTests: XCTestCase {
//}
}
- func testBlob() async throws {
+ @Test
+ func blob() async throws {
struct CreateFoo: AsyncMigration {
func prepare(on database: any Database) async throws {
try await database.schema("foos")
@@ -114,11 +150,14 @@ final class FluentPostgresDriverTests: XCTestCase {
}
}
- try await CreateFoo().prepare(on: self.db)
- try await CreateFoo().revert(on: self.db)
+ try await withDbs { _, db in
+ try await CreateFoo().prepare(on: db)
+ try await CreateFoo().revert(on: db)
+ }
}
- func testSaveModelWithBool() async throws {
+ @Test
+ func saveModelWithBool() async throws {
final class Organization: Model, @unchecked Sendable {
static let schema = "orgs"
@@ -141,84 +180,93 @@ final class FluentPostgresDriverTests: XCTestCase {
}
}
- try await CreateOrganization().prepare(on: self.db)
- do {
- let new = Organization()
- new.disabled = false
- try await new.save(on: self.db)
- } catch {
- try? await CreateOrganization().revert(on: self.db)
- throw error
+ try await withDbs { _, db in
+ try await CreateOrganization().prepare(on: db)
+ do {
+ let new = Organization()
+ new.disabled = false
+ try await new.save(on: db)
+ } catch {
+ try? await CreateOrganization().revert(on: db)
+ throw error
+ }
+ try await CreateOrganization().revert(on: db)
}
- try await CreateOrganization().revert(on: self.db)
}
- func testCustomJSON() async throws {
- let jsonEncoder = JSONEncoder()
- jsonEncoder.dateEncodingStrategy = .iso8601
- let jsonDecoder = JSONDecoder()
- jsonDecoder.dateDecodingStrategy = .iso8601
-
- self.dbs.use(
- .testPostgres(
- subconfig: "A",
- encodingContext: .init(jsonEncoder: jsonEncoder),
- decodingContext: .init(jsonDecoder: jsonDecoder)
- ),
- as: .iso8601
- )
- let db = self.dbs.database(
- .iso8601,
- logger: .init(label: "test"),
- on: self.eventLoopGroup.any()
- )!
-
- try await EventMigration().prepare(on: db)
- do {
- let date = Date()
- let event = Event()
- event.id = 1
- event.metadata = Metadata(createdAt: date)
- try await event.save(on: db)
-
- let rows = try await EventStringlyTyped.query(on: db).filter(\.$id == 1).all()
- let expected = ISO8601DateFormatter().string(from: date)
- XCTAssertEqual(rows[0].metadata["createdAt"], expected)
- } catch {
- try? await EventMigration().revert(on: db)
- throw error
+ @Test
+ func customJSON() async throws {
+ try await withDbs { dbs, _ in
+ let jsonEncoder = JSONEncoder()
+ jsonEncoder.dateEncodingStrategy = .iso8601
+ let jsonDecoder = JSONDecoder()
+ jsonDecoder.dateDecodingStrategy = .iso8601
+
+ dbs.use(
+ .testPostgres(
+ subconfig: "A",
+ encodingContext: .init(jsonEncoder: jsonEncoder),
+ decodingContext: .init(jsonDecoder: jsonDecoder)
+ ),
+ as: .iso8601
+ )
+ let db = dbs.database(
+ .iso8601,
+ logger: .init(label: "test"),
+ on: dbs.eventLoopGroup.any()
+ )!
+
+ try await EventMigration().prepare(on: db)
+ do {
+ let date = Date()
+ let event = Event()
+ event.id = 1
+ event.metadata = Metadata(createdAt: date)
+ try await event.save(on: db)
+
+ let rows = try await EventStringlyTyped.query(on: db).filter(\.$id == 1).all()
+ let expected = ISO8601DateFormatter().string(from: date)
+ #expect(rows[0].metadata["createdAt"] == expected)
+ } catch {
+ try? await EventMigration().revert(on: db)
+ throw error
+ }
+ try await EventMigration().revert(on: db)
}
- try await EventMigration().revert(on: db)
}
- func testEnumAddingMultipleCases() async throws {
- try await EnumMigration().prepare(on: self.db)
- do {
- try await EventWithFooMigration().prepare(on: self.db)
+ @Test
+ func enumAddingMultipleCases() async throws {
+ try await withDbs { _, db in
+ try await EnumMigration().prepare(on: db)
do {
- let event = EventWithFoo()
- event.foobar = .foo
- try await event.save(on: self.db)
-
- await XCTAssertNoThrowAsync(try await EnumAddMultipleCasesMigration().prepare(on: self.db))
-
- event.foobar = .baz
- await XCTAssertNoThrowAsync(try await event.update(on: self.db))
- event.foobar = .qux
- await XCTAssertNoThrowAsync(try await event.update(on: self.db))
-
- await XCTAssertNoThrowAsync(try await EnumAddMultipleCasesMigration().revert(on: self.db))
+ try await EventWithFooMigration().prepare(on: db)
+ do {
+ let event = EventWithFoo()
+ event.foobar = .foo
+ try await event.save(on: db)
+
+ await #expect(throws: Never.self) { try await EnumAddMultipleCasesMigration().prepare(on: db) }
+
+ event.foobar = .baz
+ await #expect(throws: Never.self) { try await event.update(on: db) }
+ event.foobar = .qux
+ await #expect(throws: Never.self) { try await event.update(on: db) }
+
+ await #expect(throws: Never.self) { try await EnumAddMultipleCasesMigration().revert(on: db) }
+ } catch {
+ try? await EventWithFooMigration().revert(on: db)
+ throw error
+ }
} catch {
- try? await EventWithFooMigration().revert(on: self.db)
+ try? await EnumMigration().revert(on: db)
throw error
}
- } catch {
- try? await EnumMigration().revert(on: self.db)
- throw error
}
}
- func testEncodingArrayOfModels() async throws {
+ @Test
+ func encodingArrayOfModels() async throws {
final class Elem: Model, ExpressibleByIntegerLiteral, @unchecked Sendable {
static let schema = ""
@ID(custom: .id) var id: Int?
@@ -233,65 +281,37 @@ final class FluentPostgresDriverTests: XCTestCase {
init(nilLiteral: ()) { self.list = nil }
init(arrayLiteral el: Elem...) { self.list = el }
}
- do {
- try await self.db.schema(Seq.schema).field(.id, .int, .identifier(auto: true)).field("list", .sql(embed: "JSONB[]")).create()
-
- let s1: Seq = [1, 2]
- let s2: Seq = nil
- try [s1, s2].forEach { try $0.create(on: self.db).wait() }
-
- // Make sure it went into the DB as "array of jsonb" rather than as "array of one jsonb containing array" or such.
- let raws = try await (self.db as! any SQLDatabase).raw("SELECT array_to_json(list)::text t FROM seqs").all().map {
- try $0.decode(column: "t", as: String?.self)
+ try await withDbs { _, db in
+ do {
+ try await db.schema(Seq.schema).field(.id, .int, .identifier(auto: true)).field("list", .sql(embed: "JSONB[]")).create()
+
+ let s1: Seq = [1, 2]
+ let s2: Seq = nil
+ try await s1.create(on: db)
+ try await s2.create(on: db)
+
+ // Make sure it went into the DB as "array of jsonb" rather than as "array of one jsonb containing array" or such.
+ let raws = try await (db as! any SQLDatabase).raw("SELECT array_to_json(list)::text t FROM seqs").all().map {
+ try $0.decode(column: "t", as: String?.self)
+ }
+ #expect(raws == [#"[{"id": 1},{"id": 2}]"#, nil])
+
+ // Make sure it round-trips through Fluent.
+ let seqs = try await Seq.query(on: db).all()
+
+ #expect(seqs.count == 2)
+ #expect(seqs.dropFirst(0).first?.id == s1.id)
+ #expect(seqs.dropFirst(0).first?.list?.map(\.id) == s1.list?.map(\.id))
+ #expect(seqs.dropFirst(1).first?.id == s2.id)
+ #expect(seqs.dropFirst(1).first?.list?.map(\.id) == s2.list?.map(\.id))
+ } catch let error {
+ Issue.record("caught error: \(String(reflecting: error))")
}
- XCTAssertEqual(raws, [#"[{"id": 1},{"id": 2}]"#, nil])
-
- // Make sure it round-trips through Fluent.
- let seqs = try await Seq.query(on: self.db).all()
-
- XCTAssertEqual(seqs.count, 2)
- XCTAssertEqual(seqs.dropFirst(0).first?.id, s1.id)
- XCTAssertEqual(seqs.dropFirst(0).first?.list?.map(\.id), s1.list?.map(\.id))
- XCTAssertEqual(seqs.dropFirst(1).first?.id, s2.id)
- XCTAssertEqual(seqs.dropFirst(1).first?.list?.map(\.id), s2.list?.map(\.id))
- } catch let error {
- XCTFail("caught error: \(String(reflecting: error))")
+ try await db.schema(Seq.schema).delete()
}
- try await db.schema(Seq.schema).delete()
- }
-
- var benchmarker: FluentBenchmarker { .init(databases: self.dbs) }
- var eventLoopGroup: any EventLoopGroup { MultiThreadedEventLoopGroup.singleton }
- var threadPool: NIOThreadPool { NIOThreadPool.singleton }
- var dbs: Databases!
- var db: (any Database)!
- var postgres: any PostgresDatabase { self.db as! any PostgresDatabase }
-
- override func setUp() async throws {
- try await super.setUp()
-
- XCTAssert(isLoggingConfigured)
- self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup)
-
- self.dbs.use(.testPostgres(subconfig: "A"), as: .a)
- self.dbs.use(.testPostgres(subconfig: "B"), as: .b)
-
- let a = self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.eventLoopGroup.any())
- _ = try await (a as! any PostgresDatabase).query("drop schema public cascade").get()
- _ = try await (a as! any PostgresDatabase).query("create schema public").get()
-
- let b = self.dbs.database(.b, logger: .init(label: "test.fluent.b"), on: self.eventLoopGroup.any())
- _ = try await (b as! any PostgresDatabase).query("drop schema public cascade").get()
- _ = try await (b as! any PostgresDatabase).query("create schema public").get()
-
- self.db = a
- }
-
- override func tearDown() async throws {
- await self.dbs.shutdownAsync()
- try await super.tearDown()
}
}
+}
extension DatabaseConfigurationFactory {
static func testPostgres(
@@ -300,11 +320,11 @@ extension DatabaseConfigurationFactory {
decodingContext: PostgresDecodingContext = .default
) -> Self {
let baseSubconfig = SQLPostgresConfiguration(
- hostname: env("POSTGRES_HOSTNAME_\(subconfig)") ?? "localhost",
- port: env("POSTGRES_PORT_\(subconfig)").flatMap(Int.init) ?? SQLPostgresConfiguration.ianaPortNumber,
- username: env("POSTGRES_USER_\(subconfig)") ?? "test_username",
- password: env("POSTGRES_PASSWORD_\(subconfig)") ?? "test_password",
- database: env("POSTGRES_DB_\(subconfig)") ?? "test_database",
+ hostname: ProcessInfo.processInfo.environment["POSTGRES_HOSTNAME_\(subconfig)"] ?? "localhost",
+ port: ProcessInfo.processInfo.environment["POSTGRES_PORT_\(subconfig)"].flatMap(Int.init) ?? SQLPostgresConfiguration.ianaPortNumber,
+ username: ProcessInfo.processInfo.environment["POSTGRES_USER_\(subconfig)"] ?? "test_username",
+ password: ProcessInfo.processInfo.environment["POSTGRES_PASSWORD_\(subconfig)"] ?? "test_password",
+ database: ProcessInfo.processInfo.environment["POSTGRES_DB_\(subconfig)"] ?? "test_database",
tls: try! .prefer(.init(configuration: .makeClientConfiguration()))
)
@@ -423,15 +443,37 @@ struct EnumAddMultipleCasesMigration: AsyncMigration {
}
}
-func env(_ name: String) -> String? {
- ProcessInfo.processInfo.environment[name]
-}
-
let isLoggingConfigured: Bool = {
- LoggingSystem.bootstrap { label in
- var handler = StreamLogHandler.standardOutput(label: label)
- handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info
- return handler
- }
+ LoggingSystem.bootstrap { QuickLogHandler(label: $0, level: ProcessInfo.processInfo.environment["LOG_LEVEL"].flatMap { .init(rawValue: $0) } ?? .info) }
return true
}()
+
+struct QuickLogHandler: LogHandler {
+ private let label: String
+ var logLevel = Logger.Level.info, metadataProvider = LoggingSystem.metadataProvider, metadata = Logger.Metadata()
+ subscript(metadataKey key: String) -> Logger.Metadata.Value? { get { self.metadata[key] } set { self.metadata[key] = newValue } }
+ init(label: String, level: Logger.Level) { (self.label, self.logLevel) = (label, level) }
+ func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) {
+ print("\(self.timestamp()) \(level) \(self.label) :\(self.prettify(metadata ?? [:]).map { " \($0)" } ?? "") [\(source)] \(message)")
+ }
+ private func prettify(_ metadata: Logger.Metadata) -> String? {
+ self.metadata.merging(self.metadataProvider?.get() ?? [:]) { $1 }.merging(metadata) { $1 }.sorted { $0.0 < $1.0 }.map { "\($0)=\($1.mvDesc)" }.joined(separator: " ")
+ }
+ private func timestamp() -> String { .init(unsafeUninitializedCapacity: 255) { buffer in
+ var timestamp = time(nil)
+ return localtime(×tamp).map { strftime(buffer.baseAddress!, buffer.count, "%Y-%m-%dT%H:%M:%S%z", $0) } ?? buffer.initialize(fromContentsOf: "".utf8)
+ } }
+}
+extension Logger.MetadataValue {
+ var mvDesc: String { switch self {
+ case .dictionary(let dict): "[\(dict.mapValues(\.mvDesc).lazy.sorted { $0.0 < $1.0 }.map { "\($0): \($1)" }.joined(separator: ", "))]"
+ case .array(let list): "[\(list.map(\.mvDesc).joined(separator: ", "))]"
+ case .string(let str): #""\#(str)""#
+ case .stringConvertible(let repr): switch repr {
+ case let repr as Bool: "\(repr)"
+ case let repr as any FixedWidthInteger: "\(repr)"
+ case let repr as any BinaryFloatingPoint: "\(repr)"
+ default: #""\#(String(describing: repr))""#
+ }
+ } }
+}
diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift
index 4bdc52f..fac35b6 100644
--- a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift
+++ b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift
@@ -1,92 +1,43 @@
-import FluentBenchmark
import FluentKit
-import FluentPostgresDriver
-import Logging
-import PostgresKit
-import XCTest
-
-final class FluentPostgresTransactionControlTests: XCTestCase {
- func testRollback() async throws {
- do {
- try await self.db.withConnection { db -> EventLoopFuture in
- (db as! any TransactionControlDatabase).beginTransaction().flatMap { () -> EventLoopFuture in
- let todo1 = Todo(title: "Test")
- return todo1.save(on: db)
- }.flatMap { () -> EventLoopFuture in
- let duplicate = Todo(title: "Test")
- return duplicate.create(on: db)
- .flatMap {
- (db as! any TransactionControlDatabase).commitTransaction()
- }.flatMapError { (e: Error) -> EventLoopFuture in
- return (db as! any TransactionControlDatabase).rollbackTransaction()
- .flatMap { db.eventLoop.makeFailedFuture(e) }
- }
+import Testing
+
+extension AllSuites {
+@Suite
+struct FluentPostgresTransactionControlTests {
+ init() { #expect(isLoggingConfigured) }
+
+ @Test
+ func rollback() async throws {
+ try await withDbs { _, db in try await db.withConnection { db in
+ try await CreateTodo().prepare(on: db)
+ do {
+ try await (db as! any TransactionControlDatabase).beginTransaction().get()
+ let error = await #expect(throws: (any Error).self) {
+ try await [Todo(title: "Test"), Todo(title: "Test")].create(on: db)
+ try await (db as! any TransactionControlDatabase).commitTransaction().get()
}
- }.get()
- XCTFail("Expected error but none was thrown")
- } catch let error where String(reflecting: error).contains("sqlState: 23505") {
- // ignore
- } catch {
- XCTFail("Expected SQL state 23505 but got \(String(reflecting: error))")
- }
-
- let count2 = try await Todo.query(on: self.db).count()
- XCTAssertEqual(count2, 0)
- }
-
- var eventLoopGroup: any EventLoopGroup { MultiThreadedEventLoopGroup.singleton }
- var threadPool: NIOThreadPool { NIOThreadPool.singleton }
- var dbs: Databases!
- var db: (any Database)!
-
- override func setUp() async throws {
- try await super.setUp()
-
- XCTAssert(isLoggingConfigured)
- self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup)
-
- self.dbs.use(.testPostgres(subconfig: "A"), as: .a)
-
- self.db = self.dbs.database(.a, logger: Logger(label: "test.fluent.a"), on: self.eventLoopGroup.any())
- _ = try await (self.db as! any PostgresDatabase).query("drop schema public cascade").get()
- _ = try await (self.db as! any PostgresDatabase).query("create schema public").get()
-
- try await CreateTodo().prepare(on: self.db)
- }
-
- override func tearDown() async throws {
- try await CreateTodo().revert(on: self.db)
- await self.dbs.shutdownAsync()
- try await super.tearDown()
+ #expect(String(reflecting: error).contains("sqlState: 23505"), "\(String(reflecting: error))")
+ try await (db as! any TransactionControlDatabase).rollbackTransaction().get()
+ #expect(try await Todo.query(on: db).count() == 0)
+ } catch {
+ try? await CreateTodo().revert(on: db)
+ throw error
+ }
+ try await CreateTodo().revert(on: db)
+ } }
}
final class Todo: Model, @unchecked Sendable {
static let schema = "todos"
-
- @ID
- var id: UUID?
-
- @Field(key: "title")
- var title: String
-
+ @ID var id
+ @Field(key: "title") var title: String
init() {}
- init(title: String) {
- self.title = title
- id = nil
- }
+ init(title: String) { self.title = title }
}
struct CreateTodo: AsyncMigration {
- func prepare(on database: any Database) async throws {
- try await database.schema("todos")
- .id()
- .field("title", .string, .required)
- .unique(on: "title")
- .create()
- }
-
- func revert(on database: any Database) async throws {
- try await database.schema("todos").delete()
- }
+ func prepare(on database: any Database) async throws { try await database.schema(Todo.schema).id().field("title", .string, .required).unique(on: "title").create() }
+ func revert(on database: any Database) async throws { try await database.schema(Todo.schema).delete() }
}
}
+}
From 39b36329bb639063528cb8aecba0ea73798d7f00 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Fri, 24 Oct 2025 03:12:21 -0500
Subject: [PATCH 2/5] For some reason only 6.0 complains about non-public
imports
---
Package.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Package.swift b/Package.swift
index 93320b0..b211c69 100644
--- a/Package.swift
+++ b/Package.swift
@@ -41,7 +41,7 @@ let package = Package(
var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
- .enableUpcomingFeature("InternalImportsByDefault"),
+ //.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("InferIsolatedConformances"),
//.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
From 22e581c06c5b5c0316d5650e68463de155705434 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Mon, 27 Oct 2025 12:21:00 -0500
Subject: [PATCH 3/5] Fix tests in 6.0
---
.../FluentPostgresDriverTests.swift | 20 ++++++++++---------
...luentPostgresTransactionControlTests.swift | 4 +++-
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
index ff6b07c..0af223c 100644
--- a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
+++ b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
@@ -102,24 +102,25 @@ struct FluentPostgresDriverTests {
init() {
#expect(isLoggingConfigured)
}
-
+
+ #if !compiler(<6.1) // #expect(throws:) doesn't return the Error until 6.1
@Test
func databaseError() async throws {
try await withDbs { dbs, db in
let sql1 = (db as! any SQLDatabase)
- let error = await #expect(throws: (any Error).self) { try await sql1.raw("asdf").run() }
- #expect((error as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: error))")
- #expect(!((error as? any DatabaseError)?.isConstraintFailure ?? true), "\(String(reflecting: error))")
- #expect(!((error as? any DatabaseError)?.isConnectionClosed ?? true), "\(String(reflecting: error))")
+ let error1 = await #expect(throws: (any Error).self) { try await sql1.raw("asdf").run() }
+ #expect((error1 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: error1))")
+ #expect(!((error1 as? any DatabaseError)?.isConstraintFailure ?? true), "\(String(reflecting: error1))")
+ #expect(!((error1 as? any DatabaseError)?.isConnectionClosed ?? true), "\(String(reflecting: error1))")
let sql2 = (dbs.database(.a, logger: .init(label: "test.fluent.a"), on: dbs.eventLoopGroup.any())!) as! any SQLDatabase
try await sql2.drop(table: "foo").ifExists().run()
try await sql2.create(table: "foo").column("name", type: .text, .unique).run()
try await sql2.insert(into: "foo").columns("name").values("bar").run()
- await #expect(throws: (any Error).self) { try await sql2.insert(into: "foo").columns("name").values("bar").run() }
- #expect((error as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: error))")
- #expect(!((error as? any DatabaseError)?.isConstraintFailure ?? true), "\(String(reflecting: error))")
- #expect(!((error as? any DatabaseError)?.isConnectionClosed ?? true), "\(String(reflecting: error))")
+ let error2 = await #expect(throws: (any Error).self) { try await sql2.insert(into: "foo").columns("name").values("bar").run() }
+ #expect((error2 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: error2))")
+ #expect(!((error2 as? any DatabaseError)?.isConstraintFailure ?? true), "\(String(reflecting: error2))")
+ #expect(!((error2 as? any DatabaseError)?.isConnectionClosed ?? true), "\(String(reflecting: error2))")
}
// Disabled until we figure out why it hangs instead of throwing an error.
@@ -134,6 +135,7 @@ struct FluentPostgresDriverTests {
// XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))")
//}
}
+ #endif
@Test
func blob() async throws {
diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift
index fac35b6..50d2e35 100644
--- a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift
+++ b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift
@@ -6,6 +6,7 @@ extension AllSuites {
struct FluentPostgresTransactionControlTests {
init() { #expect(isLoggingConfigured) }
+ #if !compiler(<6.1) // #expect(throws:) doesn't return the Error until 6.1
@Test
func rollback() async throws {
try await withDbs { _, db in try await db.withConnection { db in
@@ -26,7 +27,8 @@ struct FluentPostgresTransactionControlTests {
try await CreateTodo().revert(on: db)
} }
}
-
+ #endif
+
final class Todo: Model, @unchecked Sendable {
static let schema = "todos"
@ID var id
From 3ebf94dfa26bb2fffff012f54cbb170d9093770b Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Mon, 27 Oct 2025 12:27:12 -0500
Subject: [PATCH 4/5] Enable CI to specify environment more sparsely
---
.../FluentPostgresDriverTests.swift | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
index 0af223c..5117b3b 100644
--- a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
+++ b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
@@ -322,11 +322,11 @@ extension DatabaseConfigurationFactory {
decodingContext: PostgresDecodingContext = .default
) -> Self {
let baseSubconfig = SQLPostgresConfiguration(
- hostname: ProcessInfo.processInfo.environment["POSTGRES_HOSTNAME_\(subconfig)"] ?? "localhost",
- port: ProcessInfo.processInfo.environment["POSTGRES_PORT_\(subconfig)"].flatMap(Int.init) ?? SQLPostgresConfiguration.ianaPortNumber,
- username: ProcessInfo.processInfo.environment["POSTGRES_USER_\(subconfig)"] ?? "test_username",
- password: ProcessInfo.processInfo.environment["POSTGRES_PASSWORD_\(subconfig)"] ?? "test_password",
- database: ProcessInfo.processInfo.environment["POSTGRES_DB_\(subconfig)"] ?? "test_database",
+ hostname: env("POSTGRES_HOSTNAME_\(subconfig)") ?? env("POSTGRES_HOSTNAME_A") ?? env("POSTGRES_HOSTNAME") ?? "localhost",
+ port: (env("POSTGRES_PORT_\(subconfig)") ?? env("POSTGRES_PORT_A") ?? env("POSTGRES_PORT")).flatMap(Int.init) ?? SQLPostgresConfiguration.ianaPortNumber,
+ username: env("POSTGRES_USER_\(subconfig)") ?? env("POSTGRES_USER_A") ?? env("POSTGRES_USER") ?? "test_username",
+ password: env("POSTGRES_PASSWORD_\(subconfig)") ?? env("POSTGRES_PASSWORD_A") ?? env("POSTGRES_PASSWORD") ?? "test_password",
+ database: env("POSTGRES_DB_\(subconfig)") ?? env("POSTGRES_DB_A") ?? env("POSTGRES_DB") ?? "test_database",
tls: try! .prefer(.init(configuration: .makeClientConfiguration()))
)
@@ -445,8 +445,12 @@ struct EnumAddMultipleCasesMigration: AsyncMigration {
}
}
+func env(_ e: String) -> String? {
+ ProcessInfo.processInfo.environment[e]
+}
+
let isLoggingConfigured: Bool = {
- LoggingSystem.bootstrap { QuickLogHandler(label: $0, level: ProcessInfo.processInfo.environment["LOG_LEVEL"].flatMap { .init(rawValue: $0) } ?? .info) }
+ LoggingSystem.bootstrap { QuickLogHandler(label: $0, level: env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info) }
return true
}()
From 3958961ec73413877f00147f9b40659ec8365fab Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Mon, 27 Oct 2025 12:33:53 -0500
Subject: [PATCH 5/5] Fix test
---
.../FluentPostgresDriverTests/FluentPostgresDriverTests.swift | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
index 5117b3b..97ee68d 100644
--- a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
+++ b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
@@ -118,8 +118,8 @@ struct FluentPostgresDriverTests {
try await sql2.create(table: "foo").column("name", type: .text, .unique).run()
try await sql2.insert(into: "foo").columns("name").values("bar").run()
let error2 = await #expect(throws: (any Error).self) { try await sql2.insert(into: "foo").columns("name").values("bar").run() }
- #expect((error2 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: error2))")
- #expect(!((error2 as? any DatabaseError)?.isConstraintFailure ?? true), "\(String(reflecting: error2))")
+ #expect(!((error2 as? any DatabaseError)?.isSyntaxError ?? true), "\(String(reflecting: error2))")
+ #expect((error2 as? any DatabaseError)?.isConstraintFailure ?? false, "\(String(reflecting: error2))")
#expect(!((error2 as? any DatabaseError)?.isConnectionClosed ?? true), "\(String(reflecting: error2))")
}