@@ -18,6 +18,19 @@ public actor SQLiteDatabase {
1818
1919 internal let connection : SQLite . Connection
2020
21+ /// Fetched objects by entity and ID, mirroring CoreData's row cache: a repeated
22+ /// `fetch(_:for:)` returns the cached value without touching SQLite, skipping both
23+ /// the row query and the per-relationship queries that derive to-many values.
24+ /// Predicate fetches also register their results, so ID lookups following a list
25+ /// fetch are free.
26+ ///
27+ /// Writes drop the cached objects of every entity whose stored or derived values
28+ /// they can change (see `invalidateCache(for:)`). The cache assumes this
29+ /// actor is the database's only writer — the same coherency contract as separate
30+ /// CoreData stacks on one store file. `SQLiteViewContext` reads its own connection
31+ /// directly and always observes committed state.
32+ internal var cache = [ EntityName: [ ObjectID: ModelData] ] ( )
33+
2134 /// Creates the schema eagerly, synchronously, as part of initialization — not lazily
2235 /// on first use. A `SQLiteViewContext` opens its own read-only connection to the same
2336 /// file and, being read-only, can never create the schema itself; without this, a
@@ -45,12 +58,24 @@ extension SQLiteDatabase: ModelStorage {
4558
4659 public func fetch( _ entity: EntityName , for id: ObjectID ) async throws -> ModelData ? {
4760 try await asyncYield ( )
48- return try connection. fetch ( entity, for: id, model: model)
61+ if let cached = cache [ entity] ? [ id] {
62+ return cached
63+ }
64+ let value = try connection. fetch ( entity, for: id, model: model)
65+ if let value {
66+ cache [ entity, default: [ : ] ] [ id] = value
67+ }
68+ return value
4969 }
5070
5171 public func fetch( _ fetchRequest: FetchRequest ) async throws -> [ ModelData ] {
5272 try await asyncYield ( )
53- return try connection. fetch ( fetchRequest, model: model)
73+ let values = try connection. fetch ( fetchRequest, model: model)
74+ // register results so subsequent ID lookups are cache hits
75+ for value in values {
76+ cache [ fetchRequest. entity, default: [ : ] ] [ value. id] = value
77+ }
78+ return values
5479 }
5580
5681 public func fetchID( _ fetchRequest: FetchRequest ) async throws -> [ ObjectID ] {
@@ -66,21 +91,25 @@ extension SQLiteDatabase: ModelStorage {
6691 public func insert( _ value: ModelData ) async throws {
6792 try await asyncYield ( )
6893 try connection. insert ( value, model: model)
94+ invalidateCache ( for: [ value. entity] )
6995 }
7096
7197 public func insert( _ values: [ ModelData ] ) async throws {
7298 try await asyncYield ( )
7399 try connection. insert ( values, model: model)
100+ invalidateCache ( for: Set ( values. lazy. map { $0. entity } ) )
74101 }
75102
76103 public func delete( _ entity: EntityName , for id: ObjectID ) async throws {
77104 try await asyncYield ( )
78105 try connection. delete ( entity, for: id, model: model)
106+ invalidateCache ( for: [ entity] )
79107 }
80108
81109 public func delete( _ entity: EntityName , for ids: [ ObjectID ] ) async throws {
82110 try await asyncYield ( )
83111 try connection. delete ( entity, for: ids, model: model)
112+ invalidateCache ( for: [ entity] )
84113 }
85114}
86115
@@ -90,6 +119,28 @@ internal extension SQLiteDatabase {
90119 await Task . yield ( )
91120 try Task . checkCancellation ( )
92121 }
122+
123+ /// Drop cached objects of every entity a write to the given entities can affect.
124+ ///
125+ /// A write to entity E touches E's own table, the foreign key columns of E's
126+ /// relationship destinations, and their shared join tables. Derived to-many values
127+ /// are computed only from those same foreign keys and join tables, so the affected
128+ /// set is exactly E plus E's destination entities — including E-typed rows other
129+ /// than the one written (e.g. reassigning a person to a new team also changes the
130+ /// old team's `members`), which is why whole entity caches are dropped rather than
131+ /// single objects.
132+ func invalidateCache( for entities: Set < EntityName > ) {
133+ var affected = entities
134+ for entity in entities {
135+ guard let description = model. entities. first ( where: { $0. id == entity } ) else { continue }
136+ for relationship in description. relationships {
137+ affected. insert ( relationship. destinationEntity)
138+ }
139+ }
140+ for entity in affected {
141+ cache [ entity] = nil
142+ }
143+ }
93144}
94145
95146internal extension Connection {
0 commit comments