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

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 (196) hide show
  1. package/README.md +9 -0
  2. package/SKILL.md +468 -0
  3. package/dist/engine-wasm/index.d.ts +15 -11
  4. package/dist/engine-wasm/index.js +105 -38
  5. package/dist/engine-wasm/wasm/lix_engine.d.ts +14 -2
  6. package/dist/engine-wasm/wasm/lix_engine.js +18 -17
  7. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  8. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +2 -1
  9. package/dist/generated/builtin-schemas.d.ts +31 -41
  10. package/dist/generated/builtin-schemas.js +52 -56
  11. package/dist/open-lix.d.ts +141 -24
  12. package/dist/open-lix.js +199 -35
  13. package/dist/sqlite/index.js +99 -22
  14. package/dist-engine-src/README.md +18 -0
  15. package/dist-engine-src/src/backend/kv.rs +358 -0
  16. package/dist-engine-src/src/backend/mod.rs +12 -0
  17. package/dist-engine-src/src/backend/testing.rs +658 -0
  18. package/dist-engine-src/src/backend/types.rs +96 -0
  19. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  20. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  21. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  22. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  23. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  24. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  25. package/dist-engine-src/src/cel/context.rs +86 -0
  26. package/dist-engine-src/src/cel/error.rs +19 -0
  27. package/dist-engine-src/src/cel/mod.rs +8 -0
  28. package/dist-engine-src/src/cel/provider.rs +9 -0
  29. package/dist-engine-src/src/cel/runtime.rs +167 -0
  30. package/dist-engine-src/src/cel/value.rs +50 -0
  31. package/dist-engine-src/src/changelog/codec.rs +321 -0
  32. package/dist-engine-src/src/changelog/context.rs +92 -0
  33. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  34. package/dist-engine-src/src/changelog/mod.rs +13 -0
  35. package/dist-engine-src/src/changelog/reader.rs +20 -0
  36. package/dist-engine-src/src/changelog/storage.rs +220 -0
  37. package/dist-engine-src/src/changelog/types.rs +38 -0
  38. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  39. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  40. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  41. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  42. package/dist-engine-src/src/common/error.rs +313 -0
  43. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  44. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  45. package/dist-engine-src/src/common/identity.rs +135 -0
  46. package/dist-engine-src/src/common/metadata.rs +35 -0
  47. package/dist-engine-src/src/common/mod.rs +23 -0
  48. package/dist-engine-src/src/common/types.rs +105 -0
  49. package/dist-engine-src/src/common/wire.rs +222 -0
  50. package/dist-engine-src/src/engine.rs +239 -0
  51. package/dist-engine-src/src/entity_identity.rs +285 -0
  52. package/dist-engine-src/src/functions/context.rs +327 -0
  53. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  54. package/dist-engine-src/src/functions/mod.rs +18 -0
  55. package/dist-engine-src/src/functions/provider.rs +130 -0
  56. package/dist-engine-src/src/functions/state.rs +363 -0
  57. package/dist-engine-src/src/functions/types.rs +37 -0
  58. package/dist-engine-src/src/init.rs +505 -0
  59. package/dist-engine-src/src/json_store/compression.rs +77 -0
  60. package/dist-engine-src/src/json_store/context.rs +129 -0
  61. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  62. package/dist-engine-src/src/json_store/mod.rs +9 -0
  63. package/dist-engine-src/src/json_store/store.rs +236 -0
  64. package/dist-engine-src/src/json_store/types.rs +52 -0
  65. package/dist-engine-src/src/lib.rs +61 -0
  66. package/dist-engine-src/src/live_state/context.rs +2241 -0
  67. package/dist-engine-src/src/live_state/mod.rs +15 -0
  68. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  69. package/dist-engine-src/src/live_state/reader.rs +23 -0
  70. package/dist-engine-src/src/live_state/types.rs +239 -0
  71. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  72. package/dist-engine-src/src/plugin/archive.rs +441 -0
  73. package/dist-engine-src/src/plugin/component.rs +183 -0
  74. package/dist-engine-src/src/plugin/install.rs +637 -0
  75. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  76. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  77. package/dist-engine-src/src/plugin/mod.rs +33 -0
  78. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  79. package/dist-engine-src/src/plugin/storage.rs +74 -0
  80. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  81. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  82. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  83. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  84. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  85. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  86. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  87. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  88. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  89. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  90. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  91. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  92. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  93. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  94. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  95. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  96. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  97. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  98. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  99. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  100. package/dist-engine-src/src/schema/definition.json +157 -0
  101. package/dist-engine-src/src/schema/definition.rs +636 -0
  102. package/dist-engine-src/src/schema/key.rs +206 -0
  103. package/dist-engine-src/src/schema/mod.rs +20 -0
  104. package/dist-engine-src/src/schema/seed.rs +14 -0
  105. package/dist-engine-src/src/schema/tests.rs +739 -0
  106. package/dist-engine-src/src/schema_registry.rs +294 -0
  107. package/dist-engine-src/src/session/context.rs +366 -0
  108. package/dist-engine-src/src/session/create_version.rs +80 -0
  109. package/dist-engine-src/src/session/execute.rs +447 -0
  110. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  111. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  112. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  113. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  114. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  115. package/dist-engine-src/src/session/merge/version.rs +437 -0
  116. package/dist-engine-src/src/session/mod.rs +25 -0
  117. package/dist-engine-src/src/session/switch_version.rs +121 -0
  118. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  119. package/dist-engine-src/src/sql2/classify.rs +147 -0
  120. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  121. package/dist-engine-src/src/sql2/context.rs +307 -0
  122. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  123. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  124. package/dist-engine-src/src/sql2/dml.rs +148 -0
  125. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  126. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  127. package/dist-engine-src/src/sql2/error.rs +196 -0
  128. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  129. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  130. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  131. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  132. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  133. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  134. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  135. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  136. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  137. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  138. package/dist-engine-src/src/sql2/mod.rs +43 -0
  139. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  140. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  141. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  142. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  143. package/dist-engine-src/src/sql2/session.rs +135 -0
  144. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  148. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  149. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  150. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  151. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  152. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  153. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  154. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  155. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  156. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  157. package/dist-engine-src/src/storage/context.rs +356 -0
  158. package/dist-engine-src/src/storage/mod.rs +14 -0
  159. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  160. package/dist-engine-src/src/storage/types.rs +501 -0
  161. package/dist-engine-src/src/storage_bench.rs +3406 -0
  162. package/dist-engine-src/src/test_support.rs +81 -0
  163. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  164. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  165. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  166. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  167. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  168. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  169. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  170. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  171. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  172. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  173. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  174. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  175. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  176. package/dist-engine-src/src/transaction/context.rs +1307 -0
  177. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  178. package/dist-engine-src/src/transaction/mod.rs +11 -0
  179. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  180. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  181. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  182. package/dist-engine-src/src/transaction/types.rs +351 -0
  183. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  184. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  185. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  186. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  187. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  188. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  189. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  190. package/dist-engine-src/src/version/context.rs +52 -0
  191. package/dist-engine-src/src/version/mod.rs +12 -0
  192. package/dist-engine-src/src/version/refs.rs +421 -0
  193. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  194. package/dist-engine-src/src/version/types.rs +21 -0
  195. package/dist-engine-src/src/wasm/mod.rs +60 -0
  196. package/package.json +68 -63
@@ -0,0 +1,2744 @@
1
+ use std::{collections::BTreeMap, future::Future, ops::Range, pin::Pin};
2
+
3
+ use crate::storage::{StorageReader, StorageWriteSet};
4
+ use crate::tracked_state::codec::{
5
+ boundary_trigger, child_summary_from_node, decode_key, decode_node, decode_value,
6
+ encode_internal_node, encode_key, encode_leaf_node, encode_schema_file_prefix,
7
+ encode_schema_key_prefix, encode_value, ChildSummary, DecodedNode, EncodedLeafEntry,
8
+ PendingChunkWrite,
9
+ };
10
+ use crate::tracked_state::storage;
11
+ use crate::tracked_state::tree_types::{
12
+ TrackedStateApplyResult, TrackedStateKey, TrackedStateMutation, TrackedStateRootId,
13
+ TrackedStateTreeDiffEntry, TrackedStateTreeScanRequest, TrackedStateValue,
14
+ TRACKED_STATE_HASH_BYTES,
15
+ };
16
+ use crate::{LixError, NullableKeyFilter};
17
+
18
+ #[derive(Debug, Clone, PartialEq, Eq)]
19
+ pub(crate) struct TrackedStateTreeOptions {
20
+ pub(crate) target_chunk_bytes: usize,
21
+ pub(crate) min_chunk_bytes: usize,
22
+ pub(crate) max_chunk_bytes: usize,
23
+ }
24
+
25
+ impl Default for TrackedStateTreeOptions {
26
+ fn default() -> Self {
27
+ Self {
28
+ target_chunk_bytes: 4 * 1024,
29
+ min_chunk_bytes: 512,
30
+ max_chunk_bytes: 16 * 1024,
31
+ }
32
+ }
33
+ }
34
+
35
+ /// Content-addressed tracked-state tree operations.
36
+ ///
37
+ /// This type owns tracked-state tree mechanics only. Version refs, untracked overlay,
38
+ /// and SQL visibility remain outside the tree.
39
+ #[derive(Debug, Clone)]
40
+ pub(crate) struct TrackedStateTree {
41
+ options: TrackedStateTreeOptions,
42
+ }
43
+
44
+ impl TrackedStateTree {
45
+ pub(crate) fn new() -> Self {
46
+ Self {
47
+ options: TrackedStateTreeOptions::default(),
48
+ }
49
+ }
50
+
51
+ #[allow(dead_code)]
52
+ pub(crate) fn with_options(options: TrackedStateTreeOptions) -> Self {
53
+ Self { options }
54
+ }
55
+
56
+ pub(crate) async fn load_root(
57
+ &self,
58
+ store: &mut (impl StorageReader + ?Sized),
59
+ commit_id: &str,
60
+ ) -> Result<Option<TrackedStateRootId>, LixError> {
61
+ storage::load_root(store, commit_id).await
62
+ }
63
+
64
+ pub(crate) async fn get(
65
+ &self,
66
+ store: &mut impl StorageReader,
67
+ root_id: &TrackedStateRootId,
68
+ key: &TrackedStateKey,
69
+ ) -> Result<Option<TrackedStateValue>, LixError> {
70
+ let encoded_key = encode_key(key);
71
+ let mut current = *root_id.as_bytes();
72
+ loop {
73
+ match self.load_node(store, &current).await? {
74
+ DecodedNode::Leaf(leaf) => {
75
+ let entry = leaf
76
+ .entries()
77
+ .binary_search_by(|entry| entry.key.as_slice().cmp(&encoded_key))
78
+ .ok()
79
+ .map(|index| &leaf.entries()[index]);
80
+ return entry.map(|entry| decode_value(&entry.value)).transpose();
81
+ }
82
+ DecodedNode::Internal(internal) => {
83
+ let child = internal
84
+ .children()
85
+ .iter()
86
+ .find(|child| child.last_key.as_slice() >= encoded_key.as_slice())
87
+ .or_else(|| internal.children().last())
88
+ .ok_or_else(|| {
89
+ LixError::new(
90
+ "LIX_ERROR_UNKNOWN",
91
+ "tracked-state tree internal node has no children",
92
+ )
93
+ })?;
94
+ current = child.child_hash;
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ pub(crate) async fn get_many(
101
+ &self,
102
+ store: &mut impl StorageReader,
103
+ root_id: &TrackedStateRootId,
104
+ keys: &[TrackedStateKey],
105
+ ) -> Result<Vec<Option<TrackedStateValue>>, LixError> {
106
+ if keys.is_empty() {
107
+ return Ok(Vec::new());
108
+ }
109
+
110
+ let mut encoded_keys = keys
111
+ .iter()
112
+ .enumerate()
113
+ .map(|(index, key)| (index, encode_key(key)))
114
+ .collect::<Vec<_>>();
115
+ encoded_keys.sort_by(|left, right| left.1.cmp(&right.1));
116
+
117
+ let mut values = vec![None; keys.len()];
118
+ self.get_many_node(store, *root_id.as_bytes(), &encoded_keys, &mut values)
119
+ .await?;
120
+ Ok(values)
121
+ }
122
+
123
+ pub(crate) async fn row_count(
124
+ &self,
125
+ store: &mut impl StorageReader,
126
+ root_id: &TrackedStateRootId,
127
+ ) -> Result<usize, LixError> {
128
+ match self.load_node(store, root_id.as_bytes()).await? {
129
+ DecodedNode::Leaf(leaf) => Ok(leaf.entries().len()),
130
+ DecodedNode::Internal(internal) => Ok(internal
131
+ .children()
132
+ .iter()
133
+ .map(|child| child.subtree_count as usize)
134
+ .sum()),
135
+ }
136
+ }
137
+
138
+ pub(crate) async fn scan(
139
+ &self,
140
+ store: &mut impl StorageReader,
141
+ root_id: &TrackedStateRootId,
142
+ request: &TrackedStateTreeScanRequest,
143
+ ) -> Result<Vec<(TrackedStateKey, TrackedStateValue)>, LixError> {
144
+ if request.limit == Some(0) {
145
+ return Ok(Vec::new());
146
+ }
147
+
148
+ let ranges = scan_ranges(request);
149
+ let mut rows = Vec::new();
150
+ self.scan_node(store, *root_id.as_bytes(), request, &ranges, &mut rows)
151
+ .await?;
152
+ Ok(rows)
153
+ }
154
+
155
+ pub(crate) async fn count_matching_keys(
156
+ &self,
157
+ store: &mut impl StorageReader,
158
+ root_id: &TrackedStateRootId,
159
+ request: &TrackedStateTreeScanRequest,
160
+ ) -> Result<usize, LixError> {
161
+ if request.limit == Some(0) {
162
+ return Ok(0);
163
+ }
164
+
165
+ let ranges = scan_ranges(request);
166
+ self.count_matching_keys_node(store, *root_id.as_bytes(), request, &ranges)
167
+ .await
168
+ }
169
+
170
+ pub(crate) async fn diff(
171
+ &self,
172
+ store: &mut impl StorageReader,
173
+ left_root: Option<&TrackedStateRootId>,
174
+ right_root: Option<&TrackedStateRootId>,
175
+ request: &TrackedStateTreeScanRequest,
176
+ ) -> Result<Vec<TrackedStateTreeDiffEntry>, LixError> {
177
+ match (left_root, right_root) {
178
+ (None, None) => Ok(Vec::new()),
179
+ (Some(left), Some(right)) if left == right => Ok(Vec::new()),
180
+ (Some(left), Some(right)) => {
181
+ let mut out = Vec::new();
182
+ self.diff_nodes(
183
+ store,
184
+ *left.as_bytes(),
185
+ *right.as_bytes(),
186
+ request,
187
+ &mut out,
188
+ )
189
+ .await?;
190
+ Ok(out)
191
+ }
192
+ (Some(left), None) => Ok(self
193
+ .collect_filtered_entries(store, left, request)
194
+ .await?
195
+ .into_iter()
196
+ .map(|(key, value)| TrackedStateTreeDiffEntry {
197
+ before: Some((key, value)),
198
+ after: None,
199
+ })
200
+ .collect()),
201
+ (None, Some(right)) => Ok(self
202
+ .collect_filtered_entries(store, right, request)
203
+ .await?
204
+ .into_iter()
205
+ .map(|(key, value)| TrackedStateTreeDiffEntry {
206
+ before: None,
207
+ after: Some((key, value)),
208
+ })
209
+ .collect()),
210
+ }
211
+ }
212
+
213
+ pub(crate) async fn apply_mutations(
214
+ &self,
215
+ store: &mut (impl StorageReader + ?Sized),
216
+ writes: &mut StorageWriteSet,
217
+ base_root: Option<&TrackedStateRootId>,
218
+ mutations: Vec<TrackedStateMutation>,
219
+ commit_id: Option<&str>,
220
+ ) -> Result<TrackedStateApplyResult, LixError> {
221
+ let mut overlay = storage::TrackedStateChunkOverlay::new();
222
+ if let Some(root_id) = base_root {
223
+ if mutations.len() == 1 {
224
+ if let Some(result) = self
225
+ .apply_single_mutation(
226
+ store,
227
+ writes,
228
+ &mut overlay,
229
+ root_id,
230
+ &mutations[0],
231
+ commit_id,
232
+ )
233
+ .await?
234
+ {
235
+ return Ok(result);
236
+ }
237
+ } else if mutations.len() > 1 {
238
+ if let Some(result) = self
239
+ .apply_sorted_mutations_chunker(
240
+ store,
241
+ writes,
242
+ &mut overlay,
243
+ root_id,
244
+ &mutations,
245
+ commit_id,
246
+ )
247
+ .await?
248
+ {
249
+ return Ok(result);
250
+ }
251
+ }
252
+ }
253
+
254
+ let mut entries = match base_root {
255
+ Some(root_id) => self
256
+ .collect_leaf_entries(store, root_id)
257
+ .await?
258
+ .into_iter()
259
+ .map(|entry| (entry.key, entry.value))
260
+ .collect::<BTreeMap<_, _>>(),
261
+ None => BTreeMap::new(),
262
+ };
263
+
264
+ // Apply in caller order so repeated writes to the same key behave like
265
+ // normal transaction staging: the latest mutation wins.
266
+ for mutation in mutations {
267
+ match mutation {
268
+ TrackedStateMutation::Put { key, value } => {
269
+ entries.insert(encode_key(&key), encode_value(&value));
270
+ }
271
+ }
272
+ }
273
+
274
+ let built = self.build_tree_from_entries(
275
+ entries
276
+ .into_iter()
277
+ .map(|(key, value)| EncodedLeafEntry { key, value })
278
+ .collect(),
279
+ )?;
280
+ overlay.stage_chunks(writes, &built.chunks);
281
+ let persisted_root = if let Some(commit_id) = commit_id {
282
+ storage::stage_root(writes, commit_id, &built.root_id);
283
+ true
284
+ } else {
285
+ false
286
+ };
287
+
288
+ Ok(TrackedStateApplyResult {
289
+ root_id: built.root_id,
290
+ row_count: built.row_count,
291
+ tree_height: built.tree_height,
292
+ chunk_count: built.chunks.len(),
293
+ chunk_bytes: built.chunk_bytes,
294
+ persisted_root,
295
+ })
296
+ }
297
+
298
+ async fn apply_single_mutation(
299
+ &self,
300
+ store: &mut (impl StorageReader + ?Sized),
301
+ writes: &mut StorageWriteSet,
302
+ overlay: &mut storage::TrackedStateChunkOverlay,
303
+ root_id: &TrackedStateRootId,
304
+ mutation: &TrackedStateMutation,
305
+ commit_id: Option<&str>,
306
+ ) -> Result<Option<TrackedStateApplyResult>, LixError> {
307
+ let TrackedStateMutation::Put { key, value } = mutation;
308
+ let encoded_key = encode_key(key);
309
+ let encoded_value = encode_value(value);
310
+
311
+ if let Some(result) = self
312
+ .apply_single_mutation_from_seek_path(
313
+ store,
314
+ writes,
315
+ overlay,
316
+ root_id,
317
+ &encoded_key,
318
+ &encoded_value,
319
+ commit_id,
320
+ )
321
+ .await?
322
+ {
323
+ return Ok(result);
324
+ }
325
+
326
+ let levels = self
327
+ .collect_summary_levels_with_overlay(store, overlay, root_id)
328
+ .await?;
329
+ let Some(leaves) = levels.first() else {
330
+ return Ok(None);
331
+ };
332
+ let target_leaf_index = leaves
333
+ .iter()
334
+ .position(|leaf| leaf.last_key.as_slice() >= encoded_key.as_slice())
335
+ .unwrap_or_else(|| leaves.len().saturating_sub(1));
336
+ let Some(target_leaf) = leaves.get(target_leaf_index).cloned() else {
337
+ return Ok(None);
338
+ };
339
+
340
+ let mut entries = self
341
+ .load_leaf_entries_with_overlay(store, overlay, &target_leaf.child_hash)
342
+ .await?;
343
+ match entries.binary_search_by(|entry| entry.key.as_slice().cmp(&encoded_key)) {
344
+ Ok(index) => {
345
+ if entries[index].value == encoded_value {
346
+ return Ok(None);
347
+ }
348
+ entries[index].value = encoded_value;
349
+ }
350
+ Err(index) => entries.insert(
351
+ index,
352
+ EncodedLeafEntry {
353
+ key: encoded_key.clone(),
354
+ value: encoded_value,
355
+ },
356
+ ),
357
+ }
358
+
359
+ let mut chunks = BTreeMap::new();
360
+ let mut suffix_entries = entries;
361
+ let mut next_leaf_index = target_leaf_index + 1;
362
+ let mut replacement_leaves;
363
+ let old_leaf_count;
364
+
365
+ // Rechunk from the edited leaf until a generated leaf matches an
366
+ // existing post-mutation leaf, then reuse the rest of the old suffix.
367
+ loop {
368
+ let mut candidate_chunks = BTreeMap::new();
369
+ let candidate_summaries =
370
+ self.build_leaf_level(suffix_entries.clone(), &mut candidate_chunks);
371
+
372
+ if let Some((generated_resync_index, existing_resync_index)) = first_resync_index(
373
+ &candidate_summaries,
374
+ &leaves[target_leaf_index..],
375
+ &encoded_key,
376
+ ) {
377
+ for summary in &candidate_summaries[..generated_resync_index] {
378
+ if let Some(chunk) = candidate_chunks.remove(&summary.child_hash) {
379
+ chunks.entry(chunk.hash).or_insert(chunk);
380
+ }
381
+ }
382
+ replacement_leaves = candidate_summaries[..generated_resync_index].to_vec();
383
+ old_leaf_count = existing_resync_index;
384
+ break;
385
+ }
386
+
387
+ if next_leaf_index >= leaves.len() {
388
+ chunks.extend(candidate_chunks);
389
+ replacement_leaves = candidate_summaries;
390
+ old_leaf_count = leaves.len() - target_leaf_index;
391
+ break;
392
+ }
393
+
394
+ suffix_entries.extend(
395
+ self.load_leaf_entries_with_overlay(
396
+ store,
397
+ overlay,
398
+ &leaves[next_leaf_index].child_hash,
399
+ )
400
+ .await?,
401
+ );
402
+ next_leaf_index += 1;
403
+ }
404
+
405
+ let built = self.build_tree_from_leaf_patch(
406
+ &levels,
407
+ target_leaf_index,
408
+ old_leaf_count,
409
+ std::mem::take(&mut replacement_leaves),
410
+ chunks,
411
+ &encoded_key,
412
+ )?;
413
+ overlay.stage_chunks(writes, &built.chunks);
414
+ let persisted_root = if let Some(commit_id) = commit_id {
415
+ storage::stage_root(writes, commit_id, &built.root_id);
416
+ true
417
+ } else {
418
+ false
419
+ };
420
+
421
+ Ok(Some(TrackedStateApplyResult {
422
+ root_id: built.root_id,
423
+ row_count: built.row_count,
424
+ tree_height: built.tree_height,
425
+ chunk_count: built.chunks.len(),
426
+ chunk_bytes: built.chunk_bytes,
427
+ persisted_root,
428
+ }))
429
+ }
430
+
431
+ fn diff_nodes<'a, S>(
432
+ &'a self,
433
+ store: &'a mut S,
434
+ left_hash: [u8; TRACKED_STATE_HASH_BYTES],
435
+ right_hash: [u8; TRACKED_STATE_HASH_BYTES],
436
+ request: &'a TrackedStateTreeScanRequest,
437
+ out: &'a mut Vec<TrackedStateTreeDiffEntry>,
438
+ ) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + 'a>>
439
+ where
440
+ S: StorageReader + 'a,
441
+ {
442
+ Box::pin(async move {
443
+ if left_hash == right_hash {
444
+ return Ok(());
445
+ }
446
+
447
+ let left = self.load_node(store, &left_hash).await?;
448
+ let right = self.load_node(store, &right_hash).await?;
449
+ match (left, right) {
450
+ (DecodedNode::Leaf(left), DecodedNode::Leaf(right)) => {
451
+ self.diff_leaf_entries(left.entries(), right.entries(), request, out)?;
452
+ }
453
+ (DecodedNode::Internal(left), DecodedNode::Internal(right))
454
+ if internal_boundaries_match(left.children(), right.children()) =>
455
+ {
456
+ for (left_child, right_child) in left.children().iter().zip(right.children()) {
457
+ if left_child == right_child {
458
+ continue;
459
+ }
460
+ self.diff_nodes(
461
+ store,
462
+ left_child.child_hash,
463
+ right_child.child_hash,
464
+ request,
465
+ out,
466
+ )
467
+ .await?;
468
+ }
469
+ }
470
+ _ => {
471
+ self.diff_leaf_summary_cursors(store, left_hash, right_hash, request, out)
472
+ .await?;
473
+ }
474
+ }
475
+ Ok(())
476
+ })
477
+ }
478
+
479
+ async fn diff_leaf_summary_cursors(
480
+ &self,
481
+ store: &mut impl StorageReader,
482
+ left_hash: [u8; TRACKED_STATE_HASH_BYTES],
483
+ right_hash: [u8; TRACKED_STATE_HASH_BYTES],
484
+ request: &TrackedStateTreeScanRequest,
485
+ out: &mut Vec<TrackedStateTreeDiffEntry>,
486
+ ) -> Result<(), LixError> {
487
+ let mut left = LeafSummaryCursor::new(self, store, left_hash).await?;
488
+ let mut right = LeafSummaryCursor::new(self, store, right_hash).await?;
489
+ let mut left_window = Vec::new();
490
+ let mut right_window = Vec::new();
491
+
492
+ loop {
493
+ match (left.current(), right.current()) {
494
+ (Some(left_leaf), Some(right_leaf)) if left_leaf == right_leaf => {
495
+ self.diff_leaf_summary_window(store, &left_window, &right_window, request, out)
496
+ .await?;
497
+ left_window.clear();
498
+ right_window.clear();
499
+ left.advance(self, store).await?;
500
+ right.advance(self, store).await?;
501
+ }
502
+ (Some(left_leaf), Some(right_leaf)) => {
503
+ match left_leaf.last_key.cmp(&right_leaf.last_key) {
504
+ std::cmp::Ordering::Less => {
505
+ left_window.push(left_leaf.clone());
506
+ left.advance(self, store).await?;
507
+ }
508
+ std::cmp::Ordering::Greater => {
509
+ right_window.push(right_leaf.clone());
510
+ right.advance(self, store).await?;
511
+ }
512
+ std::cmp::Ordering::Equal => {
513
+ left_window.push(left_leaf.clone());
514
+ right_window.push(right_leaf.clone());
515
+ left.advance(self, store).await?;
516
+ right.advance(self, store).await?;
517
+ }
518
+ }
519
+ }
520
+ (Some(left_leaf), None) => {
521
+ left_window.push(left_leaf.clone());
522
+ left.advance(self, store).await?;
523
+ }
524
+ (None, Some(right_leaf)) => {
525
+ right_window.push(right_leaf.clone());
526
+ right.advance(self, store).await?;
527
+ }
528
+ (None, None) => {
529
+ self.diff_leaf_summary_window(store, &left_window, &right_window, request, out)
530
+ .await?;
531
+ return Ok(());
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ async fn diff_leaf_summary_window(
538
+ &self,
539
+ store: &mut impl StorageReader,
540
+ left_leaves: &[ChildSummary],
541
+ right_leaves: &[ChildSummary],
542
+ request: &TrackedStateTreeScanRequest,
543
+ out: &mut Vec<TrackedStateTreeDiffEntry>,
544
+ ) -> Result<(), LixError> {
545
+ if left_leaves.is_empty() && right_leaves.is_empty() {
546
+ return Ok(());
547
+ }
548
+ let left_entries = self
549
+ .collect_entries_from_leaf_summaries(store, left_leaves)
550
+ .await?;
551
+ let right_entries = self
552
+ .collect_entries_from_leaf_summaries(store, right_leaves)
553
+ .await?;
554
+ self.diff_leaf_entries(&left_entries, &right_entries, request, out)
555
+ }
556
+
557
+ fn diff_leaf_entries(
558
+ &self,
559
+ left: &[EncodedLeafEntry],
560
+ right: &[EncodedLeafEntry],
561
+ request: &TrackedStateTreeScanRequest,
562
+ out: &mut Vec<TrackedStateTreeDiffEntry>,
563
+ ) -> Result<(), LixError> {
564
+ let mut left_index = 0usize;
565
+ let mut right_index = 0usize;
566
+ while left_index < left.len() && right_index < right.len() {
567
+ match left[left_index].key.cmp(&right[right_index].key) {
568
+ std::cmp::Ordering::Less => {
569
+ self.push_removed_diff(&left[left_index], request, out)?;
570
+ left_index += 1;
571
+ }
572
+ std::cmp::Ordering::Greater => {
573
+ self.push_added_diff(&right[right_index], request, out)?;
574
+ right_index += 1;
575
+ }
576
+ std::cmp::Ordering::Equal => {
577
+ if left[left_index].value != right[right_index].value {
578
+ self.push_modified_diff(
579
+ &left[left_index],
580
+ &right[right_index],
581
+ request,
582
+ out,
583
+ )?;
584
+ }
585
+ left_index += 1;
586
+ right_index += 1;
587
+ }
588
+ }
589
+ }
590
+ for entry in &left[left_index..] {
591
+ self.push_removed_diff(entry, request, out)?;
592
+ }
593
+ for entry in &right[right_index..] {
594
+ self.push_added_diff(entry, request, out)?;
595
+ }
596
+ Ok(())
597
+ }
598
+
599
+ fn push_removed_diff(
600
+ &self,
601
+ entry: &EncodedLeafEntry,
602
+ request: &TrackedStateTreeScanRequest,
603
+ out: &mut Vec<TrackedStateTreeDiffEntry>,
604
+ ) -> Result<(), LixError> {
605
+ let (key, value) = decode_entry(entry)?;
606
+ if request.matches(&key, &value) {
607
+ out.push(TrackedStateTreeDiffEntry {
608
+ before: Some((key, value)),
609
+ after: None,
610
+ });
611
+ }
612
+ Ok(())
613
+ }
614
+
615
+ fn push_added_diff(
616
+ &self,
617
+ entry: &EncodedLeafEntry,
618
+ request: &TrackedStateTreeScanRequest,
619
+ out: &mut Vec<TrackedStateTreeDiffEntry>,
620
+ ) -> Result<(), LixError> {
621
+ let (key, value) = decode_entry(entry)?;
622
+ if request.matches(&key, &value) {
623
+ out.push(TrackedStateTreeDiffEntry {
624
+ before: None,
625
+ after: Some((key, value)),
626
+ });
627
+ }
628
+ Ok(())
629
+ }
630
+
631
+ fn push_modified_diff(
632
+ &self,
633
+ left: &EncodedLeafEntry,
634
+ right: &EncodedLeafEntry,
635
+ request: &TrackedStateTreeScanRequest,
636
+ out: &mut Vec<TrackedStateTreeDiffEntry>,
637
+ ) -> Result<(), LixError> {
638
+ let (left_key, left_value) = decode_entry(left)?;
639
+ let (right_key, right_value) = decode_entry(right)?;
640
+ if request.matches(&left_key, &left_value) || request.matches(&right_key, &right_value) {
641
+ out.push(TrackedStateTreeDiffEntry {
642
+ before: Some((left_key, left_value)),
643
+ after: Some((right_key, right_value)),
644
+ });
645
+ }
646
+ Ok(())
647
+ }
648
+
649
+ async fn apply_sorted_mutations_chunker(
650
+ &self,
651
+ store: &mut (impl StorageReader + ?Sized),
652
+ writes: &mut StorageWriteSet,
653
+ overlay: &mut storage::TrackedStateChunkOverlay,
654
+ root_id: &TrackedStateRootId,
655
+ mutations: &[TrackedStateMutation],
656
+ commit_id: Option<&str>,
657
+ ) -> Result<Option<TrackedStateApplyResult>, LixError> {
658
+ let mut mutation_map = BTreeMap::new();
659
+ for mutation in mutations {
660
+ let TrackedStateMutation::Put { key, value } = mutation;
661
+ mutation_map.insert(encode_key(key), encode_value(value));
662
+ }
663
+ if mutation_map.is_empty() {
664
+ return Ok(None);
665
+ }
666
+
667
+ let mutations = mutation_map.into_iter().collect::<Vec<_>>();
668
+
669
+ let levels = self
670
+ .collect_summary_levels_with_overlay(store, overlay, root_id)
671
+ .await?;
672
+ let Some(leaves) = levels.first() else {
673
+ return Ok(None);
674
+ };
675
+
676
+ let base_row_count = leaves
677
+ .iter()
678
+ .map(|leaf| leaf.subtree_count as usize)
679
+ .sum::<usize>();
680
+ let append_only = leaves
681
+ .last()
682
+ .is_some_and(|leaf| mutations[0].0.as_slice() > leaf.last_key.as_slice());
683
+ if !append_only && mutations.len() * 2 > base_row_count {
684
+ return Ok(None);
685
+ }
686
+
687
+ let mut output_leaves = Vec::new();
688
+ let mut chunks = BTreeMap::new();
689
+ let mut leaf_index = 0usize;
690
+ let mut next_mutation_index = 0usize;
691
+
692
+ while leaf_index < leaves.len() {
693
+ if next_mutation_index >= mutations.len()
694
+ || mutations[next_mutation_index].0.as_slice()
695
+ > leaves[leaf_index].last_key.as_slice()
696
+ {
697
+ output_leaves.push(leaves[leaf_index].clone());
698
+ leaf_index += 1;
699
+ continue;
700
+ }
701
+
702
+ let window_start = leaf_index;
703
+ let mut window_entries = BTreeMap::new();
704
+ let mut window_mutation_ceiling = mutations[next_mutation_index].0.clone();
705
+
706
+ loop {
707
+ if leaf_index < leaves.len() {
708
+ let leaf = &leaves[leaf_index];
709
+ for entry in self
710
+ .load_leaf_entries_with_overlay(store, overlay, &leaf.child_hash)
711
+ .await?
712
+ {
713
+ window_entries.insert(entry.key, entry.value);
714
+ }
715
+
716
+ while next_mutation_index < mutations.len()
717
+ && mutations[next_mutation_index].0.as_slice() <= leaf.last_key.as_slice()
718
+ {
719
+ let (key, value) = &mutations[next_mutation_index];
720
+ window_entries.insert(key.clone(), value.clone());
721
+ window_mutation_ceiling = key.clone();
722
+ next_mutation_index += 1;
723
+ }
724
+ leaf_index += 1;
725
+ }
726
+
727
+ while next_mutation_index < mutations.len() {
728
+ let (key, value) = &mutations[next_mutation_index];
729
+ if leaf_index < leaves.len()
730
+ && key.as_slice() >= leaves[leaf_index].first_key.as_slice()
731
+ {
732
+ break;
733
+ }
734
+ window_entries.insert(key.clone(), value.clone());
735
+ window_mutation_ceiling = key.clone();
736
+ next_mutation_index += 1;
737
+ }
738
+
739
+ if next_mutation_index < mutations.len()
740
+ && leaf_index < leaves.len()
741
+ && mutations[next_mutation_index].0.as_slice()
742
+ <= leaves[leaf_index].last_key.as_slice()
743
+ {
744
+ continue;
745
+ }
746
+
747
+ let mut candidate_chunks = BTreeMap::new();
748
+ let candidate_leaves = self.build_leaf_level(
749
+ window_entries
750
+ .iter()
751
+ .map(|(key, value)| EncodedLeafEntry {
752
+ key: key.clone(),
753
+ value: value.clone(),
754
+ })
755
+ .collect(),
756
+ &mut candidate_chunks,
757
+ );
758
+
759
+ if let Some((generated_resync_index, existing_resync_index)) = first_resync_index(
760
+ &candidate_leaves,
761
+ &leaves[window_start..],
762
+ &window_mutation_ceiling,
763
+ ) {
764
+ for summary in &candidate_leaves[..generated_resync_index] {
765
+ if let Some(chunk) = candidate_chunks.remove(&summary.child_hash) {
766
+ chunks.entry(chunk.hash).or_insert(chunk);
767
+ }
768
+ }
769
+ output_leaves.extend_from_slice(&candidate_leaves[..generated_resync_index]);
770
+ leaf_index = window_start + existing_resync_index;
771
+ break;
772
+ }
773
+
774
+ if leaf_index >= leaves.len() {
775
+ chunks.extend(candidate_chunks);
776
+ output_leaves.extend(candidate_leaves);
777
+ break;
778
+ }
779
+ }
780
+ }
781
+
782
+ if next_mutation_index < mutations.len() {
783
+ let mut entries = Vec::new();
784
+ for (key, value) in &mutations[next_mutation_index..] {
785
+ entries.push(EncodedLeafEntry {
786
+ key: key.clone(),
787
+ value: value.clone(),
788
+ });
789
+ }
790
+ output_leaves.extend(self.build_leaf_level(entries, &mut chunks));
791
+ }
792
+
793
+ let built = self.build_tree_from_leaf_summaries(output_leaves, chunks)?;
794
+ self.persist_built_tree(writes, overlay, built, commit_id)
795
+ .await
796
+ }
797
+
798
+ async fn apply_single_mutation_from_seek_path(
799
+ &self,
800
+ store: &mut (impl StorageReader + ?Sized),
801
+ writes: &mut StorageWriteSet,
802
+ overlay: &mut storage::TrackedStateChunkOverlay,
803
+ root_id: &TrackedStateRootId,
804
+ encoded_key: &[u8],
805
+ encoded_value: &[u8],
806
+ commit_id: Option<&str>,
807
+ ) -> Result<Option<Option<TrackedStateApplyResult>>, LixError> {
808
+ let mut current = *root_id.as_bytes();
809
+ let mut path = Vec::new();
810
+ let mut entries = loop {
811
+ match self
812
+ .load_node_with_overlay(store, overlay, &current)
813
+ .await?
814
+ {
815
+ DecodedNode::Leaf(leaf) => break leaf.entries().to_vec(),
816
+ DecodedNode::Internal(internal) => {
817
+ let children = internal.children().to_vec();
818
+ let child_index = children
819
+ .iter()
820
+ .position(|child| child.last_key.as_slice() >= encoded_key)
821
+ .or_else(|| (!children.is_empty()).then_some(children.len() - 1))
822
+ .ok_or_else(|| {
823
+ LixError::new(
824
+ "LIX_ERROR_UNKNOWN",
825
+ "tracked-state tree internal node has no children",
826
+ )
827
+ })?;
828
+ current = children[child_index].child_hash;
829
+ path.push(SeekPathFrame {
830
+ children,
831
+ child_index,
832
+ });
833
+ }
834
+ }
835
+ };
836
+
837
+ match entries.binary_search_by(|entry| entry.key.as_slice().cmp(encoded_key)) {
838
+ Ok(index) => {
839
+ if entries[index].value == encoded_value {
840
+ return Ok(Some(None));
841
+ }
842
+ entries[index].value = encoded_value.to_vec();
843
+ }
844
+ Err(index) => entries.insert(
845
+ index,
846
+ EncodedLeafEntry {
847
+ key: encoded_key.to_vec(),
848
+ value: encoded_value.to_vec(),
849
+ },
850
+ ),
851
+ }
852
+
853
+ let mut chunks = BTreeMap::new();
854
+ let mut replacement_children;
855
+ let mut old_child_count;
856
+
857
+ let Some(leaf_parent) = path.pop() else {
858
+ let built = self.build_tree_from_entries(entries)?;
859
+ return self
860
+ .persist_built_tree(writes, overlay, built, commit_id)
861
+ .await
862
+ .map(Some);
863
+ };
864
+ let mutation_is_right_edge = leaf_parent.child_index + 1 == leaf_parent.children.len()
865
+ && path
866
+ .iter()
867
+ .all(|frame| frame.child_index + 1 == frame.children.len());
868
+
869
+ let mut leaf_entries = entries;
870
+ let mut next_leaf_index = leaf_parent.child_index + 1;
871
+ loop {
872
+ let mut candidate_chunks = BTreeMap::new();
873
+ let candidate_leaves =
874
+ self.build_leaf_level(leaf_entries.clone(), &mut candidate_chunks);
875
+ if let Some((generated_resync_index, existing_resync_index)) = first_resync_index(
876
+ &candidate_leaves,
877
+ &leaf_parent.children[leaf_parent.child_index..],
878
+ encoded_key,
879
+ ) {
880
+ for summary in &candidate_leaves[..generated_resync_index] {
881
+ if let Some(chunk) = candidate_chunks.remove(&summary.child_hash) {
882
+ chunks.entry(chunk.hash).or_insert(chunk);
883
+ }
884
+ }
885
+ replacement_children = candidate_leaves[..generated_resync_index].to_vec();
886
+ old_child_count = existing_resync_index;
887
+ break;
888
+ }
889
+
890
+ if next_leaf_index >= leaf_parent.children.len() {
891
+ if !mutation_is_right_edge {
892
+ return Ok(None);
893
+ }
894
+ chunks.extend(candidate_chunks);
895
+ replacement_children = candidate_leaves;
896
+ old_child_count = leaf_parent.children.len() - leaf_parent.child_index;
897
+ break;
898
+ }
899
+
900
+ leaf_entries.extend(
901
+ self.load_leaf_entries_with_overlay(
902
+ store,
903
+ overlay,
904
+ &leaf_parent.children[next_leaf_index].child_hash,
905
+ )
906
+ .await?,
907
+ );
908
+ next_leaf_index += 1;
909
+ }
910
+
911
+ let mut child_index = leaf_parent.child_index;
912
+ let mut children = leaf_parent.children;
913
+ let mut parent_level = 1usize;
914
+ loop {
915
+ children.splice(
916
+ child_index..child_index + old_child_count,
917
+ replacement_children,
918
+ );
919
+ replacement_children = self.build_internal_level(children, parent_level, &mut chunks);
920
+ old_child_count = 1;
921
+
922
+ let Some(frame) = path.pop() else {
923
+ let mut summaries = replacement_children;
924
+ let mut tree_height = parent_level + 1;
925
+ while summaries.len() > 1 {
926
+ summaries = self.build_internal_level(summaries, tree_height, &mut chunks);
927
+ tree_height += 1;
928
+ }
929
+ let root = summaries.pop().ok_or_else(|| {
930
+ LixError::new(
931
+ "LIX_ERROR_UNKNOWN",
932
+ "tracked-state seek-path mutation produced no root",
933
+ )
934
+ })?;
935
+ let chunks = chunks.into_values().collect::<Vec<_>>();
936
+ let chunk_bytes = chunks.iter().map(|chunk| chunk.data.len()).sum();
937
+ let built = BuiltTree {
938
+ root_id: TrackedStateRootId::new(root.child_hash),
939
+ chunks,
940
+ row_count: root.subtree_count as usize,
941
+ tree_height,
942
+ chunk_bytes,
943
+ };
944
+ return self
945
+ .persist_built_tree(writes, overlay, built, commit_id)
946
+ .await
947
+ .map(Some);
948
+ };
949
+
950
+ child_index = frame.child_index;
951
+ children = frame.children;
952
+ parent_level += 1;
953
+ }
954
+ }
955
+
956
+ async fn persist_built_tree(
957
+ &self,
958
+ writes: &mut StorageWriteSet,
959
+ overlay: &mut storage::TrackedStateChunkOverlay,
960
+ built: BuiltTree,
961
+ commit_id: Option<&str>,
962
+ ) -> Result<Option<TrackedStateApplyResult>, LixError> {
963
+ overlay.stage_chunks(writes, &built.chunks);
964
+ let persisted_root = if let Some(commit_id) = commit_id {
965
+ storage::stage_root(writes, commit_id, &built.root_id);
966
+ true
967
+ } else {
968
+ false
969
+ };
970
+ Ok(Some(TrackedStateApplyResult {
971
+ root_id: built.root_id,
972
+ row_count: built.row_count,
973
+ tree_height: built.tree_height,
974
+ chunk_count: built.chunks.len(),
975
+ chunk_bytes: built.chunk_bytes,
976
+ persisted_root,
977
+ }))
978
+ }
979
+
980
+ fn build_tree_from_entries(
981
+ &self,
982
+ entries: Vec<EncodedLeafEntry>,
983
+ ) -> Result<BuiltTree, LixError> {
984
+ let row_count = entries.len();
985
+ let mut chunks = BTreeMap::<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>::new();
986
+ let mut summaries = self.build_leaf_level(entries, &mut chunks);
987
+ let mut tree_height = 1usize;
988
+ while summaries.len() > 1 {
989
+ summaries = self.build_internal_level(summaries, tree_height, &mut chunks);
990
+ tree_height += 1;
991
+ }
992
+ let root = summaries.pop().ok_or_else(|| {
993
+ LixError::new(
994
+ "LIX_ERROR_UNKNOWN",
995
+ "tracked-state tree tree build produced no root",
996
+ )
997
+ })?;
998
+ let chunks = chunks.into_values().collect::<Vec<_>>();
999
+ let chunk_bytes = chunks.iter().map(|chunk| chunk.data.len()).sum();
1000
+ Ok(BuiltTree {
1001
+ root_id: TrackedStateRootId::new(root.child_hash),
1002
+ chunks,
1003
+ row_count,
1004
+ tree_height,
1005
+ chunk_bytes,
1006
+ })
1007
+ }
1008
+
1009
+ fn build_tree_from_leaf_summaries(
1010
+ &self,
1011
+ leaf_summaries: Vec<ChildSummary>,
1012
+ mut chunks: BTreeMap<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>,
1013
+ ) -> Result<BuiltTree, LixError> {
1014
+ let row_count = leaf_summaries
1015
+ .iter()
1016
+ .map(|summary| summary.subtree_count as usize)
1017
+ .sum();
1018
+ let mut summaries = leaf_summaries;
1019
+ let mut tree_height = 1usize;
1020
+ while summaries.len() > 1 {
1021
+ summaries = self.build_internal_level(summaries, tree_height, &mut chunks);
1022
+ tree_height += 1;
1023
+ }
1024
+ let root = summaries.pop().ok_or_else(|| {
1025
+ LixError::new(
1026
+ "LIX_ERROR_UNKNOWN",
1027
+ "tracked-state tree build from leaves produced no root",
1028
+ )
1029
+ })?;
1030
+ let chunks = chunks.into_values().collect::<Vec<_>>();
1031
+ let chunk_bytes = chunks.iter().map(|chunk| chunk.data.len()).sum();
1032
+ Ok(BuiltTree {
1033
+ root_id: TrackedStateRootId::new(root.child_hash),
1034
+ chunks,
1035
+ row_count,
1036
+ tree_height,
1037
+ chunk_bytes,
1038
+ })
1039
+ }
1040
+
1041
+ fn build_tree_from_leaf_patch(
1042
+ &self,
1043
+ levels: &[Vec<ChildSummary>],
1044
+ leaf_start: usize,
1045
+ old_leaf_count: usize,
1046
+ replacement_leaves: Vec<ChildSummary>,
1047
+ mut chunks: BTreeMap<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>,
1048
+ mutation_key: &[u8],
1049
+ ) -> Result<BuiltTree, LixError> {
1050
+ if levels.len() <= 1 {
1051
+ let mut leaves = levels.first().cloned().unwrap_or_default();
1052
+ leaves.splice(leaf_start..leaf_start + old_leaf_count, replacement_leaves);
1053
+ return self.build_tree_from_leaf_summaries(leaves, chunks);
1054
+ }
1055
+
1056
+ let mut child_start = leaf_start;
1057
+ let mut old_child_count = old_leaf_count;
1058
+ let mut replacement_children = replacement_leaves;
1059
+
1060
+ for level in 0..levels.len() - 1 {
1061
+ let patch = self.patch_parent_level(
1062
+ &levels[level],
1063
+ &levels[level + 1],
1064
+ child_start,
1065
+ old_child_count,
1066
+ replacement_children,
1067
+ level + 1,
1068
+ &mut chunks,
1069
+ mutation_key,
1070
+ )?;
1071
+ child_start = patch.parent_start;
1072
+ old_child_count = patch.old_parent_count;
1073
+ replacement_children = patch.replacement_parents;
1074
+ }
1075
+
1076
+ let mut summaries = replacement_children;
1077
+ let mut tree_height = levels.len();
1078
+ while summaries.len() > 1 {
1079
+ summaries = self.build_internal_level(summaries, tree_height, &mut chunks);
1080
+ tree_height += 1;
1081
+ }
1082
+ let root = summaries.pop().ok_or_else(|| {
1083
+ LixError::new(
1084
+ "LIX_ERROR_UNKNOWN",
1085
+ "tracked-state patched tree produced no root",
1086
+ )
1087
+ })?;
1088
+ let chunks = chunks.into_values().collect::<Vec<_>>();
1089
+ let chunk_bytes = chunks.iter().map(|chunk| chunk.data.len()).sum();
1090
+ Ok(BuiltTree {
1091
+ root_id: TrackedStateRootId::new(root.child_hash),
1092
+ chunks,
1093
+ row_count: root.subtree_count as usize,
1094
+ tree_height,
1095
+ chunk_bytes,
1096
+ })
1097
+ }
1098
+
1099
+ fn patch_parent_level(
1100
+ &self,
1101
+ old_children: &[ChildSummary],
1102
+ old_parents: &[ChildSummary],
1103
+ child_start: usize,
1104
+ old_child_count: usize,
1105
+ replacement_children: Vec<ChildSummary>,
1106
+ parent_level: usize,
1107
+ chunks: &mut BTreeMap<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>,
1108
+ mutation_key: &[u8],
1109
+ ) -> Result<ParentLevelPatch, LixError> {
1110
+ if old_parents.is_empty() {
1111
+ return Ok(ParentLevelPatch {
1112
+ parent_start: 0,
1113
+ old_parent_count: 0,
1114
+ replacement_parents: self.build_internal_level(
1115
+ replacement_children,
1116
+ parent_level,
1117
+ chunks,
1118
+ ),
1119
+ });
1120
+ }
1121
+
1122
+ let parent_start = parent_index_for_child_index(old_children, old_parents, child_start);
1123
+ let parent_child_range = child_range_for_parent(old_children, &old_parents[parent_start])?;
1124
+ let old_child_end = child_start + old_child_count;
1125
+ let parent_end = if old_child_count == 0 {
1126
+ parent_start
1127
+ } else {
1128
+ parent_index_for_child_index(old_children, old_parents, old_child_end - 1)
1129
+ };
1130
+ let parent_end_child_range =
1131
+ child_range_for_parent(old_children, &old_parents[parent_end])?;
1132
+ let mut window_children = Vec::new();
1133
+ window_children.extend_from_slice(&old_children[parent_child_range.start..child_start]);
1134
+ window_children.extend(replacement_children);
1135
+ window_children.extend_from_slice(&old_children[old_child_end..parent_end_child_range.end]);
1136
+ let mut next_parent_index = parent_end + 1;
1137
+
1138
+ loop {
1139
+ let mut candidate_chunks = BTreeMap::new();
1140
+ let candidate_parents = self.build_internal_level(
1141
+ window_children.clone(),
1142
+ parent_level,
1143
+ &mut candidate_chunks,
1144
+ );
1145
+
1146
+ if let Some((generated_resync_index, existing_resync_index)) = first_resync_index(
1147
+ &candidate_parents,
1148
+ &old_parents[parent_start..],
1149
+ mutation_key,
1150
+ ) {
1151
+ for summary in &candidate_parents[..generated_resync_index] {
1152
+ if let Some(chunk) = candidate_chunks.remove(&summary.child_hash) {
1153
+ chunks.entry(chunk.hash).or_insert(chunk);
1154
+ }
1155
+ }
1156
+ return Ok(ParentLevelPatch {
1157
+ parent_start,
1158
+ old_parent_count: existing_resync_index,
1159
+ replacement_parents: candidate_parents[..generated_resync_index].to_vec(),
1160
+ });
1161
+ }
1162
+
1163
+ if next_parent_index >= old_parents.len() {
1164
+ chunks.extend(candidate_chunks);
1165
+ return Ok(ParentLevelPatch {
1166
+ parent_start,
1167
+ old_parent_count: old_parents.len() - parent_start,
1168
+ replacement_parents: candidate_parents,
1169
+ });
1170
+ }
1171
+
1172
+ let next_range = child_range_for_parent(old_children, &old_parents[next_parent_index])?;
1173
+ window_children.extend_from_slice(&old_children[next_range]);
1174
+ next_parent_index += 1;
1175
+ }
1176
+ }
1177
+
1178
+ fn build_leaf_level(
1179
+ &self,
1180
+ entries: Vec<EncodedLeafEntry>,
1181
+ chunks: &mut BTreeMap<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>,
1182
+ ) -> Vec<ChildSummary> {
1183
+ let groups = chunk_leaf_entries(entries, &self.options);
1184
+ groups
1185
+ .into_iter()
1186
+ .map(|group| {
1187
+ let subtree_count = group.entries.len() as u64;
1188
+ let first_key = group
1189
+ .entries
1190
+ .first()
1191
+ .map(|entry| entry.key.clone())
1192
+ .unwrap_or_default();
1193
+ let last_key = group
1194
+ .entries
1195
+ .last()
1196
+ .map(|entry| entry.key.clone())
1197
+ .unwrap_or_default();
1198
+ let node = encode_leaf_node(&group.entries);
1199
+ let (chunk, summary) =
1200
+ child_summary_from_node(node, first_key, last_key, subtree_count);
1201
+ chunks.entry(chunk.hash).or_insert(chunk);
1202
+ summary
1203
+ })
1204
+ .collect()
1205
+ }
1206
+
1207
+ fn build_internal_level(
1208
+ &self,
1209
+ children: Vec<ChildSummary>,
1210
+ level: usize,
1211
+ chunks: &mut BTreeMap<[u8; TRACKED_STATE_HASH_BYTES], PendingChunkWrite>,
1212
+ ) -> Vec<ChildSummary> {
1213
+ let groups = chunk_internal_entries(children, &self.options, level);
1214
+ groups
1215
+ .into_iter()
1216
+ .map(|group| {
1217
+ let subtree_count = group.children.iter().map(|child| child.subtree_count).sum();
1218
+ let first_key = group
1219
+ .children
1220
+ .first()
1221
+ .map(|child| child.first_key.clone())
1222
+ .unwrap_or_default();
1223
+ let last_key = group
1224
+ .children
1225
+ .last()
1226
+ .map(|child| child.last_key.clone())
1227
+ .unwrap_or_default();
1228
+ let node = encode_internal_node(&group.children);
1229
+ let (chunk, summary) =
1230
+ child_summary_from_node(node, first_key, last_key, subtree_count);
1231
+ chunks.entry(chunk.hash).or_insert(chunk);
1232
+ summary
1233
+ })
1234
+ .collect()
1235
+ }
1236
+
1237
+ async fn collect_leaf_entries(
1238
+ &self,
1239
+ store: &mut (impl StorageReader + ?Sized),
1240
+ root_id: &TrackedStateRootId,
1241
+ ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1242
+ let mut out = Vec::new();
1243
+ let mut current = vec![*root_id.as_bytes()];
1244
+ while !current.is_empty() {
1245
+ let mut next = Vec::new();
1246
+ for hash in current {
1247
+ match self.load_node(store, &hash).await? {
1248
+ DecodedNode::Leaf(leaf) => out.extend(leaf.entries().iter().cloned()),
1249
+ DecodedNode::Internal(internal) => {
1250
+ next.extend(internal.children().iter().map(|child| child.child_hash));
1251
+ }
1252
+ }
1253
+ }
1254
+ current = next;
1255
+ }
1256
+ Ok(out)
1257
+ }
1258
+
1259
+ async fn collect_filtered_entries(
1260
+ &self,
1261
+ store: &mut impl StorageReader,
1262
+ root_id: &TrackedStateRootId,
1263
+ request: &TrackedStateTreeScanRequest,
1264
+ ) -> Result<Vec<(TrackedStateKey, TrackedStateValue)>, LixError> {
1265
+ self.scan(store, root_id, request).await
1266
+ }
1267
+
1268
+ fn scan_node<'a, S>(
1269
+ &'a self,
1270
+ store: &'a mut S,
1271
+ hash: [u8; TRACKED_STATE_HASH_BYTES],
1272
+ request: &'a TrackedStateTreeScanRequest,
1273
+ ranges: &'a [EncodedScanRange],
1274
+ rows: &'a mut Vec<(TrackedStateKey, TrackedStateValue)>,
1275
+ ) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + Send + 'a>>
1276
+ where
1277
+ S: StorageReader + Send + 'a,
1278
+ {
1279
+ Box::pin(async move {
1280
+ match self.load_node(store, &hash).await? {
1281
+ DecodedNode::Leaf(leaf) => {
1282
+ for entry in leaf.entries() {
1283
+ if scan_limit_reached(request, rows.len()) {
1284
+ break;
1285
+ }
1286
+ if !encoded_key_in_scan_ranges(&entry.key, ranges) {
1287
+ continue;
1288
+ }
1289
+ let key = decode_key(&entry.key)?;
1290
+ if !key_matches_scan_filters(request, &key) {
1291
+ continue;
1292
+ }
1293
+ let value = decode_value(&entry.value)?;
1294
+ if request.matches(&key, &value) {
1295
+ rows.push((key, value));
1296
+ }
1297
+ }
1298
+ }
1299
+ DecodedNode::Internal(internal) => {
1300
+ for child in internal.children() {
1301
+ if scan_limit_reached(request, rows.len()) {
1302
+ break;
1303
+ }
1304
+ if child_summary_overlaps_scan_ranges(child, ranges) {
1305
+ self.scan_node(store, child.child_hash, request, ranges, rows)
1306
+ .await?;
1307
+ }
1308
+ }
1309
+ }
1310
+ }
1311
+ Ok(())
1312
+ })
1313
+ }
1314
+
1315
+ fn get_many_node<'a, S>(
1316
+ &'a self,
1317
+ store: &'a mut S,
1318
+ hash: [u8; TRACKED_STATE_HASH_BYTES],
1319
+ encoded_keys: &'a [(usize, Vec<u8>)],
1320
+ values: &'a mut [Option<TrackedStateValue>],
1321
+ ) -> Pin<Box<dyn Future<Output = Result<(), LixError>> + Send + 'a>>
1322
+ where
1323
+ S: StorageReader + Send + 'a,
1324
+ {
1325
+ Box::pin(async move {
1326
+ if encoded_keys.is_empty() {
1327
+ return Ok(());
1328
+ }
1329
+
1330
+ match self.load_node(store, &hash).await? {
1331
+ DecodedNode::Leaf(leaf) => {
1332
+ for (original_index, encoded_key) in encoded_keys {
1333
+ let Some(entry_index) = leaf
1334
+ .entries()
1335
+ .binary_search_by(|entry| entry.key.as_slice().cmp(encoded_key))
1336
+ .ok()
1337
+ else {
1338
+ continue;
1339
+ };
1340
+ values[*original_index] =
1341
+ Some(decode_value(&leaf.entries()[entry_index].value)?);
1342
+ }
1343
+ }
1344
+ DecodedNode::Internal(internal) => {
1345
+ let mut start = 0usize;
1346
+ let children = internal.children();
1347
+ for (child_index, child) in children.iter().enumerate() {
1348
+ if start >= encoded_keys.len() {
1349
+ break;
1350
+ }
1351
+
1352
+ let mut end = start;
1353
+ if child_index + 1 == children.len() {
1354
+ end = encoded_keys.len();
1355
+ } else {
1356
+ while end < encoded_keys.len()
1357
+ && encoded_keys[end].1.as_slice() <= child.last_key.as_slice()
1358
+ {
1359
+ end += 1;
1360
+ }
1361
+ }
1362
+
1363
+ if start < end {
1364
+ self.get_many_node(
1365
+ store,
1366
+ child.child_hash,
1367
+ &encoded_keys[start..end],
1368
+ values,
1369
+ )
1370
+ .await?;
1371
+ }
1372
+ start = end;
1373
+ }
1374
+ }
1375
+ }
1376
+ Ok(())
1377
+ })
1378
+ }
1379
+
1380
+ fn count_matching_keys_node<'a, S>(
1381
+ &'a self,
1382
+ store: &'a mut S,
1383
+ hash: [u8; TRACKED_STATE_HASH_BYTES],
1384
+ request: &'a TrackedStateTreeScanRequest,
1385
+ ranges: &'a [EncodedScanRange],
1386
+ ) -> Pin<Box<dyn Future<Output = Result<usize, LixError>> + Send + 'a>>
1387
+ where
1388
+ S: StorageReader + Send + 'a,
1389
+ {
1390
+ Box::pin(async move {
1391
+ let mut count = 0usize;
1392
+ match self.load_node(store, &hash).await? {
1393
+ DecodedNode::Leaf(leaf) => {
1394
+ for entry in leaf.entries() {
1395
+ if !encoded_key_in_scan_ranges(&entry.key, ranges) {
1396
+ continue;
1397
+ }
1398
+ let key = decode_key(&entry.key)?;
1399
+ if key_matches_scan_filters(request, &key) {
1400
+ count += 1;
1401
+ }
1402
+ }
1403
+ }
1404
+ DecodedNode::Internal(internal) => {
1405
+ for child in internal.children() {
1406
+ if child_summary_contained_by_scan_ranges(child, ranges)
1407
+ && request.entity_ids.is_empty()
1408
+ {
1409
+ count += child.subtree_count as usize;
1410
+ } else if child_summary_overlaps_scan_ranges(child, ranges) {
1411
+ count += self
1412
+ .count_matching_keys_node(store, child.child_hash, request, ranges)
1413
+ .await?;
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ Ok(count)
1419
+ })
1420
+ }
1421
+
1422
+ async fn collect_entries_from_leaf_summaries(
1423
+ &self,
1424
+ store: &mut impl StorageReader,
1425
+ leaves: &[ChildSummary],
1426
+ ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1427
+ let mut entries = Vec::new();
1428
+ for leaf in leaves {
1429
+ entries.extend(self.load_leaf_entries(store, &leaf.child_hash).await?);
1430
+ }
1431
+ Ok(entries)
1432
+ }
1433
+
1434
+ async fn collect_summary_levels_with_overlay(
1435
+ &self,
1436
+ store: &mut (impl StorageReader + ?Sized),
1437
+ overlay: &storage::TrackedStateChunkOverlay,
1438
+ root_id: &TrackedStateRootId,
1439
+ ) -> Result<Vec<Vec<ChildSummary>>, LixError> {
1440
+ let mut levels = Vec::new();
1441
+ self.collect_summary_levels_for_node_with_overlay(
1442
+ store,
1443
+ overlay,
1444
+ *root_id.as_bytes(),
1445
+ &mut levels,
1446
+ )
1447
+ .await?;
1448
+ Ok(levels)
1449
+ }
1450
+
1451
+ fn collect_summary_levels_for_node_with_overlay<'a, S>(
1452
+ &'a self,
1453
+ store: &'a mut S,
1454
+ overlay: &'a storage::TrackedStateChunkOverlay,
1455
+ hash: [u8; TRACKED_STATE_HASH_BYTES],
1456
+ levels: &'a mut Vec<Vec<ChildSummary>>,
1457
+ ) -> Pin<Box<dyn Future<Output = Result<(ChildSummary, usize), LixError>> + 'a>>
1458
+ where
1459
+ S: StorageReader + ?Sized + 'a,
1460
+ {
1461
+ Box::pin(async move {
1462
+ match self.load_node_with_overlay(store, overlay, &hash).await? {
1463
+ DecodedNode::Leaf(leaf) => {
1464
+ let summary = leaf_summary(hash, leaf.entries());
1465
+ push_level_summary(levels, 0, summary.clone());
1466
+ Ok((summary, 0))
1467
+ }
1468
+ DecodedNode::Internal(internal) => {
1469
+ let children = internal.children().to_vec();
1470
+ let child_height = match children.first() {
1471
+ Some(child) => match self
1472
+ .load_node_with_overlay(store, overlay, &child.child_hash)
1473
+ .await?
1474
+ {
1475
+ DecodedNode::Leaf(_) => {
1476
+ if levels.is_empty() {
1477
+ levels.push(Vec::new());
1478
+ }
1479
+ levels[0].extend(children.iter().cloned());
1480
+ 0
1481
+ }
1482
+ DecodedNode::Internal(_) => {
1483
+ let mut child_height = None;
1484
+ for child in &children {
1485
+ let (_, height) = self
1486
+ .collect_summary_levels_for_node_with_overlay(
1487
+ store,
1488
+ overlay,
1489
+ child.child_hash,
1490
+ levels,
1491
+ )
1492
+ .await?;
1493
+ child_height = Some(height);
1494
+ }
1495
+ child_height.unwrap_or(0)
1496
+ }
1497
+ },
1498
+ None => 0,
1499
+ };
1500
+ let height = child_height + 1;
1501
+ let summary = internal_summary(hash, &children)?;
1502
+ push_level_summary(levels, height, summary.clone());
1503
+ Ok((summary, height))
1504
+ }
1505
+ }
1506
+ })
1507
+ }
1508
+
1509
+ async fn load_leaf_entries(
1510
+ &self,
1511
+ store: &mut (impl StorageReader + ?Sized),
1512
+ hash: &[u8; TRACKED_STATE_HASH_BYTES],
1513
+ ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1514
+ match self.load_node(store, hash).await? {
1515
+ DecodedNode::Leaf(leaf) => Ok(leaf.entries().to_vec()),
1516
+ DecodedNode::Internal(_) => Err(LixError::new(
1517
+ "LIX_ERROR_UNKNOWN",
1518
+ "tracked-state expected leaf chunk but found internal node",
1519
+ )),
1520
+ }
1521
+ }
1522
+
1523
+ async fn load_leaf_entries_with_overlay(
1524
+ &self,
1525
+ store: &mut (impl StorageReader + ?Sized),
1526
+ overlay: &storage::TrackedStateChunkOverlay,
1527
+ hash: &[u8; TRACKED_STATE_HASH_BYTES],
1528
+ ) -> Result<Vec<EncodedLeafEntry>, LixError> {
1529
+ match self.load_node_with_overlay(store, overlay, hash).await? {
1530
+ DecodedNode::Leaf(leaf) => Ok(leaf.entries().to_vec()),
1531
+ DecodedNode::Internal(_) => Err(LixError::new(
1532
+ "LIX_ERROR_UNKNOWN",
1533
+ "tracked-state expected leaf chunk but found internal node",
1534
+ )),
1535
+ }
1536
+ }
1537
+
1538
+ async fn load_node(
1539
+ &self,
1540
+ store: &mut (impl StorageReader + ?Sized),
1541
+ hash: &[u8; TRACKED_STATE_HASH_BYTES],
1542
+ ) -> Result<DecodedNode, LixError> {
1543
+ let bytes = storage::read_chunk(store, hash).await?.ok_or_else(|| {
1544
+ LixError::new("LIX_ERROR_UNKNOWN", "tracked-state tree chunk is missing")
1545
+ })?;
1546
+ storage::verify_chunk_hash(hash, &bytes)?;
1547
+ decode_node(&bytes)
1548
+ }
1549
+
1550
+ async fn load_node_with_overlay(
1551
+ &self,
1552
+ store: &mut (impl StorageReader + ?Sized),
1553
+ overlay: &storage::TrackedStateChunkOverlay,
1554
+ hash: &[u8; TRACKED_STATE_HASH_BYTES],
1555
+ ) -> Result<DecodedNode, LixError> {
1556
+ let bytes = overlay.read_chunk(store, hash).await?.ok_or_else(|| {
1557
+ LixError::new("LIX_ERROR_UNKNOWN", "tracked-state tree chunk is missing")
1558
+ })?;
1559
+ storage::verify_chunk_hash(hash, &bytes)?;
1560
+ decode_node(&bytes)
1561
+ }
1562
+ }
1563
+
1564
+ #[derive(Debug)]
1565
+ struct BuiltTree {
1566
+ root_id: TrackedStateRootId,
1567
+ chunks: Vec<PendingChunkWrite>,
1568
+ row_count: usize,
1569
+ tree_height: usize,
1570
+ chunk_bytes: usize,
1571
+ }
1572
+
1573
+ struct ParentLevelPatch {
1574
+ parent_start: usize,
1575
+ old_parent_count: usize,
1576
+ replacement_parents: Vec<ChildSummary>,
1577
+ }
1578
+
1579
+ struct SeekPathFrame {
1580
+ children: Vec<ChildSummary>,
1581
+ child_index: usize,
1582
+ }
1583
+
1584
+ #[derive(Debug, Clone)]
1585
+ struct EncodedScanRange {
1586
+ start: Vec<u8>,
1587
+ end: Option<Vec<u8>>,
1588
+ }
1589
+
1590
+ struct LeafSummaryCursor {
1591
+ stack: Vec<LeafSummaryCursorFrame>,
1592
+ current: Option<ChildSummary>,
1593
+ }
1594
+
1595
+ struct LeafSummaryCursorFrame {
1596
+ children: Vec<ChildSummary>,
1597
+ next_index: usize,
1598
+ children_are_leaves: bool,
1599
+ }
1600
+
1601
+ impl LeafSummaryCursor {
1602
+ async fn new(
1603
+ tree: &TrackedStateTree,
1604
+ store: &mut impl StorageReader,
1605
+ root_hash: [u8; TRACKED_STATE_HASH_BYTES],
1606
+ ) -> Result<Self, LixError> {
1607
+ let mut cursor = Self {
1608
+ stack: Vec::new(),
1609
+ current: None,
1610
+ };
1611
+ match tree.load_node(store, &root_hash).await? {
1612
+ DecodedNode::Leaf(leaf) => {
1613
+ cursor.current = Some(leaf_summary(root_hash, leaf.entries()));
1614
+ }
1615
+ DecodedNode::Internal(internal) => {
1616
+ let children = internal.children().to_vec();
1617
+ let children_are_leaves =
1618
+ child_summaries_are_leaves(tree, store, &children).await?;
1619
+ cursor.stack.push(LeafSummaryCursorFrame {
1620
+ children,
1621
+ next_index: 0,
1622
+ children_are_leaves,
1623
+ });
1624
+ cursor.advance(tree, store).await?;
1625
+ }
1626
+ }
1627
+ Ok(cursor)
1628
+ }
1629
+
1630
+ fn current(&self) -> Option<&ChildSummary> {
1631
+ self.current.as_ref()
1632
+ }
1633
+
1634
+ async fn advance(
1635
+ &mut self,
1636
+ tree: &TrackedStateTree,
1637
+ store: &mut impl StorageReader,
1638
+ ) -> Result<(), LixError> {
1639
+ self.current = None;
1640
+ while let Some(frame) = self.stack.last_mut() {
1641
+ if frame.next_index >= frame.children.len() {
1642
+ self.stack.pop();
1643
+ continue;
1644
+ }
1645
+
1646
+ let next = frame.children[frame.next_index].clone();
1647
+ let next_is_leaf = frame.children_are_leaves;
1648
+ frame.next_index += 1;
1649
+ if next_is_leaf {
1650
+ self.current = Some(next);
1651
+ return Ok(());
1652
+ }
1653
+ self.descend_to_leaf(tree, store, next).await?;
1654
+ return Ok(());
1655
+ }
1656
+ Ok(())
1657
+ }
1658
+
1659
+ async fn descend_to_leaf(
1660
+ &mut self,
1661
+ tree: &TrackedStateTree,
1662
+ store: &mut impl StorageReader,
1663
+ mut summary: ChildSummary,
1664
+ ) -> Result<(), LixError> {
1665
+ loop {
1666
+ match tree.load_node(store, &summary.child_hash).await? {
1667
+ DecodedNode::Leaf(_) => {
1668
+ self.current = Some(summary);
1669
+ return Ok(());
1670
+ }
1671
+ DecodedNode::Internal(internal) => {
1672
+ let children = internal.children().to_vec();
1673
+ let children_are_leaves =
1674
+ child_summaries_are_leaves(tree, store, &children).await?;
1675
+ let Some(first_child) = children.first().cloned() else {
1676
+ return Err(LixError::new(
1677
+ "LIX_ERROR_UNKNOWN",
1678
+ "tracked-state internal node has no children",
1679
+ ));
1680
+ };
1681
+ self.stack.push(LeafSummaryCursorFrame {
1682
+ children,
1683
+ next_index: 1,
1684
+ children_are_leaves,
1685
+ });
1686
+ if children_are_leaves {
1687
+ self.current = Some(first_child);
1688
+ return Ok(());
1689
+ } else {
1690
+ summary = first_child;
1691
+ }
1692
+ }
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ #[derive(Debug, Default)]
1699
+ struct LeafChunkAccumulator {
1700
+ entries: Vec<EncodedLeafEntry>,
1701
+ key_bytes: usize,
1702
+ value_bytes: usize,
1703
+ }
1704
+
1705
+ #[derive(Debug, Default)]
1706
+ struct InternalChunkAccumulator {
1707
+ children: Vec<ChildSummary>,
1708
+ first_key_bytes: usize,
1709
+ last_key_bytes: usize,
1710
+ }
1711
+
1712
+ fn chunk_leaf_entries(
1713
+ entries: Vec<EncodedLeafEntry>,
1714
+ options: &TrackedStateTreeOptions,
1715
+ ) -> Vec<LeafChunkAccumulator> {
1716
+ if entries.is_empty() {
1717
+ return vec![LeafChunkAccumulator::default()];
1718
+ }
1719
+ let mut groups = Vec::new();
1720
+ let mut current = LeafChunkAccumulator::default();
1721
+ for entry in entries {
1722
+ let item_size = entry.key.len() + entry.value.len();
1723
+ let projected_size = estimate_leaf_chunk_size(
1724
+ current.entries.len() + 1,
1725
+ current.key_bytes + entry.key.len(),
1726
+ current.value_bytes + entry.value.len(),
1727
+ );
1728
+ if !current.entries.is_empty() && projected_size > options.max_chunk_bytes {
1729
+ groups.push(std::mem::take(&mut current));
1730
+ }
1731
+
1732
+ current.key_bytes += entry.key.len();
1733
+ current.value_bytes += entry.value.len();
1734
+ current.entries.push(entry);
1735
+ let current_size = estimate_leaf_chunk_size(
1736
+ current.entries.len(),
1737
+ current.key_bytes,
1738
+ current.value_bytes,
1739
+ );
1740
+ if current_size >= options.min_chunk_bytes
1741
+ && (current_size >= options.max_chunk_bytes
1742
+ || current.entries.last().is_some_and(|entry| {
1743
+ boundary_trigger(
1744
+ &entry.key,
1745
+ 0,
1746
+ current_size,
1747
+ item_size,
1748
+ options.target_chunk_bytes,
1749
+ )
1750
+ }))
1751
+ {
1752
+ groups.push(std::mem::take(&mut current));
1753
+ }
1754
+ }
1755
+ if !current.entries.is_empty() {
1756
+ groups.push(current);
1757
+ }
1758
+ groups
1759
+ }
1760
+
1761
+ fn chunk_internal_entries(
1762
+ children: Vec<ChildSummary>,
1763
+ options: &TrackedStateTreeOptions,
1764
+ level: usize,
1765
+ ) -> Vec<InternalChunkAccumulator> {
1766
+ let mut groups = Vec::new();
1767
+ let mut current = InternalChunkAccumulator::default();
1768
+ for child in children {
1769
+ let item_size = child.first_key.len()
1770
+ + child.last_key.len()
1771
+ + TRACKED_STATE_HASH_BYTES
1772
+ + std::mem::size_of::<u64>();
1773
+ let projected_size = estimate_internal_chunk_size(
1774
+ current.children.len() + 1,
1775
+ current.first_key_bytes + child.first_key.len(),
1776
+ current.last_key_bytes + child.last_key.len(),
1777
+ );
1778
+ if !current.children.is_empty() && projected_size > options.max_chunk_bytes {
1779
+ groups.push(std::mem::take(&mut current));
1780
+ }
1781
+
1782
+ current.first_key_bytes += child.first_key.len();
1783
+ current.last_key_bytes += child.last_key.len();
1784
+ current.children.push(child);
1785
+ let current_size = estimate_internal_chunk_size(
1786
+ current.children.len(),
1787
+ current.first_key_bytes,
1788
+ current.last_key_bytes,
1789
+ );
1790
+ if current_size >= options.min_chunk_bytes
1791
+ && (current_size >= options.max_chunk_bytes
1792
+ || current.children.last().is_some_and(|child| {
1793
+ boundary_trigger(
1794
+ &child.first_key,
1795
+ level,
1796
+ current_size,
1797
+ item_size,
1798
+ options.target_chunk_bytes,
1799
+ )
1800
+ }))
1801
+ {
1802
+ groups.push(std::mem::take(&mut current));
1803
+ }
1804
+ }
1805
+ if !current.children.is_empty() {
1806
+ groups.push(current);
1807
+ }
1808
+ groups
1809
+ }
1810
+
1811
+ fn estimate_leaf_chunk_size(entry_count: usize, key_bytes: usize, value_bytes: usize) -> usize {
1812
+ 16 + entry_count * 8 + key_bytes + value_bytes
1813
+ }
1814
+
1815
+ fn estimate_internal_chunk_size(
1816
+ child_count: usize,
1817
+ first_key_bytes: usize,
1818
+ last_key_bytes: usize,
1819
+ ) -> usize {
1820
+ 16 + child_count * (8 + TRACKED_STATE_HASH_BYTES + std::mem::size_of::<u64>())
1821
+ + first_key_bytes
1822
+ + last_key_bytes
1823
+ }
1824
+
1825
+ fn first_resync_index(
1826
+ generated: &[ChildSummary],
1827
+ existing: &[ChildSummary],
1828
+ mutation_key: &[u8],
1829
+ ) -> Option<(usize, usize)> {
1830
+ for (generated_index, generated) in generated.iter().enumerate() {
1831
+ // A matching old chunk before the mutation key is only unchanged
1832
+ // prefix; resync is only valid after the mutation has been emitted.
1833
+ if generated.first_key.as_slice() <= mutation_key {
1834
+ continue;
1835
+ }
1836
+ if let Some(existing_index) = existing.iter().position(|existing| generated == existing) {
1837
+ return Some((generated_index, existing_index));
1838
+ }
1839
+ }
1840
+ None
1841
+ }
1842
+
1843
+ fn internal_boundaries_match(left: &[ChildSummary], right: &[ChildSummary]) -> bool {
1844
+ left.len() == right.len()
1845
+ && left.iter().zip(right).all(|(left, right)| {
1846
+ left.first_key == right.first_key && left.last_key == right.last_key
1847
+ })
1848
+ }
1849
+
1850
+ async fn child_summaries_are_leaves(
1851
+ tree: &TrackedStateTree,
1852
+ store: &mut impl StorageReader,
1853
+ children: &[ChildSummary],
1854
+ ) -> Result<bool, LixError> {
1855
+ let Some(first_child) = children.first() else {
1856
+ return Ok(false);
1857
+ };
1858
+ Ok(matches!(
1859
+ tree.load_node(store, &first_child.child_hash).await?,
1860
+ DecodedNode::Leaf(_)
1861
+ ))
1862
+ }
1863
+
1864
+ fn decode_entry(
1865
+ entry: &EncodedLeafEntry,
1866
+ ) -> Result<(TrackedStateKey, TrackedStateValue), LixError> {
1867
+ Ok((decode_key(&entry.key)?, decode_value(&entry.value)?))
1868
+ }
1869
+
1870
+ fn parent_index_for_child_index(
1871
+ old_children: &[ChildSummary],
1872
+ old_parents: &[ChildSummary],
1873
+ child_index: usize,
1874
+ ) -> usize {
1875
+ let key = if child_index < old_children.len() {
1876
+ old_children[child_index].first_key.as_slice()
1877
+ } else {
1878
+ old_children
1879
+ .last()
1880
+ .map(|child| child.last_key.as_slice())
1881
+ .unwrap_or_default()
1882
+ };
1883
+ old_parents
1884
+ .iter()
1885
+ .position(|parent| parent.last_key.as_slice() >= key)
1886
+ .unwrap_or_else(|| old_parents.len().saturating_sub(1))
1887
+ }
1888
+
1889
+ fn child_range_for_parent(
1890
+ old_children: &[ChildSummary],
1891
+ parent: &ChildSummary,
1892
+ ) -> Result<Range<usize>, LixError> {
1893
+ let start = old_children
1894
+ .iter()
1895
+ .position(|child| child.last_key.as_slice() >= parent.first_key.as_slice())
1896
+ .ok_or_else(|| {
1897
+ LixError::new(
1898
+ "LIX_ERROR_UNKNOWN",
1899
+ "tracked-state parent summary does not overlap child summaries",
1900
+ )
1901
+ })?;
1902
+ let end = old_children[start..]
1903
+ .iter()
1904
+ .position(|child| child.last_key == parent.last_key)
1905
+ .map(|offset| start + offset + 1)
1906
+ .ok_or_else(|| {
1907
+ LixError::new(
1908
+ "LIX_ERROR_UNKNOWN",
1909
+ "tracked-state parent summary end does not match child summaries",
1910
+ )
1911
+ })?;
1912
+ Ok(start..end)
1913
+ }
1914
+
1915
+ fn leaf_summary(
1916
+ hash: [u8; TRACKED_STATE_HASH_BYTES],
1917
+ entries: &[EncodedLeafEntry],
1918
+ ) -> ChildSummary {
1919
+ ChildSummary {
1920
+ first_key: entries
1921
+ .first()
1922
+ .map(|entry| entry.key.clone())
1923
+ .unwrap_or_default(),
1924
+ last_key: entries
1925
+ .last()
1926
+ .map(|entry| entry.key.clone())
1927
+ .unwrap_or_default(),
1928
+ child_hash: hash,
1929
+ subtree_count: entries.len() as u64,
1930
+ }
1931
+ }
1932
+
1933
+ fn internal_summary(
1934
+ hash: [u8; TRACKED_STATE_HASH_BYTES],
1935
+ children: &[ChildSummary],
1936
+ ) -> Result<ChildSummary, LixError> {
1937
+ let first_key = children
1938
+ .first()
1939
+ .map(|child| child.first_key.clone())
1940
+ .ok_or_else(|| {
1941
+ LixError::new(
1942
+ "LIX_ERROR_UNKNOWN",
1943
+ "tracked-state internal node has no children",
1944
+ )
1945
+ })?;
1946
+ let last_key = children
1947
+ .last()
1948
+ .map(|child| child.last_key.clone())
1949
+ .ok_or_else(|| {
1950
+ LixError::new(
1951
+ "LIX_ERROR_UNKNOWN",
1952
+ "tracked-state internal node has no children",
1953
+ )
1954
+ })?;
1955
+ Ok(ChildSummary {
1956
+ first_key,
1957
+ last_key,
1958
+ child_hash: hash,
1959
+ subtree_count: children.iter().map(|child| child.subtree_count).sum(),
1960
+ })
1961
+ }
1962
+
1963
+ fn push_level_summary(levels: &mut Vec<Vec<ChildSummary>>, level: usize, summary: ChildSummary) {
1964
+ while levels.len() <= level {
1965
+ levels.push(Vec::new());
1966
+ }
1967
+ levels[level].push(summary);
1968
+ }
1969
+
1970
+ fn scan_ranges(request: &TrackedStateTreeScanRequest) -> Vec<EncodedScanRange> {
1971
+ if request.schema_keys.is_empty() {
1972
+ return Vec::new();
1973
+ }
1974
+
1975
+ let can_bind_entity = !request.entity_ids.is_empty()
1976
+ && !request.file_ids.is_empty()
1977
+ && request
1978
+ .file_ids
1979
+ .iter()
1980
+ .all(|filter| !matches!(filter, NullableKeyFilter::Any));
1981
+
1982
+ let mut ranges = Vec::new();
1983
+ for schema_key in &request.schema_keys {
1984
+ if can_bind_entity {
1985
+ for file_filter in &request.file_ids {
1986
+ let file_id = match file_filter {
1987
+ NullableKeyFilter::Null => None,
1988
+ NullableKeyFilter::Value(file_id) => Some(file_id.clone()),
1989
+ NullableKeyFilter::Any => unreachable!("filtered above"),
1990
+ };
1991
+ for entity_id in &request.entity_ids {
1992
+ let key = TrackedStateKey {
1993
+ schema_key: schema_key.clone(),
1994
+ file_id: file_id.clone(),
1995
+ entity_id: entity_id.clone(),
1996
+ };
1997
+ ranges.push(exact_scan_range(encode_key(&key)));
1998
+ }
1999
+ }
2000
+ continue;
2001
+ }
2002
+
2003
+ if request.file_ids.is_empty()
2004
+ || request
2005
+ .file_ids
2006
+ .iter()
2007
+ .any(|filter| matches!(filter, NullableKeyFilter::Any))
2008
+ {
2009
+ ranges.push(prefix_scan_range(encode_schema_key_prefix(schema_key)));
2010
+ continue;
2011
+ }
2012
+
2013
+ for file_filter in &request.file_ids {
2014
+ let prefix = match file_filter {
2015
+ NullableKeyFilter::Null => encode_schema_file_prefix(schema_key, None),
2016
+ NullableKeyFilter::Value(file_id) => {
2017
+ encode_schema_file_prefix(schema_key, Some(file_id))
2018
+ }
2019
+ NullableKeyFilter::Any => unreachable!("handled above"),
2020
+ };
2021
+ ranges.push(prefix_scan_range(prefix));
2022
+ }
2023
+ }
2024
+ ranges
2025
+ }
2026
+
2027
+ fn prefix_scan_range(prefix: Vec<u8>) -> EncodedScanRange {
2028
+ EncodedScanRange {
2029
+ end: lexicographic_successor(&prefix),
2030
+ start: prefix,
2031
+ }
2032
+ }
2033
+
2034
+ fn exact_scan_range(key: Vec<u8>) -> EncodedScanRange {
2035
+ EncodedScanRange {
2036
+ end: lexicographic_successor(&key),
2037
+ start: key,
2038
+ }
2039
+ }
2040
+
2041
+ fn lexicographic_successor(bytes: &[u8]) -> Option<Vec<u8>> {
2042
+ let mut out = bytes.to_vec();
2043
+ for index in (0..out.len()).rev() {
2044
+ if out[index] != u8::MAX {
2045
+ out[index] += 1;
2046
+ out.truncate(index + 1);
2047
+ return Some(out);
2048
+ }
2049
+ }
2050
+ None
2051
+ }
2052
+
2053
+ fn child_summary_overlaps_scan_ranges(child: &ChildSummary, ranges: &[EncodedScanRange]) -> bool {
2054
+ ranges.is_empty()
2055
+ || ranges.iter().any(|range| {
2056
+ child.last_key.as_slice() >= range.start.as_slice()
2057
+ && range
2058
+ .end
2059
+ .as_ref()
2060
+ .is_none_or(|end| child.first_key.as_slice() < end.as_slice())
2061
+ })
2062
+ }
2063
+
2064
+ fn child_summary_contained_by_scan_ranges(
2065
+ child: &ChildSummary,
2066
+ ranges: &[EncodedScanRange],
2067
+ ) -> bool {
2068
+ ranges.is_empty()
2069
+ || ranges.iter().any(|range| {
2070
+ child.first_key.as_slice() >= range.start.as_slice()
2071
+ && range
2072
+ .end
2073
+ .as_ref()
2074
+ .is_none_or(|end| child.last_key.as_slice() < end.as_slice())
2075
+ })
2076
+ }
2077
+
2078
+ fn encoded_key_in_scan_ranges(key: &[u8], ranges: &[EncodedScanRange]) -> bool {
2079
+ ranges.is_empty()
2080
+ || ranges.iter().any(|range| {
2081
+ key >= range.start.as_slice()
2082
+ && range.end.as_ref().is_none_or(|end| key < end.as_slice())
2083
+ })
2084
+ }
2085
+
2086
+ fn key_matches_scan_filters(request: &TrackedStateTreeScanRequest, key: &TrackedStateKey) -> bool {
2087
+ if !request.schema_keys.is_empty() && !request.schema_keys.contains(&key.schema_key) {
2088
+ return false;
2089
+ }
2090
+ if !request.entity_ids.is_empty() && !request.entity_ids.contains(&key.entity_id) {
2091
+ return false;
2092
+ }
2093
+ if !request.file_ids.is_empty()
2094
+ && !request
2095
+ .file_ids
2096
+ .iter()
2097
+ .any(|filter| filter.matches(key.file_id.as_ref()))
2098
+ {
2099
+ return false;
2100
+ }
2101
+ true
2102
+ }
2103
+
2104
+ fn scan_limit_reached(request: &TrackedStateTreeScanRequest, row_count: usize) -> bool {
2105
+ request.limit.is_some_and(|limit| row_count >= limit)
2106
+ }
2107
+
2108
+ #[cfg(test)]
2109
+ mod tests {
2110
+ use std::sync::Arc;
2111
+
2112
+ use super::*;
2113
+ use crate::backend::testing::UnitTestBackend;
2114
+ use crate::entity_identity::EntityIdentity;
2115
+ use crate::storage::{StorageContext, StorageWriteTransaction};
2116
+
2117
+ #[tokio::test]
2118
+ async fn exact_read_roundtrips_from_stored_root() {
2119
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2120
+ let tree = TrackedStateTree::new();
2121
+ let key = key("schema", None, "entity");
2122
+ let value = value("change-1", Some("{}"));
2123
+
2124
+ let mut transaction = storage
2125
+ .begin_write_transaction()
2126
+ .await
2127
+ .expect("transaction should open");
2128
+ let result = apply_mutations_for_test(
2129
+ &tree,
2130
+ transaction.as_mut(),
2131
+ None,
2132
+ vec![TrackedStateMutation::put(key.clone(), value.clone())],
2133
+ Some("commit-1"),
2134
+ )
2135
+ .await
2136
+ .expect("mutations should apply");
2137
+ transaction
2138
+ .commit()
2139
+ .await
2140
+ .expect("transaction should commit");
2141
+
2142
+ let mut store = storage.clone();
2143
+ assert_eq!(
2144
+ tree.load_root(&mut store, "commit-1")
2145
+ .await
2146
+ .expect("root should load"),
2147
+ Some(result.root_id.clone())
2148
+ );
2149
+ assert_eq!(
2150
+ tree.get(&mut store, &result.root_id, &key)
2151
+ .await
2152
+ .expect("row should load"),
2153
+ Some(value)
2154
+ );
2155
+ }
2156
+
2157
+ #[tokio::test]
2158
+ async fn latest_mutation_for_key_wins() {
2159
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2160
+ let tree = TrackedStateTree::new();
2161
+ let key = key("schema", None, "entity");
2162
+
2163
+ let mut transaction = storage
2164
+ .begin_write_transaction()
2165
+ .await
2166
+ .expect("transaction should open");
2167
+ let result = apply_mutations_for_test(
2168
+ &tree,
2169
+ transaction.as_mut(),
2170
+ None,
2171
+ vec![
2172
+ TrackedStateMutation::put(key.clone(), value("change-old", Some("{\"v\":1}"))),
2173
+ TrackedStateMutation::put(key.clone(), value("change-new", Some("{\"v\":2}"))),
2174
+ ],
2175
+ None,
2176
+ )
2177
+ .await
2178
+ .expect("mutations should apply");
2179
+ transaction
2180
+ .commit()
2181
+ .await
2182
+ .expect("transaction should commit");
2183
+
2184
+ let mut store = storage.clone();
2185
+ let loaded = tree
2186
+ .get(&mut store, &result.root_id, &key)
2187
+ .await
2188
+ .expect("row should load")
2189
+ .expect("row should exist");
2190
+ assert_eq!(loaded.change_id, "change-new");
2191
+ assert_eq!(
2192
+ loaded.snapshot_ref,
2193
+ Some(crate::json_store::JsonRef::from_hash_bytes([2; 32]))
2194
+ );
2195
+ }
2196
+
2197
+ #[tokio::test]
2198
+ async fn scan_filters_and_hides_tombstones_by_default() {
2199
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2200
+ let tree = TrackedStateTree::new();
2201
+
2202
+ let mut transaction = storage
2203
+ .begin_write_transaction()
2204
+ .await
2205
+ .expect("transaction should open");
2206
+ let result = apply_mutations_for_test(
2207
+ &tree,
2208
+ transaction.as_mut(),
2209
+ None,
2210
+ vec![
2211
+ TrackedStateMutation::put(
2212
+ key("schema-a", None, "visible"),
2213
+ value("c1", Some("{}")),
2214
+ ),
2215
+ TrackedStateMutation::put(key("schema-a", None, "deleted"), value("c2", None)),
2216
+ TrackedStateMutation::put(key("schema-b", None, "other"), value("c3", Some("{}"))),
2217
+ ],
2218
+ None,
2219
+ )
2220
+ .await
2221
+ .expect("mutations should apply");
2222
+ transaction
2223
+ .commit()
2224
+ .await
2225
+ .expect("transaction should commit");
2226
+
2227
+ let mut store = storage.clone();
2228
+ let rows = tree
2229
+ .scan(
2230
+ &mut store,
2231
+ &result.root_id,
2232
+ &TrackedStateTreeScanRequest {
2233
+ schema_keys: vec!["schema-a".to_string()],
2234
+ ..Default::default()
2235
+ },
2236
+ )
2237
+ .await
2238
+ .expect("scan should succeed");
2239
+ assert_eq!(rows.len(), 1);
2240
+ assert_eq!(
2241
+ rows[0].0.entity_id.as_string().expect("identity"),
2242
+ "visible"
2243
+ );
2244
+ }
2245
+
2246
+ #[tokio::test]
2247
+ async fn scan_filters_by_schema_entity_and_file() {
2248
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2249
+ let tree = TrackedStateTree::new();
2250
+
2251
+ let mut transaction = storage
2252
+ .begin_write_transaction()
2253
+ .await
2254
+ .expect("transaction should open");
2255
+ let result = apply_mutations_for_test(
2256
+ &tree,
2257
+ transaction.as_mut(),
2258
+ None,
2259
+ vec![
2260
+ TrackedStateMutation::put(
2261
+ key("schema-a", Some("file-a"), "entity-a"),
2262
+ value("c1", Some("{}")),
2263
+ ),
2264
+ TrackedStateMutation::put(
2265
+ key("schema-a", Some("file-b"), "entity-a"),
2266
+ value("c2", Some("{}")),
2267
+ ),
2268
+ TrackedStateMutation::put(
2269
+ key("schema-a", Some("file-a"), "entity-b"),
2270
+ value("c3", Some("{}")),
2271
+ ),
2272
+ TrackedStateMutation::put(
2273
+ key("schema-b", Some("file-a"), "entity-a"),
2274
+ value("c4", Some("{}")),
2275
+ ),
2276
+ ],
2277
+ None,
2278
+ )
2279
+ .await
2280
+ .expect("mutations should apply");
2281
+ transaction
2282
+ .commit()
2283
+ .await
2284
+ .expect("transaction should commit");
2285
+
2286
+ let mut store = storage.clone();
2287
+ let rows = tree
2288
+ .scan(
2289
+ &mut store,
2290
+ &result.root_id,
2291
+ &TrackedStateTreeScanRequest {
2292
+ schema_keys: vec!["schema-a".to_string()],
2293
+ entity_ids: vec![crate::entity_identity::EntityIdentity::single("entity-a")],
2294
+ file_ids: vec![crate::NullableKeyFilter::Value("file-a".to_string())],
2295
+ ..Default::default()
2296
+ },
2297
+ )
2298
+ .await
2299
+ .expect("scan should succeed");
2300
+
2301
+ assert_eq!(rows.len(), 1);
2302
+ assert_eq!(rows[0].0.schema_key, "schema-a");
2303
+ assert_eq!(
2304
+ rows[0].0.entity_id.as_string().expect("identity"),
2305
+ "entity-a"
2306
+ );
2307
+ assert_eq!(rows[0].0.file_id.as_deref(), Some("file-a"));
2308
+ }
2309
+
2310
+ #[tokio::test]
2311
+ async fn applying_to_base_root_reuses_existing_rows_and_overwrites_changed_rows() {
2312
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2313
+ let tree = TrackedStateTree::new();
2314
+ let unchanged_key = key("schema", None, "unchanged");
2315
+ let changed_key = key("schema", None, "changed");
2316
+
2317
+ let mut transaction = storage
2318
+ .begin_write_transaction()
2319
+ .await
2320
+ .expect("transaction should open");
2321
+ let base = apply_mutations_for_test(
2322
+ &tree,
2323
+ transaction.as_mut(),
2324
+ None,
2325
+ vec![
2326
+ TrackedStateMutation::put(unchanged_key.clone(), value("c1", Some("{}"))),
2327
+ TrackedStateMutation::put(changed_key.clone(), value("c2", Some("{\"old\":true}"))),
2328
+ ],
2329
+ None,
2330
+ )
2331
+ .await
2332
+ .expect("base should build");
2333
+ let next = apply_mutations_for_test(
2334
+ &tree,
2335
+ transaction.as_mut(),
2336
+ Some(&base.root_id),
2337
+ vec![TrackedStateMutation::put(
2338
+ changed_key.clone(),
2339
+ value("c3", Some("{\"new\":true}")),
2340
+ )],
2341
+ None,
2342
+ )
2343
+ .await
2344
+ .expect("next should build");
2345
+ transaction
2346
+ .commit()
2347
+ .await
2348
+ .expect("transaction should commit");
2349
+
2350
+ let mut store = storage.clone();
2351
+ assert_eq!(
2352
+ tree.get(&mut store, &next.root_id, &unchanged_key)
2353
+ .await
2354
+ .expect("unchanged read")
2355
+ .expect("unchanged exists")
2356
+ .change_id,
2357
+ "c1"
2358
+ );
2359
+ assert_eq!(
2360
+ tree.get(&mut store, &next.root_id, &changed_key)
2361
+ .await
2362
+ .expect("changed read")
2363
+ .expect("changed exists")
2364
+ .change_id,
2365
+ "c3"
2366
+ );
2367
+ }
2368
+
2369
+ #[tokio::test]
2370
+ async fn two_commit_roots_can_share_unchanged_rows() {
2371
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2372
+ let tree = TrackedStateTree::new();
2373
+ let shared_key = key("schema", None, "shared");
2374
+ let branch_a_key = key("schema", None, "branch-a");
2375
+ let branch_b_key = key("schema", None, "branch-b");
2376
+
2377
+ let mut transaction = storage
2378
+ .begin_write_transaction()
2379
+ .await
2380
+ .expect("transaction should open");
2381
+ let base = apply_mutations_for_test(
2382
+ &tree,
2383
+ transaction.as_mut(),
2384
+ None,
2385
+ vec![TrackedStateMutation::put(
2386
+ shared_key.clone(),
2387
+ value("shared-change", Some("{\"shared\":true}")),
2388
+ )],
2389
+ Some("commit-base"),
2390
+ )
2391
+ .await
2392
+ .expect("base root should build");
2393
+ let branch_a = apply_mutations_for_test(
2394
+ &tree,
2395
+ transaction.as_mut(),
2396
+ Some(&base.root_id),
2397
+ vec![TrackedStateMutation::put(
2398
+ branch_a_key.clone(),
2399
+ value("branch-a-change", Some("{\"branch\":\"a\"}")),
2400
+ )],
2401
+ Some("commit-a"),
2402
+ )
2403
+ .await
2404
+ .expect("branch a root should build");
2405
+ let branch_b = apply_mutations_for_test(
2406
+ &tree,
2407
+ transaction.as_mut(),
2408
+ Some(&base.root_id),
2409
+ vec![TrackedStateMutation::put(
2410
+ branch_b_key.clone(),
2411
+ value("branch-b-change", Some("{\"branch\":\"b\"}")),
2412
+ )],
2413
+ Some("commit-b"),
2414
+ )
2415
+ .await
2416
+ .expect("branch b root should build");
2417
+ transaction
2418
+ .commit()
2419
+ .await
2420
+ .expect("transaction should commit");
2421
+
2422
+ assert_ne!(branch_a.root_id, branch_b.root_id);
2423
+ let mut store = storage.clone();
2424
+ assert_eq!(
2425
+ tree.get(&mut store, &branch_a.root_id, &shared_key)
2426
+ .await
2427
+ .expect("branch a shared row should load"),
2428
+ Some(value("shared-change", Some("{\"shared\":true}")))
2429
+ );
2430
+ assert_eq!(
2431
+ tree.get(&mut store, &branch_b.root_id, &shared_key)
2432
+ .await
2433
+ .expect("branch b shared row should load"),
2434
+ Some(value("shared-change", Some("{\"shared\":true}")))
2435
+ );
2436
+ assert!(tree
2437
+ .get(&mut store, &branch_a.root_id, &branch_b_key)
2438
+ .await
2439
+ .expect("branch a should read")
2440
+ .is_none());
2441
+ assert!(tree
2442
+ .get(&mut store, &branch_b.root_id, &branch_a_key)
2443
+ .await
2444
+ .expect("branch b should read")
2445
+ .is_none());
2446
+ }
2447
+
2448
+ #[tokio::test]
2449
+ async fn single_update_matches_full_canonical_rebuild() {
2450
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2451
+ let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
2452
+ target_chunk_bytes: 128,
2453
+ min_chunk_bytes: 64,
2454
+ max_chunk_bytes: 256,
2455
+ });
2456
+ let rows = (0..100)
2457
+ .map(|index| {
2458
+ TrackedStateMutation::put(
2459
+ key("schema", None, &format!("entity-{index:03}")),
2460
+ value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
2461
+ )
2462
+ })
2463
+ .collect::<Vec<_>>();
2464
+ let changed_key = key("schema", None, "entity-000");
2465
+ let changed_value = value("changed", Some("{\"v\":\"changed\"}"));
2466
+
2467
+ let mut transaction = storage
2468
+ .begin_write_transaction()
2469
+ .await
2470
+ .expect("transaction should open");
2471
+ let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2472
+ .await
2473
+ .expect("base should build");
2474
+ let fast = apply_mutations_for_test(
2475
+ &tree,
2476
+ transaction.as_mut(),
2477
+ Some(&base.root_id),
2478
+ vec![TrackedStateMutation::put(
2479
+ changed_key.clone(),
2480
+ changed_value.clone(),
2481
+ )],
2482
+ None,
2483
+ )
2484
+ .await
2485
+ .expect("fast path should apply");
2486
+ let mut canonical_entries = tree
2487
+ .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2488
+ .await
2489
+ .expect("base entries should collect");
2490
+ assert!(canonical_entries
2491
+ .windows(2)
2492
+ .all(|window| window[0].key < window[1].key));
2493
+ let encoded_changed_key = encode_key(&changed_key);
2494
+ let encoded_changed_value = encode_value(&changed_value);
2495
+ let index = canonical_entries
2496
+ .binary_search_by(|entry| entry.key.as_slice().cmp(&encoded_changed_key))
2497
+ .expect("changed key should exist");
2498
+ canonical_entries[index].value = encoded_changed_value;
2499
+ let canonical = tree
2500
+ .build_tree_from_entries(canonical_entries)
2501
+ .expect("canonical root should build");
2502
+
2503
+ assert_eq!(fast.root_id, canonical.root_id);
2504
+ }
2505
+
2506
+ #[tokio::test]
2507
+ async fn single_insert_matches_full_canonical_rebuild() {
2508
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2509
+ let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
2510
+ target_chunk_bytes: 128,
2511
+ min_chunk_bytes: 64,
2512
+ max_chunk_bytes: 256,
2513
+ });
2514
+ let rows = (0..100)
2515
+ .map(|index| {
2516
+ TrackedStateMutation::put(
2517
+ key("schema", None, &format!("entity-{index:03}")),
2518
+ value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
2519
+ )
2520
+ })
2521
+ .collect::<Vec<_>>();
2522
+ let inserted_key = key("schema", None, "entity-050a");
2523
+ let inserted_value = value("inserted", Some("{\"v\":\"inserted\"}"));
2524
+
2525
+ let mut transaction = storage
2526
+ .begin_write_transaction()
2527
+ .await
2528
+ .expect("transaction should open");
2529
+ let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2530
+ .await
2531
+ .expect("base should build");
2532
+ let fast = apply_mutations_for_test(
2533
+ &tree,
2534
+ transaction.as_mut(),
2535
+ Some(&base.root_id),
2536
+ vec![TrackedStateMutation::put(
2537
+ inserted_key.clone(),
2538
+ inserted_value.clone(),
2539
+ )],
2540
+ None,
2541
+ )
2542
+ .await
2543
+ .expect("fast path should apply");
2544
+ let mut canonical_entries = tree
2545
+ .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2546
+ .await
2547
+ .expect("base entries should collect");
2548
+ let encoded_inserted_key = encode_key(&inserted_key);
2549
+ let encoded_inserted_value = encode_value(&inserted_value);
2550
+ let index = canonical_entries
2551
+ .binary_search_by(|entry| entry.key.as_slice().cmp(&encoded_inserted_key))
2552
+ .expect_err("inserted key should not exist");
2553
+ canonical_entries.insert(
2554
+ index,
2555
+ EncodedLeafEntry {
2556
+ key: encoded_inserted_key,
2557
+ value: encoded_inserted_value,
2558
+ },
2559
+ );
2560
+ let canonical = tree
2561
+ .build_tree_from_entries(canonical_entries)
2562
+ .expect("canonical root should build");
2563
+
2564
+ assert_eq!(fast.root_id, canonical.root_id);
2565
+ }
2566
+
2567
+ #[tokio::test]
2568
+ async fn batch_update_matches_full_canonical_rebuild() {
2569
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2570
+ let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
2571
+ target_chunk_bytes: 128,
2572
+ min_chunk_bytes: 64,
2573
+ max_chunk_bytes: 256,
2574
+ });
2575
+ let rows = (0..100)
2576
+ .map(|index| {
2577
+ TrackedStateMutation::put(
2578
+ key("schema", None, &format!("entity-{index:03}")),
2579
+ value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
2580
+ )
2581
+ })
2582
+ .collect::<Vec<_>>();
2583
+ let updates = (10..25)
2584
+ .map(|index| {
2585
+ TrackedStateMutation::put(
2586
+ key("schema", None, &format!("entity-{index:03}")),
2587
+ value(
2588
+ &format!("changed-{index}"),
2589
+ Some(&format!("{{\"changed\":{index}}}")),
2590
+ ),
2591
+ )
2592
+ })
2593
+ .collect::<Vec<_>>();
2594
+
2595
+ let mut transaction = storage
2596
+ .begin_write_transaction()
2597
+ .await
2598
+ .expect("transaction should open");
2599
+ let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2600
+ .await
2601
+ .expect("base should build");
2602
+ let fast = apply_mutations_for_test(
2603
+ &tree,
2604
+ transaction.as_mut(),
2605
+ Some(&base.root_id),
2606
+ updates.clone(),
2607
+ None,
2608
+ )
2609
+ .await
2610
+ .expect("batch path should apply");
2611
+ let mut canonical_entries = tree
2612
+ .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2613
+ .await
2614
+ .expect("base entries should collect");
2615
+ for update in updates {
2616
+ let TrackedStateMutation::Put { key, value } = update;
2617
+ let encoded_key = encode_key(&key);
2618
+ let encoded_value = encode_value(&value);
2619
+ let index = canonical_entries
2620
+ .binary_search_by(|entry| entry.key.as_slice().cmp(&encoded_key))
2621
+ .expect("updated key should exist");
2622
+ canonical_entries[index].value = encoded_value;
2623
+ }
2624
+ let canonical = tree
2625
+ .build_tree_from_entries(canonical_entries)
2626
+ .expect("canonical root should build");
2627
+
2628
+ assert_eq!(fast.root_id, canonical.root_id);
2629
+ }
2630
+
2631
+ #[tokio::test]
2632
+ async fn batch_insert_matches_full_canonical_rebuild() {
2633
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
2634
+ let tree = TrackedStateTree::with_options(TrackedStateTreeOptions {
2635
+ target_chunk_bytes: 128,
2636
+ min_chunk_bytes: 64,
2637
+ max_chunk_bytes: 256,
2638
+ });
2639
+ let rows = (0..100)
2640
+ .map(|index| {
2641
+ TrackedStateMutation::put(
2642
+ key("schema", None, &format!("entity-{index:03}")),
2643
+ value(&format!("c-{index}"), Some(&format!("{{\"v\":{index}}}"))),
2644
+ )
2645
+ })
2646
+ .collect::<Vec<_>>();
2647
+ let inserts = ["entity-050a", "entity-050b", "entity-050c"]
2648
+ .into_iter()
2649
+ .enumerate()
2650
+ .map(|(index, entity_id)| {
2651
+ TrackedStateMutation::put(
2652
+ key("schema", None, entity_id),
2653
+ value(
2654
+ &format!("inserted-{index}"),
2655
+ Some(&format!("{{\"inserted\":{index}}}")),
2656
+ ),
2657
+ )
2658
+ })
2659
+ .collect::<Vec<_>>();
2660
+
2661
+ let mut transaction = storage
2662
+ .begin_write_transaction()
2663
+ .await
2664
+ .expect("transaction should open");
2665
+ let base = apply_mutations_for_test(&tree, transaction.as_mut(), None, rows, None)
2666
+ .await
2667
+ .expect("base should build");
2668
+ let fast = apply_mutations_for_test(
2669
+ &tree,
2670
+ transaction.as_mut(),
2671
+ Some(&base.root_id),
2672
+ inserts.clone(),
2673
+ None,
2674
+ )
2675
+ .await
2676
+ .expect("batch path should apply");
2677
+ let mut canonical_entries = tree
2678
+ .collect_leaf_entries(&mut transaction.as_mut(), &base.root_id)
2679
+ .await
2680
+ .expect("base entries should collect");
2681
+ for insert in inserts {
2682
+ let TrackedStateMutation::Put { key, value } = insert;
2683
+ let encoded_key = encode_key(&key);
2684
+ let encoded_value = encode_value(&value);
2685
+ let index = canonical_entries
2686
+ .binary_search_by(|entry| entry.key.as_slice().cmp(&encoded_key))
2687
+ .expect_err("inserted key should not exist");
2688
+ canonical_entries.insert(
2689
+ index,
2690
+ EncodedLeafEntry {
2691
+ key: encoded_key,
2692
+ value: encoded_value,
2693
+ },
2694
+ );
2695
+ }
2696
+ let canonical = tree
2697
+ .build_tree_from_entries(canonical_entries)
2698
+ .expect("canonical root should build");
2699
+
2700
+ assert_eq!(fast.root_id, canonical.root_id);
2701
+ }
2702
+
2703
+ async fn apply_mutations_for_test(
2704
+ tree: &TrackedStateTree,
2705
+ transaction: &mut dyn StorageWriteTransaction,
2706
+ base_root: Option<&TrackedStateRootId>,
2707
+ mutations: Vec<TrackedStateMutation>,
2708
+ commit_id: Option<&str>,
2709
+ ) -> Result<TrackedStateApplyResult, LixError> {
2710
+ let mut writes = StorageWriteSet::new();
2711
+ let result = tree
2712
+ .apply_mutations(transaction, &mut writes, base_root, mutations, commit_id)
2713
+ .await?;
2714
+ writes.apply(transaction).await?;
2715
+ Ok(result)
2716
+ }
2717
+
2718
+ fn key(schema_key: &str, file_id: Option<&str>, entity_id: &str) -> TrackedStateKey {
2719
+ TrackedStateKey {
2720
+ schema_key: schema_key.to_string(),
2721
+ file_id: file_id.map(str::to_string),
2722
+ entity_id: EntityIdentity::single(entity_id),
2723
+ }
2724
+ }
2725
+
2726
+ fn value(change_id: &str, snapshot_content: Option<&str>) -> TrackedStateValue {
2727
+ let snapshot_ref = match snapshot_content {
2728
+ Some("{\"v\":1}") => Some(crate::json_store::JsonRef::from_hash_bytes([1; 32])),
2729
+ Some("{\"v\":2}") => Some(crate::json_store::JsonRef::from_hash_bytes([2; 32])),
2730
+ Some(_) => Some(crate::json_store::JsonRef::from_hash_bytes([3; 32])),
2731
+ None => None,
2732
+ };
2733
+ TrackedStateValue {
2734
+ snapshot_ref,
2735
+ metadata_ref: None,
2736
+ schema_version: "1".to_string(),
2737
+ created_at: "2026-01-01T00:00:00Z".to_string(),
2738
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
2739
+ change_id: change_id.to_string(),
2740
+ commit_id: "commit".to_string(),
2741
+ deleted: snapshot_content.is_none(),
2742
+ }
2743
+ }
2744
+ }