diff --git a/.api-breakage/allowlist-branch-fix-deprecations.txt b/.api-breakage/allowlist-branch-fix-deprecations.txt deleted file mode 100644 index fb86138..0000000 --- a/.api-breakage/allowlist-branch-fix-deprecations.txt +++ /dev/null @@ -1,18 +0,0 @@ -API breakage: func DatabaseConfigurationFactory.postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:) has removed default argument from parameter 3 -API breakage: func DatabaseConfigurationFactory.postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:) has removed default argument from parameter 4 -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:sqlLogLevel:) has parameter 0 type change from Swift.String to PostgresKit.SQLPostgresConfiguration -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:sqlLogLevel:) has parameter 2 type change from Swift.String to NIOCore.TimeAmount -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:sqlLogLevel:) has parameter 3 type change from Swift.String to Logging.Logger.Level -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:sqlLogLevel:) has parameter 0 type change from Swift.String to PostgresKit.SQLPostgresConfiguration -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:sqlLogLevel:) has parameter 2 type change from Swift.String to NIOCore.TimeAmount -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:sqlLogLevel:) has parameter 3 type change from Swift.String to PostgresNIO.PostgresEncodingContext<some PostgresNIO.PostgresJSONEncoder> -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:sqlLogLevel:) has parameter 4 type change from Swift.String? to Logging.Logger.Level -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:decoder:sqlLogLevel:) has parameter 0 type change from Swift.String to PostgresKit.SQLPostgresConfiguration -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:decoder:sqlLogLevel:) has parameter 2 type change from Swift.String to NIOCore.TimeAmount -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:decoder:sqlLogLevel:) has parameter 3 type change from Swift.String to PostgresNIO.PostgresDecodingContext<some PostgresNIO.PostgresJSONDecoder> -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:decoder:sqlLogLevel:) has parameter 4 type change from Swift.String? to Logging.Logger.Level -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:sqlLogLevel:) has been renamed to func postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:sqlLogLevel:) -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:sqlLogLevel:) has been renamed to func postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:sqlLogLevel:) -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:decoder:sqlLogLevel:) has been renamed to func postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:decodingContext:sqlLogLevel:) -API breakage: func DatabaseConfigurationFactory.postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:sqlLogLevel:) has been removed -API breakage: func DatabaseConfigurationFactory.postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:decoder:sqlLogLevel:) has been removed diff --git a/.api-breakage/allowlist-branch-update-for-new-pnio.txt b/.api-breakage/allowlist-branch-update-for-new-pnio.txt deleted file mode 100644 index b99af90..0000000 --- a/.api-breakage/allowlist-branch-update-for-new-pnio.txt +++ /dev/null @@ -1,8 +0,0 @@ -API breakage: func DatabaseConfigurationFactory.postgres(url:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 3 -API breakage: func DatabaseConfigurationFactory.postgres(url:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 4 -API breakage: func DatabaseConfigurationFactory.postgres(url:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 3 -API breakage: func DatabaseConfigurationFactory.postgres(url:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 4 -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 8 -API breakage: func DatabaseConfigurationFactory.postgres(hostname:port:username:password:database:tlsConfiguration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 9 -API breakage: func DatabaseConfigurationFactory.postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 3 -API breakage: func DatabaseConfigurationFactory.postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:sqlLogLevel:) has removed default argument from parameter 4 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 849dbe1..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing to Fluent's PostgreSQL Driver - -👋 Welcome to the Vapor team! - -## Docker - -In order to build and test against Postgres, you will need a database running. The easiest way to do this is using Docker and the included `docker-compose.yml` file. - -If you have Docker installed on your computer, all you will need to do is: - -```sh -docker-compose up -``` - -This will start the two databases required for running this package's unit tests. - -## Xcode - -To open the project in Xcode: - -- Clone the repo to your computer -- Drag and drop the folder onto Xcode - -To test within Xcode, press `CMD+U`. - -## SPM - -To develop using SPM, open the code in your favorite code editor. Use the following commands from within the project's root folder to build and test. - -```sh -swift build -swift test -``` - -## SemVer - -Vapor follows [SemVer](https://semver.org). This means that any changes to the source code that can cause -existing code to stop compiling _must_ wait until the next major version to be included. - -Code that is only additive and will not break any existing code can be included in the next minor release. - ----------- - -Join us on Discord if you have any questions: [vapor.team](http://vapor.team). - -— Thanks! 🙌 diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index a7122a8..c6086d5 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -11,4 +11,4 @@ jobs: with: package_name: fluent-postgres-driver modules: FluentPostgresDriver - pathsToInvalidate: /fluentpostgresdriver + pathsToInvalidate: /fluentpostgresdriver/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b4aa7c..4af2166 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,9 +12,9 @@ env: POSTGRES_HOSTNAME: 'psql-a' POSTGRES_HOSTNAME_A: 'psql-a' POSTGRES_HOSTNAME_B: 'psql-b' - POSTGRES_DB: 'test_database' - POSTGRES_DB_A: 'test_database' - POSTGRES_DB_B: 'test_database' + POSTGRES_DB: 'test_database_a' + POSTGRES_DB_A: 'test_database_a' + POSTGRES_DB_B: 'test_database_b' POSTGRES_USER: 'test_username' POSTGRES_USER_A: 'test_username' POSTGRES_USER_B: 'test_username' @@ -24,15 +24,17 @@ env: jobs: api-breakage: - if: ${{ !(github.event.pull_request.draft || false) }} + if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:5.8-jammy + container: swift:jammy steps: - - name: Check out package - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: { 'fetch-depth': 0 } - - name: Run API breakage check action - uses: vapor/ci/.github/actions/ci-swift-check-api-breakage@reusable-workflows + - name: API breaking changes + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + swift package diagnose-api-breaking-changes origin/main linux-all: if: ${{ !(github.event.pull_request.draft || false) }} @@ -40,50 +42,46 @@ jobs: fail-fast: false matrix: include: - - {dbimage: 'postgres:11', dbauth: 'trust', swiftver: 'swift:5.7-focal'} - - {dbimage: 'postgres:13', dbauth: 'md5', swiftver: 'swift:5.7-jammy'} - - {dbimage: 'postgres:15', dbauth: 'scram-sha-256', swiftver: 'swift:5.8-jammy'} - - {dbimage: 'postgres:15', dbauth: 'scram-sha-256', swiftver: 'swiftlang/swift:nightly-5.9-jammy'} - - {dbimage: 'postgres:15', dbauth: 'scram-sha-256', swiftver: 'swiftlang/swift:nightly-main-jammy'} - container: ${{ matrix.swiftver }} + - postgres-image-a: 'postgres:13' + postgres-image-b: 'postgres:14' + postgres-auth: 'trust' + swift-image: 'swift:5.8-focal' + - postgres-image-a: 'postgres:15' + postgres-image-b: 'postgres:16' + postgres-auth: 'md5' + swift-image: 'swift:5.10-jammy' + - postgres-image-a: 'postgres:15' + postgres-image-b: 'postgres:16' + postgres-auth: 'scram-sha-256' + swift-image: 'swiftlang/swift:nightly-6.0-jammy' + container: ${{ matrix.swift-image }} runs-on: ubuntu-latest services: psql-a: - image: ${{ matrix.dbimage }} + image: ${{ matrix.postgres-image-a }} env: POSTGRES_USER: 'test_username' - POSTGRES_DB: 'test_database' + POSTGRES_DB: 'test_database_a' POSTGRES_PASSWORD: 'test_password' - POSTGRES_HOST_AUTH_METHOD: ${{ matrix.dbauth }} - POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.dbauth }} + POSTGRES_HOST_AUTH_METHOD: ${{ matrix.postgres-auth }} + POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }} psql-b: - image: ${{ matrix.dbimage }} + image: ${{ matrix.postgres-image-b }} env: POSTGRES_USER: 'test_username' - POSTGRES_DB: 'test_database' + POSTGRES_DB: 'test_database_b' POSTGRES_PASSWORD: 'test_password' - POSTGRES_HOST_AUTH_METHOD: ${{ matrix.dbauth }} - POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.dbauth }} + POSTGRES_HOST_AUTH_METHOD: ${{ matrix.postgres-auth }} + POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }} steps: - - name: Display versions - shell: bash - run: | - echo POSTGRES_VERSION='${{ matrix.dbimage }}' >> $GITHUB_ENV - echo POSTGRES_AUTH_METHOD='${{ matrix.dbauth }}' >> $GITHUB_ENV - if [[ '${{ contains(matrix.container, 'nightly') }}' == 'true' ]]; then - SWIFT_PLATFORM="$(source /etc/os-release && echo "${ID}${VERSION_ID}")" SWIFT_VERSION="$(cat /.swift_tag)" - printf 'SWIFT_PLATFORM=%s\nSWIFT_VERSION=%s\n' "${SWIFT_PLATFORM}" "${SWIFT_VERSION}" >>"${GITHUB_ENV}" - fi - printf 'OS: %s\nTag: %s\nVersion:\n' "${SWIFT_PLATFORM}-${RUNNER_ARCH}" "${SWIFT_VERSION}" && swift --version - name: Check out package - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run all tests run: swift test --sanitize=thread --enable-code-coverage - name: Submit coverage report to Codecov.io - uses: vapor/swift-codecov-action@v0.2 + uses: vapor/swift-codecov-action@v0.3 with: - cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,POSTGRES_VERSION,POSTGRES_AUTH_METHOD' - cc_fail_ci_if_error: false + codecov_token: ${{ secrets.CODECOV_TOKEN }} macos-all: if: ${{ !(github.event.pull_request.draft || false) }} @@ -91,41 +89,34 @@ jobs: fail-fast: false matrix: include: - - dbimage: postgresql@14 - dbauth: scram-sha-256 - macos: macos-13 - xcode: latest-stable - runs-on: ${{ matrix.macos }} + - macos-version: macos-13 + xcode-version: '~14.3' + - macos-version: macos-14 + xcode-version: latest + runs-on: ${{ matrix.macos-version }} env: LOG_LEVEL: debug POSTGRES_HOSTNAME: 127.0.0.1 POSTGRES_HOSTNAME_A: 127.0.0.1 POSTGRES_HOSTNAME_B: 127.0.0.1 - POSTGRES_DB: 'test_database_a' - POSTGRES_DB_A: 'test_database_a' - POSTGRES_DB_B: 'test_database_b' - POSTGRES_USER: 'test_username_a' - POSTGRES_USER_A: 'test_username_a' - POSTGRES_USER_B: 'test_username_b' steps: - name: Select latest available Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: ${{ matrix.xcode }} + xcode-version: ${{ matrix.xcode-version }} - name: Install Postgres, setup DB and auth, and wait for server start run: | - export PATH="$(brew --prefix)/opt/${{ matrix.dbimage }}/bin:$PATH" PGDATA=/tmp/vapor-postgres-test PGUSER=postgres - (brew unlink postgresql || true) && brew install ${{ matrix.dbimage }} && brew link --force ${{ matrix.dbimage }} - initdb --locale=C --auth-host ${{ matrix.dbauth }} --username=postgres --pwfile=<(echo postgres) + brew upgrade || true + export PATH="$(brew --prefix)/opt/postgresql@16/bin:$PATH" PGDATA=/tmp/vapor-postgres-test PGUSER="${POSTGRES_USER_A}" + (brew unlink postgresql@14 || true) && brew install postgresql@16 && brew link --force postgresql@16 + initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER_A}" --pwfile=<(echo "${POSTGRES_PASSWORD_A}") pg_ctl start --wait - psql postgres <<<"CREATE ROLE $POSTGRES_USER_A LOGIN PASSWORD '$POSTGRES_PASSWORD_A';" - psql postgres <<<"CREATE ROLE $POSTGRES_USER_B LOGIN PASSWORD '$POSTGRES_PASSWORD_B';" - psql postgres <<<"CREATE DATABASE $POSTGRES_DB_A OWNER = $POSTGRES_USER_A;" - psql postgres <<<"CREATE DATABASE $POSTGRES_DB_B OWNER = $POSTGRES_USER_B;" - psql $POSTGRES_DB_A <<<"ALTER SCHEMA public OWNER TO $POSTGRES_USER_A;" - psql $POSTGRES_DB_B <<<"ALTER SCHEMA public OWNER TO $POSTGRES_USER_B;" - timeout-minutes: 2 + PGPASSWORD="${POSTGRES_PASSWORD_A}" createdb -w -O "${POSTGRES_USER_A}" "${POSTGRES_DB_A}" + PGPASSWORD="${POSTGRES_PASSWORD_A}" createdb -w -O "${POSTGRES_USER_B}" "${POSTGRES_DB_B}" + PGPASSWORD="${POSTGRES_PASSWORD_A}" psql -w "${POSTGRES_DB_A}" <<<"ALTER SCHEMA public OWNER TO ${POSTGRES_USER_A};" + PGPASSWORD="${POSTGRES_PASSWORD_A}" psql -w "${POSTGRES_DB_B}" <<<"ALTER SCHEMA public OWNER TO ${POSTGRES_USER_B};" + timeout-minutes: 15 - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run all tests run: swift test --sanitize=thread diff --git a/Package.swift b/Package.swift index 3dfaa0a..e827a9c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 import PackageDescription let package = Package( @@ -18,15 +18,30 @@ let package = Package( .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.11.4"), ], targets: [ - .target(name: "FluentPostgresDriver", dependencies: [ - .product(name: "AsyncKit", package: "async-kit"), - .product(name: "FluentKit", package: "fluent-kit"), - .product(name: "FluentSQL", package: "fluent-kit"), - .product(name: "PostgresKit", package: "postgres-kit"), - ]), - .testTarget(name: "FluentPostgresDriverTests", dependencies: [ - .product(name: "FluentBenchmark", package: "fluent-kit"), - .target(name: "FluentPostgresDriver"), - ]), + .target( + name: "FluentPostgresDriver", + dependencies: [ + .product(name: "AsyncKit", package: "async-kit"), + .product(name: "FluentKit", package: "fluent-kit"), + .product(name: "FluentSQL", package: "fluent-kit"), + .product(name: "PostgresKit", package: "postgres-kit"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "FluentPostgresDriverTests", + dependencies: [ + .product(name: "FluentBenchmark", package: "fluent-kit"), + .target(name: "FluentPostgresDriver"), + ], + swiftSettings: swiftSettings + ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] } diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 0000000..68670a8 --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,48 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "fluent-postgres-driver", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + ], + products: [ + .library(name: "FluentPostgresDriver", targets: ["FluentPostgresDriver"]), + ], + dependencies: [ + .package(url: "https://github.com/vapor/async-kit.git", from: "1.17.0"), + .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.43.0"), + .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.11.4"), + ], + targets: [ + .target( + name: "FluentPostgresDriver", + dependencies: [ + .product(name: "AsyncKit", package: "async-kit"), + .product(name: "FluentKit", package: "fluent-kit"), + .product(name: "FluentSQL", package: "fluent-kit"), + .product(name: "PostgresKit", package: "postgres-kit"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "FluentPostgresDriverTests", + dependencies: [ + .product(name: "FluentBenchmark", package: "fluent-kit"), + .target(name: "FluentPostgresDriver"), + ], + swiftSettings: swiftSettings + ), + ] +) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] } diff --git a/README.md b/README.md index 8803fe2..76a07bb 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,35 @@ <p align="center"> - <img - src="https://user-images.githubusercontent.com/1342803/59065097-ec656880-8879-11e9-9e80-2e393dc313c1.png" - height="64" - alt="FluentPostgresDriver" - > - <br> - <br> - <a href="https://docs.vapor.codes/4.0/"> - <img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation"> - </a> - <a href="https://discord.gg/vapor"> - <img src="https://img.shields.io/discord/431917998102675485.svg" alt="Team Chat"> - </a> - <a href="LICENSE"> - <img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License"> - </a> - <a href="https://github.com/vapor/fluent-postgres-driver/actions/workflows/test.yml"> - <img src="https://github.com/vapor/fluent-postgres-driver/actions/workflows/test.yml/badge.svg?event=push" alt="Continuous Integration"> - </a> - <a href="https://codecov.io/gh/vapor/fluent-postgres-driver"> - <img src="https://codecov.io/gh/vapor/fluent-postgres-driver/branch/main/graph/badge.svg?token=PizqqlcRSJ" alt="Test Coverage"> - </a> - <a href="https://swift.org"> - <img src="http://img.shields.io/badge/swift-5.7-brightgreen.svg" alt="Swift 5.7"> - </a> +<picture> + <source media="(prefers-color-scheme: dark)" srcset="https://github.com/vapor/fluent-postgres-driver/assets/1130717/c2350b70-aaf1-43e1-ab79-86fc88ba8da4"> + <source media="(prefers-color-scheme: light)" srcset="https://github.com/vapor/fluent-postgres-driver/assets/1130717/dfc94dc2-281b-4e54-be86-549813496373"> + <img src="https://github.com/vapor/fluent-postgres-driver/assets/1130717/dfc94dc2-281b-4e54-be86-549813496373" height="96" alt="FluentPostgresDriver"> +</picture> +<br> +<br> +<a href="https://docs.vapor.codes/4.0/"><img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation"></a> +<a href="https://discord.gg/vapor"><img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat"></a> +<a href="LICENSE"><img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License"></a> +<a href="https://github.com/vapor/fluent-postgres-driver/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/vapor/fluent-postgres-driver/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration"></a> +<a href="https://codecov.io/github/vapor/fluent-postgres-driver"><img src="https://img.shields.io/codecov/c/github/vapor/fluent-postgres-driver?style=plastic&logo=codecov&label=codecov"></a> +<a href="https://swift.org"><img src="https://design.vapor.codes/images/swift58up.svg" alt="Swift 5.8+"></a> </p> + +<br> + +FluentPostgresDriver is a [FluentKit] driver for PostgreSQL clients. It provides support for using the Fluent ORM with PostgreSQL databases, and uses [PostgresKit] to provide [SQLKit] driver services, [PostgresNIO] to connect and communicate with the database server asynchronously, and [AsyncKit] to provide connection pooling. + +[FluentKit]: https://github.com/vapor/fluent-kit +[SQLKit]: https://github.com/vapor/sql-kit +[PostgresKit]: https://github.com/vapor/postgres-kit +[PostgresNIO]: https://github.com/vapor/postgres-nio +[AsyncKit]: https://github.com/vapor/async-kit + +### Usage + +Use the SPM string to easily include the dependendency in your `Package.swift` file: + +```swift +.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") +``` + +For additional information, see [the Fluent documentation](https://docs.vapor.codes/fluent/overview/). diff --git a/Sources/FluentPostgresDriver/DatabaseID+PostgreSQL.swift b/Sources/FluentPostgresDriver/DatabaseID+PostgreSQL.swift index fc8370e..d50397f 100644 --- a/Sources/FluentPostgresDriver/DatabaseID+PostgreSQL.swift +++ b/Sources/FluentPostgresDriver/DatabaseID+PostgreSQL.swift @@ -2,6 +2,6 @@ import FluentKit extension DatabaseID { public static var psql: DatabaseID { - return .init(string: "psql") + .init(string: "psql") } } diff --git a/Sources/FluentPostgresDriver/Docs.docc/Resources/vapor-fluentpostgresdriver-logo.svg b/Sources/FluentPostgresDriver/Docs.docc/Resources/vapor-fluentpostgresdriver-logo.svg new file mode 100644 index 0000000..4cc9947 --- /dev/null +++ b/Sources/FluentPostgresDriver/Docs.docc/Resources/vapor-fluentpostgresdriver-logo.svg @@ -0,0 +1,21 @@ +<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" id="technology"> + <defs> + <style> + @media(prefers-color-scheme:dark){:root{--color-logo-shape:#000;--color-logo-base:#fff}} + body[data-color-scheme="dark"]{--color-logo-shape:#000;--color-logo-base:#fff} + </style> + <path id="d" d="M6,47l58-47,58,47-58,45z" /> + <path id="s" d="M6,47v12l58,45v-12z" /> + <path id="h" d="m61.7,22c-3-.1-6.6,2-6.2,5.6l2,12-13.3-2.5c-6-1.2-9.8,7.3-3.8,8.5l13.3,2.4-11.4,9.5c-5,4.3,1,9.9,6,5.7l11.4-9.5,1.9,12c.9,5.3,10.7,2.6,9.8-2.8l-1.9-11.9,13.3,2.4c5.9,1.2,9.7-7.3,3.8-8.4l-13.4-2.5,11.4-9.5c5.1-4.2-1-9.8-6-5.7l-11.4,9.5-1.9-12c-.3-1.9-1.8-2.8-3.6-2.8zm2.2,11.9c-.1.2,0,.3.4.3-.9.5-1.3.9-1,1.3,1,.8,2.9-.1,4,2.8.1.4.4-.5.5-1.6.2.2.4.6.3,1.2,0,0,.6-1.9.6-1.9,1.3,2.1-.8,3.2-1.6,4.5-2.3,3.8,2,6,2.7,5.9,1.5.5.2,1.3,1.6,1.8.2.2-.3.6-1.6,1,.1.3.3.4.4.3,2.3.6.3,1.2-.5,1.3-.4-.2-.7-.5-.9-1-.8-.7-1.8-1.5-3-2.2-.7.2-1.4.8-1.9,1.8.8.5,1.5,1.1,2,1.7,1.1.1,1.4.3,1,.7-.5.3-.9.6-1.4.8-.3.1-1-.5-2.2-1.5.6.7,1.1,1.5,1.5,2.4,2,0,.3,1.3.1,1.4-.5.4-.9.3-1.3-.1-.9-1.9-4.5-3.4-5.2-3.6.3.2.6,1.2.5,1.7-.1-.7-.5-1.3-.9-1.9-.3-.6-2.4-4,3.8-6.8,1.1-.4-3.2-.8.8-3.1,0-.6.9-1.4,2.8-2.3.6-1.8-1.5-2-2.5-2.8-.4-.3-.5-1.2.8-1.9,0,0,.1-.2.2-.2zm3.6,13.1c.4.5.8.8.9,1,.3-.3-.1-.7-.9-1z" /> + </defs> + <use href="#s" fill="#9ee0ff"/> + <use href="#s" fill="#6bd0ff" y="12"/> + <use href="#s" fill="#38c0ff" y="24"/> + <g transform="matrix(-1 0 0 1 128 0)"> + <use href="#s" fill="#f29eff"/> + <use href="#s" fill="#eb6bff" y="12"/> + <use href="#s" fill="#e438ff" y="24"/> + </g> + <use href="#d" style="fill:var(--color-logo-base,#000)"/> + <use href="#h" style="fill:var(--color-logo-shape,#fff)"/> +</svg> diff --git a/Sources/FluentPostgresDriver/Docs.docc/index.md b/Sources/FluentPostgresDriver/Docs.docc/index.md index e6f927d..08593bc 100644 --- a/Sources/FluentPostgresDriver/Docs.docc/index.md +++ b/Sources/FluentPostgresDriver/Docs.docc/index.md @@ -1,3 +1,14 @@ # ``FluentPostgresDriver`` -FluentPostgresDriver is a package to integrate PostgresNIO and and PostrgresKit with FluentKit to make it easy to use and write database operations in Swift. \ No newline at end of file +FluentPostgresDriver is a [FluentKit] driver for PostgreSQL clients. + +## Overview + +FluentPostgresDriver provides support for using the Fluent ORM with PostgresSQL databases. It uses [PostgresKit] to provide [SQLKit] driver services, [PostgresNIO] to connect and communicate with the database server asynchronously, and [AsyncKit] to provide connection pooling. + +[FluentKit]: https://github.com/vapor/fluent-kit +[SQLKit]: https://github.com/vapor/sql-kit +[PostgresKit]: https://github.com/vapor/postgres-kit +[PostgresNIO]: https://github.com/vapor/postgres-nio +[AsyncKit]: https://github.com/vapor/async-kit + diff --git a/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json b/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json new file mode 100644 index 0000000..6f0b9d4 --- /dev/null +++ b/Sources/FluentPostgresDriver/Docs.docc/theme-settings.json @@ -0,0 +1,21 @@ +{ + "theme": { + "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, + "border-radius": "0", + "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, + "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, + "color": { + "fluentpsqldriver": "#336791", + "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentpsqldriver) 30%, #000 100%)", + "documentation-intro-accent": "var(--color-fluentpsqldriver)", + "logo-base": { "dark": "#fff", "light": "#000" }, + "logo-shape": { "dark": "#000", "light": "#fff" }, + "fill": { "dark": "#000", "light": "#fff" } + }, + "icons": { "technology": "/fluentpostgresdriver/images/vapor-fluentpostgresdriver-logo.svg" } + }, + "features": { + "quickNavigation": { "enable": true }, + "i18n": { "enable": true } + } +} diff --git a/Sources/FluentPostgresDriver/Exports.swift b/Sources/FluentPostgresDriver/Exports.swift index 8aee3c6..f33f24c 100644 --- a/Sources/FluentPostgresDriver/Exports.swift +++ b/Sources/FluentPostgresDriver/Exports.swift @@ -1,12 +1,2 @@ -#if swift(>=5.8) - @_documentation(visibility: internal) @_exported import FluentKit @_documentation(visibility: internal) @_exported import PostgresKit - -#else - -@_exported import FluentKit -@_exported import PostgresKit - -#endif - diff --git a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift index b7f835b..39c94d0 100644 --- a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift +++ b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift @@ -81,7 +81,9 @@ extension DatabaseConfigurationFactory { decodingContext: PostgresDecodingContext<some PostgresJSONDecoder>, sqlLogLevel: Logger.Level = .debug ) -> DatabaseConfigurationFactory { - .init { + let configuration = FakeSendable(wrappedValue: configuration) + + return .init { FluentPostgresConfiguration( configuration: configuration, maxConnectionsPerEventLoop: maxConnectionsPerEventLoop, @@ -93,6 +95,8 @@ extension DatabaseConfigurationFactory { } } +fileprivate struct FakeSendable<T>: @unchecked Sendable { let wrappedValue: T } + /// We'd like to just default the context parameters of the "actual" method. Unfortunately, there are a few /// cases involving the UNIX domain socket initalizer where usage can resolve to either the new /// ``SQLPostgresConfiguration``-based method or the deprecated ``PostgresConfiguration``-based method, with no @@ -166,7 +170,7 @@ extension DatabaseConfigurationFactory { /// The actual concrete configuration type produced by a configuration factory. struct FluentPostgresConfiguration<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: DatabaseConfiguration { var middleware: [any AnyModelMiddleware] = [] - let configuration: SQLPostgresConfiguration + fileprivate let configuration: FakeSendable<SQLPostgresConfiguration> let maxConnectionsPerEventLoop: Int let connectionPoolTimeout: TimeAmount let encodingContext: PostgresEncodingContext<E> @@ -174,7 +178,7 @@ struct FluentPostgresConfiguration<E: PostgresJSONEncoder, D: PostgresJSONDecode let sqlLogLevel: Logger.Level func makeDriver(for databases: Databases) -> any DatabaseDriver { - let connectionSource = PostgresConnectionSource(sqlConfiguration: self.configuration) + let connectionSource = PostgresConnectionSource(sqlConfiguration: self.configuration.wrappedValue) let elgPool = EventLoopGroupConnectionPool( source: connectionSource, maxConnectionsPerEventLoop: self.maxConnectionsPerEventLoop, diff --git a/Sources/FluentPostgresDriver/FluentPostgresDatabase.swift b/Sources/FluentPostgresDriver/FluentPostgresDatabase.swift index 3ec68a7..91eac91 100644 --- a/Sources/FluentPostgresDriver/FluentPostgresDatabase.swift +++ b/Sources/FluentPostgresDriver/FluentPostgresDatabase.swift @@ -16,14 +16,14 @@ struct _FluentPostgresDatabase<E: PostgresJSONEncoder, D: PostgresJSONDecoder> { extension _FluentPostgresDatabase: Database { func execute( query: DatabaseQuery, - onOutput: @escaping (any DatabaseOutput) -> () + onOutput: @escaping @Sendable (any DatabaseOutput) -> () ) -> EventLoopFuture<Void> { var expression = SQLQueryConverter(delegate: PostgresConverterDelegate()).convert(query) /// For `.create` query actions, we want to return the generated IDs, unless the `customIDKey` is the /// empty string, which we use as a very hacky signal for "we don't implement this for composite IDs yet". if case .create = query.action, query.customIDKey != .some(.string("")) { - expression = PostgresReturningID(base: expression, idKey: query.customIDKey ?? .id) + expression = SQLKit.SQLList([expression, SQLReturning(.init((query.customIDKey ?? .id).description))], separator: SQLRaw(" ")) } return self.execute(sql: expression, { onOutput($0.databaseOutput()) }) @@ -34,7 +34,7 @@ extension _FluentPostgresDatabase: Database { return self.execute(sql: expression, // N.B.: Don't fatalError() here; what're users supposed to do about it? - { self.logger.error("Unexpected row returned from schema query: \($0)") } + { self.logger.debug("Unexpected row returned from schema query: \($0)") } ) } @@ -44,7 +44,7 @@ extension _FluentPostgresDatabase: Database { return e.createCases.reduce(self.create(enum: e.name)) { $0.value($1) }.run() case .update: if !e.deleteCases.isEmpty { - self.logger.error("PostgreSQL does not support deleting enum cases.") + self.logger.debug("PostgreSQL does not support deleting enum cases.") } guard !e.createCases.isEmpty else { return self.eventLoop.makeSucceededFuture(()) @@ -58,7 +58,7 @@ extension _FluentPostgresDatabase: Database { } } - func transaction<T>(_ closure: @escaping (any Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> { + func transaction<T>(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> { guard !self.inTransaction else { return closure(self) } @@ -70,8 +70,8 @@ extension _FluentPostgresDatabase: Database { """) } return sqlConn.raw("BEGIN").run().flatMap { - return closure(conn).flatMap { result in - sqlConn.raw("COMMIT").run().map { result } + closure(conn).flatMap { result in + sqlConn.raw("COMMIT").run().and(value: result).map { $1 } }.flatMapError { error in sqlConn.raw("ROLLBACK").run().flatMapThrowing { throw error } } @@ -79,7 +79,7 @@ extension _FluentPostgresDatabase: Database { } } - func withConnection<T>(_ closure: @escaping (any Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> { + func withConnection<T>(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> { self.withConnection { (underlying: any PostgresDatabase) in closure(_FluentPostgresDatabase( database: underlying.sql(encodingContext: self.encodingContext, decodingContext: self.decodingContext, queryLogLevel: self.database.queryLogLevel), @@ -111,9 +111,17 @@ extension _FluentPostgresDatabase: SQLDatabase { var dialect: any SQLDialect { self.database.dialect } var queryLogLevel: Logger.Level? { self.database.queryLogLevel } - func execute(sql query: any SQLExpression, _ onRow: @escaping (any SQLRow) -> ()) -> EventLoopFuture<Void> { + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture<Void> { self.database.execute(sql: query, onRow) } + + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) async throws { + try await self.database.execute(sql: query, onRow) + } + + func withSession<R>(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { + try await self.database.withSession(closure) + } } extension _FluentPostgresDatabase: PostgresDatabase { @@ -132,15 +140,3 @@ extension _FluentPostgresDatabase: PostgresDatabase { return psqlDb.withConnection(closure) } } - -private struct PostgresReturningID: SQLExpression { - let base: any SQLExpression - let idKey: FieldKey - - func serialize(to serializer: inout SQLSerializer) { - serializer.statement { - $0.append(self.base) - $0.append("RETURNING", SQLIdentifier(self.idKey.description)) - } - } -} diff --git a/Sources/FluentPostgresDriver/FluentPostgresDriver.swift b/Sources/FluentPostgresDriver/FluentPostgresDriver.swift index d7201ef..400ae5a 100644 --- a/Sources/FluentPostgresDriver/FluentPostgresDriver.swift +++ b/Sources/FluentPostgresDriver/FluentPostgresDriver.swift @@ -4,7 +4,8 @@ import Logging import FluentKit import PostgresKit -struct _FluentPostgresDriver<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: DatabaseDriver { +/// Marked `@unchecked Sendable` to silence warning about `PostgresConnectionSource` +struct _FluentPostgresDriver<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: DatabaseDriver, @unchecked Sendable { let pool: EventLoopGroupConnectionPool<PostgresConnectionSource> let encodingContext: PostgresEncodingContext<E> let decodingContext: PostgresDecodingContext<D> diff --git a/Sources/FluentPostgresDriver/PostgresError+Database.swift b/Sources/FluentPostgresDriver/PostgresError+Database.swift index 68cc3ee..a214b6e 100644 --- a/Sources/FluentPostgresDriver/PostgresError+Database.swift +++ b/Sources/FluentPostgresDriver/PostgresError+Database.swift @@ -92,7 +92,7 @@ extension PSQLError: DatabaseError { public var isConnectionClosed: Bool { switch self.code { - case .connectionClosed: return true + case .serverClosedConnection, .clientClosedConnection: return true default: return false } } diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift index a2f510e..2297391 100644 --- a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift +++ b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift @@ -6,11 +6,37 @@ import XCTest import PostgresKit import SQLKit +func XCTAssertThrowsErrorAsync<T>( + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, line: UInt = #line, + _ callback: (any Error) -> Void = { _ in } +) async { + do { + _ = try await expression() + XCTAssertThrowsError({}(), message(), file: file, line: line, callback) + } catch { + XCTAssertThrowsError(try { throw error }(), message(), file: file, line: line, callback) + } +} + +func XCTAssertNoThrowAsync<T>( + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, line: UInt = #line +) async { + do { + _ = try await expression() + } catch { + XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line) + } +} + final class FluentPostgresDriverTests: XCTestCase { - //func testAll() throws { try self.benchmarker.testAll() } func testAggregate() throws { try self.benchmarker.testAggregate() } func testArray() throws { try self.benchmarker.testArray() } func testBatch() throws { try self.benchmarker.testBatch() } + func testChild() throws { try self.benchmarker.testChildren() } func testChildren() throws { try self.benchmarker.testChildren() } func testChunk() throws { try self.benchmarker.testChunk() } func testCodable() throws { try self.benchmarker.testCodable() } @@ -40,84 +66,91 @@ final class FluentPostgresDriverTests: XCTestCase { func testTransaction() throws { try self.benchmarker.testTransaction() } func testUnique() throws { try self.benchmarker.testUnique() } - func testDatabaseError() throws { - let sql = (self.db as! any SQLDatabase) - do { - try sql.raw("asd").run().wait() - } catch let error as DatabaseError where error.isSyntaxError { - // PASS - } catch { - XCTFail("\(error)") + func testDatabaseError() async throws { + let sql1 = (self.db as! any SQLDatabase) + await XCTAssertThrowsErrorAsync(try await sql1.raw("asdf").run()) { + XCTAssertTrue(($0 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))") } - do { - try sql.raw("CREATE TABLE foo (name TEXT UNIQUE)").run().wait() - try sql.raw("INSERT INTO foo (name) VALUES ('bar')").run().wait() - try sql.raw("INSERT INTO foo (name) VALUES ('bar')").run().wait() - } catch let error as DatabaseError where error.isConstraintFailure { - // pass - } catch { - XCTFail("\(error)") + + let sql2 = (self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.eventLoopGroup.any())!) as! any SQLDatabase + try await sql2.drop(table: "foo").ifExists().run() + try await sql2.create(table: "foo").column("name", type: .text, .unique).run() + try await sql2.insert(into: "foo").columns("name").values("bar").run() + await XCTAssertThrowsErrorAsync(try await sql2.insert(into: "foo").columns("name").values("bar").run()) { + XCTAssertTrue(($0 as? any DatabaseError)?.isConstraintFailure ?? false, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isSyntaxError ?? true, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))") } + + // Disabled until we figure out why it hangs instead of throwing an error. + //let postgres = (self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.eventLoopGroup.any())!) as! any PostgresDatabase + //await XCTAssertThrowsErrorAsync(try await postgres.withConnection { conn in + // conn.close().flatMap { + // conn.sql().insert(into: "foo").columns("name").values("bar").run() + // } + //}.get()) { + // XCTAssertTrue(($0 as? any DatabaseError)?.isConnectionClosed ?? false, "\(String(reflecting: $0))") + // XCTAssertFalse(($0 as? any DatabaseError)?.isSyntaxError ?? true, "\(String(reflecting: $0))") + // XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))") + //} } - func testBlob() throws { - struct CreateFoo: Migration { - func prepare(on database: any Database) -> EventLoopFuture<Void> { - database.schema("foos") + func testBlob() async throws { + struct CreateFoo: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema("foos") .field("id", .int, .identifier(auto: true)) .field("data", .data, .required) .create() } - func revert(on database: any Database) -> EventLoopFuture<Void> { - database.schema("foos").delete() + func revert(on database: any Database) async throws { + try await database.schema("foos").delete() } } - try CreateFoo().prepare(on: self.db).wait() - try CreateFoo().revert(on: self.db).wait() + try await CreateFoo().prepare(on: self.db) + try await CreateFoo().revert(on: self.db) } - func testSaveModelWithBool() throws { - final class Organization: Model { + func testSaveModelWithBool() async throws { + final class Organization: Model, @unchecked Sendable { static let schema = "orgs" - @ID(custom: "id", generatedBy: .database) - var id: Int? + @ID(custom: "id", generatedBy: .database) var id: Int? + @Field(key: "disabled") var disabled: Bool - @Field(key: "disabled") - var disabled: Bool - - init() { } + init() {} } - struct CreateOrganization: Migration { - func prepare(on database: any Database) -> EventLoopFuture<Void> { - database.schema("orgs") + struct CreateOrganization: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema("orgs") .field("id", .int, .identifier(auto: true)) .field("disabled", .bool, .required) .create() } - func revert(on database: any Database) -> EventLoopFuture<Void> { - database.schema("orgs").delete() + func revert(on database: any Database) async throws { + try await database.schema("orgs").delete() } } - try CreateOrganization().prepare(on: self.db).wait() - defer { - try! CreateOrganization().revert(on: self.db).wait() + try await CreateOrganization().prepare(on: self.db) + do { + let new = Organization() + new.disabled = false + try await new.save(on: self.db) + } catch { + try? await CreateOrganization().revert(on: self.db) + throw error } - - let new = Organization() - new.disabled = false - try new.save(on: self.db).wait() + try await CreateOrganization().revert(on: self.db) } - func testCustomJSON() throws { - try EventMigration().prepare(on: self.db).wait() - defer { try! EventMigration().revert(on: self.db).wait() } - + func testCustomJSON() async throws { let jsonEncoder = JSONEncoder() jsonEncoder.dateEncodingStrategy = .iso8601 let jsonDecoder = JSONDecoder() @@ -133,60 +166,73 @@ final class FluentPostgresDriverTests: XCTestCase { on: self.eventLoopGroup.any() )! - let date = Date() - let event = Event() - event.id = 1 - event.metadata = Metadata(createdAt: date) - try event.save(on: db).wait() - - let rows = try EventStringlyTyped.query(on: db).filter(\.$id == 1).all().wait() - let expected = ISO8601DateFormatter().string(from: date) - XCTAssertEqual(rows[0].metadata["createdAt"], expected) + try await EventMigration().prepare(on: db) + do { + let date = Date() + let event = Event() + event.id = 1 + event.metadata = Metadata(createdAt: date) + try await event.save(on: db) + + let rows = try await EventStringlyTyped.query(on: db).filter(\.$id == 1).all() + let expected = ISO8601DateFormatter().string(from: date) + XCTAssertEqual(rows[0].metadata["createdAt"], expected) + } catch { + try? await EventMigration().revert(on: db) + throw error + } + try await EventMigration().revert(on: db) } - func testEnumAddingMultipleCases() throws { - try EnumMigration().prepare(on: self.db).wait() - try EventWithFooMigration().prepare(on: self.db).wait() - - let event = EventWithFoo() - event.foobar = .foo - try event.save(on: self.db).wait() - - XCTAssertNoThrow(try EnumAddMultipleCasesMigration().prepare(on: self.db).wait()) - - event.foobar = .baz - XCTAssertNoThrow(try event.update(on: self.db).wait()) - event.foobar = .qux - XCTAssertNoThrow(try event.update(on: self.db).wait()) - - XCTAssertNoThrow(try EnumAddMultipleCasesMigration().revert(on: self.db).wait()) - try! EventWithFooMigration().revert(on: self.db).wait() - try! EnumMigration().revert(on: self.db).wait() + func testEnumAddingMultipleCases() async throws { + try await EnumMigration().prepare(on: self.db) + do { + try await EventWithFooMigration().prepare(on: self.db) + do { + let event = EventWithFoo() + event.foobar = .foo + try await event.save(on: self.db) + + await XCTAssertNoThrowAsync(try await EnumAddMultipleCasesMigration().prepare(on: self.db)) + + event.foobar = .baz + await XCTAssertNoThrowAsync(try await event.update(on: self.db)) + event.foobar = .qux + await XCTAssertNoThrowAsync(try await event.update(on: self.db)) + + await XCTAssertNoThrowAsync(try await EnumAddMultipleCasesMigration().revert(on: self.db)) + } catch { + try? await EventWithFooMigration().revert(on: self.db) + throw error + } + } catch { + try? await EnumMigration().revert(on: self.db) + throw error + } } - func testEncodingArrayOfModels() throws { - final class Elem: Model, ExpressibleByIntegerLiteral { + func testEncodingArrayOfModels() async throws { + final class Elem: Model, ExpressibleByIntegerLiteral, @unchecked Sendable { static let schema = "" @ID(custom: .id) var id: Int? init() {}; init(integerLiteral l: Int) { self.id = l } } - final class Seq: Model, ExpressibleByNilLiteral, ExpressibleByArrayLiteral { + final class Seq: Model, ExpressibleByNilLiteral, ExpressibleByArrayLiteral, @unchecked Sendable { static let schema = "seqs" @ID(custom: .id) var id: Int?; @OptionalField(key: "list") var list: [Elem]? init() {}; init(nilLiteral: ()) { self.list = nil }; init(arrayLiteral el: Elem...) { self.list = el } } do { - try self.db.schema(Seq.schema).field(.id, .int, .identifier(auto: true)).field("list", .sql(embed: "JSONB[]")).create().wait() - defer { try! db.schema(Seq.schema).delete().wait() } + try await self.db.schema(Seq.schema).field(.id, .int, .identifier(auto: true)).field("list", .sql(embed: "JSONB[]")).create() let s1: Seq = [1, 2], s2: Seq = nil; try [s1, s2].forEach { try $0.create(on: self.db).wait() } // Make sure it went into the DB as "array of jsonb" rather than as "array of one jsonb containing array" or such. - let raws = try (self.db as! SQLDatabase).raw("SELECT array_to_json(list)::text t FROM seqs").all().wait().map { try $0.decode(column: "t", as: String?.self) } + let raws = try await (self.db as! any SQLDatabase).raw("SELECT array_to_json(list)::text t FROM seqs").all().map { try $0.decode(column: "t", as: String?.self) } XCTAssertEqual(raws, [#"[{"id": 1},{"id": 2}]"#, nil]) // Make sure it round-trips through Fluent. - let seqs = try Seq.query(on: self.db).all().wait() + let seqs = try await Seq.query(on: self.db).all() XCTAssertEqual(seqs.count, 2) XCTAssertEqual(seqs.dropFirst(0).first?.id, s1.id) @@ -196,47 +242,40 @@ final class FluentPostgresDriverTests: XCTestCase { } catch let error { XCTFail("caught error: \(String(reflecting: error))") } + try await db.schema(Seq.schema).delete() } - var benchmarker: FluentBenchmarker { - return .init(databases: self.dbs) - } - var eventLoopGroup: (any EventLoopGroup)! - var threadPool: NIOThreadPool! + var benchmarker: FluentBenchmarker { .init(databases: self.dbs) } + var eventLoopGroup: any EventLoopGroup { MultiThreadedEventLoopGroup.singleton } + var threadPool: NIOThreadPool { NIOThreadPool.singleton } var dbs: Databases! - var db: any Database { - self.benchmarker.database - } - var postgres: any PostgresDatabase { - self.db as! any PostgresDatabase - } + var db: (any Database)! + var postgres: any PostgresDatabase { self.db as! any PostgresDatabase } - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() XCTAssert(isLoggingConfigured) - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: Swift.min(System.coreCount, 2)) - self.threadPool = NIOThreadPool(numberOfThreads: 1) - self.dbs = Databases(threadPool: threadPool, on: self.eventLoopGroup) + self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup) self.dbs.use(.testPostgres(subconfig: "A"), as: .a) self.dbs.use(.testPostgres(subconfig: "B"), as: .b) - let a = self.dbs.database(.a, logger: Logger(label: "test.fluent.a"), on: self.eventLoopGroup.any()) - _ = try (a as! any PostgresDatabase).query("drop schema public cascade").wait() - _ = try (a as! any PostgresDatabase).query("create schema public").wait() + let a = self.dbs.database(.a, logger: .init(label: "test.fluent.a"), on: self.eventLoopGroup.any()) + _ = try await (a as! any PostgresDatabase).query("drop schema public cascade").get() + _ = try await (a as! any PostgresDatabase).query("create schema public").get() - let b = self.dbs.database(.b, logger: Logger(label: "test.fluent.b"), on: self.eventLoopGroup.any()) - _ = try (b as! any PostgresDatabase).query("drop schema public cascade").wait() - _ = try (b as! any PostgresDatabase).query("create schema public").wait() - } + let b = self.dbs.database(.b, logger: .init(label: "test.fluent.b"), on: self.eventLoopGroup.any()) + _ = try await (b as! any PostgresDatabase).query("drop schema public cascade").get() + _ = try await (b as! any PostgresDatabase).query("create schema public").get() + + self.db = a + } - override func tearDownWithError() throws { + override func tearDown() async throws { self.dbs.shutdown() - try self.threadPool.syncShutdownGracefully() - try self.eventLoopGroup.syncShutdownGracefully() - try super.tearDownWithError() + try await super.tearDown() } } @@ -265,15 +304,11 @@ extension DatabaseID { static let b = DatabaseID(string: "b") } -func env(_ name: String) -> String? { - ProcessInfo.processInfo.environment[name] -} - struct Metadata: Codable { let createdAt: Date } -final class Event: Model { +final class Event: Model, @unchecked Sendable { static let schema = "events" @ID(custom: "id", generatedBy: .database) @@ -283,7 +318,7 @@ final class Event: Model { var metadata: Metadata } -final class EventStringlyTyped: Model { +final class EventStringlyTyped: Model, @unchecked Sendable { static let schema = "events" @ID(custom: "id", generatedBy: .database) @@ -293,20 +328,20 @@ final class EventStringlyTyped: Model { var metadata: [String: String] } -struct EventMigration: Migration { - func prepare(on database: any Database) -> EventLoopFuture<Void> { - database.schema(Event.schema) +struct EventMigration: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema(Event.schema) .field("id", .int, .identifier(auto: true)) .field("metadata", .json, .required) .create() } - func revert(on database: any Database) -> EventLoopFuture<Void> { - database.schema(Event.schema).delete() + func revert(on database: any Database) async throws { + try await database.schema(Event.schema).delete() } } -final class EventWithFoo: Model { +final class EventWithFoo: Model, @unchecked Sendable { static let schema = "foobar_events" @ID @@ -324,58 +359,57 @@ enum Foobar: String, Codable { case qux } -struct EventWithFooMigration: Migration { - func prepare(on database: any Database) -> EventLoopFuture<Void> { - database.enum(Foobar.schema).read() - .flatMap { foobar in - database.schema(EventWithFoo.schema) - .id() - .field("foo", foobar, .required) - .create() - } +struct EventWithFooMigration: AsyncMigration { + func prepare(on database: any Database) async throws { + let foobar = try await database.enum(Foobar.schema).read() + try await database.schema(EventWithFoo.schema) + .id() + .field("foo", foobar, .required) + .create() } - func revert(on database: any Database) -> EventLoopFuture<Void> { - database.schema(EventWithFoo.schema).delete() + func revert(on database: any Database) async throws { + try await database.schema(EventWithFoo.schema).delete() } } -struct EnumMigration: Migration { - func prepare(on database: any Database) -> EventLoopFuture<Void> { - database.enum(Foobar.schema) +struct EnumMigration: AsyncMigration { + func prepare(on database: any Database) async throws { + _ = try await database.enum(Foobar.schema) .case("foo") .case("bar") .create() - .transform(to: ()) } - func revert(on database: any Database) -> EventLoopFuture<Void> { - database.enum(Foobar.schema).delete() + func revert(on database: any Database) async throws { + try await database.enum(Foobar.schema).delete() } } -struct EnumAddMultipleCasesMigration: Migration { - func prepare(on database: any Database) -> EventLoopFuture<Void> { - database.enum(Foobar.schema) +struct EnumAddMultipleCasesMigration: AsyncMigration { + func prepare(on database: any Database) async throws { + _ = try await database.enum(Foobar.schema) .case("baz") .case("qux") .update() - .transform(to: ()) } - func revert(on database: any Database) -> EventLoopFuture<Void> { - database.enum(Foobar.schema) + func revert(on database: any Database) async throws { + _ = try await database.enum(Foobar.schema) .deleteCase("baz") .deleteCase("qux") .update() - .transform(to: ()) } } +func env(_ name: String) -> String? { + ProcessInfo.processInfo.environment[name] +} + let isLoggingConfigured: Bool = { LoggingSystem.bootstrap { label in var handler = StreamLogHandler.standardOutput(label: label) - handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .info + handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info return handler } return true diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift index 727f1e3..fa8f784 100644 --- a/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift +++ b/Tests/FluentPostgresDriverTests/FluentPostgresTransactionControlTests.swift @@ -6,9 +6,9 @@ import XCTest import PostgresKit final class FluentPostgresTransactionControlTests: XCTestCase { - func testRollback() throws { + func testRollback() async throws { do { - try self.db.withConnection { db -> EventLoopFuture<Void> in + try await self.db.withConnection { db -> EventLoopFuture<Void> in (db as! any TransactionControlDatabase).beginTransaction().flatMap { () -> EventLoopFuture<Void> in let todo1 = Todo(title: "Test") return todo1.save(on: db) @@ -22,7 +22,7 @@ final class FluentPostgresTransactionControlTests: XCTestCase { .flatMap { db.eventLoop.makeFailedFuture(e) } } } - }.wait() + }.get() XCTFail("Expected error but none was thrown") } catch let error where String(reflecting: error).contains("sqlState: 23505") { // ignore @@ -30,41 +30,37 @@ final class FluentPostgresTransactionControlTests: XCTestCase { XCTFail("Expected SQL state 23505 but got \(String(reflecting: error))") } - let count2 = try Todo.query(on: self.db).count().wait() + let count2 = try await Todo.query(on: self.db).count() XCTAssertEqual(count2, 0) } - var eventLoopGroup: (any EventLoopGroup)! - var threadPool: NIOThreadPool! + var eventLoopGroup: any EventLoopGroup { MultiThreadedEventLoopGroup.singleton } + var threadPool: NIOThreadPool { NIOThreadPool.singleton } var dbs: Databases! var db: (any Database)! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() XCTAssert(isLoggingConfigured) - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: Swift.min(System.coreCount, 2)) - self.threadPool = NIOThreadPool(numberOfThreads: 1) - self.dbs = Databases(threadPool: threadPool, on: self.eventLoopGroup) + self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup) self.dbs.use(.testPostgres(subconfig: "A"), as: .a) self.db = self.dbs.database(.a, logger: Logger(label: "test.fluent.a"), on: self.eventLoopGroup.any()) - _ = try (self.db as! PostgresDatabase).query("drop schema public cascade").wait() - _ = try (self.db as! PostgresDatabase).query("create schema public").wait() + _ = try await (self.db as! any PostgresDatabase).query("drop schema public cascade").get() + _ = try await (self.db as! any PostgresDatabase).query("create schema public").get() - try CreateTodo().prepare(on: self.db).wait() + try await CreateTodo().prepare(on: self.db) } - override func tearDownWithError() throws { - try CreateTodo().revert(on: self.db).wait() + override func tearDown() async throws { + try await CreateTodo().revert(on: self.db) self.dbs.shutdown() - try self.threadPool.syncShutdownGracefully() - try self.eventLoopGroup.syncShutdownGracefully() - try super.tearDownWithError() + try await super.tearDown() } - final class Todo: Model { + final class Todo: Model, @unchecked Sendable { static let schema = "todos" @ID @@ -77,17 +73,17 @@ final class FluentPostgresTransactionControlTests: XCTestCase { init(title: String) { self.title = title; id = nil } } - struct CreateTodo: Migration { - func prepare(on database: any Database) -> EventLoopFuture<Void> { - database.schema("todos") + struct CreateTodo: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema("todos") .id() .field("title", .string, .required) .unique(on: "title") .create() } - func revert(on database: any Database) -> EventLoopFuture<Void> { - database.schema("todos").delete() + func revert(on database: any Database) async throws { + try await database.schema("todos").delete() } } } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 13b7a21..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3' - -services: - a: - image: postgres - environment: - POSTGRES_USER: vapor_username - POSTGRES_DB: vapor_database - POSTGRES_PASSWORD: vapor_password - ports: - - 5432:5432 - b: - image: postgres - environment: - POSTGRES_USER: vapor_username - POSTGRES_DB: vapor_database - POSTGRES_PASSWORD: vapor_password - ports: - - 5433:5432