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

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 (235) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +105 -65
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +30 -6
  5. package/dist/engine-wasm/wasm/lix_engine.js +187 -117
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +14 -8
  8. package/dist/generated/builtin-schemas.d.ts +69 -69
  9. package/dist/generated/builtin-schemas.js +94 -94
  10. package/dist/open-lix.d.ts +42 -28
  11. package/dist/open-lix.js +49 -10
  12. package/dist/sqlite/index.js +86 -30
  13. package/dist-engine-src/README.md +3 -3
  14. package/dist-engine-src/src/backend/capabilities.rs +67 -0
  15. package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
  16. package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
  17. package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
  18. package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
  19. package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
  20. package/dist-engine-src/src/backend/conformance/model.rs +28 -0
  21. package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
  22. package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
  23. package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
  24. package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
  25. package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
  26. package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
  27. package/dist-engine-src/src/backend/conformance/write.rs +16 -0
  28. package/dist-engine-src/src/backend/error.rs +94 -0
  29. package/dist-engine-src/src/backend/in_memory.rs +670 -0
  30. package/dist-engine-src/src/backend/mod.rs +36 -9
  31. package/dist-engine-src/src/backend/predicate.rs +80 -0
  32. package/dist-engine-src/src/backend/traits.rs +260 -0
  33. package/dist-engine-src/src/backend/types.rs +224 -81
  34. package/dist-engine-src/src/binary_cas/context.rs +8 -8
  35. package/dist-engine-src/src/binary_cas/kv.rs +234 -259
  36. package/dist-engine-src/src/{version → branch}/context.rs +12 -12
  37. package/dist-engine-src/src/branch/lifecycle.rs +221 -0
  38. package/dist-engine-src/src/branch/mod.rs +13 -0
  39. package/dist-engine-src/src/branch/refs.rs +321 -0
  40. package/dist-engine-src/src/branch/stage_rows.rs +67 -0
  41. package/dist-engine-src/src/branch/types.rs +21 -0
  42. package/dist-engine-src/src/catalog/context.rs +18 -18
  43. package/dist-engine-src/src/catalog/snapshot.rs +8 -8
  44. package/dist-engine-src/src/changelog/bench_support.rs +785 -0
  45. package/dist-engine-src/src/changelog/change.rs +1 -0
  46. package/dist-engine-src/src/changelog/codec.rs +497 -0
  47. package/dist-engine-src/src/changelog/commit.rs +1 -0
  48. package/dist-engine-src/src/changelog/context.rs +1614 -0
  49. package/dist-engine-src/src/changelog/mod.rs +29 -0
  50. package/dist-engine-src/src/changelog/store.rs +163 -0
  51. package/dist-engine-src/src/changelog/test_support.rs +54 -0
  52. package/dist-engine-src/src/changelog/types.rs +213 -0
  53. package/dist-engine-src/src/commit_graph/context.rs +317 -274
  54. package/dist-engine-src/src/commit_graph/mod.rs +2 -4
  55. package/dist-engine-src/src/commit_graph/types.rs +22 -42
  56. package/dist-engine-src/src/commit_graph/walker.rs +133 -103
  57. package/dist-engine-src/src/common/error.rs +52 -18
  58. package/dist-engine-src/src/common/identity.rs +2 -2
  59. package/dist-engine-src/src/common/mod.rs +1 -1
  60. package/dist-engine-src/src/domain.rs +42 -46
  61. package/dist-engine-src/src/engine.rs +74 -96
  62. package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
  63. package/dist-engine-src/src/functions/context.rs +56 -52
  64. package/dist-engine-src/src/functions/state.rs +51 -52
  65. package/dist-engine-src/src/init.rs +288 -154
  66. package/dist-engine-src/src/json_store/context.rs +15 -266
  67. package/dist-engine-src/src/json_store/mod.rs +26 -0
  68. package/dist-engine-src/src/json_store/store.rs +103 -718
  69. package/dist-engine-src/src/json_store/types.rs +4 -9
  70. package/dist-engine-src/src/lib.rs +49 -19
  71. package/dist-engine-src/src/live_state/context.rs +654 -790
  72. package/dist-engine-src/src/live_state/mod.rs +9 -3
  73. package/dist-engine-src/src/live_state/overlay.rs +4 -4
  74. package/dist-engine-src/src/live_state/types.rs +30 -21
  75. package/dist-engine-src/src/live_state/visibility.rs +514 -71
  76. package/dist-engine-src/src/plugin/install.rs +48 -48
  77. package/dist-engine-src/src/plugin/manifest.rs +7 -7
  78. package/dist-engine-src/src/plugin/materializer.rs +0 -275
  79. package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
  80. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
  81. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
  82. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
  85. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
  86. package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
  87. package/dist-engine-src/src/schema/compatibility.rs +11 -11
  88. package/dist-engine-src/src/schema/definition.json +2 -2
  89. package/dist-engine-src/src/schema/definition.rs +5 -5
  90. package/dist-engine-src/src/schema/key.rs +3 -3
  91. package/dist-engine-src/src/schema/mod.rs +1 -1
  92. package/dist-engine-src/src/schema/tests.rs +18 -18
  93. package/dist-engine-src/src/session/context.rs +819 -124
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +260 -57
  96. package/dist-engine-src/src/session/merge/analysis.rs +9 -3
  97. package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
  98. package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
  99. package/dist-engine-src/src/session/merge/mod.rs +5 -6
  100. package/dist-engine-src/src/session/merge/stats.rs +7 -11
  101. package/dist-engine-src/src/session/mod.rs +19 -16
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +557 -0
  104. package/dist-engine-src/src/sql2/bind/classify.rs +102 -0
  105. package/dist-engine-src/src/sql2/bind/error.rs +5 -0
  106. package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
  107. package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
  108. package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +98 -3
  109. package/dist-engine-src/src/sql2/bind/read.rs +65 -0
  110. package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
  111. package/dist-engine-src/src/sql2/bind/table.rs +273 -0
  112. package/dist-engine-src/src/sql2/bind/write.rs +86 -0
  113. package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
  114. package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
  115. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
  116. package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
  117. package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
  118. package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
  119. package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
  120. package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
  121. package/dist-engine-src/src/sql2/context.rs +36 -30
  122. package/dist-engine-src/src/sql2/error.rs +4 -5
  123. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
  124. package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
  125. package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
  126. package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
  127. package/dist-engine-src/src/sql2/exec/write.rs +661 -0
  128. package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
  129. package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
  130. package/dist-engine-src/src/sql2/history_projection.rs +8 -8
  131. package/dist-engine-src/src/sql2/history_route.rs +35 -31
  132. package/dist-engine-src/src/sql2/mod.rs +30 -24
  133. package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
  134. package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
  135. package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
  136. package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
  137. package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
  138. package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
  139. package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
  140. package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
  141. package/dist-engine-src/src/sql2/plan/write.rs +147 -0
  142. package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
  143. package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
  144. package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
  145. package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
  146. package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
  147. package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
  148. package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
  149. package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
  150. package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
  151. package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
  152. package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
  153. package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
  154. package/dist-engine-src/src/sql2/read_only.rs +2 -2
  155. package/dist-engine-src/src/sql2/session.rs +47 -96
  156. package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
  157. package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
  158. package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
  159. package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
  160. package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
  161. package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
  162. package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
  163. package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
  164. package/dist-engine-src/src/storage/conformance.rs +399 -0
  165. package/dist-engine-src/src/storage/context.rs +552 -288
  166. package/dist-engine-src/src/storage/mod.rs +48 -10
  167. package/dist-engine-src/src/storage/point.rs +440 -0
  168. package/dist-engine-src/src/storage/read_scope.rs +43 -64
  169. package/dist-engine-src/src/storage/reader.rs +867 -0
  170. package/dist-engine-src/src/storage/scan.rs +784 -0
  171. package/dist-engine-src/src/storage/spaces.rs +236 -0
  172. package/dist-engine-src/src/storage/stats.rs +80 -0
  173. package/dist-engine-src/src/storage/write_set.rs +962 -0
  174. package/dist-engine-src/src/storage_bench.rs +136 -4828
  175. package/dist-engine-src/src/test_support.rs +360 -138
  176. package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
  177. package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
  178. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
  179. package/dist-engine-src/src/tracked_state/context.rs +1927 -993
  180. package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
  181. package/dist-engine-src/src/tracked_state/merge.rs +74 -88
  182. package/dist-engine-src/src/tracked_state/mod.rs +19 -16
  183. package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
  184. package/dist-engine-src/src/tracked_state/storage.rs +243 -191
  185. package/dist-engine-src/src/tracked_state/tree.rs +247 -371
  186. package/dist-engine-src/src/tracked_state/types.rs +49 -42
  187. package/dist-engine-src/src/transaction/bench_support.rs +407 -0
  188. package/dist-engine-src/src/transaction/commit.rs +821 -713
  189. package/dist-engine-src/src/transaction/context.rs +705 -600
  190. package/dist-engine-src/src/transaction/mod.rs +13 -2
  191. package/dist-engine-src/src/transaction/normalization.rs +63 -76
  192. package/dist-engine-src/src/transaction/prep.rs +13 -13
  193. package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
  194. package/dist-engine-src/src/transaction/staging.rs +228 -434
  195. package/dist-engine-src/src/transaction/types.rs +41 -98
  196. package/dist-engine-src/src/transaction/validation.rs +382 -446
  197. package/dist-engine-src/src/untracked_state/codec.rs +337 -29
  198. package/dist-engine-src/src/untracked_state/context.rs +7 -7
  199. package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
  200. package/dist-engine-src/src/untracked_state/mod.rs +1 -1
  201. package/dist-engine-src/src/untracked_state/storage.rs +659 -157
  202. package/dist-engine-src/src/untracked_state/types.rs +21 -21
  203. package/package.json +71 -68
  204. package/dist-engine-src/src/backend/kv.rs +0 -358
  205. package/dist-engine-src/src/backend/testing.rs +0 -658
  206. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  207. package/dist-engine-src/src/commit_store/context.rs +0 -944
  208. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  209. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  210. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  211. package/dist-engine-src/src/commit_store/types.rs +0 -215
  212. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  213. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  214. package/dist-engine-src/src/session/create_version.rs +0 -88
  215. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  216. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  217. package/dist-engine-src/src/session/switch_version.rs +0 -109
  218. package/dist-engine-src/src/sql2/classify.rs +0 -182
  219. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  220. package/dist-engine-src/src/sql2/execute.rs +0 -3440
  221. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  222. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  223. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -166
  224. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -25
  225. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  226. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  227. package/dist-engine-src/src/storage/types.rs +0 -501
  228. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  229. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  230. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  231. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  232. package/dist-engine-src/src/version/mod.rs +0 -13
  233. package/dist-engine-src/src/version/refs.rs +0 -330
  234. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  235. package/dist-engine-src/src/version/types.rs +0 -21
@@ -1,79 +1,91 @@
1
1
  use std::collections::BTreeSet;
2
2
 
3
+ use crate::changelog::{
4
+ ChangeLoadRequest, ChangeRecord, ChangelogContext, ChangelogReader, CommitLoadEntry,
5
+ CommitLoadRequest, CommitProjection, CommitRecord, CommitScanRequest,
6
+ };
3
7
  use crate::commit_graph::walker::{best_common_ancestors, walk_reachable_commits};
4
8
  use crate::commit_graph::{
5
- CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphCommit,
6
- CommitGraphEdge, CommitGraphReader, ReachableCommitGraphCommit,
9
+ CommitGraphChange, CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest,
10
+ CommitGraphCommit, CommitGraphEdge, CommitGraphReader, ReachableCommitGraphCommit,
7
11
  };
8
- use crate::commit_store::{Change, Commit, CommitStoreContext, CommitStoreReader, LocatedChange};
9
- use crate::entity_identity::EntityIdentity;
10
- use crate::storage::StorageReader;
11
- use crate::storage::{ScopedStorageReader, StorageReadScope};
12
+ use crate::entity_pk::EntityPk;
13
+ use crate::storage::StorageRead;
12
14
  use crate::LixError;
13
15
 
14
16
  const COMMIT_SCHEMA_KEY: &str = "lix_commit";
15
17
 
16
- /// Read model for resolving commit-store commits into entity state at a head.
18
+ /// Read model for resolving changelog commits into entity state at a head.
17
19
  ///
18
- /// This module does not own durable storage. It reads immutable commit-store
20
+ /// This module does not own durable storage. It reads immutable changelog
19
21
  /// facts through a caller-provided KV store and applies commit graph rules on
20
22
  /// top.
21
23
  #[derive(Clone)]
22
- pub(crate) struct CommitGraphContext {
23
- commit_store: CommitStoreContext,
24
- }
24
+ pub(crate) struct CommitGraphContext;
25
25
 
26
26
  impl CommitGraphContext {
27
27
  pub(crate) fn new() -> Self {
28
- Self {
29
- commit_store: CommitStoreContext::new(),
30
- }
28
+ Self
31
29
  }
32
30
 
33
31
  /// Creates a graph reader over a caller-provided KV store.
34
32
  pub(crate) fn reader<S>(&self, store: S) -> CommitGraphStoreReader<S>
35
33
  where
36
- S: StorageReader,
34
+ S: StorageRead + Send + Sync,
37
35
  {
38
- let read_scope = StorageReadScope::new(store);
39
- CommitGraphStoreReader {
40
- commit_store_reader: self.commit_store.reader(read_scope.store()),
41
- }
36
+ CommitGraphStoreReader { store }
42
37
  }
43
38
  }
44
39
 
45
- /// Commit-graph reader that resolves commit-store entities at a commit head.
40
+ /// Commit-graph reader that resolves changelog entities at a commit head.
46
41
  pub(crate) struct CommitGraphStoreReader<S>
47
42
  where
48
- S: StorageReader,
43
+ S: StorageRead + Send + Sync,
49
44
  {
50
- commit_store_reader: CommitStoreReader<ScopedStorageReader<S>>,
45
+ store: S,
51
46
  }
52
47
 
53
48
  impl<S> CommitGraphStoreReader<S>
54
49
  where
55
- S: StorageReader,
50
+ S: StorageRead + Send + Sync,
56
51
  {
57
52
  /// Loads and parses a `lix_commit` canonical change by commit id.
58
53
  pub(crate) async fn load_commit(
59
54
  &mut self,
60
55
  commit_id: &str,
61
56
  ) -> Result<Option<CommitGraphCommit>, LixError> {
62
- let Some(commit) = self.commit_store_reader.load_commit(commit_id).await? else {
63
- return Ok(None);
64
- };
65
- self.graph_commit_from_store_commit(commit).await.map(Some)
57
+ self.load_changelog_commit(commit_id).await
66
58
  }
67
59
 
68
- /// Loads every commit fact from the commit store.
60
+ /// Loads every direct commit fact from the changelog.
69
61
  ///
70
62
  /// This is used by global commit surfaces where the caller wants the durable
71
- /// graph facts themselves, not reachability from a particular version head.
63
+ /// graph facts themselves, not reachability from a particular branch head.
72
64
  pub(crate) async fn all_commits(&mut self) -> Result<Vec<CommitGraphCommit>, LixError> {
73
- let stored_commits = self.commit_store_reader.scan_commits().await?;
74
65
  let mut commits = Vec::new();
75
- for commit in stored_commits {
76
- commits.push(self.graph_commit_from_store_commit(commit).await?);
66
+ let mut start_after = None::<String>;
67
+ loop {
68
+ let mut reader = ChangelogContext::new().reader(&self.store);
69
+ let scan = reader
70
+ .scan_commits(CommitScanRequest {
71
+ start_after: start_after.as_deref(),
72
+ limit: Some(1024),
73
+ projection: CommitProjection::Record,
74
+ })
75
+ .await?;
76
+ for entry in scan.entries {
77
+ let CommitLoadEntry::Record(record) = entry else {
78
+ return Err(LixError::new(
79
+ LixError::CODE_INTERNAL_ERROR,
80
+ "changelog commit scan returned non-record entry",
81
+ ));
82
+ };
83
+ commits.push(commit_graph_commit_from_commit_record(record, Vec::new()));
84
+ }
85
+ let Some(next) = scan.next_start_after else {
86
+ break;
87
+ };
88
+ start_after = Some(next);
77
89
  }
78
90
  commits.sort_by(|left, right| left.commit_id.cmp(&right.commit_id));
79
91
  Ok(commits)
@@ -150,9 +162,10 @@ where
150
162
 
151
163
  /// Returns canonical changes reachable from `start_commit_id`.
152
164
  ///
153
- /// This is the primitive history API. It reports the commit/depth where
154
- /// each matching canonical change was introduced or adopted during graph
155
- /// traversal and leaves row shaping to callers such as SQL providers.
165
+ /// This is the primitive history API. It reports the commit/depth where a
166
+ /// reachable commit's change-ref set first exposes each matching canonical
167
+ /// change during graph traversal and leaves row shaping to callers such as
168
+ /// SQL providers.
156
169
  pub(crate) async fn change_history_from_commit(
157
170
  &mut self,
158
171
  start_commit_id: &str,
@@ -168,16 +181,26 @@ where
168
181
  }
169
182
 
170
183
  let commit_id = reachable.commit.commit_id;
184
+ let canonical_change = reachable.commit.canonical_change;
185
+ if seen_change_ids.insert(canonical_change.id.clone())
186
+ && change_matches_history_request(&canonical_change, request)
187
+ {
188
+ entries.push(CommitGraphChangeHistoryEntry {
189
+ change: canonical_change,
190
+ observed_commit_id: commit_id.clone(),
191
+ start_commit_id: start_commit_id.to_string(),
192
+ depth: reachable.depth,
193
+ });
194
+ }
195
+
171
196
  for change_id in reachable.commit.change_ids {
172
197
  if !seen_change_ids.insert(change_id.clone()) {
173
198
  continue;
174
199
  }
175
- let change = self
176
- .load_member_canonical_change(&change_id, &commit_id)
177
- .await?;
178
- if change_matches_history_request(&change.record, request) {
200
+ let change = self.load_member_canonical_change(&change_id).await?;
201
+ if change_matches_history_request(&change, request) {
179
202
  entries.push(CommitGraphChangeHistoryEntry {
180
- located_change: change,
203
+ change,
181
204
  observed_commit_id: commit_id.clone(),
182
205
  start_commit_id: start_commit_id.to_string(),
183
206
  depth: reachable.depth,
@@ -192,8 +215,7 @@ where
192
215
  async fn load_member_canonical_change(
193
216
  &mut self,
194
217
  change_id: &str,
195
- source_commit_id: &str,
196
- ) -> Result<LocatedChange, LixError> {
218
+ ) -> Result<CommitGraphChange, LixError> {
197
219
  let change_ids = vec![change_id.to_string()];
198
220
  self.load_canonical_changes(&change_ids)
199
221
  .await?
@@ -203,72 +225,77 @@ where
203
225
  .ok_or_else(|| {
204
226
  LixError::new(
205
227
  "LIX_ERROR_UNKNOWN",
206
- format!(
207
- "commit_graph commit '{source_commit_id}' references missing change '{change_id}'"
208
- ),
228
+ format!("commit_graph references missing change '{change_id}'"),
209
229
  )
210
230
  })
211
231
  }
212
232
 
213
- async fn graph_commit_from_store_commit(
233
+ async fn load_changelog_commit(
214
234
  &mut self,
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));
232
- }
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));
235
+ commit_id: &str,
236
+ ) -> Result<Option<CommitGraphCommit>, LixError> {
237
+ let mut reader = ChangelogContext::new().reader(&self.store);
238
+ let batch = reader
239
+ .load_commits(CommitLoadRequest {
240
+ commit_ids: &[commit_id.to_string()],
241
+ projection: CommitProjection::Full,
242
+ })
243
+ .await?;
244
+ let Some(entry) = batch.entries.into_iter().next().flatten() else {
245
+ return Ok(None);
246
+ };
247
+ match entry {
248
+ CommitLoadEntry::Full {
249
+ record,
250
+ change_ref_chunks,
251
+ } => {
252
+ let change_ids = change_ref_chunks
253
+ .into_iter()
254
+ .flat_map(|chunk| chunk.entries.into_iter().map(|entry| entry.change_id))
255
+ .collect::<Vec<_>>();
256
+ Ok(Some(commit_graph_commit_from_commit_record(
257
+ record, change_ids,
258
+ )))
259
+ }
260
+ _ => Err(LixError::new(
261
+ LixError::CODE_INTERNAL_ERROR,
262
+ "changelog full commit projection returned non-full entry",
263
+ )),
242
264
  }
243
- Ok(change_ids)
244
265
  }
245
266
 
246
267
  async fn load_canonical_changes(
247
268
  &self,
248
269
  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
- })
270
+ ) -> Result<Vec<Option<CommitGraphChange>>, LixError> {
271
+ let mut reader = ChangelogContext::new().reader(&self.store);
272
+ let batch = reader
273
+ .load_changes(ChangeLoadRequest { change_ids })
274
+ .await?;
275
+ batch
276
+ .entries
277
+ .into_iter()
278
+ .map(|entry| Ok(entry.map(commit_graph_change_from_change_record)))
279
+ .collect()
280
+ }
281
+ }
282
+
283
+ fn commit_graph_change_from_change_record(change: ChangeRecord) -> CommitGraphChange {
284
+ CommitGraphChange {
285
+ id: change.change_id,
286
+ entity_pk: change.entity_pk,
287
+ schema_key: change.schema_key,
288
+ file_id: change.file_id,
289
+ snapshot_ref: change.snapshot_ref,
290
+ metadata_ref: change.metadata_ref,
291
+ created_at: change.created_at,
265
292
  }
266
293
  }
267
294
 
268
295
  #[async_trait::async_trait]
269
296
  impl<S> CommitGraphReader for CommitGraphStoreReader<S>
270
297
  where
271
- S: StorageReader,
298
+ S: StorageRead + Send + Sync,
272
299
  {
273
300
  async fn load_commit(
274
301
  &mut self,
@@ -277,10 +304,6 @@ where
277
304
  CommitGraphStoreReader::load_commit(self, commit_id).await
278
305
  }
279
306
 
280
- async fn all_commits(&mut self) -> Result<Vec<CommitGraphCommit>, LixError> {
281
- CommitGraphStoreReader::all_commits(self).await
282
- }
283
-
284
307
  async fn reachable_commits(
285
308
  &mut self,
286
309
  head_commit_id: &str,
@@ -288,26 +311,6 @@ where
288
311
  CommitGraphStoreReader::reachable_commits(self, head_commit_id).await
289
312
  }
290
313
 
291
- async fn best_common_ancestors(
292
- &mut self,
293
- left_commit_id: &str,
294
- right_commit_id: &str,
295
- ) -> Result<Vec<CommitGraphCommit>, LixError> {
296
- CommitGraphStoreReader::best_common_ancestors(self, left_commit_id, right_commit_id).await
297
- }
298
-
299
- async fn merge_base(
300
- &mut self,
301
- left_commit_id: &str,
302
- right_commit_id: &str,
303
- ) -> Result<CommitGraphCommit, LixError> {
304
- CommitGraphStoreReader::merge_base(self, left_commit_id, right_commit_id).await
305
- }
306
-
307
- fn commit_edges(&self, commits: &[CommitGraphCommit]) -> Vec<CommitGraphEdge> {
308
- CommitGraphStoreReader::commit_edges(self, commits)
309
- }
310
-
311
314
  async fn change_history_from_commit(
312
315
  &mut self,
313
316
  start_commit_id: &str,
@@ -323,11 +326,11 @@ fn depth_matches(depth: u32, request: &CommitGraphChangeHistoryRequest) -> bool
323
326
  }
324
327
 
325
328
  fn change_matches_history_request(
326
- change: &Change,
329
+ change: &CommitGraphChange,
327
330
  request: &CommitGraphChangeHistoryRequest,
328
331
  ) -> bool {
329
332
  (request.include_tombstones || change.snapshot_ref.is_some())
330
- && (request.entity_ids.is_empty() || request.entity_ids.contains(&change.entity_id))
333
+ && (request.entity_pks.is_empty() || request.entity_pks.contains(&change.entity_pk))
331
334
  && (request.schema_keys.is_empty() || request.schema_keys.contains(&change.schema_key))
332
335
  && (request.file_ids.is_empty()
333
336
  || change
@@ -336,81 +339,76 @@ fn change_matches_history_request(
336
339
  .is_some_and(|file_id| request.file_ids.contains(file_id)))
337
340
  }
338
341
 
339
- fn commit_graph_commit_from_store_commit(
340
- commit: Commit,
342
+ fn commit_graph_commit_from_commit_record(
343
+ record: CommitRecord,
341
344
  change_ids: Vec<String>,
342
- ) -> Result<CommitGraphCommit, LixError> {
343
- let change = commit_header_canonical_change(commit.clone());
344
- Ok(CommitGraphCommit {
345
+ ) -> CommitGraphCommit {
346
+ let change = commit_record_canonical_change(&record);
347
+ CommitGraphCommit {
345
348
  canonical_change: change.clone(),
346
349
  change,
347
- commit_id: commit.id,
350
+ commit_id: record.commit_id,
348
351
  change_ids,
349
- author_account_ids: commit.author_account_ids,
350
- parent_commit_ids: commit.parent_ids,
351
- })
352
+ author_account_ids: record.author_account_ids,
353
+ parent_commit_ids: record.parent_commit_ids,
354
+ }
352
355
  }
353
356
 
354
- fn commit_header_canonical_change(commit: Commit) -> Change {
355
- Change {
356
- id: commit.change_id,
357
- entity_id: EntityIdentity::single(&commit.id),
357
+ fn commit_record_canonical_change(record: &CommitRecord) -> CommitGraphChange {
358
+ let snapshot_content = serde_json::to_string(&serde_json::json!({
359
+ "id": record.commit_id,
360
+ }))
361
+ .expect("lix_commit snapshot serialization should not fail");
362
+ CommitGraphChange {
363
+ id: record.change_id.clone(),
364
+ entity_pk: EntityPk::single(&record.commit_id),
358
365
  schema_key: COMMIT_SCHEMA_KEY.to_string(),
359
366
  file_id: None,
360
- snapshot_ref: None,
367
+ snapshot_ref: Some(crate::json_store::JsonRef::for_content(
368
+ snapshot_content.as_bytes(),
369
+ )),
361
370
  metadata_ref: None,
362
- created_at: commit.created_at,
363
- }
364
- }
365
-
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,
371
+ created_at: record.created_at.clone(),
375
372
  }
376
373
  }
377
374
 
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
- )
383
- }
384
-
385
375
  #[cfg(test)]
386
376
  mod tests {
387
377
  use std::collections::{BTreeMap, BTreeSet};
388
- use std::sync::Arc;
389
378
 
390
- use crate::backend::testing::UnitTestBackend;
391
- use crate::commit_graph::{CommitGraphChangeHistoryRequest, CommitGraphContext};
392
- use crate::commit_store::{
393
- Change, ChangeLocator, ChangeRef, CommitDraftRef, CommitStoreContext,
379
+ use crate::changelog::{
380
+ ChangeRecord, ChangelogAppend, ChangelogContext, ChangelogWriter, CommitChangeRef,
381
+ CommitChangeRefSet, CommitRecord,
394
382
  };
395
- use crate::storage::{StorageContext, StorageWriteSet};
383
+ use crate::commit_graph::{
384
+ CommitGraphChange, CommitGraphChangeHistoryRequest, CommitGraphContext,
385
+ };
386
+ use crate::storage::StorageContext;
387
+ use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
396
388
 
397
389
  #[tokio::test]
398
390
  async fn load_commit_parses_commit_snapshot() {
399
- let backend = Arc::new(UnitTestBackend::new());
400
- let storage = StorageContext::new(backend.clone());
391
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
401
392
  append_changes(
402
- storage.clone(),
403
- &[commit_change(
404
- "commit-1-change",
405
- "commit-1",
406
- &["change-1", "change-2"],
407
- &["parent-1"],
408
- )],
393
+ &storage,
394
+ &[
395
+ entity_change("change-1", "entity-1", "example", "{}"),
396
+ entity_change("change-2", "entity-2", "example", "{}"),
397
+ commit_change(
398
+ "commit-1-change",
399
+ "commit-1",
400
+ &["change-1", "change-2"],
401
+ &["parent-1"],
402
+ ),
403
+ ],
409
404
  )
410
405
  .await;
411
406
 
412
407
  let graph = CommitGraphContext::new();
413
- let mut reader = graph.reader(storage);
408
+ let read = storage
409
+ .begin_read(StorageReadOptions::default())
410
+ .expect("read should open");
411
+ let mut reader = graph.reader(read);
414
412
  let commit = reader
415
413
  .load_commit("commit-1")
416
414
  .await
@@ -425,10 +423,12 @@ mod tests {
425
423
 
426
424
  #[tokio::test]
427
425
  async fn load_commit_returns_none_for_missing_commit() {
428
- let backend = Arc::new(UnitTestBackend::new());
429
- let storage = StorageContext::new(backend.clone());
426
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
430
427
  let graph = CommitGraphContext::new();
431
- let mut reader = graph.reader(storage);
428
+ let read = storage
429
+ .begin_read(StorageReadOptions::default())
430
+ .expect("read should open");
431
+ let mut reader = graph.reader(read);
432
432
 
433
433
  let commit = reader
434
434
  .load_commit("missing")
@@ -440,10 +440,9 @@ mod tests {
440
440
 
441
441
  #[tokio::test]
442
442
  async fn all_commits_returns_parsed_commits_sorted_by_id() {
443
- let backend = Arc::new(UnitTestBackend::new());
444
- let storage = StorageContext::new(backend.clone());
443
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
445
444
  append_changes(
446
- storage.clone(),
445
+ &storage,
447
446
  &[
448
447
  commit_change("commit-b-change", "commit-b", &[], &[]),
449
448
  entity_change("change-1", "entity-1", "example", "{}"),
@@ -453,7 +452,10 @@ mod tests {
453
452
  .await;
454
453
 
455
454
  let graph = CommitGraphContext::new();
456
- let mut reader = graph.reader(storage);
455
+ let read = storage
456
+ .begin_read(StorageReadOptions::default())
457
+ .expect("read should open");
458
+ let mut reader = graph.reader(read);
457
459
  let commits = reader
458
460
  .all_commits()
459
461
  .await
@@ -471,7 +473,11 @@ mod tests {
471
473
  #[tokio::test]
472
474
  async fn commit_edges_are_derived_from_parent_commit_ids() {
473
475
  let graph = CommitGraphContext::new();
474
- let reader = graph.reader(StorageContext::new(Arc::new(UnitTestBackend::new())));
476
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
477
+ let read = storage
478
+ .begin_read(StorageReadOptions::default())
479
+ .expect("read should open");
480
+ let reader = graph.reader(read);
475
481
  let commits = vec![parsed_commit(
476
482
  "commit-head",
477
483
  &[],
@@ -498,10 +504,9 @@ mod tests {
498
504
 
499
505
  #[tokio::test]
500
506
  async fn change_history_from_commit_reports_matching_canonical_changes_with_depth() {
501
- let backend = Arc::new(UnitTestBackend::new());
502
- let storage = StorageContext::new(backend.clone());
507
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
503
508
  append_changes(
504
- storage.clone(),
509
+ &storage,
505
510
  &[
506
511
  entity_change("change-root", "entity-root", "test_schema", "{}"),
507
512
  entity_change("change-head", "entity-head", "test_schema", "{}"),
@@ -517,7 +522,10 @@ mod tests {
517
522
  .await;
518
523
 
519
524
  let graph = CommitGraphContext::new();
520
- let mut reader = graph.reader(storage);
525
+ let read = storage
526
+ .begin_read(StorageReadOptions::default())
527
+ .expect("read should open");
528
+ let mut reader = graph.reader(read);
521
529
  let history = reader
522
530
  .change_history_from_commit(
523
531
  "commit-head",
@@ -534,7 +542,7 @@ mod tests {
534
542
  history
535
543
  .iter()
536
544
  .map(|entry| (
537
- entry.located_change.record.id.as_str(),
545
+ entry.change.id.as_str(),
538
546
  entry.observed_commit_id.as_str(),
539
547
  entry.start_commit_id.as_str(),
540
548
  entry.depth
@@ -549,10 +557,9 @@ mod tests {
549
557
 
550
558
  #[tokio::test]
551
559
  async fn change_history_from_commit_filters_depth_entity_file_and_tombstones() {
552
- let backend = Arc::new(UnitTestBackend::new());
553
- let storage = StorageContext::new(backend.clone());
560
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
554
561
  append_changes(
555
- storage.clone(),
562
+ &storage,
556
563
  &[
557
564
  entity_change_with_file(
558
565
  "change-file-a",
@@ -581,12 +588,15 @@ mod tests {
581
588
  .await;
582
589
 
583
590
  let graph = CommitGraphContext::new();
584
- let mut reader = graph.reader(storage);
591
+ let read = storage
592
+ .begin_read(StorageReadOptions::default())
593
+ .expect("read should open");
594
+ let mut reader = graph.reader(read);
585
595
  let history = reader
586
596
  .change_history_from_commit(
587
597
  "commit-head",
588
598
  &CommitGraphChangeHistoryRequest {
589
- entity_ids: vec![crate::entity_identity::EntityIdentity::single("entity-1")],
599
+ entity_pks: vec![crate::entity_pk::EntityPk::single("entity-1")],
590
600
  file_ids: vec!["file-a".to_string()],
591
601
  min_depth: Some(1),
592
602
  max_depth: Some(1),
@@ -598,16 +608,15 @@ mod tests {
598
608
  .expect("history should resolve");
599
609
 
600
610
  assert_eq!(history.len(), 1);
601
- assert_eq!(history[0].located_change.record.id, "change-file-a");
611
+ assert_eq!(history[0].change.id, "change-file-a");
602
612
  assert_eq!(history[0].depth, 1);
603
613
  }
604
614
 
605
615
  #[tokio::test]
606
616
  async fn change_history_from_commit_includes_tombstones_when_requested() {
607
- let backend = Arc::new(UnitTestBackend::new());
608
- let storage = StorageContext::new(backend.clone());
617
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
609
618
  append_changes(
610
- storage.clone(),
619
+ &storage,
611
620
  &[
612
621
  entity_tombstone("change-deleted", "entity-1", "test_schema"),
613
622
  commit_change(
@@ -621,15 +630,25 @@ mod tests {
621
630
  .await;
622
631
 
623
632
  let graph = CommitGraphContext::new();
624
- let mut reader = graph.reader(storage);
633
+ let read = storage
634
+ .begin_read(StorageReadOptions::default())
635
+ .expect("read should open");
636
+ let mut reader = graph.reader(read);
625
637
  let hidden = reader
626
- .change_history_from_commit("commit-head", &CommitGraphChangeHistoryRequest::default())
638
+ .change_history_from_commit(
639
+ "commit-head",
640
+ &CommitGraphChangeHistoryRequest {
641
+ schema_keys: vec!["test_schema".to_string()],
642
+ ..CommitGraphChangeHistoryRequest::default()
643
+ },
644
+ )
627
645
  .await
628
646
  .expect("history should resolve");
629
647
  let visible = reader
630
648
  .change_history_from_commit(
631
649
  "commit-head",
632
650
  &CommitGraphChangeHistoryRequest {
651
+ schema_keys: vec!["test_schema".to_string()],
633
652
  include_tombstones: true,
634
653
  ..CommitGraphChangeHistoryRequest::default()
635
654
  },
@@ -639,12 +658,12 @@ mod tests {
639
658
 
640
659
  assert!(hidden.is_empty());
641
660
  assert_eq!(visible.len(), 1);
642
- assert_eq!(visible[0].located_change.record.id, "change-deleted");
661
+ assert_eq!(visible[0].change.id, "change-deleted");
643
662
  }
644
663
 
645
664
  #[derive(Clone)]
646
665
  struct TestChange {
647
- change: Change,
666
+ change: CommitGraphChange,
648
667
  commit_change_ids: Vec<String>,
649
668
  parent_commit_ids: Vec<String>,
650
669
  author_account_ids: Vec<String>,
@@ -658,9 +677,9 @@ mod tests {
658
677
  parent_commit_ids: &[&str],
659
678
  ) -> Self {
660
679
  Self {
661
- change: Change {
680
+ change: CommitGraphChange {
662
681
  id: change_id.to_string(),
663
- entity_id: crate::entity_identity::EntityIdentity::single(commit_id),
682
+ entity_pk: crate::entity_pk::EntityPk::single(commit_id),
664
683
  schema_key: super::COMMIT_SCHEMA_KEY.to_string(),
665
684
  file_id: None,
666
685
  snapshot_ref: None,
@@ -675,16 +694,16 @@ mod tests {
675
694
 
676
695
  fn entity(
677
696
  change_id: &str,
678
- entity_id: &str,
697
+ entity_pk: &str,
679
698
  schema_key: &str,
680
699
  file_id: Option<&str>,
681
700
  snapshot_content: Option<&str>,
682
701
  created_at: &str,
683
702
  ) -> Self {
684
703
  Self {
685
- change: Change {
704
+ change: CommitGraphChange {
686
705
  id: change_id.to_string(),
687
- entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
706
+ entity_pk: crate::entity_pk::EntityPk::single(entity_pk),
688
707
  schema_key: schema_key.to_string(),
689
708
  file_id: file_id.map(str::to_string),
690
709
  snapshot_ref: snapshot_content.map(|content| {
@@ -704,102 +723,126 @@ mod tests {
704
723
  }
705
724
  }
706
725
 
707
- async fn append_changes(storage: StorageContext, changes: &[TestChange]) {
708
- let mut tx = storage
709
- .begin_write_transaction()
710
- .await
711
- .expect("transaction should open");
712
- let mut writes = StorageWriteSet::new();
726
+ async fn append_changes(storage: &StorageContext, changes: &[TestChange]) {
727
+ let mut read = storage
728
+ .begin_read(StorageReadOptions::default())
729
+ .expect("read should open");
730
+ let mut writes = storage.new_write_set();
713
731
  let canonical_changes = changes
714
732
  .iter()
715
733
  .filter(|change| !change.is_commit())
716
- .map(|change| change.change.clone())
734
+ .cloned()
717
735
  .collect::<Vec<_>>();
718
- let changes_by_id: BTreeMap<&str, &Change> = canonical_changes
736
+ let changes_by_id: BTreeMap<&str, &TestChange> = canonical_changes
719
737
  .iter()
720
- .map(|change| (change.id.as_str(), change))
738
+ .map(|change| (change.change.id.as_str(), change))
721
739
  .collect::<BTreeMap<_, _>>();
722
740
  let mut authored_change_ids = BTreeSet::new();
723
- let commit_store = CommitStoreContext::new();
741
+ let provided_commit_ids = changes
742
+ .iter()
743
+ .filter(|change| change.is_commit())
744
+ .map(|change| {
745
+ change
746
+ .change
747
+ .entity_pk
748
+ .as_single_string()
749
+ .expect("commit fixture should use single entity pk")
750
+ .to_string()
751
+ })
752
+ .collect::<BTreeSet<_>>();
753
+ let mut staged_commit_ids = BTreeSet::new();
754
+ let changelog = ChangelogContext::new();
755
+ let mut writer = changelog.writer(&mut read, &mut writes);
756
+ let mut append = ChangelogAppend::default();
724
757
  for change in changes.iter().filter(|change| change.is_commit()) {
725
758
  let commit = crate::commit_graph::CommitGraphCommit {
726
759
  canonical_change: change.change.clone(),
727
760
  change: change.change.clone(),
728
761
  commit_id: change
729
762
  .change
730
- .entity_id
763
+ .entity_pk
731
764
  .as_single_string()
732
- .expect("commit fixture should use single entity id")
765
+ .expect("commit fixture should use single entity pk")
733
766
  .to_string(),
734
767
  change_ids: change.commit_change_ids.clone(),
735
768
  author_account_ids: change.author_account_ids.clone(),
736
769
  parent_commit_ids: change.parent_commit_ids.clone(),
737
770
  };
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();
771
+ for parent_commit_id in &commit.parent_commit_ids {
772
+ if !provided_commit_ids.contains(parent_commit_id)
773
+ && staged_commit_ids.insert(parent_commit_id.clone())
774
+ {
775
+ append_empty_commit(&mut append, parent_commit_id);
776
+ }
777
+ }
778
+ let mut refs = Vec::new();
751
779
  for change_id in &commit.change_ids {
752
780
  if let Some(change) = changes_by_id.get(change_id.as_str()) {
753
781
  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()));
782
+ append.changes.push(change_record_from_test_change(change));
757
783
  }
758
- } else {
759
- corrupt_missing_members.push(change_id.clone());
784
+ refs.push(commit_change_ref_from_test_change(change));
760
785
  }
761
786
  }
762
787
 
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
- }
788
+ append.commits.push(CommitRecord {
789
+ format_version: 1,
790
+ commit_id: commit.commit_id.clone(),
791
+ parent_commit_ids: commit.parent_commit_ids.clone(),
792
+ change_id: commit.canonical_change.id.clone(),
793
+ author_account_ids: commit.author_account_ids.clone(),
794
+ created_at: commit.canonical_change.created_at.clone(),
795
+ });
796
+ append.commit_change_refs.push(CommitChangeRefSet {
797
+ commit_id: commit.commit_id.clone(),
798
+ entries: refs,
799
+ });
800
+ staged_commit_ids.insert(commit.commit_id.clone());
786
801
  }
787
- writes
788
- .apply(&mut tx.as_mut())
802
+ writer
803
+ .stage_append(append)
789
804
  .await
790
- .expect("writes should apply");
791
- tx.commit().await.expect("commit should succeed");
792
- }
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,
805
+ .expect("changelog append should stage");
806
+ storage
807
+ .commit_write_set(writes, StorageWriteOptions::default())
808
+ .expect("commit should succeed");
809
+ }
810
+
811
+ fn append_empty_commit(append: &mut ChangelogAppend, commit_id: &str) {
812
+ let change_id = format!("{commit_id}-change");
813
+ append.commits.push(CommitRecord {
814
+ format_version: 1,
815
+ commit_id: commit_id.to_string(),
816
+ parent_commit_ids: Vec::new(),
817
+ change_id: change_id.clone(),
818
+ author_account_ids: Vec::new(),
819
+ created_at: "2026-01-01T00:00:00Z".to_string(),
820
+ });
821
+ append.commit_change_refs.push(CommitChangeRefSet {
822
+ commit_id: commit_id.to_string(),
823
+ entries: Vec::new(),
824
+ });
825
+ }
826
+
827
+ fn change_record_from_test_change(change: &TestChange) -> ChangeRecord {
828
+ ChangeRecord {
829
+ format_version: 1,
830
+ change_id: change.change.id.clone(),
831
+ entity_pk: change.change.entity_pk.clone(),
832
+ schema_key: change.change.schema_key.clone(),
833
+ file_id: change.change.file_id.clone(),
834
+ snapshot_ref: change.change.snapshot_ref,
835
+ metadata_ref: change.change.metadata_ref,
836
+ created_at: change.change.created_at.clone(),
837
+ }
838
+ }
839
+
840
+ fn commit_change_ref_from_test_change(change: &TestChange) -> CommitChangeRef {
841
+ CommitChangeRef {
842
+ schema_key: change.change.schema_key.clone(),
843
+ file_id: change.change.file_id.clone(),
844
+ entity_pk: change.change.entity_pk.clone(),
845
+ change_id: change.change.id.clone(),
803
846
  }
804
847
  }
805
848
 
@@ -841,13 +884,13 @@ mod tests {
841
884
 
842
885
  fn entity_change(
843
886
  change_id: &str,
844
- entity_id: &str,
887
+ entity_pk: &str,
845
888
  schema_key: &str,
846
889
  snapshot_content: &str,
847
890
  ) -> TestChange {
848
891
  entity_change_at(
849
892
  change_id,
850
- entity_id,
893
+ entity_pk,
851
894
  schema_key,
852
895
  snapshot_content,
853
896
  "2026-01-01T00:00:00Z",
@@ -856,14 +899,14 @@ mod tests {
856
899
 
857
900
  fn entity_change_at(
858
901
  change_id: &str,
859
- entity_id: &str,
902
+ entity_pk: &str,
860
903
  schema_key: &str,
861
904
  snapshot_content: &str,
862
905
  created_at: &str,
863
906
  ) -> TestChange {
864
907
  TestChange::entity(
865
908
  change_id,
866
- entity_id,
909
+ entity_pk,
867
910
  schema_key,
868
911
  None,
869
912
  Some(snapshot_content),
@@ -873,14 +916,14 @@ mod tests {
873
916
 
874
917
  fn entity_change_with_file(
875
918
  change_id: &str,
876
- entity_id: &str,
919
+ entity_pk: &str,
877
920
  schema_key: &str,
878
921
  file_id: Option<&str>,
879
922
  snapshot_content: &str,
880
923
  ) -> TestChange {
881
924
  TestChange::entity(
882
925
  change_id,
883
- entity_id,
926
+ entity_pk,
884
927
  schema_key,
885
928
  file_id,
886
929
  Some(snapshot_content),
@@ -888,10 +931,10 @@ mod tests {
888
931
  )
889
932
  }
890
933
 
891
- fn entity_tombstone(change_id: &str, entity_id: &str, schema_key: &str) -> TestChange {
934
+ fn entity_tombstone(change_id: &str, entity_pk: &str, schema_key: &str) -> TestChange {
892
935
  TestChange::entity(
893
936
  change_id,
894
- entity_id,
937
+ entity_pk,
895
938
  schema_key,
896
939
  None,
897
940
  None,