@lix-js/sdk 0.6.0-preview.2 → 0.6.0-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/SKILL.md +4 -5
  2. package/dist/engine-wasm/wasm/lix_engine.js +1 -1
  3. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  4. package/dist/generated/builtin-schemas.d.ts +87 -162
  5. package/dist/generated/builtin-schemas.js +139 -236
  6. package/dist/open-lix.d.ts +1 -1
  7. package/dist-engine-src/src/binary_cas/types.rs +0 -6
  8. package/dist-engine-src/src/catalog/context.rs +412 -0
  9. package/dist-engine-src/src/catalog/mod.rs +10 -0
  10. package/dist-engine-src/src/catalog/schema.rs +4 -0
  11. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  12. package/dist-engine-src/src/cel/mod.rs +1 -1
  13. package/dist-engine-src/src/cel/provider.rs +1 -1
  14. package/dist-engine-src/src/commit_graph/context.rs +328 -1015
  15. package/dist-engine-src/src/commit_graph/mod.rs +2 -3
  16. package/dist-engine-src/src/commit_graph/types.rs +7 -43
  17. package/dist-engine-src/src/commit_graph/walker.rs +57 -81
  18. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  19. package/dist-engine-src/src/commit_store/context.rs +944 -0
  20. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  21. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  22. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  23. package/dist-engine-src/src/commit_store/types.rs +215 -0
  24. package/dist-engine-src/src/common/identity.rs +15 -5
  25. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  26. package/dist-engine-src/src/common/metadata.rs +17 -12
  27. package/dist-engine-src/src/common/mod.rs +5 -5
  28. package/dist-engine-src/src/domain.rs +324 -0
  29. package/dist-engine-src/src/engine.rs +29 -43
  30. package/dist-engine-src/src/entity_identity.rs +238 -118
  31. package/dist-engine-src/src/functions/context.rs +17 -52
  32. package/dist-engine-src/src/functions/deterministic.rs +1 -1
  33. package/dist-engine-src/src/functions/mod.rs +1 -1
  34. package/dist-engine-src/src/functions/provider.rs +4 -4
  35. package/dist-engine-src/src/functions/state.rs +39 -66
  36. package/dist-engine-src/src/functions/types.rs +1 -1
  37. package/dist-engine-src/src/init.rs +204 -151
  38. package/dist-engine-src/src/json_store/context.rs +354 -60
  39. package/dist-engine-src/src/json_store/encoded.rs +6 -6
  40. package/dist-engine-src/src/json_store/mod.rs +4 -1
  41. package/dist-engine-src/src/json_store/store.rs +884 -11
  42. package/dist-engine-src/src/json_store/types.rs +166 -1
  43. package/dist-engine-src/src/lib.rs +10 -9
  44. package/dist-engine-src/src/live_state/context.rs +608 -830
  45. package/dist-engine-src/src/live_state/mod.rs +3 -3
  46. package/dist-engine-src/src/live_state/overlay.rs +7 -7
  47. package/dist-engine-src/src/live_state/reader.rs +5 -5
  48. package/dist-engine-src/src/live_state/types.rs +19 -36
  49. package/dist-engine-src/src/live_state/visibility.rs +19 -14
  50. package/dist-engine-src/src/plugin/archive.rs +3 -6
  51. package/dist-engine-src/src/plugin/install.rs +0 -18
  52. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
  53. package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
  54. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
  55. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
  56. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
  57. package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
  58. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
  59. package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
  60. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
  61. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
  62. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
  63. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
  64. package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
  65. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  66. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
  67. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
  68. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
  69. package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
  70. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  71. package/dist-engine-src/src/schema/definition.json +47 -17
  72. package/dist-engine-src/src/schema/definition.rs +202 -96
  73. package/dist-engine-src/src/schema/key.rs +9 -77
  74. package/dist-engine-src/src/schema/mod.rs +4 -4
  75. package/dist-engine-src/src/schema/tests.rs +133 -92
  76. package/dist-engine-src/src/session/context.rs +40 -42
  77. package/dist-engine-src/src/session/create_version.rs +22 -14
  78. package/dist-engine-src/src/session/execute.rs +45 -14
  79. package/dist-engine-src/src/session/merge/apply.rs +4 -4
  80. package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
  81. package/dist-engine-src/src/session/merge/stats.rs +1 -1
  82. package/dist-engine-src/src/session/merge/version.rs +35 -45
  83. package/dist-engine-src/src/session/mod.rs +4 -2
  84. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  85. package/dist-engine-src/src/session/switch_version.rs +16 -28
  86. package/dist-engine-src/src/sql2/change_provider.rs +14 -20
  87. package/dist-engine-src/src/sql2/classify.rs +61 -26
  88. package/dist-engine-src/src/sql2/context.rs +22 -18
  89. package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
  90. package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
  91. package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
  92. package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
  93. package/dist-engine-src/src/sql2/error.rs +21 -1
  94. package/dist-engine-src/src/sql2/execute.rs +325 -264
  95. package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
  96. package/dist-engine-src/src/sql2/file_provider.rs +533 -108
  97. package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
  98. package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
  99. package/dist-engine-src/src/sql2/history_projection.rs +3 -27
  100. package/dist-engine-src/src/sql2/history_provider.rs +11 -17
  101. package/dist-engine-src/src/sql2/history_route.rs +22 -8
  102. package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
  103. package/dist-engine-src/src/sql2/mod.rs +6 -3
  104. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  105. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  106. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  107. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  108. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  109. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  110. package/dist-engine-src/src/sql2/read_only.rs +10 -12
  111. package/dist-engine-src/src/sql2/session.rs +7 -10
  112. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  113. package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
  114. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  115. package/dist-engine-src/src/sql2/version_provider.rs +46 -31
  116. package/dist-engine-src/src/sql2/version_scope.rs +4 -4
  117. package/dist-engine-src/src/storage_bench.rs +1782 -325
  118. package/dist-engine-src/src/test_support.rs +183 -36
  119. package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
  120. package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
  121. package/dist-engine-src/src/tracked_state/context.rs +1155 -271
  122. package/dist-engine-src/src/tracked_state/diff.rs +249 -57
  123. package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
  124. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  125. package/dist-engine-src/src/tracked_state/merge.rs +37 -19
  126. package/dist-engine-src/src/tracked_state/mod.rs +8 -7
  127. package/dist-engine-src/src/tracked_state/storage.rs +138 -6
  128. package/dist-engine-src/src/tracked_state/tree.rs +695 -252
  129. package/dist-engine-src/src/tracked_state/types.rs +176 -6
  130. package/dist-engine-src/src/transaction/commit.rs +695 -435
  131. package/dist-engine-src/src/transaction/context.rs +551 -310
  132. package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
  133. package/dist-engine-src/src/transaction/mod.rs +2 -0
  134. package/dist-engine-src/src/transaction/normalization.rs +311 -447
  135. package/dist-engine-src/src/transaction/prep.rs +37 -0
  136. package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
  137. package/dist-engine-src/src/transaction/staging.rs +701 -406
  138. package/dist-engine-src/src/transaction/types.rs +231 -122
  139. package/dist-engine-src/src/transaction/validation.rs +2717 -1698
  140. package/dist-engine-src/src/untracked_state/codec.rs +40 -96
  141. package/dist-engine-src/src/untracked_state/context.rs +21 -5
  142. package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
  143. package/dist-engine-src/src/untracked_state/mod.rs +3 -5
  144. package/dist-engine-src/src/untracked_state/storage.rs +105 -57
  145. package/dist-engine-src/src/untracked_state/types.rs +63 -13
  146. package/dist-engine-src/src/version/context.rs +1 -13
  147. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  148. package/dist-engine-src/src/version/mod.rs +3 -2
  149. package/dist-engine-src/src/version/refs.rs +12 -103
  150. package/dist-engine-src/src/version/stage_rows.rs +15 -19
  151. package/package.json +1 -1
  152. package/dist-engine-src/src/changelog/codec.rs +0 -321
  153. package/dist-engine-src/src/changelog/context.rs +0 -92
  154. package/dist-engine-src/src/changelog/materialization.rs +0 -121
  155. package/dist-engine-src/src/changelog/mod.rs +0 -13
  156. package/dist-engine-src/src/changelog/reader.rs +0 -20
  157. package/dist-engine-src/src/changelog/storage.rs +0 -220
  158. package/dist-engine-src/src/changelog/types.rs +0 -38
  159. package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
  160. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
  161. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
  162. package/dist-engine-src/src/schema_registry.rs +0 -294
  163. package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
  164. package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
  165. package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
@@ -1,35 +1,33 @@
1
- use std::collections::{BTreeMap, BTreeSet};
1
+ use std::collections::BTreeSet;
2
2
 
3
- use crate::changelog::{
4
- materialize_change, CanonicalChange, ChangelogContext, ChangelogStoreReader,
5
- MaterializedCanonicalChange,
6
- };
7
3
  use crate::commit_graph::walker::{best_common_ancestors, walk_reachable_commits};
8
4
  use crate::commit_graph::{
9
- CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphChangeSet,
10
- CommitGraphChangeSetElement, CommitGraphCommit, CommitGraphEdge, CommitGraphEntity,
11
- CommitGraphReader, ReachableCommitGraphCommit,
5
+ CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphCommit,
6
+ CommitGraphEdge, CommitGraphReader, ReachableCommitGraphCommit,
12
7
  };
8
+ use crate::commit_store::{Change, Commit, CommitStoreContext, CommitStoreReader, LocatedChange};
13
9
  use crate::entity_identity::EntityIdentity;
14
- use crate::json_store::{JsonStoreContext, JsonStoreReader};
15
10
  use crate::storage::StorageReader;
16
11
  use crate::storage::{ScopedStorageReader, StorageReadScope};
17
12
  use crate::LixError;
18
13
 
19
14
  const COMMIT_SCHEMA_KEY: &str = "lix_commit";
20
15
 
21
- /// Read model for resolving changelog commits into entity state at a head.
16
+ /// Read model for resolving commit-store commits into entity state at a head.
22
17
  ///
23
- /// This module does not own durable storage. It reads immutable changelog facts
24
- /// through a caller-provided KV store and applies commit graph rules on top.
18
+ /// This module does not own durable storage. It reads immutable commit-store
19
+ /// facts through a caller-provided KV store and applies commit graph rules on
20
+ /// top.
25
21
  #[derive(Clone)]
26
22
  pub(crate) struct CommitGraphContext {
27
- changelog: ChangelogContext,
23
+ commit_store: CommitStoreContext,
28
24
  }
29
25
 
30
26
  impl CommitGraphContext {
31
- pub(crate) fn new(changelog: ChangelogContext) -> Self {
32
- Self { changelog }
27
+ pub(crate) fn new() -> Self {
28
+ Self {
29
+ commit_store: CommitStoreContext::new(),
30
+ }
33
31
  }
34
32
 
35
33
  /// Creates a graph reader over a caller-provided KV store.
@@ -39,64 +37,43 @@ impl CommitGraphContext {
39
37
  {
40
38
  let read_scope = StorageReadScope::new(store);
41
39
  CommitGraphStoreReader {
42
- changelog_reader: self.changelog.reader(read_scope.store()),
43
- json_reader: JsonStoreContext::new().reader(read_scope.store()),
40
+ commit_store_reader: self.commit_store.reader(read_scope.store()),
44
41
  }
45
42
  }
46
43
  }
47
44
 
48
- /// Commit-graph reader that resolves changelog entities at a commit head.
45
+ /// Commit-graph reader that resolves commit-store entities at a commit head.
49
46
  pub(crate) struct CommitGraphStoreReader<S>
50
47
  where
51
48
  S: StorageReader,
52
49
  {
53
- changelog_reader: ChangelogStoreReader<ScopedStorageReader<S>>,
54
- json_reader: JsonStoreReader<ScopedStorageReader<S>>,
50
+ commit_store_reader: CommitStoreReader<ScopedStorageReader<S>>,
55
51
  }
56
52
 
57
53
  impl<S> CommitGraphStoreReader<S>
58
54
  where
59
55
  S: StorageReader,
60
56
  {
61
- /// Returns the canonical entities that are effective at `head_commit_id`.
62
- ///
63
- /// Reachable commits are visited nearest-first. For each commit, the commit
64
- /// row itself is visible, then introduced/adopted `change_ids` are visited
65
- /// in reverse order so later writes in the same commit win.
66
- pub(crate) async fn entities_at(
67
- &mut self,
68
- head_commit_id: &str,
69
- ) -> Result<Vec<CommitGraphEntity>, LixError> {
70
- let commits = self.reachable_commits(head_commit_id).await?;
71
- self.select_entities(commits).await
72
- }
73
-
74
57
  /// Loads and parses a `lix_commit` canonical change by commit id.
75
58
  pub(crate) async fn load_commit(
76
59
  &mut self,
77
60
  commit_id: &str,
78
61
  ) -> Result<Option<CommitGraphCommit>, LixError> {
79
- let Some(change) = self.find_commit_change(commit_id).await? else {
62
+ let Some(commit) = self.commit_store_reader.load_commit(commit_id).await? else {
80
63
  return Ok(None);
81
64
  };
82
- parse_commit_change(change).map(Some)
65
+ self.graph_commit_from_store_commit(commit).await.map(Some)
83
66
  }
84
67
 
85
- /// Loads every commit fact from the changelog.
68
+ /// Loads every commit fact from the commit store.
86
69
  ///
87
70
  /// This is used by global commit surfaces where the caller wants the durable
88
71
  /// graph facts themselves, not reachability from a particular version head.
89
72
  pub(crate) async fn all_commits(&mut self) -> Result<Vec<CommitGraphCommit>, LixError> {
90
- let changes = self
91
- .changelog_reader
92
- .scan_changes(&crate::changelog::ChangelogScanRequest { limit: None })
93
- .await?;
73
+ let stored_commits = self.commit_store_reader.scan_commits().await?;
94
74
  let mut commits = Vec::new();
95
- for change in changes
96
- .into_iter()
97
- .filter(|change| change.schema_key == COMMIT_SCHEMA_KEY)
98
- {
99
- commits.push(parse_commit_change(self.materialize_change(change).await?)?);
75
+ for commit in stored_commits {
76
+ commits.push(self.graph_commit_from_store_commit(commit).await?);
100
77
  }
101
78
  commits.sort_by(|left, right| left.commit_id.cmp(&right.commit_id));
102
79
  Ok(commits)
@@ -160,48 +137,17 @@ where
160
137
  commits
161
138
  .iter()
162
139
  .flat_map(|commit| {
163
- commit
164
- .parent_commit_ids
165
- .iter()
166
- .map(|parent_commit_id| CommitGraphEdge {
140
+ commit.parent_commit_ids.iter().enumerate().map(
141
+ |(parent_order, parent_commit_id)| CommitGraphEdge {
167
142
  parent_commit_id: parent_commit_id.clone(),
168
143
  child_commit_id: commit.commit_id.clone(),
169
- })
170
- })
171
- .collect()
172
- }
173
-
174
- /// Derives one change-set row for each parsed commit.
175
- pub(crate) fn change_sets(&self, commits: &[CommitGraphCommit]) -> Vec<CommitGraphChangeSet> {
176
- commits
177
- .iter()
178
- .map(|commit| CommitGraphChangeSet {
179
- id: commit.change_set_id.clone(),
180
- commit_id: commit.commit_id.clone(),
144
+ parent_order: parent_order as u32,
145
+ },
146
+ )
181
147
  })
182
148
  .collect()
183
149
  }
184
150
 
185
- /// Loads the canonical changes introduced/adopted by each commit's change set.
186
- pub(crate) async fn change_set_elements(
187
- &mut self,
188
- commits: &[CommitGraphCommit],
189
- ) -> Result<Vec<CommitGraphChangeSetElement>, LixError> {
190
- let mut elements = Vec::new();
191
- for commit in commits {
192
- for change_id in &commit.change_ids {
193
- let change = self
194
- .load_member_change(change_id, &commit.commit_id)
195
- .await?;
196
- elements.push(CommitGraphChangeSetElement {
197
- change_set_id: commit.change_set_id.clone(),
198
- change,
199
- });
200
- }
201
- }
202
- Ok(elements)
203
- }
204
-
205
151
  /// Returns canonical changes reachable from `start_commit_id`.
206
152
  ///
207
153
  /// This is the primitive history API. It reports the commit/depth where
@@ -229,9 +175,9 @@ where
229
175
  let change = self
230
176
  .load_member_canonical_change(&change_id, &commit_id)
231
177
  .await?;
232
- if change_matches_history_request(&change, request) {
178
+ if change_matches_history_request(&change.record, request) {
233
179
  entries.push(CommitGraphChangeHistoryEntry {
234
- change,
180
+ located_change: change,
235
181
  observed_commit_id: commit_id.clone(),
236
182
  start_commit_id: start_commit_id.to_string(),
237
183
  depth: reachable.depth,
@@ -247,10 +193,13 @@ where
247
193
  &mut self,
248
194
  change_id: &str,
249
195
  source_commit_id: &str,
250
- ) -> Result<CanonicalChange, LixError> {
251
- self.changelog_reader
252
- .load_change(change_id)
196
+ ) -> Result<LocatedChange, LixError> {
197
+ let change_ids = vec![change_id.to_string()];
198
+ self.load_canonical_changes(&change_ids)
253
199
  .await?
200
+ .into_iter()
201
+ .next()
202
+ .flatten()
254
203
  .ok_or_else(|| {
255
204
  LixError::new(
256
205
  "LIX_ERROR_UNKNOWN",
@@ -261,95 +210,58 @@ where
261
210
  })
262
211
  }
263
212
 
264
- /// Selects the first reachable change for each canonical entity identity.
265
- async fn select_entities(
213
+ async fn graph_commit_from_store_commit(
266
214
  &mut self,
267
- commits: Vec<ReachableCommitGraphCommit>,
268
- ) -> Result<Vec<CommitGraphEntity>, LixError> {
269
- let mut order = Vec::new();
270
- let mut entities = BTreeMap::new();
271
-
272
- for reachable in commits {
273
- let depth = reachable.depth;
274
- let source_commit_id = reachable.commit.commit_id;
275
-
276
- observe_change(
277
- &mut order,
278
- &mut entities,
279
- reachable.commit.change,
280
- source_commit_id.clone(),
281
- depth,
282
- );
283
-
284
- for change_id in reachable.commit.change_ids.iter().rev() {
285
- let change = self
286
- .load_member_change(change_id, &source_commit_id)
287
- .await?;
288
- observe_change(
289
- &mut order,
290
- &mut entities,
291
- change,
292
- source_commit_id.clone(),
293
- depth,
294
- );
295
- }
215
+ commit: Commit,
216
+ ) -> Result<CommitGraphCommit, LixError> {
217
+ let change_ids = self.load_commit_change_ids(&commit).await?;
218
+ Ok(commit_graph_commit_from_store_commit(commit, change_ids)?)
219
+ }
220
+
221
+ async fn load_commit_change_ids(&self, commit: &Commit) -> Result<Vec<String>, LixError> {
222
+ let mut change_ids = Vec::new();
223
+ for pack_id in 0..commit.change_pack_count {
224
+ let Some(changes) = self
225
+ .commit_store_reader
226
+ .load_change_pack(&commit.id, pack_id)
227
+ .await?
228
+ else {
229
+ return Err(missing_pack_error("change", &commit.id, pack_id));
230
+ };
231
+ change_ids.extend(changes.into_iter().map(|change| change.id));
296
232
  }
297
-
298
- Ok(order
299
- .into_iter()
300
- .filter_map(|identity| {
301
- entities
302
- .remove(&identity)
303
- .map(|accumulator| accumulator.entity)
304
- })
305
- .collect())
306
- }
307
-
308
- async fn load_member_change(
309
- &mut self,
310
- change_id: &str,
311
- source_commit_id: &str,
312
- ) -> Result<MaterializedCanonicalChange, LixError> {
313
- let change = self
314
- .changelog_reader
315
- .load_change(change_id)
316
- .await?
317
- .ok_or_else(|| {
318
- LixError::new(
319
- "LIX_ERROR_UNKNOWN",
320
- format!(
321
- "commit_graph commit '{source_commit_id}' references missing change '{change_id}'"
322
- ),
323
- )
324
- })?;
325
- self.materialize_change(change).await
326
- }
327
-
328
- async fn materialize_change(
329
- &mut self,
330
- change: CanonicalChange,
331
- ) -> Result<MaterializedCanonicalChange, LixError> {
332
- materialize_change(&mut self.json_reader, change).await
233
+ for pack_id in 0..commit.membership_pack_count {
234
+ let Some(members) = self
235
+ .commit_store_reader
236
+ .load_membership_pack(&commit.id, pack_id)
237
+ .await?
238
+ else {
239
+ return Err(missing_pack_error("membership", &commit.id, pack_id));
240
+ };
241
+ change_ids.extend(members.into_iter().map(|locator| locator.change_id));
242
+ }
243
+ Ok(change_ids)
333
244
  }
334
245
 
335
- async fn find_commit_change(
336
- &mut self,
337
- commit_id: &str,
338
- ) -> Result<Option<MaterializedCanonicalChange>, LixError> {
339
- let changes = self
340
- .changelog_reader
341
- .scan_changes(&crate::changelog::ChangelogScanRequest { limit: None })
342
- .await?;
343
- let Some(change) = changes.into_iter().find(|change| {
344
- change.schema_key == COMMIT_SCHEMA_KEY
345
- && change
346
- .entity_id
347
- .as_string()
348
- .is_ok_and(|entity_id| entity_id == commit_id)
349
- }) else {
350
- return Ok(None);
351
- };
352
- self.materialize_change(change).await.map(Some)
246
+ async fn load_canonical_changes(
247
+ &self,
248
+ change_ids: &[String],
249
+ ) -> Result<Vec<Option<LocatedChange>>, LixError> {
250
+ self.commit_store_reader
251
+ .load_located_changes(change_ids)
252
+ .await
253
+ .map(|changes| {
254
+ changes
255
+ .into_iter()
256
+ .map(|located| {
257
+ located.map(|located| LocatedChange {
258
+ record: canonical_change_from_store_change(located.record),
259
+ source_commit_id: located.source_commit_id,
260
+ source_pack_id: located.source_pack_id,
261
+ })
262
+ })
263
+ .collect()
264
+ })
353
265
  }
354
266
  }
355
267
 
@@ -396,17 +308,6 @@ where
396
308
  CommitGraphStoreReader::commit_edges(self, commits)
397
309
  }
398
310
 
399
- fn change_sets(&self, commits: &[CommitGraphCommit]) -> Vec<CommitGraphChangeSet> {
400
- CommitGraphStoreReader::change_sets(self, commits)
401
- }
402
-
403
- async fn change_set_elements(
404
- &mut self,
405
- commits: &[CommitGraphCommit],
406
- ) -> Result<Vec<CommitGraphChangeSetElement>, LixError> {
407
- CommitGraphStoreReader::change_set_elements(self, commits).await
408
- }
409
-
410
311
  async fn change_history_from_commit(
411
312
  &mut self,
412
313
  start_commit_id: &str,
@@ -422,7 +323,7 @@ fn depth_matches(depth: u32, request: &CommitGraphChangeHistoryRequest) -> bool
422
323
  }
423
324
 
424
325
  fn change_matches_history_request(
425
- change: &CanonicalChange,
326
+ change: &Change,
426
327
  request: &CommitGraphChangeHistoryRequest,
427
328
  ) -> bool {
428
329
  (request.include_tombstones || change.snapshot_ref.is_some())
@@ -435,202 +336,70 @@ fn change_matches_history_request(
435
336
  .is_some_and(|file_id| request.file_ids.contains(file_id)))
436
337
  }
437
338
 
438
- fn observe_change(
439
- order: &mut Vec<CanonicalEntityIdentity>,
440
- entities: &mut BTreeMap<CanonicalEntityIdentity, EntityAccumulator>,
441
- change: MaterializedCanonicalChange,
442
- source_commit_id: String,
443
- depth: u32,
444
- ) {
445
- let identity = CanonicalEntityIdentity::from_change(&change);
446
- if let Some(accumulator) = entities.get_mut(&identity) {
447
- // TODO: represent unresolved parent-parent merge conflicts instead of
448
- // collapsing them through deterministic traversal order. A head commit
449
- // change for the same identity should remain the explicit resolution.
450
- accumulator.entity.created_at = change.created_at.clone();
451
- return;
452
- }
453
-
454
- order.push(identity.clone());
455
- entities.insert(
456
- identity,
457
- EntityAccumulator {
458
- entity: CommitGraphEntity {
459
- created_at: change.created_at.clone(),
460
- updated_at: change.created_at.clone(),
461
- change,
462
- source_commit_id,
463
- depth,
464
- },
465
- },
466
- );
467
- }
468
-
469
- #[derive(Debug)]
470
- struct EntityAccumulator {
471
- entity: CommitGraphEntity,
472
- }
473
-
474
- #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
475
- struct CanonicalEntityIdentity {
476
- entity_id: EntityIdentity,
477
- schema_key: String,
478
- file_id: Option<String>,
479
- }
480
-
481
- impl CanonicalEntityIdentity {
482
- fn from_change(change: &MaterializedCanonicalChange) -> Self {
483
- Self {
484
- entity_id: change.entity_id.clone(),
485
- schema_key: change.schema_key.clone(),
486
- file_id: change.file_id.clone(),
487
- }
488
- }
489
- }
490
-
491
- fn parse_commit_change(
492
- change: crate::changelog::MaterializedCanonicalChange,
339
+ fn commit_graph_commit_from_store_commit(
340
+ commit: Commit,
341
+ change_ids: Vec<String>,
493
342
  ) -> Result<CommitGraphCommit, LixError> {
494
- let change_entity_id = change.entity_id.as_string()?;
495
- if change.schema_key != COMMIT_SCHEMA_KEY {
496
- return Err(LixError::new(
497
- "LIX_ERROR_UNKNOWN",
498
- format!(
499
- "commit_graph expected schema_key '{COMMIT_SCHEMA_KEY}' but got '{}'",
500
- change.schema_key
501
- ),
502
- ));
503
- }
504
-
505
- let snapshot_content = change.snapshot_content.as_deref().ok_or_else(|| {
506
- LixError::new(
507
- "LIX_ERROR_UNKNOWN",
508
- format!(
509
- "commit_graph commit '{}' is missing snapshot_content",
510
- change_entity_id
511
- ),
512
- )
513
- })?;
514
- let snapshot =
515
- serde_json::from_str::<serde_json::Value>(snapshot_content).map_err(|error| {
516
- LixError::new(
517
- "LIX_ERROR_UNKNOWN",
518
- format!(
519
- "commit_graph commit '{}' snapshot_content is invalid JSON: {error}",
520
- change_entity_id
521
- ),
522
- )
523
- })?;
524
-
525
- let commit_id = required_string(&snapshot, "id", &change_entity_id)?;
526
- if commit_id != change_entity_id {
527
- return Err(LixError::new(
528
- "LIX_ERROR_UNKNOWN",
529
- format!(
530
- "commit_graph commit change entity_id '{}' does not match snapshot id '{}'",
531
- change_entity_id, commit_id
532
- ),
533
- ));
534
- }
535
-
536
- let change_ids = required_string_array(&snapshot, "change_ids", &change_entity_id)?;
537
- let author_account_ids =
538
- optional_string_array(&snapshot, "author_account_ids", &change_entity_id)?;
539
- let parent_commit_ids =
540
- required_string_array(&snapshot, "parent_commit_ids", &change_entity_id)?;
541
- let change_set_id = required_string(&snapshot, "change_set_id", &change_entity_id)?;
542
-
343
+ let change = commit_header_canonical_change(commit.clone());
543
344
  Ok(CommitGraphCommit {
345
+ canonical_change: change.clone(),
544
346
  change,
545
- commit_id,
546
- change_set_id,
347
+ commit_id: commit.id,
547
348
  change_ids,
548
- author_account_ids,
549
- parent_commit_ids,
349
+ author_account_ids: commit.author_account_ids,
350
+ parent_commit_ids: commit.parent_ids,
550
351
  })
551
352
  }
552
353
 
553
- fn required_string(
554
- snapshot: &serde_json::Value,
555
- field: &str,
556
- commit_id: &str,
557
- ) -> Result<String, LixError> {
558
- snapshot
559
- .get(field)
560
- .and_then(serde_json::Value::as_str)
561
- .filter(|value| !value.is_empty())
562
- .map(str::to_string)
563
- .ok_or_else(|| {
564
- LixError::new(
565
- "LIX_ERROR_UNKNOWN",
566
- format!("commit_graph commit '{commit_id}' requires string field '{field}'"),
567
- )
568
- })
354
+ fn commit_header_canonical_change(commit: Commit) -> Change {
355
+ Change {
356
+ id: commit.change_id,
357
+ entity_id: EntityIdentity::single(&commit.id),
358
+ schema_key: COMMIT_SCHEMA_KEY.to_string(),
359
+ file_id: None,
360
+ snapshot_ref: None,
361
+ metadata_ref: None,
362
+ created_at: commit.created_at,
363
+ }
569
364
  }
570
365
 
571
- fn required_string_array(
572
- snapshot: &serde_json::Value,
573
- field: &str,
574
- commit_id: &str,
575
- ) -> Result<Vec<String>, LixError> {
576
- let values = snapshot
577
- .get(field)
578
- .and_then(serde_json::Value::as_array)
579
- .ok_or_else(|| {
580
- LixError::new(
581
- "LIX_ERROR_UNKNOWN",
582
- format!("commit_graph commit '{commit_id}' requires array field '{field}'"),
583
- )
584
- })?;
585
-
586
- values
587
- .iter()
588
- .map(|value| {
589
- value.as_str().filter(|value| !value.is_empty()).map(str::to_string).ok_or_else(|| {
590
- LixError::new(
591
- "LIX_ERROR_UNKNOWN",
592
- format!(
593
- "commit_graph commit '{commit_id}' field '{field}' must contain only non-empty strings"
594
- ),
595
- )
596
- })
597
- })
598
- .collect()
366
+ fn canonical_change_from_store_change(change: Change) -> Change {
367
+ Change {
368
+ id: change.id,
369
+ entity_id: change.entity_id,
370
+ schema_key: change.schema_key,
371
+ file_id: change.file_id,
372
+ snapshot_ref: change.snapshot_ref,
373
+ metadata_ref: change.metadata_ref,
374
+ created_at: change.created_at,
375
+ }
599
376
  }
600
377
 
601
- fn optional_string_array(
602
- snapshot: &serde_json::Value,
603
- field: &str,
604
- commit_id: &str,
605
- ) -> Result<Vec<String>, LixError> {
606
- match snapshot.get(field) {
607
- Some(_) => required_string_array(snapshot, field, commit_id),
608
- None => Ok(Vec::new()),
609
- }
378
+ fn missing_pack_error(label: &str, commit_id: &str, pack_id: u32) -> LixError {
379
+ LixError::new(
380
+ LixError::CODE_INTERNAL_ERROR,
381
+ format!("commit_graph missing {label} pack ({commit_id}, {pack_id})"),
382
+ )
610
383
  }
611
384
 
612
385
  #[cfg(test)]
613
386
  mod tests {
387
+ use std::collections::{BTreeMap, BTreeSet};
614
388
  use std::sync::Arc;
615
389
 
616
- use serde_json::json;
617
-
618
390
  use crate::backend::testing::UnitTestBackend;
619
- use crate::changelog::{
620
- canonicalize_materialized_change, ChangelogContext, MaterializedCanonicalChange,
621
- };
622
391
  use crate::commit_graph::{CommitGraphChangeHistoryRequest, CommitGraphContext};
623
- use crate::json_store::JsonStoreContext;
392
+ use crate::commit_store::{
393
+ Change, ChangeLocator, ChangeRef, CommitDraftRef, CommitStoreContext,
394
+ };
624
395
  use crate::storage::{StorageContext, StorageWriteSet};
625
396
 
626
397
  #[tokio::test]
627
398
  async fn load_commit_parses_commit_snapshot() {
628
399
  let backend = Arc::new(UnitTestBackend::new());
629
400
  let storage = StorageContext::new(backend.clone());
630
- let changelog = ChangelogContext::new();
631
401
  append_changes(
632
402
  storage.clone(),
633
- &changelog,
634
403
  &[commit_change(
635
404
  "commit-1-change",
636
405
  "commit-1",
@@ -640,7 +409,7 @@ mod tests {
640
409
  )
641
410
  .await;
642
411
 
643
- let graph = CommitGraphContext::new(changelog);
412
+ let graph = CommitGraphContext::new();
644
413
  let mut reader = graph.reader(storage);
645
414
  let commit = reader
646
415
  .load_commit("commit-1")
@@ -649,7 +418,6 @@ mod tests {
649
418
  .expect("commit should exist");
650
419
 
651
420
  assert_eq!(commit.commit_id, "commit-1");
652
- assert_eq!(commit.change_set_id, "change-set-1");
653
421
  assert_eq!(commit.change_ids, vec!["change-1", "change-2"]);
654
422
  assert_eq!(commit.parent_commit_ids, vec!["parent-1"]);
655
423
  assert_eq!(commit.change.id, "commit-1-change");
@@ -659,7 +427,7 @@ mod tests {
659
427
  async fn load_commit_returns_none_for_missing_commit() {
660
428
  let backend = Arc::new(UnitTestBackend::new());
661
429
  let storage = StorageContext::new(backend.clone());
662
- let graph = CommitGraphContext::new(ChangelogContext::new());
430
+ let graph = CommitGraphContext::new();
663
431
  let mut reader = graph.reader(storage);
664
432
 
665
433
  let commit = reader
@@ -670,45 +438,12 @@ mod tests {
670
438
  assert_eq!(commit, None);
671
439
  }
672
440
 
673
- #[tokio::test]
674
- async fn load_commit_rejects_malformed_snapshot() {
675
- let backend = Arc::new(UnitTestBackend::new());
676
- let storage = StorageContext::new(backend.clone());
677
- let changelog = ChangelogContext::new();
678
- append_changes(
679
- storage.clone(),
680
- &changelog,
681
- &[MaterializedCanonicalChange {
682
- id: "commit-1-change".to_string(),
683
- entity_id: crate::entity_identity::EntityIdentity::single("commit-1"),
684
- schema_key: super::COMMIT_SCHEMA_KEY.to_string(),
685
- schema_version: "1".to_string(),
686
- file_id: None,
687
- snapshot_content: Some("{\"id\":\"commit-1\"}".to_string()),
688
- metadata: None,
689
- created_at: "2026-01-01T00:00:00Z".to_string(),
690
- }],
691
- )
692
- .await;
693
-
694
- let graph = CommitGraphContext::new(changelog);
695
- let mut reader = graph.reader(storage);
696
- let error = reader
697
- .load_commit("commit-1")
698
- .await
699
- .expect_err("malformed commit should fail");
700
-
701
- assert!(error.message.contains("change_ids"));
702
- }
703
-
704
441
  #[tokio::test]
705
442
  async fn all_commits_returns_parsed_commits_sorted_by_id() {
706
443
  let backend = Arc::new(UnitTestBackend::new());
707
444
  let storage = StorageContext::new(backend.clone());
708
- let changelog = ChangelogContext::new();
709
445
  append_changes(
710
446
  storage.clone(),
711
- &changelog,
712
447
  &[
713
448
  commit_change("commit-b-change", "commit-b", &[], &[]),
714
449
  entity_change("change-1", "entity-1", "example", "{}"),
@@ -717,7 +452,7 @@ mod tests {
717
452
  )
718
453
  .await;
719
454
 
720
- let graph = CommitGraphContext::new(changelog);
455
+ let graph = CommitGraphContext::new();
721
456
  let mut reader = graph.reader(storage);
722
457
  let commits = reader
723
458
  .all_commits()
@@ -733,53 +468,9 @@ mod tests {
733
468
  );
734
469
  }
735
470
 
736
- #[tokio::test]
737
- async fn entities_at_walks_ancestors_and_computes_nearest_depth() {
738
- let backend = Arc::new(UnitTestBackend::new());
739
- let storage = StorageContext::new(backend.clone());
740
- let changelog = ChangelogContext::new();
741
- append_changes(
742
- storage.clone(),
743
- &changelog,
744
- &[
745
- commit_change("commit-root-change", "commit-root", &[], &[]),
746
- commit_change("commit-left-change", "commit-left", &[], &["commit-root"]),
747
- commit_change("commit-right-change", "commit-right", &[], &["commit-root"]),
748
- commit_change(
749
- "commit-head-change",
750
- "commit-head",
751
- &[],
752
- &["commit-left", "commit-right"],
753
- ),
754
- ],
755
- )
756
- .await;
757
-
758
- let graph = CommitGraphContext::new(changelog);
759
- let mut reader = graph.reader(storage);
760
- let entities = reader
761
- .entities_at("commit-head")
762
- .await
763
- .expect("ancestor traversal should succeed");
764
-
765
- let depths = entities
766
- .into_iter()
767
- .map(|entity| (entity.source_commit_id, entity.depth))
768
- .collect::<Vec<_>>();
769
- assert_eq!(
770
- depths,
771
- vec![
772
- ("commit-head".to_string(), 0),
773
- ("commit-left".to_string(), 1),
774
- ("commit-right".to_string(), 1),
775
- ("commit-root".to_string(), 2),
776
- ]
777
- );
778
- }
779
-
780
471
  #[tokio::test]
781
472
  async fn commit_edges_are_derived_from_parent_commit_ids() {
782
- let graph = CommitGraphContext::new(ChangelogContext::new());
473
+ let graph = CommitGraphContext::new();
783
474
  let reader = graph.reader(StorageContext::new(Arc::new(UnitTestBackend::new())));
784
475
  let commits = vec![parsed_commit(
785
476
  "commit-head",
@@ -794,76 +485,23 @@ mod tests {
794
485
  .iter()
795
486
  .map(|edge| (
796
487
  edge.parent_commit_id.as_str(),
797
- edge.child_commit_id.as_str()
488
+ edge.child_commit_id.as_str(),
489
+ edge.parent_order,
798
490
  ))
799
491
  .collect::<Vec<_>>(),
800
492
  vec![
801
- ("commit-left", "commit-head"),
802
- ("commit-right", "commit-head")
493
+ ("commit-left", "commit-head", 0),
494
+ ("commit-right", "commit-head", 1)
803
495
  ]
804
496
  );
805
497
  }
806
498
 
807
- #[tokio::test]
808
- async fn change_sets_are_derived_from_commits() {
809
- let graph = CommitGraphContext::new(ChangelogContext::new());
810
- let reader = graph.reader(StorageContext::new(Arc::new(UnitTestBackend::new())));
811
- let commits = vec![parsed_commit("commit-1", &[], &[])];
812
-
813
- let change_sets = reader.change_sets(&commits);
814
-
815
- assert_eq!(change_sets.len(), 1);
816
- assert_eq!(change_sets[0].id, "change-set-1");
817
- assert_eq!(change_sets[0].commit_id, "commit-1");
818
- }
819
-
820
- #[tokio::test]
821
- async fn change_set_elements_load_member_changes() {
822
- let backend = Arc::new(UnitTestBackend::new());
823
- let storage = StorageContext::new(backend.clone());
824
- let changelog = ChangelogContext::new();
825
- append_changes(
826
- storage.clone(),
827
- &changelog,
828
- &[
829
- entity_change("change-1", "entity-1", "example", "{}"),
830
- commit_change("commit-1-change", "commit-1", &["change-1"], &[]),
831
- ],
832
- )
833
- .await;
834
-
835
- let graph = CommitGraphContext::new(changelog);
836
- let mut reader = graph.reader(storage);
837
- let commits = reader
838
- .all_commits()
839
- .await
840
- .expect("commit scan should succeed");
841
- let elements = reader
842
- .change_set_elements(&commits)
843
- .await
844
- .expect("change-set elements should load");
845
-
846
- assert_eq!(elements.len(), 1);
847
- assert_eq!(elements[0].change_set_id, "change-set-1");
848
- assert_eq!(elements[0].change.id, "change-1");
849
- assert_eq!(
850
- elements[0]
851
- .change
852
- .entity_id
853
- .as_string()
854
- .expect("entity id should project"),
855
- "entity-1"
856
- );
857
- }
858
-
859
499
  #[tokio::test]
860
500
  async fn change_history_from_commit_reports_matching_canonical_changes_with_depth() {
861
501
  let backend = Arc::new(UnitTestBackend::new());
862
502
  let storage = StorageContext::new(backend.clone());
863
- let changelog = ChangelogContext::new();
864
503
  append_changes(
865
504
  storage.clone(),
866
- &changelog,
867
505
  &[
868
506
  entity_change("change-root", "entity-root", "test_schema", "{}"),
869
507
  entity_change("change-head", "entity-head", "test_schema", "{}"),
@@ -878,7 +516,7 @@ mod tests {
878
516
  )
879
517
  .await;
880
518
 
881
- let graph = CommitGraphContext::new(changelog);
519
+ let graph = CommitGraphContext::new();
882
520
  let mut reader = graph.reader(storage);
883
521
  let history = reader
884
522
  .change_history_from_commit(
@@ -896,7 +534,7 @@ mod tests {
896
534
  history
897
535
  .iter()
898
536
  .map(|entry| (
899
- entry.change.id.as_str(),
537
+ entry.located_change.record.id.as_str(),
900
538
  entry.observed_commit_id.as_str(),
901
539
  entry.start_commit_id.as_str(),
902
540
  entry.depth
@@ -913,10 +551,8 @@ mod tests {
913
551
  async fn change_history_from_commit_filters_depth_entity_file_and_tombstones() {
914
552
  let backend = Arc::new(UnitTestBackend::new());
915
553
  let storage = StorageContext::new(backend.clone());
916
- let changelog = ChangelogContext::new();
917
554
  append_changes(
918
555
  storage.clone(),
919
- &changelog,
920
556
  &[
921
557
  entity_change_with_file(
922
558
  "change-file-a",
@@ -944,7 +580,7 @@ mod tests {
944
580
  )
945
581
  .await;
946
582
 
947
- let graph = CommitGraphContext::new(changelog);
583
+ let graph = CommitGraphContext::new();
948
584
  let mut reader = graph.reader(storage);
949
585
  let history = reader
950
586
  .change_history_from_commit(
@@ -962,7 +598,7 @@ mod tests {
962
598
  .expect("history should resolve");
963
599
 
964
600
  assert_eq!(history.len(), 1);
965
- assert_eq!(history[0].change.id, "change-file-a");
601
+ assert_eq!(history[0].located_change.record.id, "change-file-a");
966
602
  assert_eq!(history[0].depth, 1);
967
603
  }
968
604
 
@@ -970,10 +606,8 @@ mod tests {
970
606
  async fn change_history_from_commit_includes_tombstones_when_requested() {
971
607
  let backend = Arc::new(UnitTestBackend::new());
972
608
  let storage = StorageContext::new(backend.clone());
973
- let changelog = ChangelogContext::new();
974
609
  append_changes(
975
610
  storage.clone(),
976
- &changelog,
977
611
  &[
978
612
  entity_tombstone("change-deleted", "entity-1", "test_schema"),
979
613
  commit_change(
@@ -986,7 +620,7 @@ mod tests {
986
620
  )
987
621
  .await;
988
622
 
989
- let graph = CommitGraphContext::new(changelog);
623
+ let graph = CommitGraphContext::new();
990
624
  let mut reader = graph.reader(storage);
991
625
  let hidden = reader
992
626
  .change_history_from_commit("commit-head", &CommitGraphChangeHistoryRequest::default())
@@ -1005,453 +639,151 @@ mod tests {
1005
639
 
1006
640
  assert!(hidden.is_empty());
1007
641
  assert_eq!(visible.len(), 1);
1008
- assert_eq!(visible[0].change.id, "change-deleted");
1009
- }
1010
-
1011
- #[tokio::test]
1012
- async fn entities_at_selects_nearest_member_change_for_identity() {
1013
- let backend = Arc::new(UnitTestBackend::new());
1014
- let storage = StorageContext::new(backend.clone());
1015
- let changelog = ChangelogContext::new();
1016
- append_changes(
1017
- storage.clone(),
1018
- &changelog,
1019
- &[
1020
- entity_change(
1021
- "change-old",
1022
- "entity-1",
1023
- "test_schema",
1024
- "{\"value\":\"old\"}",
1025
- ),
1026
- entity_change(
1027
- "change-new",
1028
- "entity-1",
1029
- "test_schema",
1030
- "{\"value\":\"new\"}",
1031
- ),
1032
- commit_change("commit-root-change", "commit-root", &["change-old"], &[]),
1033
- commit_change(
1034
- "commit-head-change",
1035
- "commit-head",
1036
- &["change-new"],
1037
- &["commit-root"],
1038
- ),
1039
- ],
1040
- )
1041
- .await;
1042
-
1043
- let graph = CommitGraphContext::new(changelog);
1044
- let mut reader = graph.reader(storage);
1045
- let entities = reader
1046
- .entities_at("commit-head")
1047
- .await
1048
- .expect("entities should resolve");
1049
-
1050
- assert_eq!(
1051
- entity_ids_for_schema(&entities, "test_schema"),
1052
- vec![("change-new".to_string(), "commit-head".to_string(), 0)]
1053
- );
1054
- }
1055
-
1056
- #[tokio::test]
1057
- async fn entities_at_reports_created_at_from_oldest_reachable_change() {
1058
- let backend = Arc::new(UnitTestBackend::new());
1059
- let storage = StorageContext::new(backend.clone());
1060
- let changelog = ChangelogContext::new();
1061
- append_changes(
1062
- storage.clone(),
1063
- &changelog,
1064
- &[
1065
- entity_change_at(
1066
- "change-created",
1067
- "entity-1",
1068
- "test_schema",
1069
- "{\"value\":\"created\"}",
1070
- "2026-01-01T00:00:00Z",
1071
- ),
1072
- entity_change_at(
1073
- "change-updated",
1074
- "entity-1",
1075
- "test_schema",
1076
- "{\"value\":\"updated\"}",
1077
- "2026-01-02T00:00:00Z",
1078
- ),
1079
- commit_change(
1080
- "commit-root-change",
1081
- "commit-root",
1082
- &["change-created"],
1083
- &[],
1084
- ),
1085
- commit_change(
1086
- "commit-head-change",
1087
- "commit-head",
1088
- &["change-updated"],
1089
- &["commit-root"],
1090
- ),
1091
- ],
1092
- )
1093
- .await;
1094
-
1095
- let graph = CommitGraphContext::new(changelog);
1096
- let mut reader = graph.reader(storage);
1097
- let entities = reader
1098
- .entities_at("commit-head")
1099
- .await
1100
- .expect("entities should resolve");
1101
- let entity = entities
1102
- .iter()
1103
- .find(|entity| entity.change.schema_key == "test_schema")
1104
- .expect("test entity should resolve");
1105
-
1106
- assert_eq!(entity.change.id, "change-updated");
1107
- assert_eq!(entity.created_at, "2026-01-01T00:00:00Z");
1108
- assert_eq!(entity.updated_at, "2026-01-02T00:00:00Z");
1109
- }
1110
-
1111
- #[tokio::test]
1112
- async fn entities_at_uses_reverse_change_order_within_commit() {
1113
- let backend = Arc::new(UnitTestBackend::new());
1114
- let storage = StorageContext::new(backend.clone());
1115
- let changelog = ChangelogContext::new();
1116
- append_changes(
1117
- storage.clone(),
1118
- &changelog,
1119
- &[
1120
- entity_change(
1121
- "change-first",
1122
- "entity-1",
1123
- "test_schema",
1124
- "{\"value\":\"first\"}",
1125
- ),
1126
- entity_change(
1127
- "change-last",
1128
- "entity-1",
1129
- "test_schema",
1130
- "{\"value\":\"last\"}",
1131
- ),
1132
- commit_change(
1133
- "commit-head-change",
1134
- "commit-head",
1135
- &["change-first", "change-last"],
1136
- &[],
1137
- ),
1138
- ],
1139
- )
1140
- .await;
1141
-
1142
- let graph = CommitGraphContext::new(changelog);
1143
- let mut reader = graph.reader(storage);
1144
- let entities = reader
1145
- .entities_at("commit-head")
1146
- .await
1147
- .expect("entities should resolve");
1148
-
1149
- assert_eq!(
1150
- entity_ids_for_schema(&entities, "test_schema"),
1151
- vec![("change-last".to_string(), "commit-head".to_string(), 0)]
1152
- );
1153
- }
1154
-
1155
- #[tokio::test]
1156
- async fn entities_at_head_change_overrides_both_merge_parents() {
1157
- let backend = Arc::new(UnitTestBackend::new());
1158
- let storage = StorageContext::new(backend.clone());
1159
- let changelog = ChangelogContext::new();
1160
- append_changes(
1161
- storage.clone(),
1162
- &changelog,
1163
- &[
1164
- entity_change(
1165
- "change-left",
1166
- "entity-1",
1167
- "test_schema",
1168
- "{\"value\":\"left\"}",
1169
- ),
1170
- entity_change(
1171
- "change-right",
1172
- "entity-1",
1173
- "test_schema",
1174
- "{\"value\":\"right\"}",
1175
- ),
1176
- entity_change(
1177
- "change-resolved",
1178
- "entity-1",
1179
- "test_schema",
1180
- "{\"value\":\"resolved\"}",
1181
- ),
1182
- commit_change("commit-left-change", "commit-left", &["change-left"], &[]),
1183
- commit_change(
1184
- "commit-right-change",
1185
- "commit-right",
1186
- &["change-right"],
1187
- &[],
1188
- ),
1189
- commit_change(
1190
- "commit-head-change",
1191
- "commit-head",
1192
- &["change-resolved"],
1193
- &["commit-left", "commit-right"],
1194
- ),
1195
- ],
1196
- )
1197
- .await;
1198
-
1199
- let graph = CommitGraphContext::new(changelog);
1200
- let mut reader = graph.reader(storage);
1201
- let entities = reader
1202
- .entities_at("commit-head")
1203
- .await
1204
- .expect("entities should resolve");
1205
-
1206
- assert_eq!(
1207
- entity_ids_for_schema(&entities, "test_schema"),
1208
- vec![("change-resolved".to_string(), "commit-head".to_string(), 0)]
1209
- );
1210
- }
1211
-
1212
- #[tokio::test]
1213
- async fn entities_at_distinguishes_same_entity_with_different_file_id() {
1214
- let backend = Arc::new(UnitTestBackend::new());
1215
- let storage = StorageContext::new(backend.clone());
1216
- let changelog = ChangelogContext::new();
1217
- append_changes(
1218
- storage.clone(),
1219
- &changelog,
1220
- &[
1221
- entity_change_with_file(
1222
- "change-file-a",
1223
- "entity-1",
1224
- "test_schema",
1225
- Some("file-a"),
1226
- "{\"value\":\"file-a\"}",
1227
- ),
1228
- entity_change_with_file(
1229
- "change-file-b",
1230
- "entity-1",
1231
- "test_schema",
1232
- Some("file-b"),
1233
- "{\"value\":\"file-b\"}",
1234
- ),
1235
- commit_change(
1236
- "commit-head-change",
1237
- "commit-head",
1238
- &["change-file-a", "change-file-b"],
1239
- &[],
1240
- ),
1241
- ],
1242
- )
1243
- .await;
1244
-
1245
- let graph = CommitGraphContext::new(changelog);
1246
- let mut reader = graph.reader(storage);
1247
- let entities = reader
1248
- .entities_at("commit-head")
1249
- .await
1250
- .expect("entities should resolve");
1251
-
1252
- assert_eq!(
1253
- entity_ids_for_schema(&entities, "test_schema"),
1254
- vec![
1255
- ("change-file-b".to_string(), "commit-head".to_string(), 0),
1256
- ("change-file-a".to_string(), "commit-head".to_string(), 0),
1257
- ]
1258
- );
1259
- }
1260
-
1261
- #[tokio::test]
1262
- async fn entities_at_uses_latest_change_for_same_entity_identity() {
1263
- let backend = Arc::new(UnitTestBackend::new());
1264
- let storage = StorageContext::new(backend.clone());
1265
- let changelog = ChangelogContext::new();
1266
- append_changes(
1267
- storage.clone(),
1268
- &changelog,
1269
- &[
1270
- entity_change_with_file(
1271
- "change-entity-a",
1272
- "entity-1",
1273
- "test_schema",
1274
- None,
1275
- "{\"value\":\"a\"}",
1276
- ),
1277
- entity_change_with_file(
1278
- "change-entity-b",
1279
- "entity-1",
1280
- "test_schema",
1281
- None,
1282
- "{\"value\":\"b\"}",
1283
- ),
1284
- commit_change(
1285
- "commit-head-change",
1286
- "commit-head",
1287
- &["change-entity-a", "change-entity-b"],
1288
- &[],
1289
- ),
1290
- ],
1291
- )
1292
- .await;
1293
-
1294
- let graph = CommitGraphContext::new(changelog);
1295
- let mut reader = graph.reader(storage);
1296
- let entities = reader
1297
- .entities_at("commit-head")
1298
- .await
1299
- .expect("entities should resolve");
1300
- let entity = entities
1301
- .iter()
1302
- .find(|entity| entity.change.schema_key == "test_schema")
1303
- .expect("entity should resolve");
1304
-
1305
- assert_eq!(
1306
- entity_ids_for_schema(&entities, "test_schema"),
1307
- vec![("change-entity-b".to_string(), "commit-head".to_string(), 0)]
1308
- );
1309
- assert_eq!(
1310
- entity.change.snapshot_content.as_deref(),
1311
- Some("{\"value\":\"b\"}")
1312
- );
1313
- }
1314
-
1315
- #[tokio::test]
1316
- async fn entities_at_head_tombstone_hides_parent_entity() {
1317
- let backend = Arc::new(UnitTestBackend::new());
1318
- let storage = StorageContext::new(backend.clone());
1319
- let changelog = ChangelogContext::new();
1320
- append_changes(
1321
- storage.clone(),
1322
- &changelog,
1323
- &[
1324
- entity_change(
1325
- "change-created",
1326
- "entity-1",
1327
- "test_schema",
1328
- "{\"value\":\"created\"}",
1329
- ),
1330
- entity_tombstone("change-deleted", "entity-1", "test_schema"),
1331
- commit_change(
1332
- "commit-root-change",
1333
- "commit-root",
1334
- &["change-created"],
1335
- &[],
1336
- ),
1337
- commit_change(
1338
- "commit-head-change",
1339
- "commit-head",
1340
- &["change-deleted"],
1341
- &["commit-root"],
1342
- ),
1343
- ],
1344
- )
1345
- .await;
1346
-
1347
- let graph = CommitGraphContext::new(changelog);
1348
- let mut reader = graph.reader(storage);
1349
- let entities = reader
1350
- .entities_at("commit-head")
1351
- .await
1352
- .expect("entities should resolve");
1353
- let entity = entities
1354
- .iter()
1355
- .find(|entity| entity.change.schema_key == "test_schema")
1356
- .expect("tombstone entity should resolve");
1357
-
1358
- assert_eq!(
1359
- entity_ids_for_schema(&entities, "test_schema"),
1360
- vec![("change-deleted".to_string(), "commit-head".to_string(), 0)]
1361
- );
1362
- assert_eq!(entity.change.snapshot_content, None);
1363
- }
1364
-
1365
- #[tokio::test]
1366
- async fn entities_at_includes_reachable_commit_rows() {
1367
- let backend = Arc::new(UnitTestBackend::new());
1368
- let storage = StorageContext::new(backend.clone());
1369
- let changelog = ChangelogContext::new();
1370
- append_changes(
1371
- storage.clone(),
1372
- &changelog,
1373
- &[
1374
- commit_change("commit-root-change", "commit-root", &[], &[]),
1375
- commit_change("commit-head-change", "commit-head", &[], &["commit-root"]),
1376
- ],
1377
- )
1378
- .await;
1379
-
1380
- let graph = CommitGraphContext::new(changelog);
1381
- let mut reader = graph.reader(storage);
1382
- let entities = reader
1383
- .entities_at("commit-head")
1384
- .await
1385
- .expect("entities should resolve");
1386
-
1387
- assert_eq!(
1388
- entity_ids_for_schema(&entities, super::COMMIT_SCHEMA_KEY),
1389
- vec![
1390
- (
1391
- "commit-head-change".to_string(),
1392
- "commit-head".to_string(),
1393
- 0
1394
- ),
1395
- (
1396
- "commit-root-change".to_string(),
1397
- "commit-root".to_string(),
1398
- 1
1399
- ),
1400
- ]
1401
- );
1402
- }
1403
-
1404
- #[tokio::test]
1405
- async fn entities_at_errors_on_missing_member_change() {
1406
- let backend = Arc::new(UnitTestBackend::new());
1407
- let storage = StorageContext::new(backend.clone());
1408
- let changelog = ChangelogContext::new();
1409
- append_changes(
1410
- storage.clone(),
1411
- &changelog,
1412
- &[commit_change(
1413
- "commit-head-change",
1414
- "commit-head",
1415
- &["missing-change"],
1416
- &[],
1417
- )],
1418
- )
1419
- .await;
642
+ assert_eq!(visible[0].located_change.record.id, "change-deleted");
643
+ }
644
+
645
+ #[derive(Clone)]
646
+ struct TestChange {
647
+ change: Change,
648
+ commit_change_ids: Vec<String>,
649
+ parent_commit_ids: Vec<String>,
650
+ author_account_ids: Vec<String>,
651
+ }
652
+
653
+ impl TestChange {
654
+ fn commit(
655
+ change_id: &str,
656
+ commit_id: &str,
657
+ change_ids: &[&str],
658
+ parent_commit_ids: &[&str],
659
+ ) -> Self {
660
+ Self {
661
+ change: Change {
662
+ id: change_id.to_string(),
663
+ entity_id: crate::entity_identity::EntityIdentity::single(commit_id),
664
+ schema_key: super::COMMIT_SCHEMA_KEY.to_string(),
665
+ file_id: None,
666
+ snapshot_ref: None,
667
+ metadata_ref: None,
668
+ created_at: "2026-01-01T00:00:00Z".to_string(),
669
+ },
670
+ commit_change_ids: change_ids.iter().map(|id| id.to_string()).collect(),
671
+ parent_commit_ids: parent_commit_ids.iter().map(|id| id.to_string()).collect(),
672
+ author_account_ids: Vec::new(),
673
+ }
674
+ }
1420
675
 
1421
- let graph = CommitGraphContext::new(changelog);
1422
- let mut reader = graph.reader(storage);
1423
- let error = reader
1424
- .entities_at("commit-head")
1425
- .await
1426
- .expect_err("missing member change should fail");
676
+ fn entity(
677
+ change_id: &str,
678
+ entity_id: &str,
679
+ schema_key: &str,
680
+ file_id: Option<&str>,
681
+ snapshot_content: Option<&str>,
682
+ created_at: &str,
683
+ ) -> Self {
684
+ Self {
685
+ change: Change {
686
+ id: change_id.to_string(),
687
+ entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
688
+ schema_key: schema_key.to_string(),
689
+ file_id: file_id.map(str::to_string),
690
+ snapshot_ref: snapshot_content.map(|content| {
691
+ crate::json_store::JsonRef::from_hash(blake3::hash(content.as_bytes()))
692
+ }),
693
+ metadata_ref: None,
694
+ created_at: created_at.to_string(),
695
+ },
696
+ commit_change_ids: Vec::new(),
697
+ parent_commit_ids: Vec::new(),
698
+ author_account_ids: Vec::new(),
699
+ }
700
+ }
1427
701
 
1428
- assert!(error.message.contains("missing-change"));
702
+ fn is_commit(&self) -> bool {
703
+ self.change.schema_key == super::COMMIT_SCHEMA_KEY
704
+ }
1429
705
  }
1430
706
 
1431
- async fn append_changes(
1432
- storage: StorageContext,
1433
- changelog: &ChangelogContext,
1434
- changes: &[MaterializedCanonicalChange],
1435
- ) {
707
+ async fn append_changes(storage: StorageContext, changes: &[TestChange]) {
1436
708
  let mut tx = storage
1437
709
  .begin_write_transaction()
1438
710
  .await
1439
711
  .expect("transaction should open");
1440
712
  let mut writes = StorageWriteSet::new();
1441
- let canonical_changes = {
1442
- let mut json_writer = JsonStoreContext::new().writer();
1443
- changes
1444
- .iter()
1445
- .map(|change| {
1446
- canonicalize_materialized_change(&mut writes, &mut json_writer, change)
1447
- })
1448
- .collect::<Result<Vec<_>, _>>()
1449
- .expect("changes should canonicalize")
1450
- };
1451
- changelog
1452
- .writer(&mut writes)
1453
- .stage_changes(&canonical_changes)
1454
- .expect("append should succeed");
713
+ let canonical_changes = changes
714
+ .iter()
715
+ .filter(|change| !change.is_commit())
716
+ .map(|change| change.change.clone())
717
+ .collect::<Vec<_>>();
718
+ let changes_by_id: BTreeMap<&str, &Change> = canonical_changes
719
+ .iter()
720
+ .map(|change| (change.id.as_str(), change))
721
+ .collect::<BTreeMap<_, _>>();
722
+ let mut authored_change_ids = BTreeSet::new();
723
+ let commit_store = CommitStoreContext::new();
724
+ for change in changes.iter().filter(|change| change.is_commit()) {
725
+ let commit = crate::commit_graph::CommitGraphCommit {
726
+ canonical_change: change.change.clone(),
727
+ change: change.change.clone(),
728
+ commit_id: change
729
+ .change
730
+ .entity_id
731
+ .as_single_string()
732
+ .expect("commit fixture should use single entity id")
733
+ .to_string(),
734
+ change_ids: change.commit_change_ids.clone(),
735
+ author_account_ids: change.author_account_ids.clone(),
736
+ parent_commit_ids: change.parent_commit_ids.clone(),
737
+ };
738
+ let parent_commit_ids = commit.parent_commit_ids.clone();
739
+ let author_account_ids = commit.author_account_ids.clone();
740
+ let commit_draft = CommitDraftRef {
741
+ id: &commit.commit_id,
742
+ change_id: &commit.canonical_change.id,
743
+ parent_ids: &parent_commit_ids,
744
+ author_account_ids: &author_account_ids,
745
+ created_at: &commit.canonical_change.created_at,
746
+ };
747
+
748
+ let mut authored_changes = Vec::new();
749
+ let mut adopted_changes = Vec::new();
750
+ let mut corrupt_missing_members = Vec::new();
751
+ for change_id in &commit.change_ids {
752
+ if let Some(change) = changes_by_id.get(change_id.as_str()) {
753
+ if authored_change_ids.insert(change_id.clone()) {
754
+ authored_changes.push(change_ref_from_canonical(change.as_ref()));
755
+ } else {
756
+ adopted_changes.push(change_ref_from_canonical(change.as_ref()));
757
+ }
758
+ } else {
759
+ corrupt_missing_members.push(change_id.clone());
760
+ }
761
+ }
762
+
763
+ if corrupt_missing_members.is_empty() {
764
+ commit_store
765
+ .writer(tx.as_mut(), &mut writes)
766
+ .stage_commit_draft(commit_draft, authored_changes, adopted_changes)
767
+ .await
768
+ .expect("commit-store append should succeed");
769
+ } else {
770
+ crate::commit_store::storage::stage_commit(
771
+ &mut writes,
772
+ commit_draft,
773
+ authored_changes,
774
+ corrupt_missing_members
775
+ .into_iter()
776
+ .map(|change_id| ChangeLocator {
777
+ source_commit_id: "missing-source-commit".to_string(),
778
+ source_pack_id: 0,
779
+ source_ordinal: 0,
780
+ change_id,
781
+ })
782
+ .collect(),
783
+ )
784
+ .expect("corrupt commit-store fixture should stage");
785
+ }
786
+ }
1455
787
  writes
1456
788
  .apply(&mut tx.as_mut())
1457
789
  .await
@@ -1459,30 +791,25 @@ mod tests {
1459
791
  tx.commit().await.expect("commit should succeed");
1460
792
  }
1461
793
 
794
+ fn change_ref_from_canonical<'a>(change: crate::commit_store::ChangeRef<'a>) -> ChangeRef<'a> {
795
+ ChangeRef {
796
+ id: change.id,
797
+ entity_id: change.entity_id,
798
+ schema_key: change.schema_key,
799
+ file_id: change.file_id,
800
+ snapshot_ref: change.snapshot_ref,
801
+ metadata_ref: change.metadata_ref,
802
+ created_at: change.created_at,
803
+ }
804
+ }
805
+
1462
806
  fn commit_change(
1463
807
  change_id: &str,
1464
808
  commit_id: &str,
1465
809
  change_ids: &[&str],
1466
810
  parent_commit_ids: &[&str],
1467
- ) -> MaterializedCanonicalChange {
1468
- MaterializedCanonicalChange {
1469
- id: change_id.to_string(),
1470
- entity_id: crate::entity_identity::EntityIdentity::single(commit_id),
1471
- schema_key: super::COMMIT_SCHEMA_KEY.to_string(),
1472
- schema_version: "1".to_string(),
1473
- file_id: None,
1474
- snapshot_content: Some(
1475
- serde_json::to_string(&json!({
1476
- "id": commit_id,
1477
- "change_set_id": "change-set-1",
1478
- "change_ids": change_ids,
1479
- "parent_commit_ids": parent_commit_ids,
1480
- }))
1481
- .expect("snapshot should serialize"),
1482
- ),
1483
- metadata: None,
1484
- created_at: "2026-01-01T00:00:00Z".to_string(),
1485
- }
811
+ ) -> TestChange {
812
+ TestChange::commit(change_id, commit_id, change_ids, parent_commit_ids)
1486
813
  }
1487
814
 
1488
815
  fn parsed_commit(
@@ -1490,13 +817,26 @@ mod tests {
1490
817
  change_ids: &[&str],
1491
818
  parent_commit_ids: &[&str],
1492
819
  ) -> crate::commit_graph::CommitGraphCommit {
1493
- super::parse_commit_change(commit_change(
820
+ let fixture = commit_change(
1494
821
  &format!("{commit_id}-change"),
1495
822
  commit_id,
1496
823
  change_ids,
1497
824
  parent_commit_ids,
1498
- ))
1499
- .expect("commit helper should parse")
825
+ );
826
+ crate::commit_graph::CommitGraphCommit {
827
+ canonical_change: fixture.change.clone(),
828
+ change: fixture.change,
829
+ commit_id: commit_id.to_string(),
830
+ change_ids: change_ids
831
+ .iter()
832
+ .map(|change_id| change_id.to_string())
833
+ .collect(),
834
+ author_account_ids: Vec::new(),
835
+ parent_commit_ids: parent_commit_ids
836
+ .iter()
837
+ .map(|parent_id| parent_id.to_string())
838
+ .collect(),
839
+ }
1500
840
  }
1501
841
 
1502
842
  fn entity_change(
@@ -1504,7 +844,7 @@ mod tests {
1504
844
  entity_id: &str,
1505
845
  schema_key: &str,
1506
846
  snapshot_content: &str,
1507
- ) -> MaterializedCanonicalChange {
847
+ ) -> TestChange {
1508
848
  entity_change_at(
1509
849
  change_id,
1510
850
  entity_id,
@@ -1520,17 +860,15 @@ mod tests {
1520
860
  schema_key: &str,
1521
861
  snapshot_content: &str,
1522
862
  created_at: &str,
1523
- ) -> MaterializedCanonicalChange {
1524
- MaterializedCanonicalChange {
1525
- id: change_id.to_string(),
1526
- entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1527
- schema_key: schema_key.to_string(),
1528
- schema_version: "1".to_string(),
1529
- file_id: None,
1530
- snapshot_content: Some(snapshot_content.to_string()),
1531
- metadata: None,
1532
- created_at: created_at.to_string(),
1533
- }
863
+ ) -> TestChange {
864
+ TestChange::entity(
865
+ change_id,
866
+ entity_id,
867
+ schema_key,
868
+ None,
869
+ Some(snapshot_content),
870
+ created_at,
871
+ )
1534
872
  }
1535
873
 
1536
874
  fn entity_change_with_file(
@@ -1539,50 +877,25 @@ mod tests {
1539
877
  schema_key: &str,
1540
878
  file_id: Option<&str>,
1541
879
  snapshot_content: &str,
1542
- ) -> MaterializedCanonicalChange {
1543
- MaterializedCanonicalChange {
1544
- id: change_id.to_string(),
1545
- entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1546
- schema_key: schema_key.to_string(),
1547
- schema_version: "1".to_string(),
1548
- file_id: file_id.map(str::to_string),
1549
- snapshot_content: Some(snapshot_content.to_string()),
1550
- metadata: None,
1551
- created_at: "2026-01-01T00:00:00Z".to_string(),
1552
- }
1553
- }
1554
-
1555
- fn entity_tombstone(
1556
- change_id: &str,
1557
- entity_id: &str,
1558
- schema_key: &str,
1559
- ) -> MaterializedCanonicalChange {
1560
- MaterializedCanonicalChange {
1561
- id: change_id.to_string(),
1562
- entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1563
- schema_key: schema_key.to_string(),
1564
- schema_version: "1".to_string(),
1565
- file_id: None,
1566
- snapshot_content: None,
1567
- metadata: None,
1568
- created_at: "2026-01-02T00:00:00Z".to_string(),
1569
- }
880
+ ) -> TestChange {
881
+ TestChange::entity(
882
+ change_id,
883
+ entity_id,
884
+ schema_key,
885
+ file_id,
886
+ Some(snapshot_content),
887
+ "2026-01-01T00:00:00Z",
888
+ )
1570
889
  }
1571
890
 
1572
- fn entity_ids_for_schema(
1573
- entities: &[crate::commit_graph::CommitGraphEntity],
1574
- schema_key: &str,
1575
- ) -> Vec<(String, String, u32)> {
1576
- entities
1577
- .iter()
1578
- .filter(|entity| entity.change.schema_key == schema_key)
1579
- .map(|entity| {
1580
- (
1581
- entity.change.id.clone(),
1582
- entity.source_commit_id.clone(),
1583
- entity.depth,
1584
- )
1585
- })
1586
- .collect()
891
+ fn entity_tombstone(change_id: &str, entity_id: &str, schema_key: &str) -> TestChange {
892
+ TestChange::entity(
893
+ change_id,
894
+ entity_id,
895
+ schema_key,
896
+ None,
897
+ None,
898
+ "2026-01-02T00:00:00Z",
899
+ )
1587
900
  }
1588
901
  }