import Foundation import XCTest @testable import GRDB class FetchableRecordDecodableTests: GRDBTestCase { } // MARK: - FetchableRecord conformance derived from Decodable extension FetchableRecordDecodableTests { func testTrivialDecodable() throws { struct Struct : FetchableRecord, Decodable { let value: String } do { let s = try Struct(row: ["value": "foo"]) XCTAssertEqual(s.value, "foo") } } func testCustomDecodable() throws { struct Struct : FetchableRecord, Decodable { let value: String private enum CodingKeys : String, CodingKey { case value = "someColumn" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) value = try container.decode(String.self, forKey: .value) } } do { let s = try Struct(row: ["someColumn": "foo"]) XCTAssertEqual(s.value, "foo") } } func testCustomFetchableRecord() { struct Struct : FetchableRecord, Decodable { let value: String init(row: Row) { value = (row["value"] as String) + " (FetchableRecord)" } } do { let s = Struct(row: ["value": "foo"]) XCTAssertEqual(s.value, "foo (FetchableRecord)") } } } // MARK: - Different kinds of single-value properties extension FetchableRecordDecodableTests { func testTrivialProperty() throws { struct Struct : FetchableRecord, Decodable { let int64: Int64 let optionalInt64: Int64? } do { // No null values let s = try Struct(row: ["int64": 1, "optionalInt64": 2]) XCTAssertEqual(s.int64, 1) XCTAssertEqual(s.optionalInt64, 2) } do { // Null values let s = try Struct(row: ["int64": 2, "optionalInt64": nil]) XCTAssertEqual(s.int64, 2) XCTAssertNil(s.optionalInt64) } do { // Missing and extra values let s = try Struct(row: ["int64": 3, "ignored": "?"]) XCTAssertEqual(s.int64, 3) XCTAssertNil(s.optionalInt64) } } func testTrivialSingleValueDecodableProperty() throws { struct Value : Decodable { let string: String init(from decoder: Decoder) throws { string = try decoder.singleValueContainer().decode(String.self) } } struct Struct : FetchableRecord, Decodable { let value: Value let optionalValue: Value? } do { // No null values let s = try Struct(row: ["value": "foo", "optionalValue": "bar"]) XCTAssertEqual(s.value.string, "foo") XCTAssertEqual(s.optionalValue!.string, "bar") } do { // Null values let s = try Struct(row: ["value": "foo", "optionalValue": nil]) XCTAssertEqual(s.value.string, "foo") XCTAssertNil(s.optionalValue) } do { // Missing and extra values let s = try Struct(row: ["value": "foo", "ignored": "?"]) XCTAssertEqual(s.value.string, "foo") XCTAssertNil(s.optionalValue) } } func testNonTrivialSingleValueDecodableProperty() throws { struct NestedValue : Decodable { let string: String init(from decoder: Decoder) throws { string = try decoder.singleValueContainer().decode(String.self) } } struct Value : Decodable { let nestedValue: NestedValue init(from decoder: Decoder) throws { nestedValue = try decoder.singleValueContainer().decode(NestedValue.self) } } struct Struct : FetchableRecord, Decodable { let value: Value let optionalValue: Value? } do { // No null values let s = try Struct(row: ["value": "foo", "optionalValue": "bar"]) XCTAssertEqual(s.value.nestedValue.string, "foo") XCTAssertEqual(s.optionalValue!.nestedValue.string, "bar") } do { // Null values let s = try Struct(row: ["value": "foo", "optionalValue": nil]) XCTAssertEqual(s.value.nestedValue.string, "foo") XCTAssertNil(s.optionalValue) } do { // Missing and extra values let s = try Struct(row: ["value": "foo", "ignored": "?"]) XCTAssertEqual(s.value.nestedValue.string, "foo") XCTAssertNil(s.optionalValue) } } func testDecodableRawRepresentableProperty() throws { // This test is somewhat redundant with testSingleValueDecodableProperty, // since a RawRepresentable enum is a "single-value" Decodable. // // But with an explicit test for enums, we are sure that enums, widely // used, are supported. enum Value : String, Decodable { case foo, bar } struct Struct : FetchableRecord, Decodable { let value: Value let optionalValue: Value? } do { // No null values let s = try Struct(row: ["value": "foo", "optionalValue": "bar"]) XCTAssertEqual(s.value, .foo) XCTAssertEqual(s.optionalValue!, .bar) } do { // Null values let s = try Struct(row: ["value": "foo", "optionalValue": nil]) XCTAssertEqual(s.value, .foo) XCTAssertNil(s.optionalValue) } do { // Missing and extra values let s = try Struct(row: ["value": "foo", "ignored": "?"]) XCTAssertEqual(s.value, .foo) XCTAssertNil(s.optionalValue) } } func testDatabaseValueConvertibleProperty() throws { // This test makes sure that Date, for example, can be read from a String. // // Without this preference for fromDatabaseValue(_:) over init(from:Decoder), // Date would only decode from doubles. struct Value : Decodable, DatabaseValueConvertible { let string: String init(string: String) { self.string = string } init(from decoder: Decoder) throws { string = try decoder.singleValueContainer().decode(String.self) + " (Decodable)" } // DatabaseValueConvertible adoption var databaseValue: DatabaseValue { fatalError("irrelevant") } static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { if let string = String.fromDatabaseValue(databaseValue) { return Value(string: string + " (DatabaseValueConvertible)") } else { return nil } } } struct Struct : FetchableRecord, Decodable { let value: Value let optionalValue: Value? } do { // No null values let s = try Struct(row: ["value": "foo", "optionalValue": "bar"]) XCTAssertEqual(s.value.string, "foo (DatabaseValueConvertible)") XCTAssertEqual(s.optionalValue!.string, "bar (DatabaseValueConvertible)") } do { // Null values let s = try Struct(row: ["value": "foo", "optionalValue": nil]) XCTAssertEqual(s.value.string, "foo (DatabaseValueConvertible)") XCTAssertNil(s.optionalValue) } do { // Missing and extra values let s = try Struct(row: ["value": "foo", "ignored": "?"]) XCTAssertEqual(s.value.string, "foo (DatabaseValueConvertible)") XCTAssertNil(s.optionalValue) } } } // MARK: - Foundation Codable Types extension FetchableRecordDecodableTests { func testStructWithData() throws { struct StructWithData : FetchableRecord, Decodable { let data: Data } let dbQueue = try makeDatabaseQueue() do { let data = "foo".data(using: .utf8) do { let value = try StructWithData(row: ["data": data]) XCTAssertEqual(value.data, data) } do { let value = try dbQueue.read { try StructWithData.fetchOne($0, sql: "SELECT ? AS data", arguments: [data])! } XCTAssertEqual(value.data, data) } } do { do { _ = try StructWithData(row: ["data": nil]) XCTFail("Expected Error") } catch let error as RowDecodingError { switch error { case .valueMismatch: XCTAssertEqual(error.description, """ could not decode Data from database value NULL - \ column: "data", \ column index: 0, \ row: [data:NULL] """) default: XCTFail("Unexpected Error") } } do { try dbQueue.read { _ = try StructWithData.fetchOne($0, sql: "SELECT NULL AS data") } XCTFail("Expected Error") } catch let error as RowDecodingError { switch error { case .valueMismatch: XCTAssertEqual(error.description, """ could not decode Data from database value NULL - \ column: "data", \ column index: 0, \ row: [data:NULL], \ sql: `SELECT NULL AS data`, \ arguments: [] """) default: XCTFail("Unexpected Error") } } } } func testStructWithDate() throws { struct StructWithDate : FetchableRecord, Decodable { let date: Date } let dbQueue = try makeDatabaseQueue() do { let date = Date() do { let value = try StructWithDate(row: ["date": date]) XCTAssert(abs(value.date.timeIntervalSince(date)) < 0.001) } do { let value = try dbQueue.read { try StructWithDate.fetchOne($0, sql: "SELECT ? AS date", arguments: [date])! } XCTAssert(abs(value.date.timeIntervalSince(date)) < 0.001) } } do { do { _ = try StructWithDate(row: ["date": nil]) XCTFail("Expected Error") } catch let error as RowDecodingError { switch error { case .valueMismatch: XCTAssertEqual(error.description, """ could not decode Date from database value NULL - \ column: "date", \ column index: 0, \ row: [date:NULL] """) default: XCTFail("Unexpected Error") } } do { try dbQueue.read { _ = try StructWithDate.fetchOne($0, sql: "SELECT NULL AS date") } XCTFail("Expected Error") } catch let error as RowDecodingError { switch error { case .valueMismatch: XCTAssertEqual(error.description, """ could not decode Date from database value NULL - \ column: "date", \ column index: 0, \ row: [date:NULL], \ sql: `SELECT NULL AS date`, \ arguments: [] """) default: XCTFail("Unexpected Error") } } } } func testStructWithURL() throws { struct StructWithURL : FetchableRecord, Decodable { let url: URL } let url = URL(string: "https://github.com") let value = try StructWithURL(row: ["url": url]) XCTAssertEqual(value.url, url) } func testStructWithUUID() throws { struct StructWithUUID : FetchableRecord, Decodable { let uuid: UUID } let uuid = UUID() let value = try StructWithUUID(row: ["uuid": uuid]) XCTAssertEqual(value.uuid, uuid) } } // MARK: - Custom nested Decodable types - nested saved as JSON extension FetchableRecordDecodableTests { func testOptionalNestedStruct() throws { struct NestedStruct : Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let nested: NestedStruct? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: nested) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) guard let nestedModel = parentModel.first?.nested else { XCTFail() return } // Check the nested model contains the expected values of first and last name XCTAssertEqual(nestedModel.firstName, "Bob") XCTAssertEqual(nestedModel.lastName, "Dylan") } } func testOptionalNestedStructNil() throws { struct NestedStruct : Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let nested: NestedStruct? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let value = StructWithNestedType(nested: nil) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) XCTAssertNil(parentModel.first?.nested) } } func testOptionalNestedArrayStruct() throws { struct NestedStruct : Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let nested: [NestedStruct]? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: [nested, nested]) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) guard let arrayOfNestedModel = parentModel.first?.nested, let firstNestedModelInArray = arrayOfNestedModel.first else { XCTFail() return } // Check there are two models in array XCTAssertTrue(arrayOfNestedModel.count == 2) // Check the nested model contains the expected values of first and last name XCTAssertEqual(firstNestedModelInArray.firstName, "Bob") XCTAssertEqual(firstNestedModelInArray.lastName, "Dylan") } } func testOptionalNestedArrayStructNil() throws { struct NestedStruct: Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let nested: [NestedStruct]? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let value = StructWithNestedType(nested: nil) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) XCTAssertNil(parentModel.first?.nested) } } func testNonOptionalNestedStruct() throws { struct NestedStruct: Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let nested: NestedStruct } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: nested) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) guard let nestedModel = parentModel.first?.nested else { XCTFail() return } // Check the nested model contains the expected values of first and last name XCTAssertEqual(nestedModel.firstName, "Bob") XCTAssertEqual(nestedModel.lastName, "Dylan") } } func testNonOptionalNestedArrayStruct() throws { struct NestedStruct : Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let nested: [NestedStruct] } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } } try dbQueue.inTransaction { db in let value = StructWithNestedType(nested: [ NestedStruct(firstName: "Bob", lastName: "Dylan"), NestedStruct(firstName: "Bob", lastName: "Dylan")]) try value.insert(db) let parentModel = try StructWithNestedType.fetchOne(db) guard let nested = parentModel?.nested else { XCTFail() return .rollback } // Check there are two models in array XCTAssertEqual(nested.count, 2) // Check the nested model contains the expected values of first and last name XCTAssertEqual(nested[0].firstName, "Bob") XCTAssertEqual(nested[0].lastName, "Dylan") return .rollback } try dbQueue.inTransaction { db in let value = StructWithNestedType(nested: []) try value.insert(db) let parentModel = try StructWithNestedType.fetchOne(db) guard let nested = parentModel?.nested else { XCTFail() return .rollback } XCTAssertTrue(nested.isEmpty) return .rollback } } func testCodableExampleCode() throws { struct Player: PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let name: String let score: Int let scores: [Int] let lastMedal: PlayerMedal let medals: [PlayerMedal] let timeline: [String: PlayerMedal] } // A simple Codable that will be nested in a parent Codable struct PlayerMedal : Codable { let name: String? let type: String? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("name", .text) t.column("score", .integer) t.column("scores", .integer) t.column("lastMedal", .text) t.column("medals", .text) t.column("timeline", .text) } let medal1 = PlayerMedal(name: "First", type: "Gold") let medal2 = PlayerMedal(name: "Second", type: "Silver") let timeline = ["Local Contest": medal1, "National Contest": medal2] let value = Player(name: "PlayerName", score: 10, scores: [1,2,3,4,5], lastMedal: medal1, medals: [medal1, medal2], timeline: timeline) try value.insert(db) let parentModel = try Player.fetchAll(db) guard let first = parentModel.first, let firstNestedModelInArray = first.medals.first, let secondNestedModelInArray = first.medals.last else { XCTFail() return } // Check there are two models in array XCTAssertTrue(first.medals.count == 2) // Check the nested model contains the expected values of first and last name XCTAssertEqual(firstNestedModelInArray.name, "First") XCTAssertEqual(secondNestedModelInArray.name, "Second") XCTAssertEqual(first.name, "PlayerName") XCTAssertEqual(first.score, 10) XCTAssertEqual(first.scores, [1,2,3,4,5]) XCTAssertEqual(first.lastMedal.name, medal1.name) XCTAssertEqual(first.timeline["Local Contest"]?.name, medal1.name) XCTAssertEqual(first.timeline["National Contest"]?.name, medal2.name) } } // MARK: - JSON data in Detached Rows func testDetachedRows() throws { struct NestedStruct : PersistableRecord, FetchableRecord, Codable { let firstName: String? let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let nested: NestedStruct } let row: Row = ["nested": """ {"firstName":"Bob","lastName":"Dylan"} """] let model = try StructWithNestedType(row: row) XCTAssertEqual(model.nested.firstName, "Bob") XCTAssertEqual(model.nested.lastName, "Dylan") } func testArrayOfDetachedRowsAsData() throws { struct TestStruct : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let name: String } let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" let jsonAsData = jsonAsString.data(using: .utf8) let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("name", .text) } let model = TestStruct(name: jsonAsString) try model.insert(db) } try dbQueue.read { db in // ... with an array of detached rows: let array = try Row.fetchAll(db, sql: "SELECT * FROM t1") for row in array { let data1: Data? = row["name"] XCTAssertEqual(jsonAsData, data1) try row.withUnsafeData(named: "name") { data in XCTAssertEqual(jsonAsData, data) } } } } func testArrayOfDetachedRowsAsString() throws { struct TestStruct : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let name: String } let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("name", .text) } let model = TestStruct(name: jsonAsString) try model.insert(db) } try dbQueue.read { db in // ... with an array of detached rows: let array = try Row.fetchAll(db, sql: "SELECT * FROM t1") for row in array { let string: String? = row["name"] XCTAssertEqual(jsonAsString, string) } } } func testCursorRowsAsData() throws { struct TestStruct : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let name: String } let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" let jsonAsData = jsonAsString.data(using: .utf8) let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("name", .text) } let model = TestStruct(name: jsonAsString) try model.insert(db) } try dbQueue.read { db in // Compare cursor of low-level rows: let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM t1") while let row = try cursor.next() { let data1: Data? = row["name"] XCTAssertEqual(jsonAsData, data1) try row.withUnsafeData(named: "name") { data in XCTAssertEqual(jsonAsData, data) } } } } func testCursorRowsAsString() throws { struct TestStruct : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" let name: String } let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("name", .text) } let model = TestStruct(name: jsonAsString) try model.insert(db) } try dbQueue.read { db in // Compare cursor of low-level rows: let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM t1") while let row = try cursor.next() { let string: String? = row["name"] XCTAssertEqual(jsonAsString, string) } } } func testJSONDataEncodingStrategy() throws { struct Record: FetchableRecord, Decodable { let data: Data let optionalData: Data? let datas: [Data] let optionalDatas: [Data?] } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let data = "foo".data(using: .utf8)! let record = try Record.fetchOne(db, sql: "SELECT ? AS data, ? AS optionalData, ? AS datas, ? AS optionalDatas", arguments: [ data, data, "[\"Zm9v\"]", "[null, \"Zm9v\"]" ])! XCTAssertEqual(record.data, data) XCTAssertEqual(record.optionalData!, data) XCTAssertEqual(record.datas.count, 1) XCTAssertEqual(record.datas[0], data) XCTAssertEqual(record.optionalDatas.count, 2) XCTAssertNil(record.optionalDatas[0]) XCTAssertEqual(record.optionalDatas[1]!, data) } } func testJSONDateEncodingStrategy() throws { struct Record: FetchableRecord, Decodable { let date: Date let optionalDate: Date? let dates: [Date] let optionalDates: [Date?] } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let record = try Record.fetchOne(db, sql: "SELECT ? AS date, ? AS optionalDate, ? AS dates, ? AS optionalDates", arguments: [ "1970-01-01 00:02:08.000", "1970-01-01 00:02:08.000", "[128000]", "[null,128000]" ])! XCTAssertEqual(record.date.timeIntervalSince1970, 128) XCTAssertEqual(record.optionalDate!.timeIntervalSince1970, 128) XCTAssertEqual(record.dates.count, 1) XCTAssertEqual(record.dates[0].timeIntervalSince1970, 128) XCTAssertEqual(record.optionalDates.count, 2) XCTAssertNil(record.optionalDates[0]) XCTAssertEqual(record.optionalDates[1]!.timeIntervalSince1970, 128) } } // MARK: - DatabaseColumnDecodingStrategy func testDatabaseColumnDecodingStrategy_useDefaultKeys() throws { struct Record: FetchableRecord, Decodable, Equatable { static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.useDefaultKeys let requiredId: Int let optionalName: String? let requiredDates: [Date] let optionalDates: [Date?] let optionalDates2: [Date]? } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "requiredId": 1, "optionalName": "test1", "requiredDates": "[128000]", "optionalDates": "[null, 128000]", "optionalDates2": "[128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: "test1", requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: [Date(timeIntervalSince1970: 128)])) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "REQUIREDID": 1, "OPTIONALNAME": "test1", "REQUIREDDATES": "[128000]", "OPTIONALDATES": "[null, 128000]", "OPTIONALDATES2": "[128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: "test1", requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: [Date(timeIntervalSince1970: 128)])) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "requiredId": 1, "optionalName": nil, "requiredDates": "[128000]", "optionalDates": "[null, 128000]", "optionalDates2": nil, ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: nil, requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: nil)) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "requiredId": 1, "requiredDates": "[128000]", "optionalDates": "[null, 128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: nil, requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: nil)) } do { _ = try FetchableRecordDecoder().decode(Record.self, from: [ "required_id": 1, "optionalName": "test1", "requiredDates": "[128000]", "optionalDates": "[null, 128000]", "optionalDates2": "[128000]", ]) XCTFail("Expected error") } catch let RowDecodingError.keyNotFound(.columnName(column), context) { XCTAssertEqual(column, "requiredId") XCTAssertEqual(context.debugDescription, "column not found: \"requiredId\"") } } func testDatabaseColumnDecodingStrategy_convertFromSnakeCase() throws { struct Record: FetchableRecord, Decodable, Equatable { static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase let requiredId: Int let optionalName: String? let requiredDates: [Date] let optionalDates: [Date?] let optionalDates2: [Date]? } struct IncorrectRecord: FetchableRecord, Decodable, Equatable { static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.convertFromSnakeCase let requiredID: Int // incorrect: should be requiredId let optionalName: String? let requiredDates: [Date] let optionalDates: [Date?] let optionalDates2: [Date]? } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "required_id": 1, "optional_name": "test1", "required_dates": "[128000]", "optional_dates": "[null, 128000]", "optional_dates2": "[128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: "test1", requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: [Date(timeIntervalSince1970: 128)])) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "REQUIRED_ID": 1, "OPTIONAL_NAME": "test1", "REQUIRED_DATES": "[128000]", "OPTIONAL_DATES": "[null, 128000]", "OPTIONAL_DATES2": "[128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: "test1", requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: [Date(timeIntervalSince1970: 128)])) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "required_id": 1, "optional_name": nil, "required_dates": "[128000]", "optional_dates": "[null, 128000]", "optional_dates2": nil, ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: nil, requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: nil)) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "required_id": 1, "required_dates": "[128000]", "optional_dates": "[null, 128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: nil, requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: nil)) } do { // Matches JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase behavior let record = try FetchableRecordDecoder().decode(Record.self, from: [ "requiredId": 1, "optionalName": "test1", "requiredDates": "[128000]", "optionalDates": "[null, 128000]", "optionalDates2": "[128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: "test1", requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: [Date(timeIntervalSince1970: 128)])) } do { _ = try FetchableRecordDecoder().decode(Record.self, from: [ "required_idx": 1, "optional_name": "test1", "required_dates": "[128000]", "optional_dates": "[null, 128000]", "optional_dates2": "[128000]", ]) XCTFail("Expected error") } catch let RowDecodingError.keyNotFound(.columnName(column), context) { XCTAssertEqual(column, "requiredId") XCTAssertEqual(context.debugDescription, """ key not found: CodingKeys(stringValue: "requiredId", intValue: nil) ("requiredId"), \ converted to required_id """) } do { _ = try FetchableRecordDecoder().decode(IncorrectRecord.self, from: [ "required_id": 1, "optional_name": "test1", "required_dates": "[128000]", "optional_dates": "[null, 128000]", "optional_dates2": "[128000]", ]) XCTFail("Expected error") } catch let RowDecodingError.keyNotFound(.columnName(column), context) { XCTAssertEqual(column, "requiredID") XCTAssertEqual(context.debugDescription, """ key not found: CodingKeys(stringValue: "requiredID", intValue: nil) ("requiredID"), \ with divergent representation requiredId, \ converted to required_id """) } } func testDatabaseColumnDecodingStrategy_custom() throws { struct AnyKey: CodingKey { var stringValue: String var intValue: Int? { nil } init(stringValue: String) { self.stringValue = stringValue } init?(intValue: Int) { nil } } struct Record: FetchableRecord, Decodable, Equatable { static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.custom { column in AnyKey(stringValue: String(column.dropFirst())) } let requiredId: Int let optionalName: String? let requiredDates: [Date] let optionalDates: [Date?] let optionalDates2: [Date]? } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "_requiredId": 1, "_optionalName": "test1", "_requiredDates": "[128000]", "_optionalDates": "[null, 128000]", "_optionalDates2": "[128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: "test1", requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: [Date(timeIntervalSince1970: 128)])) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "_requiredId": 1, "_optionalName": nil, "_requiredDates": "[128000]", "_optionalDates": "[null, 128000]", "_optionalDates2": nil, ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: nil, requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: nil)) } do { let record = try FetchableRecordDecoder().decode(Record.self, from: [ "_requiredId": 1, "_requiredDates": "[128000]", "_optionalDates": "[null, 128000]", ]) XCTAssertEqual(record, Record( requiredId: 1, optionalName: nil, requiredDates: [Date(timeIntervalSince1970: 128)], optionalDates: [nil, Date(timeIntervalSince1970: 128)], optionalDates2: nil)) } do { _ = try FetchableRecordDecoder().decode(Record.self, from: [ "requiredId": 1, "_optionalName": "test1", "_requiredDates": "[128000]", "_optionalDates": "[null, 128000]", "_optionalDates2": "[128000]", ]) XCTFail("Expected error") } catch let RowDecodingError.keyNotFound(.columnName(column), context) { XCTAssertEqual(column, "requiredId") XCTAssertEqual(context.debugDescription, """ key not found: CodingKeys(stringValue: "requiredId", intValue: nil) ("requiredId") """) } } } // MARK: - User Infos & Coding Keys private let testKeyRoot = CodingUserInfoKey(rawValue: "test1")! private let testKeyNested = CodingUserInfoKey(rawValue: "test2")! extension FetchableRecordDecodableTests { struct NestedKeyed: Decodable { var name: String var key: String? var context: String? enum CodingKeys: String, CodingKey { case name } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) key = decoder.codingPath.last?.stringValue context = decoder.userInfo[testKeyNested] as? String } } struct NestedSingle: Decodable { var name: String var key: String? var context: String? init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() name = try container.decode(String.self) key = decoder.codingPath.last?.stringValue context = decoder.userInfo[testKeyNested] as? String } } struct NestedUnkeyed: Decodable { var name: String var key: String? var context: String? init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() name = try container.decode(String.self) key = decoder.codingPath.last?.stringValue context = decoder.userInfo[testKeyNested] as? String } } struct Record: Decodable, FetchableRecord { var nestedKeyed: NestedKeyed var nestedSingle: NestedSingle var nestedUnkeyed: NestedUnkeyed var key: String? var context: String? enum CodingKeys: String, CodingKey { case nestedKeyed, nestedSingle, nestedUnkeyed } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) nestedKeyed = try container.decode(NestedKeyed.self, forKey: .nestedKeyed) nestedSingle = try container.decode(NestedSingle.self, forKey: .nestedSingle) nestedUnkeyed = try container.decode(NestedUnkeyed.self, forKey: .nestedUnkeyed) key = decoder.codingPath.last?.stringValue context = decoder.userInfo[testKeyRoot] as? String } } class CustomizedRecord: Decodable, FetchableRecord { var nestedKeyed: NestedKeyed var nestedSingle: NestedSingle var nestedUnkeyed: NestedUnkeyed var key: String? var context: String? enum CodingKeys: String, CodingKey { case nestedKeyed, nestedSingle, nestedUnkeyed } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) nestedKeyed = try container.decode(NestedKeyed.self, forKey: .nestedKeyed) nestedSingle = try container.decode(NestedSingle.self, forKey: .nestedSingle) nestedUnkeyed = try container.decode(NestedUnkeyed.self, forKey: .nestedUnkeyed) key = decoder.codingPath.last?.stringValue context = decoder.userInfo[testKeyRoot] as? String } static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [ testKeyRoot: "GRDB root", testKeyNested: "GRDB column or scope"] static func databaseJSONDecoder(for column: String) -> JSONDecoder { let decoder = JSONDecoder() decoder.userInfo = [ testKeyRoot: "JSON root", testKeyNested: "JSON column: \(column)"] return decoder } } // Used as a reference func testFoundationBehavior() throws { let json = """ { "nestedKeyed": { "name": "foo" }, "nestedSingle": "bar", "nestedUnkeyed": ["baz"] } """.data(using: .utf8)! do { let decoder = JSONDecoder() let record = try decoder.decode(Record.self, from: json) XCTAssertNil(record.key) XCTAssertNil(record.context) XCTAssertEqual(record.nestedKeyed.name, "foo") XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed") XCTAssertNil(record.nestedKeyed.context) XCTAssertEqual(record.nestedSingle.name, "bar") XCTAssertEqual(record.nestedSingle.key, "nestedSingle") XCTAssertNil(record.nestedSingle.context) XCTAssertEqual(record.nestedUnkeyed.name, "baz") XCTAssertEqual(record.nestedUnkeyed.key, "nestedUnkeyed") XCTAssertNil(record.nestedUnkeyed.context) } do { let decoder = JSONDecoder() decoder.userInfo = [testKeyRoot: "root", testKeyNested: "nested"] let record = try decoder.decode(Record.self, from: json) XCTAssertNil(record.key) XCTAssertEqual(record.context, "root") XCTAssertEqual(record.nestedKeyed.name, "foo") XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed") XCTAssertEqual(record.nestedKeyed.context, "nested") XCTAssertEqual(record.nestedSingle.name, "bar") XCTAssertEqual(record.nestedSingle.key, "nestedSingle") XCTAssertEqual(record.nestedSingle.context, "nested") XCTAssertEqual(record.nestedUnkeyed.name, "baz") XCTAssertEqual(record.nestedUnkeyed.key, "nestedUnkeyed") XCTAssertEqual(record.nestedUnkeyed.context, "nested") } } func testRecord1() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in func test(_ record: Record) { XCTAssertNil(record.key) XCTAssertNil(record.context) // scope XCTAssertEqual(record.nestedKeyed.name, "foo") XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed") XCTAssertNil(record.nestedKeyed.context) // column XCTAssertEqual(record.nestedSingle.name, "bar") XCTAssertEqual(record.nestedSingle.key, "nestedSingle") XCTAssertNil(record.nestedSingle.context) // JSON column XCTAssertEqual(record.nestedUnkeyed.name, "baz") XCTAssertNil(record.nestedUnkeyed.key) XCTAssertNil(record.nestedUnkeyed.context) } let adapter = SuffixRowAdapter(fromIndex: 1).addingScopes(["nestedKeyed": RangeRowAdapter(0..<1)]) let request = SQLRequest( sql: "SELECT ? AS name, ? AS nestedSingle, ? AS nestedUnkeyed", arguments: ["foo", "bar", "[\"baz\"]"], adapter: adapter) let record = try Record.fetchOne(db, request)! test(record) let row = try Row.fetchOne(db, request)! try test(Record(row: row)) } } func testRecord2() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in func test(_ record: Record) { XCTAssertNil(record.key) XCTAssertNil(record.context) // JSON column XCTAssertEqual(record.nestedKeyed.name, "foo") XCTAssertNil(record.nestedKeyed.key) XCTAssertNil(record.nestedKeyed.context) // column XCTAssertEqual(record.nestedSingle.name, "bar") XCTAssertEqual(record.nestedSingle.key, "nestedSingle") XCTAssertNil(record.nestedSingle.context) // JSON column XCTAssertEqual(record.nestedUnkeyed.name, "baz") XCTAssertNil(record.nestedUnkeyed.key) XCTAssertNil(record.nestedUnkeyed.context) } let request = SQLRequest( sql: "SELECT ? AS nestedKeyed, ? AS nestedSingle, ? AS nestedUnkeyed", arguments: ["{\"name\":\"foo\"}", "bar", "[\"baz\"]"]) let record = try Record.fetchOne(db, request)! test(record) let row = try Row.fetchOne(db, request)! try test(Record(row: row)) } } func testCustomizedRecord1() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in func test(_ record: CustomizedRecord) { XCTAssertNil(record.key) XCTAssertEqual(record.context, "GRDB root") // scope XCTAssertEqual(record.nestedKeyed.name, "foo") XCTAssertEqual(record.nestedKeyed.key, "nestedKeyed") XCTAssertEqual(record.nestedKeyed.context, "GRDB column or scope") // column XCTAssertEqual(record.nestedSingle.name, "bar") XCTAssertEqual(record.nestedSingle.key, "nestedSingle") XCTAssertEqual(record.nestedSingle.context, "GRDB column or scope") // JSON column XCTAssertEqual(record.nestedUnkeyed.name, "baz") XCTAssertNil(record.nestedUnkeyed.key) XCTAssertEqual(record.nestedUnkeyed.context, "JSON column: nestedUnkeyed") } let adapter = SuffixRowAdapter(fromIndex: 1).addingScopes(["nestedKeyed": RangeRowAdapter(0..<1)]) let request = SQLRequest( sql: "SELECT ? AS name, ? AS nestedSingle, ? AS nestedUnkeyed", arguments: ["foo", "bar", "[\"baz\"]"], adapter: adapter) let record = try CustomizedRecord.fetchOne(db, request)! test(record) let row = try Row.fetchOne(db, request)! try test(CustomizedRecord(row: row)) } } func testCustomizedRecord2() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in func test(_ record: CustomizedRecord) { XCTAssertNil(record.key) XCTAssertEqual(record.context, "GRDB root") // JSON column XCTAssertEqual(record.nestedKeyed.name, "foo") XCTAssertNil(record.nestedKeyed.key) XCTAssertEqual(record.nestedKeyed.context, "JSON column: nestedKeyed") // column XCTAssertEqual(record.nestedSingle.name, "bar") XCTAssertEqual(record.nestedSingle.key, "nestedSingle") XCTAssertEqual(record.nestedSingle.context, "GRDB column or scope") // JSON column XCTAssertEqual(record.nestedUnkeyed.name, "baz") XCTAssertNil(record.nestedUnkeyed.key) XCTAssertEqual(record.nestedUnkeyed.context, "JSON column: nestedUnkeyed") } let request = SQLRequest( sql: "SELECT ? AS nestedKeyed, ? AS nestedSingle, ? AS nestedUnkeyed", arguments: ["{\"name\":\"foo\"}", "bar", "[\"baz\"]"]) let record = try CustomizedRecord.fetchOne(db, request)! test(record) let row = try Row.fetchOne(db, request)! try test(CustomizedRecord(row: row)) } } func testMissingKeys1() throws { struct A: Decodable { } struct B: Decodable { } struct C: Decodable { } struct Composed: Decodable, FetchableRecord { var a: A var b: B? var c: C? } // No error expected: // - a is successfully decoded because it consumes the one and unique // allowed missing key // - b and c are successfully decoded, because they are optionals, and // all optionals decode missing keys are nil. This is because GRDB // records accept rows with missing columns, and b and c may want to // decode columns. _ = try FetchableRecordDecoder().decode(Composed.self, from: [:]) } // This is a regression test for https://github.com/groue/GRDB.swift/issues/664 func testMissingKeys2() throws { struct A: Decodable { } struct B: Decodable { } struct Composed: Decodable, FetchableRecord { var a: A var b: B } do { _ = try FetchableRecordDecoder().decode(Composed.self, from: [:]) XCTFail("Expected error") } catch DecodingError.keyNotFound { // a or b can not be decoded because only one key is allowed to be missing } } // Regression test for https://github.com/groue/GRDB.swift/issues/836 func testRootKeyDecodingError() throws { struct Record: Decodable { } struct Composed: Decodable, FetchableRecord { var a: Record var b: Record var c: Record } try makeDatabaseQueue().read { db in do { // - a is present // - root is b and c is missing, or the opposite (two possible user intents) let row = try Row.fetchOne(db, sql: "SELECT NULL", adapter: ScopeAdapter(["a": EmptyRowAdapter()]))! _ = try FetchableRecordDecoder().decode(Composed.self, from: row) XCTFail("Expected error") } catch let DecodingError.keyNotFound(key, context) { XCTAssert(["b", "c"].contains(key.stringValue)) XCTAssertEqual(context.debugDescription, "No such key: b or c") } do { // - b is present // - root is a and c is missing, or the opposite (two possible user intents) let row = try Row.fetchOne(db, sql: "SELECT NULL", adapter: ScopeAdapter(["b": EmptyRowAdapter()]))! _ = try FetchableRecordDecoder().decode(Composed.self, from: row) XCTFail("Expected error") } catch let DecodingError.keyNotFound(key, context) { XCTAssert(["a", "c"].contains(key.stringValue)) XCTAssertEqual(context.debugDescription, "No such key: a or c") } do { // - c is present // - root is a and b is missing, or the opposite (two possible user intents) let row = try Row.fetchOne(db, sql: "SELECT NULL", adapter: ScopeAdapter(["c": EmptyRowAdapter()]))! _ = try FetchableRecordDecoder().decode(Composed.self, from: row) XCTFail("Expected error") } catch let DecodingError.keyNotFound(key, context) { XCTAssert(["a", "b"].contains(key.stringValue)) XCTAssertEqual(context.debugDescription, "No such key: a or b") } } } func testUserInfoJsonDecoding() throws { struct NestedStruct : Codable { let firstName: String? let lastName: String? init(firstName: String?, lastName: String?) { self.firstName = firstName self.lastName = lastName } init(from decoder: Decoder) throws { let userInfoValue = decoder.userInfo[.testKey] as? String let container = try decoder.container(keyedBy: CodingKeys.self) if userInfoValue == "correct" { firstName = try container.decode(String.self, forKey: .firstName) lastName = try container.decode(String.self, forKey: .lastName) } else { firstName = try container.decode(String.self, forKey: .lastName) lastName = try container.decode(String.self, forKey: .firstName) } } } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [CodingUserInfoKey.testKey: "correct"] let nested: NestedStruct? } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "t1") { t in t.column("nested", .text) } let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") let value = StructWithNestedType(nested: nested) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) guard let nestedModel = parentModel.first?.nested else { XCTFail() return } // Check the nested model contains the expected values of first and last name XCTAssertEqual(nestedModel.firstName, "Bob") XCTAssertEqual(nestedModel.lastName, "Dylan") } } } fileprivate extension CodingUserInfoKey { static let testKey = CodingUserInfoKey(rawValue: "correct")! } // MARK: - Decodable + custom init extension FetchableRecordDecodableTests { // Test for func testStructWithCustomizedRowInitializer() throws { struct Player: Decodable, FetchableRecord { var id: Int var name: String var isFetched: Bool = false enum CodingKeys: String, CodingKey { case id, name } init(id: Int, name: String) { self.id = id self.name = name self.isFetched = false } init(row: Row) throws { self = try FetchableRecordDecoder().decode(Player.self, from: row) self.isFetched = true } } do { let player = Player(id: 1, name: "Arthur") XCTAssertEqual(player.id, 1) XCTAssertEqual(player.name, "Arthur") XCTAssertEqual(player.isFetched, false) } do { let player = try Player(row: ["id": 2, "name": "Barbara"]) XCTAssertEqual(player.id, 2) XCTAssertEqual(player.name, "Barbara") XCTAssertEqual(player.isFetched, true) } } } // MARK: - KeyedContainer tests extension FetchableRecordDecodableTests { struct AnyCodingKey: CodingKey { var stringValue: String var intValue: Int? { nil } init(_ key: String) { self.stringValue = key } init(stringValue: String) { self.stringValue = stringValue } init?(intValue: Int) { return nil } } func test_allKeys_and_containsKey() throws { struct Witness: Decodable, FetchableRecord { init(from decoder: any Decoder) throws { // Top let container = try decoder.container(keyedBy: AnyCodingKey.self) do { // Test allKeys let allKeys = container.allKeys XCTAssertEqual(Set(allKeys.map(\.stringValue)), [ "a", "topLevelScope1", "topLevelScope2", "nestedScope1", "nestedScope2", "prefetchedRows1", "prefetchedRows2"]) // Test contains(_:) for key in allKeys { XCTAssertTrue(container.contains(key)) } XCTAssertFalse(container.contains(AnyCodingKey("b"))) XCTAssertFalse(container.contains(AnyCodingKey("c"))) } // topLevelScope1 let topLevelScope1Container = try container.nestedContainer( keyedBy: AnyCodingKey.self, forKey: AnyCodingKey("topLevelScope1")) do { // Test allKeys let allKeys = topLevelScope1Container.allKeys XCTAssertEqual(Set(allKeys.map(\.stringValue)), [ "c", ]) // Test contains(_:) for key in allKeys { XCTAssertTrue(topLevelScope1Container.contains(key)) } } // topLevelScope2 let topLevelScope2Container = try container.nestedContainer( keyedBy: AnyCodingKey.self, forKey: AnyCodingKey("topLevelScope2")) do { // Test allKeys let allKeys = topLevelScope2Container.allKeys XCTAssertEqual(Set(allKeys.map(\.stringValue)), [ "nestedScope2", "nestedScope1", "prefetchedRows2", ]) // Test contains(_:) for key in allKeys { XCTAssertTrue(topLevelScope2Container.contains(key)) } } } } try makeDatabaseQueue().read { db in let row = try Row.fetchOne( db, sql: """ SELECT 1 AS a, -- main row 2 AS b, -- not exposed 3 AS c, -- scope topLevelScope1 4 AS d, -- scope topLevelScope2.nestedScope1 5 AS e -- scope topLevelScope2.nestedScope2 """, adapter: RangeRowAdapter(0..<1) .addingScopes([ "topLevelScope1": RangeRowAdapter(2..<3), "topLevelScope2": EmptyRowAdapter().addingScopes([ "nestedScope1": RangeRowAdapter(3..<4), "nestedScope2": RangeRowAdapter(4..<5), ]), ]))! row.prefetchedRows.setRows([], forKeyPath: ["prefetchedRows1"]) row.prefetchedRows.setRows([Row()], forKeyPath: ["topLevelScope2", "prefetchedRows2"]) // Check test setup XCTAssertEqual(row.debugDescription, """ ▿ [a:1] unadapted: [a:1 b:2 c:3 d:4 e:5] - topLevelScope1: [c:3] - topLevelScope2: [] - nestedScope1: [d:4] - nestedScope2: [e:5] + prefetchedRows2: 1 row + prefetchedRows1: 0 row + prefetchedRows2: 1 row """) // Test keyed container _ = try FetchableRecordDecoder().decode(Witness.self, from: row) } } // Regression test for func test_decodeNil_and_containsKey() throws { struct Witness: Decodable, FetchableRecord { struct NestedRecord: Decodable, FetchableRecord { } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: AnyCodingKey.self) // column do { let key = AnyCodingKey("a") let nilDecoded = try container.decodeNil(forKey: key) let value = try container.decodeIfPresent(Int.self, forKey: key) XCTAssertTrue(nilDecoded == (value == nil)) XCTAssertTrue(container.contains(key)) } // scope do { let key = AnyCodingKey("nested") let nilDecoded = try container.decodeNil(forKey: key) let value = try container.decodeIfPresent(NestedRecord.self, forKey: key) XCTAssertTrue(nilDecoded == (value == nil)) XCTAssertTrue(container.contains(key)) } // missing key do { let key = AnyCodingKey("missing") try XCTAssertTrue(container.decodeNil(forKey: key)) try XCTAssertNil(container.decodeIfPresent(Int.self, forKey: key)) try XCTAssertNil(container.decodeIfPresent(NestedRecord.self, forKey: key)) XCTAssertFalse(container.contains(key)) } } } try makeDatabaseQueue().read { db in do { let row = try Row.fetchOne( db, sql: """ SELECT 1 AS a, 2 AS b """, adapter: ScopeAdapter([ "nested": RangeRowAdapter(1..<2), ]))! // Check test setup XCTAssertEqual(row.debugDescription, """ ▿ [a:1 b:2] unadapted: [a:1 b:2] - nested: [b:2] """) // Test keyed container _ = try FetchableRecordDecoder().decode(Witness.self, from: row) } do { let row = try Row.fetchOne( db, sql: """ SELECT NULL AS a, NULL AS b """, adapter: ScopeAdapter([ "nested": RangeRowAdapter(1..<2), ]))! // Check test setup XCTAssertEqual(row.debugDescription, """ ▿ [a:NULL b:NULL] unadapted: [a:NULL b:NULL] - nested: [b:NULL] """) // Test keyed container _ = try FetchableRecordDecoder().decode(Witness.self, from: row) } } } // Regression test for func test_decodeNil_when_scope_and_column_have_the_same_name() throws { struct Witness: Decodable, FetchableRecord { struct NestedRecord: Decodable, FetchableRecord { } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: AnyCodingKey.self) let key = AnyCodingKey("a") let nilDecoded = try container.decodeNil(forKey: key) let intValue = try container.decodeIfPresent(Int.self, forKey: key) let recordValue = try container.decodeIfPresent(NestedRecord.self, forKey: key) XCTAssertTrue(nilDecoded == (intValue == nil)) XCTAssertTrue(nilDecoded == (recordValue == nil)) } } try makeDatabaseQueue().read { db in do { let row = try Row.fetchOne( db, sql: """ SELECT 1 AS a """, adapter: ScopeAdapter([ "a": SuffixRowAdapter(fromIndex: 0), ]))! // Check test setup XCTAssertEqual(row.debugDescription, """ ▿ [a:1] unadapted: [a:1] - a: [a:1] """) // Test keyed container _ = try FetchableRecordDecoder().decode(Witness.self, from: row) } do { let row = try Row.fetchOne( db, sql: """ SELECT NULL AS a """, adapter: ScopeAdapter([ "a": SuffixRowAdapter(fromIndex: 0), ]))! // Check test setup XCTAssertEqual(row.debugDescription, """ ▿ [a:NULL] unadapted: [a:NULL] - a: [a:NULL] """) // Test keyed container _ = try FetchableRecordDecoder().decode(Witness.self, from: row) } } } // Regression test for func testSingleValueContainer() throws { struct Struct: Decodable { let value: String } struct Wrapper: FetchableRecord, Decodable { var model: Model var otherValue: String enum CodingKeys: String, CodingKey { case otherValue } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) otherValue = try container.decode(String.self, forKey: . otherValue) let singleValueContainer = try decoder.singleValueContainer() model = try singleValueContainer.decode(Model.self) } } let row = Row(["value": "foo", "otherValue": "bar"]) let wrapper = try Wrapper(row: row) XCTAssertEqual(wrapper.model.value, "foo") XCTAssertEqual(wrapper.otherValue, "bar") } // Regression test for // Here we test that `FetchableRecord` takes precedence over `Decodable` // when a record is encoded with a `SingleValueEncodingContainer`. func testSingleValueContainerWithFetchableRecord() throws { struct Struct: Decodable, FetchableRecord { let value: String init(row: Row) throws { value = row["actualValue"] } } struct Wrapper: FetchableRecord, Decodable { var model: Model var otherValue: String enum CodingKeys: String, CodingKey { case otherValue } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) otherValue = try container.decode(String.self, forKey: . otherValue) let singleValueContainer = try decoder.singleValueContainer() model = try singleValueContainer.decode(Model.self) } } let row = Row(["value": "foo", "otherValue": "bar", "actualValue": "test"]) let wrapper = try Wrapper(row: row) XCTAssertEqual(wrapper.model.value, "test") XCTAssertEqual(wrapper.otherValue, "bar") } }