@@ -792,6 +792,35 @@ struct FeatherPostgresDatabaseTestSuite {
792792 }
793793 }
794794
795+ @Test
796+ func transactionClosureErrorPropagates( ) async throws {
797+ try await runUsingTestDatabaseClient { database in
798+ enum TestError : Error , Equatable {
799+ case boom
800+ }
801+
802+ do {
803+ _ = try await database. withTransaction { _ in
804+ throw TestError . boom
805+ }
806+ Issue . record ( " Expected transaction error to be thrown. " )
807+ }
808+ catch DatabaseError . transaction( let error) {
809+ #expect( error. beginError == nil )
810+ #expect( error. commitError == nil )
811+ #expect( error. rollbackError == nil )
812+ #expect( ( error. closureError as? TestError ) == . boom)
813+ #expect( error. file. isEmpty == false )
814+ #expect( error. line > 0 )
815+ }
816+ catch {
817+ Issue . record (
818+ " Expected database transaction error to be thrown. "
819+ )
820+ }
821+ }
822+ }
823+
795824 @Test
796825 func concurrentTransactionUpdates( ) async throws {
797826 try await runUsingTestDatabaseClient { database in
@@ -1122,6 +1151,217 @@ struct FeatherPostgresDatabaseTestSuite {
11221151 }
11231152 }
11241153
1154+ @Test
1155+ func nullDecodingThrowsTypeMismatch( ) async throws {
1156+ try await runUsingTestDatabaseClient { database in
1157+ let suffix = randomTableSuffix ( )
1158+ let table = " nullable_values_ \( suffix) "
1159+
1160+ try await database. withConnection { connection in
1161+
1162+ try await connection. run (
1163+ query: #"""
1164+ DROP TABLE IF EXISTS " \#( unescaped: table) " CASCADE;
1165+ """#
1166+ )
1167+ try await connection. run (
1168+ query: #"""
1169+ CREATE TABLE " \#( unescaped: table) " (
1170+ "id" INTEGER NOT NULL PRIMARY KEY,
1171+ "value" INTEGER
1172+ );
1173+ """#
1174+ )
1175+
1176+ try await connection. run (
1177+ query: #"""
1178+ INSERT INTO " \#( unescaped: table) "
1179+ ("id", "value")
1180+ VALUES
1181+ (1, NULL);
1182+ """#
1183+ )
1184+
1185+ let result =
1186+ try await connection. run (
1187+ query: #"""
1188+ SELECT "value"
1189+ FROM " \#( unescaped: table) ";
1190+ """#
1191+ ) { try await $0. collect ( ) }
1192+
1193+ #expect( result. count == 1 )
1194+
1195+ do {
1196+ _ = try result [ 0 ] . decode ( column: " value " , as: Int . self)
1197+ Issue . record ( " Expected decoding NULL as Int to throw. " )
1198+ }
1199+ catch let DecodingError . typeMismatch( _, context) {
1200+ #expect(
1201+ context. debugDescription. contains (
1202+ " PostgresDecodingError "
1203+ )
1204+ )
1205+ }
1206+ catch {
1207+ Issue . record (
1208+ " Expected a typeMismatch error when decoding NULL as Int. "
1209+ )
1210+ }
1211+ }
1212+ }
1213+ }
1214+
1215+ @Test
1216+ func nonPostgresDecodableTypeMismatch( ) async throws {
1217+ try await runUsingTestDatabaseClient { database in
1218+ let suffix = randomTableSuffix ( )
1219+ let table = " custom_types_ \( suffix) "
1220+
1221+ struct CustomValue : Decodable , Sendable {
1222+ let value : String
1223+ }
1224+
1225+ try await database. withConnection { connection in
1226+
1227+ try await connection. run (
1228+ query: #"""
1229+ DROP TABLE IF EXISTS " \#( unescaped: table) " CASCADE;
1230+ """#
1231+ )
1232+ try await connection. run (
1233+ query: #"""
1234+ CREATE TABLE " \#( unescaped: table) " (
1235+ "id" INTEGER NOT NULL PRIMARY KEY,
1236+ "value" TEXT NOT NULL
1237+ );
1238+ """#
1239+ )
1240+
1241+ try await connection. run (
1242+ query: #"""
1243+ INSERT INTO " \#( unescaped: table) "
1244+ ("id", "value")
1245+ VALUES
1246+ (1, 'alpha');
1247+ """#
1248+ )
1249+
1250+ let result =
1251+ try await connection. run (
1252+ query: #"""
1253+ SELECT "value"
1254+ FROM " \#( unescaped: table) ";
1255+ """#
1256+ ) { try await $0. collect ( ) }
1257+
1258+ #expect( result. count == 1 )
1259+
1260+ do {
1261+ _ = try result [ 0 ]
1262+ . decode (
1263+ column: " value " ,
1264+ as: CustomValue . self
1265+ )
1266+ Issue . record (
1267+ " Expected decoding non-PostgresDecodable type to throw. "
1268+ )
1269+ }
1270+ catch let DecodingError . typeMismatch( _, context) {
1271+ #expect(
1272+ context. debugDescription. contains (
1273+ " Data is not convertible "
1274+ )
1275+ )
1276+ }
1277+ catch {
1278+ Issue . record (
1279+ " Expected a typeMismatch error for non-PostgresDecodable types. "
1280+ )
1281+ }
1282+ }
1283+ }
1284+ }
1285+
1286+ @Test
1287+ func postgresRowDecodeMetatypeMismatch( ) async throws {
1288+ try await runUsingTestDatabaseClient { database in
1289+ let suffix = randomTableSuffix ( )
1290+ let table = " metatype_mismatch_ \( suffix) "
1291+
1292+ func decodeWithMismatchedMetatype< T: Decodable , U: Decodable > (
1293+ row: DatabaseRow ,
1294+ column: String ,
1295+ as _: T . Type ,
1296+ using _: U . Type
1297+ ) throws -> T {
1298+ // Force a mismatched metatype to exercise the guard cast failure.
1299+ let mismatchedType = unsafeBitCast ( U . self, to: T . Type. self)
1300+ return try row. decode ( column: column, as: mismatchedType)
1301+ }
1302+
1303+ try await database. withConnection { connection in
1304+
1305+ try await connection. run (
1306+ query: #"""
1307+ DROP TABLE IF EXISTS " \#( unescaped: table) " CASCADE;
1308+ """#
1309+ )
1310+ try await connection. run (
1311+ query: #"""
1312+ CREATE TABLE " \#( unescaped: table) " (
1313+ "id" INTEGER NOT NULL PRIMARY KEY,
1314+ "value" TEXT NOT NULL
1315+ );
1316+ """#
1317+ )
1318+
1319+ try await connection. run (
1320+ query: #"""
1321+ INSERT INTO " \#( unescaped: table) "
1322+ ("id", "value")
1323+ VALUES
1324+ (1, 'abc');
1325+ """#
1326+ )
1327+
1328+ let result =
1329+ try await connection. run (
1330+ query: #"""
1331+ SELECT "value"
1332+ FROM " \#( unescaped: table) ";
1333+ """#
1334+ ) { try await $0. collect ( ) }
1335+
1336+ #expect( result. count == 1 )
1337+
1338+ do {
1339+ _ = try decodeWithMismatchedMetatype (
1340+ row: result [ 0 ] ,
1341+ column: " value " ,
1342+ as: Int . self,
1343+ using: String . self
1344+ )
1345+ Issue . record (
1346+ " Expected mismatched metatype decode to throw. "
1347+ )
1348+ }
1349+ catch let DecodingError . typeMismatch( _, context) {
1350+ #expect(
1351+ context. debugDescription. contains (
1352+ " Could not convert data "
1353+ )
1354+ )
1355+ }
1356+ catch {
1357+ Issue . record (
1358+ " Expected a typeMismatch error for metatype mismatch. "
1359+ )
1360+ }
1361+ }
1362+ }
1363+ }
1364+
11251365 @Test
11261366 func queryFailureErrorText( ) async throws {
11271367 try await runUsingTestDatabaseClient { database in
0 commit comments