import XCTest import GRDB // A <- B private struct A : TableRecord { static let databaseTableName = "a" static let b = hasOne(B.self) static let restrictedB = hasOne(RestrictedB.self) static let extendedB = hasOne(ExtendedB.self) } private struct B : TableRecord { static let a = belongsTo(A.self) static let databaseTableName = "b" } private struct RestrictedB : TableRecord { static let databaseTableName = "b" static let databaseSelection: [any SQLSelectable] = [Column("name")] } private struct ExtendedB : TableRecord { static let databaseTableName = "b" static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] } /// Test SQL generation class AssociationHasOneSQLDerivationTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { try dbWriter.write { db in 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") t.column("name", .text) } } } func testDefaultSelection() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try assertEqualSQL(db, A.including(required: A.b), """ SELECT "a".*, "b".* \ FROM "a" \ JOIN "b" ON "b"."aid" = "a"."id" """) try assertEqualSQL(db, A.including(required: A.restrictedB), """ SELECT "a".*, "b"."name" \ FROM "a" \ JOIN "b" ON "b"."aid" = "a"."id" """) try assertEqualSQL(db, A.including(required: A.extendedB), """ SELECT "a".*, "b".*, "b"."rowid" \ FROM "a" \ JOIN "b" ON "b"."aid" = "a"."id" """) } } func testCustomSelection() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { let request = A.including(required: A.b .select(Column("name"))) try assertEqualSQL(db, request, """ SELECT "a".*, "b"."name" \ FROM "a" \ JOIN "b" ON "b"."aid" = "a"."id" """) } do { let request = A.including(required: A.b .select( AllColumns(), Column.rowID)) try assertEqualSQL(db, request, """ SELECT "a".*, "b".*, "b"."rowid" \ FROM "a" \ JOIN "b" ON "b"."aid" = "a"."id" """) } do { let aAlias = TableAlias() let request = A .aliased(aAlias) .including(required: A.b .select( Column("name"), (Column("id") + aAlias[Column("id")]).forKey("foo"))) try assertEqualSQL(db, request, """ SELECT "a".*, "b"."name", "b"."id" + "a"."id" AS "foo" \ FROM "a" \ JOIN "b" ON "b"."aid" = "a"."id" """) } } } func testFilteredAssociationImpactsJoinOnClause() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { let request = A.including(required: A.b.filter(Column("name") != nil)) try assertEqualSQL(db, request, """ SELECT "a".*, "b".* \ FROM "a" \ JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL) """) } do { let request = A.including(required: A.b.filter(key: 1)) try assertEqualSQL(db, request, """ SELECT "a".*, "b".* \ FROM "a" \ JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."id" = 1) """) } do { let request = A.including(required: A.b.filter(keys: [1, 2, 3])) try assertEqualSQL(db, request, """ SELECT "a".*, "b".* \ FROM "a" \ JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."id" IN (1, 2, 3)) """) } do { let request = A.including(required: A.b.filter(key: ["id": 1])) try assertEqualSQL(db, request, """ SELECT "a".*, "b".* \ FROM "a" \ JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."id" = 1) """) } do { let request = A.including(required: A.b.filter(keys: [["id": 1], ["id": 2]])) try assertEqualSQL(db, request, """ SELECT "a".*, "b".* \ FROM "a" \ JOIN "b" ON ("b"."aid" = "a"."id") AND (("b"."id" = 1) OR ("b"."id" = 2)) """) } do { let bAlias = TableAlias(name: "customB") let request = A.including(required: A.b .aliased(bAlias) .filter(sql: "customB.name = ?", arguments: ["foo"])) try assertEqualSQL(db, request, """ SELECT "a".*, "customB".* \ FROM "a" \ JOIN "b" "customB" ON ("customB"."aid" = "a"."id") AND (customB.name = 'foo') """) } } } func testFilterAssociationInWhereClause() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let bAlias = TableAlias() let request = A .including(required: A.b.aliased(bAlias)) .filter(bAlias[Column("name")] != nil) try assertEqualSQL(db, request, """ SELECT "a".*, "b".* \ FROM "a" \ JOIN "b" ON "b"."aid" = "a"."id" \ WHERE "b"."name" IS NOT NULL """) } } func testAssociationOrderBubbleUp() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let aBase = A.all().aliased(TableAlias(name: "a")) let abBase = A.b.aliased(TableAlias(name: "ab")) let abaBase = B.a.aliased(TableAlias(name: "aba")) let aTransforms = [ { (r: QueryInterfaceRequest) in return r }, { (r: QueryInterfaceRequest) in return r.order(Column("id")) }, { (r: QueryInterfaceRequest) in return r.order(Column("id")).reversed() }, ] let abTransforms = [ { (r: HasOneAssociation) in return r }, { (r: HasOneAssociation) in return r.order(Column("name"), Column("id").desc) }, { (r: HasOneAssociation) in return r.order(Column("name")).order(Column("id").desc) }, { (r: HasOneAssociation) in return r.order(Column("name")).reversed() }, { (r: HasOneAssociation) in return r.reversed() }, ] let abaTransforms = [ { (r: BelongsToAssociation) in return r }, { (r: BelongsToAssociation) in return r.order(Column("id")) }, { (r: BelongsToAssociation) in return r.order(Column("id")).reversed() }, ] var sqls: [String] = [] for aTransform in aTransforms { for abTransform in abTransforms { for abaTransform in abaTransforms { let request = aTransform(aBase) .including(required: abTransform(abBase) .including(required: abaTransform(abaBase))) try sqls.append(request.build(db).sql) } } } let prefix = """ SELECT "a".*, "ab".*, "aba".* \ FROM "a" \ JOIN "b" "ab" ON "ab"."aid" = "a"."id" \ JOIN "a" "aba" ON "aba"."id" = "ab"."aid" """ let orderClauses = sqls.map { sql -> String in let prefixEndIndex = sql.index(sql.startIndex, offsetBy: prefix.count) return String(sql.suffix(from: prefixEndIndex)) } XCTAssertEqual(orderClauses, [ // a: { (r: QueryInterfaceRequest) in return r }, // ab: { (r: BelongsToAssociation) in return r } // aba: { (r: HasOneAssociation) in return r } "", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"aba\".\"id\" DESC", // ab: { (r: BelongsToAssociation) in return r.order(Column("name"), Column("id").desc) } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC", // ab: { (r: BelongsToAssociation) in return r.order(Column("name")).order(Column("id").desc) } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"ab\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"ab\".\"id\" DESC, \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"ab\".\"id\" DESC, \"aba\".\"id\" DESC", // ab: { (r: BelongsToAssociation) in return r.order(Column("name")).reversed() } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"ab\".\"name\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"ab\".\"name\" DESC, \"aba\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"ab\".\"name\" DESC, \"aba\".\"id\"", // ab: { (r: BelongsToAssociation) in return r.reversed() } // aba: { (r: HasOneAssociation) in return r } "", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"aba\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"aba\".\"id\"", // a: { (r: QueryInterfaceRequest) in return r.order(Column("id")) }, // ab: { (r: BelongsToAssociation) in return r } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\", \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\", \"aba\".\"id\" DESC", // ab: { (r: BelongsToAssociation) in return r.order(Column("name"), Column("id").desc) } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC", // ab: { (r: BelongsToAssociation) in return r.order(Column("name")).order(Column("id").desc) } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC, \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC", // ab: { (r: BelongsToAssociation) in return r.order(Column("name")).reversed() } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC, \"aba\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC, \"aba\".\"id\"", // ab: { (r: BelongsToAssociation) in return r.reversed() } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\", \"aba\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\", \"aba\".\"id\"", // a: { (r: QueryInterfaceRequest) in return r.order(Column("id")).reversed() } // ab: { (r: BelongsToAssociation) in return r } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\"", // ab: { (r: BelongsToAssociation) in return r.order(Column("name"), Column("id").desc) } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\"", // ab: { (r: BelongsToAssociation) in return r.order(Column("name")).order(Column("id").desc) } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\"", // ab: { (r: BelongsToAssociation) in return r.order(Column("name")).reversed() } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\", \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\", \"aba\".\"id\" DESC", // ab: { (r: BelongsToAssociation) in return r.reversed() } // aba: { (r: HasOneAssociation) in return r } " ORDER BY \"a\".\"id\" DESC", // aba: { (r: HasOneAssociation) in return r.order(Column("id")) } " ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\"", // aba: { (r: HasOneAssociation) in return r.order(Column("id")).reversed() } " ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\" DESC", ]) } } // TODO: Test if and only if we really want to build an association from any request // func testAssociationFromRequest() throws { // let dbQueue = try makeDatabaseQueue() // try dbQueue.inDatabase { db in // do { // let bRequest = B // .filter(Column("name") != nil) // .order(Column("id")) // let association = A.hasOne(bRequest) // let request = A.including(required: association) // try assertEqualSQL(db, request, """ // SELECT "a".*, "b".* \ // FROM "a" \ // JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL) \ // ORDER BY "b"."id" // """) // } // do { // let bRequest = RestrictedB // .filter(Column("name") != nil) // .order(Column("id")) // let association = A.hasOne(bRequest) // let request = A.including(required: association) // try assertEqualSQL(db, request, """ // SELECT "a".*, "b"."name" \ // FROM "a" \ // JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL) \ // ORDER BY "b"."id" // """) // } // do { // let bRequest = ExtendedB // .select([Column("name")]) // .filter(Column("name") != nil) // .order(Column("id")) // let association = A.hasOne(bRequest) // let request = A.including(required: association) // try assertEqualSQL(db, request, """ // SELECT "a".*, "b"."name" \ // FROM "a" \ // JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL) \ // ORDER BY "b"."id" // """) // } // } // } }