Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,41 @@ extension DatabaseConfigurationFactory {
) -> Self {
.init {
FluentPostgresClientConfiguration(
configuration: configuration,
client: .configuration(configuration),
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel,
logger: logger
)
}
}

/// Create a PostgreSQL database configuration from a PostgresClient.
///
/// This is different to the other configuration factories as it uses PostgresNIO's
/// modern PostgresClient connection pool under the hood instead of the AsyncKit pool.
/// The client's lifecycle is expected to be managed by the caller.
///
/// > Warning: The database that's returned using this configuration is not castable
/// > to a `PostgresDatabase`, and `TransactionControlDatabase` is to be used inside
/// > `withConnection`.
///
/// - Parameters:
/// - client: A ``PostgresClient``.
/// - encodingContext: Encoding context to use for serializing data.
/// - decodingContext: Decoding context to use for deserializing data.
/// - sqlLogLevel: Level at which to log SQL queries.
/// - logger: Logger to use in the client.
public static func postgres(
client: PostgresClient,
encodingContext: PostgresEncodingContext<some PostgresJSONEncoder> = .default,
decodingContext: PostgresDecodingContext<some PostgresJSONDecoder> = .default,
sqlLogLevel: Logger.Level = .debug,
logger: Logger
) -> Self {
.init {
FluentPostgresClientConfiguration(
client: .client(client),
encodingContext: encodingContext,
decodingContext: decodingContext,
sqlLogLevel: sqlLogLevel,
Expand All @@ -39,23 +73,41 @@ extension DatabaseConfigurationFactory {
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
struct FluentPostgresClientConfiguration<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: DatabaseConfiguration {
private struct FluentPostgresClientConfiguration<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: DatabaseConfiguration {
enum Client {
case client(PostgresClient)
case configuration(PostgresClient.Configuration)
}

var middleware: [any AnyModelMiddleware] = []
fileprivate let configuration: PostgresClient.Configuration

let client: Client

let encodingContext: PostgresEncodingContext<E>
let decodingContext: PostgresDecodingContext<D>
let sqlLogLevel: Logger.Level
let logger: Logger

func makeDriver(for databases: Databases) -> any DatabaseDriver {
_FluentPostgresClientDriver<E, D>(
client: PostgresClient(configuration: self.configuration, eventLoopGroup: databases.eventLoopGroup, backgroundLogger: logger),
eventLoopGroup: databases.eventLoopGroup,
encodingContext: self.encodingContext,
decodingContext: self.decodingContext,
sqlLogLevel: sqlLogLevel,
logger: logger
)
switch client {
case .client(let client):
_FluentPostgresClientDriver<E, D>(
client: client,
encodingContext: self.encodingContext,
decodingContext: self.decodingContext,
sqlLogLevel: sqlLogLevel,
logger: logger
)

case .configuration(let config):
_FluentPostgresClientDriver<E, D>(
configuration: config,
eventLoopGroup: databases.eventLoopGroup,
encodingContext: self.encodingContext,
decodingContext: self.decodingContext,
sqlLogLevel: sqlLogLevel,
logger: logger
)
}
}
}
41 changes: 31 additions & 10 deletions Sources/FluentPostgresDriver/FluentPostgresClientDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,50 @@ import ServiceLifecycle

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
final class _FluentPostgresClientDriver<E: PostgresJSONEncoder, D: PostgresJSONDecoder>: DatabaseDriver {
enum Lifecycle {
/// The driver created the client and runs it in a private ServiceGroup.
case owned(group: ServiceGroup, runFinished: EventLoopFuture<Void>)
/// The user runs the client elsewhere; the driver only borrows it.
case external
}

let client: PostgresClient

let group: ServiceGroup
// To track service group shutdown
let runFinished: EventLoopFuture<Void>
let lifecycle: Lifecycle

let encodingContext: PostgresEncodingContext<E>
let decodingContext: PostgresDecodingContext<D>
let sqlLogLevel: Logger.Level

init(
client: PostgresClient,
encodingContext: PostgresEncodingContext<E>,
decodingContext: PostgresDecodingContext<D>,
sqlLogLevel: Logger.Level,
logger: Logger
) {
self.client = client
self.lifecycle = .external
self.encodingContext = encodingContext
self.decodingContext = decodingContext
self.sqlLogLevel = sqlLogLevel
}

init(
configuration: PostgresClient.Configuration,
eventLoopGroup: some EventLoopGroup,
encodingContext: PostgresEncodingContext<E>,
decodingContext: PostgresDecodingContext<D>,
sqlLogLevel: Logger.Level,
logger: Logger
) {
let client = PostgresClient(configuration: configuration)
var config = ServiceGroupConfiguration(services: [client], logger: logger)
config.maximumGracefulShutdownDuration = .seconds(10)
let group = ServiceGroup(configuration: config)
let finished = eventLoopGroup.any().makePromise(of: Void.self)

self.group = group
self.runFinished = finished.futureResult
self.lifecycle = .owned(group: group, runFinished: finished.futureResult)

self.client = client
self.encodingContext = encodingContext
self.decodingContext = decodingContext
Expand Down Expand Up @@ -58,12 +77,14 @@ final class _FluentPostgresClientDriver<E: PostgresJSONEncoder, D: PostgresJSOND

// Should be called off loop
func shutdown() {
Task.detached { await self.group.triggerGracefulShutdown() }
try? self.runFinished.wait()
guard case .owned(let group, let finished) = self.lifecycle else { return }
Task.detached { await group.triggerGracefulShutdown() }
try? finished.wait()
}

func shutdownAsync() async {
await self.group.triggerGracefulShutdown()
try? await self.runFinished.get()
guard case .owned(let group, let finished) = self.lifecycle else { return }
await group.triggerGracefulShutdown()
try? await finished.get()
}
}
2 changes: 1 addition & 1 deletion Sources/FluentPostgresDriver/FluentPostgresError.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
enum FluentPostgresError: Error {
package enum FluentPostgresError: Error, Equatable {
case invalidURL(String)
case transactionControlRequiresConnection
}

This file was deleted.

87 changes: 37 additions & 50 deletions Tests/FluentPostgresDriverTests/FluentBenchmarksTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,61 +8,48 @@ import SQLKit
import XCTest
import PostgresNIO

class FluentBenchmarksTests: XCTestCase {
var benchmarker: FluentBenchmarker { .init(databases: self.dbs) }
var dbs: Databases!

final class FluentBenchmarksTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()

XCTAssert(isLoggingConfigured)
self.dbs = Databases(threadPool: .singleton, on: MultiThreadedEventLoopGroup.singleton)

self.dbs.use(.testPostgres(subconfig: "A"), as: .a)
self.dbs.use(.testPostgres(subconfig: "B"), as: .b)

for (id, label) in [(DatabaseID.a, "test.fluent.a"), (.b, "test.fluent.b")] {
let sql = self.dbs.database(id, logger: .init(label: label), on: self.dbs.eventLoopGroup.any()) as! any SQLDatabase
try await sql.raw("drop schema public cascade").run()
try await sql.raw("create schema public").run()
}
}

override func tearDown() async throws {
await self.dbs.shutdownAsync()
try await super.tearDown()
private func forEachDriver(_ body: @escaping @Sendable (FluentBenchmarker) throws -> Void) async throws {
for driver in TestDriver.allCases {
try await withDbs(driver) { dbs, _ in try body(FluentBenchmarker(databases: dbs)) }
}
}

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() }
func testCompositeID() throws { try self.benchmarker.testCompositeID() }
func testCRUD() throws { try self.benchmarker.testCRUD() }
func testEagerLoad() throws { try self.benchmarker.testEagerLoad() }
func testEnum() throws { try self.benchmarker.testEnum() }
func testFilter() throws { try self.benchmarker.testFilter() }
func testGroup() throws { try self.benchmarker.testGroup() }
func testID() throws { try self.benchmarker.testID() }
func testJoin() throws { try self.benchmarker.testJoin() }
func testMiddleware() throws { try self.benchmarker.testMiddleware() }
func testMigrator() throws { try self.benchmarker.testMigrator() }
func testModel() throws { try self.benchmarker.testModel() }
func testOptionalParent() throws { try self.benchmarker.testOptionalParent() }
func testPagination() throws { try self.benchmarker.testPagination() }
func testParent() throws { try self.benchmarker.testParent() }
func testPerformance() throws { try self.benchmarker.testPerformance() }
func testRange() throws { try self.benchmarker.testRange() }
func testSchema() throws { try self.benchmarker.testSchema() }
func testSet() throws { try self.benchmarker.testSet() }
func testSiblings() throws { try self.benchmarker.testSiblings() }
func testSoftDelete() throws { try self.benchmarker.testSoftDelete() }
func testSort() throws { try self.benchmarker.testSort() }
func testSQL() throws { try self.benchmarker.testSQL() }
func testTimestamp() throws { try self.benchmarker.testTimestamp() }
func testTransaction() throws { try self.benchmarker.testTransaction() }
func testUnique() throws { try self.benchmarker.testUnique() }
func testAggregate() async throws { try await self.forEachDriver { try $0.testAggregate() } }
func testArray() async throws { try await self.forEachDriver { try $0.testArray() } }
func testBatch() async throws { try await self.forEachDriver { try $0.testBatch() } }
func testChild() async throws { try await self.forEachDriver { try $0.testChildren() } }
func testChildren() async throws { try await self.forEachDriver { try $0.testChildren() } }
func testChunk() async throws { try await self.forEachDriver { try $0.testChunk() } }
func testCodable() async throws { try await self.forEachDriver { try $0.testCodable() } }
func testCompositeID() async throws { try await self.forEachDriver { try $0.testCompositeID() } }
func testCRUD() async throws { try await self.forEachDriver { try $0.testCRUD() } }
func testEagerLoad() async throws { try await self.forEachDriver { try $0.testEagerLoad() } }
func testEnum() async throws { try await self.forEachDriver { try $0.testEnum() } }
func testFilter() async throws { try await self.forEachDriver { try $0.testFilter() } }
func testGroup() async throws { try await self.forEachDriver { try $0.testGroup() } }
func testID() async throws { try await self.forEachDriver { try $0.testID() } }
func testJoin() async throws { try await self.forEachDriver { try $0.testJoin() } }
func testMiddleware() async throws { try await self.forEachDriver { try $0.testMiddleware() } }
func testMigrator() async throws { try await self.forEachDriver { try $0.testMigrator() } }
func testModel() async throws { try await self.forEachDriver { try $0.testModel() } }
func testOptionalParent() async throws { try await self.forEachDriver { try $0.testOptionalParent() } }
func testPagination() async throws { try await self.forEachDriver { try $0.testPagination() } }
func testParent() async throws { try await self.forEachDriver { try $0.testParent() } }
func testPerformance() async throws { try await self.forEachDriver { try $0.testPerformance() } }
func testRange() async throws { try await self.forEachDriver { try $0.testRange() } }
func testSchema() async throws { try await self.forEachDriver { try $0.testSchema() } }
func testSet() async throws { try await self.forEachDriver { try $0.testSet() } }
func testSiblings() async throws { try await self.forEachDriver { try $0.testSiblings() } }
func testSoftDelete() async throws { try await self.forEachDriver { try $0.testSoftDelete() } }
func testSort() async throws { try await self.forEachDriver { try $0.testSort() } }
func testSQL() async throws { try await self.forEachDriver { try $0.testSQL() } }
func testTimestamp() async throws { try await self.forEachDriver { try $0.testTimestamp() } }
func testTransaction() async throws { try await self.forEachDriver { try $0.testTransaction() } }
func testUnique() async throws { try await self.forEachDriver { try $0.testUnique() } }
}
Loading
Loading