@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,37 +1,39 @@
1
1
  use crate::binary_cas::BinaryCasContext;
2
- use crate::commit_store::{ChangeRef, CommitDraftRef, CommitStoreContext, StagedCommitStoreCommit};
2
+ use crate::branch::{BranchContext, BranchRefReader};
3
+ use crate::changelog::{
4
+ ChangeRecord, ChangelogAppend, ChangelogContext, ChangelogWriter, CommitChangeRef,
5
+ CommitChangeRefSet, CommitRecord,
6
+ };
7
+ use crate::entity_pk::EntityPk;
3
8
  use crate::functions::FunctionContext;
4
- use crate::json_store::{JsonStoreContext, JsonWritePlacementRef, NormalizedJsonRef};
5
- use crate::storage::{StorageReader, StorageWriteSet, StorageWriteTransaction};
9
+ use crate::json_store::{JsonRef, JsonStoreContext, JsonWritePlacementRef, NormalizedJsonRef};
10
+ use crate::storage::{StorageRead, StorageWriteSet};
6
11
  use crate::tracked_state::{TrackedStateContext, TrackedStateDeltaRef};
7
- use crate::transaction::prepare_version_ref_row;
12
+ use crate::transaction::prepare_branch_ref_row;
8
13
  use crate::transaction::staging::PreparedWriteSet;
9
- use crate::transaction::types::{PreparedAdoptedStateRow, PreparedStateRow, StagedCommitMembers};
14
+ use crate::transaction::types::{PreparedStateRow, StagedCommitChangeRef, StagedCommitChangeRefs};
10
15
  use crate::untracked_state::{
11
16
  UntrackedStateContext, UntrackedStateIdentity, UntrackedStateIdentityRef, UntrackedStateRowRef,
12
17
  };
13
- use crate::version::{VersionContext, VersionRefReader};
14
18
  use crate::LixError;
15
- use std::collections::BTreeMap;
19
+ use std::collections::{BTreeMap, BTreeSet};
16
20
 
17
21
  type RowIndex = usize;
18
- type AdoptedRowIndex = usize;
19
22
 
20
23
  /// Commits prepared transaction rows into durable tracked and untracked stores.
21
24
  ///
22
25
  /// Providers decode DataFusion DML into hydrated `PreparedStateRow`s. Untracked
23
- /// rows are durable local overlay state and bypass commit-store rows. Tracked
24
- /// rows stage canonical commit-store facts, then update the live-state serving
25
- /// projection. The tracked side of that projection is a prolly root keyed by
26
+ /// rows are durable local overlay state and bypass changelog change refs. Tracked
27
+ /// rows stage canonical changelog facts, then update the live-state serving
28
+ /// commit root. The tracked side of that commit root is a prolly root keyed by
26
29
  /// the new commit id.
27
30
  pub(crate) async fn commit_prepared_writes(
28
31
  binary_cas: &BinaryCasContext,
29
- commit_store: &CommitStoreContext,
30
- version_ctx: &VersionContext,
32
+ branch_ctx: &BranchContext,
31
33
  runtime_functions: Option<&FunctionContext>,
32
- transaction: &mut (impl StorageWriteTransaction + ?Sized),
34
+ read: &mut (impl StorageRead + Send + Sync),
33
35
  prepared_writes: PreparedWriteSet,
34
- ) -> Result<(), LixError> {
36
+ ) -> Result<StorageWriteSet, LixError> {
35
37
  let mut writes = StorageWriteSet::new();
36
38
  let mut json_writer = JsonStoreContext::new().writer();
37
39
 
@@ -43,19 +45,17 @@ pub(crate) async fn commit_prepared_writes(
43
45
  }
44
46
 
45
47
  let state_rows = prepared_writes.state_rows;
46
- let adopted_rows = prepared_writes.adopted_rows;
47
48
  let finalized = finalize_commit_rows(
48
- prepared_writes.commit_members_by_version,
49
- prepared_writes.extra_commit_parents_by_version,
50
- version_ctx,
51
- transaction,
49
+ prepared_writes.commit_change_refs_by_branch,
50
+ prepared_writes.extra_commit_parents_by_branch,
51
+ branch_ctx,
52
+ &*read,
52
53
  )
53
54
  .await?;
54
55
  let commit_rows = finalized.commit_rows;
55
- let version_heads = finalized.version_heads;
56
+ let branch_heads = finalized.branch_heads;
56
57
  let tracked_roots = finalized.tracked_roots;
57
58
  let row_index = index_prepared_rows(&state_rows)?;
58
- let adopted_index = index_adopted_rows(&adopted_rows);
59
59
 
60
60
  if let Some(runtime_functions) = runtime_functions {
61
61
  runtime_functions
@@ -64,50 +64,39 @@ pub(crate) async fn commit_prepared_writes(
64
64
  }
65
65
 
66
66
  if state_rows.is_empty()
67
- && adopted_rows.is_empty()
68
67
  && commit_rows.is_empty()
69
- && version_heads.is_empty()
68
+ && branch_heads.is_empty()
70
69
  && writes.is_empty()
71
70
  {
72
- return Ok(());
71
+ return Ok(writes);
73
72
  }
74
73
 
75
- let staged_commits = stage_commit_store_commits(
76
- commit_store,
77
- transaction,
74
+ let staged_commits = stage_changelog_commits(
75
+ read,
78
76
  &mut writes,
79
77
  &state_rows,
80
78
  &row_index.tracked_row_indices_by_commit,
81
- &adopted_rows,
82
- &adopted_index.tracked_row_indices_by_commit,
83
79
  &commit_rows,
84
80
  )
85
81
  .await?;
86
82
 
87
- let json_pack_indexes_by_commit = stage_prepared_json_payloads(
83
+ stage_state_json_payloads(
88
84
  &mut json_writer,
89
85
  &mut writes,
90
86
  &state_rows,
91
- &row_index.tracked_row_indices_by_commit,
92
- &staged_commits,
93
- &row_index.untracked_row_indices,
87
+ &row_index.canonical_row_indices,
94
88
  )?;
95
89
 
96
- // The serving projection is updated in the same backend transaction as the
97
- // commit-store append. Tracked rows become prolly mutations under their owning
90
+ // The serving commit root is updated in the same backend transaction as the
91
+ // changelog append. Tracked rows become prolly mutations under their owning
98
92
  // commit root; untracked rows remain in the separate local overlay store.
99
93
  {
100
94
  let untracked_overlay_delete_identities = existing_untracked_overlay_delete_identities(
101
- transaction,
95
+ &*read,
102
96
  row_index
103
97
  .canonical_row_indices
104
98
  .iter()
105
- .map(|&row_index| untracked_identity_ref_from_state_row(&state_rows[row_index]))
106
- .chain(
107
- adopted_rows
108
- .iter()
109
- .map(untracked_identity_ref_from_adopted_row),
110
- ),
99
+ .map(|&row_index| untracked_identity_ref_from_state_row(&state_rows[row_index])),
111
100
  )
112
101
  .await?;
113
102
  UntrackedStateContext::new()
@@ -124,89 +113,44 @@ pub(crate) async fn commit_prepared_writes(
124
113
  untracked_overlay_delete_identities
125
114
  .iter()
126
115
  .map(UntrackedStateIdentity::as_ref),
127
- );
116
+ )?;
128
117
  stage_tracked_roots(
129
- transaction,
118
+ read,
130
119
  &mut writes,
131
120
  &state_rows,
132
121
  row_index.tracked_row_indices_by_commit,
133
- &adopted_rows,
134
- adopted_index.tracked_row_indices_by_commit,
135
122
  tracked_roots,
136
123
  staged_commits,
137
- json_pack_indexes_by_commit,
138
124
  )
139
125
  .await?;
140
126
  }
141
127
 
142
- for version_head in version_heads {
143
- let canonical_row = prepare_version_ref_row(
144
- &version_head.version_id,
145
- &version_head.commit_id,
146
- &version_head.timestamp,
128
+ for branch_head in branch_heads {
129
+ let canonical_row = prepare_branch_ref_row(
130
+ &branch_head.branch_id,
131
+ &branch_head.commit_id,
132
+ &branch_head.timestamp,
147
133
  )?;
148
- version_ctx.stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
134
+ branch_ctx.stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
149
135
  }
150
136
 
151
- writes.apply(transaction).await?;
152
- Ok(())
137
+ Ok(writes)
153
138
  }
154
139
 
155
- fn stage_prepared_json_payloads(
140
+ fn stage_state_json_payloads(
156
141
  json_writer: &mut crate::json_store::JsonStoreWriter,
157
142
  writes: &mut StorageWriteSet,
158
143
  state_rows: &[PreparedStateRow],
159
- tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
160
- staged_commits: &BTreeMap<String, StagedCommitStoreCommit>,
161
- untracked_row_indices: &[RowIndex],
162
- ) -> Result<BTreeMap<String, BTreeMap<u32, std::collections::HashMap<[u8; 32], usize>>>, LixError> {
163
- let mut pack_indexes_by_commit = BTreeMap::new();
164
- for (commit_id, row_indices) in tracked_row_indices_by_commit {
165
- let staged_commit = staged_commits.get(commit_id).ok_or_else(|| {
166
- LixError::new(
167
- LixError::CODE_INTERNAL_ERROR,
168
- format!("commit '{commit_id}' has tracked JSON rows but no staged commit-store locators"),
169
- )
170
- })?;
171
- if row_indices.len() != staged_commit.authored_locators.len() {
172
- return Err(LixError::new(
173
- LixError::CODE_INTERNAL_ERROR,
174
- format!(
175
- "commit '{commit_id}' has {} tracked JSON rows but {} authored locators",
176
- row_indices.len(),
177
- staged_commit.authored_locators.len()
178
- ),
179
- ));
180
- }
181
- let mut row_indices_by_pack = BTreeMap::<u32, Vec<RowIndex>>::new();
182
- for (&row_index, locator) in row_indices.iter().zip(&staged_commit.authored_locators) {
183
- row_indices_by_pack
184
- .entry(locator.source_pack_id)
185
- .or_default()
186
- .push(row_index);
187
- }
188
- for (pack_id, pack_row_indices) in row_indices_by_pack {
189
- let report = json_writer.stage_batch_report(
190
- writes,
191
- JsonWritePlacementRef::CommitPack { commit_id, pack_id },
192
- pack_row_indices
193
- .iter()
194
- .flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
195
- )?;
196
- pack_indexes_by_commit
197
- .entry(commit_id.clone())
198
- .or_insert_with(BTreeMap::new)
199
- .insert(pack_id, report.pack_indexes);
200
- }
201
- }
144
+ row_indices: &[RowIndex],
145
+ ) -> Result<(), LixError> {
202
146
  json_writer.stage_batch(
203
147
  writes,
204
148
  JsonWritePlacementRef::OutOfBand,
205
- untracked_row_indices
149
+ row_indices
206
150
  .iter()
207
151
  .flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
208
152
  )?;
209
- Ok(pack_indexes_by_commit)
153
+ Ok(())
210
154
  }
211
155
 
212
156
  fn json_payloads_from_state_row(
@@ -219,11 +163,11 @@ fn json_payloads_from_state_row(
219
163
  }
220
164
 
221
165
  async fn existing_untracked_overlay_delete_identities<'a>(
222
- transaction: &mut (impl StorageReader + ?Sized),
166
+ read: &(impl StorageRead + Send + Sync + ?Sized),
223
167
  identities: impl IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
224
168
  ) -> Result<Vec<UntrackedStateIdentity>, LixError> {
225
169
  UntrackedStateContext::new()
226
- .reader(transaction)
170
+ .reader(read)
227
171
  .existing_identities(identities)
228
172
  .await
229
173
  }
@@ -234,10 +178,6 @@ struct PreparedRowIndex {
234
178
  tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
235
179
  }
236
180
 
237
- struct PreparedAdoptedRowIndex {
238
- tracked_row_indices_by_commit: BTreeMap<String, Vec<AdoptedRowIndex>>,
239
- }
240
-
241
181
  fn index_prepared_rows(rows: &[PreparedStateRow]) -> Result<PreparedRowIndex, LixError> {
242
182
  let mut canonical_row_indices = Vec::new();
243
183
  let mut untracked_row_indices = Vec::new();
@@ -268,242 +208,277 @@ fn index_prepared_rows(rows: &[PreparedStateRow]) -> Result<PreparedRowIndex, Li
268
208
  })
269
209
  }
270
210
 
271
- fn index_adopted_rows(rows: &[PreparedAdoptedStateRow]) -> PreparedAdoptedRowIndex {
272
- let mut tracked_row_indices_by_commit = BTreeMap::<String, Vec<AdoptedRowIndex>>::new();
273
- for (row_index, row) in rows.iter().enumerate() {
274
- tracked_row_indices_by_commit
275
- .entry(row.commit_id.clone())
276
- .or_default()
277
- .push(row_index);
278
- }
279
- PreparedAdoptedRowIndex {
280
- tracked_row_indices_by_commit,
281
- }
211
+ #[derive(Clone, Debug)]
212
+ struct StagedChangelogCommit {
213
+ change_ids: Vec<String>,
214
+ selected_change_refs: Vec<StagedCommitChangeRef>,
215
+ commit_change_id: String,
216
+ commit_created_at: String,
282
217
  }
283
218
 
284
- async fn stage_commit_store_commits(
285
- commit_store: &CommitStoreContext,
286
- transaction: &mut (impl StorageReader + ?Sized),
219
+ async fn stage_changelog_commits(
220
+ read: &mut (impl StorageRead + Send + Sync),
287
221
  writes: &mut StorageWriteSet,
288
222
  state_rows: &[PreparedStateRow],
289
223
  tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
290
- adopted_rows: &[PreparedAdoptedStateRow],
291
- adopted_row_indices_by_commit: &BTreeMap<String, Vec<AdoptedRowIndex>>,
292
224
  commit_rows: &[FinalizedCommitRow],
293
- ) -> Result<BTreeMap<String, StagedCommitStoreCommit>, LixError> {
225
+ ) -> Result<BTreeMap<String, StagedChangelogCommit>, LixError> {
226
+ if commit_rows.is_empty() {
227
+ return Ok(BTreeMap::new());
228
+ }
229
+
294
230
  let mut commits = Vec::with_capacity(commit_rows.len());
295
- let mut commit_ids = Vec::with_capacity(commit_rows.len());
231
+ let mut changes = Vec::new();
232
+ let mut commit_change_refs = Vec::with_capacity(commit_rows.len());
233
+ let mut staged = BTreeMap::<String, StagedChangelogCommit>::new();
296
234
  for commit_row in commit_rows {
297
235
  let state_row_indices = tracked_row_indices_by_commit
298
236
  .get(&commit_row.commit_id)
299
237
  .map(Vec::as_slice)
300
238
  .unwrap_or_default();
301
- let adopted_row_indices = adopted_row_indices_by_commit
302
- .get(&commit_row.commit_id)
303
- .map(Vec::as_slice)
304
- .unwrap_or_default();
305
- let mut authored_changes = Vec::with_capacity(state_row_indices.len());
239
+ let mut refs = Vec::with_capacity(state_row_indices.len());
240
+ let mut change_ids =
241
+ Vec::with_capacity(state_row_indices.len() + commit_row.selected_change_refs.len());
306
242
  for &row_index in state_row_indices {
307
- authored_changes.push(change_ref_from_state_row(&state_rows[row_index])?);
243
+ let row = &state_rows[row_index];
244
+ let change_id = row.change_id.as_ref().ok_or_else(|| {
245
+ LixError::new(
246
+ LixError::CODE_INTERNAL_ERROR,
247
+ "tracked staged row is missing change_id before changelog append",
248
+ )
249
+ })?;
250
+ refs.push(commit_change_ref_from_state_row(row, change_id));
251
+ change_ids.push(change_id.clone());
252
+ changes.push(change_record_from_state_row(row)?);
308
253
  }
309
- let mut adopted_changes = Vec::with_capacity(adopted_row_indices.len());
310
- for &row_index in adopted_row_indices {
311
- adopted_changes.push(change_ref_from_adopted_row(&adopted_rows[row_index]));
254
+ for change_ref in &commit_row.selected_change_refs {
255
+ refs.push(commit_change_ref_from_selected_change_ref(change_ref));
256
+ change_ids.push(change_ref.change_id.clone());
312
257
  }
313
-
314
- let commit = CommitDraftRef {
315
- id: &commit_row.commit_id,
316
- change_id: &commit_row.change_id,
317
- parent_ids: &commit_row.parent_commit_ids,
318
- author_account_ids: &[],
319
- created_at: &commit_row.created_at,
320
- };
321
- commit_ids.push(commit_row.commit_id.clone());
322
- commits.push((commit, authored_changes, adopted_changes));
258
+ commits.push(CommitRecord {
259
+ format_version: 1,
260
+ commit_id: commit_row.commit_id.clone(),
261
+ parent_commit_ids: commit_row.parent_commit_ids.clone(),
262
+ change_id: commit_row.change_id.clone(),
263
+ author_account_ids: Vec::new(),
264
+ created_at: commit_row.created_at.clone(),
265
+ });
266
+ commit_change_refs.push(CommitChangeRefSet {
267
+ commit_id: commit_row.commit_id.clone(),
268
+ entries: refs,
269
+ });
270
+ staged.insert(
271
+ commit_row.commit_id.clone(),
272
+ StagedChangelogCommit {
273
+ change_ids,
274
+ selected_change_refs: commit_row.selected_change_refs.clone(),
275
+ commit_change_id: commit_row.change_id.clone(),
276
+ commit_created_at: commit_row.created_at.clone(),
277
+ },
278
+ );
323
279
  }
324
- let staged = commit_store
325
- .writer(transaction, writes)
326
- .stage_tracked_commit_drafts(commits)
327
- .await?;
328
- if staged.len() != commit_ids.len() {
280
+
281
+ let append = ChangelogAppend {
282
+ commits,
283
+ changes,
284
+ commit_change_refs,
285
+ };
286
+
287
+ let mut writer = ChangelogContext::new().writer(read, writes);
288
+ writer.stage_append(append).await?;
289
+ stage_commit_row_json_payloads(writes, commit_rows)?;
290
+ Ok(staged)
291
+ }
292
+
293
+ fn change_record_from_state_row(row: &PreparedStateRow) -> Result<ChangeRecord, LixError> {
294
+ let Some(change_id) = row.change_id.as_ref() else {
329
295
  return Err(LixError::new(
330
296
  LixError::CODE_INTERNAL_ERROR,
331
- format!(
332
- "commit-store staged {} commits for {} finalized commit rows",
333
- staged.len(),
334
- commit_ids.len()
335
- ),
297
+ "tracked staged row is missing change_id before changelog change construction",
336
298
  ));
299
+ };
300
+ Ok(ChangeRecord {
301
+ format_version: 1,
302
+ change_id: change_id.clone(),
303
+ entity_pk: row.entity_pk.clone(),
304
+ schema_key: row.schema_key.clone(),
305
+ file_id: row.file_id.clone(),
306
+ snapshot_ref: row.snapshot.as_ref().map(|snapshot| snapshot.json_ref),
307
+ metadata_ref: row.metadata.as_ref().map(|metadata| metadata.json_ref),
308
+ created_at: row.updated_at.clone(),
309
+ })
310
+ }
311
+
312
+ fn stage_commit_row_json_payloads(
313
+ writes: &mut StorageWriteSet,
314
+ commit_rows: &[FinalizedCommitRow],
315
+ ) -> Result<(), LixError> {
316
+ let snapshots = commit_rows
317
+ .iter()
318
+ .map(|row| commit_row_snapshot_content(&row.commit_id))
319
+ .collect::<Result<Vec<_>, _>>()?;
320
+ JsonStoreContext::new().writer().stage_batch(
321
+ writes,
322
+ JsonWritePlacementRef::OutOfBand,
323
+ snapshots
324
+ .iter()
325
+ .map(|snapshot| NormalizedJsonRef::new(snapshot.as_str())),
326
+ )?;
327
+ Ok(())
328
+ }
329
+
330
+ fn commit_row_snapshot_content(commit_id: &str) -> Result<String, LixError> {
331
+ serde_json::to_string(&serde_json::json!({
332
+ "id": commit_id,
333
+ }))
334
+ .map_err(|error| {
335
+ LixError::new(
336
+ LixError::CODE_INTERNAL_ERROR,
337
+ format!("failed to encode lix_commit snapshot: {error}"),
338
+ )
339
+ })
340
+ }
341
+
342
+ fn commit_change_ref_from_state_row(row: &PreparedStateRow, change_id: &str) -> CommitChangeRef {
343
+ CommitChangeRef {
344
+ schema_key: row.schema_key.clone(),
345
+ file_id: row.file_id.clone(),
346
+ entity_pk: row.entity_pk.clone(),
347
+ change_id: change_id.to_string(),
337
348
  }
338
- Ok(commit_ids.into_iter().zip(staged).collect())
339
349
  }
340
350
 
341
- fn change_ref_from_state_row(row: &PreparedStateRow) -> Result<ChangeRef<'_>, LixError> {
351
+ fn commit_change_ref_from_selected_change_ref(
352
+ change_ref: &StagedCommitChangeRef,
353
+ ) -> CommitChangeRef {
354
+ CommitChangeRef {
355
+ schema_key: change_ref.schema_key.clone(),
356
+ file_id: change_ref.file_id.clone(),
357
+ entity_pk: change_ref.entity_pk.clone(),
358
+ change_id: change_ref.change_id.clone(),
359
+ }
360
+ }
361
+
362
+ fn tracked_delta_from_state_row(
363
+ row: &PreparedStateRow,
364
+ ) -> Result<TrackedStateDeltaRef<'_>, LixError> {
342
365
  let Some(change_id) = row.change_id.as_deref() else {
343
366
  return Err(LixError::new(
344
- "LIX_ERROR_UNKNOWN",
345
- "tracked staged row is missing change_id before commit-store append",
367
+ LixError::CODE_INTERNAL_ERROR,
368
+ "tracked staged row is missing change_id before tracked root staging",
346
369
  ));
347
370
  };
348
-
349
- Ok(ChangeRef {
350
- id: change_id,
351
- entity_id: &row.entity_id,
371
+ let Some(commit_id) = row.commit_id.as_deref() else {
372
+ return Err(LixError::new(
373
+ LixError::CODE_INTERNAL_ERROR,
374
+ "tracked staged row is missing commit_id before tracked root staging",
375
+ ));
376
+ };
377
+ Ok(TrackedStateDeltaRef {
352
378
  schema_key: &row.schema_key,
353
379
  file_id: row.file_id.as_deref(),
380
+ entity_pk: &row.entity_pk,
381
+ change_id,
382
+ commit_id,
354
383
  snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
355
384
  metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
356
- created_at: &row.updated_at,
385
+ deleted: row.snapshot.is_none(),
386
+ created_at: &row.created_at,
387
+ updated_at: &row.updated_at,
357
388
  })
358
389
  }
359
390
 
360
- fn change_ref_from_adopted_row(row: &PreparedAdoptedStateRow) -> ChangeRef<'_> {
361
- ChangeRef {
362
- id: &row.change_id,
363
- entity_id: &row.entity_id,
364
- schema_key: &row.schema_key,
365
- file_id: row.file_id.as_deref(),
366
- snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
367
- metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
368
- created_at: &row.updated_at,
369
- }
391
+ fn tracked_delta_from_selected_change_ref<'a>(
392
+ change_ref: &'a StagedCommitChangeRef,
393
+ commit_id: &'a str,
394
+ ) -> Result<TrackedStateDeltaRef<'a>, LixError> {
395
+ Ok(TrackedStateDeltaRef {
396
+ schema_key: &change_ref.schema_key,
397
+ file_id: change_ref.file_id.as_deref(),
398
+ entity_pk: &change_ref.entity_pk,
399
+ change_id: &change_ref.change_id,
400
+ commit_id,
401
+ snapshot_ref: change_ref.snapshot_ref.as_ref(),
402
+ metadata_ref: change_ref.metadata_ref.as_ref(),
403
+ deleted: change_ref.deleted,
404
+ created_at: &change_ref.created_at,
405
+ updated_at: &change_ref.updated_at,
406
+ })
370
407
  }
371
408
 
372
409
  async fn stage_tracked_roots(
373
- transaction: &mut (impl StorageReader + ?Sized),
410
+ read: &(impl StorageRead + Send + Sync + ?Sized),
374
411
  writes: &mut StorageWriteSet,
375
412
  state_rows: &[PreparedStateRow],
376
- mut tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
377
- adopted_rows: &[PreparedAdoptedStateRow],
378
- mut adopted_row_indices_by_commit: BTreeMap<String, Vec<AdoptedRowIndex>>,
413
+ tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
379
414
  tracked_roots: Vec<PendingTrackedRoot>,
380
- mut staged_commits: BTreeMap<String, StagedCommitStoreCommit>,
381
- json_pack_indexes_by_commit: BTreeMap<
382
- String,
383
- BTreeMap<u32, std::collections::HashMap<[u8; 32], usize>>,
384
- >,
415
+ staged_commits: BTreeMap<String, StagedChangelogCommit>,
385
416
  ) -> Result<(), LixError> {
386
417
  let tracked_state = TrackedStateContext::new();
387
- let mut writer = tracked_state.writer(transaction, writes);
388
- for root in tracked_roots {
389
- let staged = staged_commits.remove(&root.commit_id).ok_or_else(|| {
418
+ let mut tracked_writer = tracked_state.writer(read, writes);
419
+ for root in tracked_roots_parent_first(&tracked_roots)? {
420
+ let staged = staged_commits.get(&root.commit_id).ok_or_else(|| {
390
421
  LixError::new(
391
422
  LixError::CODE_INTERNAL_ERROR,
392
423
  format!(
393
- "tracked-state root for commit '{}' has no staged commit-store locators",
424
+ "tracked-state root for commit '{}' has no staged changelog facts",
394
425
  root.commit_id
395
426
  ),
396
427
  )
397
428
  })?;
398
429
  let state_row_indices = tracked_row_indices_by_commit
399
- .remove(&root.commit_id)
400
- .unwrap_or_default();
401
- let adopted_row_indices = adopted_row_indices_by_commit
402
- .remove(&root.commit_id)
430
+ .get(&root.commit_id)
431
+ .map(Vec::as_slice)
403
432
  .unwrap_or_default();
404
- if state_row_indices.len() != staged.authored_locators.len() {
433
+ if state_row_indices.len() > staged.change_ids.len() {
405
434
  return Err(LixError::new(
406
435
  LixError::CODE_INTERNAL_ERROR,
407
436
  format!(
408
- "commit '{}' has {} tracked authored rows but {} commit-store authored locators",
437
+ "commit '{}' has {} tracked rows but only {} changelog changes",
409
438
  root.commit_id,
410
439
  state_row_indices.len(),
411
- staged.authored_locators.len()
412
- ),
413
- ));
414
- }
415
- if adopted_row_indices.len() != staged.adopted_locators.len() {
416
- return Err(LixError::new(
417
- LixError::CODE_INTERNAL_ERROR,
418
- format!(
419
- "commit '{}' has {} tracked adopted rows but {} commit-store adopted locators",
420
- root.commit_id,
421
- adopted_row_indices.len(),
422
- staged.adopted_locators.len()
440
+ staged.change_ids.len()
423
441
  ),
424
442
  ));
425
443
  }
426
- let authored_changes = state_row_indices
444
+ let commit_snapshot = commit_row_snapshot_content(&root.commit_id)?;
445
+ let commit_snapshot_ref = JsonRef::for_content(commit_snapshot.as_bytes());
446
+ let commit_entity_pk = EntityPk::single(root.commit_id.clone());
447
+ let mut deltas = state_row_indices
427
448
  .iter()
428
- .map(|&row_index| change_ref_from_state_row(&state_rows[row_index]))
449
+ .map(|&row_index| tracked_delta_from_state_row(&state_rows[row_index]))
450
+ .chain(staged.selected_change_refs.iter().map(|change_ref| {
451
+ tracked_delta_from_selected_change_ref(change_ref, &root.commit_id)
452
+ }))
429
453
  .collect::<Result<Vec<_>, _>>()?;
430
- let adopted_changes = adopted_row_indices
431
- .iter()
432
- .map(|&row_index| change_ref_from_adopted_row(&adopted_rows[row_index]))
433
- .collect::<Vec<_>>();
434
- let authored_updated_at = state_row_indices
435
- .iter()
436
- .map(|&row_index| state_rows[row_index].updated_at.as_str())
437
- .collect::<Vec<_>>();
438
- let authored_created_at = state_row_indices
439
- .iter()
440
- .map(|&row_index| state_rows[row_index].created_at.as_str())
441
- .collect::<Vec<_>>();
442
- let adopted_updated_at = adopted_row_indices
443
- .iter()
444
- .map(|&row_index| adopted_rows[row_index].updated_at.as_str())
445
- .collect::<Vec<_>>();
446
- let adopted_created_at = adopted_row_indices
447
- .iter()
448
- .map(|&row_index| adopted_rows[row_index].created_at.as_str())
449
- .collect::<Vec<_>>();
450
- let mut deltas = Vec::with_capacity(authored_changes.len() + adopted_changes.len());
451
- deltas.extend(
452
- authored_changes
453
- .iter()
454
- .zip(&staged.authored_locators)
455
- .zip(authored_created_at)
456
- .zip(authored_updated_at)
457
- .map(
458
- |(((change, locator), created_at), updated_at)| TrackedStateDeltaRef {
459
- change: *change,
460
- locator: locator.as_ref(),
461
- created_at,
462
- updated_at,
463
- },
464
- ),
465
- );
466
- deltas.extend(
467
- adopted_changes
468
- .iter()
469
- .zip(&staged.adopted_locators)
470
- .zip(adopted_created_at)
471
- .zip(adopted_updated_at)
472
- .map(
473
- |(((change, locator), created_at), updated_at)| TrackedStateDeltaRef {
474
- change: *change,
475
- locator: locator.as_ref(),
476
- created_at,
477
- updated_at,
478
- },
479
- ),
480
- );
481
- if let Some(indexes) = json_pack_indexes_by_commit
482
- .get(&root.commit_id)
483
- .and_then(|packs| packs.get(&0))
484
- {
485
- writer
486
- .stage_delta_with_json_pack_indexes(
487
- &root.commit_id,
488
- root.parent_commit_id.as_deref(),
489
- &deltas,
490
- crate::tracked_state::DeltaJsonPackIndexesRef {
491
- commit_id: &root.commit_id,
492
- pack_id: 0,
493
- indexes,
494
- },
495
- )
496
- .await?;
497
- } else {
498
- writer
499
- .stage_delta(&root.commit_id, root.parent_commit_id.as_deref(), &deltas)
500
- .await?;
501
- }
454
+ deltas.push(TrackedStateDeltaRef {
455
+ schema_key: "lix_commit",
456
+ file_id: None,
457
+ entity_pk: &commit_entity_pk,
458
+ change_id: &staged.commit_change_id,
459
+ commit_id: &root.commit_id,
460
+ snapshot_ref: Some(&commit_snapshot_ref),
461
+ metadata_ref: None,
462
+ deleted: false,
463
+ created_at: &staged.commit_created_at,
464
+ updated_at: &staged.commit_created_at,
465
+ });
466
+ tracked_writer
467
+ .stage_commit_root(&root.commit_id, root.parent_commit_id.as_deref(), deltas)
468
+ .await?;
502
469
  }
503
- if !tracked_row_indices_by_commit.is_empty() || !adopted_row_indices_by_commit.is_empty() {
470
+ let rooted_commit_ids = tracked_roots
471
+ .iter()
472
+ .map(|root| root.commit_id.as_str())
473
+ .collect::<BTreeSet<_>>();
474
+ let extra_tracked = tracked_row_indices_by_commit
475
+ .keys()
476
+ .filter(|commit_id| !rooted_commit_ids.contains(commit_id.as_str()))
477
+ .cloned()
478
+ .collect::<BTreeSet<_>>();
479
+ if !extra_tracked.is_empty() {
504
480
  let mut commit_ids = tracked_row_indices_by_commit
505
481
  .keys()
506
- .chain(adopted_row_indices_by_commit.keys())
507
482
  .cloned()
508
483
  .collect::<Vec<_>>();
509
484
  commit_ids.sort();
@@ -517,11 +492,18 @@ async fn stage_tracked_roots(
517
492
  ));
518
493
  }
519
494
  if !staged_commits.is_empty() {
520
- let commit_ids = staged_commits.keys().cloned().collect::<Vec<_>>();
495
+ let commit_ids = staged_commits
496
+ .keys()
497
+ .filter(|commit_id| !rooted_commit_ids.contains(commit_id.as_str()))
498
+ .cloned()
499
+ .collect::<Vec<_>>();
500
+ if commit_ids.is_empty() {
501
+ return Ok(());
502
+ }
521
503
  return Err(LixError::new(
522
504
  LixError::CODE_INTERNAL_ERROR,
523
505
  format!(
524
- "commit-store staged commits without tracked root metadata: {}",
506
+ "changelog staged commits without tracked root metadata: {}",
525
507
  commit_ids.join(", ")
526
508
  ),
527
509
  ));
@@ -529,9 +511,67 @@ async fn stage_tracked_roots(
529
511
  Ok(())
530
512
  }
531
513
 
514
+ fn tracked_roots_parent_first(
515
+ tracked_roots: &[PendingTrackedRoot],
516
+ ) -> Result<Vec<&PendingTrackedRoot>, LixError> {
517
+ let mut roots_by_id = BTreeMap::new();
518
+ for root in tracked_roots {
519
+ if roots_by_id.insert(root.commit_id.as_str(), root).is_some() {
520
+ return Err(LixError::unknown(format!(
521
+ "cannot stage duplicate tracked_state root '{}'",
522
+ root.commit_id
523
+ )));
524
+ }
525
+ }
526
+
527
+ let mut ordered = Vec::with_capacity(tracked_roots.len());
528
+ let mut visiting = BTreeSet::new();
529
+ let mut visited = BTreeSet::new();
530
+ for root in tracked_roots {
531
+ visit_tracked_root_parent_first(
532
+ root.commit_id.as_str(),
533
+ &roots_by_id,
534
+ &mut visiting,
535
+ &mut visited,
536
+ &mut ordered,
537
+ )?;
538
+ }
539
+ Ok(ordered)
540
+ }
541
+
542
+ fn visit_tracked_root_parent_first<'a>(
543
+ commit_id: &str,
544
+ roots_by_id: &BTreeMap<&'a str, &'a PendingTrackedRoot>,
545
+ visiting: &mut BTreeSet<&'a str>,
546
+ visited: &mut BTreeSet<&'a str>,
547
+ ordered: &mut Vec<&'a PendingTrackedRoot>,
548
+ ) -> Result<(), LixError> {
549
+ if visited.contains(commit_id) {
550
+ return Ok(());
551
+ }
552
+ let Some(root) = roots_by_id.get(commit_id).copied() else {
553
+ return Ok(());
554
+ };
555
+ if !visiting.insert(root.commit_id.as_str()) {
556
+ return Err(LixError::unknown(format!(
557
+ "cannot stage tracked_state root '{}' because staged root parents contain a cycle",
558
+ root.commit_id
559
+ )));
560
+ }
561
+ if let Some(parent_id) = root.parent_commit_id.as_deref() {
562
+ if roots_by_id.contains_key(parent_id) {
563
+ visit_tracked_root_parent_first(parent_id, roots_by_id, visiting, visited, ordered)?;
564
+ }
565
+ }
566
+ visiting.remove(root.commit_id.as_str());
567
+ visited.insert(root.commit_id.as_str());
568
+ ordered.push(root);
569
+ Ok(())
570
+ }
571
+
532
572
  fn untracked_row_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateRowRef<'_> {
533
573
  UntrackedStateRowRef {
534
- entity_id: &row.entity_id,
574
+ entity_pk: &row.entity_pk,
535
575
  schema_key: &row.schema_key,
536
576
  file_id: row.file_id.as_deref(),
537
577
  snapshot_content: row
@@ -545,51 +585,37 @@ fn untracked_row_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateRow
545
585
  created_at: &row.created_at,
546
586
  updated_at: &row.updated_at,
547
587
  global: row.global,
548
- version_id: &row.version_id,
588
+ branch_id: &row.branch_id,
549
589
  }
550
590
  }
551
591
 
552
592
  fn untracked_identity_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateIdentityRef<'_> {
553
593
  UntrackedStateIdentityRef {
554
- version_id: &row.version_id,
594
+ branch_id: &row.branch_id,
555
595
  schema_key: &row.schema_key,
556
- entity_id: &row.entity_id,
596
+ entity_pk: &row.entity_pk,
557
597
  file_id: row.file_id.as_deref(),
558
598
  }
559
599
  }
560
600
 
561
- fn untracked_identity_ref_from_adopted_row(
562
- row: &PreparedAdoptedStateRow,
563
- ) -> UntrackedStateIdentityRef<'_> {
564
- UntrackedStateIdentityRef {
565
- version_id: &row.version_id,
566
- schema_key: &row.schema_key,
567
- entity_id: &row.entity_id,
568
- file_id: row.file_id.as_deref(),
569
- }
570
- }
571
-
572
- /// Materializes tracked staged membership into commit-store commits.
601
+ /// Materializes tracked staged change refs into changelog commits.
573
602
  ///
574
- /// Staging only accumulates `version_id -> change_ids` because commit ids,
603
+ /// Staging only accumulates `branch_id -> change_ids` because commit ids,
575
604
  /// parent heads, and commit-row timestamps belong to transaction finalization.
576
605
  /// The `change_ids` list is the ordered set of canonical changes whose effects
577
- /// the commit introduces relative to its first parent; merge commits may later
578
- /// populate this list with existing source-parent changes instead of copied
579
- /// change payloads.
580
- /// This function turns those membership sets into finalized commit facts.
606
+ /// the commit introduces relative to its first parent.
607
+ /// This function turns those change-ref sets into finalized commit facts.
581
608
  ///
582
609
  /// Commit finalization output split by durability target.
583
610
  ///
584
- /// `commit_rows` are canonical commit-store facts. live_state later projects
585
- /// commit SQL surfaces from commit_store; tracked_state roots do not store
586
- /// commit graph facts.
611
+ /// `commit_rows` are canonical changelog commit facts. tracked_state roots store
612
+ /// serving commit roots keyed by the corresponding commit id.
587
613
  ///
588
- /// `version_heads` are moving refs. They are written through `VersionContext`,
589
- /// not the canonical commit store.
614
+ /// `branch_heads` are moving refs. They are written through `BranchContext`,
615
+ /// not the canonical changelog.
590
616
  struct FinalizedCommitRows {
591
617
  commit_rows: Vec<FinalizedCommitRow>,
592
- version_heads: Vec<PendingVersionHead>,
618
+ branch_heads: Vec<PendingBranchHead>,
593
619
  tracked_roots: Vec<PendingTrackedRoot>,
594
620
  }
595
621
 
@@ -598,10 +624,11 @@ struct FinalizedCommitRow {
598
624
  parent_commit_ids: Vec<String>,
599
625
  created_at: String,
600
626
  change_id: String,
627
+ selected_change_refs: Vec<StagedCommitChangeRef>,
601
628
  }
602
629
 
603
- struct PendingVersionHead {
604
- version_id: String,
630
+ struct PendingBranchHead {
631
+ branch_id: String,
605
632
  commit_id: String,
606
633
  timestamp: String,
607
634
  }
@@ -612,34 +639,34 @@ struct PendingTrackedRoot {
612
639
  }
613
640
 
614
641
  async fn finalize_commit_rows(
615
- commit_members_by_version: BTreeMap<String, StagedCommitMembers>,
616
- extra_commit_parents_by_version: BTreeMap<String, Vec<String>>,
617
- version_ctx: &VersionContext,
618
- transaction: &mut (impl StorageReader + ?Sized),
642
+ commit_change_refs_by_branch: BTreeMap<String, StagedCommitChangeRefs>,
643
+ extra_commit_parents_by_branch: BTreeMap<String, Vec<String>>,
644
+ branch_ctx: &BranchContext,
645
+ read: &(impl StorageRead + Send + Sync + ?Sized),
619
646
  ) -> Result<FinalizedCommitRows, LixError> {
620
647
  let mut commit_rows = Vec::new();
621
- let mut version_heads = Vec::new();
648
+ let mut branch_heads = Vec::new();
622
649
  let mut tracked_roots = Vec::new();
623
650
 
624
- for (version_id, members) in commit_members_by_version {
625
- if members.is_empty() && !members.allow_empty {
651
+ for (branch_id, change_refs) in commit_change_refs_by_branch {
652
+ if change_refs.is_empty() && !change_refs.allow_empty {
626
653
  continue;
627
654
  }
628
655
 
629
- let commit_id = members.commit_id;
630
- let commit_change_id = members.commit_change_id;
631
- let timestamp = members.created_at;
632
- let _change_ids = members.change_ids;
633
- let parent_commit_ids = version_ctx
634
- .ref_reader(&mut *transaction)
635
- .load_head_commit_id(&version_id)
656
+ let commit_id = change_refs.commit_id;
657
+ let commit_change_id = change_refs.commit_change_id;
658
+ let timestamp = change_refs.created_at;
659
+ let selected_change_refs = change_refs.selected_change_refs;
660
+ let parent_commit_ids = branch_ctx
661
+ .ref_reader(read)
662
+ .load_head_commit_id(&branch_id)
636
663
  .await?
637
664
  .into_iter()
638
665
  .collect::<Vec<_>>();
639
666
  let parent_commit_ids = merge_parent_commit_ids(
640
667
  parent_commit_ids,
641
- extra_commit_parents_by_version
642
- .get(&version_id)
668
+ extra_commit_parents_by_branch
669
+ .get(&branch_id)
643
670
  .cloned()
644
671
  .unwrap_or_default(),
645
672
  );
@@ -650,9 +677,10 @@ async fn finalize_commit_rows(
650
677
  parent_commit_ids: parent_commit_ids.clone(),
651
678
  created_at: timestamp.clone(),
652
679
  change_id: commit_change_id,
680
+ selected_change_refs,
653
681
  });
654
- version_heads.push(PendingVersionHead {
655
- version_id: version_id.clone(),
682
+ branch_heads.push(PendingBranchHead {
683
+ branch_id: branch_id.clone(),
656
684
  commit_id: commit_id.clone(),
657
685
  timestamp,
658
686
  });
@@ -664,7 +692,7 @@ async fn finalize_commit_rows(
664
692
 
665
693
  Ok(FinalizedCommitRows {
666
694
  commit_rows,
667
- version_heads,
695
+ branch_heads,
668
696
  tracked_roots,
669
697
  })
670
698
  }
@@ -688,23 +716,23 @@ mod tests {
688
716
 
689
717
  use super::*;
690
718
  use crate::backend::{
691
- testing::UnitTestBackend, Backend, BackendKvEntryPage, BackendKvExistsBatch,
692
- BackendKvGetRequest, BackendKvKeyPage, BackendKvScanRequest, BackendKvValueBatch,
693
- BackendKvValuePage, BackendKvWriteBatch, BackendKvWriteStats, BackendReadTransaction,
694
- BackendWriteTransaction,
719
+ Backend, BackendCapabilities, BackendError, BackendWrite, CommitResult, DurableWriteLock,
720
+ KeyRange, PutBatch,
695
721
  };
722
+ use crate::branch::BranchContext;
696
723
  use crate::catalog::SchemaPlanId;
697
- use crate::commit_store::{ChangeIndexEntry, ChangeLocator};
724
+ use crate::changelog::ChangelogReader;
698
725
  use crate::live_state::{LiveStateContext, LiveStateRowRequest};
699
- use crate::storage::StorageContext;
726
+ use crate::storage::{
727
+ InMemoryStorageBackend, InMemoryStorageRead, InMemoryStorageWrite, StorageContext,
728
+ StorageKey, StorageReadOptions, StorageWriteOptions,
729
+ };
700
730
  use crate::transaction::types::PreparedRowFacts;
701
731
  use crate::untracked_state::{
702
732
  MaterializedUntrackedStateRow, UntrackedStateContext, UntrackedStateRowRequest,
703
733
  };
704
- use crate::version::VersionContext;
705
734
  use crate::NullableKeyFilter;
706
- use crate::GLOBAL_VERSION_ID;
707
- use async_trait::async_trait;
735
+ use crate::GLOBAL_BRANCH_ID;
708
736
 
709
737
  const DETERMINISTIC_MODE_KEY: &str = "lix_deterministic_mode";
710
738
  const DETERMINISTIC_SEQUENCE_KEY: &str = "lix_deterministic_sequence_number";
@@ -718,138 +746,215 @@ mod tests {
718
746
  }
719
747
 
720
748
  #[tokio::test]
721
- async fn commit_staged_writes_appends_commit_store_and_updates_serving_projection() {
722
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
723
- let storage = StorageContext::new(Arc::clone(&backend));
749
+ async fn commit_staged_writes_appends_changelog_and_updates_commit_root() {
750
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
724
751
  let binary_cas = BinaryCasContext::new();
725
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
726
- let mut transaction = storage
727
- .begin_write_transaction()
728
- .await
729
- .expect("transaction should open");
752
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
753
+ let mut read = storage
754
+ .begin_read(StorageReadOptions::default())
755
+ .expect("read should open");
730
756
 
731
757
  let state_rows = vec![tracked_global_row("change-1")];
732
- commit_prepared_writes(
758
+ let writes = commit_prepared_writes(
733
759
  &binary_cas,
734
- &crate::commit_store::CommitStoreContext::new(),
735
- &version_ctx,
760
+ &branch_ctx,
736
761
  None,
737
- transaction.as_mut(),
762
+ &mut read,
738
763
  PreparedWriteSet {
739
764
  insert_identities: BTreeMap::new(),
740
765
  state_rows,
741
- adopted_rows: Vec::new(),
742
- commit_members_by_version: BTreeMap::from([(
743
- GLOBAL_VERSION_ID.to_string(),
744
- members(["change-1"]),
766
+ commit_change_refs_by_branch: BTreeMap::from([(
767
+ GLOBAL_BRANCH_ID.to_string(),
768
+ change_refs(["change-1"]),
745
769
  )]),
746
- extra_commit_parents_by_version: BTreeMap::new(),
770
+ extra_commit_parents_by_branch: BTreeMap::new(),
747
771
  file_data_writes: Vec::new(),
748
772
  },
749
773
  )
750
774
  .await
751
775
  .expect("commit should flush staged rows");
752
- transaction
753
- .commit()
754
- .await
755
- .expect("commit should persist kv");
756
-
757
- let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
758
- let commit = commit_reader
759
- .load_commit("test-uuid-1")
776
+ storage
777
+ .commit_write_set(writes, StorageWriteOptions::default())
778
+ .expect("writes should commit");
779
+
780
+ let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
781
+ storage
782
+ .begin_read(StorageReadOptions::default())
783
+ .expect("read should open"),
784
+ );
785
+ let commits = changelog_reader
786
+ .load_commits(crate::changelog::CommitLoadRequest {
787
+ commit_ids: &["test-uuid-1".to_string()],
788
+ projection: crate::changelog::CommitProjection::Full,
789
+ })
760
790
  .await
761
- .expect("commit-store commit should load")
762
- .expect("commit-store commit should exist");
763
- assert_eq!(commit.change_id, "test-uuid-2");
764
- assert_eq!(commit.change_pack_count, 1);
765
- assert_eq!(commit.membership_pack_count, 0);
766
- let index_entries = commit_reader
767
- .load_change_index_entries(&["change-1".to_string(), "test-uuid-2".to_string()])
791
+ .expect("changelog commit should load");
792
+ let Some(crate::changelog::CommitLoadEntry::Full {
793
+ record,
794
+ change_ref_chunks,
795
+ }) = commits.entries.into_iter().next().flatten()
796
+ else {
797
+ panic!("changelog commit should exist");
798
+ };
799
+ assert_eq!(record.change_id, "test-uuid-2");
800
+ assert!(change_ref_chunks
801
+ .iter()
802
+ .flat_map(|chunk| chunk.entries.iter())
803
+ .any(|entry| entry.change_id == "change-1"));
804
+ let changes = changelog_reader
805
+ .load_changes(crate::changelog::ChangeLoadRequest {
806
+ change_ids: &["change-1".to_string(), record.change_id.clone()],
807
+ })
768
808
  .await
769
- .expect("commit-store change index should load");
770
- assert_eq!(
771
- index_entries,
772
- vec![
773
- Some(ChangeIndexEntry::PackedChange {
774
- locator: ChangeLocator {
775
- source_commit_id: "test-uuid-1".to_string(),
776
- source_pack_id: 0,
777
- source_ordinal: 0,
778
- change_id: "change-1".to_string(),
779
- },
780
- }),
781
- Some(ChangeIndexEntry::CommitHeader {
782
- commit_id: "test-uuid-1".to_string(),
783
- change_id: "test-uuid-2".to_string(),
784
- }),
785
- ]
809
+ .expect("changelog change should load");
810
+ let mut loaded_changes = changes.entries.into_iter();
811
+ let Some(change) = loaded_changes.next().flatten() else {
812
+ panic!("changelog change should exist");
813
+ };
814
+ assert_eq!(change.change_id, "change-1");
815
+ assert_eq!(change.schema_key, "test_schema");
816
+ assert!(
817
+ loaded_changes.next().flatten().is_none(),
818
+ "commit row change is derived from changelog.commit, not stored as changelog.change"
819
+ );
820
+
821
+ let mut tracked_reader = crate::tracked_state::TrackedStateContext::new().reader(
822
+ storage
823
+ .begin_read(StorageReadOptions::default())
824
+ .expect("read should open"),
786
825
  );
787
- let change_pack = commit_reader
788
- .load_change_pack("test-uuid-1", 0)
826
+ let commit_rows = tracked_reader
827
+ .scan_rows_at_commit(
828
+ "test-uuid-1",
829
+ &crate::tracked_state::TrackedStateScanRequest {
830
+ filter: crate::tracked_state::TrackedStateFilter {
831
+ schema_keys: vec!["lix_commit".to_string()],
832
+ ..Default::default()
833
+ },
834
+ ..Default::default()
835
+ },
836
+ )
789
837
  .await
790
- .expect("commit-store change pack should load")
791
- .expect("commit-store change pack should exist");
792
- assert_eq!(change_pack.len(), 1);
793
- assert_eq!(change_pack[0].id, "change-1");
794
- assert_eq!(change_pack[0].schema_key, "test_schema");
795
-
796
- let loaded_head = version_ctx
797
- .ref_reader(storage.clone())
798
- .load_head_commit_id(GLOBAL_VERSION_ID)
838
+ .expect("commit root should scan");
839
+ assert!(
840
+ commit_rows
841
+ .iter()
842
+ .any(|row| row.change_id == record.change_id && row.snapshot_content.is_some()),
843
+ "commit root should surface the derived lix_commit row"
844
+ );
845
+
846
+ let loaded_head = branch_ctx
847
+ .ref_reader(
848
+ storage
849
+ .begin_read(StorageReadOptions::default())
850
+ .expect("read should open"),
851
+ )
852
+ .load_head_commit_id(GLOBAL_BRANCH_ID)
799
853
  .await
800
- .expect("version ref load should succeed");
854
+ .expect("branch ref load should succeed");
801
855
  assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
802
856
  }
803
857
 
858
+ #[tokio::test]
859
+ async fn stage_changelog_commits_orders_staged_parents_before_children() {
860
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
861
+ let mut writes = StorageWriteSet::new();
862
+ let mut read = storage
863
+ .begin_read(StorageReadOptions::default())
864
+ .expect("read should open");
865
+ let mut parent_row = tracked_global_row("parent-change");
866
+ parent_row.commit_id = Some("parent-commit".to_string());
867
+ let mut child_row = tracked_global_row("child-change");
868
+ child_row.commit_id = Some("child-commit".to_string());
869
+
870
+ let commits = vec![
871
+ FinalizedCommitRow {
872
+ commit_id: "child-commit".to_string(),
873
+ parent_commit_ids: vec!["parent-commit".to_string()],
874
+ created_at: "2026-01-01T00:00:01Z".to_string(),
875
+ change_id: "child-commit-change".to_string(),
876
+ selected_change_refs: Vec::new(),
877
+ },
878
+ FinalizedCommitRow {
879
+ commit_id: "parent-commit".to_string(),
880
+ parent_commit_ids: Vec::new(),
881
+ created_at: "2026-01-01T00:00:00Z".to_string(),
882
+ change_id: "parent-commit-change".to_string(),
883
+ selected_change_refs: Vec::new(),
884
+ },
885
+ ];
886
+ stage_changelog_commits(
887
+ &mut read,
888
+ &mut writes,
889
+ &[parent_row, child_row],
890
+ &BTreeMap::from([
891
+ ("parent-commit".to_string(), vec![0]),
892
+ ("child-commit".to_string(), vec![1]),
893
+ ]),
894
+ &commits,
895
+ )
896
+ .await
897
+ .expect("child-before-parent input should still stage parent first");
898
+ storage
899
+ .commit_write_set(writes, StorageWriteOptions::default())
900
+ .expect("writes should persist");
901
+
902
+ let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
903
+ storage
904
+ .begin_read(StorageReadOptions::default())
905
+ .expect("read should open"),
906
+ );
907
+ let commits = changelog_reader
908
+ .load_commits(crate::changelog::CommitLoadRequest {
909
+ commit_ids: &["parent-commit".to_string(), "child-commit".to_string()],
910
+ projection: crate::changelog::CommitProjection::Record,
911
+ })
912
+ .await
913
+ .expect("commits should load");
914
+ assert!(commits.entries.iter().all(Option::is_some));
915
+ }
916
+
804
917
  #[tokio::test]
805
918
  async fn commit_with_only_untracked_writes_does_not_create_lix_commit() {
806
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
807
- let storage = StorageContext::new(Arc::clone(&backend));
919
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
808
920
  let binary_cas = BinaryCasContext::new();
809
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
921
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
810
922
  let untracked_state = UntrackedStateContext::new();
811
- let mut transaction = storage
812
- .begin_write_transaction()
813
- .await
814
- .expect("transaction should open");
923
+ let mut read = storage
924
+ .begin_read(StorageReadOptions::default())
925
+ .expect("read should open");
815
926
 
816
927
  let state_rows = vec![untracked_global_row("change-untracked")];
817
- commit_prepared_writes(
928
+ let writes = commit_prepared_writes(
818
929
  &binary_cas,
819
- &crate::commit_store::CommitStoreContext::new(),
820
- &version_ctx,
930
+ &branch_ctx,
821
931
  None,
822
- transaction.as_mut(),
932
+ &mut read,
823
933
  PreparedWriteSet {
824
934
  insert_identities: BTreeMap::new(),
825
935
  state_rows,
826
- adopted_rows: Vec::new(),
827
- commit_members_by_version: BTreeMap::new(),
828
- extra_commit_parents_by_version: BTreeMap::new(),
936
+ commit_change_refs_by_branch: BTreeMap::new(),
937
+ extra_commit_parents_by_branch: BTreeMap::new(),
829
938
  file_data_writes: Vec::new(),
830
939
  },
831
940
  )
832
941
  .await
833
942
  .expect("commit should flush untracked row");
834
- transaction
835
- .commit()
836
- .await
837
- .expect("commit should persist kv");
838
-
839
- let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
840
- let index_entries = commit_reader
841
- .load_change_index_entries(&["change-untracked".to_string()])
842
- .await
843
- .expect("commit-store change index should load");
844
- assert_eq!(index_entries, vec![None]);
943
+ storage
944
+ .commit_write_set(writes, StorageWriteOptions::default())
945
+ .expect("writes should commit");
845
946
 
846
947
  let loaded = {
847
- let mut untracked_reader = untracked_state.reader(storage.clone());
948
+ let mut untracked_reader = untracked_state.reader(
949
+ storage
950
+ .begin_read(StorageReadOptions::default())
951
+ .expect("read should open"),
952
+ );
848
953
  untracked_reader
849
954
  .load_row(&UntrackedStateRowRequest {
850
955
  schema_key: "test_schema".to_string(),
851
- version_id: GLOBAL_VERSION_ID.to_string(),
852
- entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
956
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
957
+ entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
853
958
  file_id: NullableKeyFilter::Null,
854
959
  })
855
960
  .await
@@ -864,17 +969,12 @@ mod tests {
864
969
 
865
970
  #[tokio::test]
866
971
  async fn tracked_write_deletes_matching_untracked_overlay() {
867
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
868
- let storage = StorageContext::new(Arc::clone(&backend));
972
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
869
973
  let binary_cas = BinaryCasContext::new();
870
974
  let untracked_state = UntrackedStateContext::new();
871
975
  let live_state = Arc::new(live_state_context());
872
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
976
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
873
977
 
874
- let mut seed_transaction = storage
875
- .begin_write_transaction()
876
- .await
877
- .expect("seed transaction should open");
878
978
  let mut writes = StorageWriteSet::new();
879
979
  let staged_row = untracked_global_row("change-untracked");
880
980
  let canonical_row = crate::test_support::untracked_state_row_from_materialized(
@@ -886,54 +986,53 @@ mod tests {
886
986
  .writer(&mut writes)
887
987
  .stage_rows(std::iter::once(canonical_row.as_ref()))
888
988
  .expect("untracked seed should write");
889
- writes
890
- .apply(&mut seed_transaction.as_mut())
891
- .await
892
- .expect("untracked seed should apply");
893
- seed_transaction
894
- .commit()
895
- .await
896
- .expect("seed transaction should persist");
989
+ storage
990
+ .commit_write_set(writes, StorageWriteOptions::default())
991
+ .expect("untracked seed should commit");
897
992
 
898
- let mut transaction = storage
899
- .begin_write_transaction()
900
- .await
901
- .expect("transaction should open");
993
+ let mut read = storage
994
+ .begin_read(StorageReadOptions::default())
995
+ .expect("read should open");
902
996
  let state_rows = vec![tracked_global_row("change-tracked")];
903
- commit_prepared_writes(
997
+ let writes = commit_prepared_writes(
904
998
  &binary_cas,
905
- &crate::commit_store::CommitStoreContext::new(),
906
- &version_ctx,
999
+ &branch_ctx,
907
1000
  None,
908
- transaction.as_mut(),
1001
+ &mut read,
909
1002
  PreparedWriteSet {
910
1003
  insert_identities: BTreeMap::new(),
911
1004
  state_rows,
912
- adopted_rows: Vec::new(),
913
- commit_members_by_version: BTreeMap::from([(
914
- GLOBAL_VERSION_ID.to_string(),
915
- members(["change-tracked"]),
1005
+ commit_change_refs_by_branch: BTreeMap::from([(
1006
+ GLOBAL_BRANCH_ID.to_string(),
1007
+ change_refs(["change-tracked"]),
916
1008
  )]),
917
- extra_commit_parents_by_version: BTreeMap::new(),
1009
+ extra_commit_parents_by_branch: BTreeMap::new(),
918
1010
  file_data_writes: Vec::new(),
919
1011
  },
920
1012
  )
921
1013
  .await
922
1014
  .expect("tracked commit should flush");
923
- transaction
924
- .commit()
925
- .await
926
- .expect("commit should persist kv");
1015
+ storage
1016
+ .commit_write_set(writes, StorageWriteOptions::default())
1017
+ .expect("writes should commit");
927
1018
 
928
1019
  let untracked = {
929
- let mut untracked_reader = untracked_state.reader(storage.clone());
1020
+ let mut untracked_reader = untracked_state.reader(
1021
+ storage
1022
+ .begin_read(StorageReadOptions::default())
1023
+ .expect("read should open"),
1024
+ );
930
1025
  untracked_reader.load_row(&untracked_request()).await
931
1026
  }
932
1027
  .expect("untracked load should succeed");
933
1028
  assert_eq!(untracked, None);
934
1029
 
935
1030
  let visible = live_state
936
- .reader(storage.clone())
1031
+ .reader(
1032
+ storage
1033
+ .begin_read(StorageReadOptions::default())
1034
+ .expect("read should open"),
1035
+ )
937
1036
  .load_row(&live_state_request())
938
1037
  .await
939
1038
  .expect("live-state load should succeed")
@@ -945,20 +1044,43 @@ mod tests {
945
1044
 
946
1045
  #[tokio::test]
947
1046
  async fn commit_staged_writes_applies_cross_subsystem_rows_as_one_backend_batch() {
948
- let counting_backend = Arc::new(CountingBackend::new());
1047
+ let counting_backend = CountingBackend::new();
949
1048
  let write_batches = counting_backend.write_batches();
950
- let backend: Arc<dyn Backend + Send + Sync> = counting_backend;
951
- let storage = StorageContext::new(backend);
1049
+ let storage = StorageContext::new(counting_backend);
952
1050
  let binary_cas = BinaryCasContext::new();
953
1051
  let live_state = Arc::new(live_state_context());
954
1052
  let untracked_state = UntrackedStateContext::new();
955
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
956
- crate::test_support::seed_global_version_head(storage.clone()).await;
1053
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1054
+ {
1055
+ let mut read = storage
1056
+ .begin_read(StorageReadOptions::default())
1057
+ .expect("seed read should open");
1058
+ let mut writes = storage.new_write_set();
1059
+ crate::test_support::stage_tracked_root_from_materialized(
1060
+ &mut read,
1061
+ &mut writes,
1062
+ &crate::tracked_state::TrackedStateContext::new(),
1063
+ crate::test_support::TEST_EMPTY_ROOT_COMMIT_ID,
1064
+ None,
1065
+ &[],
1066
+ )
1067
+ .await
1068
+ .expect("empty tracked root should stage");
1069
+ let branch_ref_row = crate::transaction::prepare_branch_ref_row(
1070
+ GLOBAL_BRANCH_ID,
1071
+ crate::test_support::TEST_EMPTY_ROOT_COMMIT_ID,
1072
+ "1970-01-01T00:00:00.000Z",
1073
+ )
1074
+ .expect("global branch ref should stage");
1075
+ UntrackedStateContext::new()
1076
+ .writer(&mut writes)
1077
+ .stage_rows([branch_ref_row.row.as_ref()])
1078
+ .expect("global branch ref should stage");
1079
+ storage
1080
+ .commit_write_set(writes, StorageWriteOptions::default())
1081
+ .expect("global branch ref should commit");
1082
+ }
957
1083
  {
958
- let mut seed_transaction = storage
959
- .begin_write_transaction()
960
- .await
961
- .expect("seed transaction should open");
962
1084
  let mut writes = StorageWriteSet::new();
963
1085
  let mode_snapshot = serde_json::to_string(&serde_json::json!({
964
1086
  "key": DETERMINISTIC_MODE_KEY,
@@ -974,7 +1096,7 @@ mod tests {
974
1096
  )
975
1097
  .expect("deterministic mode snapshot should stage");
976
1098
  let row = crate::untracked_state::UntrackedStateRow {
977
- entity_id: crate::entity_identity::EntityIdentity::single(DETERMINISTIC_MODE_KEY),
1099
+ entity_pk: crate::entity_pk::EntityPk::single(DETERMINISTIC_MODE_KEY),
978
1100
  schema_key: "lix_key_value".to_string(),
979
1101
  file_id: None,
980
1102
  snapshot_content: Some(mode_snapshot.to_string()),
@@ -982,53 +1104,49 @@ mod tests {
982
1104
  created_at: "2026-01-01T00:00:00Z".to_string(),
983
1105
  updated_at: "2026-01-01T00:00:00Z".to_string(),
984
1106
  global: true,
985
- version_id: GLOBAL_VERSION_ID.to_string(),
1107
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
986
1108
  };
987
1109
  UntrackedStateContext::new()
988
1110
  .writer(&mut writes)
989
1111
  .stage_rows(std::iter::once(row.as_ref()))
990
1112
  .expect("deterministic mode should stage");
991
- writes
992
- .apply(&mut seed_transaction.as_mut())
993
- .await
994
- .expect("deterministic mode should apply");
995
- seed_transaction
996
- .commit()
997
- .await
998
- .expect("seed transaction should persist");
1113
+ storage
1114
+ .commit_write_set(writes, StorageWriteOptions::default())
1115
+ .expect("deterministic mode should commit");
999
1116
  }
1000
1117
  write_batches.store(0, Ordering::SeqCst);
1001
1118
  let runtime_functions = {
1002
- let reader = live_state.reader(storage.clone());
1119
+ let reader = live_state.reader(
1120
+ storage
1121
+ .begin_read(StorageReadOptions::default())
1122
+ .expect("read should open"),
1123
+ );
1003
1124
  FunctionContext::prepare(&reader)
1004
1125
  .await
1005
1126
  .expect("runtime context should prepare")
1006
1127
  };
1007
1128
  runtime_functions.provider().call_uuid_v7();
1008
- let mut transaction = storage
1009
- .begin_write_transaction()
1010
- .await
1011
- .expect("transaction should open");
1129
+ let mut read = storage
1130
+ .begin_read(StorageReadOptions::default())
1131
+ .expect("read should open");
1012
1132
 
1013
1133
  let tracked_row = tracked_global_row("change-tracked");
1014
1134
  let mut untracked_row = untracked_global_row("change-untracked");
1015
- untracked_row.entity_id = crate::entity_identity::EntityIdentity::single("entity-2");
1135
+ untracked_row.entity_pk = crate::entity_pk::EntityPk::single("entity-2");
1016
1136
 
1017
- commit_prepared_writes(
1137
+ let writes = commit_prepared_writes(
1018
1138
  &binary_cas,
1019
- &crate::commit_store::CommitStoreContext::new(),
1020
- &version_ctx,
1139
+ &branch_ctx,
1021
1140
  Some(&runtime_functions),
1022
- transaction.as_mut(),
1141
+ &mut read,
1023
1142
  PreparedWriteSet {
1024
1143
  insert_identities: BTreeMap::new(),
1025
1144
  state_rows: vec![tracked_row, untracked_row],
1026
- adopted_rows: Vec::new(),
1027
- commit_members_by_version: BTreeMap::from([(
1028
- GLOBAL_VERSION_ID.to_string(),
1029
- members(["change-tracked"]),
1145
+ commit_change_refs_by_branch: BTreeMap::from([(
1146
+ GLOBAL_BRANCH_ID.to_string(),
1147
+ change_refs(["change-tracked"]),
1030
1148
  )]),
1031
- extra_commit_parents_by_version: BTreeMap::new(),
1149
+ extra_commit_parents_by_branch: BTreeMap::new(),
1032
1150
  file_data_writes: Vec::new(),
1033
1151
  },
1034
1152
  )
@@ -1037,46 +1155,65 @@ mod tests {
1037
1155
 
1038
1156
  assert_eq!(
1039
1157
  write_batches.load(Ordering::SeqCst),
1040
- 1,
1041
- "tracked, json, untracked, commit-store, and version refs must apply as one backend write batch"
1158
+ 0,
1159
+ "prepared writes should not touch the backend before the write set is committed"
1042
1160
  );
1043
-
1044
- transaction
1045
- .commit()
1046
- .await
1047
- .expect("commit should persist kv");
1161
+ storage
1162
+ .commit_write_set(writes, StorageWriteOptions::default())
1163
+ .expect("writes should commit");
1048
1164
  assert_eq!(write_batches.load(Ordering::SeqCst), 1);
1049
1165
 
1050
- let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
1051
- let commit = commit_reader
1052
- .load_commit("test-uuid-1")
1166
+ let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
1167
+ storage
1168
+ .begin_read(StorageReadOptions::default())
1169
+ .expect("read should open"),
1170
+ );
1171
+ let commits = changelog_reader
1172
+ .load_commits(crate::changelog::CommitLoadRequest {
1173
+ commit_ids: &["test-uuid-1".to_string()],
1174
+ projection: crate::changelog::CommitProjection::Record,
1175
+ })
1053
1176
  .await
1054
- .expect("commit-store commit should load")
1055
- .expect("commit-store commit should exist");
1177
+ .expect("changelog commit should load");
1178
+ let Some(crate::changelog::CommitLoadEntry::Record(commit)) =
1179
+ commits.entries.into_iter().next().flatten()
1180
+ else {
1181
+ panic!("changelog commit should exist");
1182
+ };
1056
1183
  assert_eq!(commit.change_id, "test-uuid-2");
1057
- let index_entries = commit_reader
1058
- .load_change_index_entries(&["change-tracked".to_string()])
1184
+ let changes = changelog_reader
1185
+ .load_changes(crate::changelog::ChangeLoadRequest {
1186
+ change_ids: &["change-tracked".to_string()],
1187
+ })
1059
1188
  .await
1060
- .expect("commit-store change index should load");
1189
+ .expect("changelog change should load");
1061
1190
  assert!(matches!(
1062
- index_entries.as_slice(),
1063
- [Some(ChangeIndexEntry::PackedChange { .. })]
1191
+ changes.entries.as_slice(),
1192
+ [Some(change)] if change.change_id == "change-tracked"
1064
1193
  ));
1065
1194
 
1066
- let loaded_head = version_ctx
1067
- .ref_reader(storage.clone())
1068
- .load_head_commit_id(GLOBAL_VERSION_ID)
1195
+ let loaded_head = branch_ctx
1196
+ .ref_reader(
1197
+ storage
1198
+ .begin_read(StorageReadOptions::default())
1199
+ .expect("read should open"),
1200
+ )
1201
+ .load_head_commit_id(GLOBAL_BRANCH_ID)
1069
1202
  .await
1070
- .expect("version ref load should succeed");
1203
+ .expect("branch ref load should succeed");
1071
1204
  assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
1072
1205
 
1073
1206
  let untracked = {
1074
- let mut untracked_reader = untracked_state.reader(storage.clone());
1207
+ let mut untracked_reader = untracked_state.reader(
1208
+ storage
1209
+ .begin_read(StorageReadOptions::default())
1210
+ .expect("read should open"),
1211
+ );
1075
1212
  untracked_reader
1076
1213
  .load_row(&UntrackedStateRowRequest {
1077
1214
  schema_key: "test_schema".to_string(),
1078
- version_id: GLOBAL_VERSION_ID.to_string(),
1079
- entity_id: crate::entity_identity::EntityIdentity::single("entity-2"),
1215
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
1216
+ entity_pk: crate::entity_pk::EntityPk::single("entity-2"),
1080
1217
  file_id: NullableKeyFilter::Null,
1081
1218
  })
1082
1219
  .await
@@ -1089,13 +1226,15 @@ mod tests {
1089
1226
  );
1090
1227
 
1091
1228
  let sequence_row = live_state
1092
- .reader(storage.clone())
1229
+ .reader(
1230
+ storage
1231
+ .begin_read(StorageReadOptions::default())
1232
+ .expect("read should open"),
1233
+ )
1093
1234
  .load_row(&LiveStateRowRequest {
1094
1235
  schema_key: "lix_key_value".to_string(),
1095
- version_id: GLOBAL_VERSION_ID.to_string(),
1096
- entity_id: crate::entity_identity::EntityIdentity::single(
1097
- DETERMINISTIC_SEQUENCE_KEY,
1098
- ),
1236
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
1237
+ entity_pk: crate::entity_pk::EntityPk::single(DETERMINISTIC_SEQUENCE_KEY),
1099
1238
  file_id: NullableKeyFilter::Null,
1100
1239
  })
1101
1240
  .await
@@ -1108,189 +1247,181 @@ mod tests {
1108
1247
  }
1109
1248
 
1110
1249
  #[tokio::test]
1111
- async fn non_global_tracked_write_creates_one_commit_and_advances_only_touched_version() {
1112
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1113
- let storage = StorageContext::new(Arc::clone(&backend));
1250
+ async fn non_global_tracked_write_creates_one_commit_and_advances_only_touched_branch() {
1251
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1114
1252
  let binary_cas = BinaryCasContext::new();
1115
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1116
- crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
1117
- .await;
1118
- crate::test_support::seed_version_head(storage.clone(), "version-a", "version-a-before")
1253
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1254
+ crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "global-before")
1119
1255
  .await;
1256
+ crate::test_support::seed_branch_head(storage.clone(), "branch-a", "branch-a-before").await;
1120
1257
 
1121
- let mut transaction = storage
1122
- .begin_write_transaction()
1123
- .await
1124
- .expect("transaction should open");
1125
- let state_rows = vec![tracked_version_row("version-a", "change-version-a")];
1126
- commit_prepared_writes(
1258
+ let mut read = storage
1259
+ .begin_read(StorageReadOptions::default())
1260
+ .expect("read should open");
1261
+ let state_rows = vec![tracked_branch_row("branch-a", "change-branch-a")];
1262
+ let writes = commit_prepared_writes(
1127
1263
  &binary_cas,
1128
- &crate::commit_store::CommitStoreContext::new(),
1129
- &version_ctx,
1264
+ &branch_ctx,
1130
1265
  None,
1131
- transaction.as_mut(),
1266
+ &mut read,
1132
1267
  PreparedWriteSet {
1133
1268
  insert_identities: BTreeMap::new(),
1134
1269
  state_rows,
1135
- adopted_rows: Vec::new(),
1136
- commit_members_by_version: BTreeMap::from([(
1137
- "version-a".to_string(),
1138
- members(["change-version-a"]),
1270
+ commit_change_refs_by_branch: BTreeMap::from([(
1271
+ "branch-a".to_string(),
1272
+ change_refs(["change-branch-a"]),
1139
1273
  )]),
1140
- extra_commit_parents_by_version: BTreeMap::new(),
1274
+ extra_commit_parents_by_branch: BTreeMap::new(),
1141
1275
  file_data_writes: Vec::new(),
1142
1276
  },
1143
1277
  )
1144
1278
  .await
1145
- .expect("version commit should flush");
1146
- transaction
1147
- .commit()
1148
- .await
1149
- .expect("commit should persist kv");
1150
-
1151
- let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
1152
- let commit = commit_reader
1153
- .load_commit("test-uuid-1")
1279
+ .expect("branch commit should flush");
1280
+ storage
1281
+ .commit_write_set(writes, StorageWriteOptions::default())
1282
+ .expect("writes should commit");
1283
+
1284
+ let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
1285
+ storage
1286
+ .begin_read(StorageReadOptions::default())
1287
+ .expect("read should open"),
1288
+ );
1289
+ let commits = changelog_reader
1290
+ .load_commits(crate::changelog::CommitLoadRequest {
1291
+ commit_ids: &["test-uuid-1".to_string()],
1292
+ projection: crate::changelog::CommitProjection::Record,
1293
+ })
1154
1294
  .await
1155
- .expect("commit-store commit should load")
1156
- .expect("commit-store commit should exist");
1295
+ .expect("changelog commit should load");
1296
+ let Some(crate::changelog::CommitLoadEntry::Record(commit)) =
1297
+ commits.entries.into_iter().next().flatten()
1298
+ else {
1299
+ panic!("changelog commit should exist");
1300
+ };
1157
1301
  assert_eq!(commit.change_id, "test-uuid-2");
1158
- assert_eq!(commit.parent_ids, vec!["version-a-before"]);
1159
- let index_entries = commit_reader
1160
- .load_change_index_entries(&["change-version-a".to_string()])
1161
- .await
1162
- .expect("commit-store change index should load");
1163
- assert!(matches!(
1164
- index_entries.as_slice(),
1165
- [Some(ChangeIndexEntry::PackedChange { .. })]
1166
- ));
1302
+ assert_eq!(commit.parent_commit_ids, vec!["branch-a-before"]);
1167
1303
 
1168
- let global_head = version_ctx
1169
- .ref_reader(storage.clone())
1170
- .load_head_commit_id(GLOBAL_VERSION_ID)
1304
+ let global_head = branch_ctx
1305
+ .ref_reader(
1306
+ storage
1307
+ .begin_read(StorageReadOptions::default())
1308
+ .expect("read should open"),
1309
+ )
1310
+ .load_head_commit_id(GLOBAL_BRANCH_ID)
1171
1311
  .await
1172
1312
  .expect("global head should load");
1173
- let version_head = version_ctx
1174
- .ref_reader(storage.clone())
1175
- .load_head_commit_id("version-a")
1313
+ let branch_head = branch_ctx
1314
+ .ref_reader(
1315
+ storage
1316
+ .begin_read(StorageReadOptions::default())
1317
+ .expect("read should open"),
1318
+ )
1319
+ .load_head_commit_id("branch-a")
1176
1320
  .await
1177
- .expect("version head should load");
1321
+ .expect("branch head should load");
1178
1322
  assert_eq!(global_head.as_deref(), Some("global-before"));
1179
- assert_eq!(version_head.as_deref(), Some("test-uuid-1"));
1323
+ assert_eq!(branch_head.as_deref(), Some("test-uuid-1"));
1180
1324
  }
1181
1325
 
1182
1326
  #[tokio::test]
1183
- async fn finalize_commit_rows_parents_global_commit_to_existing_version_ref() {
1184
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1185
- let storage = StorageContext::new(Arc::clone(&backend));
1186
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1187
- crate::test_support::seed_version_head(
1188
- storage.clone(),
1189
- GLOBAL_VERSION_ID,
1190
- "initial-commit",
1191
- )
1192
- .await;
1327
+ async fn finalize_commit_rows_parents_global_commit_to_existing_branch_ref() {
1328
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1329
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1330
+ crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "initial-commit")
1331
+ .await;
1193
1332
 
1194
- let mut transaction = storage
1195
- .begin_write_transaction()
1196
- .await
1197
- .expect("transaction should open");
1333
+ let mut read = storage
1334
+ .begin_read(StorageReadOptions::default())
1335
+ .expect("read should open");
1198
1336
  let rows = finalize_commit_rows(
1199
1337
  BTreeMap::from([(
1200
- GLOBAL_VERSION_ID.to_string(),
1201
- members(["change-a", "change-b"]),
1338
+ GLOBAL_BRANCH_ID.to_string(),
1339
+ change_refs(["change-a", "change-b"]),
1202
1340
  )]),
1203
1341
  BTreeMap::new(),
1204
- &version_ctx,
1205
- transaction.as_mut(),
1342
+ &branch_ctx,
1343
+ &mut read,
1206
1344
  )
1207
1345
  .await
1208
1346
  .expect("global commit row should finalize");
1209
1347
 
1210
1348
  assert_eq!(rows.commit_rows.len(), 1);
1211
- assert_eq!(rows.version_heads.len(), 1);
1349
+ assert_eq!(rows.branch_heads.len(), 1);
1212
1350
  let row = &rows.commit_rows[0];
1213
1351
  assert_eq!(row.commit_id, "test-uuid-1");
1214
1352
  assert_eq!(row.change_id, "test-uuid-2");
1215
1353
  assert_eq!(row.created_at, "test-timestamp-1");
1216
1354
  assert_eq!(row.parent_commit_ids, vec!["initial-commit"]);
1217
1355
 
1218
- let version_head = &rows.version_heads[0];
1219
- assert_eq!(version_head.version_id, GLOBAL_VERSION_ID);
1220
- assert_eq!(version_head.commit_id, "test-uuid-1");
1356
+ let branch_head = &rows.branch_heads[0];
1357
+ assert_eq!(branch_head.branch_id, GLOBAL_BRANCH_ID);
1358
+ assert_eq!(branch_head.commit_id, "test-uuid-1");
1221
1359
  }
1222
1360
 
1223
1361
  #[tokio::test]
1224
1362
  async fn finalize_commit_rows_skips_empty_members() {
1225
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1226
- let storage = StorageContext::new(Arc::clone(&backend));
1227
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1228
- let mut transaction = storage
1229
- .begin_write_transaction()
1230
- .await
1231
- .expect("transaction should open");
1363
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1364
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1365
+ let mut read = storage
1366
+ .begin_read(StorageReadOptions::default())
1367
+ .expect("read should open");
1232
1368
  let rows = finalize_commit_rows(
1233
1369
  BTreeMap::from([(
1234
- GLOBAL_VERSION_ID.to_string(),
1235
- StagedCommitMembers::default(),
1370
+ GLOBAL_BRANCH_ID.to_string(),
1371
+ StagedCommitChangeRefs::default(),
1236
1372
  )]),
1237
1373
  BTreeMap::new(),
1238
- &version_ctx,
1239
- transaction.as_mut(),
1374
+ &branch_ctx,
1375
+ &mut read,
1240
1376
  )
1241
1377
  .await
1242
- .expect("empty members should be ignored");
1378
+ .expect("empty change_refs should be ignored");
1243
1379
 
1244
1380
  assert!(rows.commit_rows.is_empty());
1245
- assert!(rows.version_heads.is_empty());
1381
+ assert!(rows.branch_heads.is_empty());
1246
1382
  }
1247
1383
 
1248
1384
  #[tokio::test]
1249
- async fn finalize_commit_rows_uses_existing_version_ref_as_parent() {
1250
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1251
- let storage = StorageContext::new(Arc::clone(&backend));
1252
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1253
- crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
1254
- .await;
1255
- crate::test_support::seed_version_head(storage.clone(), "version-a", "previous-commit")
1385
+ async fn finalize_commit_rows_uses_existing_branch_ref_as_parent() {
1386
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1387
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1388
+ crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "global-before")
1256
1389
  .await;
1390
+ crate::test_support::seed_branch_head(storage.clone(), "branch-a", "previous-commit").await;
1257
1391
 
1258
- let mut transaction = storage
1259
- .begin_write_transaction()
1260
- .await
1261
- .expect("transaction should open");
1392
+ let mut read = storage
1393
+ .begin_read(StorageReadOptions::default())
1394
+ .expect("read should open");
1262
1395
  let rows = finalize_commit_rows(
1263
- BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
1396
+ BTreeMap::from([("branch-a".to_string(), change_refs(["change-a"]))]),
1264
1397
  BTreeMap::new(),
1265
- &version_ctx,
1266
- transaction.as_mut(),
1398
+ &branch_ctx,
1399
+ &mut read,
1267
1400
  )
1268
1401
  .await
1269
- .expect("active-version commit finalization should resolve parent");
1402
+ .expect("active-branch commit finalization should resolve parent");
1270
1403
 
1271
1404
  assert_eq!(
1272
1405
  rows.commit_rows[0].parent_commit_ids,
1273
1406
  vec!["previous-commit"]
1274
1407
  );
1275
- assert_eq!(rows.version_heads[0].version_id, "version-a");
1408
+ assert_eq!(rows.branch_heads[0].branch_id, "branch-a");
1276
1409
  }
1277
1410
 
1278
1411
  #[tokio::test]
1279
1412
  async fn finalize_commit_rows_appends_extra_merge_parent_after_target_head() {
1280
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1281
- let storage = StorageContext::new(Arc::clone(&backend));
1282
- let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1283
- crate::test_support::seed_version_head(storage.clone(), "version-a", "target-head").await;
1413
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1414
+ let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1415
+ crate::test_support::seed_branch_head(storage.clone(), "branch-a", "target-head").await;
1284
1416
 
1285
- let mut transaction = storage
1286
- .begin_write_transaction()
1287
- .await
1288
- .expect("transaction should open");
1417
+ let mut read = storage
1418
+ .begin_read(StorageReadOptions::default())
1419
+ .expect("read should open");
1289
1420
  let rows = finalize_commit_rows(
1290
- BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
1291
- BTreeMap::from([("version-a".to_string(), vec!["source-head".to_string()])]),
1292
- &version_ctx,
1293
- transaction.as_mut(),
1421
+ BTreeMap::from([("branch-a".to_string(), change_refs(["change-a"]))]),
1422
+ BTreeMap::from([("branch-a".to_string(), vec!["source-head".to_string()])]),
1423
+ &branch_ctx,
1424
+ &mut read,
1294
1425
  )
1295
1426
  .await
1296
1427
  .expect("merge commit finalization should resolve parents");
@@ -1301,27 +1432,27 @@ mod tests {
1301
1432
  );
1302
1433
  }
1303
1434
 
1304
- fn members<const N: usize>(change_ids: [&str; N]) -> StagedCommitMembers {
1305
- let mut members = StagedCommitMembers::new(
1435
+ fn change_refs<const N: usize>(change_ids: [&str; N]) -> StagedCommitChangeRefs {
1436
+ let mut change_refs = StagedCommitChangeRefs::new(
1306
1437
  "test-uuid-1".to_string(),
1307
1438
  "test-uuid-2".to_string(),
1308
1439
  "test-timestamp-1".to_string(),
1309
1440
  );
1310
1441
  for change_id in change_ids {
1311
- members.add_change_id(change_id.to_string());
1442
+ change_refs.add_change_id(change_id.to_string());
1312
1443
  }
1313
- members
1444
+ change_refs
1314
1445
  }
1315
1446
 
1316
1447
  fn tracked_global_row(change_id: &str) -> PreparedStateRow {
1317
- tracked_version_row(GLOBAL_VERSION_ID, change_id)
1448
+ tracked_branch_row(GLOBAL_BRANCH_ID, change_id)
1318
1449
  }
1319
1450
 
1320
- fn tracked_version_row(version_id: &str, change_id: &str) -> PreparedStateRow {
1451
+ fn tracked_branch_row(branch_id: &str, change_id: &str) -> PreparedStateRow {
1321
1452
  PreparedStateRow {
1322
1453
  schema_plan_id: SchemaPlanId::for_test(0),
1323
1454
  facts: PreparedRowFacts::default(),
1324
- entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
1455
+ entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
1325
1456
  schema_key: "test_schema".to_string(),
1326
1457
  file_id: None,
1327
1458
  snapshot: Some(
@@ -1337,11 +1468,11 @@ mod tests {
1337
1468
  origin: None,
1338
1469
  created_at: "2026-01-01T00:00:00Z".to_string(),
1339
1470
  updated_at: "2026-01-01T00:00:00Z".to_string(),
1340
- global: version_id == GLOBAL_VERSION_ID,
1471
+ global: branch_id == GLOBAL_BRANCH_ID,
1341
1472
  change_id: Some(change_id.to_string()),
1342
1473
  commit_id: Some("test-uuid-1".to_string()),
1343
1474
  untracked: false,
1344
- version_id: version_id.to_string(),
1475
+ branch_id: branch_id.to_string(),
1345
1476
  }
1346
1477
  }
1347
1478
 
@@ -1367,8 +1498,8 @@ mod tests {
1367
1498
  fn untracked_request() -> UntrackedStateRowRequest {
1368
1499
  UntrackedStateRowRequest {
1369
1500
  schema_key: "test_schema".to_string(),
1370
- version_id: GLOBAL_VERSION_ID.to_string(),
1371
- entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
1501
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
1502
+ entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
1372
1503
  file_id: NullableKeyFilter::Null,
1373
1504
  }
1374
1505
  }
@@ -1376,21 +1507,21 @@ mod tests {
1376
1507
  fn live_state_request() -> LiveStateRowRequest {
1377
1508
  LiveStateRowRequest {
1378
1509
  schema_key: "test_schema".to_string(),
1379
- version_id: GLOBAL_VERSION_ID.to_string(),
1380
- entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
1510
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
1511
+ entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
1381
1512
  file_id: NullableKeyFilter::Null,
1382
1513
  }
1383
1514
  }
1384
1515
 
1385
1516
  struct CountingBackend {
1386
- inner: UnitTestBackend,
1517
+ inner: InMemoryStorageBackend,
1387
1518
  write_batches: Arc<AtomicUsize>,
1388
1519
  }
1389
1520
 
1390
1521
  impl CountingBackend {
1391
1522
  fn new() -> Self {
1392
1523
  Self {
1393
- inner: UnitTestBackend::new(),
1524
+ inner: InMemoryStorageBackend::new(),
1394
1525
  write_batches: Arc::new(AtomicUsize::new(0)),
1395
1526
  }
1396
1527
  }
@@ -1400,85 +1531,62 @@ mod tests {
1400
1531
  }
1401
1532
  }
1402
1533
 
1403
- #[async_trait]
1404
1534
  impl Backend for CountingBackend {
1405
- async fn begin_read_transaction(
1406
- &self,
1407
- ) -> Result<Box<dyn BackendReadTransaction + Send + Sync + 'static>, LixError> {
1408
- self.inner.begin_read_transaction().await
1535
+ type Read<'a>
1536
+ = InMemoryStorageRead
1537
+ where
1538
+ Self: 'a;
1539
+
1540
+ type Write<'a>
1541
+ = CountingWrite
1542
+ where
1543
+ Self: 'a;
1544
+
1545
+ fn capabilities(&self) -> BackendCapabilities {
1546
+ self.inner.capabilities()
1409
1547
  }
1410
1548
 
1411
- async fn begin_write_transaction(
1412
- &self,
1413
- ) -> Result<Box<dyn BackendWriteTransaction + Send + Sync + 'static>, LixError> {
1414
- Ok(Box::new(CountingWriteTransaction {
1415
- inner: self.inner.begin_write_transaction().await?,
1416
- write_batches: Arc::clone(&self.write_batches),
1417
- }))
1549
+ fn begin_read(&self, opts: StorageReadOptions) -> Result<Self::Read<'_>, BackendError> {
1550
+ self.inner.begin_read(opts)
1418
1551
  }
1419
- }
1420
-
1421
- struct CountingWriteTransaction {
1422
- inner: Box<dyn BackendWriteTransaction + Send + Sync + 'static>,
1423
- write_batches: Arc<AtomicUsize>,
1424
- }
1425
1552
 
1426
- #[async_trait]
1427
- impl BackendReadTransaction for CountingWriteTransaction {
1428
- async fn get_values(
1429
- &mut self,
1430
- request: BackendKvGetRequest,
1431
- ) -> Result<BackendKvValueBatch, LixError> {
1432
- self.inner.get_values(request).await
1553
+ fn begin_write(&self, opts: StorageWriteOptions) -> Result<Self::Write<'_>, BackendError> {
1554
+ Ok(CountingWrite {
1555
+ inner: self.inner.begin_write(opts)?,
1556
+ write_batches: Arc::clone(&self.write_batches),
1557
+ })
1433
1558
  }
1434
1559
 
1435
- async fn exists_many(
1436
- &mut self,
1437
- request: BackendKvGetRequest,
1438
- ) -> Result<BackendKvExistsBatch, LixError> {
1439
- self.inner.exists_many(request).await
1560
+ fn durable_write_lock(&self) -> DurableWriteLock {
1561
+ self.inner.durable_write_lock()
1440
1562
  }
1563
+ }
1441
1564
 
1442
- async fn scan_keys(
1443
- &mut self,
1444
- request: BackendKvScanRequest,
1445
- ) -> Result<BackendKvKeyPage, LixError> {
1446
- self.inner.scan_keys(request).await
1447
- }
1565
+ struct CountingWrite {
1566
+ inner: InMemoryStorageWrite,
1567
+ write_batches: Arc<AtomicUsize>,
1568
+ }
1448
1569
 
1449
- async fn scan_values(
1450
- &mut self,
1451
- request: BackendKvScanRequest,
1452
- ) -> Result<BackendKvValuePage, LixError> {
1453
- self.inner.scan_values(request).await
1570
+ impl BackendWrite for CountingWrite {
1571
+ fn put_many(&mut self, entries: PutBatch) -> Result<(), BackendError> {
1572
+ self.inner.put_many(entries)
1454
1573
  }
1455
1574
 
1456
- async fn scan_entries(
1457
- &mut self,
1458
- request: BackendKvScanRequest,
1459
- ) -> Result<BackendKvEntryPage, LixError> {
1460
- self.inner.scan_entries(request).await
1575
+ fn delete_many(&mut self, keys: &[StorageKey]) -> Result<(), BackendError> {
1576
+ self.inner.delete_many(keys)
1461
1577
  }
1462
1578
 
1463
- async fn rollback(self: Box<Self>) -> Result<(), LixError> {
1464
- let Self { inner, .. } = *self;
1465
- inner.rollback().await
1579
+ fn delete_range(&mut self, range: KeyRange) -> Result<(), BackendError> {
1580
+ self.inner.delete_range(range)
1466
1581
  }
1467
- }
1468
1582
 
1469
- #[async_trait]
1470
- impl BackendWriteTransaction for CountingWriteTransaction {
1471
- async fn write_kv_batch(
1472
- &mut self,
1473
- batch: BackendKvWriteBatch,
1474
- ) -> Result<BackendKvWriteStats, LixError> {
1583
+ fn commit(self) -> Result<CommitResult, BackendError> {
1475
1584
  self.write_batches.fetch_add(1, Ordering::SeqCst);
1476
- self.inner.write_kv_batch(batch).await
1585
+ self.inner.commit()
1477
1586
  }
1478
1587
 
1479
- async fn commit(self: Box<Self>) -> Result<(), LixError> {
1480
- let Self { inner, .. } = *self;
1481
- inner.commit().await
1588
+ fn rollback(self) -> Result<(), BackendError> {
1589
+ self.inner.rollback()
1482
1590
  }
1483
1591
  }
1484
1592
  }