diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 15b15bb..cd903c4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -29,7 +29,7 @@ jobs:
container: swift:noble
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with: { 'fetch-depth': 0 }
- name: API breaking changes
run: |
@@ -74,10 +74,12 @@ jobs:
POSTGRES_HOST_AUTH_METHOD: ${{ matrix.postgres-auth }}
POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }}
steps:
+ - name: Ensure curl is available
+ run: apt-get update -y && apt-get install -y curl
- name: Check out package
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Run all tests
- run: swift test --sanitize=thread --enable-code-coverage
+ run: swift test --enable-code-coverage
- name: Submit coverage report to Codecov.io
uses: vapor/swift-codecov-action@v0.3
with:
@@ -115,6 +117,6 @@ jobs:
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@v4
+ uses: actions/checkout@v5
- name: Run all tests
- run: swift test --sanitize=thread
+ run: swift test
diff --git a/Package.swift b/Package.swift
index 8035350..0ea5ef3 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,8 +13,9 @@ let package = Package(
.library(name: "FluentPostgresDriver", targets: ["FluentPostgresDriver"]),
],
dependencies: [
- .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.49.0"),
- .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.13.4"),
+ .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.52.2"),
+ .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.14.0"),
+ .package(url: "https://github.com/vapor/async-kit.git", from: "1.21.0"),
],
targets: [
.target(
@@ -23,6 +24,7 @@ let package = Package(
.product(name: "FluentKit", package: "fluent-kit"),
.product(name: "FluentSQL", package: "fluent-kit"),
.product(name: "PostgresKit", package: "postgres-kit"),
+ .product(name: "AsyncKit", package: "async-kit"),
],
swiftSettings: swiftSettings
),
@@ -39,6 +41,7 @@ let package = Package(
var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
+ .enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("ConciseMagicFile"),
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("DisableOutwardActorInference"),
diff --git a/Sources/FluentPostgresDriver/Docs.docc/Resources/vapor-fluentpostgresdriver-logo.svg b/Sources/FluentPostgresDriver/Docs.docc/Resources/vapor-fluentpostgresdriver-logo.svg
index 4cc9947..79211b7 100644
--- a/Sources/FluentPostgresDriver/Docs.docc/Resources/vapor-fluentpostgresdriver-logo.svg
+++ b/Sources/FluentPostgresDriver/Docs.docc/Resources/vapor-fluentpostgresdriver-logo.svg
@@ -1,21 +1,25 @@
diff --git a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift
index 4055646..56ca567 100644
--- a/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift
+++ b/Sources/FluentPostgresDriver/FluentPostgresConfiguration.swift
@@ -25,11 +25,12 @@ extension DatabaseConfigurationFactory {
encodingContext: PostgresEncodingContext = .default,
decodingContext: PostgresDecodingContext = .default,
sqlLogLevel: Logger.Level = .debug
- ) throws -> DatabaseConfigurationFactory {
+ ) throws -> Self {
.postgres(
configuration: try .init(url: urlString),
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: nil,
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel
@@ -54,11 +55,12 @@ extension DatabaseConfigurationFactory {
encodingContext: PostgresEncodingContext = .default,
decodingContext: PostgresDecodingContext = .default,
sqlLogLevel: Logger.Level = .debug
- ) throws -> DatabaseConfigurationFactory {
+ ) throws -> Self {
.postgres(
configuration: try .init(url: url),
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: nil,
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel
@@ -81,12 +83,185 @@ extension DatabaseConfigurationFactory {
encodingContext: PostgresEncodingContext,
decodingContext: PostgresDecodingContext,
sqlLogLevel: Logger.Level = .debug
- ) -> DatabaseConfigurationFactory {
+ ) -> Self {
+ .postgres(
+ configuration: configuration,
+ maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
+ connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: nil,
+ encodingContext: encodingContext,
+ decodingContext: decodingContext,
+ sqlLogLevel: sqlLogLevel
+ )
+ }
+}
+
+extension DatabaseConfigurationFactory {
+ /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:)``
+ /// with the `decodingContext` defaulted.
+ public static func postgres(
+ configuration: SQLPostgresConfiguration,
+ maxConnectionsPerEventLoop: Int = 1,
+ connectionPoolTimeout: TimeAmount = .seconds(10),
+ encodingContext: PostgresEncodingContext,
+ sqlLogLevel: Logger.Level = .debug
+ ) -> Self {
+ .postgres(
+ configuration: configuration,
+ maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
+ connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: nil,
+ encodingContext: encodingContext,
+ decodingContext: .default,
+ sqlLogLevel: sqlLogLevel
+ )
+ }
+
+ /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:)``
+ /// with the `encodingContext` defaulted.
+ public static func postgres(
+ configuration: SQLPostgresConfiguration,
+ maxConnectionsPerEventLoop: Int = 1,
+ connectionPoolTimeout: TimeAmount = .seconds(10),
+ decodingContext: PostgresDecodingContext,
+ sqlLogLevel: Logger.Level = .debug
+ ) -> Self {
+ .postgres(
+ configuration: configuration,
+ maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
+ connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: nil,
+ encodingContext: .default,
+ decodingContext: decodingContext,
+ sqlLogLevel: sqlLogLevel
+ )
+ }
+
+ /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:)``
+ /// with both `encodingContext` and `decodingContext` defaulted.
+ public static func postgres(
+ configuration: SQLPostgresConfiguration,
+ maxConnectionsPerEventLoop: Int = 1,
+ connectionPoolTimeout: TimeAmount = .seconds(10),
+ sqlLogLevel: Logger.Level = .debug
+ ) -> Self {
+ .postgres(
+ configuration: configuration,
+ maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
+ connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: nil,
+ encodingContext: .default,
+ decodingContext: .default,
+ sqlLogLevel: sqlLogLevel
+ )
+ }
+}
+
+extension DatabaseConfigurationFactory {
+ /// Create a PostgreSQL database configuration from a URL string.
+ ///
+ /// See ``PostgresKit/SQLPostgresConfiguration/init(url:)`` for the allowed URL format.
+ ///
+ /// - Parameters:
+ /// - urlString: The URL describing the connection, as a string.
+ /// - maxConnectionsPerEventLoop: Maximum number of connections to open per event loop.
+ /// - connectionPoolTimeout: Maximum time to wait for a connection to become available per request.
+ /// - pruneInterval: How often to check for and prune idle database connections. If `nil` (the default),
+ /// no pruning is performed.
+ /// - maxIdleTimeBeforePruning: How long a connection may remain idle before being pruned, if pruning is enabled.
+ /// Defaults to 2 minutes. Ignored if `pruneInterval` is `nil`.
+ /// - encodingContext: Encoding context to use for serializing data.
+ /// - decodingContext: Decoding context to use for deserializing data.
+ /// - sqlLogLevel: Level at which to log SQL queries.
+ public static func postgres(
+ url urlString: String,
+ maxConnectionsPerEventLoop: Int = 1,
+ connectionPoolTimeout: TimeAmount = .seconds(10),
+ pruneInterval: TimeAmount?,
+ maxIdleTimeBeforePruning: TimeAmount = .seconds(120),
+ encodingContext: PostgresEncodingContext = .default,
+ decodingContext: PostgresDecodingContext = .default,
+ sqlLogLevel: Logger.Level = .debug
+ ) throws -> Self {
+ .postgres(
+ configuration: try .init(url: urlString),
+ maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
+ connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: pruneInterval,
+ maxIdleTimeBeforePruning: maxIdleTimeBeforePruning,
+ encodingContext: encodingContext,
+ decodingContext: decodingContext,
+ sqlLogLevel: sqlLogLevel
+ )
+ }
+
+ /// Create a PostgreSQL database configuration from a URL.
+ ///
+ /// See ``PostgresKit/SQLPostgresConfiguration/init(url:)`` for the allowed URL format.
+ ///
+ /// - Parameters:
+ /// - url: The URL describing the connection.
+ /// - maxConnectionsPerEventLoop: Maximum number of connections to open per event loop.
+ /// - connectionPoolTimeout: Maximum time to wait for a connection to become available per request.
+ /// - pruneInterval: How often to check for and prune idle database connections. If `nil` (the default),
+ /// no pruning is performed.
+ /// - maxIdleTimeBeforePruning: How long a connection may remain idle before being pruned, if pruning is enabled.
+ /// Defaults to 2 minutes. Ignored if `pruneInterval` is `nil`.
+ /// - encodingContext: Encoding context to use for serializing data.
+ /// - decodingContext: Decoding context to use for deserializing data.
+ /// - sqlLogLevel: Level at which to log SQL queries.
+ public static func postgres(
+ url: URL,
+ maxConnectionsPerEventLoop: Int = 1,
+ connectionPoolTimeout: TimeAmount = .seconds(10),
+ pruneInterval: TimeAmount?,
+ maxIdleTimeBeforePruning: TimeAmount = .seconds(120),
+ encodingContext: PostgresEncodingContext = .default,
+ decodingContext: PostgresDecodingContext = .default,
+ sqlLogLevel: Logger.Level = .debug
+ ) throws -> Self {
+ .postgres(
+ configuration: try .init(url: url),
+ maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
+ connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: pruneInterval,
+ maxIdleTimeBeforePruning: maxIdleTimeBeforePruning,
+ encodingContext: encodingContext,
+ decodingContext: decodingContext,
+ sqlLogLevel: sqlLogLevel
+ )
+ }
+
+ /// Create a PostgreSQL database configuration from lower-level configuration.
+ ///
+ /// - Parameters:
+ /// - configuration: A ``PostgresKit/SQLPostgresConfiguration`` describing the connection.
+ /// - maxConnectionsPerEventLoop: Maximum number of connections to open per event loop.
+ /// - connectionPoolTimeout: Maximum time to wait for a connection to become available per request.
+ /// - pruneInterval: How often to check for and prune idle database connections. If `nil` (the default),
+ /// no pruning is performed.
+ /// - maxIdleTimeBeforePruning: How long a connection may remain idle before being pruned, if pruning is enabled.
+ /// Defaults to 2 minutes. Ignored if `pruneInterval` is `nil`.
+ /// - encodingContext: Encoding context to use for serializing data.
+ /// - decodingContext: Decoding context to use for deserializing data.
+ /// - sqlLogLevel: Level at which to log SQL queries.
+ public static func postgres(
+ configuration: SQLPostgresConfiguration,
+ maxConnectionsPerEventLoop: Int = 1,
+ connectionPoolTimeout: TimeAmount = .seconds(10),
+ pruneInterval: TimeAmount?,
+ maxIdleTimeBeforePruning: TimeAmount = .seconds(120),
+ encodingContext: PostgresEncodingContext,
+ decodingContext: PostgresDecodingContext,
+ sqlLogLevel: Logger.Level = .debug
+ ) -> Self {
.init {
FluentPostgresConfiguration(
configuration: configuration,
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
+ pruningInterval: pruneInterval,
+ maxIdleTimeBeforePruning: maxIdleTimeBeforePruning,
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel
@@ -108,56 +283,68 @@ extension DatabaseConfigurationFactory {
///
/// _ = DatabaseConfigurationFactory.postgres(configuration: .init(unixDomainSocketPath: "", username: ""))
extension DatabaseConfigurationFactory {
- /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:)``
+ /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:pruneInterval:maxIdleTimeBeforePruning:encodingContext:decodingContext:sqlLogLevel:)``
/// with the `decodingContext` defaulted.
public static func postgres(
configuration: SQLPostgresConfiguration,
maxConnectionsPerEventLoop: Int = 1,
connectionPoolTimeout: TimeAmount = .seconds(10),
+ pruneInterval: TimeAmount?,
+ maxIdleTimeBeforePruning: TimeAmount = .seconds(120),
encodingContext: PostgresEncodingContext,
sqlLogLevel: Logger.Level = .debug
- ) -> DatabaseConfigurationFactory {
+ ) -> Self {
.postgres(
configuration: configuration,
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: pruneInterval,
+ maxIdleTimeBeforePruning: maxIdleTimeBeforePruning,
encodingContext: encodingContext,
decodingContext: .default,
sqlLogLevel: sqlLogLevel
)
}
- /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:)``
+ /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:pruneInterval:maxIdleTimeBeforePruning:encodingContext:decodingContext:sqlLogLevel:)``
/// with the `encodingContext` defaulted.
public static func postgres(
configuration: SQLPostgresConfiguration,
maxConnectionsPerEventLoop: Int = 1,
connectionPoolTimeout: TimeAmount = .seconds(10),
+ pruneInterval: TimeAmount?,
+ maxIdleTimeBeforePruning: TimeAmount = .seconds(120),
decodingContext: PostgresDecodingContext,
sqlLogLevel: Logger.Level = .debug
- ) -> DatabaseConfigurationFactory {
+ ) -> Self {
.postgres(
configuration: configuration,
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: pruneInterval,
+ maxIdleTimeBeforePruning: maxIdleTimeBeforePruning,
encodingContext: .default,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel
)
}
- /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:encodingContext:decodingContext:sqlLogLevel:)``
+ /// ``postgres(configuration:maxConnectionsPerEventLoop:connectionPoolTimeout:pruneInterval:maxIdleTimeBeforePruning:encodingContext:decodingContext:sqlLogLevel:)``
/// with both `encodingContext` and `decodingContext` defaulted.
public static func postgres(
configuration: SQLPostgresConfiguration,
maxConnectionsPerEventLoop: Int = 1,
connectionPoolTimeout: TimeAmount = .seconds(10),
+ pruneInterval: TimeAmount?,
+ maxIdleTimeBeforePruning: TimeAmount = .seconds(120),
sqlLogLevel: Logger.Level = .debug
- ) -> DatabaseConfigurationFactory {
+ ) -> Self {
.postgres(
configuration: configuration,
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
connectionPoolTimeout: connectionPoolTimeout,
+ pruneInterval: pruneInterval,
+ maxIdleTimeBeforePruning: maxIdleTimeBeforePruning,
encodingContext: .default,
decodingContext: .default,
sqlLogLevel: sqlLogLevel
@@ -171,6 +358,8 @@ struct FluentPostgresConfiguration
let decodingContext: PostgresDecodingContext
let sqlLogLevel: Logger.Level
@@ -181,6 +370,8 @@ struct FluentPostgresConfiguration(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture {
+ func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture {
guard !self.inTransaction else {
return closure(self)
}
diff --git a/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift b/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift
index 63e91bd..ed525dd 100644
--- a/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift
+++ b/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift
@@ -20,7 +20,7 @@ struct PostgresConverterDelegate: SQLConverterDelegate {
case .dictionary:
SQLRaw("JSONB")
case .array(of: let type):
- if let type = type, let dataType = self.customDataType(type) {
+ if let type, let dataType = self.customDataType(type) {
SQLArrayDataType(dataType: dataType)
} else {
SQLRaw("JSONB")
diff --git a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
index c268085..a45ebc7 100644
--- a/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
+++ b/Tests/FluentPostgresDriverTests/FluentPostgresDriverTests.swift
@@ -1,6 +1,7 @@
import FluentBenchmark
import FluentKit
import FluentPostgresDriver
+import FluentSQL
import Logging
import PostgresKit
import SQLKit
@@ -297,7 +298,7 @@ extension DatabaseConfigurationFactory {
subconfig: String,
encodingContext: PostgresEncodingContext = .default,
decodingContext: PostgresDecodingContext = .default
- ) -> DatabaseConfigurationFactory {
+ ) -> Self {
let baseSubconfig = SQLPostgresConfiguration(
hostname: env("POSTGRES_HOSTNAME_\(subconfig)") ?? "localhost",
port: env("POSTGRES_PORT_\(subconfig)").flatMap(Int.init) ?? SQLPostgresConfiguration.ianaPortNumber,
@@ -310,6 +311,8 @@ extension DatabaseConfigurationFactory {
return .postgres(
configuration: baseSubconfig,
connectionPoolTimeout: .seconds(30),
+ pruneInterval: .seconds(30),
+ maxIdleTimeBeforePruning: .seconds(60),
encodingContext: encodingContext,
decodingContext: decodingContext
)