383 lines
16 KiB
Swift
383 lines
16 KiB
Swift
import XCTest
|
|
import GRDB
|
|
|
|
private struct Parent: TableRecord, FetchableRecord, Decodable, Equatable {
|
|
static let children = hasMany(Child.self)
|
|
var id: Int64
|
|
var name: String
|
|
}
|
|
|
|
private struct Child: TableRecord, FetchableRecord, Decodable, Equatable {
|
|
var id: Int64
|
|
var parentId: Int64
|
|
var name: String
|
|
}
|
|
|
|
private struct ParentInfo: FetchableRecord, Decodable, Equatable {
|
|
var parent: Parent
|
|
var children: [Child]
|
|
}
|
|
|
|
class ValueObservationQueryInterfaceRequestTests: GRDBTestCase {
|
|
private func setup(_ db: Database) throws {
|
|
try db.create(table: "parent") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.column("name", .text)
|
|
}
|
|
try db.create(table: "child") { t in
|
|
t.autoIncrementedPrimaryKey("id")
|
|
t.belongsTo("parent", onDelete: .cascade)
|
|
t.column("name", .text)
|
|
}
|
|
}
|
|
|
|
private func performDatabaseModifications(_ db: Database) throws {
|
|
try db.inTransaction {
|
|
try db.execute(sql: """
|
|
INSERT INTO parent (id, name) VALUES (1, 'foo');
|
|
INSERT INTO parent (id, name) VALUES (2, 'bar');
|
|
""")
|
|
return .commit
|
|
}
|
|
try db.inTransaction {
|
|
try db.execute(sql: """
|
|
INSERT INTO child (id, parentId, name) VALUES (1, 1, 'fooA');
|
|
INSERT INTO child (id, parentId, name) VALUES (2, 1, 'fooB');
|
|
INSERT INTO child (id, parentId, name) VALUES (3, 2, 'barA');
|
|
""")
|
|
return .commit
|
|
}
|
|
try db.inTransaction {
|
|
try db.execute(sql: """
|
|
UPDATE child SET name = 'fooA2' WHERE id = 1;
|
|
""")
|
|
return .commit
|
|
}
|
|
try db.inTransaction {
|
|
try db.execute(sql: """
|
|
INSERT INTO parent (id, name) VALUES (3, 'baz');
|
|
INSERT INTO child (id, parentId, name) VALUES (4, 3, 'bazA');
|
|
""")
|
|
return .commit
|
|
}
|
|
try db.inTransaction {
|
|
try db.execute(sql: """
|
|
DELETE FROM parent WHERE id = 1;
|
|
DELETE FROM child;
|
|
""")
|
|
return .commit
|
|
}
|
|
}
|
|
|
|
func testOneRowWithPrefetchedRows() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write(setup)
|
|
let request = Parent
|
|
.including(all: Parent.children.orderByPrimaryKey())
|
|
.orderByPrimaryKey()
|
|
.asRequest(of: Row.self)
|
|
let observation = ValueObservation.trackingConstantRegion(request.fetchOne)
|
|
|
|
let recorder = observation.record(in: dbQueue)
|
|
try dbQueue.writeWithoutTransaction(performDatabaseModifications)
|
|
let results = try wait(for: recorder.next(6), timeout: 5)
|
|
|
|
XCTAssertNil(results[0])
|
|
|
|
XCTAssertEqual(results[1]!.unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[1]!.prefetchedRows["children"], [])
|
|
|
|
XCTAssertEqual(results[2]!.unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[2]!.prefetchedRows["children"], [
|
|
["id": 1, "parentId": 1, "name": "fooA", "grdb_parentId": 1],
|
|
["id": 2, "parentId": 1, "name": "fooB", "grdb_parentId": 1]])
|
|
|
|
XCTAssertEqual(results[3]!.unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[3]!.prefetchedRows["children"], [
|
|
["id": 1, "parentId": 1, "name": "fooA2", "grdb_parentId": 1],
|
|
["id": 2, "parentId": 1, "name": "fooB", "grdb_parentId": 1]])
|
|
|
|
XCTAssertEqual(results[4]!.unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[4]!.prefetchedRows["children"], [
|
|
["id": 1, "parentId": 1, "name": "fooA2", "grdb_parentId": 1],
|
|
["id": 2, "parentId": 1, "name": "fooB", "grdb_parentId": 1]])
|
|
|
|
XCTAssertEqual(results[5]!.unscoped, ["id": 2, "name": "bar"])
|
|
XCTAssertEqual(results[5]!.prefetchedRows["children"], [])
|
|
}
|
|
|
|
func testAllRowsWithPrefetchedRows() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.write(setup)
|
|
let request = Parent
|
|
.including(all: Parent.children.orderByPrimaryKey())
|
|
.orderByPrimaryKey()
|
|
.asRequest(of: Row.self)
|
|
let observation = ValueObservation.trackingConstantRegion(request.fetchAll)
|
|
|
|
let recorder = observation.record(in: dbQueue)
|
|
try dbQueue.writeWithoutTransaction(performDatabaseModifications)
|
|
let results = try wait(for: recorder.next(6), timeout: 5)
|
|
|
|
XCTAssertEqual(results[0].count, 0)
|
|
|
|
XCTAssertEqual(results[1].count, 2)
|
|
XCTAssertEqual(results[1][0].unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[1][0].prefetchedRows["children"], [])
|
|
XCTAssertEqual(results[1][1].unscoped, ["id": 2, "name": "bar"])
|
|
XCTAssertEqual(results[1][1].prefetchedRows["children"], [])
|
|
|
|
XCTAssertEqual(results[2].count, 2)
|
|
XCTAssertEqual(results[2][0].unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[2][0].prefetchedRows["children"], [
|
|
["id": 1, "parentId": 1, "name": "fooA", "grdb_parentId": 1],
|
|
["id": 2, "parentId": 1, "name": "fooB", "grdb_parentId": 1]])
|
|
XCTAssertEqual(results[2][1].unscoped, ["id": 2, "name": "bar"])
|
|
XCTAssertEqual(results[2][1].prefetchedRows["children"], [
|
|
["id": 3, "parentId": 2, "name": "barA", "grdb_parentId": 2]])
|
|
|
|
XCTAssertEqual(results[3].count, 2)
|
|
XCTAssertEqual(results[3][0].unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[3][0].prefetchedRows["children"], [
|
|
["id": 1, "parentId": 1, "name": "fooA2", "grdb_parentId": 1],
|
|
["id": 2, "parentId": 1, "name": "fooB", "grdb_parentId": 1]])
|
|
XCTAssertEqual(results[3][1].unscoped, ["id": 2, "name": "bar"])
|
|
XCTAssertEqual(results[3][1].prefetchedRows["children"], [
|
|
["id": 3, "parentId": 2, "name": "barA", "grdb_parentId": 2]])
|
|
|
|
XCTAssertEqual(results[4].count, 3)
|
|
XCTAssertEqual(results[4][0].unscoped, ["id": 1, "name": "foo"])
|
|
XCTAssertEqual(results[4][0].prefetchedRows["children"], [
|
|
["id": 1, "parentId": 1, "name": "fooA2", "grdb_parentId": 1],
|
|
["id": 2, "parentId": 1, "name": "fooB", "grdb_parentId": 1]])
|
|
XCTAssertEqual(results[4][1].unscoped, ["id": 2, "name": "bar"])
|
|
XCTAssertEqual(results[4][1].prefetchedRows["children"], [
|
|
["id": 3, "parentId": 2, "name": "barA", "grdb_parentId": 2]])
|
|
XCTAssertEqual(results[4][2].unscoped, ["id": 3, "name": "baz"])
|
|
XCTAssertEqual(results[4][2].prefetchedRows["children"], [
|
|
["id": 4, "parentId": 3, "name": "bazA", "grdb_parentId": 3]])
|
|
|
|
XCTAssertEqual(results[5].count, 2)
|
|
XCTAssertEqual(results[5][0].unscoped, ["id": 2, "name": "bar"])
|
|
XCTAssertEqual(results[5][0].prefetchedRows["children"], [])
|
|
XCTAssertEqual(results[5][1].unscoped, ["id": 3, "name": "baz"])
|
|
XCTAssertEqual(results[5][1].prefetchedRows["children"], [])
|
|
}
|
|
|
|
func testOneRecordWithPrefetchedRows() throws {
|
|
let request = Parent
|
|
.including(all: Parent.children.orderByPrimaryKey())
|
|
.orderByPrimaryKey()
|
|
.asRequest(of: ParentInfo.self)
|
|
|
|
try assertValueObservation(
|
|
ValueObservation.trackingConstantRegion(request.fetchOne),
|
|
records: [
|
|
nil,
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: []),
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA2"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA2"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: []),
|
|
],
|
|
setup: setup,
|
|
recordedUpdates: performDatabaseModifications)
|
|
|
|
// The fundamental technique for removing duplicates of non-Equatable types
|
|
try assertValueObservation(
|
|
ValueObservation
|
|
.trackingConstantRegion { db in try Row.fetchOne(db, request) }
|
|
.removeDuplicates()
|
|
.map { row in try row.map(ParentInfo.init(row:)) },
|
|
records: [
|
|
nil,
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: []),
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA2"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: []),
|
|
],
|
|
setup: setup,
|
|
recordedUpdates: performDatabaseModifications)
|
|
}
|
|
|
|
func testAllRecordsWithPrefetchedRows() throws {
|
|
let request = Parent
|
|
.including(all: Parent.children.orderByPrimaryKey())
|
|
.orderByPrimaryKey()
|
|
.asRequest(of: ParentInfo.self)
|
|
|
|
try assertValueObservation(
|
|
ValueObservation.trackingConstantRegion(request.fetchAll),
|
|
records: [
|
|
[],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: []),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: []),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: [
|
|
Child(id: 3, parentId: 2, name: "barA"),
|
|
]),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA2"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: [
|
|
Child(id: 3, parentId: 2, name: "barA"),
|
|
]),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA2"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: [
|
|
Child(id: 3, parentId: 2, name: "barA"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 3, name: "baz"),
|
|
children: [
|
|
Child(id: 4, parentId: 3, name: "bazA"),
|
|
]),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: []),
|
|
ParentInfo(
|
|
parent: Parent(id: 3, name: "baz"),
|
|
children: []),
|
|
],
|
|
],
|
|
setup: setup,
|
|
recordedUpdates: performDatabaseModifications)
|
|
|
|
// The fundamental technique for removing duplicates of non-Equatable types
|
|
try assertValueObservation(
|
|
ValueObservation
|
|
.trackingConstantRegion { db in try Row.fetchAll(db, request) }
|
|
.removeDuplicates()
|
|
.map { rows in try rows.map(ParentInfo.init(row:)) },
|
|
records: [
|
|
[],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: []),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: []),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: [
|
|
Child(id: 3, parentId: 2, name: "barA"),
|
|
]),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA2"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: [
|
|
Child(id: 3, parentId: 2, name: "barA"),
|
|
]),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 1, name: "foo"),
|
|
children: [
|
|
Child(id: 1, parentId: 1, name: "fooA2"),
|
|
Child(id: 2, parentId: 1, name: "fooB"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: [
|
|
Child(id: 3, parentId: 2, name: "barA"),
|
|
]),
|
|
ParentInfo(
|
|
parent: Parent(id: 3, name: "baz"),
|
|
children: [
|
|
Child(id: 4, parentId: 3, name: "bazA"),
|
|
]),
|
|
],
|
|
[
|
|
ParentInfo(
|
|
parent: Parent(id: 2, name: "bar"),
|
|
children: []),
|
|
ParentInfo(
|
|
parent: Parent(id: 3, name: "baz"),
|
|
children: []),
|
|
],
|
|
],
|
|
setup: setup,
|
|
recordedUpdates: performDatabaseModifications)
|
|
}
|
|
}
|