Skip to content

Commit c8330f7

Browse files
committed
Add symmetric relationship and offset tests
Cover the symmetric self-relationship join paths (fetch/delete/membership UNION), a missing inverse relationship, and a fetch offset with no limit.
1 parent b796963 commit c8330f7

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//
2+
// RelationshipBranchTests.swift
3+
// CoreModel-SQLite
4+
//
5+
// Covers the symmetric (self-inverse) many-to-many relationship paths — the
6+
// `UNION` halves of the join-table fetch, delete, and membership subquery — plus
7+
// a missing inverse relationship and a fetch offset with no limit.
8+
//
9+
10+
import Foundation
11+
import Testing
12+
import CoreModel
13+
import SQLite
14+
@testable import CoreModelSQLite
15+
16+
/// A single entity with a symmetric self-relationship (`friends` is its own inverse),
17+
/// which resolves to a symmetric join table.
18+
private var symmetricModel: Model {
19+
Model(entities: [
20+
EntityDescription(
21+
id: "Node",
22+
attributes: [.init(id: "name", type: .string)],
23+
relationships: [
24+
.init(id: "friends", type: .toMany, destinationEntity: "Node", inverseRelationship: "friends")
25+
]
26+
)
27+
])
28+
}
29+
30+
@Test func symmetricManyToManyRelationship() async throws {
31+
let database = try SQLiteDatabase(path: temporaryDatabasePath(named: "Symmetric"), model: symmetricModel)
32+
let nodes = ["a", "b", "c"].map {
33+
ModelData(entity: "Node", id: ObjectID(rawValue: $0), attributes: ["name": .string($0)])
34+
}
35+
try await database.insert(nodes)
36+
37+
// a befriends b and c; symmetry means b and c befriend a in return.
38+
var a = nodes[0]
39+
a.relationships["friends"] = .toMany(["b", "c"])
40+
try await database.insert(a)
41+
42+
// Reading back exercises the symmetric UNION in JoinTable.fetch.
43+
let fetchedA = try #require(try await database.fetch("Node", for: "a"))
44+
#expect(fetchedA.relationships["friends"] == .toMany(["b", "c"]))
45+
let fetchedB = try #require(try await database.fetch("Node", for: "b"))
46+
#expect(fetchedB.relationships["friends"] == .toMany(["a"]))
47+
48+
// Membership predicate exercises the symmetric UNION in the query subquery:
49+
// which nodes count `a` among their friends? -> b and c.
50+
let request = FetchRequest(
51+
entity: "Node",
52+
predicate: .comparison(.init(left: .keyPath("friends"), right: .relationship(.toOne("a")), type: .contains))
53+
)
54+
let ids = try await database.fetchID(request).sorted { $0.rawValue < $1.rawValue }
55+
#expect(ids == ["b", "c"])
56+
57+
// Re-writing the set exercises the symmetric branch of removeAll.
58+
a.relationships["friends"] = .toMany(["b"])
59+
try await database.insert(a)
60+
let updatedC = try #require(try await database.fetch("Node", for: "c"))
61+
#expect(updatedC.relationships["friends"] == .toMany([]))
62+
}
63+
64+
/// A to-many relationship whose declared inverse doesn't exist on the destination.
65+
private var missingInverseModel: Model {
66+
Model(entities: [
67+
EntityDescription(
68+
id: "A",
69+
attributes: [.init(id: "name", type: .string)],
70+
relationships: [
71+
.init(id: "items", type: .toMany, destinationEntity: "B", inverseRelationship: "missing")
72+
]
73+
),
74+
EntityDescription(id: "B", attributes: [.init(id: "name", type: .string)], relationships: [])
75+
])
76+
}
77+
78+
@Test func missingInverseRelationshipThrows() async throws {
79+
let path = temporaryDatabasePath(named: "MissingInverse")
80+
// Building the schema resolves the to-many relationship's inverse type, which fails
81+
// because the destination entity has no relationship named "missing".
82+
await #expect(throws: CoreModelError.self) {
83+
let database = try SQLiteDatabase(path: path, model: missingInverseModel)
84+
try await database.insert(ModelData(entity: "B", id: "b1", attributes: ["name": .string("B")]))
85+
}
86+
}
87+
88+
@Test func fetchOffsetWithoutLimit() async throws {
89+
let database = try makeDatabase()
90+
let people = (0..<5).map { index in
91+
ModelData(
92+
entity: "Person",
93+
id: ObjectID(rawValue: "p\(index)"),
94+
attributes: ["name": .string("P\(index)"), "age": .int32(Int32(index))]
95+
)
96+
}
97+
try await database.insert(people)
98+
// Offset with no limit -> `LIMIT -1 OFFSET n`.
99+
let request = FetchRequest(
100+
entity: "Person",
101+
sortDescriptors: [.init(property: "age", ascending: true)],
102+
fetchOffset: 2
103+
)
104+
#expect(try await database.fetchID(request) == ["p2", "p3", "p4"])
105+
}

0 commit comments

Comments
 (0)