@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
@@ -5,7 +5,7 @@ use std::{
5
5
  pin::Pin,
6
6
  };
7
7
 
8
- use crate::storage::{StorageReader, StorageWriteSet};
8
+ use crate::storage::{StorageRead, StorageWriteSet};
9
9
  use crate::tracked_state::codec::{
10
10
  boundary_trigger, child_summary_from_node, decode_key, decode_key_with_trusted_prefix,
11
11
  decode_node, decode_node_ref, decode_value, decode_visible_value, encode_internal_node,
@@ -46,7 +46,7 @@ impl Default for TrackedStateTreeOptions {
46
46
 
47
47
  /// Content-addressed tracked-state tree operations.
48
48
  ///
49
- /// This type owns tracked-state tree mechanics only. Version refs, untracked overlay,
49
+ /// This type owns tracked-state tree mechanics only. Branch refs, untracked overlay,
50
50
  /// and SQL visibility remain outside the tree.
51
51
  #[derive(Debug, Clone)]
52
52
  pub(crate) struct TrackedStateTree {
@@ -60,14 +60,14 @@ impl TrackedStateTree {
60
60
  }
61
61
  }
62
62
 
63
- #[allow(dead_code)]
63
+ #[cfg(test)]
64
64
  pub(crate) fn with_options(options: TrackedStateTreeOptions) -> Self {
65
65
  Self { options }
66
66
  }
67
67
 
68
68
  pub(crate) async fn load_root(
69
69
  &self,
70
- store: &mut (impl StorageReader + ?Sized),
70
+ store: &(impl StorageRead + Send + Sync + ?Sized),
71
71
  commit_id: &str,
72
72
  ) -> Result<Option<TrackedStateRootId>, LixError> {
73
73
  storage::load_root(store, commit_id).await
@@ -76,7 +76,7 @@ impl TrackedStateTree {
76
76
  #[cfg(test)]
77
77
  pub(crate) async fn get(
78
78
  &self,
79
- store: &mut impl StorageReader,
79
+ store: &(impl StorageRead + Send + Sync),
80
80
  root_id: &TrackedStateRootId,
81
81
  key: &TrackedStateKey,
82
82
  ) -> Result<Option<TrackedStateIndexValue>, LixError> {
@@ -112,7 +112,7 @@ impl TrackedStateTree {
112
112
 
113
113
  pub(crate) async fn get_many(
114
114
  &self,
115
- store: &mut impl StorageReader,
115
+ store: &(impl StorageRead + Send + Sync + ?Sized),
116
116
  root_id: &TrackedStateRootId,
117
117
  keys: &[TrackedStateKey],
118
118
  ) -> Result<Vec<Option<TrackedStateIndexValue>>, LixError> {
@@ -133,24 +133,9 @@ impl TrackedStateTree {
133
133
  Ok(values)
134
134
  }
135
135
 
136
- pub(crate) async fn row_count(
137
- &self,
138
- store: &mut impl StorageReader,
139
- root_id: &TrackedStateRootId,
140
- ) -> Result<usize, LixError> {
141
- match self.load_node(store, root_id.as_bytes()).await? {
142
- DecodedNode::Leaf(leaf) => Ok(leaf.entries().len()),
143
- DecodedNode::Internal(internal) => Ok(internal
144
- .children()
145
- .iter()
146
- .map(|child| child.subtree_count as usize)
147
- .sum()),
148
- }
149
- }
150
-
151
136
  pub(crate) async fn scan(
152
137
  &self,
153
- store: &mut impl StorageReader,
138
+ store: &(impl StorageRead + Send + Sync + ?Sized),
154
139
  root_id: &TrackedStateRootId,
155
140
  request: &TrackedStateTreeScanRequest,
156
141
  ) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
@@ -173,24 +158,9 @@ impl TrackedStateTree {
173
158
  Ok(rows)
174
159
  }
175
160
 
176
- pub(crate) async fn count_matching_keys(
177
- &self,
178
- store: &mut impl StorageReader,
179
- root_id: &TrackedStateRootId,
180
- request: &TrackedStateTreeScanRequest,
181
- ) -> Result<usize, LixError> {
182
- if request.limit == Some(0) {
183
- return Ok(0);
184
- }
185
-
186
- let ranges = scan_ranges(request);
187
- self.count_matching_keys_node(store, *root_id.as_bytes(), request, &ranges)
188
- .await
189
- }
190
-
191
161
  pub(crate) async fn diff(
192
162
  &self,
193
- store: &mut impl StorageReader,
163
+ store: &(impl StorageRead + Send + Sync),
194
164
  left_root: Option<&TrackedStateRootId>,
195
165
  right_root: Option<&TrackedStateRootId>,
196
166
  request: &TrackedStateTreeScanRequest,
@@ -231,27 +201,41 @@ impl TrackedStateTree {
231
201
  }
232
202
  }
233
203
 
204
+ #[cfg(test)]
234
205
  pub(crate) async fn apply_mutations(
235
206
  &self,
236
- store: &mut (impl StorageReader + ?Sized),
207
+ store: &(impl StorageRead + Send + Sync + ?Sized),
237
208
  writes: &mut StorageWriteSet,
238
209
  base_root: Option<&TrackedStateRootId>,
239
- mut mutations: Vec<TrackedStateMutation>,
210
+ mutations: Vec<TrackedStateMutation>,
240
211
  commit_id: Option<&str>,
241
212
  ) -> Result<TrackedStateApplyResult, LixError> {
242
213
  let mut overlay = storage::TrackedStateChunkOverlay::new();
214
+ self.apply_mutations_with_overlay(
215
+ store,
216
+ writes,
217
+ &mut overlay,
218
+ base_root,
219
+ mutations,
220
+ commit_id,
221
+ )
222
+ .await
223
+ }
224
+
225
+ pub(crate) async fn apply_mutations_with_overlay(
226
+ &self,
227
+ store: &(impl StorageRead + Send + Sync + ?Sized),
228
+ writes: &mut StorageWriteSet,
229
+ overlay: &mut storage::TrackedStateChunkOverlay,
230
+ base_root: Option<&TrackedStateRootId>,
231
+ mut mutations: Vec<TrackedStateMutation>,
232
+ commit_id: Option<&str>,
233
+ ) -> Result<TrackedStateApplyResult, LixError> {
243
234
  if let Some(root_id) = base_root {
244
235
  if mutations.len() == 1 {
245
236
  let mutation = mutations.pop().expect("single mutation should exist");
246
237
  match self
247
- .apply_single_mutation(
248
- store,
249
- writes,
250
- &mut overlay,
251
- root_id,
252
- mutation,
253
- commit_id,
254
- )
238
+ .apply_single_mutation(store, writes, overlay, root_id, mutation, commit_id)
255
239
  .await?
256
240
  {
257
241
  MutationApply::Applied(result) => return Ok(result),
@@ -260,12 +244,7 @@ impl TrackedStateTree {
260
244
  } else if mutations.len() > 1 {
261
245
  match self
262
246
  .apply_sorted_mutations_chunker(
263
- store,
264
- writes,
265
- &mut overlay,
266
- root_id,
267
- mutations,
268
- commit_id,
247
+ store, writes, overlay, root_id, mutations, commit_id,
269
248
  )
270
249
  .await?
271
250
  {
@@ -277,7 +256,7 @@ impl TrackedStateTree {
277
256
 
278
257
  let mut entries = match base_root {
279
258
  Some(root_id) => self
280
- .collect_leaf_entries(store, root_id)
259
+ .collect_leaf_entries_with_overlay(store, overlay, root_id)
281
260
  .await?
282
261
  .into_iter()
283
262
  .map(|entry| (entry.key, entry.value))
@@ -298,12 +277,6 @@ impl TrackedStateTree {
298
277
  .collect(),
299
278
  )?;
300
279
  overlay.stage_chunks(writes, &built.chunks);
301
- let persisted_root = if let Some(commit_id) = commit_id {
302
- storage::stage_root(writes, commit_id, &built.root_id);
303
- true
304
- } else {
305
- false
306
- };
307
280
 
308
281
  Ok(TrackedStateApplyResult {
309
282
  root_id: built.root_id,
@@ -311,13 +284,12 @@ impl TrackedStateTree {
311
284
  tree_height: built.tree_height,
312
285
  chunk_count: built.chunks.len(),
313
286
  chunk_bytes: built.chunk_bytes,
314
- persisted_root,
315
287
  })
316
288
  }
317
289
 
318
290
  async fn apply_single_mutation(
319
291
  &self,
320
- store: &mut (impl StorageReader + ?Sized),
292
+ store: &(impl StorageRead + Send + Sync + ?Sized),
321
293
  writes: &mut StorageWriteSet,
322
294
  overlay: &mut storage::TrackedStateChunkOverlay,
323
295
  root_id: &TrackedStateRootId,
@@ -447,12 +419,6 @@ impl TrackedStateTree {
447
419
  suffix_entries[mutation_entry_index].key.as_slice(),
448
420
  )?;
449
421
  overlay.stage_chunks(writes, &built.chunks);
450
- let persisted_root = if let Some(commit_id) = commit_id {
451
- storage::stage_root(writes, commit_id, &built.root_id);
452
- true
453
- } else {
454
- false
455
- };
456
422
 
457
423
  Ok(MutationApply::Applied(TrackedStateApplyResult {
458
424
  root_id: built.root_id,
@@ -460,20 +426,19 @@ impl TrackedStateTree {
460
426
  tree_height: built.tree_height,
461
427
  chunk_count: built.chunks.len(),
462
428
  chunk_bytes: built.chunk_bytes,
463
- persisted_root,
464
429
  }))
465
430
  }
466
431
 
467
432
  fn diff_nodes<'a, S>(
468
433
  &'a self,
469
- store: &'a mut S,
434
+ store: &'a S,
470
435
  left_hash: [u8; TRACKED_STATE_HASH_BYTES],
471
436
  right_hash: [u8; TRACKED_STATE_HASH_BYTES],
472
437
  request: &'a TrackedStateTreeScanRequest,
473
438
  out: &'a mut Vec<TrackedStateTreeDiffEntry>,
474
439
  ) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + 'a>>
475
440
  where
476
- S: StorageReader + 'a,
441
+ S: StorageRead + Send + Sync + 'a,
477
442
  {
478
443
  Box::pin(async move {
479
444
  if left_hash == right_hash {
@@ -514,7 +479,7 @@ impl TrackedStateTree {
514
479
 
515
480
  async fn diff_leaf_summary_cursors(
516
481
  &self,
517
- store: &mut impl StorageReader,
482
+ store: &(impl StorageRead + Send + Sync),
518
483
  left_hash: [u8; TRACKED_STATE_HASH_BYTES],
519
484
  right_hash: [u8; TRACKED_STATE_HASH_BYTES],
520
485
  request: &TrackedStateTreeScanRequest,
@@ -572,7 +537,7 @@ impl TrackedStateTree {
572
537
 
573
538
  async fn diff_leaf_summary_window(
574
539
  &self,
575
- store: &mut impl StorageReader,
540
+ store: &(impl StorageRead + Send + Sync),
576
541
  left_leaves: &[ChildSummary],
577
542
  right_leaves: &[ChildSummary],
578
543
  request: &TrackedStateTreeScanRequest,
@@ -684,7 +649,7 @@ impl TrackedStateTree {
684
649
 
685
650
  async fn apply_sorted_mutations_chunker(
686
651
  &self,
687
- store: &mut (impl StorageReader + ?Sized),
652
+ store: &(impl StorageRead + Send + Sync + ?Sized),
688
653
  writes: &mut StorageWriteSet,
689
654
  overlay: &mut storage::TrackedStateChunkOverlay,
690
655
  root_id: &TrackedStateRootId,
@@ -851,7 +816,7 @@ impl TrackedStateTree {
851
816
 
852
817
  async fn apply_single_mutation_from_seek_path(
853
818
  &self,
854
- store: &mut (impl StorageReader + ?Sized),
819
+ store: &(impl StorageRead + Send + Sync + ?Sized),
855
820
  writes: &mut StorageWriteSet,
856
821
  overlay: &mut storage::TrackedStateChunkOverlay,
857
822
  root_id: &TrackedStateRootId,
@@ -1033,22 +998,15 @@ impl TrackedStateTree {
1033
998
  writes: &mut StorageWriteSet,
1034
999
  overlay: &mut storage::TrackedStateChunkOverlay,
1035
1000
  built: BuiltTree,
1036
- commit_id: Option<&str>,
1001
+ _commit_id: Option<&str>,
1037
1002
  ) -> Result<TrackedStateApplyResult, LixError> {
1038
1003
  overlay.stage_chunks(writes, &built.chunks);
1039
- let persisted_root = if let Some(commit_id) = commit_id {
1040
- storage::stage_root(writes, commit_id, &built.root_id);
1041
- true
1042
- } else {
1043
- false
1044
- };
1045
1004
  Ok(TrackedStateApplyResult {
1046
1005
  root_id: built.root_id,
1047
1006
  row_count: built.row_count,
1048
1007
  tree_height: built.tree_height,
1049
1008
  chunk_count: built.chunks.len(),
1050
1009
  chunk_bytes: built.chunk_bytes,
1051
- persisted_root,
1052
1010
  })
1053
1011
  }
1054
1012
 
@@ -1379,9 +1337,21 @@ impl TrackedStateTree {
1379
1337
  .collect()
1380
1338
  }
1381
1339
 
1340
+ #[cfg(test)]
1382
1341
  async fn collect_leaf_entries(
1383
1342
  &self,
1384
- store: &mut (impl StorageReader + ?Sized),
1343
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1344
+ root_id: &TrackedStateRootId,
1345
+ ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1346
+ let overlay = storage::TrackedStateChunkOverlay::new();
1347
+ self.collect_leaf_entries_with_overlay(store, &overlay, root_id)
1348
+ .await
1349
+ }
1350
+
1351
+ async fn collect_leaf_entries_with_overlay(
1352
+ &self,
1353
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1354
+ overlay: &storage::TrackedStateChunkOverlay,
1385
1355
  root_id: &TrackedStateRootId,
1386
1356
  ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1387
1357
  let mut out = Vec::new();
@@ -1389,7 +1359,7 @@ impl TrackedStateTree {
1389
1359
  while !current.is_empty() {
1390
1360
  let mut next = Vec::new();
1391
1361
  for hash in current {
1392
- match self.load_node(store, &hash).await? {
1362
+ match self.load_node_with_overlay(store, overlay, &hash).await? {
1393
1363
  DecodedNode::Leaf(leaf) => out.extend(leaf.entries().iter().cloned()),
1394
1364
  DecodedNode::Internal(internal) => {
1395
1365
  next.extend(internal.children().iter().map(|child| child.child_hash));
@@ -1403,7 +1373,7 @@ impl TrackedStateTree {
1403
1373
 
1404
1374
  async fn collect_filtered_entries(
1405
1375
  &self,
1406
- store: &mut impl StorageReader,
1376
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1407
1377
  root_id: &TrackedStateRootId,
1408
1378
  request: &TrackedStateTreeScanRequest,
1409
1379
  ) -> Result<Vec<(TrackedStateKey, TrackedStateIndexValue)>, LixError> {
@@ -1412,7 +1382,7 @@ impl TrackedStateTree {
1412
1382
 
1413
1383
  fn scan_node<'a, S>(
1414
1384
  &'a self,
1415
- store: &'a mut S,
1385
+ store: &'a S,
1416
1386
  hash: [u8; TRACKED_STATE_HASH_BYTES],
1417
1387
  request: &'a TrackedStateTreeScanRequest,
1418
1388
  ranges: &'a [EncodedScanRange],
@@ -1420,7 +1390,7 @@ impl TrackedStateTree {
1420
1390
  rows: &'a mut Vec<(TrackedStateKey, TrackedStateIndexValue)>,
1421
1391
  ) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + Send + 'a>>
1422
1392
  where
1423
- S: StorageReader + Send + 'a,
1393
+ S: StorageRead + Send + Sync + ?Sized + 'a,
1424
1394
  {
1425
1395
  Box::pin(async move {
1426
1396
  let bytes = self.load_node_bytes(store, &hash).await?;
@@ -1486,13 +1456,13 @@ impl TrackedStateTree {
1486
1456
 
1487
1457
  fn get_many_node<'a, S>(
1488
1458
  &'a self,
1489
- store: &'a mut S,
1459
+ store: &'a S,
1490
1460
  hash: [u8; TRACKED_STATE_HASH_BYTES],
1491
1461
  encoded_keys: &'a [(usize, Vec<u8>)],
1492
1462
  values: &'a mut [Option<TrackedStateIndexValue>],
1493
1463
  ) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + Send + 'a>>
1494
1464
  where
1495
- S: StorageReader + Send + 'a,
1465
+ S: StorageRead + Send + Sync + ?Sized + 'a,
1496
1466
  {
1497
1467
  Box::pin(async move {
1498
1468
  if encoded_keys.is_empty() {
@@ -1550,51 +1520,9 @@ impl TrackedStateTree {
1550
1520
  })
1551
1521
  }
1552
1522
 
1553
- fn count_matching_keys_node<'a, S>(
1554
- &'a self,
1555
- store: &'a mut S,
1556
- hash: [u8; TRACKED_STATE_HASH_BYTES],
1557
- request: &'a TrackedStateTreeScanRequest,
1558
- ranges: &'a [EncodedScanRange],
1559
- ) -> Pin<Box<dyn Future<Output = Result<usize, LixError>> + Send + 'a>>
1560
- where
1561
- S: StorageReader + Send + 'a,
1562
- {
1563
- Box::pin(async move {
1564
- let mut count = 0usize;
1565
- match self.load_node(store, &hash).await? {
1566
- DecodedNode::Leaf(leaf) => {
1567
- for entry in leaf.entries() {
1568
- if !encoded_key_in_scan_ranges(&entry.key, ranges) {
1569
- continue;
1570
- }
1571
- let key = decode_key(&entry.key)?;
1572
- if key_matches_scan_filters(request, &key) {
1573
- count += 1;
1574
- }
1575
- }
1576
- }
1577
- DecodedNode::Internal(internal) => {
1578
- for child in internal.children() {
1579
- if child_summary_contained_by_scan_ranges(child, ranges)
1580
- && request.entity_ids.is_empty()
1581
- {
1582
- count += child.subtree_count as usize;
1583
- } else if child_summary_overlaps_scan_ranges(child, ranges) {
1584
- count += self
1585
- .count_matching_keys_node(store, child.child_hash, request, ranges)
1586
- .await?;
1587
- }
1588
- }
1589
- }
1590
- }
1591
- Ok(count)
1592
- })
1593
- }
1594
-
1595
1523
  async fn collect_entries_from_leaf_summaries(
1596
1524
  &self,
1597
- store: &mut impl StorageReader,
1525
+ store: &(impl StorageRead + Send + Sync),
1598
1526
  leaves: &[ChildSummary],
1599
1527
  ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1600
1528
  let mut entries = Vec::new();
@@ -1606,7 +1534,7 @@ impl TrackedStateTree {
1606
1534
 
1607
1535
  async fn collect_summary_levels_with_overlay(
1608
1536
  &self,
1609
- store: &mut (impl StorageReader + ?Sized),
1537
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1610
1538
  overlay: &storage::TrackedStateChunkOverlay,
1611
1539
  root_id: &TrackedStateRootId,
1612
1540
  ) -> Result<Vec<Vec<ChildSummary>>, LixError> {
@@ -1623,13 +1551,13 @@ impl TrackedStateTree {
1623
1551
 
1624
1552
  fn collect_summary_levels_for_node_with_overlay<'a, S>(
1625
1553
  &'a self,
1626
- store: &'a mut S,
1554
+ store: &'a S,
1627
1555
  overlay: &'a storage::TrackedStateChunkOverlay,
1628
1556
  hash: [u8; TRACKED_STATE_HASH_BYTES],
1629
1557
  levels: &'a mut Vec<Vec<ChildSummary>>,
1630
1558
  ) -> Pin<Box<dyn Future<Output = Result<(ChildSummary, usize), LixError>> + 'a>>
1631
1559
  where
1632
- S: StorageReader + ?Sized + 'a,
1560
+ S: StorageRead + Send + Sync + ?Sized + 'a,
1633
1561
  {
1634
1562
  Box::pin(async move {
1635
1563
  match self.load_node_with_overlay(store, overlay, &hash).await? {
@@ -1681,7 +1609,7 @@ impl TrackedStateTree {
1681
1609
 
1682
1610
  async fn load_leaf_entries(
1683
1611
  &self,
1684
- store: &mut (impl StorageReader + ?Sized),
1612
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1685
1613
  hash: &[u8; TRACKED_STATE_HASH_BYTES],
1686
1614
  ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1687
1615
  match self.load_node(store, hash).await? {
@@ -1695,7 +1623,7 @@ impl TrackedStateTree {
1695
1623
 
1696
1624
  async fn load_leaf_entries_with_overlay(
1697
1625
  &self,
1698
- store: &mut (impl StorageReader + ?Sized),
1626
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1699
1627
  overlay: &storage::TrackedStateChunkOverlay,
1700
1628
  hash: &[u8; TRACKED_STATE_HASH_BYTES],
1701
1629
  ) -> Result<Vec<EncodedLeafEntry>, LixError> {
@@ -1710,7 +1638,7 @@ impl TrackedStateTree {
1710
1638
 
1711
1639
  async fn load_node(
1712
1640
  &self,
1713
- store: &mut (impl StorageReader + ?Sized),
1641
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1714
1642
  hash: &[u8; TRACKED_STATE_HASH_BYTES],
1715
1643
  ) -> Result<DecodedNode, LixError> {
1716
1644
  let bytes = self.load_node_bytes(store, hash).await?;
@@ -1719,7 +1647,7 @@ impl TrackedStateTree {
1719
1647
 
1720
1648
  async fn load_node_bytes(
1721
1649
  &self,
1722
- store: &mut (impl StorageReader + ?Sized),
1650
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1723
1651
  hash: &[u8; TRACKED_STATE_HASH_BYTES],
1724
1652
  ) -> Result<Vec<u8>, LixError> {
1725
1653
  let bytes = storage::read_chunk(store, hash).await?.ok_or_else(|| {
@@ -1731,7 +1659,7 @@ impl TrackedStateTree {
1731
1659
 
1732
1660
  async fn load_node_with_overlay(
1733
1661
  &self,
1734
- store: &mut (impl StorageReader + ?Sized),
1662
+ store: &(impl StorageRead + Send + Sync + ?Sized),
1735
1663
  overlay: &storage::TrackedStateChunkOverlay,
1736
1664
  hash: &[u8; TRACKED_STATE_HASH_BYTES],
1737
1665
  ) -> Result<DecodedNode, LixError> {
@@ -1813,7 +1741,7 @@ struct LeafSummaryCursorFrame {
1813
1741
  impl LeafSummaryCursor {
1814
1742
  async fn new(
1815
1743
  tree: &TrackedStateTree,
1816
- store: &mut impl StorageReader,
1744
+ store: &(impl StorageRead + Send + Sync),
1817
1745
  root_hash: [u8; TRACKED_STATE_HASH_BYTES],
1818
1746
  ) -> Result<Self, LixError> {
1819
1747
  let mut cursor = Self {
@@ -1846,7 +1774,7 @@ impl LeafSummaryCursor {
1846
1774
  async fn advance(
1847
1775
  &mut self,
1848
1776
  tree: &TrackedStateTree,
1849
- store: &mut impl StorageReader,
1777
+ store: &(impl StorageRead + Send + Sync),
1850
1778
  ) -> Result<(), LixError> {
1851
1779
  self.current = None;
1852
1780
  while let Some(frame) = self.stack.last_mut() {
@@ -1871,7 +1799,7 @@ impl LeafSummaryCursor {
1871
1799
  async fn descend_to_leaf(
1872
1800
  &mut self,
1873
1801
  tree: &TrackedStateTree,
1874
- store: &mut impl StorageReader,
1802
+ store: &(impl StorageRead + Send + Sync),
1875
1803
  mut summary: ChildSummary,
1876
1804
  ) -> Result<(), LixError> {
1877
1805
  loop {
@@ -1911,14 +1839,12 @@ impl LeafSummaryCursor {
1911
1839
  struct LeafChunkAccumulator {
1912
1840
  entries: Vec<EncodedLeafEntry>,
1913
1841
  key_bytes: usize,
1914
- value_bytes: usize,
1915
1842
  }
1916
1843
 
1917
1844
  #[derive(Debug, Default)]
1918
1845
  struct LeafChunkRefAccumulator<'a> {
1919
1846
  entries: Vec<EncodedLeafEntryRef<'a>>,
1920
1847
  key_bytes: usize,
1921
- value_bytes: usize,
1922
1848
  }
1923
1849
 
1924
1850
  #[derive(Debug, Default)]
@@ -1945,24 +1871,19 @@ fn chunk_leaf_entries(
1945
1871
  let mut groups = Vec::new();
1946
1872
  let mut current = LeafChunkAccumulator::default();
1947
1873
  for entry in entries {
1948
- let item_size = estimate_leaf_entry_size(entry.key.len(), entry.value.len());
1949
- let projected_size = estimate_leaf_chunk_size(
1874
+ let item_size = estimate_leaf_boundary_entry_size(entry.key.len());
1875
+ let projected_size = estimate_leaf_boundary_chunk_size(
1950
1876
  current.entries.len() + 1,
1951
1877
  current.key_bytes + entry.key.len(),
1952
- current.value_bytes + entry.value.len(),
1953
1878
  );
1954
1879
  if !current.entries.is_empty() && projected_size > options.max_chunk_bytes {
1955
1880
  groups.push(std::mem::take(&mut current));
1956
1881
  }
1957
1882
 
1958
1883
  current.key_bytes += entry.key.len();
1959
- current.value_bytes += entry.value.len();
1960
1884
  current.entries.push(entry);
1961
- let current_size = estimate_leaf_chunk_size(
1962
- current.entries.len(),
1963
- current.key_bytes,
1964
- current.value_bytes,
1965
- );
1885
+ let current_size =
1886
+ estimate_leaf_boundary_chunk_size(current.entries.len(), current.key_bytes);
1966
1887
  if current_size >= options.min_chunk_bytes
1967
1888
  && (current_size >= options.max_chunk_bytes
1968
1889
  || current.entries.last().is_some_and(|entry| {
@@ -1995,24 +1916,19 @@ fn chunk_leaf_entry_refs<'a>(
1995
1916
  let mut groups = Vec::new();
1996
1917
  let mut current = LeafChunkRefAccumulator::default();
1997
1918
  for entry in iter {
1998
- let item_size = estimate_leaf_entry_size(entry.key.len(), entry.value.len());
1999
- let projected_size = estimate_leaf_chunk_size(
1919
+ let item_size = estimate_leaf_boundary_entry_size(entry.key.len());
1920
+ let projected_size = estimate_leaf_boundary_chunk_size(
2000
1921
  current.entries.len() + 1,
2001
1922
  current.key_bytes + entry.key.len(),
2002
- current.value_bytes + entry.value.len(),
2003
1923
  );
2004
1924
  if !current.entries.is_empty() && projected_size > options.max_chunk_bytes {
2005
1925
  groups.push(std::mem::take(&mut current));
2006
1926
  }
2007
1927
 
2008
1928
  current.key_bytes += entry.key.len();
2009
- current.value_bytes += entry.value.len();
2010
1929
  current.entries.push(entry);
2011
- let current_size = estimate_leaf_chunk_size(
2012
- current.entries.len(),
2013
- current.key_bytes,
2014
- current.value_bytes,
2015
- );
1930
+ let current_size =
1931
+ estimate_leaf_boundary_chunk_size(current.entries.len(), current.key_bytes);
2016
1932
  if current_size >= options.min_chunk_bytes
2017
1933
  && (current_size >= options.max_chunk_bytes
2018
1934
  || current.entries.last().is_some_and(|entry| {
@@ -2138,8 +2054,12 @@ fn estimate_leaf_chunk_size(entry_count: usize, key_bytes: usize, value_bytes: u
2138
2054
  10 + entry_count * 12 + key_bytes + value_bytes
2139
2055
  }
2140
2056
 
2141
- fn estimate_leaf_entry_size(key_bytes: usize, value_bytes: usize) -> usize {
2142
- 12 + key_bytes + value_bytes
2057
+ fn estimate_leaf_boundary_chunk_size(entry_count: usize, key_bytes: usize) -> usize {
2058
+ estimate_leaf_chunk_size(entry_count, key_bytes, 0)
2059
+ }
2060
+
2061
+ fn estimate_leaf_boundary_entry_size(key_bytes: usize) -> usize {
2062
+ 12 + key_bytes
2143
2063
  }
2144
2064
 
2145
2065
  fn estimate_internal_chunk_size(
@@ -2179,7 +2099,7 @@ fn internal_boundaries_match(left: &[ChildSummary], right: &[ChildSummary]) -> b
2179
2099
 
2180
2100
  async fn child_summaries_are_leaves(
2181
2101
  tree: &TrackedStateTree,
2182
- store: &mut impl StorageReader,
2102
+ store: &(impl StorageRead + Send + Sync),
2183
2103
  children: &[ChildSummary],
2184
2104
  ) -> Result<bool, LixError> {
2185
2105
  let Some(first_child) = children.first() else {
@@ -2302,7 +2222,7 @@ fn scan_ranges(request: &TrackedStateTreeScanRequest) -> Vec<EncodedScanRange> {
2302
2222
  return Vec::new();
2303
2223
  }
2304
2224
 
2305
- let can_bind_entity = !request.entity_ids.is_empty()
2225
+ let can_bind_entity = !request.entity_pks.is_empty()
2306
2226
  && !request.file_ids.is_empty()
2307
2227
  && request
2308
2228
  .file_ids
@@ -2318,11 +2238,11 @@ fn scan_ranges(request: &TrackedStateTreeScanRequest) -> Vec<EncodedScanRange> {
2318
2238
  NullableKeyFilter::Value(file_id) => Some(file_id.clone()),
2319
2239
  NullableKeyFilter::Any => unreachable!("filtered above"),
2320
2240
  };
2321
- for entity_id in &request.entity_ids {
2241
+ for entity_pk in &request.entity_pks {
2322
2242
  let key = TrackedStateKey {
2323
2243
  schema_key: schema_key.clone(),
2324
2244
  file_id: file_id.clone(),
2325
- entity_id: entity_id.clone(),
2245
+ entity_pk: entity_pk.clone(),
2326
2246
  };
2327
2247
  ranges.push(exact_scan_range(encode_key(&key)));
2328
2248
  }
@@ -2361,7 +2281,7 @@ fn scan_key_decode_hint<'a>(
2361
2281
  if ranges.len() != 1 || request.schema_keys.len() != 1 || request.file_ids.len() != 1 {
2362
2282
  return None;
2363
2283
  }
2364
- if !request.entity_ids.is_empty() {
2284
+ if !request.entity_pks.is_empty() {
2365
2285
  return None;
2366
2286
  }
2367
2287
  let file_id = match request.file_ids.first()? {
@@ -2413,20 +2333,6 @@ fn child_summary_overlaps_scan_ranges(child: &ChildSummary, ranges: &[EncodedSca
2413
2333
  })
2414
2334
  }
2415
2335
 
2416
- fn child_summary_contained_by_scan_ranges(
2417
- child: &ChildSummary,
2418
- ranges: &[EncodedScanRange],
2419
- ) -> bool {
2420
- ranges.is_empty()
2421
- || ranges.iter().any(|range| {
2422
- child.first_key.as_slice() >= range.start.as_slice()
2423
- && range
2424
- .end
2425
- .as_ref()
2426
- .is_none_or(|end| child.last_key.as_slice() < end.as_slice())
2427
- })
2428
- }
2429
-
2430
2336
  fn encoded_key_in_scan_ranges(key: &[u8], ranges: &[EncodedScanRange]) -> bool {
2431
2337
  ranges.is_empty()
2432
2338
  || ranges.iter().any(|range| {
@@ -2439,7 +2345,7 @@ fn key_matches_scan_filters(request: &TrackedStateTreeScanRequest, key: &Tracked
2439
2345
  if !request.schema_keys.is_empty() && !request.schema_keys.contains(&key.schema_key) {
2440
2346
  return false;
2441
2347
  }
2442
- if !request.entity_ids.is_empty() && !request.entity_ids.contains(&key.entity_id) {
2348
+ if !request.entity_pks.is_empty() && !request.entity_pks.contains(&key.entity_pk) {
2443
2349
  return false;
2444
2350
  }
2445
2351
  if !request.file_ids.is_empty()
@@ -2459,48 +2365,28 @@ fn scan_limit_reached(request: &TrackedStateTreeScanRequest, row_count: usize) -
2459
2365
 
2460
2366
  #[cfg(test)]
2461
2367
  mod tests {
2462
- use std::sync::Arc;
2463
-
2464
2368
  use super::*;
2465
- use crate::backend::testing::UnitTestBackend;
2466
- use crate::entity_identity::EntityIdentity;
2467
- use crate::storage::{StorageContext, StorageWriteTransaction};
2369
+ use crate::entity_pk::EntityPk;
2370
+ use crate::storage::StorageContext;
2371
+ use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
2468
2372
  use crate::tracked_state::codec::encode_value;
2469
2373
 
2470
2374
  #[tokio::test]
2471
- async fn exact_read_roundtrips_from_stored_root() {
2472
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2375
+ async fn exact_read_roundtrips_from_applied_root() {
2376
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2473
2377
  let tree = TrackedStateTree::new();
2474
2378
  let key = key("schema", None, "entity");
2475
2379
  let value = value("change-1", Some("{}"));
2476
-
2477
- let mut transaction = storage
2478
- .begin_write_transaction()
2479
- .await
2480
- .expect("transaction should open");
2481
- let result = apply_mutations_for_test(
2482
- &tree,
2483
- transaction.as_mut(),
2484
- None,
2485
- vec![mutation(&key, &value)],
2486
- Some("commit-1"),
2487
- )
2488
- .await
2489
- .expect("mutations should apply");
2490
- transaction
2491
- .commit()
2492
- .await
2493
- .expect("transaction should commit");
2494
-
2495
- let mut store = storage.clone();
2496
- assert_eq!(
2497
- tree.load_root(&mut store, "commit-1")
2380
+ let result =
2381
+ apply_mutations_for_test(&tree, &storage, None, vec![mutation(&key, &value)], None)
2498
2382
  .await
2499
- .expect("root should load"),
2500
- Some(result.root_id.clone())
2501
- );
2383
+ .expect("mutations should apply");
2384
+
2385
+ let store = storage
2386
+ .begin_read(StorageReadOptions::default())
2387
+ .expect("read should open");
2502
2388
  assert_eq!(
2503
- tree.get(&mut store, &result.root_id, &key)
2389
+ tree.get(&store, &result.root_id, &key)
2504
2390
  .await
2505
2391
  .expect("row should load"),
2506
2392
  Some(value)
@@ -2509,52 +2395,40 @@ mod tests {
2509
2395
 
2510
2396
  #[tokio::test]
2511
2397
  async fn latest_mutation_for_key_wins() {
2512
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2398
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2513
2399
  let tree = TrackedStateTree::new();
2514
2400
  let key = key("schema", None, "entity");
2515
2401
  let old_value = value("change-old", Some("{\"v\":1}"));
2516
2402
  let new_value = value("change-new", Some("{\"v\":2}"));
2517
-
2518
- let mut transaction = storage
2519
- .begin_write_transaction()
2520
- .await
2521
- .expect("transaction should open");
2522
2403
  let result = apply_mutations_for_test(
2523
2404
  &tree,
2524
- transaction.as_mut(),
2405
+ &storage,
2525
2406
  None,
2526
2407
  vec![mutation(&key, &old_value), mutation(&key, &new_value)],
2527
2408
  None,
2528
2409
  )
2529
2410
  .await
2530
2411
  .expect("mutations should apply");
2531
- transaction
2532
- .commit()
2533
- .await
2534
- .expect("transaction should commit");
2535
2412
 
2536
- let mut store = storage.clone();
2413
+ let store = storage
2414
+ .begin_read(StorageReadOptions::default())
2415
+ .expect("read should open");
2537
2416
  let loaded = tree
2538
- .get(&mut store, &result.root_id, &key)
2417
+ .get(&store, &result.root_id, &key)
2539
2418
  .await
2540
2419
  .expect("row should load")
2541
2420
  .expect("row should exist");
2542
- assert_eq!(loaded.change_locator.change_id, "change-new");
2543
- assert_eq!(loaded.change_locator.source_commit_id, "commit");
2421
+ assert_eq!(loaded.change_id, "change-new");
2422
+ assert_eq!(loaded.commit_id, "commit");
2544
2423
  }
2545
2424
 
2546
2425
  #[tokio::test]
2547
2426
  async fn scan_filters_by_index_key_without_materializing_tombstones() {
2548
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2427
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2549
2428
  let tree = TrackedStateTree::new();
2550
-
2551
- let mut transaction = storage
2552
- .begin_write_transaction()
2553
- .await
2554
- .expect("transaction should open");
2555
2429
  let result = apply_mutations_for_test(
2556
2430
  &tree,
2557
- transaction.as_mut(),
2431
+ &storage,
2558
2432
  None,
2559
2433
  vec![
2560
2434
  mutation_owned(key("schema-a", None, "visible"), value("c1", Some("{}"))),
@@ -2565,15 +2439,13 @@ mod tests {
2565
2439
  )
2566
2440
  .await
2567
2441
  .expect("mutations should apply");
2568
- transaction
2569
- .commit()
2570
- .await
2571
- .expect("transaction should commit");
2572
2442
 
2573
- let mut store = storage.clone();
2443
+ let store = storage
2444
+ .begin_read(StorageReadOptions::default())
2445
+ .expect("read should open");
2574
2446
  let rows = tree
2575
2447
  .scan(
2576
- &mut store,
2448
+ &store,
2577
2449
  &result.root_id,
2578
2450
  &TrackedStateTreeScanRequest {
2579
2451
  schema_keys: vec!["schema-a".to_string()],
@@ -2585,13 +2457,13 @@ mod tests {
2585
2457
  assert_eq!(rows.len(), 2);
2586
2458
  let identities = rows
2587
2459
  .iter()
2588
- .map(|(key, _)| key.entity_id.as_single_string_owned().expect("identity"))
2460
+ .map(|(key, _)| key.entity_pk.as_single_string_owned().expect("identity"))
2589
2461
  .collect::<Vec<_>>();
2590
2462
  assert_eq!(identities, vec!["deleted", "visible"]);
2591
2463
 
2592
2464
  let live_rows = tree
2593
2465
  .scan(
2594
- &mut store,
2466
+ &store,
2595
2467
  &result.root_id,
2596
2468
  &TrackedStateTreeScanRequest {
2597
2469
  schema_keys: vec!["schema-a".to_string()],
@@ -2603,23 +2475,18 @@ mod tests {
2603
2475
  .expect("live scan should succeed");
2604
2476
  let live_identities = live_rows
2605
2477
  .iter()
2606
- .map(|(key, _)| key.entity_id.as_single_string_owned().expect("identity"))
2478
+ .map(|(key, _)| key.entity_pk.as_single_string_owned().expect("identity"))
2607
2479
  .collect::<Vec<_>>();
2608
2480
  assert_eq!(live_identities, vec!["visible"]);
2609
2481
  }
2610
2482
 
2611
2483
  #[tokio::test]
2612
2484
  async fn scan_filters_by_schema_entity_and_file() {
2613
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2485
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2614
2486
  let tree = TrackedStateTree::new();
2615
-
2616
- let mut transaction = storage
2617
- .begin_write_transaction()
2618
- .await
2619
- .expect("transaction should open");
2620
2487
  let result = apply_mutations_for_test(
2621
2488
  &tree,
2622
- transaction.as_mut(),
2489
+ &storage,
2623
2490
  None,
2624
2491
  vec![
2625
2492
  mutation_owned(
@@ -2643,19 +2510,17 @@ mod tests {
2643
2510
  )
2644
2511
  .await
2645
2512
  .expect("mutations should apply");
2646
- transaction
2647
- .commit()
2648
- .await
2649
- .expect("transaction should commit");
2650
2513
 
2651
- let mut store = storage.clone();
2514
+ let store = storage
2515
+ .begin_read(StorageReadOptions::default())
2516
+ .expect("read should open");
2652
2517
  let rows = tree
2653
2518
  .scan(
2654
- &mut store,
2519
+ &store,
2655
2520
  &result.root_id,
2656
2521
  &TrackedStateTreeScanRequest {
2657
2522
  schema_keys: vec!["schema-a".to_string()],
2658
- entity_ids: vec![crate::entity_identity::EntityIdentity::single("entity-a")],
2523
+ entity_pks: vec![crate::entity_pk::EntityPk::single("entity-a")],
2659
2524
  file_ids: vec![crate::NullableKeyFilter::Value("file-a".to_string())],
2660
2525
  ..Default::default()
2661
2526
  },
@@ -2668,7 +2533,7 @@ mod tests {
2668
2533
  assert_eq!(
2669
2534
  rows[0]
2670
2535
  .0
2671
- .entity_id
2536
+ .entity_pk
2672
2537
  .as_single_string_owned()
2673
2538
  .expect("identity"),
2674
2539
  "entity-a"
@@ -2678,16 +2543,11 @@ mod tests {
2678
2543
 
2679
2544
  #[tokio::test]
2680
2545
  async fn scan_schema_file_prefix_honors_tombstones_and_limit() {
2681
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2546
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2682
2547
  let tree = TrackedStateTree::new();
2683
-
2684
- let mut transaction = storage
2685
- .begin_write_transaction()
2686
- .await
2687
- .expect("transaction should open");
2688
2548
  let result = apply_mutations_for_test(
2689
2549
  &tree,
2690
- transaction.as_mut(),
2550
+ &storage,
2691
2551
  None,
2692
2552
  vec![
2693
2553
  mutation_owned(
@@ -2711,15 +2571,13 @@ mod tests {
2711
2571
  )
2712
2572
  .await
2713
2573
  .expect("mutations should apply");
2714
- transaction
2715
- .commit()
2716
- .await
2717
- .expect("transaction should commit");
2718
2574
 
2719
- let mut store = storage.clone();
2575
+ let store = storage
2576
+ .begin_read(StorageReadOptions::default())
2577
+ .expect("read should open");
2720
2578
  let rows = tree
2721
2579
  .scan(
2722
- &mut store,
2580
+ &store,
2723
2581
  &result.root_id,
2724
2582
  &TrackedStateTreeScanRequest {
2725
2583
  schema_keys: vec!["schema-a".to_string()],
@@ -2738,7 +2596,7 @@ mod tests {
2738
2596
  ));
2739
2597
  assert_eq!(
2740
2598
  rows.iter()
2741
- .map(|(key, _)| key.entity_id.as_single_string_owned().expect("identity"))
2599
+ .map(|(key, _)| key.entity_pk.as_single_string_owned().expect("identity"))
2742
2600
  .collect::<Vec<_>>(),
2743
2601
  vec!["entity-a", "entity-c"]
2744
2602
  );
@@ -2746,21 +2604,16 @@ mod tests {
2746
2604
 
2747
2605
  #[tokio::test]
2748
2606
  async fn applying_to_base_root_reuses_existing_rows_and_overwrites_changed_rows() {
2749
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2607
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2750
2608
  let tree = TrackedStateTree::new();
2751
2609
  let unchanged_key = key("schema", None, "unchanged");
2752
2610
  let changed_key = key("schema", None, "changed");
2753
2611
  let unchanged_value = value("c1", Some("{}"));
2754
2612
  let old_changed_value = value("c2", Some("{\"old\":true}"));
2755
2613
  let new_changed_value = value("c3", Some("{\"new\":true}"));
2756
-
2757
- let mut transaction = storage
2758
- .begin_write_transaction()
2759
- .await
2760
- .expect("transaction should open");
2761
2614
  let base = apply_mutations_for_test(
2762
2615
  &tree,
2763
- transaction.as_mut(),
2616
+ &storage,
2764
2617
  None,
2765
2618
  vec![
2766
2619
  mutation(&unchanged_key, &unchanged_value),
@@ -2772,34 +2625,30 @@ mod tests {
2772
2625
  .expect("base should build");
2773
2626
  let next = apply_mutations_for_test(
2774
2627
  &tree,
2775
- transaction.as_mut(),
2628
+ &storage,
2776
2629
  Some(&base.root_id),
2777
2630
  vec![mutation(&changed_key, &new_changed_value)],
2778
2631
  None,
2779
2632
  )
2780
2633
  .await
2781
2634
  .expect("next should build");
2782
- transaction
2783
- .commit()
2784
- .await
2785
- .expect("transaction should commit");
2786
2635
 
2787
- let mut store = storage.clone();
2636
+ let store = storage
2637
+ .begin_read(StorageReadOptions::default())
2638
+ .expect("read should open");
2788
2639
  assert_eq!(
2789
- tree.get(&mut store, &next.root_id, &unchanged_key)
2640
+ tree.get(&store, &next.root_id, &unchanged_key)
2790
2641
  .await
2791
2642
  .expect("unchanged read")
2792
2643
  .expect("unchanged exists")
2793
- .change_locator
2794
2644
  .change_id,
2795
2645
  "c1"
2796
2646
  );
2797
2647
  assert_eq!(
2798
- tree.get(&mut store, &next.root_id, &changed_key)
2648
+ tree.get(&store, &next.root_id, &changed_key)
2799
2649
  .await
2800
2650
  .expect("changed read")
2801
2651
  .expect("changed exists")
2802
- .change_locator
2803
2652
  .change_id,
2804
2653
  "c3"
2805
2654
  );
@@ -2807,7 +2656,7 @@ mod tests {
2807
2656
 
2808
2657
  #[tokio::test]
2809
2658
  async fn two_commit_roots_can_share_unchanged_rows() {
2810
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2659
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2811
2660
  let tree = TrackedStateTree::new();
2812
2661
  let shared_key = key("schema", None, "shared");
2813
2662
  let branch_a_key = key("schema", None, "branch-a");
@@ -2815,14 +2664,9 @@ mod tests {
2815
2664
  let shared_value = value("shared-change", Some("{\"shared\":true}"));
2816
2665
  let branch_a_value = value("branch-a-change", Some("{\"branch\":\"a\"}"));
2817
2666
  let branch_b_value = value("branch-b-change", Some("{\"branch\":\"b\"}"));
2818
-
2819
- let mut transaction = storage
2820
- .begin_write_transaction()
2821
- .await
2822
- .expect("transaction should open");
2823
2667
  let base = apply_mutations_for_test(
2824
2668
  &tree,
2825
- transaction.as_mut(),
2669
+ &storage,
2826
2670
  None,
2827
2671
  vec![mutation(&shared_key, &shared_value)],
2828
2672
  Some("commit-base"),
@@ -2831,7 +2675,7 @@ mod tests {
2831
2675
  .expect("base root should build");
2832
2676
  let branch_a = apply_mutations_for_test(
2833
2677
  &tree,
2834
- transaction.as_mut(),
2678
+ &storage,
2835
2679
  Some(&base.root_id),
2836
2680
  vec![mutation(&branch_a_key, &branch_a_value)],
2837
2681
  Some("commit-a"),
@@ -2840,39 +2684,37 @@ mod tests {
2840
2684
  .expect("branch a root should build");
2841
2685
  let branch_b = apply_mutations_for_test(
2842
2686
  &tree,
2843
- transaction.as_mut(),
2687
+ &storage,
2844
2688
  Some(&base.root_id),
2845
2689
  vec![mutation(&branch_b_key, &branch_b_value)],
2846
2690
  Some("commit-b"),
2847
2691
  )
2848
2692
  .await
2849
2693
  .expect("branch b root should build");
2850
- transaction
2851
- .commit()
2852
- .await
2853
- .expect("transaction should commit");
2854
2694
 
2855
2695
  assert_ne!(branch_a.root_id, branch_b.root_id);
2856
- let mut store = storage.clone();
2696
+ let store = storage
2697
+ .begin_read(StorageReadOptions::default())
2698
+ .expect("read should open");
2857
2699
  assert_eq!(
2858
- tree.get(&mut store, &branch_a.root_id, &shared_key)
2700
+ tree.get(&store, &branch_a.root_id, &shared_key)
2859
2701
  .await
2860
2702
  .expect("branch a shared row should load"),
2861
2703
  Some(value("shared-change", Some("{\"shared\":true}")))
2862
2704
  );
2863
2705
  assert_eq!(
2864
- tree.get(&mut store, &branch_b.root_id, &shared_key)
2706
+ tree.get(&store, &branch_b.root_id, &shared_key)
2865
2707
  .await
2866
2708
  .expect("branch b shared row should load"),
2867
2709
  Some(value("shared-change", Some("{\"shared\":true}")))
2868
2710
  );
2869
2711
  assert!(tree
2870
- .get(&mut store, &branch_a.root_id, &branch_b_key)
2712
+ .get(&store, &branch_a.root_id, &branch_b_key)
2871
2713
  .await
2872
2714
  .expect("branch a should read")
2873
2715
  .is_none());
2874
2716
  assert!(tree
2875
- .get(&mut store, &branch_b.root_id, &branch_a_key)
2717
+ .get(&store, &branch_b.root_id, &branch_a_key)
2876
2718
  .await
2877
2719
  .expect("branch b should read")
2878
2720
  .is_none());
@@ -2880,7 +2722,7 @@ mod tests {
2880
2722
 
2881
2723
  #[tokio::test]
2882
2724
  async fn single_update_matches_full_canonical_rebuild() {
2883
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2725
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2884
2726
  let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
2885
2727
  target_chunk_bytes: 128,
2886
2728
  min_chunk_bytes: 64,
@@ -2896,25 +2738,23 @@ mod tests {
2896
2738
  .collect::<Vec<_>>();
2897
2739
  let changed_key = key("schema", None, "entity-000");
2898
2740
  let changed_value = value("changed", Some("{\"v\":\"changed\"}"));
2899
-
2900
- let mut transaction = storage
2901
- .begin_write_transaction()
2902
- .await
2903
- .expect("transaction should open");
2904
- let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2741
+ let base = apply_mutations_for_test(&tree, &storage, None, rows, None)
2905
2742
  .await
2906
2743
  .expect("base should build");
2907
2744
  let fast = apply_mutations_for_test(
2908
2745
  &tree,
2909
- transaction.as_mut(),
2746
+ &storage,
2910
2747
  Some(&base.root_id),
2911
2748
  vec![mutation(&changed_key, &changed_value)],
2912
2749
  None,
2913
2750
  )
2914
2751
  .await
2915
2752
  .expect("fast path should apply");
2753
+ let read = storage
2754
+ .begin_read(StorageReadOptions::default())
2755
+ .expect("read should open");
2916
2756
  let mut canonical_entries = tree
2917
- .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2757
+ .collect_leaf_entries(&read, &base.root_id)
2918
2758
  .await
2919
2759
  .expect("base entries should collect");
2920
2760
  assert!(canonical_entries
@@ -2935,7 +2775,7 @@ mod tests {
2935
2775
 
2936
2776
  #[tokio::test]
2937
2777
  async fn single_insert_matches_full_canonical_rebuild() {
2938
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2778
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2939
2779
  let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
2940
2780
  target_chunk_bytes: 128,
2941
2781
  min_chunk_bytes: 64,
@@ -2951,25 +2791,23 @@ mod tests {
2951
2791
  .collect::<Vec<_>>();
2952
2792
  let inserted_key = key("schema", None, "entity-050a");
2953
2793
  let inserted_value = value("inserted", Some("{\"v\":\"inserted\"}"));
2954
-
2955
- let mut transaction = storage
2956
- .begin_write_transaction()
2957
- .await
2958
- .expect("transaction should open");
2959
- let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2794
+ let base = apply_mutations_for_test(&tree, &storage, None, rows, None)
2960
2795
  .await
2961
2796
  .expect("base should build");
2962
2797
  let fast = apply_mutations_for_test(
2963
2798
  &tree,
2964
- transaction.as_mut(),
2799
+ &storage,
2965
2800
  Some(&base.root_id),
2966
2801
  vec![mutation(&inserted_key, &inserted_value)],
2967
2802
  None,
2968
2803
  )
2969
2804
  .await
2970
2805
  .expect("fast path should apply");
2806
+ let read = storage
2807
+ .begin_read(StorageReadOptions::default())
2808
+ .expect("read should open");
2971
2809
  let mut canonical_entries = tree
2972
- .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2810
+ .collect_leaf_entries(&read, &base.root_id)
2973
2811
  .await
2974
2812
  .expect("base entries should collect");
2975
2813
  let encoded_inserted_key = encode_key(&inserted_key);
@@ -2993,7 +2831,7 @@ mod tests {
2993
2831
 
2994
2832
  #[tokio::test]
2995
2833
  async fn batch_update_matches_full_canonical_rebuild() {
2996
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2834
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
2997
2835
  let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
2998
2836
  target_chunk_bytes: 128,
2999
2837
  min_chunk_bytes: 64,
@@ -3018,17 +2856,12 @@ mod tests {
3018
2856
  )
3019
2857
  })
3020
2858
  .collect::<Vec<_>>();
3021
-
3022
- let mut transaction = storage
3023
- .begin_write_transaction()
3024
- .await
3025
- .expect("transaction should open");
3026
- let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2859
+ let base = apply_mutations_for_test(&tree, &storage, None, rows, None)
3027
2860
  .await
3028
2861
  .expect("base should build");
3029
2862
  let fast = apply_mutations_for_test(
3030
2863
  &tree,
3031
- transaction.as_mut(),
2864
+ &storage,
3032
2865
  Some(&base.root_id),
3033
2866
  updates
3034
2867
  .iter()
@@ -3038,8 +2871,11 @@ mod tests {
3038
2871
  )
3039
2872
  .await
3040
2873
  .expect("batch path should apply");
2874
+ let read = storage
2875
+ .begin_read(StorageReadOptions::default())
2876
+ .expect("read should open");
3041
2877
  let mut canonical_entries = tree
3042
- .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2878
+ .collect_leaf_entries(&read, &base.root_id)
3043
2879
  .await
3044
2880
  .expect("base entries should collect");
3045
2881
  for (key, value) in updates {
@@ -3059,7 +2895,7 @@ mod tests {
3059
2895
 
3060
2896
  #[tokio::test]
3061
2897
  async fn batch_insert_matches_full_canonical_rebuild() {
3062
- let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2898
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
3063
2899
  let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
3064
2900
  target_chunk_bytes: 128,
3065
2901
  min_chunk_bytes: 64,
@@ -3076,9 +2912,9 @@ mod tests {
3076
2912
  let inserts = ["entity-050a", "entity-050b", "entity-050c"]
3077
2913
  .into_iter()
3078
2914
  .enumerate()
3079
- .map(|(index, entity_id)| {
2915
+ .map(|(index, entity_pk)| {
3080
2916
  (
3081
- key("schema", None, entity_id),
2917
+ key("schema", None, entity_pk),
3082
2918
  value(
3083
2919
  &format!("inserted-{index}"),
3084
2920
  Some(&format!("{{\"inserted\":{index}}}")),
@@ -3086,17 +2922,12 @@ mod tests {
3086
2922
  )
3087
2923
  })
3088
2924
  .collect::<Vec<_>>();
3089
-
3090
- let mut transaction = storage
3091
- .begin_write_transaction()
3092
- .await
3093
- .expect("transaction should open");
3094
- let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2925
+ let base = apply_mutations_for_test(&tree, &storage, None, rows, None)
3095
2926
  .await
3096
2927
  .expect("base should build");
3097
2928
  let fast = apply_mutations_for_test(
3098
2929
  &tree,
3099
- transaction.as_mut(),
2930
+ &storage,
3100
2931
  Some(&base.root_id),
3101
2932
  inserts
3102
2933
  .iter()
@@ -3106,8 +2937,11 @@ mod tests {
3106
2937
  )
3107
2938
  .await
3108
2939
  .expect("batch path should apply");
2940
+ let read = storage
2941
+ .begin_read(StorageReadOptions::default())
2942
+ .expect("read should open");
3109
2943
  let mut canonical_entries = tree
3110
- .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2944
+ .collect_leaf_entries(&read, &base.root_id)
3111
2945
  .await
3112
2946
  .expect("base entries should collect");
3113
2947
  for (key, value) in inserts {
@@ -3131,18 +2965,37 @@ mod tests {
3131
2965
  assert_eq!(fast.root_id, canonical.root_id);
3132
2966
  }
3133
2967
 
2968
+ #[test]
2969
+ fn leaf_chunk_boundaries_ignore_value_bytes() {
2970
+ let options = TrackedStateTreeOptions {
2971
+ target_chunk_bytes: 64,
2972
+ min_chunk_bytes: 32,
2973
+ max_chunk_bytes: 96,
2974
+ };
2975
+ let short_entries = encoded_entries_with_change_id("c");
2976
+ let large_entries = encoded_entries_with_change_id(&"c".repeat(4096));
2977
+
2978
+ assert_eq!(
2979
+ leaf_chunk_boundary_keys(chunk_leaf_entries(short_entries, &options)),
2980
+ leaf_chunk_boundary_keys(chunk_leaf_entries(large_entries, &options))
2981
+ );
2982
+ }
2983
+
3134
2984
  async fn apply_mutations_for_test(
3135
2985
  tree: &TrackedStateTree,
3136
- transaction: &mut dyn StorageWriteTransaction,
2986
+ storage: &StorageContext,
3137
2987
  base_root: Option<&TrackedStateRootId>,
3138
2988
  mutations: Vec<TrackedStateMutation>,
3139
2989
  commit_id: Option<&str>,
3140
2990
  ) -> Result<TrackedStateApplyResult, LixError> {
3141
- let mut writes = StorageWriteSet::new();
2991
+ let read = storage
2992
+ .begin_read(StorageReadOptions::default())
2993
+ .expect("read should open");
2994
+ let mut writes = storage.new_write_set();
3142
2995
  let result = tree
3143
- .apply_mutations(transaction, &mut writes, base_root, mutations, commit_id)
2996
+ .apply_mutations(&read, &mut writes, base_root, mutations, commit_id)
3144
2997
  .await?;
3145
- writes.apply(transaction).await?;
2998
+ storage.commit_write_set(writes, StorageWriteOptions::default())?;
3146
2999
  Ok(result)
3147
3000
  }
3148
3001
 
@@ -3154,28 +3007,51 @@ mod tests {
3154
3007
  mutation(&key, &value)
3155
3008
  }
3156
3009
 
3157
- fn key(schema_key: &str, file_id: Option<&str>, entity_id: &str) -> TrackedStateKey {
3010
+ fn encoded_entries_with_change_id(change_id: &str) -> Vec<EncodedLeafEntry> {
3011
+ (0..64)
3012
+ .map(|index| {
3013
+ let key = key("schema", None, &format!("entity-{index:03}"));
3014
+ EncodedLeafEntry {
3015
+ key: encode_key(&key),
3016
+ value: encode_value(&value(change_id, Some("{}"))),
3017
+ }
3018
+ })
3019
+ .collect()
3020
+ }
3021
+
3022
+ fn leaf_chunk_boundary_keys(
3023
+ groups: Vec<LeafChunkAccumulator>,
3024
+ ) -> Vec<(Vec<u8>, Vec<u8>, usize)> {
3025
+ groups
3026
+ .into_iter()
3027
+ .map(|group| {
3028
+ let first_key = group
3029
+ .entries
3030
+ .first()
3031
+ .map(|entry| entry.key.clone())
3032
+ .unwrap_or_default();
3033
+ let last_key = group
3034
+ .entries
3035
+ .last()
3036
+ .map(|entry| entry.key.clone())
3037
+ .unwrap_or_default();
3038
+ (first_key, last_key, group.entries.len())
3039
+ })
3040
+ .collect()
3041
+ }
3042
+
3043
+ fn key(schema_key: &str, file_id: Option<&str>, entity_pk: &str) -> TrackedStateKey {
3158
3044
  TrackedStateKey {
3159
3045
  schema_key: schema_key.to_string(),
3160
3046
  file_id: file_id.map(str::to_string),
3161
- entity_id: EntityIdentity::single(entity_id),
3047
+ entity_pk: EntityPk::single(entity_pk),
3162
3048
  }
3163
3049
  }
3164
3050
 
3165
3051
  fn value(change_id: &str, snapshot_content: Option<&str>) -> TrackedStateIndexValue {
3166
- let source_ordinal = match snapshot_content {
3167
- Some("{\"v\":1}") => 1,
3168
- Some("{\"v\":2}") => 2,
3169
- Some(_) => 3,
3170
- None => 0,
3171
- };
3172
3052
  TrackedStateIndexValue {
3173
- change_locator: crate::commit_store::ChangeLocator {
3174
- source_commit_id: "commit".to_string(),
3175
- source_pack_id: 0,
3176
- source_ordinal,
3177
- change_id: change_id.to_string(),
3178
- },
3053
+ change_id: change_id.to_string(),
3054
+ commit_id: "commit".to_string(),
3179
3055
  deleted: snapshot_content.is_none(),
3180
3056
  snapshot_ref: snapshot_content
3181
3057
  .map(|content| crate::json_store::JsonRef::for_content(content.as_bytes())),