@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,24 +1,24 @@
1
1
  use async_trait::async_trait;
2
2
  use tokio::sync::Mutex;
3
3
 
4
+ use crate::branch::BRANCH_REF_SCHEMA_KEY;
4
5
  use crate::commit_graph::CommitGraphContext;
5
- use crate::entity_identity::EntityIdentity;
6
- use crate::live_state::visibility;
6
+ use crate::entity_pk::EntityPk;
7
7
  use crate::live_state::{
8
- LiveStateReader, LiveStateRowRequest, LiveStateScanRequest, MaterializedLiveStateRow,
8
+ expanded_branch_ids, resolve_visible_rows, LiveStateReader, LiveStateRowRequest,
9
+ LiveStateScanRequest, MaterializedLiveStateRow, VisibilityBranchScope, VisibilityRequest,
9
10
  };
10
- use crate::storage::StorageReader;
11
+ use crate::storage::StorageRead;
11
12
  use crate::tracked_state::{
12
- MaterializedTrackedStateRow, TrackedStateContext, TrackedStateFilter, TrackedStateProjection,
13
- TrackedStateRowRequest, TrackedStateScanRequest,
13
+ MaterializedTrackedStateRow, TrackedStateContext, TrackedStateFilter, TrackedStateReadColumns,
14
+ TrackedStateScanRequest,
14
15
  };
15
16
  use crate::untracked_state::{
16
17
  UntrackedStateContext, UntrackedStateRowRequest, UntrackedStateScanRequest,
17
18
  };
18
- use crate::version::VERSION_REF_SCHEMA_KEY;
19
19
  use crate::LixError;
20
20
  use crate::NullableKeyFilter;
21
- use crate::GLOBAL_VERSION_ID;
21
+ use crate::GLOBAL_BRANCH_ID;
22
22
 
23
23
  const COMMIT_SCHEMA_KEY: &str = "lix_commit";
24
24
  const COMMIT_EDGE_SCHEMA_KEY: &str = "lix_commit_edge";
@@ -50,7 +50,7 @@ impl LiveStateContext {
50
50
  /// Creates a visible live-state reader over a caller-provided KV store.
51
51
  pub(crate) fn reader<S>(&self, store: S) -> LiveStateStoreReader<S>
52
52
  where
53
- S: StorageReader,
53
+ S: StorageRead + Send + Sync,
54
54
  {
55
55
  LiveStateStoreReader {
56
56
  store: Mutex::new(store),
@@ -71,46 +71,45 @@ pub(crate) struct LiveStateStoreReader<S> {
71
71
 
72
72
  impl<S> LiveStateStoreReader<S>
73
73
  where
74
- S: StorageReader,
74
+ S: StorageRead + Send + Sync,
75
75
  {
76
76
  pub(crate) async fn scan_rows(
77
77
  &self,
78
78
  request: &LiveStateScanRequest,
79
79
  ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
80
- let mut store = self.store.lock().await;
81
- let scope = scan_scope(&mut *store, &self.untracked_state, request).await?;
80
+ let store = self.store.lock().await;
81
+ let scope = scan_scope(&*store, &self.untracked_state, request).await?;
82
82
  let derived_rows =
83
- scan_commit_derived_rows(&mut *store, &self.commit_graph, request, &scope).await?;
83
+ scan_commit_derived_rows(&*store, &self.commit_graph, request, &scope).await?;
84
84
  let mut tracked_rows = Vec::new();
85
85
  if request.filter.untracked != Some(true) && !is_commit_derived_only_request(request) {
86
- for version_id in &scope.storage_version_ids {
86
+ for branch_id in &scope.storage_branch_ids {
87
87
  let Some(commit_id) =
88
- load_version_ref_commit_id(&mut *store, &self.untracked_state, version_id)
89
- .await?
88
+ load_branch_ref_commit_id(&*store, &self.untracked_state, branch_id).await?
90
89
  else {
91
90
  continue;
92
91
  };
93
92
  let tracked_request = tracked_scan_request_from_live(request);
94
- let source = tracked_source_from_version_id(version_id);
95
- let store: &mut dyn StorageReader = &mut *store;
93
+ let source = tracked_source_from_branch_id(branch_id);
94
+ let store = &*store;
96
95
  tracked_rows.extend(
97
96
  self.tracked_state
98
97
  .reader(store)
99
98
  .scan_rows_at_commit(&commit_id, &tracked_request)
100
99
  .await?
101
100
  .into_iter()
102
- .map(|row| project_tracked_row(row, version_id, source)),
101
+ .map(|row| project_tracked_row(row, branch_id, source)),
103
102
  );
104
103
  }
105
104
  }
106
105
 
107
106
  let untracked_rows = if request.filter.untracked != Some(false) {
108
- let store: &mut dyn StorageReader = &mut *store;
107
+ let store = &*store;
109
108
  self.untracked_state
110
109
  .reader(store)
111
110
  .scan_rows(&untracked_scan_request_from_live(
112
111
  request,
113
- &scope.storage_version_ids,
112
+ &scope.storage_branch_ids,
114
113
  ))
115
114
  .await?
116
115
  .into_iter()
@@ -132,14 +131,17 @@ where
132
131
  .chain(derived_rows)
133
132
  .collect()
134
133
  };
135
- rows = visibility::resolve_scan_rows(
134
+ rows = resolve_visible_rows(
136
135
  rows,
137
- &scope.projection_version_ids,
138
- request.filter.include_tombstones,
136
+ Vec::new(),
137
+ &VisibilityRequest {
138
+ branch_scope: VisibilityBranchScope::BranchIds {
139
+ branch_ids: scope.projection_branch_ids.clone(),
140
+ },
141
+ include_tombstones: request.filter.include_tombstones,
142
+ limit: request.limit,
143
+ },
139
144
  );
140
- if let Some(limit) = request.limit {
141
- rows.truncate(limit);
142
- }
143
145
  Ok(rows)
144
146
  }
145
147
 
@@ -147,95 +149,34 @@ where
147
149
  &self,
148
150
  request: &LiveStateRowRequest,
149
151
  ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
150
- let mut store = self.store.lock().await;
151
- if !version_ref_exists(&mut *store, &self.untracked_state, &request.version_id).await? {
152
- return Ok(None);
153
- }
154
- if is_commit_derived_schema(&request.schema_key)
155
- && request.file_id == NullableKeyFilter::Null
156
152
  {
157
- let scope = LiveStateScanScope {
158
- storage_version_ids: vec![request.version_id.clone()],
159
- projection_version_ids: vec![request.version_id.clone()],
160
- };
161
- let rows = scan_commit_derived_rows(
162
- &mut *store,
163
- &self.commit_graph,
164
- &LiveStateScanRequest {
165
- filter: crate::live_state::LiveStateFilter {
166
- schema_keys: vec![request.schema_key.clone()],
167
- entity_ids: vec![request.entity_id.clone()],
168
- version_ids: vec![request.version_id.clone()],
169
- file_ids: vec![NullableKeyFilter::Null],
170
- untracked: Some(false),
171
- include_tombstones: false,
172
- ..Default::default()
173
- },
174
- limit: Some(1),
153
+ let store = self.store.lock().await;
154
+ if !branch_ref_exists(&*store, &self.untracked_state, &request.branch_id).await? {
155
+ return Ok(None);
156
+ }
157
+ }
158
+ let rows = self
159
+ .scan_rows(&LiveStateScanRequest {
160
+ filter: crate::live_state::LiveStateFilter {
161
+ schema_keys: vec![request.schema_key.clone()],
162
+ entity_pks: vec![request.entity_pk.clone()],
163
+ branch_ids: vec![request.branch_id.clone()],
164
+ file_ids: vec![request.file_id.clone()],
165
+ include_tombstones: false,
175
166
  ..Default::default()
176
167
  },
177
- &scope,
178
- )
168
+ limit: Some(1),
169
+ ..Default::default()
170
+ })
179
171
  .await?;
180
- if let Some(row) = rows.into_iter().next() {
181
- return Ok(Some(row));
182
- }
183
- }
184
- for candidate in load_row_candidates(request) {
185
- match candidate.source {
186
- LiveStateLookupSource::Untracked => {
187
- let store: &mut dyn StorageReader = &mut *store;
188
- if let Some(row) = self
189
- .untracked_state
190
- .reader(store)
191
- .load_row(&untracked_row_request_from_live(
192
- request,
193
- &candidate.version_id,
194
- ))
195
- .await?
196
- {
197
- return Ok(Some(visibility::project_loaded_row(
198
- MaterializedLiveStateRow::from(row),
199
- &request.version_id,
200
- &candidate.version_id,
201
- )));
202
- }
203
- }
204
- LiveStateLookupSource::Tracked => {
205
- let Some(commit_id) = load_version_ref_commit_id(
206
- &mut *store,
207
- &self.untracked_state,
208
- &candidate.version_id,
209
- )
210
- .await?
211
- else {
212
- continue;
213
- };
214
- let store: &mut dyn StorageReader = &mut *store;
215
- let tracked_request = tracked_row_request_from_live(request);
216
- let mut rows = self
217
- .tracked_state
218
- .reader(store)
219
- .load_rows_at_commit(&commit_id, &[tracked_request])
220
- .await?;
221
- if let Some(row) = rows.pop().flatten() {
222
- return Ok(Some(project_tracked_row(
223
- row,
224
- &request.version_id,
225
- tracked_source_from_version_id(&candidate.version_id),
226
- )));
227
- }
228
- }
229
- }
230
- }
231
- Ok(None)
172
+ Ok(rows.into_iter().next())
232
173
  }
233
174
  }
234
175
 
235
176
  #[async_trait]
236
177
  impl<S> LiveStateReader for LiveStateStoreReader<S>
237
178
  where
238
- S: StorageReader + Sync,
179
+ S: StorageRead + Send + Sync,
239
180
  {
240
181
  async fn scan_rows(
241
182
  &self,
@@ -253,7 +194,7 @@ where
253
194
  }
254
195
 
255
196
  async fn scan_commit_derived_rows(
256
- store: &mut dyn StorageReader,
197
+ store: &(impl StorageRead + Send + Sync + ?Sized),
257
198
  commit_graph: &CommitGraphContext,
258
199
  request: &LiveStateScanRequest,
259
200
  scope: &LiveStateScanScope,
@@ -265,10 +206,10 @@ async fn scan_commit_derived_rows(
265
206
  return Ok(Vec::new());
266
207
  }
267
208
 
268
- let version_ids = if scope.projection_version_ids.is_empty() {
269
- vec![GLOBAL_VERSION_ID.to_string()]
209
+ let branch_ids = if scope.projection_branch_ids.is_empty() {
210
+ vec![GLOBAL_BRANCH_ID.to_string()]
270
211
  } else {
271
- scope.projection_version_ids.clone()
212
+ scope.projection_branch_ids.clone()
272
213
  };
273
214
  let mut graph = commit_graph.reader(store);
274
215
  let commits = graph.all_commits().await?;
@@ -277,23 +218,23 @@ async fn scan_commit_derived_rows(
277
218
  schema_filter_allows(&request.filter.schema_keys, COMMIT_EDGE_SCHEMA_KEY);
278
219
 
279
220
  let mut rows = Vec::new();
280
- for version_id in &version_ids {
221
+ for branch_id in &branch_ids {
281
222
  if include_commit {
282
223
  for commit in &commits {
283
- rows.push(commit_row(commit, version_id)?);
224
+ rows.push(commit_row(commit, branch_id)?);
284
225
  }
285
226
  }
286
227
  if include_commit_edge {
287
228
  for edge in graph.commit_edges(&commits) {
288
- rows.push(commit_edge_row(&edge, version_id)?);
229
+ rows.push(commit_edge_row(&edge, branch_id)?);
289
230
  }
290
231
  }
291
232
  }
292
233
 
293
234
  rows.retain(|row| {
294
- (request.filter.entity_ids.is_empty() || request.filter.entity_ids.contains(&row.entity_id))
295
- && (request.filter.version_ids.is_empty()
296
- || request.filter.version_ids.contains(&row.version_id))
235
+ (request.filter.entity_pks.is_empty() || request.filter.entity_pks.contains(&row.entity_pk))
236
+ && (request.filter.branch_ids.is_empty()
237
+ || request.filter.branch_ids.contains(&row.branch_id))
297
238
  });
298
239
  Ok(rows)
299
240
  }
@@ -333,7 +274,7 @@ fn file_filter_allows_null(file_ids: &[NullableKeyFilter<String>]) -> bool {
333
274
 
334
275
  fn commit_row(
335
276
  commit: &crate::commit_graph::CommitGraphCommit,
336
- version_id: &str,
277
+ branch_id: &str,
337
278
  ) -> Result<MaterializedLiveStateRow, LixError> {
338
279
  let snapshot_content = serde_json::to_string(&serde_json::json!({
339
280
  "id": commit.commit_id,
@@ -345,7 +286,7 @@ fn commit_row(
345
286
  )
346
287
  })?;
347
288
  Ok(MaterializedLiveStateRow {
348
- entity_id: EntityIdentity::single(commit.commit_id.clone()),
289
+ entity_pk: EntityPk::single(commit.commit_id.clone()),
349
290
  schema_key: COMMIT_SCHEMA_KEY.to_string(),
350
291
  file_id: None,
351
292
  snapshot_content: Some(snapshot_content),
@@ -357,13 +298,13 @@ fn commit_row(
357
298
  change_id: Some(commit.change.id.clone()),
358
299
  commit_id: Some(commit.commit_id.clone()),
359
300
  untracked: false,
360
- version_id: version_id.to_string(),
301
+ branch_id: branch_id.to_string(),
361
302
  })
362
303
  }
363
304
 
364
305
  fn commit_edge_row(
365
306
  edge: &crate::commit_graph::CommitGraphEdge,
366
- version_id: &str,
307
+ branch_id: &str,
367
308
  ) -> Result<MaterializedLiveStateRow, LixError> {
368
309
  let snapshot_content = serde_json::to_string(&serde_json::json!({
369
310
  "parent_id": edge.parent_commit_id,
@@ -377,7 +318,7 @@ fn commit_edge_row(
377
318
  )
378
319
  })?;
379
320
  Ok(MaterializedLiveStateRow {
380
- entity_id: EntityIdentity {
321
+ entity_pk: EntityPk {
381
322
  parts: vec![edge.parent_commit_id.clone(), edge.child_commit_id.clone()],
382
323
  },
383
324
  schema_key: COMMIT_EDGE_SCHEMA_KEY.to_string(),
@@ -391,7 +332,7 @@ fn commit_edge_row(
391
332
  change_id: None,
392
333
  commit_id: Some(edge.child_commit_id.clone()),
393
334
  untracked: false,
394
- version_id: version_id.to_string(),
335
+ branch_id: branch_id.to_string(),
395
336
  })
396
337
  }
397
338
 
@@ -399,13 +340,13 @@ fn tracked_scan_request_from_live(request: &LiveStateScanRequest) -> TrackedStat
399
340
  TrackedStateScanRequest {
400
341
  filter: TrackedStateFilter {
401
342
  schema_keys: request.filter.schema_keys.clone(),
402
- entity_ids: request.filter.entity_ids.clone(),
343
+ entity_pks: request.filter.entity_pks.clone(),
403
344
  file_ids: request.filter.file_ids.clone(),
404
- // Scan tombstones internally so version-local tombstones can hide
345
+ // Scan tombstones internally so branch-local tombstones can hide
405
346
  // global fallback rows before the serving facade filters them.
406
347
  include_tombstones: true,
407
348
  },
408
- projection: TrackedStateProjection {
349
+ read_columns: TrackedStateReadColumns {
409
350
  columns: request.projection.columns.clone(),
410
351
  },
411
352
  limit: None,
@@ -414,10 +355,10 @@ fn tracked_scan_request_from_live(request: &LiveStateScanRequest) -> TrackedStat
414
355
 
415
356
  fn untracked_scan_request_from_live(
416
357
  request: &LiveStateScanRequest,
417
- version_ids: &[String],
358
+ branch_ids: &[String],
418
359
  ) -> UntrackedStateScanRequest {
419
360
  let mut filter: crate::untracked_state::UntrackedStateFilter = request.filter.clone().into();
420
- filter.version_ids = version_ids.to_vec();
361
+ filter.branch_ids = branch_ids.to_vec();
421
362
  UntrackedStateScanRequest {
422
363
  filter,
423
364
  projection: crate::untracked_state::UntrackedStateProjection {
@@ -429,67 +370,67 @@ fn untracked_scan_request_from_live(
429
370
 
430
371
  #[derive(Debug, Clone, PartialEq, Eq)]
431
372
  struct LiveStateScanScope {
432
- storage_version_ids: Vec<String>,
433
- projection_version_ids: Vec<String>,
373
+ storage_branch_ids: Vec<String>,
374
+ projection_branch_ids: Vec<String>,
434
375
  }
435
376
 
436
377
  async fn scan_scope(
437
- store: &mut dyn StorageReader,
378
+ store: &(impl StorageRead + Send + Sync + ?Sized),
438
379
  untracked_state: &UntrackedStateContext,
439
380
  request: &LiveStateScanRequest,
440
381
  ) -> Result<LiveStateScanScope, LixError> {
441
- if request.filter.version_ids.is_empty() {
382
+ if request.filter.branch_ids.is_empty() {
442
383
  return Ok(LiveStateScanScope {
443
- storage_version_ids: all_version_ref_ids(store, untracked_state).await?,
444
- projection_version_ids: Vec::new(),
384
+ storage_branch_ids: all_branch_ref_ids(store, untracked_state).await?,
385
+ projection_branch_ids: Vec::new(),
445
386
  });
446
387
  }
447
388
 
448
- let mut projection_version_ids = Vec::new();
449
- for version_id in &request.filter.version_ids {
450
- if version_ref_exists(store, untracked_state, version_id).await? {
451
- projection_version_ids.push(version_id.clone());
389
+ let mut projection_branch_ids = Vec::new();
390
+ for branch_id in &request.filter.branch_ids {
391
+ if branch_ref_exists(store, untracked_state, branch_id).await? {
392
+ projection_branch_ids.push(branch_id.clone());
452
393
  }
453
394
  }
454
395
 
455
- let storage_version_ids = visibility::expanded_version_ids(&projection_version_ids);
396
+ let storage_branch_ids = expanded_branch_ids(&projection_branch_ids);
456
397
  Ok(LiveStateScanScope {
457
- storage_version_ids,
458
- projection_version_ids,
398
+ storage_branch_ids,
399
+ projection_branch_ids,
459
400
  })
460
401
  }
461
402
 
462
- async fn all_version_ref_ids(
463
- store: &mut dyn StorageReader,
403
+ async fn all_branch_ref_ids(
404
+ store: &(impl StorageRead + Send + Sync + ?Sized),
464
405
  untracked_state: &UntrackedStateContext,
465
406
  ) -> Result<Vec<String>, LixError> {
466
407
  let rows = untracked_state
467
408
  .reader(store)
468
409
  .scan_rows(&UntrackedStateScanRequest {
469
410
  filter: crate::untracked_state::UntrackedStateFilter {
470
- schema_keys: vec![VERSION_REF_SCHEMA_KEY.to_string()],
471
- version_ids: vec![GLOBAL_VERSION_ID.to_string()],
411
+ schema_keys: vec![BRANCH_REF_SCHEMA_KEY.to_string()],
412
+ branch_ids: vec![GLOBAL_BRANCH_ID.to_string()],
472
413
  ..Default::default()
473
414
  },
474
415
  ..Default::default()
475
416
  })
476
417
  .await?;
477
418
  rows.into_iter()
478
- .map(|row| row.entity_id.as_single_string_owned())
419
+ .map(|row| row.entity_pk.as_single_string_owned())
479
420
  .collect()
480
421
  }
481
422
 
482
- async fn load_version_ref_commit_id(
483
- store: &mut dyn StorageReader,
423
+ async fn load_branch_ref_commit_id(
424
+ store: &(impl StorageRead + Send + Sync + ?Sized),
484
425
  untracked_state: &UntrackedStateContext,
485
- version_id: &str,
426
+ branch_id: &str,
486
427
  ) -> Result<Option<String>, LixError> {
487
428
  let Some(row) = untracked_state
488
429
  .reader(store)
489
430
  .load_row(&UntrackedStateRowRequest {
490
- schema_key: VERSION_REF_SCHEMA_KEY.to_string(),
491
- version_id: GLOBAL_VERSION_ID.to_string(),
492
- entity_id: crate::entity_identity::EntityIdentity::single(version_id),
431
+ schema_key: BRANCH_REF_SCHEMA_KEY.to_string(),
432
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
433
+ entity_pk: crate::entity_pk::EntityPk::single(branch_id),
493
434
  file_id: crate::NullableKeyFilter::Null,
494
435
  })
495
436
  .await?
@@ -503,7 +444,7 @@ async fn load_version_ref_commit_id(
503
444
  serde_json::from_str::<serde_json::Value>(snapshot_content).map_err(|error| {
504
445
  LixError::new(
505
446
  "LIX_ERROR_UNKNOWN",
506
- format!("live_state version-ref snapshot parse failed: {error}"),
447
+ format!("live_state branch-ref snapshot parse failed: {error}"),
507
448
  )
508
449
  })?;
509
450
  Ok(snapshot
@@ -512,39 +453,37 @@ async fn load_version_ref_commit_id(
512
453
  .map(str::to_string))
513
454
  }
514
455
 
515
- async fn version_ref_exists(
516
- store: &mut dyn StorageReader,
456
+ async fn branch_ref_exists(
457
+ store: &(impl StorageRead + Send + Sync + ?Sized),
517
458
  untracked_state: &UntrackedStateContext,
518
- version_id: &str,
459
+ branch_id: &str,
519
460
  ) -> Result<bool, LixError> {
520
- Ok(
521
- load_version_ref_commit_id(store, untracked_state, version_id)
522
- .await?
523
- .is_some(),
524
- )
461
+ Ok(load_branch_ref_commit_id(store, untracked_state, branch_id)
462
+ .await?
463
+ .is_some())
525
464
  }
526
465
 
527
466
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
528
467
  enum TrackedRowSource {
529
468
  Global,
530
- Version,
469
+ Branch,
531
470
  }
532
471
 
533
- fn tracked_source_from_version_id(version_id: &str) -> TrackedRowSource {
534
- if version_id == GLOBAL_VERSION_ID {
472
+ fn tracked_source_from_branch_id(branch_id: &str) -> TrackedRowSource {
473
+ if branch_id == GLOBAL_BRANCH_ID {
535
474
  TrackedRowSource::Global
536
475
  } else {
537
- TrackedRowSource::Version
476
+ TrackedRowSource::Branch
538
477
  }
539
478
  }
540
479
 
541
480
  fn project_tracked_row(
542
481
  row: MaterializedTrackedStateRow,
543
- view_version_id: &str,
482
+ view_branch_id: &str,
544
483
  source: TrackedRowSource,
545
484
  ) -> MaterializedLiveStateRow {
546
485
  MaterializedLiveStateRow {
547
- entity_id: row.entity_id,
486
+ entity_pk: row.entity_pk,
548
487
  schema_key: row.schema_key,
549
488
  file_id: row.file_id,
550
489
  snapshot_content: row.snapshot_content,
@@ -556,83 +495,18 @@ fn project_tracked_row(
556
495
  change_id: Some(row.change_id),
557
496
  commit_id: Some(row.commit_id),
558
497
  untracked: false,
559
- version_id: view_version_id.to_string(),
560
- }
561
- }
562
-
563
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
564
- enum LiveStateLookupSource {
565
- Untracked,
566
- Tracked,
567
- }
568
-
569
- #[derive(Debug, Clone, PartialEq, Eq)]
570
- struct LiveStateLookupCandidate {
571
- source: LiveStateLookupSource,
572
- version_id: String,
573
- }
574
-
575
- fn load_row_candidates(request: &LiveStateRowRequest) -> Vec<LiveStateLookupCandidate> {
576
- let mut candidates = vec![
577
- LiveStateLookupCandidate {
578
- source: LiveStateLookupSource::Untracked,
579
- version_id: request.version_id.clone(),
580
- },
581
- LiveStateLookupCandidate {
582
- source: LiveStateLookupSource::Tracked,
583
- version_id: request.version_id.clone(),
584
- },
585
- ];
586
-
587
- if request.version_id != GLOBAL_VERSION_ID {
588
- candidates.extend([
589
- LiveStateLookupCandidate {
590
- source: LiveStateLookupSource::Untracked,
591
- version_id: GLOBAL_VERSION_ID.to_string(),
592
- },
593
- LiveStateLookupCandidate {
594
- source: LiveStateLookupSource::Tracked,
595
- version_id: GLOBAL_VERSION_ID.to_string(),
596
- },
597
- ]);
598
- }
599
-
600
- candidates
601
- }
602
-
603
- fn untracked_row_request_from_live(
604
- request: &LiveStateRowRequest,
605
- version_id: &str,
606
- ) -> crate::untracked_state::UntrackedStateRowRequest {
607
- crate::untracked_state::UntrackedStateRowRequest {
608
- schema_key: request.schema_key.clone(),
609
- version_id: version_id.to_string(),
610
- entity_id: request.entity_id.clone(),
611
- file_id: request.file_id.clone(),
612
- }
613
- }
614
-
615
- fn tracked_row_request_from_live(request: &LiveStateRowRequest) -> TrackedStateRowRequest {
616
- TrackedStateRowRequest {
617
- schema_key: request.schema_key.clone(),
618
- entity_id: request.entity_id.clone(),
619
- file_id: request.file_id.clone(),
498
+ branch_id: view_branch_id.to_string(),
620
499
  }
621
500
  }
622
501
 
623
502
  #[cfg(test)]
624
503
  mod tests {
625
- use std::sync::Arc;
626
-
627
504
  use super::*;
628
- use crate::backend::{testing::UnitTestBackend, Backend};
629
- use crate::commit_store::{CommitDraftRef, CommitStoreContext};
630
- use crate::entity_identity::EntityIdentity;
631
- use crate::json_store::{
632
- JsonStoreContext, JsonWritePlacementRef, NormalizedJson, NormalizedJsonRef,
633
- };
505
+ use crate::entity_pk::EntityPk;
506
+ use crate::json_store::{JsonRef, JsonStoreContext, JsonWritePlacementRef, NormalizedJsonRef};
634
507
  use crate::live_state::LiveStateFilter;
635
- use crate::storage::{StorageContext, StorageWriteSet, StorageWriteTransaction};
508
+ use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
509
+ use crate::storage::{StorageContext, StorageWriteSet};
636
510
  use crate::tracked_state::{TrackedStateDeltaRef, TrackedStateScanRequest};
637
511
  use crate::untracked_state::{MaterializedUntrackedStateRow, UntrackedStateContext};
638
512
  use crate::NullableKeyFilter;
@@ -649,10 +523,11 @@ mod tests {
649
523
  }
650
524
 
651
525
  async fn write_untracked_rows_to_store(
652
- store: &mut (impl StorageWriteTransaction + ?Sized),
526
+ storage: &StorageContext,
527
+ _read: &(impl crate::storage::StorageRead + Send + Sync + ?Sized),
653
528
  rows: &[MaterializedUntrackedStateRow],
654
529
  ) {
655
- let mut writes = StorageWriteSet::new();
530
+ let mut writes = storage.new_write_set();
656
531
  let canonical_rows = rows
657
532
  .iter()
658
533
  .map(|row| crate::test_support::untracked_state_row_from_materialized(&mut writes, row))
@@ -662,51 +537,92 @@ mod tests {
662
537
  .writer(&mut writes)
663
538
  .stage_rows(canonical_rows.iter().map(|row| row.as_ref()))
664
539
  .expect("untracked rows should write");
665
- writes
666
- .apply(store)
667
- .await
668
- .expect("untracked rows should apply");
540
+ storage
541
+ .commit_write_set(writes, StorageWriteOptions::default())
542
+ .expect("untracked rows should commit");
669
543
  }
670
544
 
671
545
  async fn write_empty_commits_to_store(
672
- store: &mut (impl StorageWriteTransaction + ?Sized),
546
+ storage: &StorageContext,
547
+ read: &(impl crate::storage::StorageRead + Send + Sync),
673
548
  commit_ids: &[&str],
674
549
  ) {
675
- let mut writes = StorageWriteSet::new();
550
+ let mut writes = storage.new_write_set();
551
+ let mut json_writer = JsonStoreContext::new().writer();
552
+ let mut append = crate::changelog::ChangelogAppend::default();
676
553
  for commit_id in commit_ids {
677
554
  let commit_change_id = format!("{commit_id}:commit");
678
- CommitStoreContext::new()
679
- .writer(&mut *store, &mut writes)
680
- .stage_commit_draft(
681
- CommitDraftRef {
682
- id: commit_id,
683
- change_id: &commit_change_id,
684
- parent_ids: &[],
685
- author_account_ids: &[],
686
- created_at: "1970-01-01T00:00:00.000Z",
687
- },
688
- Vec::new(),
689
- Vec::new(),
555
+ append.commits.push(crate::changelog::CommitRecord {
556
+ format_version: 1,
557
+ commit_id: (*commit_id).to_string(),
558
+ parent_commit_ids: Vec::new(),
559
+ change_id: commit_change_id.clone(),
560
+ author_account_ids: Vec::new(),
561
+ created_at: "1970-01-01T00:00:00.000Z".to_string(),
562
+ });
563
+ append
564
+ .commit_change_refs
565
+ .push(crate::changelog::CommitChangeRefSet {
566
+ commit_id: (*commit_id).to_string(),
567
+ entries: Vec::new(),
568
+ });
569
+ }
570
+ let mut changelog_read = read;
571
+ let mut writer =
572
+ crate::changelog::ChangelogContext::new().writer(&mut changelog_read, &mut writes);
573
+ crate::changelog::ChangelogWriter::stage_append(&mut writer, append)
574
+ .await
575
+ .expect("empty changelog commits should stage");
576
+ drop(writer);
577
+ for commit_id in commit_ids {
578
+ let snapshot_content =
579
+ commit_row_snapshot_content(commit_id).expect("commit snapshot should encode");
580
+ let snapshot_ref = JsonRef::for_content(snapshot_content.as_bytes());
581
+ json_writer
582
+ .stage_batch(
583
+ &mut writes,
584
+ JsonWritePlacementRef::OutOfBand,
585
+ [NormalizedJsonRef::trusted_prehashed(
586
+ &snapshot_content,
587
+ snapshot_ref.clone(),
588
+ )],
690
589
  )
590
+ .expect("commit snapshot should stage");
591
+ let change_id = format!("{commit_id}:commit");
592
+ let entity_pk = EntityPk::single(*commit_id);
593
+ let deltas = [TrackedStateDeltaRef {
594
+ schema_key: COMMIT_SCHEMA_KEY,
595
+ file_id: None,
596
+ entity_pk: &entity_pk,
597
+ change_id: &change_id,
598
+ commit_id,
599
+ snapshot_ref: Some(&snapshot_ref),
600
+ metadata_ref: None,
601
+ deleted: false,
602
+ created_at: "1970-01-01T00:00:00.000Z",
603
+ updated_at: "1970-01-01T00:00:00.000Z",
604
+ }];
605
+ TrackedStateContext::new()
606
+ .writer(read, &mut writes)
607
+ .stage_commit_root(commit_id, None, deltas)
691
608
  .await
692
- .expect("empty commit should stage");
609
+ .expect("empty tracked roots should stage");
693
610
  }
694
- writes
695
- .apply(store)
696
- .await
697
- .expect("empty commits should apply");
611
+ storage
612
+ .commit_write_set(writes, StorageWriteOptions::default())
613
+ .expect("empty commits should commit");
698
614
  }
699
615
 
700
616
  async fn stage_materialized_live_rows(
701
- store: &mut (impl StorageReader + ?Sized),
617
+ store: &(impl StorageRead + Send + Sync),
702
618
  writes: &mut StorageWriteSet,
703
- _json_writer: &mut crate::json_store::JsonStoreWriter,
619
+ json_writer: &mut crate::json_store::JsonStoreWriter,
704
620
  rows: &[MaterializedLiveStateRow],
705
621
  ) -> Result<(), LixError> {
706
622
  let mut untracked_rows = Vec::new();
707
623
  let mut tracked_rows_by_commit = std::collections::BTreeMap::<
708
624
  String,
709
- Vec<(crate::commit_store::Change, String, String)>,
625
+ Vec<(crate::changelog::ChangeRecord, String, String)>,
710
626
  >::new();
711
627
  let mut parent_by_commit = std::collections::BTreeMap::<String, Option<String>>::new();
712
628
 
@@ -732,7 +648,7 @@ mod tests {
732
648
  }
733
649
  if row.schema_key != COMMIT_SCHEMA_KEY {
734
650
  let change = crate::test_support::tracked_change_from_materialized(&materialized)?;
735
- stage_tracked_materialized_json(writes, &commit_id, &materialized)?;
651
+ stage_json_payloads_from_materialized(writes, json_writer, &materialized)?;
736
652
  tracked_rows_by_commit.entry(commit_id).or_default().push((
737
653
  change,
738
654
  materialized.created_at,
@@ -750,69 +666,125 @@ mod tests {
750
666
  .as_ref()
751
667
  .map(|parent| vec![parent.clone()])
752
668
  .unwrap_or_default();
669
+ let commit_created_at = rows
670
+ .first()
671
+ .map(|(change, _, _)| change.created_at.as_str())
672
+ .unwrap_or("1970-01-01T00:00:00.000Z")
673
+ .to_string();
674
+ let change_refs = rows
675
+ .iter()
676
+ .map(|(change, _, _)| crate::changelog::CommitChangeRef {
677
+ schema_key: change.schema_key.clone(),
678
+ file_id: change.file_id.clone(),
679
+ entity_pk: change.entity_pk.clone(),
680
+ change_id: change.change_id.clone(),
681
+ })
682
+ .collect::<Vec<_>>();
753
683
  let commit_change_id = format!("{commit_id}:commit");
754
- let commit = CommitDraftRef {
755
- id: &commit_id,
756
- change_id: &commit_change_id,
757
- parent_ids: &parent_ids,
758
- author_account_ids: &[],
759
- created_at: rows
760
- .first()
761
- .map(|(change, _, _)| change.created_at.as_str())
762
- .unwrap_or("1970-01-01T00:00:00.000Z"),
763
- };
764
- let staged = CommitStoreContext::new()
765
- .writer(&mut *store, writes)
766
- .stage_tracked_commit_draft(
767
- commit,
768
- rows.iter().map(|(change, _, _)| change.as_ref()).collect(),
769
- Vec::new(),
770
- )
771
- .await?;
772
- let deltas = rows
684
+ let mut append = crate::changelog::ChangelogAppend::default();
685
+ append
686
+ .changes
687
+ .extend(rows.iter().map(|(change, _, _)| change.clone()));
688
+ append.commits.push(crate::changelog::CommitRecord {
689
+ format_version: 1,
690
+ commit_id: commit_id.clone(),
691
+ parent_commit_ids: parent_ids,
692
+ change_id: commit_change_id.clone(),
693
+ author_account_ids: Vec::new(),
694
+ created_at: commit_created_at.clone(),
695
+ });
696
+ append
697
+ .commit_change_refs
698
+ .push(crate::changelog::CommitChangeRefSet {
699
+ commit_id: commit_id.clone(),
700
+ entries: change_refs,
701
+ });
702
+ let mut changelog_read = store;
703
+ let mut writer =
704
+ crate::changelog::ChangelogContext::new().writer(&mut changelog_read, writes);
705
+ crate::changelog::ChangelogWriter::stage_append(&mut writer, append).await?;
706
+ drop(writer);
707
+ let snapshot_content = commit_row_snapshot_content(&commit_id)?;
708
+ let snapshot_ref = JsonRef::for_content(snapshot_content.as_bytes());
709
+ json_writer.stage_batch(
710
+ writes,
711
+ JsonWritePlacementRef::OutOfBand,
712
+ [NormalizedJsonRef::trusted_prehashed(
713
+ &snapshot_content,
714
+ snapshot_ref.clone(),
715
+ )],
716
+ )?;
717
+ let commit_entity_pk = EntityPk::single(&commit_id);
718
+ let mut deltas = rows
773
719
  .iter()
774
- .zip(&staged.authored_locators)
775
- .map(
776
- |((change, created_at, updated_at), locator)| TrackedStateDeltaRef {
777
- change: change.as_ref(),
778
- locator: locator.as_ref(),
779
- created_at,
780
- updated_at,
781
- },
782
- )
720
+ .map(|(change, created_at, updated_at)| TrackedStateDeltaRef {
721
+ schema_key: &change.schema_key,
722
+ file_id: change.file_id.as_deref(),
723
+ entity_pk: &change.entity_pk,
724
+ change_id: &change.change_id,
725
+ commit_id: &commit_id,
726
+ snapshot_ref: change.snapshot_ref.as_ref(),
727
+ metadata_ref: change.metadata_ref.as_ref(),
728
+ deleted: change.snapshot_ref.is_none(),
729
+ created_at,
730
+ updated_at,
731
+ })
783
732
  .collect::<Vec<_>>();
733
+ deltas.push(TrackedStateDeltaRef {
734
+ schema_key: COMMIT_SCHEMA_KEY,
735
+ file_id: None,
736
+ entity_pk: &commit_entity_pk,
737
+ change_id: &commit_change_id,
738
+ commit_id: &commit_id,
739
+ snapshot_ref: Some(&snapshot_ref),
740
+ metadata_ref: None,
741
+ deleted: false,
742
+ created_at: &commit_created_at,
743
+ updated_at: &commit_created_at,
744
+ });
784
745
  TrackedStateContext::new()
785
- .writer(&mut *store, writes)
786
- .stage_delta(&commit_id, parent_commit_id.as_deref(), &deltas)
746
+ .writer(&*store, writes)
747
+ .stage_commit_root(&commit_id, parent_commit_id.as_deref(), deltas)
787
748
  .await?;
788
749
  }
789
750
  Ok(())
790
751
  }
791
752
 
792
- fn stage_tracked_materialized_json(
753
+ fn commit_row_snapshot_content(commit_id: &str) -> Result<String, LixError> {
754
+ serde_json::to_string(&json!({ "id": commit_id })).map_err(|error| {
755
+ LixError::new(
756
+ "LIX_ERROR_UNKNOWN",
757
+ format!("failed to encode test commit snapshot: {error}"),
758
+ )
759
+ })
760
+ }
761
+
762
+ fn stage_json_payloads_from_materialized(
793
763
  writes: &mut StorageWriteSet,
794
- commit_id: &str,
764
+ json_writer: &mut crate::json_store::JsonStoreWriter,
795
765
  row: &MaterializedTrackedStateRow,
796
766
  ) -> Result<(), LixError> {
797
- let mut payloads = Vec::new();
798
767
  if let Some(snapshot) = row.snapshot_content.as_deref() {
799
- payloads.push(NormalizedJson::from_arc_unchecked(Arc::from(snapshot)));
768
+ json_writer.stage_batch(
769
+ writes,
770
+ JsonWritePlacementRef::OutOfBand,
771
+ [NormalizedJsonRef::trusted_prehashed(
772
+ snapshot,
773
+ JsonRef::for_content(snapshot.as_bytes()),
774
+ )],
775
+ )?;
800
776
  }
801
777
  if let Some(metadata) = row.metadata.as_ref() {
802
- payloads.push(NormalizedJson::from_arc_unchecked(Arc::from(
803
- crate::serialize_row_metadata(metadata),
804
- )));
778
+ let serialized = crate::serialize_row_metadata(metadata);
779
+ json_writer.stage_batch(
780
+ writes,
781
+ JsonWritePlacementRef::OutOfBand,
782
+ [NormalizedJsonRef::trusted_prehashed(
783
+ &serialized,
784
+ JsonRef::for_content(serialized.as_bytes()),
785
+ )],
786
+ )?;
805
787
  }
806
- JsonStoreContext::new().writer().stage_batch(
807
- writes,
808
- JsonWritePlacementRef::CommitPack {
809
- commit_id,
810
- pack_id: 0,
811
- },
812
- payloads
813
- .iter()
814
- .map(|payload| NormalizedJsonRef::from(payload)),
815
- )?;
816
788
  Ok(())
817
789
  }
818
790
 
@@ -838,20 +810,18 @@ mod tests {
838
810
 
839
811
  #[tokio::test]
840
812
  async fn live_state_overlays_untracked_rows() {
841
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
842
- let storage = StorageContext::new(Arc::clone(&backend));
813
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
843
814
  let live_state = live_state_context();
844
815
 
845
- let mut transaction = storage
846
- .begin_write_transaction()
847
- .await
848
- .expect("transaction should open");
816
+ let read = storage
817
+ .begin_read(StorageReadOptions::default())
818
+ .expect("read should open");
849
819
  {
850
820
  let mut writes = StorageWriteSet::new();
851
821
  let mut json_writer = JsonStoreContext::new().writer();
852
822
  {
853
823
  stage_materialized_live_rows(
854
- transaction.as_mut(),
824
+ &read,
855
825
  &mut writes,
856
826
  &mut json_writer,
857
827
  &[tracked_row_with_commit(
@@ -863,22 +833,21 @@ mod tests {
863
833
  .await
864
834
  .expect("tracked row should stage");
865
835
  }
866
- writes
867
- .apply(&mut transaction.as_mut())
868
- .await
869
- .expect("tracked row should apply");
836
+ storage
837
+ .commit_write_set(writes, StorageWriteOptions::default())
838
+ .expect("writes should commit");
870
839
  }
871
840
  write_untracked_rows_to_store(
872
- transaction.as_mut(),
841
+ &storage,
842
+ &read,
873
843
  &[
874
- version_ref_row("global", "commit-tracked"),
844
+ branch_ref_row("global", "commit-tracked"),
875
845
  untracked_row("untracked-value"),
876
846
  ],
877
847
  )
878
848
  .await;
879
- transaction.commit().await.expect("commit should persist");
880
849
 
881
- let rows = scan_selected_tab_at(&live_state, storage.clone(), "global", false)
850
+ let rows = scan_selected_tab_at(&live_state, &storage, "global", false)
882
851
  .await
883
852
  .expect("scan should succeed");
884
853
  assert_eq!(rows.len(), 1);
@@ -890,11 +859,15 @@ mod tests {
890
859
  assert_eq!(rows[0].change_id, None);
891
860
 
892
861
  let loaded = live_state
893
- .reader(storage.clone())
862
+ .reader(
863
+ storage
864
+ .begin_read(StorageReadOptions::default())
865
+ .expect("read should open"),
866
+ )
894
867
  .load_row(&LiveStateRowRequest {
895
868
  schema_key: "lix_key_value".to_string(),
896
- version_id: "global".to_string(),
897
- entity_id: crate::entity_identity::EntityIdentity::single("selected-tab"),
869
+ branch_id: "global".to_string(),
870
+ entity_pk: crate::entity_pk::EntityPk::single("selected-tab"),
898
871
  file_id: NullableKeyFilter::Null,
899
872
  })
900
873
  .await
@@ -909,20 +882,18 @@ mod tests {
909
882
 
910
883
  #[tokio::test]
911
884
  async fn tracked_row_is_visible_without_untracked_overlay() {
912
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
913
- let storage = StorageContext::new(Arc::clone(&backend));
885
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
914
886
  let live_state = live_state_context();
915
887
 
916
- let mut transaction = storage
917
- .begin_write_transaction()
918
- .await
919
- .expect("transaction should open");
888
+ let read = storage
889
+ .begin_read(StorageReadOptions::default())
890
+ .expect("read should open");
920
891
  {
921
892
  let mut writes = StorageWriteSet::new();
922
893
  let mut json_writer = JsonStoreContext::new().writer();
923
894
  {
924
895
  stage_materialized_live_rows(
925
- transaction.as_mut(),
896
+ &read,
926
897
  &mut writes,
927
898
  &mut json_writer,
928
899
  &[tracked_row_with_commit(
@@ -934,19 +905,18 @@ mod tests {
934
905
  .await
935
906
  .expect("tracked row should stage");
936
907
  }
937
- writes
938
- .apply(&mut transaction.as_mut())
939
- .await
940
- .expect("tracked row should apply");
908
+ storage
909
+ .commit_write_set(writes, StorageWriteOptions::default())
910
+ .expect("writes should commit");
941
911
  }
942
912
  write_untracked_rows_to_store(
943
- transaction.as_mut(),
944
- &[version_ref_row("global", "commit-tracked")],
913
+ &storage,
914
+ &read,
915
+ &[branch_ref_row("global", "commit-tracked")],
945
916
  )
946
917
  .await;
947
- transaction.commit().await.expect("commit should persist");
948
918
 
949
- let loaded = load_selected_tab(&live_state, storage.clone())
919
+ let loaded = load_selected_tab(&live_state, &storage)
950
920
  .await
951
921
  .expect("load should succeed")
952
922
  .expect("tracked row should be visible");
@@ -960,20 +930,18 @@ mod tests {
960
930
 
961
931
  #[tokio::test]
962
932
  async fn deleting_untracked_row_reveals_tracked_row() {
963
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
964
- let storage = StorageContext::new(Arc::clone(&backend));
933
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
965
934
  let live_state = live_state_context();
966
935
 
967
- let mut transaction = storage
968
- .begin_write_transaction()
969
- .await
970
- .expect("transaction should open");
936
+ let read = storage
937
+ .begin_read(StorageReadOptions::default())
938
+ .expect("read should open");
971
939
  {
972
940
  let mut writes = StorageWriteSet::new();
973
941
  let mut json_writer = JsonStoreContext::new().writer();
974
942
  {
975
943
  stage_materialized_live_rows(
976
- transaction.as_mut(),
944
+ &read,
977
945
  &mut writes,
978
946
  &mut json_writer,
979
947
  &[tracked_row_with_commit(
@@ -985,15 +953,15 @@ mod tests {
985
953
  .await
986
954
  .expect("tracked row should stage");
987
955
  }
988
- writes
989
- .apply(&mut transaction.as_mut())
990
- .await
991
- .expect("tracked row should apply");
956
+ storage
957
+ .commit_write_set(writes, StorageWriteOptions::default())
958
+ .expect("writes should commit");
992
959
  }
993
960
  write_untracked_rows_to_store(
994
- transaction.as_mut(),
961
+ &storage,
962
+ &read,
995
963
  &[
996
- version_ref_row("global", "commit-tracked"),
964
+ branch_ref_row("global", "commit-tracked"),
997
965
  untracked_row("untracked-value"),
998
966
  ],
999
967
  )
@@ -1001,22 +969,21 @@ mod tests {
1001
969
  {
1002
970
  let mut writes = StorageWriteSet::new();
1003
971
  let identity = crate::untracked_state::UntrackedStateIdentity {
1004
- version_id: "global".to_string(),
972
+ branch_id: "global".to_string(),
1005
973
  schema_key: "lix_key_value".to_string(),
1006
- entity_id: EntityIdentity::single("selected-tab"),
974
+ entity_pk: EntityPk::single("selected-tab"),
1007
975
  file_id: None,
1008
976
  };
1009
977
  UntrackedStateContext::new()
1010
978
  .writer(&mut writes)
1011
- .stage_delete_rows(std::iter::once(identity.as_ref()));
1012
- writes
1013
- .apply(&mut transaction.as_mut())
1014
- .await
1015
- .expect("untracked row should delete");
979
+ .stage_delete_rows(std::iter::once(identity.as_ref()))
980
+ .expect("delete identity should stage");
981
+ storage
982
+ .commit_write_set(writes, StorageWriteOptions::default())
983
+ .expect("writes should commit");
1016
984
  }
1017
- transaction.commit().await.expect("commit should persist");
1018
985
 
1019
- let loaded = load_selected_tab(&live_state, storage.clone())
986
+ let loaded = load_selected_tab(&live_state, &storage)
1020
987
  .await
1021
988
  .expect("load should succeed")
1022
989
  .expect("tracked row should be visible again");
@@ -1029,15 +996,13 @@ mod tests {
1029
996
  }
1030
997
 
1031
998
  #[tokio::test]
1032
- async fn load_row_falls_back_to_global_tracked_row_for_requested_version() {
1033
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1034
- let storage = StorageContext::new(Arc::clone(&backend));
999
+ async fn load_row_falls_back_to_global_tracked_row_for_requested_branch() {
1000
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1035
1001
  let live_state = live_state_context();
1036
1002
 
1037
- let mut transaction = storage
1038
- .begin_write_transaction()
1039
- .await
1040
- .expect("transaction should open");
1003
+ let read = storage
1004
+ .begin_read(StorageReadOptions::default())
1005
+ .expect("read should open");
1041
1006
  {
1042
1007
  let rows = [tracked_row_with_commit(
1043
1008
  "global-tracked",
@@ -1047,37 +1012,31 @@ mod tests {
1047
1012
  let mut writes = StorageWriteSet::new();
1048
1013
  let mut json_writer = JsonStoreContext::new().writer();
1049
1014
  {
1050
- stage_materialized_live_rows(
1051
- transaction.as_mut(),
1052
- &mut writes,
1053
- &mut json_writer,
1054
- &rows,
1055
- )
1056
- .await
1057
- .expect("tracked row should stage");
1015
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1016
+ .await
1017
+ .expect("tracked row should stage");
1058
1018
  }
1059
- writes
1060
- .apply(&mut transaction.as_mut())
1061
- .await
1062
- .expect("tracked row should apply");
1019
+ storage
1020
+ .commit_write_set(writes, StorageWriteOptions::default())
1021
+ .expect("writes should commit");
1063
1022
  }
1064
1023
  write_untracked_rows_to_store(
1065
- transaction.as_mut(),
1024
+ &storage,
1025
+ &read,
1066
1026
  &[
1067
- version_ref_row("global", "commit-global"),
1068
- version_ref_row("version-a", "commit-version-a"),
1027
+ branch_ref_row("global", "commit-global"),
1028
+ branch_ref_row("branch-a", "commit-branch-a"),
1069
1029
  ],
1070
1030
  )
1071
1031
  .await;
1072
- write_empty_commits_to_store(transaction.as_mut(), &["commit-version-a"]).await;
1073
- transaction.commit().await.expect("commit should persist");
1032
+ write_empty_commits_to_store(&storage, &read, &["commit-branch-a"]).await;
1074
1033
 
1075
- let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
1034
+ let loaded = load_selected_tab_at(&live_state, &storage, "branch-a")
1076
1035
  .await
1077
1036
  .expect("load should succeed")
1078
- .expect("global row should be visible for requested version");
1037
+ .expect("global row should be visible for requested branch");
1079
1038
 
1080
- assert_eq!(loaded.version_id, "version-a");
1039
+ assert_eq!(loaded.branch_id, "branch-a");
1081
1040
  assert!(loaded.global);
1082
1041
  assert!(!loaded.untracked);
1083
1042
  assert_eq!(
@@ -1088,8 +1047,7 @@ mod tests {
1088
1047
 
1089
1048
  #[tokio::test]
1090
1049
  async fn main_sees_global_row_by_reading_global_root_separately() {
1091
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1092
- let storage = StorageContext::new(Arc::clone(&backend));
1050
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1093
1051
  let tracked_state = TrackedStateContext::new();
1094
1052
  let live_state = LiveStateContext::new(
1095
1053
  tracked_state.clone(),
@@ -1097,10 +1055,9 @@ mod tests {
1097
1055
  crate::commit_graph::CommitGraphContext::new(),
1098
1056
  );
1099
1057
 
1100
- let mut transaction = storage
1101
- .begin_write_transaction()
1102
- .await
1103
- .expect("transaction should open");
1058
+ let read = storage
1059
+ .begin_read(StorageReadOptions::default())
1060
+ .expect("read should open");
1104
1061
  {
1105
1062
  let rows = [tracked_row_with_commit(
1106
1063
  "global-tracked",
@@ -1110,121 +1067,105 @@ mod tests {
1110
1067
  let mut writes = StorageWriteSet::new();
1111
1068
  let mut json_writer = JsonStoreContext::new().writer();
1112
1069
  {
1113
- stage_materialized_live_rows(
1114
- transaction.as_mut(),
1115
- &mut writes,
1116
- &mut json_writer,
1117
- &rows,
1118
- )
1119
- .await
1120
- .expect("global tracked row should stage");
1070
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1071
+ .await
1072
+ .expect("global tracked row should stage");
1121
1073
  }
1122
- writes
1123
- .apply(&mut transaction.as_mut())
1124
- .await
1125
- .expect("global tracked row should apply");
1074
+ storage
1075
+ .commit_write_set(writes, StorageWriteOptions::default())
1076
+ .expect("writes should commit");
1126
1077
  }
1127
1078
  write_untracked_rows_to_store(
1128
- transaction.as_mut(),
1079
+ &storage,
1080
+ &read,
1129
1081
  &[
1130
- version_ref_row("global", "commit-global"),
1131
- version_ref_row("main", "commit-main"),
1082
+ branch_ref_row("global", "commit-global"),
1083
+ branch_ref_row("main", "commit-main"),
1132
1084
  ],
1133
1085
  )
1134
1086
  .await;
1135
- write_empty_commits_to_store(transaction.as_mut(), &["commit-main"]).await;
1136
- transaction.commit().await.expect("commit should persist");
1087
+ write_empty_commits_to_store(&storage, &read, &["commit-main"]).await;
1137
1088
 
1138
- let loaded = load_selected_tab_at(&live_state, storage.clone(), "main")
1089
+ let loaded = load_selected_tab_at(&live_state, &storage, "main")
1139
1090
  .await
1140
1091
  .expect("load should succeed")
1141
1092
  .expect("global row should be projected into main");
1142
- assert_eq!(loaded.version_id, "main");
1093
+ assert_eq!(loaded.branch_id, "main");
1143
1094
  assert!(loaded.global);
1144
1095
  assert_eq!(
1145
1096
  loaded.snapshot_content.as_deref(),
1146
1097
  Some("{\"value\":\"global-tracked\"}")
1147
1098
  );
1148
1099
 
1149
- let main_root_rows =
1150
- scan_tracked_root(&tracked_state, storage.clone(), "commit-main").await;
1100
+ let main_root_rows = scan_tracked_root(&tracked_state, &storage, "commit-main").await;
1151
1101
  assert_eq!(
1152
1102
  main_root_rows.len(),
1153
- 0,
1154
- "global fallback must come from the global root, not a copied main root row"
1103
+ 1,
1104
+ "empty commit root should contain only its derived lix_commit row"
1155
1105
  );
1106
+ assert_eq!(main_root_rows[0].schema_key, "lix_commit");
1156
1107
  }
1157
1108
 
1158
1109
  #[tokio::test]
1159
- async fn load_row_prefers_requested_version_over_global() {
1160
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1161
- let storage = StorageContext::new(Arc::clone(&backend));
1110
+ async fn load_row_prefers_requested_branch_over_global() {
1111
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1162
1112
  let live_state = live_state_context();
1163
1113
 
1164
- let mut transaction = storage
1165
- .begin_write_transaction()
1166
- .await
1167
- .expect("transaction should open");
1114
+ let read = storage
1115
+ .begin_read(StorageReadOptions::default())
1116
+ .expect("read should open");
1168
1117
  {
1169
1118
  let rows = [
1170
1119
  tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
1171
1120
  tracked_row_at_with_commit(
1172
- "version-a",
1173
- "version-tracked",
1174
- Some("change-version"),
1175
- "commit-version",
1121
+ "branch-a",
1122
+ "branch-tracked",
1123
+ Some("change-branch"),
1124
+ "commit-branch",
1176
1125
  ),
1177
1126
  ];
1178
1127
  let mut writes = StorageWriteSet::new();
1179
1128
  let mut json_writer = JsonStoreContext::new().writer();
1180
1129
  {
1181
- stage_materialized_live_rows(
1182
- transaction.as_mut(),
1183
- &mut writes,
1184
- &mut json_writer,
1185
- &rows,
1186
- )
1187
- .await
1188
- .expect("tracked rows should stage");
1130
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1131
+ .await
1132
+ .expect("tracked rows should stage");
1189
1133
  }
1190
- writes
1191
- .apply(&mut transaction.as_mut())
1192
- .await
1193
- .expect("tracked rows should apply");
1134
+ storage
1135
+ .commit_write_set(writes, StorageWriteOptions::default())
1136
+ .expect("writes should commit");
1194
1137
  }
1195
1138
  write_untracked_rows_to_store(
1196
- transaction.as_mut(),
1139
+ &storage,
1140
+ &read,
1197
1141
  &[
1198
- version_ref_row("global", "commit-global"),
1199
- version_ref_row("version-a", "commit-version"),
1142
+ branch_ref_row("global", "commit-global"),
1143
+ branch_ref_row("branch-a", "commit-branch"),
1200
1144
  ],
1201
1145
  )
1202
1146
  .await;
1203
- transaction.commit().await.expect("commit should persist");
1204
1147
 
1205
- let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
1148
+ let loaded = load_selected_tab_at(&live_state, &storage, "branch-a")
1206
1149
  .await
1207
1150
  .expect("load should succeed")
1208
- .expect("version row should be visible");
1151
+ .expect("branch row should be visible");
1209
1152
 
1210
- assert_eq!(loaded.version_id, "version-a");
1153
+ assert_eq!(loaded.branch_id, "branch-a");
1211
1154
  assert!(!loaded.untracked);
1212
1155
  assert_eq!(
1213
1156
  loaded.snapshot_content.as_deref(),
1214
- Some("{\"value\":\"version-tracked\"}")
1157
+ Some("{\"value\":\"branch-tracked\"}")
1215
1158
  );
1216
1159
  }
1217
1160
 
1218
1161
  #[tokio::test]
1219
1162
  async fn main_override_hides_global_row() {
1220
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1221
- let storage = StorageContext::new(Arc::clone(&backend));
1163
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1222
1164
  let live_state = live_state_context();
1223
1165
 
1224
- let mut transaction = storage
1225
- .begin_write_transaction()
1226
- .await
1227
- .expect("transaction should open");
1166
+ let read = storage
1167
+ .begin_read(StorageReadOptions::default())
1168
+ .expect("read should open");
1228
1169
  {
1229
1170
  let rows = [
1230
1171
  tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
@@ -1238,36 +1179,30 @@ mod tests {
1238
1179
  let mut writes = StorageWriteSet::new();
1239
1180
  let mut json_writer = JsonStoreContext::new().writer();
1240
1181
  {
1241
- stage_materialized_live_rows(
1242
- transaction.as_mut(),
1243
- &mut writes,
1244
- &mut json_writer,
1245
- &rows,
1246
- )
1247
- .await
1248
- .expect("tracked rows should stage");
1182
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1183
+ .await
1184
+ .expect("tracked rows should stage");
1249
1185
  }
1250
- writes
1251
- .apply(&mut transaction.as_mut())
1252
- .await
1253
- .expect("tracked rows should apply");
1186
+ storage
1187
+ .commit_write_set(writes, StorageWriteOptions::default())
1188
+ .expect("writes should commit");
1254
1189
  }
1255
1190
  write_untracked_rows_to_store(
1256
- transaction.as_mut(),
1191
+ &storage,
1192
+ &read,
1257
1193
  &[
1258
- version_ref_row("global", "commit-global"),
1259
- version_ref_row("main", "commit-main"),
1194
+ branch_ref_row("global", "commit-global"),
1195
+ branch_ref_row("main", "commit-main"),
1260
1196
  ],
1261
1197
  )
1262
1198
  .await;
1263
- transaction.commit().await.expect("commit should persist");
1264
1199
 
1265
- let loaded = load_selected_tab_at(&live_state, storage.clone(), "main")
1200
+ let loaded = load_selected_tab_at(&live_state, &storage, "main")
1266
1201
  .await
1267
1202
  .expect("load should succeed")
1268
1203
  .expect("main row should be visible");
1269
1204
 
1270
- assert_eq!(loaded.version_id, "main");
1205
+ assert_eq!(loaded.branch_id, "main");
1271
1206
  assert!(!loaded.global);
1272
1207
  assert_eq!(
1273
1208
  loaded.snapshot_content.as_deref(),
@@ -1277,135 +1212,117 @@ mod tests {
1277
1212
 
1278
1213
  #[tokio::test]
1279
1214
  async fn load_row_prefers_requested_untracked_over_requested_tracked_and_global_rows() {
1280
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1281
- let storage = StorageContext::new(Arc::clone(&backend));
1215
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1282
1216
  let live_state = live_state_context();
1283
1217
 
1284
- let mut transaction = storage
1285
- .begin_write_transaction()
1286
- .await
1287
- .expect("transaction should open");
1218
+ let read = storage
1219
+ .begin_read(StorageReadOptions::default())
1220
+ .expect("read should open");
1288
1221
  {
1289
1222
  let rows = [
1290
1223
  tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
1291
1224
  tracked_row_at_with_commit(
1292
- "version-a",
1293
- "version-tracked",
1294
- Some("change-version"),
1295
- "commit-version",
1225
+ "branch-a",
1226
+ "branch-tracked",
1227
+ Some("change-branch"),
1228
+ "commit-branch",
1296
1229
  ),
1297
1230
  ];
1298
1231
  let mut writes = StorageWriteSet::new();
1299
1232
  let mut json_writer = JsonStoreContext::new().writer();
1300
1233
  {
1301
- stage_materialized_live_rows(
1302
- transaction.as_mut(),
1303
- &mut writes,
1304
- &mut json_writer,
1305
- &rows,
1306
- )
1307
- .await
1308
- .expect("tracked rows should stage");
1234
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1235
+ .await
1236
+ .expect("tracked rows should stage");
1309
1237
  }
1310
- writes
1311
- .apply(&mut transaction.as_mut())
1312
- .await
1313
- .expect("tracked rows should apply");
1238
+ storage
1239
+ .commit_write_set(writes, StorageWriteOptions::default())
1240
+ .expect("writes should commit");
1314
1241
  }
1315
1242
  write_untracked_rows_to_store(
1316
- transaction.as_mut(),
1243
+ &storage,
1244
+ &read,
1317
1245
  &[
1318
- version_ref_row("global", "commit-global"),
1319
- version_ref_row("version-a", "commit-version"),
1246
+ branch_ref_row("global", "commit-global"),
1247
+ branch_ref_row("branch-a", "commit-branch"),
1320
1248
  untracked_row_at("global", "global-untracked"),
1321
- untracked_row_at("version-a", "version-untracked"),
1249
+ untracked_row_at("branch-a", "branch-untracked"),
1322
1250
  ],
1323
1251
  )
1324
1252
  .await;
1325
- transaction.commit().await.expect("commit should persist");
1326
1253
 
1327
- let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
1254
+ let loaded = load_selected_tab_at(&live_state, &storage, "branch-a")
1328
1255
  .await
1329
1256
  .expect("load should succeed")
1330
- .expect("version untracked row should be visible");
1257
+ .expect("branch untracked row should be visible");
1331
1258
 
1332
- assert_eq!(loaded.version_id, "version-a");
1259
+ assert_eq!(loaded.branch_id, "branch-a");
1333
1260
  assert!(loaded.untracked);
1334
1261
  assert_eq!(
1335
1262
  loaded.snapshot_content.as_deref(),
1336
- Some("{\"value\":\"version-untracked\"}")
1263
+ Some("{\"value\":\"branch-untracked\"}")
1337
1264
  );
1338
1265
  }
1339
1266
 
1340
1267
  #[tokio::test]
1341
- async fn scan_rows_overlays_requested_version_over_global() {
1342
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1343
- let storage = StorageContext::new(Arc::clone(&backend));
1268
+ async fn scan_rows_overlays_requested_branch_over_global() {
1269
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1344
1270
  let live_state = live_state_context();
1345
1271
 
1346
- let mut transaction = storage
1347
- .begin_write_transaction()
1348
- .await
1349
- .expect("transaction should open");
1272
+ let read = storage
1273
+ .begin_read(StorageReadOptions::default())
1274
+ .expect("read should open");
1350
1275
  {
1351
1276
  let rows = [
1352
1277
  tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
1353
1278
  tracked_row_at_with_commit(
1354
- "version-a",
1355
- "version-tracked",
1356
- Some("change-version"),
1357
- "commit-version",
1279
+ "branch-a",
1280
+ "branch-tracked",
1281
+ Some("change-branch"),
1282
+ "commit-branch",
1358
1283
  ),
1359
1284
  ];
1360
1285
  let mut writes = StorageWriteSet::new();
1361
1286
  let mut json_writer = JsonStoreContext::new().writer();
1362
1287
  {
1363
- stage_materialized_live_rows(
1364
- transaction.as_mut(),
1365
- &mut writes,
1366
- &mut json_writer,
1367
- &rows,
1368
- )
1369
- .await
1370
- .expect("rows should stage");
1288
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1289
+ .await
1290
+ .expect("rows should stage");
1371
1291
  }
1372
- writes
1373
- .apply(&mut transaction.as_mut())
1374
- .await
1375
- .expect("rows should apply");
1292
+ storage
1293
+ .commit_write_set(writes, StorageWriteOptions::default())
1294
+ .expect("writes should commit");
1376
1295
  }
1377
1296
  write_untracked_rows_to_store(
1378
- transaction.as_mut(),
1297
+ &storage,
1298
+ &read,
1379
1299
  &[
1380
- version_ref_row("global", "commit-global"),
1381
- version_ref_row("version-a", "commit-version"),
1300
+ branch_ref_row("global", "commit-global"),
1301
+ branch_ref_row("branch-a", "commit-branch"),
1382
1302
  ],
1383
1303
  )
1384
1304
  .await;
1385
- transaction.commit().await.expect("commit should persist");
1386
1305
 
1387
- let rows = scan_selected_tab_at(&live_state, storage.clone(), "version-a", false)
1306
+ let rows = scan_selected_tab_at(&live_state, &storage, "branch-a", false)
1388
1307
  .await
1389
1308
  .expect("scan should succeed");
1390
1309
 
1391
1310
  assert_eq!(rows.len(), 1);
1392
- assert_eq!(rows[0].version_id, "version-a");
1311
+ assert_eq!(rows[0].branch_id, "branch-a");
1393
1312
  assert_eq!(
1394
1313
  rows[0].snapshot_content.as_deref(),
1395
- Some("{\"value\":\"version-tracked\"}")
1314
+ Some("{\"value\":\"branch-tracked\"}")
1396
1315
  );
1397
1316
  }
1398
1317
 
1399
1318
  #[tokio::test]
1400
- async fn scan_rows_projects_global_row_into_requested_version() {
1401
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1402
- let storage = StorageContext::new(Arc::clone(&backend));
1319
+ async fn scan_rows_projects_global_row_into_requested_branch() {
1320
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1403
1321
  let live_state = live_state_context();
1404
1322
 
1405
- let mut transaction = storage
1406
- .begin_write_transaction()
1407
- .await
1408
- .expect("transaction should open");
1323
+ let read = storage
1324
+ .begin_read(StorageReadOptions::default())
1325
+ .expect("read should open");
1409
1326
  {
1410
1327
  let rows = [tracked_row_with_commit(
1411
1328
  "global-tracked",
@@ -1415,37 +1332,31 @@ mod tests {
1415
1332
  let mut writes = StorageWriteSet::new();
1416
1333
  let mut json_writer = JsonStoreContext::new().writer();
1417
1334
  {
1418
- stage_materialized_live_rows(
1419
- transaction.as_mut(),
1420
- &mut writes,
1421
- &mut json_writer,
1422
- &rows,
1423
- )
1424
- .await
1425
- .expect("rows should stage");
1335
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1336
+ .await
1337
+ .expect("rows should stage");
1426
1338
  }
1427
- writes
1428
- .apply(&mut transaction.as_mut())
1429
- .await
1430
- .expect("rows should apply");
1339
+ storage
1340
+ .commit_write_set(writes, StorageWriteOptions::default())
1341
+ .expect("writes should commit");
1431
1342
  }
1432
1343
  write_untracked_rows_to_store(
1433
- transaction.as_mut(),
1344
+ &storage,
1345
+ &read,
1434
1346
  &[
1435
- version_ref_row("global", "commit-global"),
1436
- version_ref_row("version-a", "commit-version-a"),
1347
+ branch_ref_row("global", "commit-global"),
1348
+ branch_ref_row("branch-a", "commit-branch-a"),
1437
1349
  ],
1438
1350
  )
1439
1351
  .await;
1440
- write_empty_commits_to_store(transaction.as_mut(), &["commit-version-a"]).await;
1441
- transaction.commit().await.expect("commit should persist");
1352
+ write_empty_commits_to_store(&storage, &read, &["commit-branch-a"]).await;
1442
1353
 
1443
- let rows = scan_selected_tab_at(&live_state, storage.clone(), "version-a", false)
1354
+ let rows = scan_selected_tab_at(&live_state, &storage, "branch-a", false)
1444
1355
  .await
1445
1356
  .expect("scan should succeed");
1446
1357
 
1447
1358
  assert_eq!(rows.len(), 1);
1448
- assert_eq!(rows[0].version_id, "version-a");
1359
+ assert_eq!(rows[0].branch_id, "branch-a");
1449
1360
  assert!(rows[0].global);
1450
1361
  assert_eq!(
1451
1362
  rows[0].snapshot_content.as_deref(),
@@ -1454,15 +1365,13 @@ mod tests {
1454
1365
  }
1455
1366
 
1456
1367
  #[tokio::test]
1457
- async fn scan_rows_does_not_project_global_rows_into_missing_version() {
1458
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1459
- let storage = StorageContext::new(Arc::clone(&backend));
1368
+ async fn scan_rows_does_not_project_global_rows_into_missing_branch() {
1369
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1460
1370
  let live_state = live_state_context();
1461
1371
 
1462
- let mut transaction = storage
1463
- .begin_write_transaction()
1464
- .await
1465
- .expect("transaction should open");
1372
+ let read = storage
1373
+ .begin_read(StorageReadOptions::default())
1374
+ .expect("read should open");
1466
1375
  {
1467
1376
  let rows = [tracked_row_with_commit(
1468
1377
  "global-tracked",
@@ -1472,107 +1381,91 @@ mod tests {
1472
1381
  let mut writes = StorageWriteSet::new();
1473
1382
  let mut json_writer = JsonStoreContext::new().writer();
1474
1383
  {
1475
- stage_materialized_live_rows(
1476
- transaction.as_mut(),
1477
- &mut writes,
1478
- &mut json_writer,
1479
- &rows,
1480
- )
1481
- .await
1482
- .expect("tracked row should stage");
1384
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1385
+ .await
1386
+ .expect("tracked row should stage");
1483
1387
  }
1484
- writes
1485
- .apply(&mut transaction.as_mut())
1486
- .await
1487
- .expect("tracked row should apply");
1388
+ storage
1389
+ .commit_write_set(writes, StorageWriteOptions::default())
1390
+ .expect("writes should commit");
1488
1391
  }
1489
1392
  write_untracked_rows_to_store(
1490
- transaction.as_mut(),
1491
- &[version_ref_row("global", "commit-global")],
1393
+ &storage,
1394
+ &read,
1395
+ &[branch_ref_row("global", "commit-global")],
1492
1396
  )
1493
1397
  .await;
1494
- transaction.commit().await.expect("commit should persist");
1495
1398
 
1496
- let rows = scan_selected_tab_at(&live_state, storage.clone(), "missing-version", false)
1399
+ let rows = scan_selected_tab_at(&live_state, &storage, "missing-branch", false)
1497
1400
  .await
1498
1401
  .expect("scan should succeed");
1499
1402
 
1500
1403
  assert_eq!(
1501
1404
  rows.len(),
1502
1405
  0,
1503
- "global rows must not be projected into a missing version scope"
1406
+ "global rows must not be projected into a missing branch scope"
1504
1407
  );
1505
1408
  }
1506
1409
 
1507
1410
  #[tokio::test]
1508
1411
  async fn winning_tombstone_hides_row_unless_tombstones_are_included() {
1509
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1510
- let storage = StorageContext::new(Arc::clone(&backend));
1412
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1511
1413
  let live_state = live_state_context();
1512
1414
 
1513
- let mut transaction = storage
1514
- .begin_write_transaction()
1515
- .await
1516
- .expect("transaction should open");
1415
+ let read = storage
1416
+ .begin_read(StorageReadOptions::default())
1417
+ .expect("read should open");
1517
1418
  {
1518
1419
  let rows = [
1519
1420
  tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
1520
1421
  tombstone_tracked_row_at_with_commit(
1521
- "version-a",
1422
+ "branch-a",
1522
1423
  Some("change-tombstone"),
1523
- "commit-version",
1424
+ "commit-branch",
1524
1425
  ),
1525
1426
  ];
1526
1427
  let mut writes = StorageWriteSet::new();
1527
1428
  let mut json_writer = JsonStoreContext::new().writer();
1528
1429
  {
1529
- stage_materialized_live_rows(
1530
- transaction.as_mut(),
1531
- &mut writes,
1532
- &mut json_writer,
1533
- &rows,
1534
- )
1535
- .await
1536
- .expect("rows should stage");
1430
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1431
+ .await
1432
+ .expect("rows should stage");
1537
1433
  }
1538
- writes
1539
- .apply(&mut transaction.as_mut())
1540
- .await
1541
- .expect("rows should apply");
1434
+ storage
1435
+ .commit_write_set(writes, StorageWriteOptions::default())
1436
+ .expect("writes should commit");
1542
1437
  }
1543
1438
  write_untracked_rows_to_store(
1544
- transaction.as_mut(),
1439
+ &storage,
1440
+ &read,
1545
1441
  &[
1546
- version_ref_row("global", "commit-global"),
1547
- version_ref_row("version-a", "commit-version"),
1442
+ branch_ref_row("global", "commit-global"),
1443
+ branch_ref_row("branch-a", "commit-branch"),
1548
1444
  ],
1549
1445
  )
1550
1446
  .await;
1551
- transaction.commit().await.expect("commit should persist");
1552
1447
 
1553
- let hidden = scan_selected_tab_at(&live_state, storage.clone(), "version-a", false)
1448
+ let hidden = scan_selected_tab_at(&live_state, &storage, "branch-a", false)
1554
1449
  .await
1555
1450
  .expect("scan should succeed");
1556
1451
  assert_eq!(hidden.len(), 0);
1557
1452
 
1558
- let with_tombstone = scan_selected_tab_at(&live_state, storage.clone(), "version-a", true)
1453
+ let with_tombstone = scan_selected_tab_at(&live_state, &storage, "branch-a", true)
1559
1454
  .await
1560
1455
  .expect("scan should succeed");
1561
1456
  assert_eq!(with_tombstone.len(), 1);
1562
- assert_eq!(with_tombstone[0].version_id, "version-a");
1457
+ assert_eq!(with_tombstone[0].branch_id, "branch-a");
1563
1458
  assert_eq!(with_tombstone[0].snapshot_content, None);
1564
1459
  }
1565
1460
 
1566
1461
  #[tokio::test]
1567
1462
  async fn main_tombstone_hides_global_row() {
1568
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1569
- let storage = StorageContext::new(Arc::clone(&backend));
1463
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1570
1464
  let live_state = live_state_context();
1571
1465
 
1572
- let mut transaction = storage
1573
- .begin_write_transaction()
1574
- .await
1575
- .expect("transaction should open");
1466
+ let read = storage
1467
+ .begin_read(StorageReadOptions::default())
1468
+ .expect("read should open");
1576
1469
  {
1577
1470
  let rows = [
1578
1471
  tracked_row_with_commit("global-tracked", Some("change-global"), "commit-global"),
@@ -1585,149 +1478,111 @@ mod tests {
1585
1478
  let mut writes = StorageWriteSet::new();
1586
1479
  let mut json_writer = JsonStoreContext::new().writer();
1587
1480
  {
1588
- stage_materialized_live_rows(
1589
- transaction.as_mut(),
1590
- &mut writes,
1591
- &mut json_writer,
1592
- &rows,
1593
- )
1594
- .await
1595
- .expect("tracked rows should stage");
1481
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1482
+ .await
1483
+ .expect("tracked rows should stage");
1596
1484
  }
1597
- writes
1598
- .apply(&mut transaction.as_mut())
1599
- .await
1600
- .expect("tracked rows should apply");
1485
+ storage
1486
+ .commit_write_set(writes, StorageWriteOptions::default())
1487
+ .expect("writes should commit");
1601
1488
  }
1602
1489
  write_untracked_rows_to_store(
1603
- transaction.as_mut(),
1490
+ &storage,
1491
+ &read,
1604
1492
  &[
1605
- version_ref_row("global", "commit-global"),
1606
- version_ref_row("main", "commit-main"),
1493
+ branch_ref_row("global", "commit-global"),
1494
+ branch_ref_row("main", "commit-main"),
1607
1495
  ],
1608
1496
  )
1609
1497
  .await;
1610
- transaction.commit().await.expect("commit should persist");
1611
1498
 
1612
- let hidden = scan_selected_tab_at(&live_state, storage.clone(), "main", false)
1499
+ let hidden = scan_selected_tab_at(&live_state, &storage, "main", false)
1613
1500
  .await
1614
1501
  .expect("scan should succeed");
1615
1502
  assert_eq!(hidden.len(), 0);
1616
1503
 
1617
- let tombstones = scan_selected_tab_at(&live_state, storage.clone(), "main", true)
1504
+ let tombstones = scan_selected_tab_at(&live_state, &storage, "main", true)
1618
1505
  .await
1619
1506
  .expect("scan should succeed");
1620
1507
  assert_eq!(tombstones.len(), 1);
1621
- assert_eq!(tombstones[0].version_id, "main");
1508
+ assert_eq!(tombstones[0].branch_id, "main");
1622
1509
  assert!(!tombstones[0].global);
1623
1510
  assert_eq!(tombstones[0].snapshot_content, None);
1624
1511
  }
1625
1512
 
1626
1513
  #[tokio::test]
1627
- async fn writer_allows_commit_fact_to_share_the_touched_version_commit_id() {
1628
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1629
- let storage = StorageContext::new(Arc::clone(&backend));
1514
+ async fn writer_allows_commit_fact_to_share_the_touched_branch_commit_id() {
1515
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1630
1516
  let live_state = live_state_context();
1631
- let mut transaction = storage
1632
- .begin_write_transaction()
1633
- .await
1634
- .expect("transaction should open");
1517
+ let read = storage
1518
+ .begin_read(StorageReadOptions::default())
1519
+ .expect("read should open");
1635
1520
 
1636
1521
  {
1637
1522
  let rows = [
1638
1523
  tracked_row_at_with_commit(
1639
- "version-a",
1640
- "version-row",
1641
- Some("change-version"),
1642
- "commit-version",
1524
+ "branch-a",
1525
+ "branch-row",
1526
+ Some("change-branch"),
1527
+ "commit-branch",
1643
1528
  ),
1644
- commit_live_state_row("commit-version"),
1529
+ commit_live_state_row("commit-branch"),
1645
1530
  ];
1646
1531
  let mut writes = StorageWriteSet::new();
1647
1532
  let mut json_writer = JsonStoreContext::new().writer();
1648
1533
  {
1649
- stage_materialized_live_rows(
1650
- transaction.as_mut(),
1651
- &mut writes,
1652
- &mut json_writer,
1653
- &rows,
1654
- )
1655
- .await
1656
- .expect("commit facts are changelog projections, not root-local rows");
1534
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1535
+ .await
1536
+ .expect("commit facts are changelog projections, not root-local rows");
1657
1537
  }
1658
- writes
1659
- .apply(&mut transaction.as_mut())
1660
- .await
1661
- .expect("commit fact rows should apply");
1538
+ storage
1539
+ .commit_write_set(writes, StorageWriteOptions::default())
1540
+ .expect("writes should commit");
1662
1541
  }
1663
1542
  write_untracked_rows_to_store(
1664
- transaction.as_mut(),
1665
- &[version_ref_row("version-a", "commit-version")],
1543
+ &storage,
1544
+ &read,
1545
+ &[branch_ref_row("branch-a", "commit-branch")],
1666
1546
  )
1667
1547
  .await;
1668
- transaction.commit().await.expect("commit should persist");
1669
1548
 
1670
- let loaded = load_selected_tab_at(&live_state, storage.clone(), "version-a")
1549
+ let loaded = load_selected_tab_at(&live_state, &storage, "branch-a")
1671
1550
  .await
1672
1551
  .expect("load should succeed")
1673
- .expect("version row should be visible");
1552
+ .expect("branch row should be visible");
1674
1553
  assert_eq!(
1675
1554
  loaded.snapshot_content.as_deref(),
1676
- Some("{\"value\":\"version-row\"}")
1555
+ Some("{\"value\":\"branch-row\"}")
1677
1556
  );
1678
1557
  }
1679
1558
 
1680
1559
  #[tokio::test]
1681
1560
  async fn writer_uses_first_parent_as_merge_root_base() {
1682
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1683
- let storage = StorageContext::new(Arc::clone(&backend));
1684
- let mut seed_transaction = storage
1685
- .begin_write_transaction()
1686
- .await
1687
- .expect("seed transaction should open");
1561
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1562
+ let read = storage
1563
+ .begin_read(StorageReadOptions::default())
1564
+ .expect("read should open");
1565
+ write_empty_commits_to_store(&storage, &read, &["parent-left"]).await;
1688
1566
  let mut writes = StorageWriteSet::new();
1689
- {
1690
- CommitStoreContext::new()
1691
- .writer(&mut seed_transaction.as_mut(), &mut writes)
1692
- .stage_commit_draft(
1693
- CommitDraftRef {
1694
- id: "parent-left",
1695
- change_id: "parent-left:commit",
1696
- parent_ids: &[],
1697
- author_account_ids: &[],
1698
- created_at: "1970-01-01T00:00:00.000Z",
1699
- },
1700
- Vec::new(),
1701
- Vec::new(),
1702
- )
1703
- .await
1704
- .expect("first parent commit should stage");
1705
- TrackedStateContext::new()
1706
- .writer(&mut seed_transaction.as_mut(), &mut writes)
1707
- .stage_delta("parent-left", None, &[])
1708
- .await
1709
- .expect("first parent root should exist");
1710
- }
1711
- writes
1712
- .apply(&mut seed_transaction.as_mut())
1567
+ TrackedStateContext::new()
1568
+ .writer(&read, &mut writes)
1569
+ .stage_commit_root("parent-left", None, [])
1713
1570
  .await
1714
- .expect("first parent root should apply");
1715
- seed_transaction
1716
- .commit()
1717
- .await
1718
- .expect("seed transaction should commit");
1571
+ .expect("first parent tracked root should stage");
1572
+ storage
1573
+ .commit_write_set(writes, StorageWriteOptions::default())
1574
+ .expect("first parent tracked root should commit");
1719
1575
 
1720
- let mut transaction = storage
1721
- .begin_write_transaction()
1722
- .await
1723
- .expect("transaction should open");
1576
+ let read = storage
1577
+ .begin_read(StorageReadOptions::default())
1578
+ .expect("read should open");
1724
1579
 
1725
1580
  {
1726
1581
  let rows = [
1727
1582
  tracked_row_at_with_commit(
1728
- "version-a",
1729
- "version-row",
1730
- Some("change-version"),
1583
+ "branch-a",
1584
+ "branch-row",
1585
+ Some("change-branch"),
1731
1586
  "commit-merge",
1732
1587
  ),
1733
1588
  commit_live_state_row_with_parents(
@@ -1738,31 +1593,23 @@ mod tests {
1738
1593
  let mut writes = StorageWriteSet::new();
1739
1594
  let mut json_writer = JsonStoreContext::new().writer();
1740
1595
  {
1741
- stage_materialized_live_rows(
1742
- transaction.as_mut(),
1743
- &mut writes,
1744
- &mut json_writer,
1745
- &rows,
1746
- )
1747
- .await
1748
- .expect("merge commit should use first parent as tracked-root base");
1596
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1597
+ .await
1598
+ .expect("merge commit should use first parent as tracked-root base");
1749
1599
  }
1750
- writes
1751
- .apply(&mut transaction.as_mut())
1752
- .await
1753
- .expect("merge commit rows should apply");
1600
+ storage
1601
+ .commit_write_set(writes, StorageWriteOptions::default())
1602
+ .expect("writes should commit");
1754
1603
  }
1755
1604
  }
1756
1605
 
1757
1606
  #[tokio::test]
1758
1607
  async fn non_global_root_does_not_store_global_rows() {
1759
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1760
- let storage = StorageContext::new(Arc::clone(&backend));
1608
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
1761
1609
  let tracked_state = TrackedStateContext::new();
1762
- let mut transaction = storage
1763
- .begin_write_transaction()
1764
- .await
1765
- .expect("transaction should open");
1610
+ let read = storage
1611
+ .begin_read(StorageReadOptions::default())
1612
+ .expect("read should open");
1766
1613
 
1767
1614
  {
1768
1615
  let rows = [
@@ -1777,49 +1624,56 @@ mod tests {
1777
1624
  let mut writes = StorageWriteSet::new();
1778
1625
  let mut json_writer = JsonStoreContext::new().writer();
1779
1626
  {
1780
- stage_materialized_live_rows(
1781
- transaction.as_mut(),
1782
- &mut writes,
1783
- &mut json_writer,
1784
- &rows,
1785
- )
1786
- .await
1787
- .expect("tracked rows should stage");
1627
+ stage_materialized_live_rows(&read, &mut writes, &mut json_writer, &rows)
1628
+ .await
1629
+ .expect("tracked rows should stage");
1788
1630
  }
1789
- writes
1790
- .apply(&mut transaction.as_mut())
1791
- .await
1792
- .expect("tracked rows should apply");
1631
+ storage
1632
+ .commit_write_set(writes, StorageWriteOptions::default())
1633
+ .expect("writes should commit");
1793
1634
  }
1794
- transaction.commit().await.expect("commit should persist");
1795
1635
 
1796
- let global_root_rows =
1797
- scan_tracked_root(&tracked_state, storage.clone(), "commit-global").await;
1798
- assert_eq!(global_root_rows.len(), 1);
1636
+ let global_root_rows = scan_tracked_root(&tracked_state, &storage, "commit-global").await;
1637
+ assert_eq!(global_root_rows.len(), 2);
1638
+ let Some(global_row) = global_root_rows
1639
+ .iter()
1640
+ .find(|row| row.schema_key == "lix_key_value")
1641
+ else {
1642
+ panic!("global root should contain the explicit global tracked row");
1643
+ };
1799
1644
  assert_eq!(
1800
- global_root_rows[0].snapshot_content.as_deref(),
1645
+ global_row.snapshot_content.as_deref(),
1801
1646
  Some("{\"value\":\"global-tracked\"}")
1802
1647
  );
1803
1648
 
1804
- let main_root_rows =
1805
- scan_tracked_root(&tracked_state, storage.clone(), "commit-main").await;
1806
- assert_eq!(main_root_rows.len(), 1);
1649
+ let main_root_rows = scan_tracked_root(&tracked_state, &storage, "commit-main").await;
1650
+ assert_eq!(main_root_rows.len(), 2);
1651
+ let Some(main_row) = main_root_rows
1652
+ .iter()
1653
+ .find(|row| row.schema_key == "lix_key_value")
1654
+ else {
1655
+ panic!("main root should contain the explicit main tracked row");
1656
+ };
1807
1657
  assert_eq!(
1808
- main_root_rows[0].snapshot_content.as_deref(),
1658
+ main_row.snapshot_content.as_deref(),
1809
1659
  Some("{\"value\":\"main-tracked\"}")
1810
1660
  );
1811
1661
  }
1812
1662
 
1813
1663
  async fn load_selected_tab(
1814
1664
  live_state: &LiveStateContext,
1815
- storage: StorageContext,
1665
+ storage: &StorageContext,
1816
1666
  ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
1817
1667
  live_state
1818
- .reader(storage)
1668
+ .reader(
1669
+ storage
1670
+ .begin_read(StorageReadOptions::default())
1671
+ .expect("read should open"),
1672
+ )
1819
1673
  .load_row(&LiveStateRowRequest {
1820
1674
  schema_key: "lix_key_value".to_string(),
1821
- version_id: "global".to_string(),
1822
- entity_id: crate::entity_identity::EntityIdentity::single("selected-tab"),
1675
+ branch_id: "global".to_string(),
1676
+ entity_pk: crate::entity_pk::EntityPk::single("selected-tab"),
1823
1677
  file_id: NullableKeyFilter::Null,
1824
1678
  })
1825
1679
  .await
@@ -1827,15 +1681,19 @@ mod tests {
1827
1681
 
1828
1682
  async fn load_selected_tab_at(
1829
1683
  live_state: &LiveStateContext,
1830
- storage: StorageContext,
1831
- version_id: &str,
1684
+ storage: &StorageContext,
1685
+ branch_id: &str,
1832
1686
  ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
1833
1687
  live_state
1834
- .reader(storage)
1688
+ .reader(
1689
+ storage
1690
+ .begin_read(StorageReadOptions::default())
1691
+ .expect("read should open"),
1692
+ )
1835
1693
  .load_row(&LiveStateRowRequest {
1836
1694
  schema_key: "lix_key_value".to_string(),
1837
- version_id: version_id.to_string(),
1838
- entity_id: crate::entity_identity::EntityIdentity::single("selected-tab"),
1695
+ branch_id: branch_id.to_string(),
1696
+ entity_pk: crate::entity_pk::EntityPk::single("selected-tab"),
1839
1697
  file_id: NullableKeyFilter::Null,
1840
1698
  })
1841
1699
  .await
@@ -1843,19 +1701,21 @@ mod tests {
1843
1701
 
1844
1702
  async fn scan_selected_tab_at(
1845
1703
  live_state: &LiveStateContext,
1846
- storage: StorageContext,
1847
- version_id: &str,
1704
+ storage: &StorageContext,
1705
+ branch_id: &str,
1848
1706
  include_tombstones: bool,
1849
1707
  ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
1850
1708
  live_state
1851
- .reader(storage)
1709
+ .reader(
1710
+ storage
1711
+ .begin_read(StorageReadOptions::default())
1712
+ .expect("read should open"),
1713
+ )
1852
1714
  .scan_rows(&LiveStateScanRequest {
1853
1715
  filter: LiveStateFilter {
1854
1716
  schema_keys: vec!["lix_key_value".to_string()],
1855
- entity_ids: vec![crate::entity_identity::EntityIdentity::single(
1856
- "selected-tab",
1857
- )],
1858
- version_ids: vec![version_id.to_string()],
1717
+ entity_pks: vec![crate::entity_pk::EntityPk::single("selected-tab")],
1718
+ branch_ids: vec![branch_id.to_string()],
1859
1719
  file_ids: vec![NullableKeyFilter::Null],
1860
1720
  include_tombstones,
1861
1721
  ..LiveStateFilter::default()
@@ -1867,11 +1727,15 @@ mod tests {
1867
1727
 
1868
1728
  async fn scan_tracked_root(
1869
1729
  tracked_state: &TrackedStateContext,
1870
- storage: StorageContext,
1730
+ storage: &StorageContext,
1871
1731
  commit_id: &str,
1872
1732
  ) -> Vec<MaterializedTrackedStateRow> {
1873
1733
  tracked_state
1874
- .reader(storage)
1734
+ .reader(
1735
+ storage
1736
+ .begin_read(StorageReadOptions::default())
1737
+ .expect("read should open"),
1738
+ )
1875
1739
  .scan_rows_at_commit(
1876
1740
  commit_id,
1877
1741
  &TrackedStateScanRequest {
@@ -1895,13 +1759,13 @@ mod tests {
1895
1759
  }
1896
1760
 
1897
1761
  fn tracked_row_at_with_commit(
1898
- version_id: &str,
1762
+ branch_id: &str,
1899
1763
  value: &str,
1900
1764
  change_id: Option<&str>,
1901
1765
  commit_id: &str,
1902
1766
  ) -> MaterializedLiveStateRow {
1903
1767
  MaterializedLiveStateRow {
1904
- entity_id: identity("selected-tab"),
1768
+ entity_pk: identity("selected-tab"),
1905
1769
  schema_key: "lix_key_value".to_string(),
1906
1770
  file_id: None,
1907
1771
  snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
@@ -1909,23 +1773,23 @@ mod tests {
1909
1773
  deleted: false,
1910
1774
  created_at: "2026-01-01T00:00:00Z".to_string(),
1911
1775
  updated_at: "2026-01-01T00:00:00Z".to_string(),
1912
- global: version_id == "global",
1776
+ global: branch_id == "global",
1913
1777
  change_id: change_id.map(str::to_string),
1914
1778
  commit_id: Some(commit_id.to_string()),
1915
1779
  untracked: false,
1916
- version_id: version_id.to_string(),
1780
+ branch_id: branch_id.to_string(),
1917
1781
  }
1918
1782
  }
1919
1783
 
1920
1784
  fn tombstone_tracked_row_at_with_commit(
1921
- version_id: &str,
1785
+ branch_id: &str,
1922
1786
  change_id: Option<&str>,
1923
1787
  commit_id: &str,
1924
1788
  ) -> MaterializedLiveStateRow {
1925
1789
  MaterializedLiveStateRow {
1926
1790
  snapshot_content: None,
1927
1791
  deleted: true,
1928
- ..tracked_row_at_with_commit(version_id, "ignored", change_id, commit_id)
1792
+ ..tracked_row_at_with_commit(branch_id, "ignored", change_id, commit_id)
1929
1793
  }
1930
1794
  }
1931
1795
 
@@ -1933,9 +1797,9 @@ mod tests {
1933
1797
  untracked_row_at("global", value)
1934
1798
  }
1935
1799
 
1936
- fn untracked_row_at(version_id: &str, value: &str) -> MaterializedUntrackedStateRow {
1800
+ fn untracked_row_at(branch_id: &str, value: &str) -> MaterializedUntrackedStateRow {
1937
1801
  MaterializedUntrackedStateRow {
1938
- entity_id: identity("selected-tab"),
1802
+ entity_pk: identity("selected-tab"),
1939
1803
  schema_key: "lix_key_value".to_string(),
1940
1804
  file_id: None,
1941
1805
  snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
@@ -1943,29 +1807,29 @@ mod tests {
1943
1807
  deleted: false,
1944
1808
  created_at: "2026-01-01T00:00:00Z".to_string(),
1945
1809
  updated_at: "2026-01-01T00:00:00Z".to_string(),
1946
- global: version_id == "global",
1947
- version_id: version_id.to_string(),
1810
+ global: branch_id == "global",
1811
+ branch_id: branch_id.to_string(),
1948
1812
  }
1949
1813
  }
1950
1814
 
1951
- fn version_ref_row(version_id: &str, commit_id: &str) -> MaterializedUntrackedStateRow {
1815
+ fn branch_ref_row(branch_id: &str, commit_id: &str) -> MaterializedUntrackedStateRow {
1952
1816
  MaterializedUntrackedStateRow {
1953
- entity_id: identity(version_id),
1954
- schema_key: "lix_version_ref".to_string(),
1817
+ entity_pk: identity(branch_id),
1818
+ schema_key: "lix_branch_ref".to_string(),
1955
1819
  file_id: None,
1956
1820
  snapshot_content: Some(
1957
1821
  serde_json::to_string(&json!({
1958
- "id": version_id,
1822
+ "id": branch_id,
1959
1823
  "commit_id": commit_id,
1960
1824
  }))
1961
- .expect("version ref should serialize"),
1825
+ .expect("branch ref should serialize"),
1962
1826
  ),
1963
1827
  metadata: None,
1964
1828
  deleted: false,
1965
1829
  created_at: "2026-01-01T00:00:00Z".to_string(),
1966
1830
  updated_at: "2026-01-01T00:00:00Z".to_string(),
1967
1831
  global: true,
1968
- version_id: "global".to_string(),
1832
+ branch_id: "global".to_string(),
1969
1833
  }
1970
1834
  }
1971
1835
 
@@ -1995,7 +1859,7 @@ mod tests {
1995
1859
  snapshot: serde_json::Value,
1996
1860
  ) -> MaterializedLiveStateRow {
1997
1861
  MaterializedLiveStateRow {
1998
- entity_id: identity(commit_id),
1862
+ entity_pk: identity(commit_id),
1999
1863
  schema_key: COMMIT_SCHEMA_KEY.to_string(),
2000
1864
  file_id: None,
2001
1865
  snapshot_content: Some(
@@ -2009,11 +1873,11 @@ mod tests {
2009
1873
  change_id: Some(format!("change-{commit_id}")),
2010
1874
  commit_id: Some(commit_id.to_string()),
2011
1875
  untracked: false,
2012
- version_id: "global".to_string(),
1876
+ branch_id: "global".to_string(),
2013
1877
  }
2014
1878
  }
2015
1879
 
2016
- fn identity(entity_id: &str) -> EntityIdentity {
2017
- EntityIdentity::single(entity_id)
1880
+ fn identity(entity_pk: &str) -> EntityPk {
1881
+ EntityPk::single(entity_pk)
2018
1882
  }
2019
1883
  }