diff --git a/Package.resolved b/Package.resolved index a22b9a1..4a91e28 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "7d26a26963ba38c459f6f07137975ce10b735b047b1cfc60429a6e34416f59f6", + "originHash" : "fcb27a1c26d180e4076632541626ef7c9983aea65297bd89ca6a9faf4514ad12", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "revision" : "8bd475b24dcf18b9b03534c99c5ccf626a8d38b9", - "version" : "1.0.0-beta.4" + "branch" : "main", + "revision" : "7e711987d123131739fd042cc4c2f2598d1a8d1d" } }, { diff --git a/Package.swift b/Package.swift index 029f8d3..afa3c16 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/postgres-nio", from: "1.27.0"), - .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.4"), +// .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.4"), + .package(url: "https://github.com/feather-framework/feather-database", branch: "main"), // [docc-plugin-placeholder] ], targets: [ diff --git a/README.md b/README.md index 31f9d48..4b8419a 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Postgres driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. [ - ![Release: 1.0.0-beta.3](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E3-F05138) + ![Release: 1.0.0-beta.4](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E4-F05138) ]( - https://github.com/feather-framework/feather-postgres-database/releases/tag/1.0.0-beta.3 + https://github.com/feather-framework/feather-postgres-database/releases/tag/1.0.0-beta.4 ) ## Features @@ -37,7 +37,7 @@ Postgres driver implementation for the abstract [Feather Database](https://githu Add the dependency to your `Package.swift`: ```swift -.package(url: "https://github.com/feather-framework/feather-postgres-database", exact: "1.0.0-beta.3"), +.package(url: "https://github.com/feather-framework/feather-postgres-database", exact: "1.0.0-beta.4"), ``` Then add `FeatherPostgresDatabase` to your target dependencies: diff --git a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift index 6cd107c..e3b3aab 100644 --- a/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift +++ b/Sources/FeatherPostgresDatabase/PostgresDatabaseConnection.swift @@ -22,6 +22,8 @@ extension DatabaseQuery { .replacing("{{\(idx)}}", with: "$\(idx)") switch binding.binding { + case .bool(let value): + postgresBindings.append(value) case .int(let value): postgresBindings.append(value) case .double(let value): diff --git a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift index 594ea52..bf0330e 100644 --- a/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift +++ b/Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift @@ -792,6 +792,35 @@ struct FeatherPostgresDatabaseTestSuite { } } + @Test + func transactionClosureErrorPropagates() async throws { + try await runUsingTestDatabaseClient { database in + enum TestError: Error, Equatable { + case boom + } + + do { + _ = try await database.withTransaction { _ in + throw TestError.boom + } + Issue.record("Expected transaction error to be thrown.") + } + catch DatabaseError.transaction(let error) { + #expect(error.beginError == nil) + #expect(error.commitError == nil) + #expect(error.rollbackError == nil) + #expect((error.closureError as? TestError) == .boom) + #expect(error.file.isEmpty == false) + #expect(error.line > 0) + } + catch { + Issue.record( + "Expected database transaction error to be thrown." + ) + } + } + } + @Test func concurrentTransactionUpdates() async throws { try await runUsingTestDatabaseClient { database in @@ -1122,6 +1151,217 @@ struct FeatherPostgresDatabaseTestSuite { } } + @Test + func nullDecodingThrowsTypeMismatch() async throws { + try await runUsingTestDatabaseClient { database in + let suffix = randomTableSuffix() + let table = "nullable_values_\(suffix)" + + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" INTEGER + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, NULL); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "value" + FROM "\#(unescaped: table)"; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + + do { + _ = try result[0].decode(column: "value", as: Int.self) + Issue.record("Expected decoding NULL as Int to throw.") + } + catch let DecodingError.typeMismatch(_, context) { + #expect( + context.debugDescription.contains( + "PostgresDecodingError" + ) + ) + } + catch { + Issue.record( + "Expected a typeMismatch error when decoding NULL as Int." + ) + } + } + } + } + + @Test + func nonPostgresDecodableTypeMismatch() async throws { + try await runUsingTestDatabaseClient { database in + let suffix = randomTableSuffix() + let table = "custom_types_\(suffix)" + + struct CustomValue: Decodable, Sendable { + let value: String + } + + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT NOT NULL + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, 'alpha'); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "value" + FROM "\#(unescaped: table)"; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + + do { + _ = try result[0] + .decode( + column: "value", + as: CustomValue.self + ) + Issue.record( + "Expected decoding non-PostgresDecodable type to throw." + ) + } + catch let DecodingError.typeMismatch(_, context) { + #expect( + context.debugDescription.contains( + "Data is not convertible" + ) + ) + } + catch { + Issue.record( + "Expected a typeMismatch error for non-PostgresDecodable types." + ) + } + } + } + } + + @Test + func postgresRowDecodeMetatypeMismatch() async throws { + try await runUsingTestDatabaseClient { database in + let suffix = randomTableSuffix() + let table = "metatype_mismatch_\(suffix)" + + func decodeWithMismatchedMetatype( + row: DatabaseRow, + column: String, + as _: T.Type, + using _: U.Type + ) throws -> T { + // Force a mismatched metatype to exercise the guard cast failure. + let mismatchedType = unsafeBitCast(U.self, to: T.Type.self) + return try row.decode(column: column, as: mismatchedType) + } + + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE "\#(unescaped: table)" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "value" TEXT NOT NULL + ); + """# + ) + + try await connection.run( + query: #""" + INSERT INTO "\#(unescaped: table)" + ("id", "value") + VALUES + (1, 'abc'); + """# + ) + + let result = + try await connection.run( + query: #""" + SELECT "value" + FROM "\#(unescaped: table)"; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + + do { + _ = try decodeWithMismatchedMetatype( + row: result[0], + column: "value", + as: Int.self, + using: String.self + ) + Issue.record( + "Expected mismatched metatype decode to throw." + ) + } + catch let DecodingError.typeMismatch(_, context) { + #expect( + context.debugDescription.contains( + "Could not convert data" + ) + ) + } + catch { + Issue.record( + "Expected a typeMismatch error for metatype mismatch." + ) + } + } + } + } + @Test func queryFailureErrorText() async throws { try await runUsingTestDatabaseClient { database in