@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,983 @@
1
+ use crate::commit_graph::CommitGraphContext;
2
+ use crate::json_store::{JsonStoreContext, JsonStoreWriter};
3
+ use crate::storage::{StorageReader, StorageWriteSet};
4
+ use crate::tracked_state::by_file_index::ByFileIndex;
5
+ use crate::tracked_state::diff::{diff_commits, TrackedStateDiff, TrackedStateDiffRequest};
6
+ use crate::tracked_state::materialize_value;
7
+ use crate::tracked_state::merge::{self, TrackedStateMergePlan};
8
+ use crate::tracked_state::rebuild::TrackedStateRebuildReport;
9
+ use crate::tracked_state::storage;
10
+ use crate::tracked_state::tree::TrackedStateTree;
11
+ use crate::tracked_state::tree_types::{
12
+ TrackedStateKey, TrackedStateMutation, TrackedStateTreeScanRequest, TrackedStateValue,
13
+ };
14
+ use crate::tracked_state::{TrackedStateRow, TrackedStateRowRequest, TrackedStateScanRequest};
15
+ use crate::LixError;
16
+
17
+ /// Factory for rebuildable tracked-state readers and writers.
18
+ ///
19
+ /// Tracked state is stored as content-addressed roots. Version refs
20
+ /// choose which commit/root to read; this context only owns root operations.
21
+ #[derive(Clone)]
22
+ pub(crate) struct TrackedStateContext {
23
+ tree: TrackedStateTree,
24
+ }
25
+
26
+ impl TrackedStateContext {
27
+ pub(crate) fn new() -> Self {
28
+ Self {
29
+ tree: TrackedStateTree::new(),
30
+ }
31
+ }
32
+
33
+ /// Creates a commit-id-addressed tracked-state reader.
34
+ pub(crate) fn reader<S>(&self, store: S) -> TrackedStateStoreReader<S>
35
+ where
36
+ S: StorageReader,
37
+ {
38
+ TrackedStateStoreReader {
39
+ store,
40
+ tree: self.tree.clone(),
41
+ }
42
+ }
43
+
44
+ /// Creates a tracked-state writer that stages into a caller-owned write set.
45
+ pub(crate) fn writer(&self) -> TrackedStateWriter {
46
+ TrackedStateWriter {
47
+ tree: self.tree.clone(),
48
+ }
49
+ }
50
+
51
+ /// Rebuilds tracked state at one commit from commit-graph entities.
52
+ pub(crate) async fn rebuild_state_at_commit<R, S>(
53
+ &self,
54
+ commit_graph: &CommitGraphContext,
55
+ read_store: R,
56
+ tracked_store: &mut S,
57
+ writes: &mut StorageWriteSet,
58
+ json_writer: &mut JsonStoreWriter,
59
+ head_commit_id: &str,
60
+ ) -> Result<TrackedStateRebuildReport, LixError>
61
+ where
62
+ R: StorageReader,
63
+ S: StorageReader + ?Sized,
64
+ {
65
+ crate::tracked_state::rebuild::rebuild_state_at_commit(
66
+ self,
67
+ commit_graph,
68
+ read_store,
69
+ tracked_store,
70
+ writes,
71
+ json_writer,
72
+ head_commit_id,
73
+ )
74
+ .await
75
+ }
76
+ }
77
+
78
+ /// Store-backed tracked-state reader created by `TrackedStateContext`.
79
+ pub(crate) struct TrackedStateStoreReader<S> {
80
+ store: S,
81
+ tree: TrackedStateTree,
82
+ }
83
+
84
+ impl<S> TrackedStateStoreReader<S>
85
+ where
86
+ S: StorageReader,
87
+ {
88
+ pub(crate) async fn scan_rows_at_commit(
89
+ &mut self,
90
+ commit_id: &str,
91
+ request: &TrackedStateScanRequest,
92
+ ) -> Result<Vec<TrackedStateRow>, LixError> {
93
+ let Some(root_id) = self.tree.load_root(&mut self.store, commit_id).await? else {
94
+ return Ok(Vec::new());
95
+ };
96
+ let rows = if ByFileIndex::should_use(request) {
97
+ let Some(by_file_root_id) =
98
+ storage::load_by_file_root(&mut self.store, commit_id).await?
99
+ else {
100
+ return Ok(Vec::new());
101
+ };
102
+ self.scan_rows_at_commit_by_file_index(&root_id, &by_file_root_id, request)
103
+ .await?
104
+ } else {
105
+ let rows = self
106
+ .tree
107
+ .scan(
108
+ &mut self.store,
109
+ &root_id,
110
+ &tree_scan_request_from_tracked(request),
111
+ )
112
+ .await?;
113
+ rows
114
+ };
115
+ let projection = crate::tracked_state::TrackedMaterializationProjection::from_columns(
116
+ &request.projection.columns,
117
+ );
118
+ let mut json_reader = JsonStoreContext::new().reader(&mut self.store);
119
+ let mut materialized = Vec::with_capacity(rows.len());
120
+ for (key, value) in rows {
121
+ materialized.push(materialize_value(&mut json_reader, key, value, &projection).await?);
122
+ }
123
+ Ok(materialized)
124
+ }
125
+
126
+ pub(crate) async fn load_row_at_commit(
127
+ &mut self,
128
+ commit_id: &str,
129
+ request: &TrackedStateRowRequest,
130
+ ) -> Result<Option<TrackedStateRow>, LixError> {
131
+ let key = tracked_key_from_request(request)?;
132
+ let Some(root_id) = self.tree.load_root(&mut self.store, commit_id).await? else {
133
+ return Ok(None);
134
+ };
135
+ let row = self
136
+ .tree
137
+ .get(&mut self.store, &root_id, &key)
138
+ .await?
139
+ .map(|value| async {
140
+ let mut json_reader = JsonStoreContext::new().reader(&mut self.store);
141
+ materialize_value(
142
+ &mut json_reader,
143
+ key,
144
+ value,
145
+ &crate::tracked_state::TrackedMaterializationProjection::full(),
146
+ )
147
+ .await
148
+ });
149
+ match row {
150
+ Some(row) => row.await.map(Some),
151
+ None => Ok(None),
152
+ }
153
+ }
154
+
155
+ pub(crate) async fn diff_commits(
156
+ &mut self,
157
+ left_commit_id: &str,
158
+ right_commit_id: &str,
159
+ request: &TrackedStateDiffRequest,
160
+ ) -> Result<TrackedStateDiff, LixError> {
161
+ diff_commits(self, left_commit_id, right_commit_id, request).await
162
+ }
163
+
164
+ pub(crate) async fn diff_tree_entries_at_commits(
165
+ &mut self,
166
+ left_commit_id: &str,
167
+ right_commit_id: &str,
168
+ request: &TrackedStateTreeScanRequest,
169
+ ) -> Result<Vec<crate::tracked_state::tree_types::TrackedStateTreeDiffEntry>, LixError> {
170
+ let left_root = self.tree.load_root(&mut self.store, left_commit_id).await?;
171
+ let right_root = self
172
+ .tree
173
+ .load_root(&mut self.store, right_commit_id)
174
+ .await?;
175
+ let entries = self
176
+ .tree
177
+ .diff(
178
+ &mut self.store,
179
+ left_root.as_ref(),
180
+ right_root.as_ref(),
181
+ request,
182
+ )
183
+ .await?;
184
+ Ok(entries)
185
+ }
186
+
187
+ pub(crate) async fn materialize_tree_value(
188
+ &mut self,
189
+ key: TrackedStateKey,
190
+ value: TrackedStateValue,
191
+ ) -> Result<TrackedStateRow, LixError> {
192
+ let mut json_reader = JsonStoreContext::new().reader(&mut self.store);
193
+ materialize_value(
194
+ &mut json_reader,
195
+ key,
196
+ value,
197
+ &crate::tracked_state::TrackedMaterializationProjection::full(),
198
+ )
199
+ .await
200
+ }
201
+
202
+ async fn scan_rows_at_commit_by_file_index(
203
+ &mut self,
204
+ primary_root_id: &crate::tracked_state::tree_types::TrackedStateRootId,
205
+ by_file_root_id: &crate::tracked_state::tree_types::TrackedStateRootId,
206
+ request: &TrackedStateScanRequest,
207
+ ) -> Result<Vec<(TrackedStateKey, TrackedStateValue)>, LixError> {
208
+ let by_file_request = ByFileIndex::scan_request_from_tracked(request);
209
+ let index_match_count = self
210
+ .tree
211
+ .count_matching_keys(&mut self.store, by_file_root_id, &by_file_request)
212
+ .await?;
213
+ let primary_row_count = self
214
+ .tree
215
+ .row_count(&mut self.store, primary_root_id)
216
+ .await?;
217
+ if index_match_count * 20 > primary_row_count {
218
+ let rows = self
219
+ .tree
220
+ .scan(
221
+ &mut self.store,
222
+ primary_root_id,
223
+ &tree_scan_request_from_tracked(request),
224
+ )
225
+ .await?;
226
+ return Ok(rows);
227
+ }
228
+ let index_rows = self
229
+ .tree
230
+ .scan(&mut self.store, by_file_root_id, &by_file_request)
231
+ .await?;
232
+ let mut rows = Vec::new();
233
+ let tree_request = tree_scan_request_from_tracked(request);
234
+ let needs_payloads = scan_needs_json_payloads(request);
235
+ if needs_payloads {
236
+ let mut primary_keys = Vec::with_capacity(index_rows.len());
237
+ for (index_key, _) in index_rows {
238
+ if let Some(primary_key) = ByFileIndex::primary_key_from_index_key(index_key) {
239
+ primary_keys.push(primary_key);
240
+ }
241
+ }
242
+ let primary_values = self
243
+ .tree
244
+ .get_many(&mut self.store, primary_root_id, &primary_keys)
245
+ .await?;
246
+ for (primary_key, value) in primary_keys.into_iter().zip(primary_values) {
247
+ if request.limit.is_some_and(|limit| rows.len() >= limit) {
248
+ break;
249
+ }
250
+ let Some(value) = value else {
251
+ continue;
252
+ };
253
+ if !tree_request.matches(&primary_key, &value) {
254
+ continue;
255
+ }
256
+ rows.push((primary_key, value));
257
+ }
258
+ return Ok(rows);
259
+ }
260
+
261
+ for (index_key, index_value) in index_rows {
262
+ if request.limit.is_some_and(|limit| rows.len() >= limit) {
263
+ break;
264
+ }
265
+ let Some(primary_key) = ByFileIndex::primary_key_from_index_key(index_key) else {
266
+ continue;
267
+ };
268
+ let value = index_value;
269
+ if tree_request.matches(&primary_key, &value) {
270
+ rows.push((primary_key, value));
271
+ }
272
+ }
273
+ Ok(rows)
274
+ }
275
+
276
+ /// Plans a three-way merge by diffing both heads against the same base.
277
+ ///
278
+ /// `target_commit_id` is the destination root that should keep its own
279
+ /// changes. `source_commit_id` is the incoming root whose non-conflicting
280
+ /// changes should be applied.
281
+ #[allow(dead_code)]
282
+ pub(crate) async fn plan_merge(
283
+ &mut self,
284
+ base_commit_id: &str,
285
+ target_commit_id: &str,
286
+ source_commit_id: &str,
287
+ request: &TrackedStateDiffRequest,
288
+ ) -> Result<TrackedStateMergePlan, LixError> {
289
+ let target_diff = self
290
+ .diff_commits(base_commit_id, target_commit_id, request)
291
+ .await?;
292
+ let source_diff = self
293
+ .diff_commits(base_commit_id, source_commit_id, request)
294
+ .await?;
295
+ merge::plan_merge(&target_diff, &source_diff)
296
+ }
297
+
298
+ #[cfg(test)]
299
+ pub(crate) async fn load_root_for_test(
300
+ &mut self,
301
+ commit_id: &str,
302
+ ) -> Result<Option<crate::tracked_state::tree_types::TrackedStateRootId>, LixError> {
303
+ self.tree.load_root(&mut self.store, commit_id).await
304
+ }
305
+ }
306
+
307
+ /// Writer for rebuildable tracked-state roots.
308
+ pub(crate) struct TrackedStateWriter {
309
+ tree: TrackedStateTree,
310
+ }
311
+
312
+ impl TrackedStateWriter {
313
+ /// Stages one root for `commit_id` from the provided row set.
314
+ ///
315
+ /// `parent_commit_id` is the tracked-state root to layer mutations on top
316
+ /// of. Rebuild passes `None` because it has already materialized the full
317
+ /// entity set for the requested head.
318
+ pub(crate) async fn stage_root(
319
+ &mut self,
320
+ store: &mut (impl StorageReader + ?Sized),
321
+ writes: &mut StorageWriteSet,
322
+ json_writer: &mut JsonStoreWriter,
323
+ commit_id: &str,
324
+ parent_commit_id: Option<&str>,
325
+ rows: &[TrackedStateRow],
326
+ ) -> Result<TrackedStateWriteReceipt, LixError> {
327
+ let base_root = match parent_commit_id {
328
+ Some(parent_commit_id) => {
329
+ let Some(root) = self.tree.load_root(store, parent_commit_id).await? else {
330
+ return Err(LixError::new(
331
+ "LIX_ERROR_UNKNOWN",
332
+ format!(
333
+ "tracked-state parent root for commit '{parent_commit_id}' is missing"
334
+ ),
335
+ ));
336
+ };
337
+ Some(root)
338
+ }
339
+ None => None,
340
+ };
341
+ let mut stored_rows = Vec::with_capacity(rows.len());
342
+ let mut mutations = Vec::with_capacity(rows.len());
343
+ for row in rows {
344
+ let stored_value =
345
+ crate::tracked_state::canonicalize_materialized_row(writes, json_writer, row)?;
346
+ mutations.push(TrackedStateMutation::put(
347
+ TrackedStateKey::from_row(row),
348
+ stored_value.clone(),
349
+ ));
350
+ stored_rows.push((row, stored_value));
351
+ }
352
+ let result = self
353
+ .tree
354
+ .apply_mutations(
355
+ store,
356
+ writes,
357
+ base_root.as_ref(),
358
+ mutations,
359
+ Some(commit_id),
360
+ )
361
+ .await?;
362
+
363
+ let by_file_base_root = match parent_commit_id {
364
+ Some(parent_commit_id) => storage::load_by_file_root(store, parent_commit_id)
365
+ .await?
366
+ .ok_or_else(|| {
367
+ LixError::new(
368
+ "LIX_ERROR_UNKNOWN",
369
+ format!(
370
+ "tracked-state by-file parent root for commit '{parent_commit_id}' is missing"
371
+ ),
372
+ )
373
+ })
374
+ .map(Some)?,
375
+ None => None,
376
+ };
377
+ let mut by_file_mutations = Vec::with_capacity(rows.len());
378
+ for (row, stored_value) in &stored_rows {
379
+ by_file_mutations.push(TrackedStateMutation::put(
380
+ ByFileIndex::key_from_row(row),
381
+ ByFileIndex::header_value_from_primary(stored_value),
382
+ ));
383
+ }
384
+ let by_file_result = self
385
+ .tree
386
+ .apply_mutations(
387
+ store,
388
+ writes,
389
+ by_file_base_root.as_ref(),
390
+ by_file_mutations,
391
+ None,
392
+ )
393
+ .await?;
394
+ storage::stage_by_file_root(writes, commit_id, &by_file_result.root_id);
395
+ Ok(TrackedStateWriteReceipt {
396
+ commit_id: commit_id.to_string(),
397
+ row_count: result.row_count,
398
+ })
399
+ }
400
+
401
+ /// Deletes the root pointer for one commit.
402
+ ///
403
+ /// This is intentionally root-scoped, not row-scoped. It is useful for
404
+ /// rebuild/corruption tests where the changelog remains authoritative and
405
+ /// the tracked-state projection must be recreated from the commit id.
406
+ #[cfg(test)]
407
+ pub(crate) fn stage_delete_root_for_rebuild(
408
+ &mut self,
409
+ writes: &mut StorageWriteSet,
410
+ commit_id: &str,
411
+ ) {
412
+ storage::stage_delete_root(writes, commit_id)
413
+ }
414
+ }
415
+
416
+ #[derive(Debug, Clone, PartialEq, Eq)]
417
+ pub(crate) struct TrackedStateWriteReceipt {
418
+ pub(crate) commit_id: String,
419
+ pub(crate) row_count: usize,
420
+ }
421
+
422
+ fn tree_scan_request_from_tracked(
423
+ request: &TrackedStateScanRequest,
424
+ ) -> TrackedStateTreeScanRequest {
425
+ TrackedStateTreeScanRequest {
426
+ schema_keys: request.filter.schema_keys.clone(),
427
+ entity_ids: request.filter.entity_ids.clone(),
428
+ file_ids: request.filter.file_ids.clone(),
429
+ include_tombstones: request.filter.include_tombstones,
430
+ limit: request.limit,
431
+ }
432
+ }
433
+
434
+ fn scan_needs_json_payloads(request: &TrackedStateScanRequest) -> bool {
435
+ if request.projection.columns.is_empty() {
436
+ return true;
437
+ }
438
+ request
439
+ .projection
440
+ .columns
441
+ .iter()
442
+ .any(|column| column == "snapshot_content" || column == "metadata")
443
+ }
444
+
445
+ fn tracked_key_from_request(request: &TrackedStateRowRequest) -> Result<TrackedStateKey, LixError> {
446
+ let file_id = match &request.file_id {
447
+ crate::NullableKeyFilter::Null => None,
448
+ crate::NullableKeyFilter::Value(value) => Some(value.clone()),
449
+ crate::NullableKeyFilter::Any => {
450
+ return Err(LixError::new(
451
+ "LIX_ERROR_UNKNOWN",
452
+ "tracked-state tree exact lookup requires a concrete file_id filter",
453
+ ))
454
+ }
455
+ };
456
+ Ok(TrackedStateKey {
457
+ schema_key: request.schema_key.clone(),
458
+ file_id,
459
+ entity_id: request.entity_id.clone(),
460
+ })
461
+ }
462
+
463
+ #[cfg(test)]
464
+ mod tests {
465
+ use std::sync::Arc;
466
+
467
+ use super::*;
468
+ use crate::backend::{testing::UnitTestBackend, Backend};
469
+ use crate::storage::{StorageContext, StorageWriteSet, StorageWriteTransaction};
470
+ use crate::NullableKeyFilter;
471
+
472
+ #[tokio::test]
473
+ async fn write_root_rejects_missing_parent_root() {
474
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
475
+ let storage = StorageContext::new(Arc::clone(&backend));
476
+ let tracked_state = TrackedStateContext::new();
477
+ let mut transaction = storage
478
+ .begin_write_transaction()
479
+ .await
480
+ .expect("transaction should open");
481
+
482
+ let error = write_root_for_test(
483
+ transaction.as_mut(),
484
+ &tracked_state,
485
+ "commit-child",
486
+ Some("missing-parent"),
487
+ &[row("entity-child", "change-child", "commit-child")],
488
+ )
489
+ .await
490
+ .expect_err("parent root must exist when parent_commit_id is provided");
491
+
492
+ assert!(
493
+ error.message.contains("parent root") && error.message.contains("missing-parent"),
494
+ "unexpected error: {error:?}"
495
+ );
496
+ }
497
+
498
+ #[tokio::test]
499
+ async fn plan_merge_from_roots_applies_source_only_change() {
500
+ let (storage, tracked_state) = seed_merge_roots(
501
+ &[row_with_value("entity-a", "change-base", "base", "base")],
502
+ &[row_with_value("entity-a", "change-base", "base", "base")],
503
+ &[row_with_value(
504
+ "entity-a",
505
+ "change-source",
506
+ "source",
507
+ "source",
508
+ )],
509
+ )
510
+ .await;
511
+
512
+ let plan = tracked_state
513
+ .reader(storage.clone())
514
+ .plan_merge(
515
+ "base",
516
+ "target",
517
+ "source",
518
+ &TrackedStateDiffRequest::default(),
519
+ )
520
+ .await
521
+ .expect("merge should plan");
522
+
523
+ assert_eq!(merge_patch_ids(&plan), vec!["entity-a"]);
524
+ assert!(plan.conflicts.is_empty());
525
+ }
526
+
527
+ #[tokio::test]
528
+ async fn plan_merge_from_roots_keeps_target_only_change() {
529
+ let (storage, tracked_state) = seed_merge_roots(
530
+ &[row("entity-a", "change-base", "base")],
531
+ &[row("entity-a", "change-target", "target")],
532
+ &[row("entity-a", "change-base", "base")],
533
+ )
534
+ .await;
535
+
536
+ let plan = tracked_state
537
+ .reader(storage.clone())
538
+ .plan_merge(
539
+ "base",
540
+ "target",
541
+ "source",
542
+ &TrackedStateDiffRequest::default(),
543
+ )
544
+ .await
545
+ .expect("merge should plan");
546
+
547
+ assert!(plan.patches.is_empty());
548
+ assert!(plan.conflicts.is_empty());
549
+ }
550
+
551
+ #[tokio::test]
552
+ async fn plan_merge_from_roots_reports_divergent_modification_conflict() {
553
+ let (storage, tracked_state) = seed_merge_roots(
554
+ &[row_with_value("entity-a", "change-base", "base", "base")],
555
+ &[row_with_value(
556
+ "entity-a",
557
+ "change-target",
558
+ "target",
559
+ "target",
560
+ )],
561
+ &[row_with_value(
562
+ "entity-a",
563
+ "change-source",
564
+ "source",
565
+ "source",
566
+ )],
567
+ )
568
+ .await;
569
+
570
+ let plan = tracked_state
571
+ .reader(storage.clone())
572
+ .plan_merge(
573
+ "base",
574
+ "target",
575
+ "source",
576
+ &TrackedStateDiffRequest::default(),
577
+ )
578
+ .await
579
+ .expect("merge should plan");
580
+
581
+ assert!(plan.patches.is_empty());
582
+ assert_eq!(merge_conflict_ids(&plan), vec!["entity-a"]);
583
+ }
584
+
585
+ #[tokio::test]
586
+ async fn plan_merge_from_roots_applies_source_tombstone() {
587
+ let (storage, tracked_state) = seed_merge_roots(
588
+ &[row("entity-a", "change-base", "base")],
589
+ &[row("entity-a", "change-base", "base")],
590
+ &[tombstone("entity-a", "change-source-delete", "source")],
591
+ )
592
+ .await;
593
+
594
+ let plan = tracked_state
595
+ .reader(storage.clone())
596
+ .plan_merge(
597
+ "base",
598
+ "target",
599
+ "source",
600
+ &TrackedStateDiffRequest::default(),
601
+ )
602
+ .await
603
+ .expect("merge should plan");
604
+
605
+ assert_eq!(merge_patch_ids(&plan), vec!["entity-a"]);
606
+ assert_eq!(plan.patches[0].projected_row().snapshot_content, None);
607
+ assert_eq!(plan.patches[0].change_id(), "change-source-delete");
608
+ }
609
+
610
+ #[tokio::test]
611
+ async fn scan_rows_by_file_uses_file_index_shape() {
612
+ let backend = Arc::new(UnitTestBackend::new());
613
+ let storage = StorageContext::new(backend.clone());
614
+ let tracked_state = TrackedStateContext::new();
615
+ let mut file_a = row("entity-a", "change-a", "commit-1");
616
+ file_a.file_id = Some("file-a.json".to_string());
617
+ let mut file_b = row("entity-b", "change-b", "commit-1");
618
+ file_b.file_id = Some("file-b.json".to_string());
619
+
620
+ let mut transaction = storage
621
+ .begin_write_transaction()
622
+ .await
623
+ .expect("transaction should open");
624
+ write_root_for_test(
625
+ transaction.as_mut(),
626
+ &tracked_state,
627
+ "commit-1",
628
+ None,
629
+ &[file_a, file_b],
630
+ )
631
+ .await
632
+ .expect("root should write");
633
+ transaction
634
+ .commit()
635
+ .await
636
+ .expect("transaction should commit");
637
+
638
+ let rows = tracked_state
639
+ .reader(storage.clone())
640
+ .scan_rows_at_commit(
641
+ "commit-1",
642
+ &TrackedStateScanRequest {
643
+ filter: crate::tracked_state::TrackedStateFilter {
644
+ file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
645
+ ..Default::default()
646
+ },
647
+ ..Default::default()
648
+ },
649
+ )
650
+ .await
651
+ .expect("file scan should read through index");
652
+
653
+ assert_eq!(rows.len(), 1);
654
+ assert_eq!(
655
+ rows[0].entity_id.as_string().expect("entity id"),
656
+ "entity-a"
657
+ );
658
+ assert_eq!(rows[0].file_id.as_deref(), Some("file-a.json"));
659
+ }
660
+
661
+ #[tokio::test]
662
+ async fn by_file_header_index_fetches_primary_payload_only_when_requested() {
663
+ let backend = Arc::new(UnitTestBackend::new());
664
+ let storage = StorageContext::new(backend.clone());
665
+ let tracked_state = TrackedStateContext::new();
666
+ let mut row = row("entity-a", "change-a", "commit-1");
667
+ row.file_id = Some("file-a.json".to_string());
668
+ let expected_snapshot = row.snapshot_content.clone();
669
+
670
+ let mut transaction = storage
671
+ .begin_write_transaction()
672
+ .await
673
+ .expect("transaction should open");
674
+ write_root_for_test(
675
+ transaction.as_mut(),
676
+ &tracked_state,
677
+ "commit-1",
678
+ None,
679
+ std::slice::from_ref(&row),
680
+ )
681
+ .await
682
+ .expect("root should write");
683
+ transaction
684
+ .commit()
685
+ .await
686
+ .expect("transaction should commit");
687
+
688
+ let mut reader = tracked_state.reader(storage.clone());
689
+ let header_rows = reader
690
+ .scan_rows_at_commit(
691
+ "commit-1",
692
+ &TrackedStateScanRequest {
693
+ filter: crate::tracked_state::TrackedStateFilter {
694
+ file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
695
+ ..Default::default()
696
+ },
697
+ projection: crate::tracked_state::TrackedStateProjection {
698
+ columns: vec!["entity_id".to_string()],
699
+ },
700
+ ..Default::default()
701
+ },
702
+ )
703
+ .await
704
+ .expect("header scan should read through by-file index");
705
+ let full_rows = reader
706
+ .scan_rows_at_commit(
707
+ "commit-1",
708
+ &TrackedStateScanRequest {
709
+ filter: crate::tracked_state::TrackedStateFilter {
710
+ file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
711
+ ..Default::default()
712
+ },
713
+ ..Default::default()
714
+ },
715
+ )
716
+ .await
717
+ .expect("full scan should fetch primary payload");
718
+
719
+ assert_eq!(header_rows[0].snapshot_content, None);
720
+ assert_eq!(full_rows[0].snapshot_content, expected_snapshot);
721
+ }
722
+
723
+ #[tokio::test]
724
+ async fn by_file_header_index_filters_tombstones_without_payload_sentinel() {
725
+ let backend = Arc::new(UnitTestBackend::new());
726
+ let storage = StorageContext::new(backend.clone());
727
+ let tracked_state = TrackedStateContext::new();
728
+ let mut live = row("entity-live", "change-live", "commit-1");
729
+ live.file_id = Some("file-a.json".to_string());
730
+ let mut deleted = tombstone("entity-deleted", "change-delete", "commit-1");
731
+ deleted.file_id = Some("file-a.json".to_string());
732
+
733
+ let mut transaction = storage
734
+ .begin_write_transaction()
735
+ .await
736
+ .expect("transaction should open");
737
+ write_root_for_test(
738
+ transaction.as_mut(),
739
+ &tracked_state,
740
+ "commit-1",
741
+ None,
742
+ &[live, deleted],
743
+ )
744
+ .await
745
+ .expect("root should write");
746
+ transaction
747
+ .commit()
748
+ .await
749
+ .expect("transaction should commit");
750
+
751
+ let rows = tracked_state
752
+ .reader(storage.clone())
753
+ .scan_rows_at_commit(
754
+ "commit-1",
755
+ &TrackedStateScanRequest {
756
+ filter: crate::tracked_state::TrackedStateFilter {
757
+ file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
758
+ ..Default::default()
759
+ },
760
+ projection: crate::tracked_state::TrackedStateProjection {
761
+ columns: vec!["entity_id".to_string()],
762
+ },
763
+ ..Default::default()
764
+ },
765
+ )
766
+ .await
767
+ .expect("file scan should read through index");
768
+
769
+ assert_eq!(rows.len(), 1);
770
+ assert_eq!(
771
+ rows[0].entity_id.as_string().expect("entity id"),
772
+ "entity-live"
773
+ );
774
+ }
775
+
776
+ #[tokio::test]
777
+ async fn reads_resolve_json_snapshot_refs() {
778
+ let backend = Arc::new(UnitTestBackend::new());
779
+ let storage = StorageContext::new(backend.clone());
780
+ let tracked_state = TrackedStateContext::new();
781
+ let large_value = "x".repeat(1536);
782
+ let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
783
+
784
+ let mut transaction = storage
785
+ .begin_write_transaction()
786
+ .await
787
+ .expect("transaction should open");
788
+ write_root_for_test(
789
+ transaction.as_mut(),
790
+ &tracked_state,
791
+ "commit-1",
792
+ None,
793
+ std::slice::from_ref(&row),
794
+ )
795
+ .await
796
+ .expect("root should write");
797
+ transaction
798
+ .commit()
799
+ .await
800
+ .expect("transaction should commit");
801
+
802
+ let mut reader = tracked_state.reader(storage.clone());
803
+ let loaded = reader
804
+ .load_row_at_commit(
805
+ "commit-1",
806
+ &TrackedStateRowRequest {
807
+ schema_key: row.schema_key.clone(),
808
+ entity_id: row.entity_id.clone(),
809
+ file_id: NullableKeyFilter::Null,
810
+ },
811
+ )
812
+ .await
813
+ .expect("row should load")
814
+ .expect("row should exist");
815
+ let scanned = reader
816
+ .scan_rows_at_commit("commit-1", &TrackedStateScanRequest::default())
817
+ .await
818
+ .expect("rows should scan");
819
+
820
+ assert_eq!(loaded.snapshot_content, row.snapshot_content);
821
+ assert_eq!(scanned[0].snapshot_content, row.snapshot_content);
822
+ }
823
+
824
+ #[tokio::test]
825
+ async fn projected_scans_do_not_materialize_snapshot_when_snapshot_content_is_omitted() {
826
+ let backend = Arc::new(UnitTestBackend::new());
827
+ let storage = StorageContext::new(backend.clone());
828
+ let tracked_state = TrackedStateContext::new();
829
+ let large_value = "x".repeat(1536);
830
+ let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
831
+
832
+ let mut transaction = storage
833
+ .begin_write_transaction()
834
+ .await
835
+ .expect("transaction should open");
836
+ write_root_for_test(
837
+ transaction.as_mut(),
838
+ &tracked_state,
839
+ "commit-1",
840
+ None,
841
+ std::slice::from_ref(&row),
842
+ )
843
+ .await
844
+ .expect("root should write");
845
+ transaction
846
+ .commit()
847
+ .await
848
+ .expect("transaction should commit");
849
+
850
+ let rows = tracked_state
851
+ .reader(storage.clone())
852
+ .scan_rows_at_commit(
853
+ "commit-1",
854
+ &TrackedStateScanRequest {
855
+ projection: crate::tracked_state::TrackedStateProjection {
856
+ columns: vec!["entity_id".to_string()],
857
+ },
858
+ ..Default::default()
859
+ },
860
+ )
861
+ .await
862
+ .expect("rows should scan");
863
+
864
+ assert_eq!(rows.len(), 1);
865
+ assert_eq!(rows[0].snapshot_content, None);
866
+ }
867
+
868
+ async fn seed_merge_roots(
869
+ base_rows: &[TrackedStateRow],
870
+ target_rows: &[TrackedStateRow],
871
+ source_rows: &[TrackedStateRow],
872
+ ) -> (StorageContext, TrackedStateContext) {
873
+ let backend = Arc::new(UnitTestBackend::new());
874
+ let storage = StorageContext::new(backend.clone());
875
+ let tracked_state = TrackedStateContext::new();
876
+ let mut transaction = storage
877
+ .begin_write_transaction()
878
+ .await
879
+ .expect("transaction should open");
880
+ write_root_for_test(
881
+ transaction.as_mut(),
882
+ &tracked_state,
883
+ "base",
884
+ None,
885
+ base_rows,
886
+ )
887
+ .await
888
+ .expect("base root should write");
889
+ write_root_for_test(
890
+ transaction.as_mut(),
891
+ &tracked_state,
892
+ "target",
893
+ None,
894
+ target_rows,
895
+ )
896
+ .await
897
+ .expect("target root should write");
898
+ write_root_for_test(
899
+ transaction.as_mut(),
900
+ &tracked_state,
901
+ "source",
902
+ None,
903
+ source_rows,
904
+ )
905
+ .await
906
+ .expect("source root should write");
907
+ transaction
908
+ .commit()
909
+ .await
910
+ .expect("transaction should commit");
911
+ (storage, tracked_state)
912
+ }
913
+
914
+ fn merge_patch_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
915
+ plan.patches
916
+ .iter()
917
+ .map(|entry| entry.identity().entity_id.as_string().expect("identity"))
918
+ .collect()
919
+ }
920
+
921
+ fn merge_conflict_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
922
+ plan.conflicts
923
+ .iter()
924
+ .map(|entry| entry.identity.entity_id.as_string().expect("identity"))
925
+ .collect()
926
+ }
927
+
928
+ async fn write_root_for_test(
929
+ transaction: &mut dyn StorageWriteTransaction,
930
+ tracked_state: &TrackedStateContext,
931
+ commit_id: &str,
932
+ parent_commit_id: Option<&str>,
933
+ rows: &[TrackedStateRow],
934
+ ) -> Result<TrackedStateWriteReceipt, LixError> {
935
+ let mut writes = StorageWriteSet::new();
936
+ let receipt = {
937
+ let mut json_writer = JsonStoreContext::new().writer();
938
+ tracked_state
939
+ .writer()
940
+ .stage_root(
941
+ transaction,
942
+ &mut writes,
943
+ &mut json_writer,
944
+ commit_id,
945
+ parent_commit_id,
946
+ rows,
947
+ )
948
+ .await?
949
+ };
950
+ writes.apply(transaction).await?;
951
+ Ok(receipt)
952
+ }
953
+
954
+ fn tombstone(entity_id: &str, change_id: &str, commit_id: &str) -> TrackedStateRow {
955
+ let mut row = row(entity_id, change_id, commit_id);
956
+ row.snapshot_content = None;
957
+ row
958
+ }
959
+
960
+ fn row(entity_id: &str, change_id: &str, commit_id: &str) -> TrackedStateRow {
961
+ row_with_value(entity_id, change_id, commit_id, "value")
962
+ }
963
+
964
+ fn row_with_value(
965
+ entity_id: &str,
966
+ change_id: &str,
967
+ commit_id: &str,
968
+ value: &str,
969
+ ) -> TrackedStateRow {
970
+ TrackedStateRow {
971
+ entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
972
+ schema_key: "test_schema".to_string(),
973
+ file_id: None,
974
+ snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
975
+ metadata: None,
976
+ schema_version: "1".to_string(),
977
+ created_at: "2026-01-01T00:00:00Z".to_string(),
978
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
979
+ change_id: change_id.to_string(),
980
+ commit_id: commit_id.to_string(),
981
+ }
982
+ }
983
+ }