Files

1944 lines
80 KiB
Swift

import XCTest
import GRDB
private struct A: TableRecord { }
private struct B: TableRecord { }
private struct C: TableRecord { }
private struct D: TableRecord { }
class AssociationPrefetchingSQLTests: GRDBTestCase {
override func setup(_ dbWriter: some DatabaseWriter) throws {
// A.hasMany(B)
// A.hasMany(C)
// B.belongsTo(A)
// C.belongsTo(A)
// C.hasMany(D)
// D.belongsTo(C)
try dbWriter.write { db in
try db.create(table: "a") { t in
t.autoIncrementedPrimaryKey("cola1")
t.column("cola2", .text)
}
try db.create(table: "b") { t in
t.autoIncrementedPrimaryKey("colb1")
t.column("colb2", .integer).references("a")
t.column("colb3", .text)
}
try db.create(table: "c") { t in
t.autoIncrementedPrimaryKey("colc1")
t.column("colc2", .integer).references("a")
}
try db.create(table: "d") { t in
t.autoIncrementedPrimaryKey("cold1")
t.column("cold2", .integer).references("c")
t.column("cold3", .text)
}
try db.execute(
sql: """
INSERT INTO a (cola1, cola2) VALUES (?, ?);
INSERT INTO a (cola1, cola2) VALUES (?, ?);
INSERT INTO a (cola1, cola2) VALUES (?, ?);
INSERT INTO b (colb1, colb2, colb3) VALUES (?, ?, ?);
INSERT INTO b (colb1, colb2, colb3) VALUES (?, ?, ?);
INSERT INTO b (colb1, colb2, colb3) VALUES (?, ?, ?);
INSERT INTO b (colb1, colb2, colb3) VALUES (?, ?, ?);
INSERT INTO c (colc1, colc2) VALUES (?, ?);
INSERT INTO c (colc1, colc2) VALUES (?, ?);
INSERT INTO c (colc1, colc2) VALUES (?, ?);
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
""",
arguments: [
1, "a1",
2, "a2",
3, "a3",
4, 1, "b1",
5, 1, "b2",
6, 2, "b3",
14, nil, "b4",
7, 1,
8, 2,
9, 2,
10, 7, "d1",
11, 8, "d2",
12, 8, "d3",
13, 9, "d4",
])
}
}
func testIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
// Plain request
do {
let request = A
.including(all: A
.hasMany(B.self)
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT *, "colb2" AS "grdb_colb2" \
FROM "b" \
WHERE "colb2" IN (1, 2, 3) \
ORDER BY "colb1"
"""])
}
// Request with avoided prefetch
do {
let request = A
.none()
.including(all: A
.hasMany(B.self)
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM \"a\" WHERE 0 ORDER BY \"cola1\"
"""])
}
// Request with filters
do {
let request = A
.filter(Column("cola1") != 3)
.including(all: A
.hasMany(B.self)
.filter(Column("colb1") == 4)
.orderByPrimaryKey()
.forKey("bs1"))
.including(all: A
.hasMany(B.self)
.filter(Column("colb1") != 4)
.orderByPrimaryKey()
.forKey("bs2"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" \
WHERE "cola1" <> 3 \
ORDER BY "cola1"
""",
"""
SELECT *, "colb2" AS "grdb_colb2" \
FROM "b" \
WHERE ("colb1" = 4) AND ("colb2" IN (1, 2)) \
ORDER BY "colb1"
""",
"""
SELECT *, "colb2" AS "grdb_colb2" \
FROM "b" \
WHERE ("colb1" <> 4) AND ("colb2" IN (1, 2)) \
ORDER BY "colb1"
"""])
}
}
}
func testIncludingAllHasManyScalar() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
// Plain request
do {
let request = A
.including(all: A
.hasMany(B.self)
.select(Column("colb2"))
.distinct()
.order(Column("colb2")))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT DISTINCT "colb2", "colb2" AS "grdb_colb2" \
FROM "b" \
WHERE "colb2" IN (1, 2, 3) \
ORDER BY "colb2"
"""])
}
}
}
func testIncludingAllHasManyWithCompoundForeignKey() throws {
// We can use the CTE technique
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.create(table: "parent") { t in
t.column("parentA", .text)
t.column("parentB", .text)
t.primaryKey(["parentA", "parentB"])
}
try db.create(table: "child") { t in
t.column("pA", .text)
t.column("pB", .text)
t.column("name", .text)
t.foreignKey(["pA", "pB"], references: "parent")
}
try db.execute(sql: """
INSERT INTO parent (parentA, parentB) VALUES ('foo', 'bar');
INSERT INTO parent (parentA, parentB) VALUES ('baz', 'qux');
INSERT INTO child (pA, pB, name) VALUES ('foo', 'bar', 'foobar1');
INSERT INTO child (pA, pB, name) VALUES ('foo', 'bar', 'foobar2');
INSERT INTO child (pA, pB, name) VALUES ('baz', 'qux', 'bazqux1');
""")
struct Parent: TableRecord { }
struct Child: TableRecord { }
// Plain request
do {
let request = Parent
.including(all: Parent
.hasMany(Child.self))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" ORDER BY "parentA", "parentB"
""",
"""
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent") \
SELECT *, "pA" AS "grdb_pA", "pB" AS "grdb_pB" \
FROM "child" WHERE ("pA", "pB") IN "grdb_base"
"""])
}
// Request with avoided prefetch
do {
let request = Parent
.none()
.including(all: Parent
.hasMany(Child.self))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" WHERE 0 ORDER BY "parentA", "parentB"
"""])
}
// Request with filters
do {
let request = Parent
.including(all: Parent
.hasMany(Child.self)
.filter(Column("name") == "foo"))
.filter(Column("parentA") == "foo")
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" WHERE "parentA" = 'foo' ORDER BY "parentA", "parentB"
""",
"""
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent" WHERE "parentA" = 'foo') \
SELECT *, "pA" AS "grdb_pA", "pB" AS "grdb_pB" \
FROM "child" \
WHERE ("name" = 'foo') AND (("pA", "pB") IN "grdb_base")
"""])
}
// Limited ordered request (preserve both ordering and limit in the CTE)
do {
let request = Parent
.including(all: Parent.hasMany(Child.self))
.orderByPrimaryKey()
.limit(1)
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" ORDER BY "parentA", "parentB" LIMIT 1
""",
"""
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent" ORDER BY "parentA", "parentB" LIMIT 1) \
SELECT *, "pA" AS "grdb_pA", "pB" AS "grdb_pB" \
FROM "child" \
WHERE ("pA", "pB") IN "grdb_base"
"""])
}
}
}
func testIncludingAllHasManyIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
// Plain request
do {
let request = A
.including(all: A
.hasMany(C.self)
.including(all: C
.hasMany(D.self)
.orderByPrimaryKey())
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT *, "colc2" AS "grdb_colc2" \
FROM "c" \
WHERE "colc2" IN (1, 2, 3) \
ORDER BY "colc1"
""",
"""
SELECT *, "cold2" AS "grdb_cold2" \
FROM "d" \
WHERE "cold2" IN (7, 8, 9) \
ORDER BY "cold1"
"""])
}
// Request with avoided prefetch
do {
let request = A
.including(all: A
.hasMany(C.self)
.none()
.including(all: C
.hasMany(D.self)
.orderByPrimaryKey())
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT *, "colc2" AS "grdb_colc2" \
FROM "c" \
WHERE 0 AND ("colc2" IN (1, 2, 3)) \
ORDER BY "colc1"
"""])
}
// Request with filters
do {
let request = A
.filter(Column("cola1") != 3)
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") > 7)
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("ds1"))
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("ds2"))
.orderByPrimaryKey()
.forKey("cs1"))
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") < 9)
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("ds1"))
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("ds2"))
.orderByPrimaryKey()
.forKey("cs2"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * \
FROM "a" \
WHERE "cola1" <> 3 \
ORDER BY "cola1"
""",
"""
SELECT *, "colc2" AS "grdb_colc2" \
FROM "c" \
WHERE ("colc1" > 7) AND ("colc2" IN (1, 2)) \
ORDER BY "colc1"
""",
"""
SELECT *, "cold2" AS "grdb_cold2" \
FROM "d" \
WHERE ("cold1" = 11) AND ("cold2" IN (8, 9)) \
ORDER BY "cold1"
""",
"""
SELECT *, "cold2" AS "grdb_cold2" \
FROM "d" \
WHERE ("cold1" <> 11) AND ("cold2" IN (8, 9)) \
ORDER BY "cold1"
""",
"""
SELECT *, "colc2" AS "grdb_colc2" \
FROM "c" \
WHERE ("colc1" < 9) AND ("colc2" IN (1, 2)) \
ORDER BY "colc1"
""",
"""
SELECT *, "cold2" AS "grdb_cold2" \
FROM "d" \
WHERE ("cold1" = 11) AND ("cold2" IN (7, 8)) \
ORDER BY "cold1"
""",
"""
SELECT *, "cold2" AS "grdb_cold2" \
FROM "d" \
WHERE ("cold1" <> 11) AND ("cold2" IN (7, 8)) \
ORDER BY "cold1"
"""])
}
}
}
func testIncludingAllHasManyIncludingAllHasManyWithCompoundForeignKey() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.create(table: "parent") { t in
t.column("parentA", .text)
t.column("parentB", .text)
t.column("name", .text)
t.primaryKey(["parentA", "parentB"])
}
try db.create(table: "child") { t in
t.column("childA", .text)
t.column("childB", .text)
t.column("pA", .text)
t.column("pB", .text)
t.column("name", .text)
t.primaryKey(["childA", "childB"])
t.foreignKey(["pA", "pB"], references: "parent")
}
try db.create(table: "grandchild") { t in
t.column("cA", .text)
t.column("cB", .text)
t.column("name", .text)
t.foreignKey(["cA", "cB"], references: "child")
}
try db.execute(sql: """
INSERT INTO parent (parentA, parentB, name) VALUES ('foo', 'bar', 'foo');
INSERT INTO parent (parentA, parentB, name) VALUES ('baz', 'qux', 'foo');
INSERT INTO child (childA, childB, pA, pB, name) VALUES ('a', 'b', 'foo', 'bar', 'blue');
INSERT INTO child (childA, childB, pA, pB, name) VALUES ('c', 'd', 'foo', 'bar', 'pink');
INSERT INTO child (childA, childB, pA, pB, name) VALUES ('e', 'f', 'baz', 'qux', 'blue');
INSERT INTO grandchild (cA, cB, name) VALUES ('a', 'b', 'dog');
INSERT INTO grandchild (cA, cB, name) VALUES ('a', 'b', 'cat');
INSERT INTO grandchild (cA, cB, name) VALUES ('c', 'd', 'cat');
INSERT INTO grandchild (cA, cB, name) VALUES ('e', 'f', 'dog');
""")
struct Parent: TableRecord { }
struct Child: TableRecord { }
struct GrandChild: TableRecord { }
// Plain request
do {
let request = Parent
.including(all: Parent.hasMany(Child.self)
.including(all: Child.hasMany(GrandChild.self)))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" ORDER BY "parentA", "parentB"
""",
"""
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent") \
SELECT *, "pA" AS "grdb_pA", "pB" AS "grdb_pB" \
FROM "child" WHERE ("pA", "pB") IN "grdb_base"
""",
"""
WITH "grdb_base" AS (\
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent") \
SELECT "childA", "childB" FROM "child" \
WHERE ("pA", "pB") IN "grdb_base"\
) \
SELECT *, "cA" AS "grdb_cA", "cB" AS "grdb_cB" \
FROM "grandChild" WHERE ("cA", "cB") IN "grdb_base"
"""])
}
// Request with avoided prefetch
do {
let request = Parent
.none()
.including(all: Parent.hasMany(Child.self)
.including(all: Child.hasMany(GrandChild.self)))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" WHERE 0 ORDER BY "parentA", "parentB"
"""])
}
do {
let request = Parent
.including(all: Parent.hasMany(Child.self)
.none()
.including(all: Child.hasMany(GrandChild.self)))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" ORDER BY "parentA", "parentB"
""",
"""
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent") \
SELECT *, "pA" AS "grdb_pA", "pB" AS "grdb_pB" \
FROM "child" \
WHERE 0 AND (("pA", "pB") IN "grdb_base")
"""])
}
// Request with filters
do {
let request = Parent
.including(all: Parent.hasMany(Child.self)
.including(all: Child.hasMany(GrandChild.self)
.filter(Column("name") == "dog"))
.filter(Column("name") == "blue"))
.filter(Column("name") == "foo")
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "parent" WHERE "name" = 'foo' ORDER BY "parentA", "parentB"
""",
"""
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent" WHERE "name" = 'foo') \
SELECT *, "pA" AS "grdb_pA", "pB" AS "grdb_pB" \
FROM "child" WHERE ("name" = 'blue') AND (("pA", "pB") IN "grdb_base")
""",
"""
WITH "grdb_base" AS (\
WITH "grdb_base" AS (SELECT "parentA", "parentB" FROM "parent" WHERE "name" = 'foo') \
SELECT "childA", "childB" FROM "child" \
WHERE ("name" = 'blue') AND (("pA", "pB") IN "grdb_base")\
) \
SELECT *, "cA" AS "grdb_cA", "cB" AS "grdb_cB" \
FROM "grandChild" \
WHERE ("name" = 'dog') AND (("cA", "cB") IN "grdb_base")
"""])
}
}
}
func testIncludingAllHasManyIncludingRequiredOrOptionalHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
// Plain request
do {
let request = A
.including(all: A
.hasMany(C.self)
.including(required: C
.hasMany(D.self)
.orderByPrimaryKey())
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT "c".*, "c"."colc2" AS "grdb_colc2", "d".* \
FROM "c" \
JOIN "d" ON "d"."cold2" = "c"."colc1" \
WHERE "c"."colc2" IN (1, 2, 3) \
ORDER BY "c"."colc1", "d"."cold1"
"""])
}
// Request with avoided prefetch
do {
let request = A
.including(all: A
.hasMany(C.self)
.none()
.including(required: C
.hasMany(D.self)
.orderByPrimaryKey())
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT "c".*, "c"."colc2" AS "grdb_colc2", "d".* \
FROM "c" \
JOIN "d" ON "d"."cold2" = "c"."colc1" \
WHERE 0 AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "c"."colc1", "d"."cold1"
"""])
}
// Request with filters
do {
let request = A
.filter(Column("cola1") != 3)
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") > 7)
.including(optional: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("d1"))
.including(required: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("d2"))
.orderByPrimaryKey()
.forKey("cs1"))
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") < 9)
.including(optional: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("d1"))
.including(required: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("d2"))
.orderByPrimaryKey()
.forKey("cs2"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * \
FROM "a" \
WHERE "cola1" <> 3 \
ORDER BY "cola1"
""",
"""
SELECT "c".*, "c"."colc2" AS "grdb_colc2", "d1".*, "d2".* \
FROM "c" \
LEFT JOIN "d" "d1" ON ("d1"."cold2" = "c"."colc1") AND ("d1"."cold1" = 11) \
JOIN "d" "d2" ON ("d2"."cold2" = "c"."colc1") AND ("d2"."cold1" <> 11) \
WHERE ("c"."colc1" > 7) AND ("c"."colc2" IN (1, 2)) \
ORDER BY "c"."colc1", "d1"."cold1", "d2"."cold1"
""",
"""
SELECT "c".*, "c"."colc2" AS "grdb_colc2", "d1".*, "d2".* \
FROM "c" \
LEFT JOIN "d" "d1" ON ("d1"."cold2" = "c"."colc1") AND ("d1"."cold1" = 11) \
JOIN "d" "d2" ON ("d2"."cold2" = "c"."colc1") AND ("d2"."cold1" <> 11) \
WHERE ("c"."colc1" < 9) AND ("c"."colc2" IN (1, 2)) \
ORDER BY "c"."colc1", "d1"."cold1", "d2"."cold1"
"""])
}
}
}
func testIncludingAllHasManyThroughHasManyUsingHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.read { db in
// Plain request
do {
let request = A
.including(all: A
.hasMany(D.self, through: A.hasMany(C.self), using: C.hasMany(D.self))
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
"""])
}
// Request with filters
do {
let request = A
.filter(Column("cola1") != 3)
.including(all: A
.hasMany(D.self, through: A.hasMany(C.self).filter(Column("colc1") == 8).forKey("cs1"), using: C.hasMany(D.self))
.orderByPrimaryKey()
.forKey("ds1"))
.including(all: A
.hasMany(D.self, through: A.hasMany(C.self).forKey("cs2"), using: C.hasMany(D.self))
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("ds2"))
.including(all: A
.hasMany(D.self, through: A.hasMany(C.self).forKey("cs2"), using: C.hasMany(D.self))
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("ds3"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" \
WHERE "cola1" <> 3 \
ORDER BY "cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 8) AND ("c"."colc2" IN (1, 2)) \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2)) \
WHERE "d"."cold1" <> 11 \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2)) \
WHERE "d"."cold1" = 11 \
ORDER BY "d"."cold1"
"""])
}
}
}
func testIncludingAllHasManyThroughBelongsToUsingHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = B
.including(all: B.hasMany(C.self, through: B.belongsTo(A.self), using: A.hasMany(C.self))
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "b" ORDER BY "colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
"""])
}
// Request with filters
do {
// This request is an example of what users are unlikely to
// want, because of the shared key between the two different
// pivot associations.
let request = B
.including(all: B
.hasMany(
C.self,
through: B.belongsTo(A.self).filter(Column("cola2") == "a1"),
using: A.hasMany(C.self))
.orderByPrimaryKey())
.including(all: B
.hasMany(
C.self,
through: B.belongsTo(A.self).filter(Column("cola2") != "a1"),
using: A.hasMany(C.self))
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "b" ORDER BY "colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola2" <> 'a1') AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
"""])
}
// Request with filters
do {
// Another example of what users are unlikely to want, still
// because of the shared key between the two different pivot
// associations, and despite the distinct hasMany keys.
let request = B
.including(all: B
.hasMany(
C.self,
through: B.belongsTo(A.self).filter(Column("cola2") == "a1"),
using: A.hasMany(C.self))
.forKey("a1")
.orderByPrimaryKey())
.including(all: B
.hasMany(
C.self,
through: B.belongsTo(A.self).filter(Column("cola2") != "a1"),
using: A.hasMany(C.self))
.forKey("nota1")
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "b" ORDER BY "colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola2" <> 'a1') AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola2" <> 'a1') AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
"""])
}
// Request with filters
do {
// This request is a "fixed" version of the previous request,
// where the two different pivot associations do not share the
// same key.
let request = B
.including(all: B
.hasMany(
C.self,
through: B.belongsTo(A.self).filter(Column("cola2") == "a1").forKey("a1"),
using: A.hasMany(C.self))
.orderByPrimaryKey())
.including(all: B
.hasMany(
C.self,
through: B.belongsTo(A.self).filter(Column("cola2") != "a1").forKey("nota1"),
using: A.hasMany(C.self))
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "b" ORDER BY "colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" <> 'a1') AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
"""])
}
}
}
func testIncludingAllHasManyThroughHasOneUsingHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = A
.including(all: A.hasMany(D.self, through: A.hasOne(C.self), using: C.hasMany(D.self))
.orderByPrimaryKey())
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
"""])
}
// Request with filters
do {
// This request is an example of what users are unlikely to
// want, because of the shared key between the two different
// pivot associations.
// However, A.hasMany(C.self) does not conflict. This is an
// indirect proof that it feeds an association key, "as", which
// is distinc from "a" (A.hasOne(C.self)).
let request = A
.including(all: A
.hasMany(
D.self,
through: A.hasOne(C.self).filter(Column("colc1") == 7),
using: C.hasMany(D.self))
.orderByPrimaryKey())
.including(all: A
.hasMany(
D.self,
through: A.hasOne(C.self).filter(Column("colc1") != 7),
using: C.hasMany(D.self))
.orderByPrimaryKey())
.including(all: A.hasMany(C.self))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 7) AND ("c"."colc1" <> 7) AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
""",
"""
SELECT *, "colc2" AS "grdb_colc2" FROM "c" WHERE "colc2" IN (1, 2, 3)
"""])
}
// Request with filters
do {
// Another example of what users are unlikely to want, still
// because of the shared key between the two different pivot
// associations, and despite the distinct hasMany keys.
let request = A
.including(all: A
.hasMany(
D.self,
through: A.hasOne(C.self).filter(Column("colc1") == 7),
using: C.hasMany(D.self))
.forKey("c7")
.orderByPrimaryKey())
.including(all: A
.hasMany(
D.self,
through: A.hasOne(C.self).filter(Column("colc1") != 7),
using: C.hasMany(D.self))
.forKey("notc7")
.orderByPrimaryKey())
.including(all: A.hasMany(C.self))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 7) AND ("c"."colc1" <> 7) AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 7) AND ("c"."colc1" <> 7) AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
""",
"""
SELECT *, "colc2" AS "grdb_colc2" FROM "c" WHERE "colc2" IN (1, 2, 3)
"""])
}
// Request with filters
do {
// This request is a "fixed" version of the previous request,
// where the two different pivot associations do not share the
// same key.
let request = A
.including(all: A
.hasMany(
D.self,
through: A.hasOne(C.self).filter(Column("colc1") == 7).forKey("c7"),
using: C.hasMany(D.self))
.orderByPrimaryKey())
.including(all: A
.hasMany(
D.self,
through: A.hasOne(C.self).filter(Column("colc1") != 7).forKey("notc7"),
using: C.hasMany(D.self))
.orderByPrimaryKey())
.including(all: A.hasMany(C.self))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" ORDER BY "cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 7) AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" <> 7) AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
""",
"""
SELECT *, "colc2" AS "grdb_colc2" FROM "c" WHERE "colc2" IN (1, 2, 3)
"""])
}
}
}
func testIncludingOptionalBelongsToIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = B
.including(optional: B
.belongsTo(A.self)
.including(all: A
.hasMany(C.self)
.orderByPrimaryKey())
)
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT "b".*, "a".* \
FROM "b" \
LEFT JOIN "a" ON "a"."cola1" = "b"."colb2" \
ORDER BY "b"."colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
"""])
}
// Request with filters
do {
let request = B
.including(optional: B
.belongsTo(A.self)
.filter(Column("cola2") == "a1")
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") == 9)
.orderByPrimaryKey()
.forKey("cs1"))
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") != 9)
.orderByPrimaryKey()
.forKey("cs2"))
.forKey("a1"))
.including(optional: B
.belongsTo(A.self)
.filter(Column("cola2") == "a2")
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") == 9)
.orderByPrimaryKey()
.forKey("cs1"))
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") != 9)
.orderByPrimaryKey()
.forKey("cs2"))
.forKey("a2"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT "b".*, "a1".*, "a2".* \
FROM "b" \
LEFT JOIN "a" "a1" ON ("a1"."cola1" = "b"."colb2") AND ("a1"."cola2" = 'a1') \
LEFT JOIN "a" "a2" ON ("a2"."cola1" = "b"."colb2") AND ("a2"."cola2" = 'a2') \
ORDER BY "b"."colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" = 9 \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" <> 9 \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a2') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" = 9 \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a2') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" <> 9 \
ORDER BY "c"."colc1"
"""])
}
}
}
func testIncludingOptionalHasOneIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = A
.including(optional: A
.hasOne(C.self)
.including(all: C
.hasMany(D.self)
.orderByPrimaryKey())
)
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT "a".*, "c".* \
FROM "a" \
LEFT JOIN "c" ON "c"."colc2" = "a"."cola1" \
ORDER BY "a"."cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
"""])
}
// Request with filters
do {
let request = A
.including(optional: A
.hasOne(C.self)
.filter(Column("colc1") == 9)
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("ds1"))
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("ds2"))
.forKey("c1"))
.including(optional: A
.hasOne(C.self)
.filter(Column("colc1") != 9)
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("ds1"))
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("ds2"))
.forKey("c2"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT "a".*, "c1".*, "c2".* \
FROM "a" \
LEFT JOIN "c" "c1" ON ("c1"."colc2" = "a"."cola1") AND ("c1"."colc1" = 9) \
LEFT JOIN "c" "c2" ON ("c2"."colc2" = "a"."cola1") AND ("c2"."colc1" <> 9) \
ORDER BY "a"."cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" = 11 \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" <> 11 \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" <> 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" = 11 \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" <> 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" <> 11 \
ORDER BY "d"."cold1"
"""])
}
}
}
func testIncludingOptionalBelongsToIncludingOptionalBelongsToIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = D
.including(optional: D
.belongsTo(C.self)
.including(optional: C
.belongsTo(A.self)
.including(all: A
.hasMany(B.self)
.orderByPrimaryKey())))
.orderByPrimaryKey()
.filter(Column("cold2") != 8)
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT "d".*, "c".*, "a".* \
FROM "d" \
LEFT JOIN "c" ON "c"."colc1" = "d"."cold2" \
LEFT JOIN "a" ON "a"."cola1" = "c"."colc2" \
WHERE "d"."cold2" <> 8 \
ORDER BY "d"."cold1"
""",
"""
SELECT "b".*, "c"."colc1" AS "grdb_colc1" \
FROM "b" \
JOIN "a" ON "a"."cola1" = "b"."colb2" \
JOIN "c" ON ("c"."colc2" = "a"."cola1") AND ("c"."colc1" IN (7, 9)) \
ORDER BY "b"."colb1"
"""])
}
}
}
func testIncludingOptionalHasOneThroughIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = D
.including(optional: D
.hasOne(A.self, through: D.belongsTo(C.self), using: C.belongsTo(A.self))
.including(all: A
.hasMany(B.self)
.orderByPrimaryKey()))
.orderByPrimaryKey()
.filter(Column("cold2") != 8)
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT "d".*, "a".* \
FROM "d" \
LEFT JOIN "c" ON "c"."colc1" = "d"."cold2" \
LEFT JOIN "a" ON "a"."cola1" = "c"."colc2" \
WHERE "d"."cold2" <> 8 \
ORDER BY "d"."cold1"
""",
"""
SELECT "b".*, "c"."colc1" AS "grdb_colc1" \
FROM "b" \
JOIN "a" ON "a"."cola1" = "b"."colb2" \
JOIN "c" ON ("c"."colc2" = "a"."cola1") AND ("c"."colc1" IN (7, 9)) \
ORDER BY "b"."colb1"
"""])
}
}
}
func testJoiningOptionalBelongsToIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = B
.joining(optional: B
.belongsTo(A.self)
.including(all: A
.hasMany(C.self)
.orderByPrimaryKey())
)
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
// LEFT JOIN in the first query are useless but harmless.
// And SQLite may well optimize them out.
// So don't bother removing them.
XCTAssertEqual(selectQueries, [
"""
SELECT "b".* \
FROM "b" \
LEFT JOIN "a" ON "a"."cola1" = "b"."colb2" \
ORDER BY "b"."colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola1" IN (1, 2)) \
ORDER BY "c"."colc1"
"""])
}
// Request with filters
do {
let request = B
.joining(optional: B
.belongsTo(A.self)
.filter(Column("cola2") == "a1")
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") == 9)
.orderByPrimaryKey()
.forKey("cs1"))
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") != 9)
.orderByPrimaryKey()
.forKey("cs2"))
.forKey("a1"))
.joining(optional: B
.belongsTo(A.self)
.filter(Column("cola2") == "a2")
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") == 9)
.orderByPrimaryKey()
.forKey("cs1"))
.including(all: A
.hasMany(C.self)
.filter(Column("colc1") != 9)
.orderByPrimaryKey()
.forKey("cs2"))
.forKey("a2"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
// LEFT JOIN in the first query are useless but harmless.
// And SQLite may well optimize them out.
// So don't bother removing them.
XCTAssertEqual(selectQueries, [
"""
SELECT "b".* \
FROM "b" \
LEFT JOIN "a" "a1" ON ("a1"."cola1" = "b"."colb2") AND ("a1"."cola2" = 'a1') \
LEFT JOIN "a" "a2" ON ("a2"."cola1" = "b"."colb2") AND ("a2"."cola2" = 'a2') \
ORDER BY "b"."colb1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" = 9 \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a1') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" <> 9 \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a2') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" = 9 \
ORDER BY "c"."colc1"
""",
"""
SELECT "c".*, "a"."cola1" AS "grdb_cola1" \
FROM "c" \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND ("a"."cola2" = 'a2') AND ("a"."cola1" IN (1, 2)) \
WHERE "c"."colc1" <> 9 \
ORDER BY "c"."colc1"
"""])
}
}
}
func testJoiningOptionalHasOneIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = A
.joining(optional: A
.hasOne(C.self)
.including(all: C
.hasMany(D.self)
.orderByPrimaryKey())
)
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
// LEFT JOIN in the first query are useless but harmless.
// And SQLite may well optimize them out.
// So don't bother removing them.
XCTAssertEqual(selectQueries, [
"""
SELECT "a".* \
FROM "a" \
LEFT JOIN "c" ON "c"."colc2" = "a"."cola1" \
ORDER BY "a"."cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2, 3)) \
ORDER BY "d"."cold1"
"""])
}
// Request with filters
do {
let request = A
.joining(optional: A
.hasOne(C.self)
.filter(Column("colc1") == 9)
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("ds1"))
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("ds2"))
.forKey("c1"))
.joining(optional: A
.hasOne(C.self)
.filter(Column("colc1") != 9)
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") == 11)
.orderByPrimaryKey()
.forKey("ds1"))
.including(all: C
.hasMany(D.self)
.filter(Column("cold1") != 11)
.orderByPrimaryKey()
.forKey("ds2"))
.forKey("c2"))
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
// LEFT JOIN in the first query are useless but harmless.
// And SQLite may well optimize them out.
// So don't bother removing them.
XCTAssertEqual(selectQueries, [
"""
SELECT "a".* \
FROM "a" \
LEFT JOIN "c" "c1" ON ("c1"."colc2" = "a"."cola1") AND ("c1"."colc1" = 9) \
LEFT JOIN "c" "c2" ON ("c2"."colc2" = "a"."cola1") AND ("c2"."colc1" <> 9) \
ORDER BY "a"."cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" = 11 \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" = 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" <> 11 \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" <> 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" = 11 \
ORDER BY "d"."cold1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc1" <> 9) AND ("c"."colc2" IN (1, 2, 3)) \
WHERE "d"."cold1" <> 11 \
ORDER BY "d"."cold1"
"""])
}
}
}
func testJoiningOptionalBelongsToJoiningOptionalBelongsToIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = D
.joining(optional: D
.belongsTo(C.self)
.joining(optional: C
.belongsTo(A.self)
.including(all: A
.hasMany(B.self)
.orderByPrimaryKey())))
.orderByPrimaryKey()
.filter(Column("cold2") != 8)
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
// LEFT JOIN in the first query are useless but harmless.
// And SQLite may well optimize them out.
// So don't bother removing them.
XCTAssertEqual(selectQueries, [
"""
SELECT "d".* \
FROM "d" \
LEFT JOIN "c" ON "c"."colc1" = "d"."cold2" \
LEFT JOIN "a" ON "a"."cola1" = "c"."colc2" \
WHERE "d"."cold2" <> 8 \
ORDER BY "d"."cold1"
""",
"""
SELECT "b".*, "c"."colc1" AS "grdb_colc1" \
FROM "b" \
JOIN "a" ON "a"."cola1" = "b"."colb2" \
JOIN "c" ON ("c"."colc2" = "a"."cola1") AND ("c"."colc1" IN (7, 9)) \
ORDER BY "b"."colb1"
"""])
}
}
}
func testJoiningOptionalHasOneThroughIncludingAllHasMany() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = D
.joining(optional: D
.hasOne(A.self, through: D.belongsTo(C.self), using: C.belongsTo(A.self))
.including(all: A
.hasMany(B.self)
.orderByPrimaryKey()))
.orderByPrimaryKey()
.filter(Column("cold2") != 8)
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
// LEFT JOIN in the first query are useless but harmless.
// And SQLite may well optimize them out.
// So don't bother removing them.
XCTAssertEqual(selectQueries, [
"""
SELECT "d".* \
FROM "d" \
LEFT JOIN "c" ON "c"."colc1" = "d"."cold2" \
LEFT JOIN "a" ON "a"."cola1" = "c"."colc2" \
WHERE "d"."cold2" <> 8 \
ORDER BY "d"."cold1"
""",
"""
SELECT "b".*, "c"."colc1" AS "grdb_colc1" \
FROM "b" \
JOIN "a" ON "a"."cola1" = "b"."colb2" \
JOIN "c" ON ("c"."colc2" = "a"."cola1") AND ("c"."colc1" IN (7, 9)) \
ORDER BY "b"."colb1"
"""])
}
}
}
func testAssociationFilteredByOtherAssociation() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
// Plain request
do {
let request = A
.including(all: A
.hasMany(
D.self,
through: A.hasMany(C.self)
.joining(required: C.belongsTo(A.self).filter(sql: "1")),
using: C.hasMany(D.self))
.orderByPrimaryKey())
.filter(sql: "1 + 1")
.orderByPrimaryKey()
sqlQueries.removeAll()
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "a" WHERE 1 + 1 ORDER BY "cola1"
""",
"""
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
FROM "d" \
JOIN "c" ON ("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2, 3)) \
JOIN "a" ON ("a"."cola1" = "c"."colc2") AND (1) \
ORDER BY "d"."cold1"
"""])
}
}
}
func testAggregate() throws {
struct Award: TableRecord { }
struct Player: TableRecord {
static let awards = hasMany(Award.self)
}
struct Team: TableRecord {
static let players = hasMany(Player.self)
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.create(table: "team") { t in
t.autoIncrementedPrimaryKey("teamId")
}
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("playerId")
t.column("parentId", .integer).references("team")
t.column("score")
t.column("category")
}
try db.create(table: "award") { t in
t.autoIncrementedPrimaryKey("awardId")
t.column("playerId", .integer).references("player")
}
try db.execute(sql: "INSERT INTO team DEFAULT VALUES")
do {
// Group an association
sqlQueries.removeAll()
let association = Team.players.select(max(Column("score"))).group(Column("category"))
let request = Team.including(all: association)
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "team"
""",
"""
SELECT MAX("score"), "parentId" AS "grdb_parentId" \
FROM "player" \
WHERE "parentId" = 1 \
GROUP BY "category"
"""])
}
do {
// Filter an association with an association aggregate
sqlQueries.removeAll()
let association = Team.players.having(Player.awards.isEmpty)
let request = Team.including(all: association)
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "team"
""",
"""
SELECT "player".*, "player"."parentId" AS "grdb_parentId" \
FROM "player" \
LEFT JOIN "award" ON "award"."playerId" = "player"."playerId" \
WHERE "player"."parentId" = 1 \
GROUP BY "player"."playerId" \
HAVING COUNT(DISTINCT "award"."awardId") = 0
"""])
}
do {
// Annotate an association with an association aggregate
sqlQueries.removeAll()
let association = Team.players.annotated(with: Player.awards.count)
let request = Team.including(all: association)
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "team"
""",
"""
SELECT "player".*, COUNT(DISTINCT "award"."awardId") AS "awardCount", "player"."parentId" AS "grdb_parentId" \
FROM "player" \
LEFT JOIN "award" ON "award"."playerId" = "player"."playerId" \
WHERE "player"."parentId" = 1 \
GROUP BY "player"."playerId"
"""])
}
}
}
func testDistinct() throws {
struct Player: TableRecord { }
struct Team: TableRecord {
static let players = hasMany(Player.self)
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.create(table: "team") { t in
t.autoIncrementedPrimaryKey("teamId")
}
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("playerId")
t.column("parentId", .integer).references("team")
}
try db.execute(sql: "INSERT INTO team DEFAULT VALUES")
do {
sqlQueries.removeAll()
let association = Team.players.distinct()
let request = Team.including(all: association)
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "team"
""",
"""
SELECT DISTINCT *, "parentId" AS "grdb_parentId" \
FROM "player" \
WHERE "parentId" = 1
"""])
}
}
}
func testCTE() throws {
struct Player: TableRecord { }
struct Team: TableRecord {
static let players = hasMany(Player.self)
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.create(table: "team") { t in
t.autoIncrementedPrimaryKey("teamId")
}
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("playerId")
t.column("parentId", .integer).references("team")
}
try db.execute(sql: "INSERT INTO team DEFAULT VALUES")
do {
sqlQueries.removeAll()
let cte = CommonTableExpression(named: "cte", sql: "SELECT 42")
let association = Team.players.with(cte).filter(Column("playerId") == cte.all())
let request = Team.including(all: association)
_ = try Row.fetchAll(db, request)
let selectQueries = sqlQueries.filter(isSelectQuery)
XCTAssertEqual(selectQueries, [
"""
SELECT * FROM "team"
""",
"""
WITH "cte" AS (SELECT 42) \
SELECT *, "parentId" AS "grdb_parentId" \
FROM "player" \
WHERE ("playerId" = (SELECT * FROM "cte")) AND ("parentId" = 1)
"""])
}
}
}
// Return SELECT queries, but omit schema queries.
private func isSelectQuery(_ query: String) -> Bool {
return query.contains("SELECT") && !query.contains("sqlite_") && !query.contains("pragma_table_xinfo")
}
}