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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,6 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
- 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
run: swift sdk install https://download.swift.org/swift-6.2.3-release/static-sdk/swift-6.2.3-RELEASE/swift-6.2.3-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum f30ec724d824ef43b5546e02ca06a8682dafab4b26a99fbb0e858c347e507a2c
- name: Build
run: swift build --swift-sdk x86_64-swift-linux-musl
69 changes: 63 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,72 @@ guard let configuration = SQLPostgresConfiguration(url: "postgres://...") else {
To connect via unix-domain sockets, use ``SQLPostgresConfiguration/init(unixDomainSocketPath:username:password:database:)`` instead of ``SQLPostgresConfiguration/init(hostname:port:username:password:database:tls:)``.

```swift
let configuration = PostgresConfiguration(
let configuration = SQLPostgresConfiguration(
unixDomainSocketPath: "/path/to/socket",
username: "vapor_username",
password: "vapor_password",
database: "vapor_database"
)
```

### Connection Pool
### Connection Pool (Modern PostgresNIO)

You don't need a ``SQLPostgresConfiguration`` to create a `PostgresClient`, an instance of PostgresNIO's modern connection pool. Instead, use `PostgresClient`'s native configuration type:

```swift
let configuration = PostgresClient.Configuration(
host: "localhost",
username: "vapor_username",
password: "vapor_password",
database: "vapor_database",
tls: .prefer(.makeClientConfiguration())
)
let psqlClient = PostgresClient(configuration: configuration)

// Start a Task to run the client:
let clientTask = Task { await client.run() }
// Or, if you're using ServiceLifecycle, add the client to a ServiceGroup:
await serviceGroup.addServiceUnlessShutdown(client)
```

You can then lease a `PostgresConnection` from the client:

```swift
try await client.withConnection { conn in
print(conn) // PostgresConnection managed by PostgresClient's connection pool
}
```

> [!NOTE]
> `PostgresClient.Configuration` does not support URL-based configuration. If you want to handle URLs, you can create an instance of `SQLPostgresConfiguration` and translate it into a `PostgresClient.Configuration`:
>
> ```swift
> extension PostgresClient.Configuration {
> init(from configuration: PostgresConnection.Configuration) {
> let tls: PostgresClient.Configuration.TLS = switch (configuration.tls.isEnforced, configuration.tls.isAllowed) {
> case (true, _): .require(configuration.tls.sslContext!.configuration)
> case (_, true): .prefer(configuration.tls.sslContext!.configuration)
> default: .disable
> }
>
> if let host = configuration.host, let port = configuration.port {
> self.init(host: host, port: port, username: configuration.username, password: configuration.password, database: configuration.database, tls: tls)
> } else if let socket = configuration.unixSocketPath {
> self.init(unixSocketPath: socket, username: configuration.username, password: configuration.password, database: configuration.database)
> } else {
> fatalError("Preconfigured channels not supported")
> }
> }
> }
>
> guard let sqlConfiguration = SQLPostgresConfiguration(url: "...") else { ... }
> let clientConfiguration = PostgresClient.Configuration(configuration: sqlConfiguration.coreConfiguration)
> ```

### Connection Pool (Legacy AsyncKit)

> [!WARNING]
> AsyncKit is deprecated; using it is strongly discouraged. You should not use this setup unless you are also working with FluentKit, which at the time of this writing is not compatible with `PostgresClient`.

Once you have a ``SQLPostgresConfiguration``, you can use it to create a connection source and pool.

Expand All @@ -91,7 +148,7 @@ Next, use the connection source to create an `EventLoopGroupConnectionPool`. You
`EventLoopGroupConnectionPool` is a collection of pools for each event loop. When using `EventLoopGroupConnectionPool` directly, random event loops will be chosen as needed.

```swift
pools.withConnection { conn
pools.withConnection { conn in
print(conn) // PostgresConnection on randomly chosen event loop
}
```
Expand All @@ -102,7 +159,7 @@ To get a pool for a specific event loop, use `pool(for:)`. This returns an `Even
let eventLoop: EventLoop = ...
let pool = pools.pool(for: eventLoop)

pool.withConnection { conn
pool.withConnection { conn in
print(conn) // PostgresConnection on eventLoop
}
```
Expand All @@ -113,7 +170,7 @@ Both `EventLoopGroupConnectionPool` and `EventLoopConnectionPool` can be used to

```swift
let postgres = pool.database(logger: ...) // PostgresDatabase
let rows = try postgres.simpleQuery("SELECT version();").wait()
let rows = try await postgres.simpleQuery("SELECT version()")
```

Visit [PostgresNIO's docs] for more information on using `PostgresDatabase`.
Expand All @@ -124,7 +181,7 @@ A `PostgresDatabase` can be used to create an instance of `SQLDatabase`.

```swift
let sql = postgres.sql() // SQLDatabase
let planets = try sql.select().column("*").from("planets").all().wait()
let planets = try await sql.select().column("*").from("planets").all()
```

Visit [SQLKit's docs] for more information on using `SQLDatabase`.
Expand Down
67 changes: 61 additions & 6 deletions Sources/PostgresKit/Docs.docc/PostgresKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,70 @@ guard let configuration = SQLPostgresConfiguration(url: "postgres://...") else {
To connect via unix-domain sockets, use ``SQLPostgresConfiguration/init(unixDomainSocketPath:username:password:database:)`` instead of ``SQLPostgresConfiguration/init(hostname:port:username:password:database:tls:)``.

```swift
let configuration = PostgresConfiguration(
let configuration = SQLPostgresConfiguration(
unixDomainSocketPath: "/path/to/socket",
username: "vapor_username",
password: "vapor_password",
database: "vapor_database"
)
```

### Connection Pool
### Connection Pool (Modern PostgresNIO)

You don't need a ``SQLPostgresConfiguration`` to create a `PostgresClient`, an instance of PostgresNIO's modern connection pool. Instead, use `PostgresClient`'s native configuration type:

```swift
let configuration = PostgresClient.Configuration(
host: "localhost",
username: "vapor_username",
password: "vapor_password",
database: "vapor_database",
tls: .prefer(.makeClientConfiguration())
)
let psqlClient = PostgresClient(configuration: configuration)

// Start a Task to run the client; be sure you cancel this task before exiting:
let clientTask = Task { await psqlClient.run() }
// Or, if you're using ServiceLifecycle, add the client to a ServiceGroup:
await serviceGroup.addServiceUnlessShutdown(psqlClient)
```

You can then lease a `PostgresConnection` from the client:

```swift
try await client.withConnection { conn in
print(conn) // PostgresConnection managed by PostgresClient's connection pool
}
```

> Note: `PostgresClient.Configuration` does not support URL-based configuration. If you want to handle URLs, you can create an instance of `SQLPostgresConfiguration` and translate it into a `PostgresClient.Configuration`:
>
> ```swift
> extension PostgresClient.Configuration {
> init(from configuration: PostgresConnection.Configuration) {
> let tls: PostgresClient.Configuration.TLS = switch (configuration.tls.isEnforced, configuration.tls.isAllowed) {
> case (true, _): .require(configuration.tls.sslContext!.configuration)
> case (_, true): .prefer(configuration.tls.sslContext!.configuration)
> default: .disable
> }
>
> if let host = configuration.host, let port = configuration.port {
> self.init(host: host, port: port, username: configuration.username, password: configuration.password, database: configuration.database, tls: tls)
> } else if let socket = configuration.unixSocketPath {
> self.init(unixSocketPath: socket, username: configuration.username, password: configuration.password, database: configuration.database)
> } else {
> fatalError("Preconfigured channels not supported")
> }
> }
> }
>
> guard let sqlConfiguration = SQLPostgresConfiguration(url: "...") else { ... }
> let clientConfiguration = PostgresClient.Configuration(configuration: sqlConfiguration.coreConfiguration)
> ```

### Connection Pool (Legacy AsyncKit)

> Warning: AsyncKit is deprecated; using it is strongly discouraged. You should not use this setup unless you are also working with FluentKit, which at the time of this writing is not compatible with `PostgresClient`.

Once you have a ``SQLPostgresConfiguration``, you can use it to create a connection source and pool.

Expand All @@ -83,7 +138,7 @@ Next, use the connection source to create an `EventLoopGroupConnectionPool`. You
`EventLoopGroupConnectionPool` is a collection of pools for each event loop. When using `EventLoopGroupConnectionPool` directly, random event loops will be chosen as needed.

```swift
pools.withConnection { conn
pools.withConnection { conn in
print(conn) // PostgresConnection on randomly chosen event loop
}
```
Expand All @@ -94,7 +149,7 @@ To get a pool for a specific event loop, use `pool(for:)`. This returns an `Even
let eventLoop: EventLoop = ...
let pool = pools.pool(for: eventLoop)

pool.withConnection { conn
pool.withConnection { conn in
print(conn) // PostgresConnection on eventLoop
}
```
Expand All @@ -105,7 +160,7 @@ Both `EventLoopGroupConnectionPool` and `EventLoopConnectionPool` can be used to

```swift
let postgres = pool.database(logger: ...) // PostgresDatabase
let rows = try postgres.simpleQuery("SELECT version();").wait()
let rows = try await postgres.simpleQuery("SELECT version()")
```

Visit [PostgresNIO's docs] for more information on using `PostgresDatabase`.
Expand All @@ -116,7 +171,7 @@ A `PostgresDatabase` can be used to create an instance of `SQLDatabase`.

```swift
let sql = postgres.sql() // SQLDatabase
let planets = try sql.select().column("*").from("planets").all().wait()
let planets = try await sql.select().column("*").from("planets").all()
```

Visit [SQLKit's docs] for more information on using `SQLDatabase`.
Expand Down
13 changes: 7 additions & 6 deletions Sources/PostgresKit/PostgresDataTranslation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,17 @@ struct PostgresDataTranslation {
context: context,
file: file, line: line
))
} catch DecodingError.dataCorrupted {
} catch DecodingError.dataCorrupted(let errContext) {
/// Glacial path: Attempt to decode as plain JSON.
guard cell.dataType == .json || cell.dataType == .jsonb else {
throw DecodingError.dataCorrupted(.init(
codingPath: codingPath,
debugDescription: "Unable to interpret value of PSQL type \(cell.dataType): \(cell.bytes.map { "\($0)" } ?? "null")"
debugDescription: "Unable to interpret value of PSQL type \(cell.dataType) as Swift type \(T.self): \(cell.bytes.map { "\($0)" } ?? "null")",
underlyingError: DecodingError.dataCorrupted(errContext)
))
}
if cell.dataType == .jsonb, cell.format == .binary, let buffer = cell.bytes {
// TODO: Un-hardcode this magic knowledge of the JSONB encoding
// Account for the leading JSONB version byte
return try context.jsonDecoder.decode(T.self, from: buffer.getSlice(at: buffer.readerIndex + 1, length: buffer.readableBytes - 1) ?? .init())
} else {
return try context.jsonDecoder.decode(T.self, from: cell.bytes ?? .init())
Expand Down Expand Up @@ -202,7 +203,7 @@ struct PostgresDataTranslation {
/// Legacy "fast"-path: Direct conformance to `PostgresDataConvertible`; use is deprecated.
else if let legacyPathValue = value as? any PostgresDataTranslation.PostgresLegacyDataConvertible {
guard let legacyData = legacyPathValue.postgresData else {
throw EncodingError.invalidValue(value, .init(codingPath: [], debugDescription: "Couldn't get PSQL encoding from value '\(value)'"))
throw EncodingError.invalidValue(value, .init(codingPath: [], debugDescription: "Couldn't get PSQL encoding from value '\(value)' of Swift type \(T.self)/\(type(of: value))"))
}
bindings.append(legacyData)
}
Expand All @@ -227,7 +228,7 @@ struct PostgresDataTranslation {
return PostgresData(type: type(of: fastPathValue).psqlType, typeModifier: nil, formatCode: type(of: fastPathValue).psqlFormat, value: buffer)
} else if let legacyPathValue = value as? any PostgresDataTranslation.PostgresLegacyDataConvertible {
guard let legacyData = legacyPathValue.postgresData else {
throw EncodingError.invalidValue(value, .init(codingPath: [], debugDescription: "Couldn't get PSQL encoding from value '\(value)'"))
throw EncodingError.invalidValue(value, .init(codingPath: [], debugDescription: "Couldn't get PSQL encoding from value '\(value)' of Swift type \(T.self)/\(type(of: value))"))
}
return legacyData
}
Expand All @@ -240,7 +241,7 @@ struct PostgresDataTranslation {
case .scalar(let scalar): return scalar
case .indexed(let ref):
let elementType = ref.contents.first?.type ?? .jsonb
assert(ref.contents.allSatisfy { $0.type == elementType }, "Type \(type(of: value)) was encoded as a heterogenous array; this is unsupported.")
assert(ref.contents.allSatisfy { $0.type == elementType }, "Type \(T.self)/\(type(of: value)) was encoded as a heterogenous array; this is unsupported.")
return PostgresData(array: ref.contents, elementType: elementType)
}
} catch is ArrayAwareBoxWrappingPostgresEncoder<E>.FallbackSentinel {
Expand Down
26 changes: 26 additions & 0 deletions Tests/PostgresKitTests/PostgresKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,32 @@ struct PostgresKitTests {
#expect(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default) == url)
}

/// This test is painful to write before Swift 6.1 due to #expect(throws:) not returning the thrown error.
///
/// This test cares that:
///
/// 1. The Swift type (i.e. `Foo`) is metnioned in the error's debug description.
/// 2. The underlying error is included.
#if swift(>=6.1)
@Test
func errorHandlingWhenDecodingNestedDictionary() throws {
struct Foo: Codable {
struct Bar: Codable { let id: Int }
let bar: Bar
}

let error = try #require(throws: DecodingError.self) {
_ = try PostgresDataTranslation.decode(Foo.self, from: .init(bytes: .init(integer: 0), dataType: .int8, format: .binary, columnName: "", columnIndex: 0), in: .default)
}

let context = try #require({ if case .dataCorrupted(let context) = error { context } else { nil } }())
#expect(context.debugDescription == "Unable to interpret value of PSQL type BIGINT as Swift type Foo: [0000000000000000](8 bytes)")

let underContext = try #require({ if case .dataCorrupted(let context2) = context.underlyingError as? DecodingError { context2 } else { nil } }())
#expect(underContext.debugDescription == "Dictionary containers must be JSON-encoded")
}
#endif

var eventLoop: any EventLoop {
MultiThreadedEventLoopGroup.singleton.any()
}
Expand Down