Skip to content

Commit 8008dae

Browse files
committed
Add unit tests for object cache
1 parent 22211c1 commit 8008dae

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import Foundation
2+
import Testing
3+
import CoreModel
4+
import SQLite
5+
@testable import CoreModelSQLite
6+
7+
/// Verifies `SQLiteDatabase`'s object cache: repeated ID lookups are served from memory,
8+
/// and every write shape that can change a fetched representation — direct updates,
9+
/// inverse relationship changes, reassignment side effects, and deletes — invalidates
10+
/// the affected entries so reads never observe stale data.
11+
@Suite("Object Cache")
12+
struct CacheTests {
13+
14+
@Test func fetchByIDPopulatesCache() async throws {
15+
let database = try makeDatabase()
16+
let person = ModelData(
17+
entity: "Person",
18+
id: "person1",
19+
attributes: ["name": .string("Alice"), "age": .int32(30)]
20+
)
21+
try await database.insert(person)
22+
#expect(await database.cache["Person"]?["person1"] == nil)
23+
let fetched = try #require(try await database.fetch("Person", for: "person1"))
24+
#expect(await database.cache["Person"]?["person1"] == fetched)
25+
// a second fetch returns the cached value
26+
let cached = try #require(try await database.fetch("Person", for: "person1"))
27+
#expect(cached == fetched)
28+
}
29+
30+
@Test func fetchRequestRegistersResults() async throws {
31+
let database = try makeDatabase()
32+
let people = (0..<5).map { index in
33+
ModelData(
34+
entity: "Person",
35+
id: ObjectID(rawValue: "person\(index)"),
36+
attributes: ["name": .string("Person \(index)"), "age": .int32(Int32(20 + index))]
37+
)
38+
}
39+
try await database.insert(people)
40+
let results = try await database.fetch(FetchRequest(entity: "Person"))
41+
#expect(results.count == 5)
42+
let cache = await database.cache["Person"]
43+
#expect(cache?.count == 5)
44+
for value in results {
45+
#expect(cache?[value.id] == value)
46+
}
47+
}
48+
49+
@Test func updateInvalidatesCachedObject() async throws {
50+
let database = try makeDatabase()
51+
var person = ModelData(
52+
entity: "Person",
53+
id: "person1",
54+
attributes: ["name": .string("Alice"), "age": .int32(30)]
55+
)
56+
try await database.insert(person)
57+
_ = try await database.fetch("Person", for: "person1")
58+
// update through the same database
59+
person.attributes["age"] = .int32(31)
60+
try await database.insert(person)
61+
#expect(await database.cache["Person"] == nil)
62+
let fetched = try #require(try await database.fetch("Person", for: "person1"))
63+
#expect(fetched.attributes["age"] == .int32(31))
64+
}
65+
66+
@Test func writeInvalidatesInverseRelationship() async throws {
67+
let database = try makeDatabase()
68+
let team = ModelData(entity: "Team", id: "team1", attributes: ["name": .string("Red")])
69+
try await database.insert(team)
70+
// cache the team with no members
71+
let cached = try #require(try await database.fetch("Team", for: "team1"))
72+
#expect(cached.relationships["members"] == .toMany([]))
73+
// inserting a person pointing at the team changes the team's derived `members`
74+
let person = ModelData(
75+
entity: "Person",
76+
id: "person1",
77+
attributes: ["name": .string("Alice")],
78+
relationships: ["team": .toOne("team1")]
79+
)
80+
try await database.insert(person)
81+
let fetched = try #require(try await database.fetch("Team", for: "team1"))
82+
#expect(fetched.relationships["members"] == .toMany(["person1"]))
83+
}
84+
85+
@Test func reassignmentInvalidatesOtherRows() async throws {
86+
let database = try makeDatabase()
87+
let teams = ["team1", "team2"].map {
88+
ModelData(entity: "Team", id: ObjectID(rawValue: $0), attributes: ["name": .string($0)])
89+
}
90+
try await database.insert(teams)
91+
let person = ModelData(
92+
entity: "Person",
93+
id: "person1",
94+
attributes: ["name": .string("Alice")],
95+
relationships: ["team": .toOne("team2")]
96+
)
97+
try await database.insert(person)
98+
// cache team2 with its member
99+
let cached = try #require(try await database.fetch("Team", for: "team2"))
100+
#expect(cached.relationships["members"] == .toMany(["person1"]))
101+
// claiming the person for team1 also changes team2's derived `members`,
102+
// a row of the written entity other than the one written
103+
var team1 = teams[0]
104+
team1.relationships["members"] = .toMany(["person1"])
105+
try await database.insert(team1)
106+
let fetched = try #require(try await database.fetch("Team", for: "team2"))
107+
#expect(fetched.relationships["members"] == .toMany([]))
108+
}
109+
110+
@Test func deleteRemovesFromCache() async throws {
111+
let database = try makeDatabase()
112+
let person = ModelData(
113+
entity: "Person",
114+
id: "person1",
115+
attributes: ["name": .string("Alice")]
116+
)
117+
try await database.insert(person)
118+
_ = try await database.fetch("Person", for: "person1")
119+
try await database.delete("Person", for: "person1")
120+
#expect(await database.cache["Person"] == nil)
121+
#expect(try await database.fetch("Person", for: "person1") == nil)
122+
}
123+
124+
@Test func deleteNullifiesCachedReferences() async throws {
125+
let database = try makeDatabase()
126+
let team = ModelData(entity: "Team", id: "team1", attributes: ["name": .string("Red")])
127+
let person = ModelData(
128+
entity: "Person",
129+
id: "person1",
130+
attributes: ["name": .string("Alice")],
131+
relationships: ["team": .toOne("team1")]
132+
)
133+
try await database.insert(team)
134+
try await database.insert(person)
135+
// cache the person with its team reference
136+
let cached = try #require(try await database.fetch("Person", for: "person1"))
137+
#expect(cached.relationships["team"] == .toOne("team1"))
138+
// deleting the team nullifies the cached person's foreign key
139+
try await database.delete("Team", for: "team1")
140+
let fetched = try #require(try await database.fetch("Person", for: "person1"))
141+
#expect(fetched.relationships["team"] == .null)
142+
}
143+
}

0 commit comments

Comments
 (0)