Repository: vapor/postgres-kit Branch: main Commit: 7c079553e9cd Files: 28 Total size: 121.6 KB Directory structure: gitextract_j417607q/ ├── .github/ │ ├── CODEOWNERS │ ├── dependabot.yml │ └── workflows/ │ ├── api-docs.yml │ └── test.yml ├── .gitignore ├── .spi.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources/ │ └── PostgresKit/ │ ├── ConnectionPool+Postgres.swift │ ├── Deprecations/ │ │ ├── PostgresColumnType.swift │ │ ├── PostgresConfiguration.swift │ │ ├── PostgresConnectionSource+PostgresConfiguration.swift │ │ ├── PostgresDataDecoder.swift │ │ ├── PostgresDataEncoder.swift │ │ └── PostgresDatabase+SQL+Deprecated.swift │ ├── Docs.docc/ │ │ ├── PostgresKit.md │ │ └── theme-settings.json │ ├── Exports.swift │ ├── PostgresConnectionSource.swift │ ├── PostgresDataTranslation.swift │ ├── PostgresDatabase+SQL.swift │ ├── PostgresDialect.swift │ ├── PostgresRow+SQL.swift │ └── SQLPostgresConfiguration.swift └── Tests/ └── PostgresKitTests/ ├── PostgresKitTests.swift ├── SQLPostgresConfigurationTests.swift └── Utilities.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ * @gwynne /.github/CONTRIBUTING.md @gwynne @0xTim /.github/workflows/*.yml @gwynne @0xTim /.github/workflows/test.yml @gwynne /.spi.yml @gwynne @0xTim /.gitignore @gwynne @0xTim /LICENSE @gwynne @0xTim /README.md @gwynne @0xTim ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" groups: dependencies: patterns: - "*" ================================================ FILE: .github/workflows/api-docs.yml ================================================ name: deploy-api-docs on: push: branches: - main permissions: contents: read id-token: write jobs: build-and-deploy: uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main secrets: inherit with: package_name: postgres-kit modules: PostgresKit pathsToInvalidate: /postgreskit/* ================================================ FILE: .github/workflows/test.yml ================================================ name: test concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: pull_request: { types: [opened, reopened, synchronize, ready_for_review] } push: { branches: [ main ] } permissions: contents: read env: LOG_LEVEL: info 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' POSTGRES_DB_B: &postgres_db_b 'test_database' 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: if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest container: swift:noble steps: - name: Checkout uses: actions/checkout@v6 with: { 'fetch-depth': 0 } - name: API breaking changes run: | git config --global --add safe.directory "${GITHUB_WORKSPACE}" swift package diagnose-api-breaking-changes origin/main linux-unit: if: ${{ !(github.event.pull_request.draft || false) }} strategy: fail-fast: false matrix: postgres-image: - postgres:18 - postgres:16 - postgres:14 swift-image: - swift:6.0-noble - swift:6.1-noble - swift:6.2-noble include: - postgres-image: postgres:18 postgres-auth: scram-sha-256 - postgres-image: postgres:16 postgres-auth: md5 - postgres-image: postgres:14 postgres-auth: trust runs-on: ubuntu-latest container: ${{ matrix.swift-image }} services: *postgres_host_a: image: ${{ matrix.postgres-image }} env: 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 }} steps: - name: Install curl run: apt-get update -y -q && apt-get install -y curl - name: Check out package uses: actions/checkout@v6 - name: Run local tests run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable - name: Upload coverage data uses: vapor/swift-codecov-action@v0.3 with: codecov_token: ${{ secrets.CODECOV_TOKEN || '' }} linux-integration: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest container: swift:6.2-noble services: *postgres_host_a: image: postgres:18 env: POSTGRES_USER: *postgres_user_a POSTGRES_DB: *postgres_db_a POSTGRES_PASSWORD: *postgres_pass_a POSTGRES_HOST_AUTH_METHOD: scram-sha-256 POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 *postgres_host_b: image: postgres:16 env: POSTGRES_USER: *postgres_user_b POSTGRES_DB: *postgres_db_b POSTGRES_PASSWORD: *postgres_pass_b POSTGRES_HOST_AUTH_METHOD: scram-sha-256 POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 steps: - name: Check out package uses: actions/checkout@v6 with: { path: 'postgres-kit' } - name: Check out fluent-postgres-driver dependent uses: actions/checkout@v6 with: { repository: 'vapor/fluent-postgres-driver', path: 'fluent-postgres-driver' } - name: Use local package run: swift package --package-path fluent-postgres-driver edit postgres-kit --path postgres-kit - name: Run fluent-postgres-kit tests run: swift test --package-path fluent-postgres-driver macos-unit: if: ${{ !(github.event.pull_request.draft || false) }} strategy: fail-fast: false matrix: include: - macos-version: macos-15 xcode-version: latest-stable - macos-version: macos-26 xcode-version: latest-stable runs-on: ${{ matrix.macos-version }} env: POSTGRES_HOSTNAME: 127.0.0.1 POSTGRES_DB: postgres steps: - name: Select latest available Xcode uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ matrix.xcode-version }} - name: Install Postgres, setup DB and auth, and wait for server start run: | brew upgrade || true export PGDATA=/tmp/vapor-postgres-test brew install "postgresql@18" && brew link --force "postgresql@18" initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER}" --pwfile=<(echo "${POSTGRES_PASSWORD}") pg_ctl start --wait timeout-minutes: 15 - name: Checkout code uses: actions/checkout@v6 - name: Run local tests run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable - name: Upload coverage data 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@v6 - name: Install SDK 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 ================================================ FILE: .gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj Package.resolved DerivedData .swiftpm Tests/LinuxMain.swift ================================================ FILE: .spi.yml ================================================ version: 1 metadata: authors: "Maintained by the Vapor Core Team with hundreds of contributions from the Vapor Community." external_links: documentation: "https://api.vapor.codes/postgreskit/documentation/postgreskit/" ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Qutheory, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:6.0 import PackageDescription let package = Package( name: "postgres-kit", platforms: [ .macOS(.v10_15), .iOS(.v13), .watchOS(.v6), .tvOS(.v13), ], products: [ .library(name: "PostgresKit", targets: ["PostgresKit"]), ], dependencies: [ .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.27.0"), .package(url: "https://github.com/vapor/sql-kit.git", from: "3.33.2"), .package(url: "https://github.com/vapor/async-kit.git", from: "1.21.0"), ], targets: [ .target( name: "PostgresKit", dependencies: [ .product(name: "AsyncKit", package: "async-kit"), .product(name: "PostgresNIO", package: "postgres-nio"), .product(name: "SQLKit", package: "sql-kit"), ], swiftSettings: swiftSettings ), .testTarget( name: "PostgresKitTests", dependencies: [ .target(name: "PostgresKit"), .product(name: "SQLKitBenchmark", package: "sql-kit"), ], swiftSettings: swiftSettings ), ] ) var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), //.enableUpcomingFeature("InternalImportsByDefault"), .enableUpcomingFeature("MemberImportVisibility"), .enableUpcomingFeature("InferIsolatedConformances"), //.enableUpcomingFeature("NonisolatedNonsendingByDefault"), .enableUpcomingFeature("ImmutableWeakCaptures"), ] } ================================================ FILE: README.md ================================================

PostgresKit

Documentation Team Chat MIT License Continuous Integration Code Coverage Swift 6.0+


PostgresKit is an [SQLKit] driver for PostgreSQL clients. ## Overview PostgresKit supports building and serializing Postgres-dialect SQL queries using [SQLKit]'s API. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. > Important: It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. ### Usage Reference this package in your `Package.swift` to include it in your project. ```swift .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0") ``` ### Supported Platforms PostgresKit supports the following platforms: - Ubuntu 20.04+ - macOS 10.15+ ### Configuration Database connection options and credentials are specified using a ``SQLPostgresConfiguration`` struct. ```swift import PostgresKit let configuration = SQLPostgresConfiguration( hostname: "localhost", username: "vapor_username", password: "vapor_password", database: "vapor_database" ) ``` URL-based configuration is also supported. ```swift 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 = SQLPostgresConfiguration( unixDomainSocketPath: "/path/to/socket", username: "vapor_username", password: "vapor_password", database: "vapor_database" ) ``` ### 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. ```swift let eventLoopGroup: EventLoopGroup = NIOSingletons.posixEventLoopGroup let pools = EventLoopGroupConnectionPool( source: PostgresConnectionSource(configuration: configuration), on: eventLoopGroup ) // When you're done: try await pools.shutdownAsync() ``` First create a ``PostgresConnectionSource`` using the configuration struct. This type is responsible for creating new connections to your database server as needed. Next, use the connection source to create an `EventLoopGroupConnectionPool`. You will also need to pass an `EventLoopGroup`. For more information on creating an `EventLoopGroup`, visit [SwiftNIO's documentation]. Make sure to shutdown the connection pool before it deinitializes. `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 in print(conn) // PostgresConnection on randomly chosen event loop } ``` To get a pool for a specific event loop, use `pool(for:)`. This returns an `EventLoopConnectionPool`. ```swift let eventLoop: EventLoop = ... let pool = pools.pool(for: eventLoop) pool.withConnection { conn in print(conn) // PostgresConnection on eventLoop } ``` ### PostgresDatabase Both `EventLoopGroupConnectionPool` and `EventLoopConnectionPool` can be used to create instances of `PostgresDatabase`. ```swift let postgres = pool.database(logger: ...) // PostgresDatabase let rows = try await postgres.simpleQuery("SELECT version()") ``` Visit [PostgresNIO's docs] for more information on using `PostgresDatabase`. ### SQLDatabase A `PostgresDatabase` can be used to create an instance of `SQLDatabase`. ```swift let sql = postgres.sql() // SQLDatabase let planets = try await sql.select().column("*").from("planets").all() ``` Visit [SQLKit's docs] for more information on using `SQLDatabase`. [SQLKit]: https://github.com/vapor/sql-kit [SQLKit's docs]: https://api.vapor.codes/sqlkit/documentation/sqlkit [PostgresNIO]: https://github.com/vapor/postgres-nio [PostgresNIO's docs]: https://api.vapor.codes/postgresnio/documentation/postgresnio [AsyncKit]: https://github.com/vapor/async-kit [PostgresClient]: https://api.vapor.codes/postgresnio/documentation/postgresnio/postgresclient [SwiftNIO's documentation]: https://swiftpackageindex.com/apple/swift-nio/documentation/nio ================================================ FILE: Sources/PostgresKit/ConnectionPool+Postgres.swift ================================================ @preconcurrency import AsyncKit import Logging import NIOCore import PostgresNIO extension EventLoopGroupConnectionPool where Source == PostgresConnectionSource { public func database(logger: Logger) -> any PostgresDatabase { _EventLoopGroupConnectionPoolPostgresDatabase(pool: self, logger: logger) } } extension EventLoopConnectionPool where Source == PostgresConnectionSource { public func database(logger: Logger) -> any PostgresDatabase { _EventLoopConnectionPoolPostgresDatabase(pool: self, logger: logger) } } private struct _EventLoopGroupConnectionPoolPostgresDatabase: PostgresDatabase { let pool: EventLoopGroupConnectionPool let logger: Logger var eventLoop: any EventLoop { self.pool.eventLoopGroup.any() } func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture { self.pool.withConnection(logger: logger) { $0.send(request, logger: logger) } } func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture { self.pool.withConnection(logger: self.logger, closure) } } private struct _EventLoopConnectionPoolPostgresDatabase: PostgresDatabase { let pool: EventLoopConnectionPool let logger: Logger var eventLoop: any EventLoop { self.pool.eventLoop } func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture { self.pool.withConnection(logger: logger) { $0.send(request, logger: logger) } } func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture { self.pool.withConnection(logger: self.logger, closure) } } ================================================ FILE: Sources/PostgresKit/Deprecations/PostgresColumnType.swift ================================================ import SQLKit /// Postgres-specific column types. @available(*, deprecated, message: "Use `PostgresDataType` instead.") public struct PostgresColumnType: SQLExpression, Hashable { public static var blob: PostgresColumnType { .varbit } /// signed eight-byte integer public static var int8: PostgresColumnType { .bigint } /// signed eight-byte integer public static var bigint: PostgresColumnType { .init(.bigint) } /// autoincrementing eight-byte integer public static var serial8: PostgresColumnType { .bigserial } /// autoincrementing eight-byte integer public static var bigserial: PostgresColumnType { .init(.bigserial) } /// fixed-length bit string public static var bit: PostgresColumnType { .init(.bit(nil)) } /// fixed-length bit string public static func bit(_ n: Int) -> PostgresColumnType { .init(.bit(n)) } /// variable-length bit string public static var varbit: PostgresColumnType { .init(.varbit(nil)) } /// variable-length bit string public static func varbit(_ n: Int) -> PostgresColumnType { .init(.varbit(n)) } /// logical Boolean (true/false) public static var bool: PostgresColumnType { .boolean } /// logical Boolean (true/false) public static var boolean: PostgresColumnType { .init(.boolean) } /// rectangular box on a plane public static var box: PostgresColumnType { .init(.box) } /// binary data (“byte array”) public static var bytea: PostgresColumnType { .init(.bytea) } /// fixed-length character string public static var char: PostgresColumnType { .init(.char(nil)) } /// fixed-length character string public static func char(_ n: Int) -> PostgresColumnType { .init(.char(n)) } /// variable-length character string public static var varchar: PostgresColumnType { .init(.varchar(nil)) } /// variable-length character string public static func varchar(_ n: Int) -> PostgresColumnType { .init(.varchar(n)) } /// IPv4 or IPv6 network address public static var cidr: PostgresColumnType { .init(.cidr) } /// circle on a plane public static var circle: PostgresColumnType { .init(.circle) } /// calendar date (year, month, day) public static var date: PostgresColumnType { .init(.date) } /// floating-point number (8 bytes) public static var float8: PostgresColumnType { .doublePrecision } /// floating-point number (8 bytes) public static var doublePrecision: PostgresColumnType { .init(.doublePrecision) } /// IPv4 or IPv6 host address public static var inet: PostgresColumnType { .init(.inet) } /// signed four-byte integer public static var int: PostgresColumnType { .integer } /// signed four-byte integer public static var int4: PostgresColumnType { .integer } /// signed four-byte integer public static var integer: PostgresColumnType { .init(.integer) } /// time span public static var interval: PostgresColumnType { .init(.interval) } /// textual JSON data public static var json: PostgresColumnType { .init(.json) } /// binary JSON data, decomposed public static var jsonb: PostgresColumnType { .init(.jsonb) } /// infinite line on a plane public static var line: PostgresColumnType { .init(.line) } /// line segment on a plane public static var lseg: PostgresColumnType { .init(.lseg) } /// MAC (Media Access Control) address public static var macaddr: PostgresColumnType { .init(.macaddr) } /// MAC (Media Access Control) address (EUI-64 format) public static var macaddr8: PostgresColumnType { .init(.macaddr8) } /// currency amount public static var money: PostgresColumnType { .init(.money) } /// exact numeric of selectable precision public static var decimal: PostgresColumnType { .init(.numeric(nil, nil)) } /// exact numeric of selectable precision public static func decimal(_ p: Int, _ s: Int) -> PostgresColumnType { .init(.numeric(p, s)) } /// exact numeric of selectable precision public static func numeric(_ p: Int, _ s: Int) -> PostgresColumnType { .init(.numeric(p, s)) } /// exact numeric of selectable precision public static var numeric: PostgresColumnType { .init(.numeric(nil, nil)) } /// geometric path on a plane public static var path: PostgresColumnType { .init(.path) } /// PostgreSQL Log Sequence Number public static var pgLSN: PostgresColumnType { .init(.pgLSN) } /// geometric point on a plane public static var point: PostgresColumnType { .init(.point) } /// closed geometric path on a plane public static var polygon: PostgresColumnType { .init(.polygon) } /// single precision floating-point number (4 bytes) public static var float4: PostgresColumnType { .real } /// single precision floating-point number (4 bytes) public static var real: PostgresColumnType { .init(.real) } /// signed two-byte integer public static var int2: PostgresColumnType { .smallint } /// signed two-byte integer public static var smallint: PostgresColumnType { .init(.smallint) } /// autoincrementing two-byte integer public static var serial2: PostgresColumnType { .smallserial } /// autoincrementing two-byte integer public static var smallserial: PostgresColumnType { .init(.smallserial) } /// autoincrementing four-byte integer public static var serial4: PostgresColumnType { .serial } /// autoincrementing four-byte integer public static var serial: PostgresColumnType { .init(.serial) } /// variable-length character string public static var text: PostgresColumnType { .init(.text) } /// time of day (no time zone) public static var time: PostgresColumnType { .init(.time(nil)) } /// time of day (no time zone) public static func time(_ n: Int) -> PostgresColumnType { .init(.time(n)) } /// time of day, including time zone public static var timetz: PostgresColumnType { .init(.timetz(nil)) } /// time of day, including time zone public static func timetz(_ n: Int) -> PostgresColumnType { .init(.timetz(n)) } /// date and time (no time zone) public static var timestamp: PostgresColumnType { .init(.timestamp(nil)) } /// date and time (no time zone) public static func timestamp(_ n: Int) -> PostgresColumnType { .init(.timestamp(n)) } /// date and time, including time zone public static var timestamptz: PostgresColumnType { .init(.timestamptz(nil)) } /// date and time, including time zone public static func timestamptz(_ n: Int) -> PostgresColumnType { .init(.timestamptz(n)) } /// text search query public static var tsquery: PostgresColumnType { .init(.tsquery) } /// text search document public static var tsvector: PostgresColumnType { .init(.tsvector) } /// user-level transaction ID snapshot public static var txidSnapshot: PostgresColumnType { .init(.txidSnapshot) } /// universally unique identifier public static var uuid: PostgresColumnType { .init(.uuid) } /// XML data public static var xml: PostgresColumnType { .init(.xml) } /// User-defined type public static func custom(_ name: String) -> PostgresColumnType { .init(.custom(name)) } /// Creates an array type from a `PostgreSQLDataType`. public static func array(_ type: PostgresColumnType) -> PostgresColumnType { .init(.array(of: type.primitive)) } private let primitive: Primitive private init(_ primitive: Primitive) { self.primitive = primitive } enum Primitive: CustomStringConvertible, Hashable, Sendable { case bigint /// signed eight-byte integer case bigserial /// autoincrementing eight-byte integer case bit(Int?) /// fixed-length bit string case varbit(Int?) /// variable-length bit string case boolean /// logical Boolean (true/false) case box /// rectangular box on a plane case bytea /// binary data (“byte array”) case char(Int?) /// fixed-length character string case varchar(Int?) /// variable-length character string case cidr /// IPv4 or IPv6 network address case circle /// circle on a plane case date /// calendar date (year, month, day) case doublePrecision /// floating-point number (8 bytes) case inet /// IPv4 or IPv6 host address case integer /// signed four-byte integer case interval /// time span case json /// textual JSON data case jsonb /// binary JSON data, decomposed case line /// infinite line on a plane case lseg /// line segment on a plane case macaddr /// MAC (Media Access Control) address case macaddr8 /// MAC (Media Access Control) address (EUI-64 format) case money /// currency amount case numeric(Int?, Int?) /// exact numeric of selectable precision case path /// geometric path on a plane case pgLSN /// PostgreSQL Log Sequence Number case point /// geometric point on a plane case polygon /// closed geometric path on a plane case real /// single precision floating-point number (4 bytes) case smallint /// signed two-byte integer case smallserial /// autoincrementing two-byte integer case serial /// autoincrementing four-byte integer case text /// variable-length character string case time(Int?) /// time of day (no time zone) case timetz(Int?) /// time of day, including time zone case timestamp(Int?) /// date and time (no time zone) case timestamptz(Int?) /// date and time, including time zone case tsquery /// text search query case tsvector /// text search document case txidSnapshot /// user-level transaction ID snapshot case uuid /// universally unique identifier case xml /// XML data case custom(String) /// User-defined type indirect case array(of: Primitive) /// Array /// See `CustomStringConvertible.description`. var description: String { switch self { case .bigint: return "BIGINT" case .bigserial: return "BIGSERIAL" case .varbit(let n): return n.map { "VARBIT(\($0))" } ?? "VARBIT" case .varchar(let n): return n.map { "VARCHAR(\($0))" } ?? "VARCHAR" case .bit(let n): return n.map { "BIT(\($0))" } ?? "BIT" case .boolean: return "BOOLEAN" case .box: return "BOX" case .bytea: return "BYTEA" case .char(let n): return n.map { "CHAR(\($0))" } ?? "CHAR" case .cidr: return "CIDR" case .circle: return "CIRCLE" case .date: return "DATE" case .doublePrecision: return "DOUBLE PRECISION" case .inet: return "INET" case .integer: return "INTEGER" case .interval: return "INTERVAL" case .json: return "JSON" case .jsonb: return "JSONB" case .line: return "LINE" case .lseg: return "LSEG" case .macaddr: return "MACADDR" case .macaddr8: return "MACADDER8" case .money: return "MONEY" case .numeric(let s, let p): return strictMap(s, p) { "NUMERIC(\($0), \($1))" } ?? "NUMERIC" case .path: return "PATH" case .pgLSN: return "PG_LSN" case .point: return "POINT" case .polygon: return "POLYGON" case .real: return "REAL" case .smallint: return "SMALLINT" case .smallserial: return "SMALLSERIAL" case .serial: return "SERIAL" case .text: return "TEXT" case .time(let p): return p.map { "TIME(\($0))" } ?? "TIME" case .timetz(let p): return p.map { "TIMETZ(\($0))" } ?? "TIMETZ" case .timestamp(let p): return p.map { "TIMESTAMP(\($0))" } ?? "TIMESTAMP" case .timestamptz(let p): return p.map { "TIMESTAMPTZ(\($0))" } ?? "TIMESTAMPTZ" case .tsquery: return "TSQUERY" case .tsvector: return "TSVECTOR" case .txidSnapshot: return "TXID_SNAPSHOT" case .uuid: return "UUID" case .xml: return "XML" case .custom(let custom): return custom case .array(let element): return "\(element)[]" } } } // See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { serializer.write(self.primitive.description) } } ================================================ FILE: Sources/PostgresKit/Deprecations/PostgresConfiguration.swift ================================================ import Foundation import NIOCore import NIOSSL @available(*, deprecated, message: "Use `SQLPostgresConfiguration` instead.") public struct PostgresConfiguration { public var address: () throws -> SocketAddress public var username: String public var password: String? public var database: String? public var tlsConfiguration: TLSConfiguration? /// Require connection to provide `BackendKeyData`. /// For use with Amazon RDS Proxy, this must be set to false. /// /// - Default: true public var requireBackendKeyData: Bool = true /// Optional `search_path` to set on new connections. public var searchPath: [String]? /// IANA-assigned port number for PostgreSQL /// `UInt16(getservbyname("postgresql", "tcp").pointee.s_port).byteSwapped` public static var ianaPortNumber: Int { 5432 } internal var _hostname: String? internal var _port: Int? public init?(url: String) { guard let url = URL(string: url) else { return nil } self.init(url: url) } public init?(url: URL) { guard let comp = URLComponents(url: url, resolvingAgainstBaseURL: true), comp.scheme?.hasPrefix("postgres") ?? false, let hostname = comp.host, let username = comp.user else { return nil } let password = comp.password, port = comp.port ?? Self.ianaPortNumber let wantTLS = (comp.queryItems ?? []).contains { ["ssl=true", "sslmode=require"].contains($0.description) } self.init( hostname: hostname, port: port, username: username, password: password, database: url.lastPathComponent, tlsConfiguration: wantTLS ? .makeClientConfiguration() : nil ) } public init( unixDomainSocketPath: String, username: String, password: String? = nil, database: String? = nil ) { self.address = { return try SocketAddress.init(unixDomainSocketPath: unixDomainSocketPath) } self.username = username self.password = password self.database = database self.tlsConfiguration = nil self._hostname = nil self._port = nil } public init( hostname: String, port: Int = Self.ianaPortNumber, username: String, password: String? = nil, database: String? = nil, tlsConfiguration: TLSConfiguration? = nil ) { self.address = { return try SocketAddress.makeAddressResolvingHost(hostname, port: port) } self.username = username self.database = database self.password = password self.tlsConfiguration = tlsConfiguration self._hostname = hostname self._port = port } } ================================================ FILE: Sources/PostgresKit/Deprecations/PostgresConnectionSource+PostgresConfiguration.swift ================================================ import Atomics import Logging import NIOCore import NIOSSL import PostgresNIO extension PostgresConnectionSource { @available(*, deprecated, message: "Use `sqlConfiguration` instead.") public var configuration: PostgresConfiguration { if let hostname = self.sqlConfiguration.coreConfiguration.host, let port = self.sqlConfiguration.coreConfiguration.port { var oldConfig = PostgresConfiguration( hostname: hostname, port: port, username: self.sqlConfiguration.coreConfiguration.username, password: self.sqlConfiguration.coreConfiguration.password, database: self.sqlConfiguration.coreConfiguration.database, tlsConfiguration: self.sqlConfiguration.coreConfiguration.tls.sslContext.map { _ in .makeClientConfiguration() } ) oldConfig.requireBackendKeyData = self.sqlConfiguration.coreConfiguration.options.requireBackendKeyData oldConfig.searchPath = self.sqlConfiguration.searchPath return oldConfig } else if let socketPath = self.sqlConfiguration.coreConfiguration.unixSocketPath { var oldConfig = PostgresConfiguration( unixDomainSocketPath: socketPath, username: self.sqlConfiguration.coreConfiguration.username, password: self.sqlConfiguration.coreConfiguration.password, database: self.sqlConfiguration.coreConfiguration.database ) oldConfig.requireBackendKeyData = self.sqlConfiguration.coreConfiguration.options.requireBackendKeyData oldConfig.searchPath = self.sqlConfiguration.searchPath return oldConfig } else { return .init(hostname: "", port: 0, username: "", password: nil, database: nil, tlsConfiguration: nil) } } @available(*, deprecated, message: "Use `sqlConfiguration` instead.") public var sslContext: Result { .success(self.sqlConfiguration.coreConfiguration.tls.sslContext) } @available(*, deprecated, message: "Use `init(sqlConfiguration:)` instead.") public init(configuration: PostgresConfiguration) { self.init(sqlConfiguration: .init(legacyConfiguration: configuration)) } } extension SQLPostgresConfiguration { // N.B.: This is public only for the sake of deprecated support in FluentPostgresDriver. Don't use it. @available(*, deprecated, message: "This initializer is not intended for public use. Stop using `PostgresConfigration`.") public init(legacyConfiguration configuration: PostgresConfiguration) { if let hostname = configuration._hostname, let port = configuration._port { self.init( hostname: hostname, port: port, username: configuration.username, password: configuration.password, database: configuration.database, tls: configuration.tlsConfiguration.flatMap { try? .require(.init(configuration: $0)) } ?? .disable ) self.coreConfiguration.options.requireBackendKeyData = configuration.requireBackendKeyData self.searchPath = configuration.searchPath } else if let address = try? configuration.address(), let socketPath = address.pathname { self.init( unixDomainSocketPath: socketPath, username: configuration.username, password: configuration.password, database: configuration.database ) self.coreConfiguration.options.requireBackendKeyData = configuration.requireBackendKeyData self.searchPath = configuration.searchPath } else { fatalError("Nonsensical legacy configuration format") } } } ================================================ FILE: Sources/PostgresKit/Deprecations/PostgresDataDecoder.swift ================================================ import Foundation import PostgresNIO @available(*, deprecated, message: "Use `PostgresDecodingContext` instead.") public final class PostgresDataDecoder { public let json: any PostgresJSONDecoder public init(json: any PostgresJSONDecoder = PostgresNIO._defaultJSONDecoder) { self.json = json } public func decode(_: T.Type, from data: PostgresData) throws -> T where T: Decodable { // If `T` can be converted directly, just do so. if let convertible = T.self as? any PostgresDataConvertible.Type { guard let value = convertible.init(postgresData: data) else { throw DecodingError.typeMismatch(T.self, .init( codingPath: [], debugDescription: "Could not convert PostgreSQL data to \(T.self): \(data as Any)" )) } return value as! T } else { // Probably a Postgres array, JSON array/object, or enum type not using @Enum. See if it can be "unwrapped" // as a single-value decoding container, since this is much faster than attempting a JSON decode, or as an // array in the Postgres-native sense; this will handle "box" types such as `RawRepresentable` enums while // still allowing falling back to JSON. do { return try T.init(from: GiftBoxUnwrapDecoder(decoder: self, data: data)) } catch DecodingError.dataCorrupted { // Couldn't unwrap it either. Fall back to attempting a JSON decode. guard let jsonData = data.jsonb ?? data.json else { throw Error.unexpectedDataType(data.type, expected: "jsonb/json") } return try self.json.decode(T.self, from: jsonData) } } } enum Error: Swift.Error, CustomStringConvertible { case unexpectedDataType(PostgresDataType, expected: String) case nestingNotSupported var description: String { switch self { case .unexpectedDataType(let type, let expected): return "Unexpected data type: \(type). Expected \(expected)." case .nestingNotSupported: return "Decoding nested containers is not supported." } } } private final class GiftBoxUnwrapDecoder: Decoder, SingleValueDecodingContainer { var codingPath: [any CodingKey] { [] } var userInfo: [CodingUserInfoKey : Any] { [:] } let dataDecoder: PostgresDataDecoder let data: PostgresData init(decoder: PostgresDataDecoder, data: PostgresData) { self.dataDecoder = decoder self.data = data } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Dictionary containers must be JSON-encoded") } func unkeyedContainer() throws -> any UnkeyedDecodingContainer { guard let array = self.data.array else { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Non-natively typed arrays must be JSON-encoded") } return ArrayContainer(data: array, dataDecoder: self.dataDecoder) } struct ArrayContainer: UnkeyedDecodingContainer { let data: [PostgresData] let dataDecoder: PostgresDataDecoder var codingPath: [any CodingKey] { [] } var count: Int? { self.data.count } var isAtEnd: Bool { self.currentIndex >= self.data.count } var currentIndex: Int = 0 mutating func decodeNil() throws -> Bool { // Do _not_ shorten this using `defer`, otherwise `currentIndex` is incorrectly incremented. if self.data[self.currentIndex].value == nil { self.currentIndex += 1 return true } return false } mutating func decode(_: T.Type) throws -> T where T: Decodable { // Do _not_ shorten this using `defer`, otherwise `currentIndex` is incorrectly incremented. let result = try self.dataDecoder.decode(T.self, from: self.data[self.currentIndex]) self.currentIndex += 1 return result } mutating func nestedContainer(keyedBy _: NewKey.Type) throws -> KeyedDecodingContainer { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Data nesting is not supported") } mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Data nesting is not supported") } mutating func superDecoder() throws -> any Decoder { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Data nesting is not supported") } } func singleValueContainer() throws -> any SingleValueDecodingContainer { return self } func decodeNil() -> Bool { self.data.value == nil } func decode(_: T.Type) throws -> T where T: Decodable { // Recurse back into the data decoder, don't repeat its logic here. return try self.dataDecoder.decode(T.self, from: self.data) } } @available(*, deprecated, renamed: "json") public var jsonDecoder: JSONDecoder { return self.json as! JSONDecoder } } ================================================ FILE: Sources/PostgresKit/Deprecations/PostgresDataEncoder.swift ================================================ import Foundation import PostgresNIO @available(*, deprecated, message: "Use `PostgresJSONEncoder` and `PostgresEncodable` instead.") public final class PostgresDataEncoder { public let json: any PostgresJSONEncoder public init(json: any PostgresJSONEncoder = PostgresNIO._defaultJSONEncoder) { self.json = json } public func encode(_ value: any Encodable) throws -> PostgresData { if let custom = value as? any PostgresDataConvertible, let data = custom.postgresData { return data } else { let encoder = _Encoder(parent: self) do { try value.encode(to: encoder) switch encoder.value { case .invalid: throw _Encoder.AssociativeValueSentinel() // this is usually "nothing was encoded at all", not an associative value, but the desired action is the same case .scalar(let scalar): return scalar case .indexed(let indexed): let elementType = indexed.contents.first?.type ?? .jsonb assert(indexed.contents.allSatisfy { $0.type == elementType }, "Type \(type(of: value)) was encoded as a heterogenous array; this is unsupported.") return PostgresData(array: indexed.contents, elementType: elementType) } } catch is _Encoder.AssociativeValueSentinel { return try PostgresData(jsonb: self.json.encode(value)) } } } private final class _Encoder: Encoder { struct AssociativeValueSentinel: Error {} enum Value { final class RefArray { var contents: [T] = [] } case invalid, indexed(RefArray), scalar(PostgresData) var isValid: Bool { if case .invalid = self { return false }; return true } mutating func requestIndexed(for encoder: _Encoder) { switch self { case .scalar(_): preconditionFailure("Invalid request for both single-value and unkeyed containers from the same encoder.") case .invalid: self = .indexed(.init()) // no existing value, make new array case .indexed(_): break // existing array, adopt it for appending (support for superEncoder()) } } mutating func storeScalar(_ scalar: PostgresData) { switch self { case .indexed(_), .scalar(_): preconditionFailure("Invalid request for multiple containers from the same encoder.") case .invalid: self = .scalar(scalar) // no existing value, store the incoming } } var indexedCount: Int { switch self { case .invalid, .scalar(_): preconditionFailure("Internal error in encoder (requested indexed count from non-indexed state)") case .indexed(let ref): return ref.contents.count } } mutating func addToIndexed(_ scalar: PostgresData) { switch self { case .invalid, .scalar(_): preconditionFailure("Internal error in encoder (attempted store to indexed in non-indexed state)") case .indexed(let ref): ref.contents.append(scalar) } } } var userInfo: [CodingUserInfoKey : Any] { [:] }; var codingPath: [any CodingKey] { [] } var parent: PostgresDataEncoder, value: Value init(parent: PostgresDataEncoder, value: Value = .invalid) { (self.parent, self.value) = (parent, value) } func container(keyedBy: K.Type) -> KeyedEncodingContainer { precondition(!self.value.isValid, "Requested multiple containers from the same encoder.") return .init(_FailingKeyedContainer()) } func unkeyedContainer() -> any UnkeyedEncodingContainer { self.value.requestIndexed(for: self) return _UnkeyedValueContainer(encoder: self) } func singleValueContainer() -> any SingleValueEncodingContainer { precondition(!self.value.isValid, "Requested multiple containers from the same encoder.") return _SingleValueContainer(encoder: self) } struct _UnkeyedValueContainer: UnkeyedEncodingContainer { let encoder: _Encoder; var codingPath: [any CodingKey] { self.encoder.codingPath } var count: Int { self.encoder.value.indexedCount } mutating func encodeNil() throws { self.encoder.value.addToIndexed(.null) } mutating func encode(_ value: T) throws { self.encoder.value.addToIndexed(try self.encoder.parent.encode(value)) } mutating func nestedContainer(keyedBy: K.Type) -> KeyedEncodingContainer { self.superEncoder().container(keyedBy: K.self) } mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { self.superEncoder().unkeyedContainer() } mutating func superEncoder() -> any Encoder { _Encoder(parent: self.encoder.parent, value: self.encoder.value) } // NOT the same as self.encoder } struct _SingleValueContainer: SingleValueEncodingContainer { let encoder: _Encoder; var codingPath: [any CodingKey] { self.encoder.codingPath } func encodeNil() throws { self.encoder.value.storeScalar(.null) } func encode(_ value: T) throws { self.encoder.value.storeScalar(try self.encoder.parent.encode(value)) } } /// This pair of types is only necessary because we can't directly throw an error from various Encoder and /// encoding container methods. We define duplicate types rather than the old implementation's use of a /// no-action keyed container because it can save a significant amount of time otherwise spent uselessly calling /// nested methods in some cases. struct _TaintedEncoder: Encoder, UnkeyedEncodingContainer, SingleValueEncodingContainer { var userInfo: [CodingUserInfoKey : Any] { [:] }; var codingPath: [any CodingKey] { [] }; var count: Int { 0 } func container(keyedBy: K.Type) -> KeyedEncodingContainer { .init(_FailingKeyedContainer()) } func nestedContainer(keyedBy: K.Type) -> KeyedEncodingContainer { .init(_FailingKeyedContainer()) } func unkeyedContainer() -> any UnkeyedEncodingContainer { self } func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { self } func singleValueContainer() -> any SingleValueEncodingContainer { self } func superEncoder() -> any Encoder { self } func encodeNil() throws { throw AssociativeValueSentinel() } func encode(_: T) throws { throw AssociativeValueSentinel() } } struct _FailingKeyedContainer: KeyedEncodingContainerProtocol { var codingPath: [any CodingKey] { [] } func encodeNil(forKey: K) throws { throw AssociativeValueSentinel() } func encode(_: T, forKey: K) throws { throw AssociativeValueSentinel() } func nestedContainer(keyedBy: NK.Type, forKey: K) -> KeyedEncodingContainer { .init(_FailingKeyedContainer()) } func nestedUnkeyedContainer(forKey: K) -> any UnkeyedEncodingContainer { _TaintedEncoder() } func superEncoder() -> any Encoder { _TaintedEncoder() } func superEncoder(forKey: K) -> any Encoder { _TaintedEncoder() } } } } ================================================ FILE: Sources/PostgresKit/Deprecations/PostgresDatabase+SQL+Deprecated.swift ================================================ import Foundation import PostgresNIO import SQLKit @available(*, deprecated, message: "Use `.sql(jsonEncoder:jsonDecoder:)` instead.") extension PostgresDatabase { @inlinable public func sql(encoder: PostgresDataEncoder) -> any SQLDatabase { self.sql(encoder: encoder, decoder: .init()) } @inlinable public func sql(decoder: PostgresDataDecoder) -> any SQLDatabase { self.sql(encoder: .init(), decoder: decoder) } @inlinable public func sql(encoder: PostgresDataEncoder, decoder: PostgresDataDecoder) -> any SQLDatabase { self.sql( encodingContext: .init(jsonEncoder: TypeErasedPostgresJSONEncoder(json: encoder.json)), decodingContext: .init(jsonDecoder: TypeErasedPostgresJSONDecoder(json: decoder.json)) ) } } extension PostgresRow { @available(*, deprecated, message: "Use `.sql(jsonDecoder:)` instead.") @inlinable public func sql(decoder: PostgresDataDecoder) -> any SQLRow { self.sql(decodingContext: .init(jsonDecoder: TypeErasedPostgresJSONDecoder(json: decoder.json))) } } @usableFromInline struct TypeErasedPostgresJSONDecoder: PostgresJSONDecoder { let json: any PostgresJSONDecoder @usableFromInline init(json: any PostgresJSONDecoder) { self.json = json } @usableFromInline func decode(_: T.Type, from data: Data) throws -> T { try self.json.decode(T.self, from: data) } @usableFromInline func decode(_: T.Type, from buffer: ByteBuffer) throws -> T { try self.json.decode(T.self, from: buffer) } } @usableFromInline struct TypeErasedPostgresJSONEncoder: PostgresJSONEncoder { let json: any PostgresJSONEncoder @usableFromInline init(json: any PostgresJSONEncoder) { self.json = json } @usableFromInline func encode(_ value: T) throws -> Data { try self.json.encode(value) } @usableFromInline func encode(_ value: T, into buffer: inout ByteBuffer) throws { try self.json.encode(value, into: &buffer) } } ================================================ FILE: Sources/PostgresKit/Docs.docc/PostgresKit.md ================================================ # ``PostgresKit`` @Metadata { @TitleHeading(Package) } PostgresKit is an [SQLKit] driver for PostgreSQL clients. ## Overview PostgresKit supports building and serializing Postgres-dialect SQL queries using [SQLKit]'s API. PostgresKit uses [PostgresNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. > Important: It is strongly recommended that users who leverage PostgresKit directly (e.g. absent the Fluent ORM layer) take advantage of PostgresNIO's [PostgresClient] API for connection management rather than relying upon the legacy AsyncKit API. ### Usage Reference this package in your `Package.swift` to include it in your project. ```swift .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0") ``` ### Supported Platforms PostgresKit supports the following platforms: - Ubuntu 20.04+ - macOS 10.15+ ### Configuration Database connection options and credentials are specified using a ``SQLPostgresConfiguration`` struct. ```swift import PostgresKit let configuration = SQLPostgresConfiguration( hostname: "localhost", username: "vapor_username", password: "vapor_password", database: "vapor_database" ) ``` URL-based configuration is also supported. ```swift 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 = SQLPostgresConfiguration( unixDomainSocketPath: "/path/to/socket", username: "vapor_username", password: "vapor_password", database: "vapor_database" ) ``` ### 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. ```swift let eventLoopGroup: EventLoopGroup = NIOSingletons.posixEventLoopGroup let pools = EventLoopGroupConnectionPool( source: PostgresConnectionSource(configuration: configuration), on: eventLoopGroup ) // When you're done: try await pools.shutdownAsync() ``` First create a ``PostgresConnectionSource`` using the configuration struct. This type is responsible for creating new connections to your database server as needed. Next, use the connection source to create an `EventLoopGroupConnectionPool`. You will also need to pass an `EventLoopGroup`. For more information on creating an `EventLoopGroup`, visit [SwiftNIO's documentation]. Make sure to shutdown the connection pool before it deinitializes. `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 in print(conn) // PostgresConnection on randomly chosen event loop } ``` To get a pool for a specific event loop, use `pool(for:)`. This returns an `EventLoopConnectionPool`. ```swift let eventLoop: EventLoop = ... let pool = pools.pool(for: eventLoop) pool.withConnection { conn in print(conn) // PostgresConnection on eventLoop } ``` ### PostgresDatabase Both `EventLoopGroupConnectionPool` and `EventLoopConnectionPool` can be used to create instances of `PostgresDatabase`. ```swift let postgres = pool.database(logger: ...) // PostgresDatabase let rows = try await postgres.simpleQuery("SELECT version()") ``` Visit [PostgresNIO's docs] for more information on using `PostgresDatabase`. ### SQLDatabase A `PostgresDatabase` can be used to create an instance of `SQLDatabase`. ```swift let sql = postgres.sql() // SQLDatabase let planets = try await sql.select().column("*").from("planets").all() ``` Visit [SQLKit's docs] for more information on using `SQLDatabase`. [SQLKit]: https://github.com/vapor/sql-kit [SQLKit's docs]: https://api.vapor.codes/sqlkit/documentation/sqlkit [PostgresNIO]: https://github.com/vapor/postgres-nio [PostgresNIO's docs]: https://api.vapor.codes/postgresnio/documentation/postgresnio [AsyncKit]: https://github.com/vapor/async-kit [PostgresClient]: https://api.vapor.codes/postgresnio/documentation/postgresnio/postgresclient [SwiftNIO's documentation]: https://swiftpackageindex.com/apple/swift-nio/documentation/nio ================================================ FILE: Sources/PostgresKit/Docs.docc/theme-settings.json ================================================ { "theme": { "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" }, "color": { "psqlkit": "#336791", "documentation-intro-fill": "radial-gradient(circle at top, var(--color-psqlkit) 30%, #000 100%)", "documentation-intro-accent": "var(--color-psqlkit)", "hero-eyebrow": "white", "documentation-intro-figure": "white", "hero-title": "white", "logo-base": { "dark": "#fff", "light": "#000" }, "logo-shape": { "dark": "#000", "light": "#fff" }, "fill": { "dark": "#000", "light": "#fff" } }, "icons": { "technology": "/postgreskit/images/PostgresKit/vapor-postgreskit-logo.svg" } }, "features": { "quickNavigation": { "enable": true }, "i18n": { "enable": true } } } ================================================ FILE: Sources/PostgresKit/Exports.swift ================================================ @_documentation(visibility: internal) @_exported import AsyncKit @_documentation(visibility: internal) @_exported import PostgresNIO @_documentation(visibility: internal) @_exported import SQLKit @_documentation(visibility: internal) @_exported import struct Foundation.URL ================================================ FILE: Sources/PostgresKit/PostgresConnectionSource.swift ================================================ import AsyncKit import Logging import NIOConcurrencyHelpers import NIOCore import NIOSSL import PostgresNIO import SQLKit public struct PostgresConnectionSource: ConnectionPoolSource { public let sqlConfiguration: SQLPostgresConfiguration private static let idGenerator = NIOLockedValueBox(0) public init(sqlConfiguration: SQLPostgresConfiguration) { self.sqlConfiguration = sqlConfiguration } public func makeConnection( logger: Logger, on eventLoop: any EventLoop ) -> EventLoopFuture { let connectionFuture = PostgresConnection.connect( on: eventLoop, configuration: self.sqlConfiguration.coreConfiguration, id: Self.idGenerator.withLockedValue { $0 += 1 return $0 }, logger: logger ) if let searchPath = self.sqlConfiguration.searchPath { return connectionFuture.flatMap { conn in conn.sql(queryLogLevel: nil) .raw("SET search_path TO \(idents: searchPath, joinedBy: ",")") .run() .map { _ in conn } } } else { return connectionFuture } } } extension PostgresNIO.PostgresConnection: AsyncKit.ConnectionPoolItem {} ================================================ FILE: Sources/PostgresKit/PostgresDataTranslation.swift ================================================ import Foundation import PostgresNIO /// Quick and dirty `CodingKey`, borrowed from FluentKit. If `CodingKeyRepresentable` wasn't broken by design /// (specifically, it can't be back-deployed before macOS 12.3 etc., even though it was introduced in Swift 5.6), /// we'd use that instead. private struct SomeCodingKey: CodingKey, Hashable { let stringValue: String, intValue: Int? init(stringValue: String) { (self.stringValue, self.intValue) = (stringValue, Int(stringValue)) } init(intValue: Int) { (self.stringValue, self.intValue) = ("\(intValue)", intValue) } } extension PostgresCell { fileprivate var codingKey: any CodingKey { PostgresKit.SomeCodingKey(stringValue: !self.columnName.isEmpty ? "\(self.columnName) (\(self.columnIndex))" : "\(self.columnIndex)") } } /// Sidestep problems with URL coding behavior by making it conform directly to Postgres coding. extension URL { public static var psqlType: PostgresDataType { String.psqlType } public static var psqlFormat: PostgresFormat { String.psqlFormat } @inlinable public func encode( into byteBuffer: inout ByteBuffer, context: PostgresEncodingContext ) { self.absoluteString.encode(into: &byteBuffer, context: context) } @inlinable public init( from buffer: inout ByteBuffer, type: PostgresDataType, format: PostgresFormat, context: PostgresDecodingContext ) throws { let string = try String(from: &buffer, type: type, format: format, context: context) if let url = URL(string: string) { self = url } // Also support the broken encoding we were emitting for awhile there. else if string.hasPrefix("\""), string.hasSuffix("\""), let url = URL(string: String(string.dropFirst().dropLast())) { self = url } else { throw PostgresDecodingError.Code.failure } } } extension URL: @retroactive PostgresNonThrowingEncodable, @retroactive PostgresDecodable {} struct PostgresDataTranslation { /// This typealias serves to limit the deprecation noise caused by `PostgresDataConvertible` to a single /// warning, down from what would otherwise be a minimum of two. It has no other purpose. fileprivate typealias PostgresLegacyDataConvertible = PostgresDataConvertible static func decode( _: T.Type = T.self, from cell: PostgresCell, in context: PostgresDecodingContext, file: String = #fileID, line: Int = #line ) throws -> T { try self.decode( codingPath: [cell.codingKey], userInfo: [:], T.self, from: cell, in: context, file: file, line: line ) } fileprivate static func decode( codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any], _: T.Type = T.self, from cell: PostgresCell, in context: PostgresDecodingContext, file: String, line: Int ) throws -> T { /// Preferred modern fast-path: Direct conformance to `PostgresDecodable`, let the cell decode. if let fastPathType = T.self as? any PostgresDecodable.Type { let cellToDecode: PostgresCell if cell.dataType.isUserDefined && (T.self is String.Type || T.self is String?.Type) { /// Workaround for Fluent's enum "support": /// /// If we're trying to decode a string and the real cell's data type is in the user-defined range, /// assume we're dealing with a Fluent enum and pretend that the cell has a string data type instead. cellToDecode = .init( bytes: cell.bytes, dataType: .name, format: cell.format, columnName: cell.columnName, columnIndex: cell.columnIndex ) } else if cell.format == .binary && [.char, .varchar, .text].contains(cell.dataType) && T.self is Decimal.Type { /// Workaround for Fluent's assumption that Decimal strings work: /// /// If the cell's data type is a binary-format string-like, and we're trying to decode a `Decimal`, /// reinterpret the cell as a text-format numeric value so that the `PostgresCodable` conformance of /// `Decimal` will work as written. cellToDecode = .init( bytes: cell.bytes, dataType: .numeric, format: .text, columnName: cell.columnName, columnIndex: cell.columnIndex ) } else if cell.format == .binary && cell.dataType == .numeric && T.self is Double.Type { /// Workaround for Fluent's expectation that Postgres's `numeric/decimal` can be decoded as `Double`: /// /// If the cell is a binary-format numeric value and we're trying to decode a `Double`, use /// `PostgresData` to manually interpret the cell as a `PostgresNumeric` and use that result to convert /// to `Double`. guard let value = PostgresData(type: cell.dataType, formatCode: cell.format, value: cell.bytes).numeric?.double else { throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Invalid numeric value encoding")) } return value as! T } else { /// No workarounds needed, use the cell as-is. cellToDecode = cell } return try cellToDecode.decode(fastPathType, context: context, file: file, line: line) as! T /// Legacy "fast"-path: Direct conformance to `PostgresDataConvertible`; use is deprecated. } else if let legacyPathType = T.self as? any PostgresLegacyDataConvertible.Type { let legacyData = PostgresData(type: cell.dataType, typeModifier: nil, formatCode: cell.format, value: cell.bytes) guard let result = legacyPathType.init(postgresData: legacyData) else { throw DecodingError.typeMismatch(T.self, .init(codingPath: codingPath, debugDescription: "Couldn't get '\(T.self)' from PSQL type \(cell.dataType): \(legacyData as Any)" )) } return result as! T } /// Slow path: Descend through the `Decodable` machinery until we fail or find something we can convert. else { do { return try T.init(from: ArrayAwareBoxUwrappingDecoder( codingPath: codingPath, userInfo: userInfo, cell: cell, context: context, file: file, line: line )) } 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) 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 { // 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()) } } catch let error as PostgresDecodingError { /// We effectively transform PostgresDecodingErrors into plain DecodingErrors here, mostly so the full /// coding path, which gives us the original type(s) involved, is preserved. let context = DecodingError.Context( codingPath: codingPath, debugDescription: "\(String(reflecting: error))", underlyingError: error ) switch error.code { case .typeMismatch: throw DecodingError.typeMismatch(T.self, context) case .missingData: throw DecodingError.valueNotFound(T.self, context) default: throw DecodingError.dataCorrupted(context) } } } } static func encode( value: T, in context: PostgresEncodingContext, to bindings: inout PostgresBindings, file: String = #fileID, line: Int = #line ) throws { /// Preferred modern fast-path: Direct conformance to `PostgresEncodable` if let fastPathValue = value as? any PostgresEncodable { try bindings.append(fastPathValue, context: context) } /// 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)' of Swift type \(T.self)/\(type(of: value))")) } bindings.append(legacyData) } /// Slow path: Descend through the `Encodable` machinery until we fail or find something we can convert. else { try bindings.append(self.encode(codingPath: [], userInfo: [:], value: value, in: context, file: file, line: line)) } } internal /*fileprivate*/ static func encode( codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any], value: T, in context: PostgresEncodingContext, file: String, line: Int ) throws -> PostgresData { // TODO: Avoid repeating the conformance checks here, or at the very least only repeat them after a second level of nesting... if let fastPathValue = value as? any PostgresEncodable { var buffer = ByteBuffer() try fastPathValue.encode(into: &buffer, context: context) 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)' of Swift type \(T.self)/\(type(of: value))")) } return legacyData } // TODO: Make all of this work without relying on the legacy PostgresData array machinery do { let encoder = ArrayAwareBoxWrappingPostgresEncoder(codingPath: codingPath, userInfo: userInfo, context: context, file: file, line: line) try value.encode(to: encoder) switch encoder.value { case .invalid: throw ArrayAwareBoxWrappingPostgresEncoder.FallbackSentinel() 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 \(T.self)/\(type(of: value)) was encoded as a heterogenous array; this is unsupported.") return PostgresData(array: ref.contents, elementType: elementType) } } catch is ArrayAwareBoxWrappingPostgresEncoder.FallbackSentinel { /// Glacial path: Fall back to encoding directly to JSON. return try PostgresData(jsonb: context.jsonEncoder.encode(value)) } } } private final class ArrayAwareBoxUwrappingDecoder: Decoder, SingleValueDecodingContainer { let codingPath: [any CodingKey] let userInfo: [CodingUserInfoKey: Any] let cell: PostgresCell let context: PostgresDecodingContext let file: String, line: Int init(codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any], cell: PostgresCell, context: PostgresDecodingContext, file: String, line: Int) { self.codingPath = codingPath self.cell = cell self.context = context self.file = file self.line = line self.userInfo = userInfo } struct ArrayContainer: UnkeyedDecodingContainer { let data: [PostgresData] let decoder: ArrayAwareBoxUwrappingDecoder var codingPath: [any CodingKey] { self.decoder.codingPath } var count: Int? { self.data.count } var isAtEnd: Bool { self.currentIndex >= self.data.count } var currentIndex = 0 mutating func decodeNil() throws -> Bool { guard self.data[self.currentIndex].value == nil else { return false } self.currentIndex += 1 return true } mutating func decode(_: T.Type) throws -> T { // TODO: Don't fake a cell. let data = self.data[self.currentIndex], cell = PostgresCell( bytes: data.value, dataType: data.type, format: data.formatCode, columnName: self.decoder.cell.columnName, columnIndex: self.decoder.cell.columnIndex ) let result = try PostgresDataTranslation.decode( codingPath: self.codingPath + [PostgresKit.SomeCodingKey(intValue: self.currentIndex)], userInfo: self.decoder.userInfo, T.self, from: cell, in: self.decoder.context, file: self.decoder.file, line: self.decoder.line ) self.currentIndex += 1 return result } private var rejectNestingError: DecodingError { .dataCorruptedError(in: self, debugDescription: "Data nesting is not supported") } mutating func nestedContainer(keyedBy: K.Type) throws -> KeyedDecodingContainer { throw self.rejectNestingError } mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { throw self.rejectNestingError } mutating func superDecoder() throws -> any Decoder { throw self.rejectNestingError } } func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer { throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Dictionary containers must be JSON-encoded")) } func unkeyedContainer() throws -> any UnkeyedDecodingContainer { // TODO: Find a better way to figure out arrays guard let array = PostgresData(type: self.cell.dataType, typeModifier: nil, formatCode: self.cell.format, value: self.cell.bytes).array else { throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Non-natively typed arrays must be JSON-encoded")) } return ArrayContainer(data: array, decoder: self) } func singleValueContainer() throws -> any SingleValueDecodingContainer { self } func decodeNil() -> Bool { self.cell.bytes == nil } func decode(_: T.Type) throws -> T { try PostgresDataTranslation.decode( codingPath: self.codingPath + [PostgresKit.SomeCodingKey(stringValue: "(Unwrapping(\(T0.self)))")], userInfo: self.userInfo, T.self, from: self.cell, in: self.context, file: self.file, line: self.line ) } } private final class ArrayAwareBoxWrappingPostgresEncoder: Encoder, SingleValueEncodingContainer { enum Value { final class ArrayRef { var contents: [T] = [] } case invalid case indexed(ArrayRef) case scalar(PostgresData) var isValid: Bool { if case .invalid = self { return false }; return true } mutating func store(scalar: PostgresData) { if case .invalid = self { self = .scalar(scalar) } // no existing value, store the incoming else { preconditionFailure("Invalid request for multiple containers from the same encoder.") } } mutating func requestIndexed() { switch self { case .scalar(_): preconditionFailure("Invalid request for both single-value and unkeyed containers from the same encoder.") case .invalid: self = .indexed(.init()) // no existing value, make new array case .indexed(_): break // existing array, adopt it for appending (support for superEncoder()) } } var indexedCount: Int { if case .indexed(let ref) = self { return ref.contents.count } else { preconditionFailure("Internal error in encoder (requested indexed count from non-indexed state)") } } mutating func store(indexedScalar: PostgresData) { if case .indexed(let ref) = self { ref.contents.append(indexedScalar) } else { preconditionFailure("Internal error in encoder (attempted store to indexed in non-indexed state)") } } } var codingPath: [any CodingKey] let userInfo: [CodingUserInfoKey: Any] let context: PostgresEncodingContext let file: String, line: Int var value: Value init(codingPath: [any CodingKey], userInfo: [CodingUserInfoKey: Any], context: PostgresEncodingContext, file: String, line: Int, value: Value = .invalid) { self.codingPath = codingPath self.userInfo = userInfo self.context = context self.file = file self.line = line self.value = value } func container(keyedBy: K.Type) -> KeyedEncodingContainer { precondition(!self.value.isValid, "Requested multiple containers from the same encoder.") return .init(FailureEncoder()) } func unkeyedContainer() -> any UnkeyedEncodingContainer { self.value.requestIndexed() return ArrayContainer(encoder: self) } func singleValueContainer() -> any SingleValueEncodingContainer { precondition(!self.value.isValid, "Requested multiple containers from the same encoder.") return self } struct ArrayContainer: UnkeyedEncodingContainer { let encoder: ArrayAwareBoxWrappingPostgresEncoder var codingPath: [any CodingKey] { self.encoder.codingPath } var count: Int { self.encoder.value.indexedCount } mutating func encodeNil() throws { self.encoder.value.store(indexedScalar: .null) } mutating func encode(_ value: T) throws { self.encoder.value.store(indexedScalar: try PostgresDataTranslation.encode( codingPath: self.codingPath + [PostgresKit.SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo, value: value, in: self.encoder.context, file: self.encoder.file, line: self.encoder.line )) } mutating func nestedContainer(keyedBy: K.Type) -> KeyedEncodingContainer { self.superEncoder().container(keyedBy: K.self) } mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { self.superEncoder().unkeyedContainer() } mutating func superEncoder() -> any Encoder { ArrayAwareBoxWrappingPostgresEncoder( codingPath: self.codingPath + [PostgresKit.SomeCodingKey(intValue: self.count)], userInfo: self.encoder.userInfo, context: self.encoder.context, file: self.encoder.file, line: self.encoder.line, value: self.encoder.value ) } // NOT the same as self.encoder } func encodeNil() throws { self.value.store(scalar: .null) } func encode(_ value: T) throws { self.value.store(scalar: try PostgresDataTranslation.encode( codingPath: self.codingPath, userInfo: self.userInfo, value: value, in: self.context, file: self.file, line: self.line )) } struct FallbackSentinel: Error {} /// This is a workaround for the inability of encoders to throw errors in various places. It's still better than fatalError()ing. struct FailureEncoder: Encoder, KeyedEncodingContainerProtocol, UnkeyedEncodingContainer, SingleValueEncodingContainer { let codingPath = [any CodingKey](), userInfo = [CodingUserInfoKey: Any](), count = 0 init() {}; init() where K == PostgresKit.SomeCodingKey {} func encodeNil() throws { throw FallbackSentinel() } func encodeNil(forKey: K) throws { throw FallbackSentinel() } func encode(_: T) throws { throw FallbackSentinel() } func encode(_: T, forKey: K) throws { throw FallbackSentinel() } func nestedContainer(keyedBy: N.Type) -> KeyedEncodingContainer { .init(FailureEncoder()) } func nestedContainer(keyedBy: N.Type, forKey: K) -> KeyedEncodingContainer { .init(FailureEncoder()) } func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { self } func nestedUnkeyedContainer(forKey: K) -> any UnkeyedEncodingContainer { self } func superEncoder() -> any Encoder { self } func superEncoder(forKey: K) -> any Encoder { self } func container(keyedBy: N.Type) -> KeyedEncodingContainer { .init(FailureEncoder()) } func unkeyedContainer() -> any UnkeyedEncodingContainer { self } func singleValueContainer() -> any SingleValueEncodingContainer { self } } } ================================================ FILE: Sources/PostgresKit/PostgresDatabase+SQL.swift ================================================ import Logging import PostgresNIO import SQLKit extension PostgresDatabase { @inlinable public func sql(queryLogLevel: Logger.Level? = .debug) -> some SQLDatabase { self.sql(encodingContext: .default, decodingContext: .default, queryLogLevel: queryLogLevel) } public func sql( encodingContext: PostgresEncodingContext, decodingContext: PostgresDecodingContext, queryLogLevel: Logger.Level? = .debug ) -> some SQLDatabase { PostgresSQLDatabase( database: self, encodingContext: encodingContext, decodingContext: decodingContext, queryLogLevel: queryLogLevel ) } } private struct PostgresSQLDatabase { let database: PDatabase let encodingContext: PostgresEncodingContext let decodingContext: PostgresDecodingContext let queryLogLevel: Logger.Level? } extension PostgresSQLDatabase: SQLDatabase, PostgresDatabase { var logger: Logger { self.database.logger } var eventLoop: any EventLoop { self.database.eventLoop } var version: (any SQLDatabaseReportedVersion)? { nil // PSQL doesn't send version in wire protocol, must use SQL to read it } var dialect: any SQLDialect { PostgresDialect() } func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void) -> EventLoopFuture { let (sql, binds) = self.serialize(query) if let queryLogLevel = self.queryLogLevel { self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(binds.map { .string("\($0)") })]) } return self.eventLoop.makeCompletedFuture { var bindings = PostgresBindings(capacity: binds.count) for bind in binds { try PostgresDataTranslation.encode(value: bind, in: self.encodingContext, to: &bindings) } return bindings }.flatMap { bindings in self.database.withConnection { $0.query( .init(unsafeSQL: sql, binds: bindings), logger: $0.logger, { onRow($0.sql(decodingContext: self.decodingContext)) } ) } }.map { _ in } } func execute( sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void ) async throws { let (sql, binds) = self.serialize(query) if let queryLogLevel = self.queryLogLevel { self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(binds.map { .string("\($0)") })]) } var bindings = PostgresBindings(capacity: binds.count) for bind in binds { try PostgresDataTranslation.encode(value: bind, in: self.encodingContext, to: &bindings) } _ = try await self.database.withConnection { $0.query( .init(unsafeSQL: sql, binds: bindings), logger: $0.logger, { onRow($0.sql(decodingContext: self.decodingContext)) } ) }.get() } func send(_ request: any PostgresRequest, logger: Logger) -> EventLoopFuture { self.database.send(request, logger: logger) } func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture { self.database.withConnection(closure) } func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { try await self.withConnection { c in c.eventLoop.makeFutureWithTask { try await closure(c.sql( encodingContext: self.encodingContext, decodingContext: self.decodingContext, queryLogLevel: self.queryLogLevel )) } }.get() } } ================================================ FILE: Sources/PostgresKit/PostgresDialect.swift ================================================ import SQLKit public struct PostgresDialect: SQLDialect { public init() {} public var name: String { "postgresql" } public var identifierQuote: any SQLExpression { SQLRaw(#"""#) } public var literalStringQuote: any SQLExpression { SQLRaw("'") } public var supportsAutoIncrement: Bool { true } public var autoIncrementClause: any SQLExpression { SQLRaw("GENERATED BY DEFAULT AS IDENTITY") } public var autoIncrementFunction: (any SQLExpression)? { nil } public func bindPlaceholder(at position: Int) -> any SQLExpression { SQLRaw("$\(position)") } public func literalBoolean(_ value: Bool) -> any SQLExpression { SQLRaw("\(value)") } public var literalDefault: any SQLExpression { SQLRaw("DEFAULT") } public var supportsIfExists: Bool { true } public var enumSyntax: SQLEnumSyntax { .typeName } public var supportsDropBehavior: Bool { true } public var supportsReturning: Bool { true } public var triggerSyntax: SQLTriggerSyntax { .init( create: [.supportsForEach, .postgreSQLChecks, .supportsCondition, .conditionRequiresParentheses, .supportsConstraints], drop: [.supportsCascade, .supportsTableName] ) } public var alterTableSyntax: SQLAlterTableSyntax { .init( alterColumnDefinitionClause: SQLRaw("ALTER COLUMN"), alterColumnDefinitionTypeKeyword: SQLRaw("SET DATA TYPE") ) } public func customDataType(for dataType: SQLDataType) -> (any SQLExpression)? { if case .custom(let expr) = dataType, (expr as? SQLRaw)?.sql == "TIMESTAMP" { return SQLRaw("TIMESTAMPTZ") } else if case .blob = dataType { return SQLRaw("BYTEA") } else { return nil } } public var upsertSyntax: SQLUpsertSyntax { .standard } public var unionFeatures: SQLUnionFeatures { [ .union, .unionAll, .intersect, .intersectAll, .except, .exceptAll, .explicitDistinct, .parenthesizedSubqueries, ] } public var sharedSelectLockExpression: (any SQLExpression)? { SQLRaw("FOR SHARE") } public var exclusiveSelectLockExpression: (any SQLExpression)? { SQLRaw("FOR UPDATE") } public func nestedSubpathExpression(in column: any SQLExpression, for path: [String]) -> (any SQLExpression)? { guard !path.isEmpty else { return nil } let descender = SQLList( [column] + path.dropLast().map(SQLLiteral.string(_:)), separator: SQLRaw("->") ) let accessor = SQLList( [descender, SQLLiteral.string(path.last!)], separator: SQLRaw("->>") ) return SQLGroupExpression(accessor) } } ================================================ FILE: Sources/PostgresKit/PostgresRow+SQL.swift ================================================ import Foundation import PostgresNIO import SQLKit extension PostgresRow { @inlinable public func sql() -> some SQLRow { self.sql(decodingContext: .default) } public func sql(decodingContext: PostgresDecodingContext) -> some SQLRow { _PostgresSQLRow(randomAccessView: self.makeRandomAccess(), decodingContext: decodingContext) } } private struct _PostgresSQLRow { let randomAccessView: PostgresRandomAccessRow let decodingContext: PostgresDecodingContext enum _Error: Error { case missingColumn(String) } } extension _PostgresSQLRow: SQLRow { var allColumns: [String] { self.randomAccessView.map { $0.columnName } } func contains(column: String) -> Bool { self.randomAccessView.contains(column) } func decodeNil(column: String) throws -> Bool { !self.randomAccessView.contains(column) || self.randomAccessView[column].bytes == nil } func decode(column: String, as type: T.Type) throws -> T { guard self.randomAccessView.contains(column) else { throw _Error.missingColumn(column) } return try PostgresDataTranslation.decode(T.self, from: self.randomAccessView[column], in: self.decodingContext) } } ================================================ FILE: Sources/PostgresKit/SQLPostgresConfiguration.swift ================================================ import Foundation import NIOCore import NIOSSL import PostgresNIO /// Provides configuration paramters for establishing PostgreSQL database connections. public struct SQLPostgresConfiguration: Sendable { /// IANA-assigned port number for PostgreSQL /// `UInt16(getservbyname("postgresql", "tcp").pointee.s_port).byteSwapped` public static var ianaPortNumber: Int { 5432 } // See `PostgresNIO.PostgresConnection.Configuration`. public var coreConfiguration: PostgresConnection.Configuration /// Optional `search_path` to set on new connections. public var searchPath: [String]? /// Create a ``SQLPostgresConfiguration`` from a string containing a properly formatted URL. /// /// See ``init(url:)`` for details on the allowed format for connection URLs. public init(url: String) throws { guard let url = URL(string: url) else { throw URLError(.badURL, userInfo: [NSURLErrorFailingURLStringErrorKey: url]) } try self.init(url: url) } /// Create a ``SQLPostgresConfiguration`` from a properly formatted URL. /// /// The supported URL formats are: /// /// postgres://username:password@hostname:port/database?tlsmode=mode /// postgres+tcp://username:password@hostname:port/database?tlsmode=mode /// postgres+uds://username:password@localhost/path?tlsmode=mode#database /// /// The `postgres+tcp` scheme requests a connection over TCP. The `postgres` scheme is an alias /// for `postgres+tcp`. Only the `hostname` and `username` components are required. /// /// The `postgres+uds` scheme requests a connection via a UNIX domain socket. The `username` and /// `path` components are required. The authority must always be empty or `localhost`, and may not /// specify a port. /// /// The allowed `mode` values for `tlsmode` are: /// /// Value|Behavior /// -|- /// `disable`|Don't use TLS, even if the server supports it. /// `prefer`|Use TLS if possible. /// `require`|Enforce TLS support. /// /// If no `tlsmode` is specified, the default mode is `prefer` for TCP connections, or `disable` /// for UDS connections. If more than one mode is specified, the last one wins. Whenever a TLS /// connection is made, full certificate verification (both chain of trust and hostname match) /// is always enforced, regardless of the mode used. /// /// For compatibility with `libpq` and previous versions of this package, any of "`sslmode`", /// "`tls`", or "`ssl`" may be used instead of "`tlsmode`". There are also various aliases for /// each of the TLS mode names, as follows: /// /// - "`disable`": "`false`" /// - "`prefer`": "`allow`", "`true`" /// - "`require`": "`verify-ca`", "`verify-full`" /// /// The aliases always have the same semantics as the "canonical" modes, despite any differences /// suggested by their names. /// /// Also for compatibility, the URL scheme may also be `postgresql` or `postgresql+uds`. /// /// > Note: It is possible to emulate `libpq`'s definitions for `prefer` (TLS if available with /// > no certificate verification), `require` (TLS enforced, but also without certificate /// > verification) and `verify-ca` (TLS enforced with no hostname verification) by manually /// > specifying the TLS configuration instead of using a URL. It is not possible, by design, to /// > emulate `libpq`'s `allow` mode (TLS only if there is no alternative). It is _strongly_ /// > recommended for both security and privacy reasons to always leave full certificate /// > verification enabled whenever possible. See NIOSSL's [`TLSConfiguration`](tlsconfig) for /// > additional information and recommendations. /// /// [tlsconfig]: /// https://swiftpackageindex.com/apple/swift-nio-ssl/documentation/niossl/tlsconfiguration public init(url: URL) throws { guard let comp = URLComponents(url: url, resolvingAgainstBaseURL: true), let username = comp.user else { throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString]) } func decideTLSConfig(from queryItems: [URLQueryItem], defaultMode: String) throws -> PostgresConnection.Configuration.TLS { switch queryItems.last(where: { ["tlsmode", "sslmode", "ssl", "tls"].contains($0.name.lowercased()) })?.value ?? defaultMode { case "verify-full", "verify-ca", "require": return try .require(.init(configuration: .makeClientConfiguration())) case "prefer", "allow", "true": return try .prefer(.init(configuration: .makeClientConfiguration())) case "disable", "false": return .disable default: throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString]) } } switch comp.scheme { case "postgres", "postgres+tcp", "postgresql", "postgresql+tcp": guard let hostname = comp.host, !hostname.isEmpty else { throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString]) } self.init( hostname: hostname, port: comp.port ?? Self.ianaPortNumber, username: username, password: comp.password, database: url.lastPathComponent.isEmpty ? nil : url.lastPathComponent, tls: try decideTLSConfig(from: comp.queryItems ?? [], defaultMode: "prefer") ) case "postgres+uds", "postgresql+uds": guard (comp.host?.isEmpty ?? true || comp.host == "localhost"), comp.port == nil, !comp.path.isEmpty, comp.path != "/" else { throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString]) } var coreConfig = PostgresConnection.Configuration(unixSocketPath: comp.path, username: username, password: comp.password, database: comp.fragment) coreConfig.tls = try decideTLSConfig(from: comp.queryItems ?? [], defaultMode: "disable") self.init(coreConfiguration: coreConfig) default: throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString]) } } /// Create a ``SQLPostgresConfiguration`` for connecting to a server with a hostname and optional port. /// /// This specifies a TCP connection. If you're unsure which kind of connection you want, you almost /// definitely want this one. public init( hostname: String, port: Int = Self.ianaPortNumber, username: String, password: String? = nil, database: String? = nil, tls: PostgresConnection.Configuration.TLS ) { self.init(coreConfiguration: .init(host: hostname, port: port, username: username, password: password, database: database, tls: tls)) } /// Create a ``SQLPostgresConfiguration`` for connecting to a server through a UNIX domain socket. public init( unixDomainSocketPath: String, username: String, password: String? = nil, database: String? = nil ) { self.init(coreConfiguration: .init(unixSocketPath: unixDomainSocketPath, username: username, password: password, database: database)) } /// Create a ``SQLPostgresConfiguration`` for establishing a connection to a server over a /// preestablished `NIOCore.Channel`. /// /// This is provided for calling code which wants to manage the underlying connection transport on its /// own, such as when tunneling a connection through SSH. public init( establishedChannel: any Channel, username: String, password: String? = nil, database: String? = nil ) { self.init(coreConfiguration: .init(establishedChannel: establishedChannel, username: username, password: password, database: database)) } public init( coreConfiguration: PostgresConnection.Configuration, searchPath: [String]? = nil ) { self.coreConfiguration = coreConfiguration self.searchPath = searchPath } } ================================================ FILE: Tests/PostgresKitTests/PostgresKitTests.swift ================================================ import Foundation import Logging import NIOCore import PostgresNIO import SQLKitBenchmark import Testing @testable import PostgresKit extension AllSuites { @Suite struct PostgresKitTests { @Test func sqlKitBenchmark() async throws { let conn = try await PostgresConnection.test(on: self.eventLoop) await #expect(throws: Never.self) { let benchmark = SQLBenchmarker(on: conn.sql()) try await benchmark.runAllTests() } try await conn.close() } @Test func leak() async throws { struct Foo: Codable { var id: String var description: String? var latitude: Double var longitude: Double var created_by: String var created_at: Date var modified_by: String var modified_at: Date } let conn = try await PostgresConnection.test(on: self.eventLoop) let db = conn.sql() await #expect(throws: Never.self) { try await db.drop(table: "foos").ifExists().run() try await db.create(table: "foos") .column("id", type: .text, .primaryKey(autoIncrement: false)) .column("description", type: .text) .column("latitude", type: .custom(SQLRaw("DOUBLE PRECISION"))) .column("longitude", type: .custom(SQLRaw("DOUBLE PRECISION"))) .column("created_by", type: .text) .column("created_at", type: .custom(SQLRaw("TIMESTAMPTZ"))) .column("modified_by", type: .text) .column("modified_at", type: .custom(SQLRaw("TIMESTAMPTZ"))) .run() for i in 0..<5_000 { let zipcode = Foo( id: UUID().uuidString, description: "test \(i)", latitude: Double.random(in: 0...100), longitude: Double.random(in: 0...100), created_by: "test", created_at: Date(), modified_by: "test", modified_at: Date() ) try await db.insert(into: "foos") .model(zipcode) .run() } } try? await db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run() try await conn.close() } @Test func arrayEncoding() async throws { let conn = try await PostgresConnection.test(on: self.eventLoop) struct Foo: Codable { var bar: Int } await #expect(throws: Never.self) { let foos: [Foo] = [.init(bar: 1), .init(bar: 2)] try await conn.sql().raw("SELECT \(bind: foos)::JSONB[] as \(ident: "foos")").run() } try await conn.close() } @Test func decodeModelWithNil() async throws { let conn = try await PostgresConnection.test(on: self.eventLoop) await #expect(throws: Never.self) { let rows = try await conn.sql().raw("SELECT \(literal: "foo")::text as \(ident: "foo"), \(SQLLiteral.null) as \(ident: "bar"), \(literal: "baz")::text as \(ident: "baz")").all() let row = rows[0] struct Test: Codable { var foo: String var bar: String? var baz: String? } let test = try row.decode(model: Test.self) #expect(test.foo == "foo") #expect(test.bar == nil) #expect(test.baz == "baz") } try await conn.close() } @Test func eventLoopGroupSQL() async throws { var configuration = SQLPostgresConfiguration.test configuration.searchPath = ["foo", "bar", "baz"] let source = PostgresConnectionSource(sqlConfiguration: configuration) let pool = EventLoopGroupConnectionPool(source: source, on: MultiThreadedEventLoopGroup.singleton) let db = pool.database(logger: .init(label: "test")).sql() await #expect(throws: Never.self) { try await #expect(db.raw("SELECT version()").all().count == 1) } try await pool.shutdownAsync() } @Test func integerArrayEncoding() async throws { let connection = try await PostgresConnection.test(on: self.eventLoop) await #expect(throws: Never.self) { let sql = connection.sql() _ = try await sql.raw("DROP TABLE IF EXISTS \(ident: "foo")").run() try await sql.withSession { db in _ = try await db.create(table: "foo").column("bar", type: .custom(SQLRaw("bigint[]")), .notNull).run() _ = try await db.insert(into: "foo").columns("bar").values(SQLBind([Bar]())).run() let rows = try await connection.query("SELECT bar FROM foo", logger: connection.logger).collect() #expect(rows.count == 1) #expect(rows.first?.count == 1) #expect(rows.first?.first?.dataType == Bar.psqlArrayType) #expect(try rows.first?.first?.decode([Bar].self) == [Bar]()) } } try await connection.close() } /// Tests dealing with encoding of values whose `encode(to:)` implementation calls one of the `superEncoder()` /// methods (most notably the implementation of `Codable` for Fluent's `Fields`, which we can't directly test /// at this layer). @Test func valuesThatUseSuperEncoder() throws { struct UnusualType: Codable { var prop1: String, prop2: [Bool], prop3: [[Bool]] // This is intentionally contrived - Fluent's implementation does Codable this roundabout way as a // workaround for the interaction of property wrappers with optional properties; it serves no purpose // here other than to demonstrate that the encoder supports it. private enum CodingKeys: String, CodingKey { case prop1, prop2, prop3 } init(prop1: String, prop2: [Bool], prop3: [[Bool]]) { (self.prop1, self.prop2, self.prop3) = (prop1, prop2, prop3) } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.prop1 = try .init(from: container.superDecoder(forKey: .prop1)) var acontainer = try container.nestedUnkeyedContainer(forKey: .prop2), ongoing: [Bool] = [] while !acontainer.isAtEnd { ongoing.append(try Bool.init(from: acontainer.superDecoder())) } self.prop2 = ongoing var bcontainer = try container.nestedUnkeyedContainer(forKey: .prop3), bongoing: [[Bool]] = [] while !bcontainer.isAtEnd { var ccontainer = try bcontainer.nestedUnkeyedContainer(), congoing: [Bool] = [] while !ccontainer.isAtEnd { congoing.append(try Bool.init(from: ccontainer.superDecoder())) } bongoing.append(congoing) } self.prop3 = bongoing } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try self.prop1.encode(to: container.superEncoder(forKey: .prop1)) var acontainer = container.nestedUnkeyedContainer(forKey: .prop2) for val in self.prop2 { try val.encode(to: acontainer.superEncoder()) } var bcontainer = container.nestedUnkeyedContainer(forKey: .prop3) for arr in self.prop3 { var ccontainer = bcontainer.nestedUnkeyedContainer() for val in arr { try val.encode(to: ccontainer.superEncoder()) } } } } let instance = UnusualType(prop1: "hello", prop2: [true, false, false, true], prop3: [[true, true], [false], [true], []]) let encoded1 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: instance, in: .default, file: #fileID, line: #line) let encoded2 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: [instance, instance], in: .default, file: #fileID, line: #line) #expect(encoded1.type == .jsonb) #expect(encoded2.type == .jsonbArray) let decoded1 = try PostgresDataTranslation.decode(UnusualType.self, from: .init(bytes: encoded1.value, dataType: encoded1.type, format: encoded1.formatCode, columnName: "", columnIndex: -1), in: .default) let decoded2 = try PostgresDataTranslation.decode([UnusualType].self, from: .init(bytes: encoded2.value, dataType: encoded2.type, format: encoded2.formatCode, columnName: "", columnIndex: -1), in: .default) #expect(decoded1.prop3 == instance.prop3) #expect(decoded2.count == 2) } @Test func fluentWorkaroundsDecoding() throws { // SQLKit benchmarks already test enum handling // Text encoding for Decimal let decimalBuffer = ByteBuffer(string: Decimal(12345.6789).description) var decimalValue: Decimal? #expect(throws: Never.self) { decimalValue = try PostgresDataTranslation.decode(Decimal.self, from: .init(bytes: decimalBuffer, dataType: .numeric, format: .text, columnName: "", columnIndex: -1), in: .default) } #expect(decimalValue == Decimal(12345.6789)) // Decoding Double from NUMERIC let numericBuffer = PostgresData(numeric: .init(decimal: 12345.6789)).value var numericValue: Double? #expect(throws: Never.self) { numericValue = try PostgresDataTranslation.decode(Double.self, from: .init(bytes: numericBuffer, dataType: .numeric, format: .binary, columnName: "", columnIndex: -1), in: .default) } #expect(numericValue == Double(Decimal(12345.6789).description)) } @Test func urlWorkaroundDecoding() throws { let url = URL(string: "https://user:pass@www.example.com:8080/path/to/endpoint?query=value#fragment")! let encodedNormal = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: url, in: .default, file: #fileID, line: #line) #expect(encodedNormal.value?.getString(at: 0, length: encodedNormal.value?.readableBytes ?? 0) == url.absoluteString) let encodedBroken = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: "\"\(url.absoluteString)\"", in: .default, file: #fileID, line: #line) #expect(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedNormal), in: .default) == url) #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() } init() { #expect(isLoggingConfigured) } } } extension PostgresCell { fileprivate init(with data: PostgresData) { self.init(bytes: data.value, dataType: data.type, format: data.formatCode, columnName: "", columnIndex: -1) } } enum Bar: Int, Codable { case one, two } extension Bar: PostgresNonThrowingEncodable, PostgresArrayEncodable, PostgresDecodable, PostgresArrayDecodable { func encode(into byteBuffer: inout ByteBuffer, context: PostgresEncodingContext) { self.rawValue.encode(into: &byteBuffer, context: context) } init(from byteBuffer: inout ByteBuffer, type: PostgresDataType, format: PostgresFormat, context: PostgresDecodingContext) throws { guard let value = try Self.init(rawValue: Self.RawValue.init(from: &byteBuffer, type: type, format: format, context: context)) else { throw PostgresDecodingError.Code.failure } self = value } static var psqlType: PostgresDataType { .int8 } static var psqlFormat: PostgresFormat { .binary } static var psqlArrayType: PostgresDataType { .int8Array } } ================================================ FILE: Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift ================================================ import PostgresKit import Testing extension AllSuites { @Suite struct SQLPostgresConfigurationTests { @Test func urlHandling() throws { let config1 = try SQLPostgresConfiguration(url: "postgres+tcp://test_username:test_password@test_hostname:9999/test_database?tlsmode=disable") #expect(config1.coreConfiguration.database == "test_database") #expect(config1.coreConfiguration.password == "test_password") #expect(config1.coreConfiguration.username == "test_username") #expect(config1.coreConfiguration.host == "test_hostname") #expect(config1.coreConfiguration.port == 9999) #expect(config1.coreConfiguration.unixSocketPath == nil) #expect(!config1.coreConfiguration.tls.isAllowed) #expect(!config1.coreConfiguration.tls.isEnforced) let config2 = try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname") #expect(config2.coreConfiguration.database == nil) #expect(config2.coreConfiguration.password == nil) #expect(config2.coreConfiguration.username == "test_username") #expect(config2.coreConfiguration.host == "test_hostname") #expect(config2.coreConfiguration.port == SQLPostgresConfiguration.ianaPortNumber) #expect(config2.coreConfiguration.unixSocketPath == nil) #expect(config2.coreConfiguration.tls.isAllowed) #expect(!config2.coreConfiguration.tls.isEnforced) let config3 = try SQLPostgresConfiguration(url: "postgres+uds://test_username:test_password@localhost/tmp/postgres.sock?tlsmode=require#test_database") #expect(config3.coreConfiguration.database == "test_database") #expect(config3.coreConfiguration.password == "test_password") #expect(config3.coreConfiguration.username == "test_username") #expect(config3.coreConfiguration.host == nil) #expect(config3.coreConfiguration.port == nil) #expect(config3.coreConfiguration.unixSocketPath == "/tmp/postgres.sock") #expect(config3.coreConfiguration.tls.isAllowed) #expect(config3.coreConfiguration.tls.isEnforced) let config4 = try SQLPostgresConfiguration(url: "postgres+uds://test_username@/tmp/postgres.sock") #expect(config4.coreConfiguration.database == nil) #expect(config4.coreConfiguration.password == nil) #expect(config4.coreConfiguration.username == "test_username") #expect(config4.coreConfiguration.host == nil) #expect(config4.coreConfiguration.port == nil) #expect(config4.coreConfiguration.unixSocketPath == "/tmp/postgres.sock") #expect(!config4.coreConfiguration.tls.isAllowed) #expect(!config4.coreConfiguration.tls.isEnforced) for modestr in ["tlsmode=false", "tlsmode=verify-full&tlsmode=disable"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") #expect(!config.coreConfiguration.tls.isAllowed) #expect(!config.coreConfiguration.tls.isEnforced) } for modestr in ["tlsmode=prefer", "tlsmode=allow", "tlsmode=true"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") #expect(config.coreConfiguration.tls.isAllowed) #expect(!config.coreConfiguration.tls.isEnforced) } for modestr in ["tlsmode=require", "tlsmode=verify-ca", "tlsmode=verify-full", "tls=verify-full", "ssl=verify-full", "tlsmode=prefer&sslmode=verify-full"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") #expect(config.coreConfiguration.tls.isAllowed) #expect(config.coreConfiguration.tls.isEnforced) } #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql://test_username@test_hostname") } #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql+tcp://test_username@test_hostname") } #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql+uds://test_username@/tmp/postgres.sock") } #expect(throws: (any Error).self, "should fail when username missing") { try SQLPostgresConfiguration(url: "postgres+tcp://test_hostname") } #expect(throws: (any Error).self, "should fail when TLS mode invalid") { try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname?tlsmode=absurd") } #expect(throws: (any Error).self, "should fail when username missing") { try SQLPostgresConfiguration(url: "postgres+uds://localhost/tmp/postgres.sock?tlsmode=require") } #expect(throws: (any Error).self, "should fail when authority missing") { try SQLPostgresConfiguration(url: "postgres+uds:///tmp/postgres.sock") } #expect(throws: (any Error).self, "should fail when path missing") { try SQLPostgresConfiguration(url: "postgres+uds://username@localhost/") } #expect(throws: (any Error).self, "should fail when authority not localhost or empty") { try SQLPostgresConfiguration(url: "postgres+uds://username@remotehost/tmp") } } init() { #expect(isLoggingConfigured) } } } ================================================ FILE: Tests/PostgresKitTests/Utilities.swift ================================================ import Foundation import Logging import NIOCore import PostgresKit import PostgresNIO import Testing extension PostgresConnection { static func test(on eventLoop: any EventLoop) async throws -> PostgresConnection { try await PostgresConnectionSource(sqlConfiguration: .test).makeConnection( logger: .init(label: "vapor.codes.postgres-kit.test"), on: eventLoop ).get() } } extension SQLPostgresConfiguration { static var test: Self { .init( hostname: env("POSTGRES_HOSTNAME") ?? "localhost", port: env("POSTGRES_PORT").flatMap(Int.init) ?? Self.ianaPortNumber, username: env("POSTGRES_USER") ?? "test_username", password: env("POSTGRES_PASSWORD") ?? "test_password", database: env("POSTGRES_DB") ?? "test_database", tls: .disable ) } } func env(_ name: String) -> String? { ProcessInfo.processInfo.environment[name] } let isLoggingConfigured: Bool = { LoggingSystem.bootstrap { QuickLogHandler(label: $0, level: env("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))""# } } } } @Suite(.serialized) struct AllSuites {}