import XCTest @testable import GRDB // a -> b* -> c // -> d* // -> c* private struct A: TableRecord, FetchableRecord, Decodable { static let databaseTableName = "a" static let b = hasOne(B.self) static let c = hasOne(C.self) var id: Int64 } private struct B: TableRecord, FetchableRecord, Decodable { static let databaseTableName = "b" static let c = hasOne(C.self) static let d = hasOne(D.self) var id: Int64 var aid: Int64 } private struct C: TableRecord, FetchableRecord, Decodable { static let databaseTableName = "c" var id: Int64 var aid: Int64? var bid: Int64? } private struct D: TableRecord, FetchableRecord, Decodable { static let databaseTableName = "d" var id: Int64 var bid: Int64 } class AssociationRowScopeSearchTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { db in // 1. Prepare data try db.create(table: "a") { t in t.primaryKey("id", .integer) } try db.create(table: "b") { t in t.primaryKey("id", .integer) t.column("aid", .integer).references("a") } try db.create(table: "c") { t in t.primaryKey("id", .integer) t.column("aid", .integer).references("a") t.column("bid", .integer).references("b") } try db.create(table: "d") { t in t.primaryKey("id", .integer) t.column("bid", .integer).references("b") } try db.execute(sql: "INSERT INTO a (id) VALUES (1)") try db.execute(sql: "INSERT INTO b (id, aid) VALUES (2, 1)") try db.execute(sql: "INSERT INTO c (id, aid, bid) VALUES (3, NULL, 2)") try db.execute(sql: "INSERT INTO c (id, aid, bid) VALUES (4, 1, NULL)") try db.execute(sql: "INSERT INTO d (id, bid) VALUES (5, 2)") } } private let testedRequest = A .including(required: A.b .including(required: B.c) .including(required: B.d) ) .including(required: A.c) func testTestedRequest() throws { // Assert that we fetch the expected nested scopes before we start testing breadth-first lookup let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let rows = try testedRequest.asRequest(of: Row.self).fetchAll(db) XCTAssertEqual(rows.count, 1) let row = rows[0] XCTAssertEqual(row.unscoped, ["id": 1]) XCTAssertEqual(row.scopes["b"]!.unscoped, ["id": 2, "aid": 1]) XCTAssertEqual(row.scopes["b"]!.scopes["c"]!, ["id": 3, "aid": nil, "bid": 2]) XCTAssertEqual(row.scopes["b"]!.scopes["d"]!, ["id": 5, "bid": 2]) XCTAssertEqual(row.scopes["c"]!, ["id": 4, "aid": 1, "bid": nil]) } } func testBreadthFirstScopeLookup() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let row = try testedRequest.asRequest(of: Row.self).fetchOne(db)! XCTAssertEqual(row.scopesTree["b"]!.unscoped, ["id": 2, "aid": 1]) XCTAssertEqual(row.scopesTree["c"]!, ["id": 4, "aid": 1, "bid": nil]) XCTAssertEqual(row.scopesTree["d"]!, ["id": 5, "bid": 2]) } } func testFetchableRecordDecoding() throws { struct Record: FetchableRecord { var a: A var b: B var c: C var d: D init(row: Row) throws { a = try A(row: row) b = row["b"] c = row["c"] d = row["d"] } } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let record = try testedRequest.asRequest(of: Record.self).fetchOne(db)! XCTAssertEqual(record.a.id, 1) XCTAssertEqual(record.b.id, 2) XCTAssertEqual(record.b.aid, 1) XCTAssertEqual(record.c.id, 4) XCTAssertEqual(record.c.aid, 1) XCTAssertNil(record.c.bid) XCTAssertEqual(record.d.id, 5) XCTAssertEqual(record.d.bid, 2) } } func testFlatDecodableRecordDecoding() throws { struct Record: FetchableRecord, Decodable { var a: A var b: B var c: C var d: D } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let record = try testedRequest.asRequest(of: Record.self).fetchOne(db)! XCTAssertEqual(record.a.id, 1) XCTAssertEqual(record.b.id, 2) XCTAssertEqual(record.b.aid, 1) XCTAssertEqual(record.c.id, 4) XCTAssertEqual(record.c.aid, 1) XCTAssertNil(record.c.bid) XCTAssertEqual(record.d.id, 5) XCTAssertEqual(record.d.bid, 2) } } func testNestedDecodableRecordDecoding() throws { struct NestedB: Decodable { var b: B var c: C var d: D } struct Record: FetchableRecord, Decodable { var a: A var b: NestedB var c: C } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let record = try testedRequest.asRequest(of: Record.self).fetchOne(db)! XCTAssertEqual(record.a.id, 1) XCTAssertEqual(record.b.b.id, 2) XCTAssertEqual(record.b.b.aid, 1) XCTAssertEqual(record.b.c.id, 3) XCTAssertNil(record.b.c.aid) XCTAssertEqual(record.b.c.bid, 2) XCTAssertEqual(record.b.d.id, 5) XCTAssertEqual(record.b.d.bid, 2) XCTAssertEqual(record.c.id, 4) XCTAssertEqual(record.c.aid, 1) XCTAssertNil(record.c.bid) } } func testDecodableWithCustomRowDecoding() throws { struct CustomA: FetchableRecord, Decodable { var id: Int64 var custom: Bool? init(row: Row) { id = row["id"] custom = true } } struct CustomB: FetchableRecord, Decodable { var id: Int64 var aid: Int64 var custom: Bool? init(row: Row) { id = row["id"] aid = row["aid"] custom = true } } struct CustomC: FetchableRecord, Decodable { var id: Int64 var aid: Int64? var bid: Int64? var custom: Bool? init(row: Row) { id = row["id"] aid = row["aid"] bid = row["bid"] custom = true } } struct CustomD: FetchableRecord, Decodable { var id: Int64 var bid: Int64 var custom: Bool? init(row: Row) { id = row["id"] bid = row["bid"] custom = true } } struct Record: FetchableRecord, Decodable { var a: CustomA var b: CustomB var c: CustomC var d: CustomD } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let record = try testedRequest.asRequest(of: Record.self).fetchOne(db)! XCTAssertEqual(record.a.id, 1) XCTAssertEqual(record.a.custom, true) XCTAssertEqual(record.b.id, 2) XCTAssertEqual(record.b.aid, 1) XCTAssertEqual(record.b.custom, true) XCTAssertEqual(record.c.id, 4) XCTAssertEqual(record.c.aid, 1) XCTAssertNil(record.c.bid) XCTAssertEqual(record.c.custom, true) XCTAssertEqual(record.d.id, 5) XCTAssertEqual(record.d.bid, 2) XCTAssertEqual(record.d.custom, true) } } }