import XCTest import GRDB let expectedSQL = """ SELECT "t1".*, "t2Left".*, "t2Right".*, "t3"."t1id", "t3"."name", COUNT(DISTINCT t5.id) AS t5count FROM t1 LEFT JOIN t2 t2Left ON t2Left.t1id = t1.id AND t2Left.name = 'left' LEFT JOIN t2 t2Right ON t2Right.t1id = t1.id AND t2Right.name = 'right' LEFT JOIN t3 ON t3.t1id = t1.id LEFT JOIN t4 ON t4.t1id = t1.id LEFT JOIN t5 ON t5.t3id = t3.t1id OR t5.t4id = t4.t1id GROUP BY t1.id ORDER BY t1.id """ let testedLiteral: SQL = """ SELECT \(columnsOf: T1.self), \(columnsOf: T2.self, tableAlias: "t2Left"), \(columnsOf: T2.self, tableAlias: "t2Right"), \(columnsOf: T3.self), COUNT(DISTINCT t5.id) AS t5count FROM t1 LEFT JOIN t2 t2Left ON t2Left.t1id = t1.id AND t2Left.name = 'left' LEFT JOIN t2 t2Right ON t2Right.t1id = t1.id AND t2Right.name = 'right' LEFT JOIN t3 ON t3.t1id = t1.id LEFT JOIN t4 ON t4.t1id = t1.id LEFT JOIN t5 ON t5.t3id = t3.t1id OR t5.t4id = t4.t1id GROUP BY t1.id ORDER BY t1.id """ private struct T1: Codable, FetchableRecord, TableRecord { static let databaseTableName = "t1" var id: Int64 var name: String } private struct T2: Codable, FetchableRecord, TableRecord { static let databaseTableName = "t2" var id: Int64 var t1id: Int64 var name: String } private struct T3: Codable, FetchableRecord, TableRecord { static let databaseTableName = "t3" static let databaseSelection: [any SQLSelectable] = [Column("t1id"), Column("name")] var t1id: Int64 var name: String } private struct T4: Codable, FetchableRecord, TableRecord { static let databaseTableName = "t4" var t1id: Int64 var name: String } private struct T5: Codable, FetchableRecord, TableRecord { static let databaseTableName = "t5" var id: Int64 var t3id: Int64? var t4id: Int64? var name: String } private struct FlatModel: FetchableRecord { private enum Scopes { static let t1 = "t1" static let t2Left = "t2Left" static let t2Right = "t2Right" static let t3 = "t3" static let suffix = "suffix" } var t1: T1 var t2Left: T2? var t2Right: T2? var t3: T3? var t5count: Int init(row: Row) { self.t1 = row[Scopes.t1] self.t2Left = row[Scopes.t2Left] self.t2Right = row[Scopes.t2Right] self.t3 = row[Scopes.t3] self.t5count = row.scopes[Scopes.suffix]!["t5count"] } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) static func modernAll() -> some FetchRequest { all() } static func all() -> AdaptedFetchRequest> { SQLRequest(literal: testedLiteral).adapted { db in let adapters = try splittingRowAdapters(columnCounts: [ T1.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T3.numberOfSelectedColumns(db)]) return ScopeAdapter([ Scopes.t1: adapters[0], Scopes.t2Left: adapters[1], Scopes.t2Right: adapters[2], Scopes.t3: adapters[3], Scopes.suffix: adapters[4]]) } } static func hierarchicalAll() -> AdaptedFetchRequest> { SQLRequest(literal: testedLiteral).adapted { db in let adapters = try splittingRowAdapters(columnCounts: [ T1.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T3.numberOfSelectedColumns(db)]) return ScopeAdapter([ Scopes.t1: adapters[0], "t2": ScopeAdapter(base: EmptyRowAdapter(), scopes: [ Scopes.t2Left: adapters[1], Scopes.t2Right: adapters[2]]), Scopes.t3: adapters[3], Scopes.suffix: adapters[4]]) } } } private struct CodableFlatModel: FetchableRecord, Codable { var t1: T1 var t2Left: T2? var t2Right: T2? var t3: T3? var t5count: Int @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) static func modernAll() -> some FetchRequest { all() } static func all() -> AdaptedFetchRequest> { SQLRequest(literal: testedLiteral).adapted { db in let adapters = try splittingRowAdapters(columnCounts: [ T1.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T3.numberOfSelectedColumns(db)]) return ScopeAdapter([ CodingKeys.t1.stringValue: adapters[0], CodingKeys.t2Left.stringValue: adapters[1], CodingKeys.t2Right.stringValue: adapters[2], CodingKeys.t3.stringValue: adapters[3]]) } } static func hierarchicalAll() -> AdaptedFetchRequest> { SQLRequest(literal: testedLiteral).adapted { db in let adapters = try splittingRowAdapters(columnCounts: [ T1.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T3.numberOfSelectedColumns(db)]) return ScopeAdapter([ CodingKeys.t1.stringValue: adapters[0], "t2": ScopeAdapter(base: EmptyRowAdapter(), scopes: [ CodingKeys.t2Left.stringValue: adapters[1], CodingKeys.t2Right.stringValue: adapters[2]]), CodingKeys.t3.stringValue: adapters[3]]) } } } private struct CodableNestedModel: FetchableRecord, Codable { struct T2Pair: Codable { var left: T2? var right: T2? } var t1: T1 var optionalT2Pair: T2Pair? var t2Pair: T2Pair var t3: T3? var t5count: Int @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) static func modernAll() -> some FetchRequest { all() } static func all() -> AdaptedFetchRequest> { SQLRequest(literal: testedLiteral).adapted { db in let adapters = try splittingRowAdapters(columnCounts: [ T1.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T3.numberOfSelectedColumns(db)]) return ScopeAdapter([ CodingKeys.t1.stringValue: adapters[0], CodingKeys.optionalT2Pair.stringValue: ScopeAdapter(base: EmptyRowAdapter(), scopes: [ // EmptyRowAdapter base so that optionalT2Pair only instantiates when its scopes contain non-null values "left": adapters[1], "right": adapters[2]]), CodingKeys.t2Pair.stringValue: ScopeAdapter([ "left": adapters[1], "right": adapters[2]]), CodingKeys.t3.stringValue: adapters[3]]) } } } class JoinSupportTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { db in try db.create(table: "t1") { t in t.primaryKey("id", .integer) t.column("name", .text).notNull() } try db.create(table: "t2") { t in t.primaryKey("id", .integer) t.column("t1id", .integer).notNull().references("t1", onDelete: .cascade) t.column("name", .text).notNull() t.uniqueKey(["t1id", "name"]) } try db.create(table: "t3") { t in t.primaryKey("t1id", .integer).references("t1", onDelete: .cascade) t.column("name", .text).notNull() t.column("ignored", .integer) } try db.create(table: "t4") { t in t.primaryKey("t1id", .integer).references("t1", onDelete: .cascade) t.column("name", .text).notNull() } try db.create(table: "t5") { t in t.primaryKey("id", .integer) t.column("t3id", .integer).references("t3", onDelete: .cascade) t.column("t4id", .integer).references("t4", onDelete: .cascade) t.column("name", .text).notNull() t.check(sql: "(t3id IS NOT NULL) + (t4id IS NOT NULL) = 1") } // Sample data try db.execute(sql: """ INSERT INTO t1 (id, name) VALUES (1, 'A1'); INSERT INTO t1 (id, name) VALUES (2, 'A2'); INSERT INTO t1 (id, name) VALUES (3, 'A3'); INSERT INTO t2 (id, t1id, name) VALUES (1, 1, 'left'); INSERT INTO t2 (id, t1id, name) VALUES (2, 1, 'right'); INSERT INTO t2 (id, t1id, name) VALUES (3, 2, 'left'); INSERT INTO t3 (t1id, name) VALUES (1, 'A3'); INSERT INTO t4 (t1id, name) VALUES (1, 'A4'); INSERT INTO t4 (t1id, name) VALUES (2, 'B4'); INSERT INTO t5 (id, t3id, t4id, name) VALUES (1, 1, NULL, 'A5'); INSERT INTO t5 (id, t3id, t4id, name) VALUES (2, 1, NULL, 'B5'); INSERT INTO t5 (id, t3id, t4id, name) VALUES (3, NULL, 1, 'C5'); INSERT INTO t5 (id, t3id, t4id, name) VALUES (4, NULL, 1, 'D5'); INSERT INTO t5 (id, t3id, t4id, name) VALUES (5, NULL, 1, 'E5'); INSERT INTO t5 (id, t3id, t4id, name) VALUES (6, NULL, 2, 'F5'); INSERT INTO t5 (id, t3id, t4id, name) VALUES (7, NULL, 2, 'G5'); """) } } func testNumberOfSelectedColumns() throws { struct T: TableRecord { } let dbQueue = try makeDatabaseQueue() try dbQueue.inTransaction { db in try db.execute(sql: "CREATE TABLE t (a)") try XCTAssertEqual(T.numberOfSelectedColumns(db), 1) return .rollback } try dbQueue.inTransaction { db in try db.execute(sql: "CREATE TABLE t (a, b)") try XCTAssertEqual(T.numberOfSelectedColumns(db), 2) return .rollback } } func testNumberOfSelectedColumnsIncludeGeneratedColumns() throws { #if !GRDBCUSTOMSQLITE throw XCTSkip("Generated columns are not available") #else struct T: TableRecord { } let dbQueue = try makeDatabaseQueue() try dbQueue.inTransaction { db in try db.execute(sql: "CREATE TABLE t (a, b ALWAYS GENERATED AS (a) VIRTUAL)") try XCTAssertEqual(T.numberOfSelectedColumns(db), 2) return .rollback } try dbQueue.inTransaction { db in try db.execute(sql: "CREATE TABLE t (a, b ALWAYS GENERATED AS (a) STORED)") try XCTAssertEqual(T.numberOfSelectedColumns(db), 2) return .rollback } #endif } func testSampleData() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let rows = try SQLRequest(literal: testedLiteral).fetchAll(db) XCTAssertEqual(rows.count, 3) XCTAssertEqual(rows[0], [ // t1.* "id": 1, "name": "A1", // t2Left.* "id": 1, "t1id": 1, "name": "left", // t2Right.* "id": 2, "t1id": 1, "name": "right", // t3.* "t1id": 1, "name": "A3", // t5count "t5count": 5]) XCTAssertEqual(rows[1], [ // t1.* "id": 2, "name": "A2", // t2Left.* "id": 3, "t1id": 2, "name": "left", // t2Right.* "id": nil, "t1id": nil, "name": nil, // t3.* "t1id": nil, "name": nil, // t5count "t5count": 2]) XCTAssertEqual(rows[2], [ // t1.* "id": 3, "name": "A3", // t2Left.* "id": nil, "t1id": nil, "name": nil, // t2Right.* "id": nil, "t1id": nil, "name": nil, // t3.* "t1id": nil, "name": nil, // t5count "t5count": 0]) } } func testTestedSQL() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let (testedSQL, _) = try testedLiteral.build(db) XCTAssertEqual(testedSQL, expectedSQL) } } func testSplittingRowAdapters() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let request = SQLRequest(literal: testedLiteral).adapted { db in let adapters = try splittingRowAdapters(columnCounts: [ T1.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T2.numberOfSelectedColumns(db), T3.numberOfSelectedColumns(db)]) XCTAssertEqual(adapters.count, 5) return ScopeAdapter([ "t1": adapters[0], "t2Left": adapters[1], "t2Right": adapters[2], "t3": adapters[3], "suffix": adapters[4]]) } let rows = try request.fetchAll(db) XCTAssertEqual(rows.count, 3) XCTAssertEqual(rows[0].unscoped, [ // t1.* "id": 1, "name": "A1", // t2Left.* "id": 1, "t1id": 1, "name": "left", // t2Right.* "id": 2, "t1id": 1, "name": "right", // t3.* "t1id": 1, "name": "A3", // t5count "t5count": 5]) XCTAssertEqual(rows[0].scopes["t1"]!, ["id": 1, "name": "A1"]) XCTAssertEqual(rows[0].scopes["t2Left"]!, ["id": 1, "t1id": 1, "name": "left"]) XCTAssertEqual(rows[0].scopes["t2Right"]!, ["id": 2, "t1id": 1, "name": "right"]) XCTAssertEqual(rows[0].scopes["t3"]!, ["t1id": 1, "name": "A3"]) XCTAssertEqual(rows[0].scopes["suffix"]!, ["t5count": 5]) XCTAssertEqual(rows[1].unscoped, [ // t1.* "id": 2, "name": "A2", // t2Left.* "id": 3, "t1id": 2, "name": "left", // t2Right.* "id": nil, "t1id": nil, "name": nil, // t3.* "t1id": nil, "name": nil, // t5count "t5count": 2]) XCTAssertEqual(rows[1].scopes["t1"]!, ["id": 2, "name": "A2"]) XCTAssertEqual(rows[1].scopes["t2Left"]!, ["id": 3, "t1id": 2, "name": "left"]) XCTAssertEqual(rows[1].scopes["t2Right"]!, ["id": nil, "t1id": nil, "name": nil]) XCTAssertEqual(rows[1].scopes["t3"]!, ["t1id": nil, "name": nil]) XCTAssertEqual(rows[1].scopes["suffix"]!, ["t5count": 2]) XCTAssertEqual(rows[2].unscoped, [ // t1.* "id": 3, "name": "A3", // t2Left.* "id": nil, "t1id": nil, "name": nil, // t2Right.* "id": nil, "t1id": nil, "name": nil, // t3.* "t1id": nil, "name": nil, // t5count "t5count": 0]) XCTAssertEqual(rows[2].scopes["t1"]!, ["id": 3, "name": "A3"]) XCTAssertEqual(rows[2].scopes["t2Left"]!, ["id": nil, "t1id": nil, "name": nil]) XCTAssertEqual(rows[2].scopes["t2Right"]!, ["id": nil, "t1id": nil, "name": nil]) XCTAssertEqual(rows[2].scopes["t3"]!, ["t1id": nil, "name": nil]) XCTAssertEqual(rows[2].scopes["suffix"]!, ["t5count": 0]) } } func testFlatModel() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let models = try FlatModel.all().fetchAll(db) XCTAssertEqual(models.count, 3) XCTAssertEqual(models[0].t1.id, 1) XCTAssertEqual(models[0].t1.name, "A1") XCTAssertEqual(models[0].t2Left!.id, 1) XCTAssertEqual(models[0].t2Left!.t1id, 1) XCTAssertEqual(models[0].t2Left!.name, "left") XCTAssertEqual(models[0].t2Right!.id, 2) XCTAssertEqual(models[0].t2Right!.t1id, 1) XCTAssertEqual(models[0].t2Right!.name, "right") XCTAssertEqual(models[0].t3!.t1id, 1) XCTAssertEqual(models[0].t3!.name, "A3") XCTAssertEqual(models[0].t5count, 5) XCTAssertEqual(models[1].t1.id, 2) XCTAssertEqual(models[1].t1.name, "A2") XCTAssertEqual(models[1].t2Left!.id, 3) XCTAssertEqual(models[1].t2Left!.t1id, 2) XCTAssertEqual(models[1].t2Left!.name, "left") XCTAssertNil(models[1].t2Right) XCTAssertNil(models[1].t3) XCTAssertEqual(models[1].t5count, 2) XCTAssertEqual(models[2].t1.id, 3) XCTAssertEqual(models[2].t1.name, "A3") XCTAssertNil(models[2].t2Left) XCTAssertNil(models[2].t2Right) XCTAssertNil(models[2].t3) XCTAssertEqual(models[2].t5count, 0) } } func testFlatModelFromHierarchicalRequest() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let models = try FlatModel.hierarchicalAll().fetchAll(db) XCTAssertEqual(models.count, 3) XCTAssertEqual(models[0].t1.id, 1) XCTAssertEqual(models[0].t1.name, "A1") XCTAssertEqual(models[0].t2Left!.id, 1) XCTAssertEqual(models[0].t2Left!.t1id, 1) XCTAssertEqual(models[0].t2Left!.name, "left") XCTAssertEqual(models[0].t2Right!.id, 2) XCTAssertEqual(models[0].t2Right!.t1id, 1) XCTAssertEqual(models[0].t2Right!.name, "right") XCTAssertEqual(models[0].t3!.t1id, 1) XCTAssertEqual(models[0].t3!.name, "A3") XCTAssertEqual(models[0].t5count, 5) XCTAssertEqual(models[1].t1.id, 2) XCTAssertEqual(models[1].t1.name, "A2") XCTAssertEqual(models[1].t2Left!.id, 3) XCTAssertEqual(models[1].t2Left!.t1id, 2) XCTAssertEqual(models[1].t2Left!.name, "left") XCTAssertNil(models[1].t2Right) XCTAssertNil(models[1].t3) XCTAssertEqual(models[1].t5count, 2) XCTAssertEqual(models[2].t1.id, 3) XCTAssertEqual(models[2].t1.name, "A3") XCTAssertNil(models[2].t2Left) XCTAssertNil(models[2].t2Right) XCTAssertNil(models[2].t3) XCTAssertEqual(models[2].t5count, 0) } } func testCodableFlatModel() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let models = try CodableFlatModel.all().fetchAll(db) XCTAssertEqual(models.count, 3) XCTAssertEqual(models[0].t1.id, 1) XCTAssertEqual(models[0].t1.name, "A1") XCTAssertEqual(models[0].t2Left!.id, 1) XCTAssertEqual(models[0].t2Left!.t1id, 1) XCTAssertEqual(models[0].t2Left!.name, "left") XCTAssertEqual(models[0].t2Right!.id, 2) XCTAssertEqual(models[0].t2Right!.t1id, 1) XCTAssertEqual(models[0].t2Right!.name, "right") XCTAssertEqual(models[0].t3!.t1id, 1) XCTAssertEqual(models[0].t3!.name, "A3") XCTAssertEqual(models[0].t5count, 5) XCTAssertEqual(models[1].t1.id, 2) XCTAssertEqual(models[1].t1.name, "A2") XCTAssertEqual(models[1].t2Left!.id, 3) XCTAssertEqual(models[1].t2Left!.t1id, 2) XCTAssertEqual(models[1].t2Left!.name, "left") XCTAssertNil(models[1].t2Right) XCTAssertNil(models[1].t3) XCTAssertEqual(models[1].t5count, 2) XCTAssertEqual(models[2].t1.id, 3) XCTAssertEqual(models[2].t1.name, "A3") XCTAssertNil(models[2].t2Left) XCTAssertNil(models[2].t2Right) XCTAssertNil(models[2].t3) XCTAssertEqual(models[2].t5count, 0) } } func testCodableFlatModelFromHierarchicalRequest() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let models = try CodableFlatModel.hierarchicalAll().fetchAll(db) XCTAssertEqual(models.count, 3) XCTAssertEqual(models[0].t1.id, 1) XCTAssertEqual(models[0].t1.name, "A1") XCTAssertEqual(models[0].t2Left!.id, 1) XCTAssertEqual(models[0].t2Left!.t1id, 1) XCTAssertEqual(models[0].t2Left!.name, "left") XCTAssertEqual(models[0].t2Right!.id, 2) XCTAssertEqual(models[0].t2Right!.t1id, 1) XCTAssertEqual(models[0].t2Right!.name, "right") XCTAssertEqual(models[0].t3!.t1id, 1) XCTAssertEqual(models[0].t3!.name, "A3") XCTAssertEqual(models[0].t5count, 5) XCTAssertEqual(models[1].t1.id, 2) XCTAssertEqual(models[1].t1.name, "A2") XCTAssertEqual(models[1].t2Left!.id, 3) XCTAssertEqual(models[1].t2Left!.t1id, 2) XCTAssertEqual(models[1].t2Left!.name, "left") XCTAssertNil(models[1].t2Right) XCTAssertNil(models[1].t3) XCTAssertEqual(models[1].t5count, 2) XCTAssertEqual(models[2].t1.id, 3) XCTAssertEqual(models[2].t1.name, "A3") XCTAssertNil(models[2].t2Left) XCTAssertNil(models[2].t2Right) XCTAssertNil(models[2].t3) XCTAssertEqual(models[2].t5count, 0) } } func testCodableNestedModel() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let models = try CodableNestedModel.all().fetchAll(db) XCTAssertEqual(models.count, 3) XCTAssertEqual(models[0].t1.id, 1) XCTAssertEqual(models[0].t1.name, "A1") XCTAssertEqual(models[0].optionalT2Pair!.left!.id, 1) XCTAssertEqual(models[0].optionalT2Pair!.left!.t1id, 1) XCTAssertEqual(models[0].optionalT2Pair!.left!.name, "left") XCTAssertEqual(models[0].optionalT2Pair!.right!.id, 2) XCTAssertEqual(models[0].optionalT2Pair!.right!.t1id, 1) XCTAssertEqual(models[0].optionalT2Pair!.right!.name, "right") XCTAssertEqual(models[0].t2Pair.left!.id, 1) XCTAssertEqual(models[0].t2Pair.left!.t1id, 1) XCTAssertEqual(models[0].t2Pair.left!.name, "left") XCTAssertEqual(models[0].t2Pair.right!.id, 2) XCTAssertEqual(models[0].t2Pair.right!.t1id, 1) XCTAssertEqual(models[0].t2Pair.right!.name, "right") XCTAssertEqual(models[0].t3!.t1id, 1) XCTAssertEqual(models[0].t3!.name, "A3") XCTAssertEqual(models[0].t5count, 5) XCTAssertEqual(models[1].t1.id, 2) XCTAssertEqual(models[1].t1.name, "A2") XCTAssertEqual(models[1].optionalT2Pair!.left!.id, 3) XCTAssertEqual(models[1].optionalT2Pair!.left!.t1id, 2) XCTAssertEqual(models[1].optionalT2Pair!.left!.name, "left") XCTAssertEqual(models[1].t2Pair.left!.id, 3) XCTAssertEqual(models[1].t2Pair.left!.t1id, 2) XCTAssertEqual(models[1].t2Pair.left!.name, "left") XCTAssertNil(models[1].optionalT2Pair!.right) XCTAssertNil(models[1].t2Pair.right) XCTAssertNil(models[1].t3) XCTAssertEqual(models[1].t5count, 2) XCTAssertEqual(models[2].t1.id, 3) XCTAssertEqual(models[2].t1.name, "A3") XCTAssertNil(models[2].optionalT2Pair) XCTAssertNil(models[2].t2Pair.left) XCTAssertNil(models[2].t2Pair.right) XCTAssertNil(models[2].t3) XCTAssertEqual(models[2].t5count, 0) } } }