@lix-js/sdk 0.6.0-preview.5 → 0.6.1

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 (274) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +38 -207
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +38 -71
  16. package/SKILL.md +0 -507
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -833
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -359
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/capabilities.rs +0 -67
  31. package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
  32. package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
  33. package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
  34. package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
  35. package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
  36. package/dist-engine-src/src/backend/conformance/model.rs +0 -28
  37. package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
  38. package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
  39. package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
  40. package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
  41. package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
  42. package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
  43. package/dist-engine-src/src/backend/conformance/write.rs +0 -16
  44. package/dist-engine-src/src/backend/error.rs +0 -94
  45. package/dist-engine-src/src/backend/in_memory.rs +0 -670
  46. package/dist-engine-src/src/backend/mod.rs +0 -39
  47. package/dist-engine-src/src/backend/predicate.rs +0 -80
  48. package/dist-engine-src/src/backend/traits.rs +0 -260
  49. package/dist-engine-src/src/backend/types.rs +0 -239
  50. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  51. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  52. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  53. package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
  54. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  55. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  56. package/dist-engine-src/src/branch/context.rs +0 -40
  57. package/dist-engine-src/src/branch/lifecycle.rs +0 -221
  58. package/dist-engine-src/src/branch/mod.rs +0 -13
  59. package/dist-engine-src/src/branch/refs.rs +0 -321
  60. package/dist-engine-src/src/branch/stage_rows.rs +0 -67
  61. package/dist-engine-src/src/branch/types.rs +0 -21
  62. package/dist-engine-src/src/catalog/context.rs +0 -412
  63. package/dist-engine-src/src/catalog/mod.rs +0 -10
  64. package/dist-engine-src/src/catalog/schema.rs +0 -4
  65. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  66. package/dist-engine-src/src/cel/context.rs +0 -86
  67. package/dist-engine-src/src/cel/error.rs +0 -19
  68. package/dist-engine-src/src/cel/mod.rs +0 -8
  69. package/dist-engine-src/src/cel/provider.rs +0 -9
  70. package/dist-engine-src/src/cel/runtime.rs +0 -167
  71. package/dist-engine-src/src/cel/value.rs +0 -50
  72. package/dist-engine-src/src/changelog/bench_support.rs +0 -785
  73. package/dist-engine-src/src/changelog/change.rs +0 -1
  74. package/dist-engine-src/src/changelog/codec.rs +0 -497
  75. package/dist-engine-src/src/changelog/commit.rs +0 -1
  76. package/dist-engine-src/src/changelog/context.rs +0 -1614
  77. package/dist-engine-src/src/changelog/mod.rs +0 -29
  78. package/dist-engine-src/src/changelog/store.rs +0 -163
  79. package/dist-engine-src/src/changelog/test_support.rs +0 -54
  80. package/dist-engine-src/src/changelog/types.rs +0 -213
  81. package/dist-engine-src/src/commit_graph/context.rs +0 -944
  82. package/dist-engine-src/src/commit_graph/mod.rs +0 -9
  83. package/dist-engine-src/src/commit_graph/types.rs +0 -89
  84. package/dist-engine-src/src/commit_graph/walker.rs +0 -786
  85. package/dist-engine-src/src/common/error.rs +0 -347
  86. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  87. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  88. package/dist-engine-src/src/common/identity.rs +0 -145
  89. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  90. package/dist-engine-src/src/common/metadata.rs +0 -40
  91. package/dist-engine-src/src/common/mod.rs +0 -23
  92. package/dist-engine-src/src/common/types.rs +0 -105
  93. package/dist-engine-src/src/common/wire.rs +0 -222
  94. package/dist-engine-src/src/domain.rs +0 -320
  95. package/dist-engine-src/src/engine.rs +0 -203
  96. package/dist-engine-src/src/entity_pk.rs +0 -402
  97. package/dist-engine-src/src/functions/context.rs +0 -296
  98. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  99. package/dist-engine-src/src/functions/mod.rs +0 -18
  100. package/dist-engine-src/src/functions/provider.rs +0 -130
  101. package/dist-engine-src/src/functions/state.rs +0 -335
  102. package/dist-engine-src/src/functions/types.rs +0 -37
  103. package/dist-engine-src/src/init.rs +0 -692
  104. package/dist-engine-src/src/json_store/compression.rs +0 -77
  105. package/dist-engine-src/src/json_store/context.rs +0 -172
  106. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  107. package/dist-engine-src/src/json_store/mod.rs +0 -38
  108. package/dist-engine-src/src/json_store/store.rs +0 -494
  109. package/dist-engine-src/src/json_store/types.rs +0 -212
  110. package/dist-engine-src/src/lib.rs +0 -92
  111. package/dist-engine-src/src/live_state/context.rs +0 -1883
  112. package/dist-engine-src/src/live_state/mod.rs +0 -21
  113. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  114. package/dist-engine-src/src/live_state/reader.rs +0 -23
  115. package/dist-engine-src/src/live_state/types.rs +0 -231
  116. package/dist-engine-src/src/live_state/visibility.rs +0 -666
  117. package/dist-engine-src/src/plugin/archive.rs +0 -438
  118. package/dist-engine-src/src/plugin/component.rs +0 -183
  119. package/dist-engine-src/src/plugin/install.rs +0 -619
  120. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  121. package/dist-engine-src/src/plugin/materializer.rs +0 -202
  122. package/dist-engine-src/src/plugin/mod.rs +0 -33
  123. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
  124. package/dist-engine-src/src/plugin/storage.rs +0 -74
  125. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  126. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  127. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  128. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  129. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  130. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
  131. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
  132. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  133. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  134. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  135. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  136. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  137. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  138. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  139. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  140. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  141. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  142. package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
  143. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  144. package/dist-engine-src/src/schema/definition.json +0 -187
  145. package/dist-engine-src/src/schema/definition.rs +0 -742
  146. package/dist-engine-src/src/schema/key.rs +0 -138
  147. package/dist-engine-src/src/schema/mod.rs +0 -20
  148. package/dist-engine-src/src/schema/seed.rs +0 -14
  149. package/dist-engine-src/src/schema/tests.rs +0 -780
  150. package/dist-engine-src/src/session/context.rs +0 -1059
  151. package/dist-engine-src/src/session/create_branch.rs +0 -94
  152. package/dist-engine-src/src/session/execute.rs +0 -681
  153. package/dist-engine-src/src/session/merge/analysis.rs +0 -108
  154. package/dist-engine-src/src/session/merge/branch.rs +0 -417
  155. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  156. package/dist-engine-src/src/session/merge/mod.rs +0 -10
  157. package/dist-engine-src/src/session/merge/stats.rs +0 -61
  158. package/dist-engine-src/src/session/mod.rs +0 -30
  159. package/dist-engine-src/src/session/switch_branch.rs +0 -113
  160. package/dist-engine-src/src/session/transaction.rs +0 -557
  161. package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
  162. package/dist-engine-src/src/sql2/bind/error.rs +0 -5
  163. package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
  164. package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
  165. package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
  166. package/dist-engine-src/src/sql2/bind/read.rs +0 -65
  167. package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
  168. package/dist-engine-src/src/sql2/bind/table.rs +0 -273
  169. package/dist-engine-src/src/sql2/bind/write.rs +0 -86
  170. package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
  171. package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
  172. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
  173. package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
  174. package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
  175. package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
  176. package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
  177. package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
  178. package/dist-engine-src/src/sql2/context.rs +0 -317
  179. package/dist-engine-src/src/sql2/dml.rs +0 -148
  180. package/dist-engine-src/src/sql2/error.rs +0 -215
  181. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
  182. package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
  183. package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
  184. package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
  185. package/dist-engine-src/src/sql2/exec/write.rs +0 -661
  186. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
  187. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  188. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  189. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  190. package/dist-engine-src/src/sql2/history_route.rs +0 -661
  191. package/dist-engine-src/src/sql2/mod.rs +0 -52
  192. package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
  193. package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
  194. package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
  195. package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
  196. package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
  197. package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
  198. package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
  199. package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
  200. package/dist-engine-src/src/sql2/plan/write.rs +0 -147
  201. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
  202. package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
  203. package/dist-engine-src/src/sql2/providers/change.rs +0 -445
  204. package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
  205. package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
  206. package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
  207. package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
  208. package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
  209. package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
  210. package/dist-engine-src/src/sql2/providers/history.rs +0 -426
  211. package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
  212. package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
  213. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  214. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  215. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  216. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  217. package/dist-engine-src/src/sql2/session.rs +0 -83
  218. package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
  219. package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
  220. package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
  221. package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
  222. package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
  223. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  224. package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
  225. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  226. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  227. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  228. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  229. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  230. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  231. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  232. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  233. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
  234. package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
  235. package/dist-engine-src/src/storage/conformance.rs +0 -399
  236. package/dist-engine-src/src/storage/context.rs +0 -620
  237. package/dist-engine-src/src/storage/mod.rs +0 -52
  238. package/dist-engine-src/src/storage/point.rs +0 -440
  239. package/dist-engine-src/src/storage/read_scope.rs +0 -67
  240. package/dist-engine-src/src/storage/reader.rs +0 -867
  241. package/dist-engine-src/src/storage/scan.rs +0 -784
  242. package/dist-engine-src/src/storage/spaces.rs +0 -236
  243. package/dist-engine-src/src/storage/stats.rs +0 -80
  244. package/dist-engine-src/src/storage/write_set.rs +0 -962
  245. package/dist-engine-src/src/storage_bench.rs +0 -171
  246. package/dist-engine-src/src/test_support.rs +0 -450
  247. package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
  248. package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
  249. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
  250. package/dist-engine-src/src/tracked_state/context.rs +0 -2801
  251. package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
  252. package/dist-engine-src/src/tracked_state/merge.rs +0 -478
  253. package/dist-engine-src/src/tracked_state/mod.rs +0 -35
  254. package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
  255. package/dist-engine-src/src/tracked_state/storage.rs +0 -427
  256. package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
  257. package/dist-engine-src/src/tracked_state/types.rs +0 -238
  258. package/dist-engine-src/src/transaction/bench_support.rs +0 -407
  259. package/dist-engine-src/src/transaction/commit.rs +0 -1592
  260. package/dist-engine-src/src/transaction/context.rs +0 -1653
  261. package/dist-engine-src/src/transaction/mod.rs +0 -24
  262. package/dist-engine-src/src/transaction/normalization.rs +0 -877
  263. package/dist-engine-src/src/transaction/prep.rs +0 -37
  264. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
  265. package/dist-engine-src/src/transaction/staging.rs +0 -1525
  266. package/dist-engine-src/src/transaction/types.rs +0 -403
  267. package/dist-engine-src/src/transaction/validation.rs +0 -5766
  268. package/dist-engine-src/src/untracked_state/codec.rs +0 -615
  269. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  270. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  271. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  272. package/dist-engine-src/src/untracked_state/storage.rs +0 -898
  273. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  274. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,2801 +0,0 @@
1
- use std::collections::{BTreeMap, HashMap, HashSet};
2
-
3
- use crate::changelog::{
4
- ChangeLoadRequest, ChangeRecord, ChangelogContext, ChangelogReader, CommitLoadEntry,
5
- CommitLoadRequest, CommitProjection, CommitRecord,
6
- };
7
- use crate::entity_pk::EntityPk;
8
- use crate::storage::{StorageRead, StorageWriteSet};
9
- use crate::tracked_state::codec::{encode_key_ref, encode_value_ref};
10
- use crate::tracked_state::diff::{
11
- diff_commits, diff_commits_with_validation, TrackedStateDiff, TrackedStateDiffRequest,
12
- TrackedStateDiffRow,
13
- };
14
- use crate::tracked_state::materialize_rows_from_index_entries;
15
- #[cfg(test)]
16
- use crate::tracked_state::merge::{self, TrackedStateMergePlan};
17
- use crate::tracked_state::storage;
18
- use crate::tracked_state::tree::TrackedStateTree;
19
- use crate::tracked_state::types::{
20
- TrackedStateCommitRoot, TrackedStateCommitRootParent, TrackedStateIndexValue, TrackedStateKey,
21
- TrackedStateKeyRef, TrackedStateMutation, TrackedStateRootId, TrackedStateTreeScanRequest,
22
- };
23
- use crate::tracked_state::{
24
- MaterializedTrackedStateRow, TrackedStateDeltaRef, TrackedStateScanRequest,
25
- };
26
- use crate::{LixError, NullableKeyFilter};
27
-
28
- #[derive(Debug, Clone, PartialEq, Eq, Hash)]
29
- struct TrackedStateIdentity {
30
- schema_key: String,
31
- file_id: Option<String>,
32
- entity_pk: EntityPk,
33
- }
34
-
35
- /// Factory for tracked-state readers, root writers, and commit-root rebuilders.
36
- ///
37
- /// Tracked state is stored as content-addressed roots. Branch refs
38
- /// choose which commit/root to read; this context only owns root operations.
39
- #[derive(Clone)]
40
- pub(crate) struct TrackedStateContext {
41
- tree: TrackedStateTree,
42
- }
43
-
44
- impl TrackedStateContext {
45
- pub(crate) fn new() -> Self {
46
- Self {
47
- tree: TrackedStateTree::new(),
48
- }
49
- }
50
-
51
- /// Creates a commit-id-addressed tracked-state reader.
52
- pub(crate) fn reader<S>(&self, store: S) -> TrackedStateStoreReader<S>
53
- where
54
- S: StorageRead + Send + Sync,
55
- {
56
- TrackedStateStoreReader {
57
- store,
58
- tree: self.tree.clone(),
59
- }
60
- }
61
-
62
- /// Creates a tracked-state writer over a caller-owned transaction and write set.
63
- pub(crate) fn writer<'a, S>(
64
- &'a self,
65
- store: &'a S,
66
- writes: &'a mut StorageWriteSet,
67
- ) -> TrackedStateWriter<'a, S>
68
- where
69
- S: StorageRead + Send + Sync + ?Sized,
70
- {
71
- TrackedStateWriter {
72
- chunk_overlay: storage::TrackedStateChunkOverlay::new(),
73
- staged_roots: BTreeMap::new(),
74
- tree: self.tree.clone(),
75
- store,
76
- writes,
77
- }
78
- }
79
-
80
- /// Creates an explicit tracked-state commit-root rebuilder.
81
- ///
82
- /// Normal commits stage commit roots directly. This rebuilder reconstructs
83
- /// a missing root from changelog facts as an explicit maintenance path.
84
- pub(crate) fn root_rebuilder<'a, S>(
85
- &'a self,
86
- store: &'a S,
87
- writes: &'a mut StorageWriteSet,
88
- ) -> TrackedStateRootRebuilder<'a, S>
89
- where
90
- S: StorageRead + Send + Sync + ?Sized,
91
- {
92
- let _ = self;
93
- TrackedStateRootRebuilder { store, writes }
94
- }
95
- }
96
-
97
- /// Store-backed tracked-state reader created by `TrackedStateContext`.
98
- pub(crate) struct TrackedStateStoreReader<S> {
99
- store: S,
100
- tree: TrackedStateTree,
101
- }
102
-
103
- struct DiffCommitRootValidationCache {
104
- commit_ref_winners: HashMap<String, HashMap<TrackedStateIdentity, String>>,
105
- commit_root_metadata: HashMap<String, TrackedStateCommitRoot>,
106
- commit_roots: HashMap<String, TrackedStateRootId>,
107
- tree_values: HashMap<(TrackedStateRootId, TrackedStateKey), Option<TrackedStateIndexValue>>,
108
- changelog_first_parents: HashMap<String, Option<String>>,
109
- }
110
-
111
- impl DiffCommitRootValidationCache {
112
- fn new() -> Self {
113
- Self {
114
- commit_ref_winners: HashMap::new(),
115
- commit_root_metadata: HashMap::new(),
116
- commit_roots: HashMap::new(),
117
- tree_values: HashMap::new(),
118
- changelog_first_parents: HashMap::new(),
119
- }
120
- }
121
- }
122
-
123
- impl<S> TrackedStateStoreReader<S>
124
- where
125
- S: StorageRead + Send + Sync,
126
- {
127
- pub(crate) async fn scan_rows_at_commit(
128
- &mut self,
129
- commit_id: &str,
130
- request: &TrackedStateScanRequest,
131
- ) -> Result<Vec<MaterializedTrackedStateRow>, LixError> {
132
- let Some(root_id) = self.tree.load_root(&mut self.store, commit_id).await? else {
133
- return Err(missing_commit_root_error(commit_id));
134
- };
135
- let rows = self
136
- .tree
137
- .scan(
138
- &mut self.store,
139
- &root_id,
140
- &tree_scan_request_from_tracked(request),
141
- )
142
- .await?;
143
- let materialization = crate::tracked_state::TrackedRowMaterialization::from_columns(
144
- &request.read_columns.columns,
145
- );
146
- let mut rows =
147
- materialize_rows_from_index_entries(&mut self.store, rows, &materialization).await?;
148
- if !request.filter.include_tombstones {
149
- rows.retain(|row| !row.deleted);
150
- }
151
- if let Some(limit) = request.limit {
152
- rows.truncate(limit);
153
- }
154
- Ok(rows)
155
- }
156
-
157
- #[cfg(any(test, feature = "storage-benches"))]
158
- pub(crate) async fn load_rows_at_commit(
159
- &mut self,
160
- commit_id: &str,
161
- keys: &[TrackedStateKey],
162
- ) -> Result<Vec<Option<MaterializedTrackedStateRow>>, LixError> {
163
- if keys.is_empty() {
164
- return Ok(Vec::new());
165
- }
166
- let values = self.commit_root_values_for_keys(commit_id, &keys).await?;
167
- let mut entry_indices = Vec::new();
168
- let mut entries = Vec::new();
169
- for (index, (key, value)) in keys.iter().cloned().zip(values).enumerate() {
170
- if let Some(value) = value {
171
- entry_indices.push(index);
172
- entries.push((key, value));
173
- }
174
- }
175
- let materialized = materialize_rows_from_index_entries(
176
- &mut self.store,
177
- entries,
178
- &crate::tracked_state::TrackedRowMaterialization::full(),
179
- )
180
- .await?;
181
- let mut rows = vec![None; keys.len()];
182
- for (index, row) in entry_indices.into_iter().zip(materialized) {
183
- rows[index] = Some(row);
184
- }
185
- Ok(rows)
186
- }
187
-
188
- pub(crate) async fn diff_commits(
189
- &mut self,
190
- left_commit_id: &str,
191
- right_commit_id: &str,
192
- request: &TrackedStateDiffRequest,
193
- ) -> Result<TrackedStateDiff, LixError> {
194
- diff_commits(self, left_commit_id, right_commit_id, request).await
195
- }
196
-
197
- pub(crate) async fn diff_commits_with_validation(
198
- &mut self,
199
- left_commit_id: &str,
200
- right_commit_id: &str,
201
- request: &TrackedStateDiffRequest,
202
- validate_left_root: bool,
203
- validate_right_root: bool,
204
- ) -> Result<TrackedStateDiff, LixError> {
205
- diff_commits_with_validation(
206
- self,
207
- left_commit_id,
208
- right_commit_id,
209
- request,
210
- validate_left_root,
211
- validate_right_root,
212
- )
213
- .await
214
- }
215
-
216
- pub(crate) async fn validate_diff_rows_for_commits_against_changelog(
217
- &mut self,
218
- rows: &[(&TrackedStateDiffRow, &str)],
219
- ) -> Result<(), LixError> {
220
- if rows.is_empty() {
221
- return Ok(());
222
- }
223
-
224
- let mut change_ids = rows
225
- .iter()
226
- .filter(|(row, _)| row.schema_key != "lix_commit")
227
- .map(|(row, _)| row.change_id.clone())
228
- .collect::<Vec<_>>();
229
- change_ids.sort();
230
- change_ids.dedup();
231
-
232
- let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
233
- let loaded_changes = changelog_reader
234
- .load_changes(ChangeLoadRequest {
235
- change_ids: &change_ids,
236
- })
237
- .await?;
238
- let mut changes = HashMap::new();
239
- for (change_id, loaded) in change_ids.into_iter().zip(loaded_changes.entries) {
240
- let Some(change) = loaded else {
241
- return Err(LixError::unknown(format!(
242
- "tracked-state diff row references missing changelog change '{change_id}'"
243
- )));
244
- };
245
- changes.insert(change_id, change);
246
- }
247
- let commit_ids = rows
248
- .iter()
249
- .filter(|(row, _)| row.schema_key == "lix_commit")
250
- .map(|(row, _)| row.commit_id.clone())
251
- .collect::<Vec<_>>();
252
- if !commit_ids.is_empty() {
253
- let batch = changelog_reader
254
- .load_commits(CommitLoadRequest {
255
- commit_ids: &commit_ids,
256
- projection: CommitProjection::Record,
257
- })
258
- .await?;
259
- for (commit_id, entry) in commit_ids.into_iter().zip(batch.entries) {
260
- let Some(CommitLoadEntry::Record(commit)) = entry else {
261
- return Err(LixError::unknown(format!(
262
- "tracked-state diff row references missing changelog commit '{commit_id}'"
263
- )));
264
- };
265
- changes.insert(
266
- commit.change_id.clone(),
267
- change_record_from_commit_record(&commit)?,
268
- );
269
- }
270
- }
271
-
272
- let mut validation_cache = DiffCommitRootValidationCache::new();
273
- for (row, expected_commit_id) in rows {
274
- validate_diff_row_against_changelog(row, &changes)?;
275
- let change_created_at = changes
276
- .get(&row.change_id)
277
- .map(|change| change.created_at.as_str())
278
- .ok_or_else(|| {
279
- LixError::unknown(format!(
280
- "tracked-state diff row references missing changelog change '{}'",
281
- row.change_id
282
- ))
283
- })?;
284
- self.validate_diff_row_commit_root_membership(
285
- row,
286
- expected_commit_id,
287
- change_created_at,
288
- &mut validation_cache,
289
- )
290
- .await?;
291
- }
292
- Ok(())
293
- }
294
-
295
- async fn validate_diff_row_commit_root_membership(
296
- &mut self,
297
- row: &TrackedStateDiffRow,
298
- root_commit_id: &str,
299
- change_created_at: &str,
300
- cache: &mut DiffCommitRootValidationCache,
301
- ) -> Result<(), LixError> {
302
- let identity = tracked_state_identity_from_diff_row(row)?;
303
- let key = TrackedStateKey {
304
- schema_key: row.schema_key.clone(),
305
- file_id: row.file_id.clone(),
306
- entity_pk: row.entity_pk.clone(),
307
- };
308
- let root_metadata = self
309
- .load_cached_commit_root_metadata(root_commit_id, cache)
310
- .await?;
311
- self.validate_commit_root_parent_matches_changelog(root_commit_id, &root_metadata, cache)
312
- .await?;
313
- let (_, row_value) = row.clone().into_index_entry();
314
- let mut current_commit_id = root_commit_id.to_string();
315
- let mut seen = HashSet::new();
316
- loop {
317
- if !seen.insert(current_commit_id.clone()) {
318
- return Err(LixError::unknown(format!(
319
- "tracked-state commit-root parent chain contains cycle at commit '{current_commit_id}'"
320
- )));
321
- }
322
-
323
- let winners = self
324
- .load_cached_commit_ref_winners(&current_commit_id, cache)
325
- .await?;
326
- if let Some(winner_change_id) = winners.get(&identity) {
327
- if winner_change_id != &row.change_id {
328
- return Err(LixError::unknown(format!(
329
- "tracked-state diff row references changelog change '{}' that is not the first-parent winner for commit '{}' and identity {:?}",
330
- row.change_id, root_commit_id, identity
331
- )));
332
- }
333
- self.validate_diff_row_created_at(row, &key, &current_commit_id, change_created_at)
334
- .await?;
335
- return Ok(());
336
- }
337
-
338
- let metadata = self
339
- .load_cached_commit_root_metadata(&current_commit_id, cache)
340
- .await?;
341
- self.validate_commit_root_parent_matches_changelog(
342
- &current_commit_id,
343
- &metadata,
344
- cache,
345
- )
346
- .await?;
347
- let Some(parent) = metadata.parent_roots.first() else {
348
- return Err(LixError::unknown(format!(
349
- "tracked-state diff row references changelog change '{}' that is not the first-parent winner for commit '{}' and identity {:?}",
350
- row.change_id, root_commit_id, identity
351
- )));
352
- };
353
- let parent_value = self
354
- .load_cached_tree_value(&parent.root_id, &key, cache)
355
- .await?;
356
- if parent_value.as_ref() != Some(&row_value) {
357
- return Err(LixError::unknown(format!(
358
- "tracked-state commit-root row for commit '{}' does not match parent root '{}' for inherited identity {:?}",
359
- root_commit_id, parent.commit_id, identity
360
- )));
361
- }
362
- current_commit_id = parent.commit_id.clone();
363
- }
364
- }
365
-
366
- async fn validate_commit_root_parent_matches_changelog(
367
- &mut self,
368
- commit_id: &str,
369
- metadata: &TrackedStateCommitRoot,
370
- cache: &mut DiffCommitRootValidationCache,
371
- ) -> Result<(), LixError> {
372
- let changelog_first_parent = self
373
- .load_cached_changelog_first_parent(commit_id, cache)
374
- .await?;
375
- let expected_parent = match changelog_first_parent.as_deref() {
376
- Some(first_parent_id) => {
377
- self.nearest_available_commit_root_parent(first_parent_id, cache)
378
- .await?
379
- }
380
- None => None,
381
- };
382
- match (expected_parent, metadata.parent_roots.first()) {
383
- (None, None) => Ok(()),
384
- (Some((expected_parent_id, expected_root)), Some(parent))
385
- if parent.commit_id == expected_parent_id && parent.root_id == expected_root =>
386
- {
387
- Ok(())
388
- }
389
- (Some((expected_parent_id, expected_root)), Some(parent))
390
- if parent.commit_id == expected_parent_id =>
391
- {
392
- let _ = expected_root;
393
- Err(LixError::unknown(format!(
394
- "tracked-state commit-root metadata for commit '{}' references stale root for commit-root parent '{}'",
395
- commit_id, expected_parent_id
396
- )))
397
- }
398
- (Some((expected_parent_id, _)), Some(parent)) => Err(LixError::unknown(format!(
399
- "tracked-state commit-root metadata for commit '{}' references parent '{}' but nearest available first-parent root is '{}'",
400
- commit_id, parent.commit_id, expected_parent_id
401
- ))),
402
- (Some((expected_parent_id, _)), None) => Err(LixError::unknown(format!(
403
- "tracked-state commit-root metadata for commit '{}' is missing commit-root parent '{}'",
404
- commit_id, expected_parent_id
405
- ))),
406
- (None, Some(parent)) => Err(LixError::unknown(format!(
407
- "tracked-state commit-root metadata for root commit '{}' references unexpected parent '{}'",
408
- commit_id, parent.commit_id
409
- ))),
410
- }
411
- }
412
-
413
- async fn nearest_available_commit_root_parent(
414
- &mut self,
415
- start_commit_id: &str,
416
- cache: &mut DiffCommitRootValidationCache,
417
- ) -> Result<Option<(String, TrackedStateRootId)>, LixError> {
418
- let mut current = Some(start_commit_id.to_string());
419
- let mut seen = HashSet::new();
420
- while let Some(commit_id) = current {
421
- if !seen.insert(commit_id.clone()) {
422
- return Err(LixError::unknown(format!(
423
- "tracked-state commit-root parent chain contains cycle at commit '{commit_id}'"
424
- )));
425
- }
426
- if let Some(root_id) = self
427
- .load_cached_commit_root_optional(&commit_id, cache)
428
- .await?
429
- {
430
- return Ok(Some((commit_id, root_id)));
431
- }
432
- current = self
433
- .load_cached_changelog_first_parent(&commit_id, cache)
434
- .await?;
435
- }
436
- Ok(None)
437
- }
438
-
439
- async fn load_cached_commit_ref_winners(
440
- &mut self,
441
- commit_id: &str,
442
- cache: &mut DiffCommitRootValidationCache,
443
- ) -> Result<HashMap<TrackedStateIdentity, String>, LixError> {
444
- if let Some(winners) = cache.commit_ref_winners.get(commit_id) {
445
- return Ok(winners.clone());
446
- }
447
- let commit_ids = [commit_id.to_string()];
448
- let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
449
- let batch = changelog_reader
450
- .load_commits(CommitLoadRequest {
451
- commit_ids: &commit_ids,
452
- projection: CommitProjection::Full,
453
- })
454
- .await?;
455
- let Some(entry) = batch.entries.into_iter().next().flatten() else {
456
- return Err(LixError::unknown(format!(
457
- "changelog commit '{commit_id}' is missing while validating tracked-state commit-root rows"
458
- )));
459
- };
460
- let CommitLoadEntry::Full {
461
- record,
462
- change_ref_chunks: chunks,
463
- } = entry
464
- else {
465
- return Err(LixError::unknown(format!(
466
- "changelog commit '{commit_id}' did not return full commit"
467
- )));
468
- };
469
- let mut winners = HashMap::new();
470
- winners.insert(
471
- TrackedStateIdentity {
472
- schema_key: "lix_commit".to_string(),
473
- file_id: None,
474
- entity_pk: EntityPk::single(&record.commit_id),
475
- },
476
- record.change_id,
477
- );
478
- for change_ref in chunks.into_iter().flat_map(|chunk| chunk.entries) {
479
- winners.insert(
480
- TrackedStateIdentity {
481
- schema_key: change_ref.schema_key,
482
- file_id: change_ref.file_id,
483
- entity_pk: change_ref.entity_pk,
484
- },
485
- change_ref.change_id,
486
- );
487
- }
488
- cache
489
- .commit_ref_winners
490
- .insert(commit_id.to_string(), winners.clone());
491
- Ok(winners)
492
- }
493
-
494
- async fn load_cached_commit_root_metadata(
495
- &mut self,
496
- commit_id: &str,
497
- cache: &mut DiffCommitRootValidationCache,
498
- ) -> Result<TrackedStateCommitRoot, LixError> {
499
- if let Some(metadata) = cache.commit_root_metadata.get(commit_id) {
500
- return Ok(metadata.clone());
501
- }
502
- let metadata = storage::load_commit_root(&mut self.store, commit_id)
503
- .await?
504
- .ok_or_else(|| missing_commit_root_error(commit_id))?;
505
- cache
506
- .commit_root_metadata
507
- .insert(commit_id.to_string(), metadata.clone());
508
- Ok(metadata)
509
- }
510
-
511
- async fn load_cached_commit_root_optional(
512
- &mut self,
513
- commit_id: &str,
514
- cache: &mut DiffCommitRootValidationCache,
515
- ) -> Result<Option<TrackedStateRootId>, LixError> {
516
- if let Some(root_id) = cache.commit_roots.get(commit_id) {
517
- return Ok(Some(root_id.clone()));
518
- }
519
- let root_id = storage::load_root(&self.store, commit_id).await?;
520
- if let Some(root_id) = &root_id {
521
- cache
522
- .commit_roots
523
- .insert(commit_id.to_string(), root_id.clone());
524
- }
525
- Ok(root_id)
526
- }
527
-
528
- async fn load_cached_tree_value(
529
- &mut self,
530
- root_id: &TrackedStateRootId,
531
- key: &TrackedStateKey,
532
- cache: &mut DiffCommitRootValidationCache,
533
- ) -> Result<Option<TrackedStateIndexValue>, LixError> {
534
- let cache_key = (root_id.clone(), key.clone());
535
- if let Some(value) = cache.tree_values.get(&cache_key) {
536
- return Ok(value.clone());
537
- }
538
- let value = self
539
- .tree
540
- .get_many(&mut self.store, root_id, std::slice::from_ref(key))
541
- .await?
542
- .into_iter()
543
- .next()
544
- .flatten();
545
- cache.tree_values.insert(cache_key, value.clone());
546
- Ok(value)
547
- }
548
-
549
- async fn load_cached_changelog_first_parent(
550
- &mut self,
551
- commit_id: &str,
552
- cache: &mut DiffCommitRootValidationCache,
553
- ) -> Result<Option<String>, LixError> {
554
- if let Some(parent_id) = cache.changelog_first_parents.get(commit_id) {
555
- return Ok(parent_id.clone());
556
- }
557
- let commit_ids = [commit_id.to_string()];
558
- let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
559
- let batch = changelog_reader
560
- .load_commits(CommitLoadRequest {
561
- commit_ids: &commit_ids,
562
- projection: CommitProjection::Record,
563
- })
564
- .await?;
565
- let Some(entry) = batch.entries.into_iter().next().flatten() else {
566
- return Err(LixError::unknown(format!(
567
- "changelog commit '{commit_id}' is missing while validating tracked-state commit-root metadata"
568
- )));
569
- };
570
- let CommitLoadEntry::Record(record) = entry else {
571
- return Err(LixError::unknown(format!(
572
- "changelog commit '{commit_id}' did not return a commit record"
573
- )));
574
- };
575
- let parent_id = record.parent_commit_ids.first().cloned();
576
- cache
577
- .changelog_first_parents
578
- .insert(commit_id.to_string(), parent_id.clone());
579
- Ok(parent_id)
580
- }
581
-
582
- async fn validate_diff_row_created_at(
583
- &mut self,
584
- row: &TrackedStateDiffRow,
585
- key: &TrackedStateKey,
586
- commit_id: &str,
587
- change_created_at: &str,
588
- ) -> Result<(), LixError> {
589
- let mut expected_created_at = change_created_at.to_string();
590
- let Some(metadata) = storage::load_commit_root(&mut self.store, commit_id).await? else {
591
- return Err(missing_commit_root_error(commit_id));
592
- };
593
- if let Some(parent) = metadata.parent_roots.first() {
594
- let parent_value = self
595
- .tree
596
- .get_many(&mut self.store, &parent.root_id, std::slice::from_ref(key))
597
- .await?
598
- .into_iter()
599
- .next()
600
- .flatten();
601
- if let Some(parent_value) = parent_value {
602
- expected_created_at = parent_value.created_at;
603
- }
604
- }
605
- if expected_created_at == change_created_at {
606
- if let Some(merge_parent_created_at) = self
607
- .load_merge_parent_created_at_for_row(commit_id, row, key)
608
- .await?
609
- {
610
- expected_created_at = merge_parent_created_at;
611
- }
612
- }
613
- if expected_created_at == change_created_at && row.commit_id != commit_id {
614
- if let Some(source_created_at) =
615
- self.load_parent_created_at_for_row_commit(row, key).await?
616
- {
617
- expected_created_at = source_created_at;
618
- }
619
- }
620
- if row.created_at == expected_created_at {
621
- return Ok(());
622
- }
623
- Err(LixError::unknown(format!(
624
- "tracked-state diff row for change '{}' created_at '{}' does not match first ancestry timestamp '{}'",
625
- row.change_id, row.created_at, expected_created_at
626
- )))
627
- }
628
-
629
- async fn load_merge_parent_created_at_for_row(
630
- &mut self,
631
- commit_id: &str,
632
- row: &TrackedStateDiffRow,
633
- key: &TrackedStateKey,
634
- ) -> Result<Option<String>, LixError> {
635
- let commit_ids = [commit_id.to_string()];
636
- let mut changelog_reader = ChangelogContext::new().reader(&mut self.store);
637
- let batch = changelog_reader
638
- .load_commits(CommitLoadRequest {
639
- commit_ids: &commit_ids,
640
- projection: CommitProjection::Record,
641
- })
642
- .await?;
643
- let Some(CommitLoadEntry::Record(commit)) = batch.entries.into_iter().next().flatten()
644
- else {
645
- return Ok(None);
646
- };
647
- for parent_id in commit.parent_commit_ids.iter().skip(1) {
648
- let Some(parent_root) = storage::load_root(&self.store, parent_id).await? else {
649
- continue;
650
- };
651
- let parent_value = self
652
- .tree
653
- .get_many(&mut self.store, &parent_root, std::slice::from_ref(key))
654
- .await?
655
- .into_iter()
656
- .next()
657
- .flatten();
658
- if let Some(parent_value) = parent_value {
659
- if parent_value.change_id == row.change_id {
660
- return Ok(Some(parent_value.created_at));
661
- }
662
- }
663
- }
664
- Ok(None)
665
- }
666
-
667
- async fn load_parent_created_at_for_row_commit(
668
- &mut self,
669
- row: &TrackedStateDiffRow,
670
- key: &TrackedStateKey,
671
- ) -> Result<Option<String>, LixError> {
672
- let Some(metadata) = storage::load_commit_root(&mut self.store, &row.commit_id).await?
673
- else {
674
- return Ok(None);
675
- };
676
- let Some(parent) = metadata.parent_roots.first() else {
677
- return Ok(None);
678
- };
679
- let parent_value = self
680
- .tree
681
- .get_many(&mut self.store, &parent.root_id, std::slice::from_ref(key))
682
- .await?
683
- .into_iter()
684
- .next()
685
- .flatten();
686
- Ok(parent_value.map(|value| value.created_at))
687
- }
688
-
689
- pub(crate) async fn validate_tree_rows_at_commit_against_changelog(
690
- &mut self,
691
- commit_id: &str,
692
- request: &TrackedStateTreeScanRequest,
693
- ) -> Result<(), LixError> {
694
- let root = self.load_ensured_root(commit_id).await?;
695
- let rows = self.tree.scan(&mut self.store, &root, request).await?;
696
- self.validate_commit_root_coverage(commit_id, request, &rows)
697
- .await?;
698
- let rows = rows
699
- .into_iter()
700
- .map(|(key, value)| TrackedStateDiffRow::from_tree_entry(key, value))
701
- .collect::<Vec<_>>();
702
- let row_refs = rows.iter().map(|row| (row, commit_id)).collect::<Vec<_>>();
703
- self.validate_diff_rows_for_commits_against_changelog(&row_refs)
704
- .await
705
- }
706
-
707
- async fn validate_commit_root_coverage(
708
- &mut self,
709
- commit_id: &str,
710
- request: &TrackedStateTreeScanRequest,
711
- rows: &[(TrackedStateKey, TrackedStateIndexValue)],
712
- ) -> Result<(), LixError> {
713
- let row_map = rows
714
- .iter()
715
- .map(|(key, value)| (tracked_state_identity_from_key(key), value))
716
- .collect::<HashMap<_, _>>();
717
- let mut cache = DiffCommitRootValidationCache::new();
718
- let winners = self
719
- .load_cached_commit_ref_winners(commit_id, &mut cache)
720
- .await?;
721
- for (identity, change_id) in &winners {
722
- if !tracked_state_identity_matches_tree_request(identity, request) {
723
- continue;
724
- }
725
- let Some(value) = row_map.get(identity) else {
726
- return Err(LixError::unknown(format!(
727
- "tracked-state commit-root for commit '{commit_id}' omits current changelog change '{change_id}' for identity {:?}",
728
- identity
729
- )));
730
- };
731
- if &value.change_id != change_id {
732
- return Err(LixError::unknown(format!(
733
- "tracked-state commit-root for commit '{commit_id}' stores change '{}' but changelog winner is '{}' for identity {:?}",
734
- value.change_id, change_id, identity
735
- )));
736
- }
737
- }
738
-
739
- let metadata = self
740
- .load_cached_commit_root_metadata(commit_id, &mut cache)
741
- .await?;
742
- let Some(parent) = metadata.parent_roots.first() else {
743
- return Ok(());
744
- };
745
- let parent_rows = self
746
- .tree
747
- .scan(&mut self.store, &parent.root_id, request)
748
- .await?;
749
- for (parent_key, parent_value) in parent_rows {
750
- let identity = tracked_state_identity_from_key(&parent_key);
751
- if winners.contains_key(&identity) {
752
- continue;
753
- }
754
- let Some(value) = row_map.get(&identity) else {
755
- return Err(LixError::unknown(format!(
756
- "tracked-state commit-root for commit '{commit_id}' omits inherited identity {:?} from parent '{}'",
757
- identity, parent.commit_id
758
- )));
759
- };
760
- if *value != &parent_value {
761
- return Err(LixError::unknown(format!(
762
- "tracked-state commit-root for commit '{commit_id}' does not preserve inherited identity {:?} from parent '{}'",
763
- identity, parent.commit_id
764
- )));
765
- }
766
- }
767
- Ok(())
768
- }
769
-
770
- pub(crate) async fn diff_tree_entries_at_commits(
771
- &mut self,
772
- left_commit_id: &str,
773
- right_commit_id: &str,
774
- request: &TrackedStateTreeScanRequest,
775
- ) -> Result<Vec<crate::tracked_state::types::TrackedStateTreeDiffEntry>, LixError> {
776
- let left_root = self.load_ensured_root(left_commit_id).await?;
777
- let right_root = self.load_ensured_root(right_commit_id).await?;
778
- self.tree
779
- .diff(
780
- &mut self.store,
781
- Some(&left_root),
782
- Some(&right_root),
783
- request,
784
- )
785
- .await
786
- }
787
-
788
- async fn load_ensured_root(
789
- &mut self,
790
- commit_id: &str,
791
- ) -> Result<crate::tracked_state::types::TrackedStateRootId, LixError> {
792
- self.tree
793
- .load_root(&mut self.store, commit_id)
794
- .await?
795
- .ok_or_else(|| missing_commit_root_error(commit_id))
796
- }
797
-
798
- #[cfg(any(test, feature = "storage-benches"))]
799
- async fn commit_root_values_for_keys(
800
- &mut self,
801
- commit_id: &str,
802
- keys: &[TrackedStateKey],
803
- ) -> Result<Vec<Option<TrackedStateIndexValue>>, LixError> {
804
- let root_id = self.load_ensured_root(commit_id).await?;
805
- self.tree.get_many(&mut self.store, &root_id, keys).await
806
- }
807
-
808
- /// Plans a three-way merge by diffing both heads against the same base.
809
- ///
810
- /// `target_commit_id` is the destination root that should keep its own
811
- /// changes. `source_commit_id` is the incoming root whose non-conflicting
812
- /// changes should be applied.
813
- #[cfg(test)]
814
- pub(crate) async fn plan_merge(
815
- &mut self,
816
- base_commit_id: &str,
817
- target_commit_id: &str,
818
- source_commit_id: &str,
819
- request: &TrackedStateDiffRequest,
820
- ) -> Result<TrackedStateMergePlan, LixError> {
821
- let target_diff = self
822
- .diff_commits(base_commit_id, target_commit_id, request)
823
- .await?;
824
- let source_diff = self
825
- .diff_commits(base_commit_id, source_commit_id, request)
826
- .await?;
827
- merge::plan_merge(&target_diff, &source_diff)
828
- }
829
- }
830
-
831
- /// Writer for changelog-backed tracked-state commit roots.
832
- pub(crate) struct TrackedStateWriter<'a, S: ?Sized> {
833
- chunk_overlay: storage::TrackedStateChunkOverlay,
834
- staged_roots: BTreeMap<String, crate::tracked_state::types::TrackedStateRootId>,
835
- tree: TrackedStateTree,
836
- store: &'a S,
837
- writes: &'a mut StorageWriteSet,
838
- }
839
-
840
- /// Explicit commit-root rebuilder created by `TrackedStateContext`.
841
- pub(crate) struct TrackedStateRootRebuilder<'a, S: ?Sized> {
842
- pub(super) store: &'a S,
843
- pub(super) writes: &'a mut StorageWriteSet,
844
- }
845
-
846
- impl<S> TrackedStateRootRebuilder<'_, S>
847
- where
848
- S: StorageRead + Send + Sync + ?Sized,
849
- {
850
- pub(crate) async fn rebuild_commit_root_at(
851
- &mut self,
852
- commit_id: &str,
853
- ) -> Result<TrackedStateWriteReport, LixError> {
854
- crate::tracked_state::commit_root_rebuild::rebuild_commit_root_at(self, commit_id).await
855
- }
856
- }
857
-
858
- impl<S> TrackedStateWriter<'_, S>
859
- where
860
- S: StorageRead + Send + Sync + ?Sized,
861
- {
862
- pub(crate) async fn stage_commit_root<'a, I>(
863
- &mut self,
864
- commit_id: &str,
865
- parent_commit_id: Option<&str>,
866
- deltas: I,
867
- ) -> Result<TrackedStateWriteReport, LixError>
868
- where
869
- I: IntoIterator<Item = TrackedStateDeltaRef<'a>>,
870
- {
871
- let deltas = deltas.into_iter().collect::<Vec<_>>();
872
- let base_root = match parent_commit_id {
873
- Some(parent_commit_id) => {
874
- let root = match self.staged_roots.get(parent_commit_id) {
875
- Some(root) => Some(root.clone()),
876
- None => self.tree.load_root(self.store, parent_commit_id).await?,
877
- };
878
- let Some(root) = root else {
879
- return Err(LixError::new(
880
- "LIX_ERROR_UNKNOWN",
881
- format!(
882
- "tracked-state parent root for commit '{parent_commit_id}' is missing"
883
- ),
884
- ));
885
- };
886
- Some(root)
887
- }
888
- None => None,
889
- };
890
- let parent_values = if let Some(base_root) = base_root.as_ref() {
891
- let keys = deltas
892
- .iter()
893
- .map(|delta| TrackedStateKey {
894
- schema_key: delta.schema_key.to_string(),
895
- file_id: delta.file_id.map(str::to_string),
896
- entity_pk: delta.entity_pk.clone(),
897
- })
898
- .collect::<Vec<_>>();
899
- self.tree.get_many(self.store, base_root, &keys).await?
900
- } else {
901
- vec![None; deltas.len()]
902
- };
903
- let mut mutations = Vec::with_capacity(deltas.len());
904
- for (delta, parent_value) in deltas.iter().zip(parent_values.iter()) {
905
- let created_at = parent_value
906
- .as_ref()
907
- .map(|value| value.created_at.as_str())
908
- .unwrap_or(delta.created_at);
909
- let key = TrackedStateKeyRef {
910
- schema_key: delta.schema_key,
911
- file_id: delta.file_id,
912
- entity_pk: delta.entity_pk,
913
- };
914
- let value = crate::tracked_state::types::TrackedStateIndexValueRef {
915
- change_id: delta.change_id,
916
- commit_id: delta.commit_id,
917
- deleted: delta.deleted,
918
- snapshot_ref: delta.snapshot_ref,
919
- metadata_ref: delta.metadata_ref,
920
- created_at,
921
- updated_at: delta.updated_at,
922
- };
923
- mutations.push(TrackedStateMutation::put_encoded(
924
- encode_key_ref(key),
925
- encode_value_ref(value),
926
- ));
927
- }
928
- let result = self
929
- .tree
930
- .apply_mutations_with_overlay(
931
- self.store,
932
- self.writes,
933
- &mut self.chunk_overlay,
934
- base_root.as_ref(),
935
- mutations,
936
- Some(commit_id),
937
- )
938
- .await?;
939
- self.staged_roots
940
- .insert(commit_id.to_string(), result.root_id.clone());
941
- storage::stage_commit_root(
942
- self.writes,
943
- &TrackedStateCommitRoot {
944
- commit_id: commit_id.to_string(),
945
- root_id: result.root_id.clone(),
946
- parent_roots: parent_commit_id
947
- .zip(base_root.as_ref())
948
- .map(|(parent_commit_id, root_id)| {
949
- vec![TrackedStateCommitRootParent {
950
- commit_id: parent_commit_id.to_string(),
951
- root_id: root_id.clone(),
952
- }]
953
- })
954
- .unwrap_or_default(),
955
- changed_key_count: u64::try_from(deltas.len()).map_err(|_| {
956
- LixError::new(
957
- LixError::CODE_INTERNAL_ERROR,
958
- "tracked_state commit_root changed key count exceeds u64",
959
- )
960
- })?,
961
- row_count_estimate: u64::try_from(result.row_count).map_err(|_| {
962
- LixError::new(
963
- LixError::CODE_INTERNAL_ERROR,
964
- "tracked_state commit_root row count exceeds u64",
965
- )
966
- })?,
967
- tree_height: u32::try_from(result.tree_height).map_err(|_| {
968
- LixError::new(
969
- LixError::CODE_INTERNAL_ERROR,
970
- "tracked_state commit_root tree height exceeds u32",
971
- )
972
- })?,
973
- primary_chunk_count: u64::try_from(result.chunk_count).map_err(|_| {
974
- LixError::new(
975
- LixError::CODE_INTERNAL_ERROR,
976
- "tracked_state commit_root chunk count exceeds u64",
977
- )
978
- })?,
979
- primary_chunk_bytes: u64::try_from(result.chunk_bytes).map_err(|_| {
980
- LixError::new(
981
- LixError::CODE_INTERNAL_ERROR,
982
- "tracked_state commit_root chunk bytes exceeds u64",
983
- )
984
- })?,
985
- },
986
- )?;
987
-
988
- Ok(TrackedStateWriteReport {
989
- commit_id: commit_id.to_string(),
990
- root_id: result.root_id,
991
- changed_rows: deltas.len(),
992
- primary_chunk_puts: result.chunk_count,
993
- })
994
- }
995
- }
996
-
997
- #[derive(Debug, Clone, PartialEq, Eq)]
998
- pub(crate) struct TrackedStateWriteReport {
999
- pub(crate) commit_id: String,
1000
- pub(crate) root_id: TrackedStateRootId,
1001
- pub(crate) changed_rows: usize,
1002
- pub(crate) primary_chunk_puts: usize,
1003
- }
1004
-
1005
- fn missing_commit_root_error(commit_id: &str) -> LixError {
1006
- LixError::new(
1007
- LixError::CODE_INTERNAL_ERROR,
1008
- format!(
1009
- "tracked_state commit_root is missing for commit '{commit_id}'; run explicit commit_root rebuild before structural diff"
1010
- ),
1011
- )
1012
- }
1013
-
1014
- fn tree_scan_request_from_tracked(
1015
- request: &TrackedStateScanRequest,
1016
- ) -> TrackedStateTreeScanRequest {
1017
- TrackedStateTreeScanRequest {
1018
- schema_keys: request.filter.schema_keys.clone(),
1019
- entity_pks: request.filter.entity_pks.clone(),
1020
- file_ids: request.filter.file_ids.clone(),
1021
- include_tombstones: request.filter.include_tombstones,
1022
- // User limits belong above delta overlay and tombstone visibility.
1023
- // Pushing them into the physical tree can stop on rows that are later
1024
- // hidden, returning too few live rows.
1025
- limit: None,
1026
- }
1027
- }
1028
-
1029
- fn validate_diff_row_against_changelog(
1030
- row: &TrackedStateDiffRow,
1031
- changes: &HashMap<String, ChangeRecord>,
1032
- ) -> Result<(), LixError> {
1033
- let Some(change) = changes.get(&row.change_id) else {
1034
- return Err(LixError::unknown(format!(
1035
- "tracked-state diff row references missing changelog change '{}'",
1036
- row.change_id
1037
- )));
1038
- };
1039
- if change.schema_key != row.schema_key
1040
- || change.file_id != row.file_id
1041
- || change.entity_pk != row.entity_pk
1042
- {
1043
- return Err(LixError::unknown(format!(
1044
- "tracked-state diff row for change '{}' does not match changelog change identity",
1045
- row.change_id
1046
- )));
1047
- }
1048
- if row.deleted != change.snapshot_ref.is_none() {
1049
- return Err(LixError::unknown(format!(
1050
- "tracked-state diff row for change '{}' deleted flag does not match changelog snapshot",
1051
- row.change_id
1052
- )));
1053
- }
1054
- if row.snapshot_ref != change.snapshot_ref || row.metadata_ref != change.metadata_ref {
1055
- return Err(LixError::unknown(format!(
1056
- "tracked-state diff row for change '{}' payload refs do not match changelog change",
1057
- row.change_id
1058
- )));
1059
- }
1060
- if row.updated_at != change.created_at {
1061
- return Err(LixError::unknown(format!(
1062
- "tracked-state diff row for change '{}' updated_at does not match changelog change timestamp",
1063
- row.change_id
1064
- )));
1065
- }
1066
- Ok(())
1067
- }
1068
-
1069
- fn change_record_from_commit_record(commit: &CommitRecord) -> Result<ChangeRecord, LixError> {
1070
- let snapshot_content = commit_row_snapshot_content(&commit.commit_id)?;
1071
- Ok(ChangeRecord {
1072
- format_version: 1,
1073
- change_id: commit.change_id.clone(),
1074
- schema_key: "lix_commit".to_string(),
1075
- entity_pk: EntityPk::single(&commit.commit_id),
1076
- file_id: None,
1077
- snapshot_ref: Some(crate::json_store::JsonRef::for_content(
1078
- snapshot_content.as_bytes(),
1079
- )),
1080
- metadata_ref: None,
1081
- created_at: commit.created_at.clone(),
1082
- })
1083
- }
1084
-
1085
- fn commit_row_snapshot_content(commit_id: &str) -> Result<String, LixError> {
1086
- serde_json::to_string(&serde_json::json!({
1087
- "id": commit_id,
1088
- }))
1089
- .map_err(|error| {
1090
- LixError::new(
1091
- LixError::CODE_INTERNAL_ERROR,
1092
- format!("failed to encode lix_commit snapshot: {error}"),
1093
- )
1094
- })
1095
- }
1096
-
1097
- fn tracked_state_identity_from_diff_row(
1098
- row: &TrackedStateDiffRow,
1099
- ) -> Result<TrackedStateIdentity, LixError> {
1100
- Ok(TrackedStateIdentity {
1101
- schema_key: row.schema_key.clone(),
1102
- file_id: row.file_id.clone(),
1103
- entity_pk: row.entity_pk.clone(),
1104
- })
1105
- }
1106
-
1107
- fn tracked_state_identity_from_key(key: &TrackedStateKey) -> TrackedStateIdentity {
1108
- TrackedStateIdentity {
1109
- schema_key: key.schema_key.clone(),
1110
- file_id: key.file_id.clone(),
1111
- entity_pk: key.entity_pk.clone(),
1112
- }
1113
- }
1114
-
1115
- fn tracked_state_identity_matches_tree_request(
1116
- identity: &TrackedStateIdentity,
1117
- request: &TrackedStateTreeScanRequest,
1118
- ) -> bool {
1119
- if !request.schema_keys.is_empty() && !request.schema_keys.contains(&identity.schema_key) {
1120
- return false;
1121
- }
1122
- if !request.entity_pks.is_empty() && !request.entity_pks.contains(&identity.entity_pk) {
1123
- return false;
1124
- }
1125
- nullable_key_filter_allows(&request.file_ids, identity.file_id.as_deref())
1126
- }
1127
-
1128
- fn nullable_key_filter_allows(filters: &[NullableKeyFilter<String>], value: Option<&str>) -> bool {
1129
- filters.is_empty()
1130
- || filters.iter().any(|filter| match (filter, value) {
1131
- (NullableKeyFilter::Any, _) => true,
1132
- (NullableKeyFilter::Null, None) => true,
1133
- (NullableKeyFilter::Value(expected), Some(value)) => expected == value,
1134
- _ => false,
1135
- })
1136
- }
1137
-
1138
- #[cfg(test)]
1139
- mod tests {
1140
- use super::*;
1141
- use crate::storage::StorageContext;
1142
- use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
1143
- use crate::NullableKeyFilter;
1144
-
1145
- #[tokio::test]
1146
- async fn stage_commit_root_requires_parent_commit_root() {
1147
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1148
- let tracked_state = TrackedStateContext::new();
1149
- {
1150
- let mut read = storage
1151
- .begin_read(StorageReadOptions::default())
1152
- .expect("parent read should open");
1153
- let mut writes = storage.new_write_set();
1154
- crate::test_support::stage_empty_changelog_commit(
1155
- &mut read,
1156
- &mut writes,
1157
- "missing-parent",
1158
- None,
1159
- )
1160
- .await
1161
- .expect("parent changelog commit should stage");
1162
- storage
1163
- .commit_write_set(writes, StorageWriteOptions::default())
1164
- .expect("parent changelog commit should commit");
1165
- }
1166
-
1167
- write_root_for_test(
1168
- &storage,
1169
- &tracked_state,
1170
- "commit-child",
1171
- Some("missing-parent"),
1172
- &[row("entity-child", "change-child", "commit-child")],
1173
- )
1174
- .await
1175
- .expect_err("root staging should require a parent commit root");
1176
- }
1177
-
1178
- #[tokio::test]
1179
- async fn stage_commit_root_writes_commit_root_metadata() {
1180
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1181
- let tracked_state = TrackedStateContext::new();
1182
- write_root_for_test(
1183
- &storage,
1184
- &tracked_state,
1185
- "parent",
1186
- None,
1187
- &[row("entity-a", "change-parent", "parent")],
1188
- )
1189
- .await
1190
- .expect("parent root should write");
1191
- write_root_for_test(
1192
- &storage,
1193
- &tracked_state,
1194
- "child",
1195
- Some("parent"),
1196
- &[
1197
- row("entity-a", "change-child-a", "child"),
1198
- row("entity-b", "change-child-b", "child"),
1199
- ],
1200
- )
1201
- .await
1202
- .expect("child root should write");
1203
-
1204
- let read = storage
1205
- .begin_read(StorageReadOptions::default())
1206
- .expect("read should open");
1207
- let parent_root = storage::load_root(&read, "parent")
1208
- .await
1209
- .expect("parent root should load")
1210
- .expect("parent root should exist");
1211
- let child_root = storage::load_root(&read, "child")
1212
- .await
1213
- .expect("child root should load")
1214
- .expect("child root should exist");
1215
- let metadata = storage::load_commit_root(&read, "child")
1216
- .await
1217
- .expect("metadata should load")
1218
- .expect("metadata should exist");
1219
-
1220
- assert_eq!(metadata.commit_id, "child");
1221
- assert_eq!(metadata.root_id, child_root);
1222
- assert_eq!(metadata.parent_roots.len(), 1);
1223
- assert_eq!(metadata.parent_roots[0].commit_id, "parent");
1224
- assert_eq!(metadata.parent_roots[0].root_id, parent_root);
1225
- assert_eq!(metadata.changed_key_count, 3);
1226
- assert_eq!(metadata.row_count_estimate, 4);
1227
- assert!(metadata.tree_height >= 1);
1228
- assert!(metadata.primary_chunk_count >= 1);
1229
- assert!(metadata.primary_chunk_bytes > 0);
1230
- }
1231
-
1232
- #[tokio::test]
1233
- async fn plan_merge_from_roots_applies_source_only_change() {
1234
- let (storage, tracked_state) = seed_merge_roots(
1235
- &[row_with_value("entity-a", "change-base", "base", "base")],
1236
- &[row_with_value("entity-a", "change-base", "base", "base")],
1237
- &[row_with_value(
1238
- "entity-a",
1239
- "change-source",
1240
- "source",
1241
- "source",
1242
- )],
1243
- )
1244
- .await;
1245
-
1246
- let plan = tracked_state
1247
- .reader(
1248
- storage
1249
- .begin_read(StorageReadOptions::default())
1250
- .expect("read should open"),
1251
- )
1252
- .plan_merge(
1253
- "base",
1254
- "target",
1255
- "source",
1256
- &TrackedStateDiffRequest::default(),
1257
- )
1258
- .await
1259
- .expect("merge should plan");
1260
-
1261
- assert_eq!(merge_pick_ids(&plan), vec!["entity-a"]);
1262
- assert!(plan.conflicts.is_empty());
1263
- }
1264
-
1265
- #[tokio::test]
1266
- async fn plan_merge_from_roots_keeps_target_only_change() {
1267
- let (storage, tracked_state) = seed_merge_roots(
1268
- &[row("entity-a", "change-base", "base")],
1269
- &[row("entity-a", "change-target", "target")],
1270
- &[row("entity-a", "change-base", "base")],
1271
- )
1272
- .await;
1273
-
1274
- let plan = tracked_state
1275
- .reader(
1276
- storage
1277
- .begin_read(StorageReadOptions::default())
1278
- .expect("read should open"),
1279
- )
1280
- .plan_merge(
1281
- "base",
1282
- "target",
1283
- "source",
1284
- &TrackedStateDiffRequest::default(),
1285
- )
1286
- .await
1287
- .expect("merge should plan");
1288
-
1289
- assert!(plan.picks.is_empty());
1290
- assert!(plan.conflicts.is_empty());
1291
- }
1292
-
1293
- #[tokio::test]
1294
- async fn plan_merge_from_roots_reports_divergent_modification_conflict() {
1295
- let (storage, tracked_state) = seed_merge_roots(
1296
- &[row_with_value("entity-a", "change-base", "base", "base")],
1297
- &[row_with_value(
1298
- "entity-a",
1299
- "change-target",
1300
- "target",
1301
- "target",
1302
- )],
1303
- &[row_with_value(
1304
- "entity-a",
1305
- "change-source",
1306
- "source",
1307
- "source",
1308
- )],
1309
- )
1310
- .await;
1311
-
1312
- let plan = tracked_state
1313
- .reader(
1314
- storage
1315
- .begin_read(StorageReadOptions::default())
1316
- .expect("read should open"),
1317
- )
1318
- .plan_merge(
1319
- "base",
1320
- "target",
1321
- "source",
1322
- &TrackedStateDiffRequest::default(),
1323
- )
1324
- .await
1325
- .expect("merge should plan");
1326
-
1327
- assert!(plan.picks.is_empty());
1328
- assert_eq!(merge_conflict_ids(&plan), vec!["entity-a"]);
1329
- }
1330
-
1331
- #[tokio::test]
1332
- async fn plan_merge_from_roots_applies_source_tombstone() {
1333
- let (storage, tracked_state) = seed_merge_roots(
1334
- &[row("entity-a", "change-base", "base")],
1335
- &[row("entity-a", "change-base", "base")],
1336
- &[tombstone("entity-a", "change-source-delete", "source")],
1337
- )
1338
- .await;
1339
-
1340
- let plan = tracked_state
1341
- .reader(
1342
- storage
1343
- .begin_read(StorageReadOptions::default())
1344
- .expect("read should open"),
1345
- )
1346
- .plan_merge(
1347
- "base",
1348
- "target",
1349
- "source",
1350
- &TrackedStateDiffRequest::default(),
1351
- )
1352
- .await
1353
- .expect("merge should plan");
1354
-
1355
- assert_eq!(merge_pick_ids(&plan), vec!["entity-a"]);
1356
- assert!(plan.picks[0].source_row().deleted);
1357
- assert_eq!(plan.picks[0].source_change_id(), "change-source-delete");
1358
- }
1359
-
1360
- #[tokio::test]
1361
- async fn explicit_rebuild_repairs_missing_child_root_from_nearest_parent() {
1362
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1363
- let tracked_state = TrackedStateContext::new();
1364
- write_root_for_test(
1365
- &storage,
1366
- &tracked_state,
1367
- "base",
1368
- None,
1369
- &[row_with_value("entity-a", "change-base", "base", "base")],
1370
- )
1371
- .await
1372
- .expect("base root should write");
1373
- write_root_for_test(
1374
- &storage,
1375
- &tracked_state,
1376
- "child",
1377
- Some("base"),
1378
- &[row_with_value("entity-a", "change-child", "child", "child")],
1379
- )
1380
- .await
1381
- .expect("child root should write");
1382
- {
1383
- let mut writes = storage.new_write_set();
1384
- writes.delete(
1385
- storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
1386
- crate::storage::StorageKey(bytes::Bytes::copy_from_slice(b"child")),
1387
- );
1388
- storage
1389
- .commit_write_set(writes, StorageWriteOptions::default())
1390
- .expect("child commit_root delete should commit");
1391
- }
1392
-
1393
- tracked_state
1394
- .reader(
1395
- storage
1396
- .begin_read(StorageReadOptions::default())
1397
- .expect("read should open"),
1398
- )
1399
- .diff_commits("base", "child", &test_schema_diff_request())
1400
- .await
1401
- .expect_err("diff should require durable roots before repair");
1402
-
1403
- let mut read = storage
1404
- .begin_read(StorageReadOptions::default())
1405
- .expect("read should open");
1406
- let mut writes = storage.new_write_set();
1407
- tracked_state
1408
- .root_rebuilder(&mut read, &mut writes)
1409
- .rebuild_commit_root_at("child")
1410
- .await
1411
- .expect("child root should repair");
1412
- storage
1413
- .commit_write_set(writes, StorageWriteOptions::default())
1414
- .expect("repaired root should commit");
1415
-
1416
- let diff = tracked_state
1417
- .reader(
1418
- storage
1419
- .begin_read(StorageReadOptions::default())
1420
- .expect("read should open"),
1421
- )
1422
- .diff_commits("base", "child", &test_schema_diff_request())
1423
- .await
1424
- .expect("diff should use repaired root");
1425
-
1426
- assert_eq!(diff.entries.len(), 1);
1427
- assert_eq!(
1428
- diff.entries[0].kind,
1429
- crate::tracked_state::TrackedStateDiffKind::Modified
1430
- );
1431
- assert_eq!(
1432
- diff.entries[0]
1433
- .after
1434
- .as_ref()
1435
- .map(|row| row.change_id.as_str()),
1436
- Some("change-child")
1437
- );
1438
- }
1439
-
1440
- #[tokio::test]
1441
- async fn diff_allows_repaired_root_with_rebuilt_ancestor_chain() {
1442
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1443
- let tracked_state = TrackedStateContext::new();
1444
- write_root_for_test(
1445
- &storage,
1446
- &tracked_state,
1447
- "base",
1448
- None,
1449
- &[row_with_value("entity-a", "change-base", "base", "base")],
1450
- )
1451
- .await
1452
- .expect("base root should write");
1453
- write_root_for_test(
1454
- &storage,
1455
- &tracked_state,
1456
- "middle",
1457
- Some("base"),
1458
- &[row_with_value(
1459
- "entity-a",
1460
- "change-middle",
1461
- "middle",
1462
- "middle",
1463
- )],
1464
- )
1465
- .await
1466
- .expect("middle root should write");
1467
- write_root_for_test(
1468
- &storage,
1469
- &tracked_state,
1470
- "child",
1471
- Some("middle"),
1472
- &[row_with_value("entity-a", "change-child", "child", "child")],
1473
- )
1474
- .await
1475
- .expect("child root should write");
1476
- {
1477
- let mut writes = storage.new_write_set();
1478
- for commit_id in ["middle", "child"] {
1479
- writes.delete(
1480
- storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
1481
- crate::storage::StorageKey(bytes::Bytes::copy_from_slice(commit_id.as_bytes())),
1482
- );
1483
- }
1484
- storage
1485
- .commit_write_set(writes, StorageWriteOptions::default())
1486
- .expect("commit_root deletes should commit");
1487
- }
1488
-
1489
- let mut read = storage
1490
- .begin_read(StorageReadOptions::default())
1491
- .expect("read should open");
1492
- let mut writes = storage.new_write_set();
1493
- tracked_state
1494
- .root_rebuilder(&mut read, &mut writes)
1495
- .rebuild_commit_root_at("child")
1496
- .await
1497
- .expect("child root should repair");
1498
- storage
1499
- .commit_write_set(writes, StorageWriteOptions::default())
1500
- .expect("repaired root should commit");
1501
-
1502
- let diff = tracked_state
1503
- .reader(
1504
- storage
1505
- .begin_read(StorageReadOptions::default())
1506
- .expect("read should open"),
1507
- )
1508
- .diff_commits("base", "child", &test_schema_diff_request())
1509
- .await
1510
- .expect("diff should accept repaired nearest-ancestor parent metadata");
1511
-
1512
- assert_eq!(diff.entries.len(), 1);
1513
- assert_eq!(
1514
- diff.entries[0]
1515
- .after
1516
- .as_ref()
1517
- .map(|row| row.change_id.as_str()),
1518
- Some("change-child")
1519
- );
1520
- }
1521
-
1522
- #[tokio::test]
1523
- async fn explicit_rebuild_repairs_missing_ancestor_chain() {
1524
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1525
- let tracked_state = TrackedStateContext::new();
1526
- write_root_for_test(
1527
- &storage,
1528
- &tracked_state,
1529
- "base",
1530
- None,
1531
- &[row_with_value("entity-a", "change-base", "base", "base")],
1532
- )
1533
- .await
1534
- .expect("base root should write");
1535
- write_root_for_test(
1536
- &storage,
1537
- &tracked_state,
1538
- "middle",
1539
- Some("base"),
1540
- &[row_with_value(
1541
- "entity-a",
1542
- "change-middle",
1543
- "middle",
1544
- "middle",
1545
- )],
1546
- )
1547
- .await
1548
- .expect("middle root should write");
1549
- write_root_for_test(
1550
- &storage,
1551
- &tracked_state,
1552
- "child",
1553
- Some("middle"),
1554
- &[row_with_value("entity-a", "change-child", "child", "child")],
1555
- )
1556
- .await
1557
- .expect("child root should write");
1558
- {
1559
- let mut writes = storage.new_write_set();
1560
- for commit_id in ["middle", "child"] {
1561
- writes.delete(
1562
- storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
1563
- crate::storage::StorageKey(bytes::Bytes::copy_from_slice(commit_id.as_bytes())),
1564
- );
1565
- }
1566
- storage
1567
- .commit_write_set(writes, StorageWriteOptions::default())
1568
- .expect("commit_root deletes should commit");
1569
- }
1570
-
1571
- let read = storage
1572
- .begin_read(StorageReadOptions::default())
1573
- .expect("read should open");
1574
- let mut writes = storage.new_write_set();
1575
- tracked_state
1576
- .root_rebuilder(&read, &mut writes)
1577
- .rebuild_commit_root_at("child")
1578
- .await
1579
- .expect("explicit rebuild should repair missing ancestor chain");
1580
- storage
1581
- .commit_write_set(writes, StorageWriteOptions::default())
1582
- .expect("repaired roots should commit");
1583
-
1584
- let diff = tracked_state
1585
- .reader(
1586
- storage
1587
- .begin_read(StorageReadOptions::default())
1588
- .expect("read should open"),
1589
- )
1590
- .diff_commits("base", "child", &test_schema_diff_request())
1591
- .await
1592
- .expect("diff should accept explicitly rebuilt chain");
1593
-
1594
- assert_eq!(diff.entries.len(), 1);
1595
- assert_eq!(
1596
- diff.entries[0]
1597
- .after
1598
- .as_ref()
1599
- .map(|row| row.change_id.as_str()),
1600
- Some("change-child")
1601
- );
1602
- }
1603
-
1604
- #[tokio::test]
1605
- async fn explicit_rebuild_errors_on_first_parent_cycle() {
1606
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1607
- let tracked_state = TrackedStateContext::new();
1608
- {
1609
- let mut read = storage
1610
- .begin_read(StorageReadOptions::default())
1611
- .expect("read should open");
1612
- let mut writes = storage.new_write_set();
1613
- crate::test_support::stage_empty_changelog_commit(
1614
- &mut read,
1615
- &mut writes,
1616
- "commit-a",
1617
- None,
1618
- )
1619
- .await
1620
- .expect("commit-a should stage");
1621
- storage
1622
- .commit_write_set(writes, StorageWriteOptions::default())
1623
- .expect("commit-a should commit");
1624
- }
1625
- {
1626
- let mut read = storage
1627
- .begin_read(StorageReadOptions::default())
1628
- .expect("read should open");
1629
- let mut writes = storage.new_write_set();
1630
- crate::test_support::stage_empty_changelog_commit_with_parents(
1631
- &mut read,
1632
- &mut writes,
1633
- "commit-b",
1634
- &["commit-a".to_string()],
1635
- )
1636
- .await
1637
- .expect("commit-b should stage");
1638
- storage
1639
- .commit_write_set(writes, StorageWriteOptions::default())
1640
- .expect("commit-b should commit");
1641
- }
1642
- {
1643
- let mut writes = storage.new_write_set();
1644
- writes.put(
1645
- crate::changelog::COMMIT_SPACE,
1646
- crate::storage::StorageKey(bytes::Bytes::copy_from_slice(b"commit-a")),
1647
- crate::changelog::encode_commit_record(&crate::changelog::CommitRecord {
1648
- format_version: 1,
1649
- commit_id: "commit-a".to_string(),
1650
- parent_commit_ids: vec!["commit-b".to_string()],
1651
- change_id: "commit-a:commit".to_string(),
1652
- author_account_ids: Vec::new(),
1653
- created_at: "1970-01-01T00:00:00.000Z".to_string(),
1654
- })
1655
- .expect("corrupt cycle commit should encode"),
1656
- );
1657
- storage
1658
- .commit_write_set(writes, StorageWriteOptions::default())
1659
- .expect("cycle corruption should commit");
1660
- }
1661
-
1662
- let read = storage
1663
- .begin_read(StorageReadOptions::default())
1664
- .expect("read should open");
1665
- let mut writes = storage.new_write_set();
1666
- let error = tracked_state
1667
- .root_rebuilder(&read, &mut writes)
1668
- .rebuild_commit_root_at("commit-a")
1669
- .await
1670
- .expect_err("first-parent cycle should not rebuild forever");
1671
-
1672
- assert_eq!(error.code, LixError::CODE_INTERNAL_ERROR);
1673
- assert!(
1674
- error.message.contains("first-parent cycle"),
1675
- "unexpected error message: {}",
1676
- error.message
1677
- );
1678
- }
1679
-
1680
- #[tokio::test]
1681
- async fn explicit_rebuild_repairs_missing_head_root_chunk() {
1682
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1683
- let tracked_state = TrackedStateContext::new();
1684
- write_root_for_test(
1685
- &storage,
1686
- &tracked_state,
1687
- "base",
1688
- None,
1689
- &[row_with_value("entity-a", "change-base", "base", "base")],
1690
- )
1691
- .await
1692
- .expect("base root should write");
1693
- write_root_for_test(
1694
- &storage,
1695
- &tracked_state,
1696
- "child",
1697
- Some("base"),
1698
- &[row_with_value("entity-a", "change-child", "child", "child")],
1699
- )
1700
- .await
1701
- .expect("child root should write");
1702
- delete_root_chunk_for_test(&storage, "child").await;
1703
-
1704
- tracked_state
1705
- .reader(
1706
- storage
1707
- .begin_read(StorageReadOptions::default())
1708
- .expect("read should open"),
1709
- )
1710
- .diff_commits("base", "child", &test_schema_diff_request())
1711
- .await
1712
- .expect_err("diff should fail before missing root chunk repair");
1713
-
1714
- let read = storage
1715
- .begin_read(StorageReadOptions::default())
1716
- .expect("read should open");
1717
- let mut writes = storage.new_write_set();
1718
- tracked_state
1719
- .root_rebuilder(&read, &mut writes)
1720
- .rebuild_commit_root_at("child")
1721
- .await
1722
- .expect("child root chunk should repair");
1723
- storage
1724
- .commit_write_set(writes, StorageWriteOptions::default())
1725
- .expect("repaired root should commit");
1726
-
1727
- let diff = tracked_state
1728
- .reader(
1729
- storage
1730
- .begin_read(StorageReadOptions::default())
1731
- .expect("read should open"),
1732
- )
1733
- .diff_commits("base", "child", &test_schema_diff_request())
1734
- .await
1735
- .expect("diff should use repaired root chunk");
1736
-
1737
- assert_eq!(diff.entries.len(), 1);
1738
- assert_eq!(
1739
- diff.entries[0]
1740
- .after
1741
- .as_ref()
1742
- .map(|row| row.change_id.as_str()),
1743
- Some("change-child")
1744
- );
1745
- }
1746
-
1747
- #[tokio::test]
1748
- async fn explicit_rebuild_repairs_corrupt_head_root_chunk() {
1749
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1750
- let tracked_state = TrackedStateContext::new();
1751
- write_root_for_test(
1752
- &storage,
1753
- &tracked_state,
1754
- "base",
1755
- None,
1756
- &[row_with_value("entity-a", "change-base", "base", "base")],
1757
- )
1758
- .await
1759
- .expect("base root should write");
1760
- write_root_for_test(
1761
- &storage,
1762
- &tracked_state,
1763
- "child",
1764
- Some("base"),
1765
- &[row_with_value("entity-a", "change-child", "child", "child")],
1766
- )
1767
- .await
1768
- .expect("child root should write");
1769
- corrupt_root_chunk_for_test(&storage, "child").await;
1770
-
1771
- let read = storage
1772
- .begin_read(StorageReadOptions::default())
1773
- .expect("read should open");
1774
- let mut writes = storage.new_write_set();
1775
- tracked_state
1776
- .root_rebuilder(&read, &mut writes)
1777
- .rebuild_commit_root_at("child")
1778
- .await
1779
- .expect("corrupt child root chunk should repair");
1780
- storage
1781
- .commit_write_set(writes, StorageWriteOptions::default())
1782
- .expect("repaired root should commit");
1783
-
1784
- let diff = tracked_state
1785
- .reader(
1786
- storage
1787
- .begin_read(StorageReadOptions::default())
1788
- .expect("read should open"),
1789
- )
1790
- .diff_commits("base", "child", &test_schema_diff_request())
1791
- .await
1792
- .expect("diff should use repaired root chunk");
1793
-
1794
- assert_eq!(diff.entries.len(), 1);
1795
- assert_eq!(
1796
- diff.entries[0]
1797
- .after
1798
- .as_ref()
1799
- .map(|row| row.change_id.as_str()),
1800
- Some("change-child")
1801
- );
1802
- }
1803
-
1804
- #[tokio::test]
1805
- async fn explicit_rebuild_repairs_stale_root_missing_commit_row() {
1806
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1807
- let tracked_state = TrackedStateContext::new();
1808
- let row = row_with_value("entity-a", "change-a", "commit-1", "value");
1809
- write_root_for_test(
1810
- &storage,
1811
- &tracked_state,
1812
- "commit-1",
1813
- None,
1814
- std::slice::from_ref(&row),
1815
- )
1816
- .await
1817
- .expect("root should write");
1818
- overwrite_root_without_commit_row_for_test(
1819
- &storage,
1820
- "commit-1",
1821
- std::slice::from_ref(&row),
1822
- )
1823
- .await;
1824
-
1825
- let read = storage
1826
- .begin_read(StorageReadOptions::default())
1827
- .expect("read should open");
1828
- let mut writes = storage.new_write_set();
1829
- tracked_state
1830
- .root_rebuilder(&read, &mut writes)
1831
- .rebuild_commit_root_at("commit-1")
1832
- .await
1833
- .expect("stale root should repair");
1834
- storage
1835
- .commit_write_set(writes, StorageWriteOptions::default())
1836
- .expect("repaired root should commit");
1837
-
1838
- let rows = tracked_state
1839
- .reader(
1840
- storage
1841
- .begin_read(StorageReadOptions::default())
1842
- .expect("read should open"),
1843
- )
1844
- .scan_rows_at_commit(
1845
- "commit-1",
1846
- &TrackedStateScanRequest {
1847
- filter: crate::tracked_state::TrackedStateFilter {
1848
- schema_keys: vec!["lix_commit".to_string()],
1849
- ..Default::default()
1850
- },
1851
- ..Default::default()
1852
- },
1853
- )
1854
- .await
1855
- .expect("repaired root should scan");
1856
- assert_eq!(rows.len(), 1);
1857
- assert_eq!(rows[0].schema_key, "lix_commit");
1858
- }
1859
-
1860
- #[tokio::test]
1861
- async fn explicit_rebuild_repairs_stale_root_missing_inherited_row() {
1862
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1863
- let tracked_state = TrackedStateContext::new();
1864
- let inherited = row_with_value("entity-a", "change-base", "base", "base");
1865
- let child = row_with_value("entity-b", "change-child", "child", "child");
1866
- write_root_for_test(
1867
- &storage,
1868
- &tracked_state,
1869
- "base",
1870
- None,
1871
- std::slice::from_ref(&inherited),
1872
- )
1873
- .await
1874
- .expect("base root should write");
1875
- write_root_for_test(
1876
- &storage,
1877
- &tracked_state,
1878
- "child",
1879
- Some("base"),
1880
- std::slice::from_ref(&child),
1881
- )
1882
- .await
1883
- .expect("child root should write");
1884
- overwrite_root_without_commit_row_for_test(&storage, "child", std::slice::from_ref(&child))
1885
- .await;
1886
-
1887
- let read = storage
1888
- .begin_read(StorageReadOptions::default())
1889
- .expect("read should open");
1890
- let mut writes = storage.new_write_set();
1891
- tracked_state
1892
- .root_rebuilder(&read, &mut writes)
1893
- .rebuild_commit_root_at("child")
1894
- .await
1895
- .expect("stale child root should repair");
1896
- storage
1897
- .commit_write_set(writes, StorageWriteOptions::default())
1898
- .expect("repaired root should commit");
1899
-
1900
- let rows = tracked_state
1901
- .reader(
1902
- storage
1903
- .begin_read(StorageReadOptions::default())
1904
- .expect("read should open"),
1905
- )
1906
- .scan_rows_at_commit("child", &test_schema_scan_request())
1907
- .await
1908
- .expect("repaired child root should scan");
1909
- assert_eq!(
1910
- rows.iter()
1911
- .map(|row| row.change_id.as_str())
1912
- .collect::<Vec<_>>(),
1913
- vec!["change-base", "change-child"]
1914
- );
1915
- }
1916
-
1917
- #[tokio::test]
1918
- async fn scan_rows_filters_by_file() {
1919
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1920
- let tracked_state = TrackedStateContext::new();
1921
- let mut file_a = row("entity-a", "change-a", "commit-1");
1922
- file_a.file_id = Some("file-a.json".to_string());
1923
- let mut file_b = row("entity-b", "change-b", "commit-1");
1924
- file_b.file_id = Some("file-b.json".to_string());
1925
- write_root_for_test(
1926
- &storage,
1927
- &tracked_state,
1928
- "commit-1",
1929
- None,
1930
- &[file_a, file_b],
1931
- )
1932
- .await
1933
- .expect("root should write");
1934
-
1935
- let rows = tracked_state
1936
- .reader(
1937
- storage
1938
- .begin_read(StorageReadOptions::default())
1939
- .expect("read should open"),
1940
- )
1941
- .scan_rows_at_commit(
1942
- "commit-1",
1943
- &TrackedStateScanRequest {
1944
- filter: crate::tracked_state::TrackedStateFilter {
1945
- file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
1946
- ..Default::default()
1947
- },
1948
- ..Default::default()
1949
- },
1950
- )
1951
- .await
1952
- .expect("file scan should use primary root");
1953
-
1954
- assert_eq!(rows.len(), 1);
1955
- assert_eq!(
1956
- rows[0]
1957
- .entity_pk
1958
- .as_single_string_owned()
1959
- .expect("entity pk"),
1960
- "entity-a"
1961
- );
1962
- assert_eq!(rows[0].file_id.as_deref(), Some("file-a.json"));
1963
- }
1964
-
1965
- #[tokio::test]
1966
- async fn file_filtered_header_scan_fetches_primary_payload_only_when_requested() {
1967
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1968
- let tracked_state = TrackedStateContext::new();
1969
- let mut row = row("entity-a", "change-a", "commit-1");
1970
- row.file_id = Some("file-a.json".to_string());
1971
- let expected_snapshot = row.snapshot_content.clone();
1972
- write_root_for_test(
1973
- &storage,
1974
- &tracked_state,
1975
- "commit-1",
1976
- None,
1977
- std::slice::from_ref(&row),
1978
- )
1979
- .await
1980
- .expect("root should write");
1981
-
1982
- let mut reader = tracked_state.reader(
1983
- storage
1984
- .begin_read(StorageReadOptions::default())
1985
- .expect("read should open"),
1986
- );
1987
- let header_rows = reader
1988
- .scan_rows_at_commit(
1989
- "commit-1",
1990
- &TrackedStateScanRequest {
1991
- filter: crate::tracked_state::TrackedStateFilter {
1992
- file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
1993
- ..Default::default()
1994
- },
1995
- read_columns: crate::tracked_state::TrackedStateReadColumns {
1996
- columns: vec!["entity_pk".to_string()],
1997
- },
1998
- ..Default::default()
1999
- },
2000
- )
2001
- .await
2002
- .expect("header scan should use primary root");
2003
- let full_rows = reader
2004
- .scan_rows_at_commit(
2005
- "commit-1",
2006
- &TrackedStateScanRequest {
2007
- filter: crate::tracked_state::TrackedStateFilter {
2008
- file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
2009
- ..Default::default()
2010
- },
2011
- ..Default::default()
2012
- },
2013
- )
2014
- .await
2015
- .expect("full scan should fetch primary payload");
2016
-
2017
- assert_eq!(header_rows[0].snapshot_content, None);
2018
- assert_eq!(full_rows[0].snapshot_content, expected_snapshot);
2019
- }
2020
-
2021
- #[tokio::test]
2022
- async fn null_file_rows_match_null_file_filter() {
2023
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2024
- let tracked_state = TrackedStateContext::new();
2025
- let row = row("entity-a", "change-a", "commit-1");
2026
- write_root_for_test(
2027
- &storage,
2028
- &tracked_state,
2029
- "commit-1",
2030
- None,
2031
- std::slice::from_ref(&row),
2032
- )
2033
- .await
2034
- .expect("root should write");
2035
-
2036
- let rows = tracked_state
2037
- .reader(
2038
- storage
2039
- .begin_read(StorageReadOptions::default())
2040
- .expect("read should open"),
2041
- )
2042
- .scan_rows_at_commit(
2043
- "commit-1",
2044
- &TrackedStateScanRequest {
2045
- filter: crate::tracked_state::TrackedStateFilter {
2046
- schema_keys: vec!["test_schema".to_string()],
2047
- file_ids: vec![NullableKeyFilter::Null],
2048
- ..Default::default()
2049
- },
2050
- ..Default::default()
2051
- },
2052
- )
2053
- .await
2054
- .expect("null file scan should use primary tree");
2055
-
2056
- assert_eq!(rows.len(), 1);
2057
- assert_eq!(
2058
- rows[0]
2059
- .entity_pk
2060
- .as_single_string_owned()
2061
- .expect("entity pk"),
2062
- "entity-a"
2063
- );
2064
- }
2065
-
2066
- #[tokio::test]
2067
- async fn mixed_null_and_concrete_file_scan_uses_primary_tree() {
2068
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2069
- let tracked_state = TrackedStateContext::new();
2070
- let null_row = row("entity-null", "change-null", "commit-1");
2071
- let mut file_row = row("entity-file", "change-file", "commit-2");
2072
- file_row.file_id = Some("file-a.json".to_string());
2073
- write_root_for_test(
2074
- &storage,
2075
- &tracked_state,
2076
- "commit-1",
2077
- None,
2078
- std::slice::from_ref(&null_row),
2079
- )
2080
- .await
2081
- .expect("parent root should write");
2082
- write_root_for_test(
2083
- &storage,
2084
- &tracked_state,
2085
- "commit-2",
2086
- Some("commit-1"),
2087
- std::slice::from_ref(&file_row),
2088
- )
2089
- .await
2090
- .expect("child root should write");
2091
-
2092
- let rows = tracked_state
2093
- .reader(
2094
- storage
2095
- .begin_read(StorageReadOptions::default())
2096
- .expect("read should open"),
2097
- )
2098
- .scan_rows_at_commit(
2099
- "commit-2",
2100
- &TrackedStateScanRequest {
2101
- filter: crate::tracked_state::TrackedStateFilter {
2102
- schema_keys: vec!["test_schema".to_string()],
2103
- file_ids: vec![
2104
- NullableKeyFilter::Null,
2105
- NullableKeyFilter::Value("file-a.json".to_string()),
2106
- ],
2107
- ..Default::default()
2108
- },
2109
- ..Default::default()
2110
- },
2111
- )
2112
- .await
2113
- .expect("mixed scan should use primary tree");
2114
-
2115
- let mut entity_pks = rows
2116
- .iter()
2117
- .map(|row| row.entity_pk.as_single_string_owned().expect("entity pk"))
2118
- .collect::<Vec<_>>();
2119
- entity_pks.sort();
2120
- assert_eq!(entity_pks, vec!["entity-file", "entity-null"]);
2121
- }
2122
-
2123
- #[tokio::test]
2124
- async fn file_filtered_header_scan_filters_tombstones_without_payload_sentinel() {
2125
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2126
- let tracked_state = TrackedStateContext::new();
2127
- let mut live = row("entity-live", "change-live", "commit-1");
2128
- live.file_id = Some("file-a.json".to_string());
2129
- let mut deleted = tombstone("entity-deleted", "change-delete", "commit-1");
2130
- deleted.file_id = Some("file-a.json".to_string());
2131
- write_root_for_test(&storage, &tracked_state, "commit-1", None, &[live, deleted])
2132
- .await
2133
- .expect("root should write");
2134
-
2135
- let rows = tracked_state
2136
- .reader(
2137
- storage
2138
- .begin_read(StorageReadOptions::default())
2139
- .expect("read should open"),
2140
- )
2141
- .scan_rows_at_commit(
2142
- "commit-1",
2143
- &TrackedStateScanRequest {
2144
- filter: crate::tracked_state::TrackedStateFilter {
2145
- file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
2146
- ..Default::default()
2147
- },
2148
- read_columns: crate::tracked_state::TrackedStateReadColumns {
2149
- columns: vec!["entity_pk".to_string()],
2150
- },
2151
- ..Default::default()
2152
- },
2153
- )
2154
- .await
2155
- .expect("file scan should use primary root");
2156
-
2157
- assert_eq!(rows.len(), 1);
2158
- assert_eq!(
2159
- rows[0]
2160
- .entity_pk
2161
- .as_single_string_owned()
2162
- .expect("entity pk"),
2163
- "entity-live"
2164
- );
2165
- }
2166
-
2167
- #[tokio::test]
2168
- async fn child_root_tombstone_hides_materialized_base_row() {
2169
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2170
- let tracked_state = TrackedStateContext::new();
2171
- let base = row("entity-a", "change-base", "base");
2172
- let delete = tombstone("entity-a", "change-delete", "child");
2173
- write_root_for_test(
2174
- &storage,
2175
- &tracked_state,
2176
- "base",
2177
- None,
2178
- std::slice::from_ref(&base),
2179
- )
2180
- .await
2181
- .expect("base root should write");
2182
- let read = storage
2183
- .begin_read(StorageReadOptions::default())
2184
- .expect("read should open");
2185
- let mut writes = storage.new_write_set();
2186
- tracked_state
2187
- .root_rebuilder(&read, &mut writes)
2188
- .rebuild_commit_root_at("base")
2189
- .await
2190
- .expect("base commit root should materialize");
2191
- storage
2192
- .commit_write_set(writes, StorageWriteOptions::default())
2193
- .expect("materialized base should commit");
2194
- write_root_for_test(
2195
- &storage,
2196
- &tracked_state,
2197
- "child",
2198
- Some("base"),
2199
- std::slice::from_ref(&delete),
2200
- )
2201
- .await
2202
- .expect("child tombstone root should write");
2203
-
2204
- let rows = tracked_state
2205
- .reader(
2206
- storage
2207
- .begin_read(StorageReadOptions::default())
2208
- .expect("read should open"),
2209
- )
2210
- .scan_rows_at_commit("child", &test_schema_scan_request())
2211
- .await
2212
- .expect("child scan should apply tombstone over base root");
2213
-
2214
- assert!(rows.is_empty(), "pending tombstone must hide base row");
2215
- }
2216
-
2217
- #[tokio::test]
2218
- async fn root_scan_keeps_last_mutation_for_duplicate_key() {
2219
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2220
- let tracked_state = TrackedStateContext::new();
2221
- write_root_for_test(
2222
- &storage,
2223
- &tracked_state,
2224
- "commit-1",
2225
- None,
2226
- &[
2227
- row_with_value("entity-a", "change-a1", "commit-1", "first"),
2228
- row_with_value("entity-b", "change-b", "commit-1", "middle"),
2229
- row_with_value("entity-a", "change-a2", "commit-1", "second"),
2230
- tombstone("entity-c", "change-c1", "commit-1"),
2231
- ],
2232
- )
2233
- .await
2234
- .expect("root should write");
2235
-
2236
- let rows = tracked_state
2237
- .reader(
2238
- storage
2239
- .begin_read(StorageReadOptions::default())
2240
- .expect("read should open"),
2241
- )
2242
- .scan_rows_at_commit("commit-1", &test_schema_scan_request())
2243
- .await
2244
- .expect("root should scan");
2245
-
2246
- assert_eq!(rows.len(), 2);
2247
- assert_eq!(
2248
- rows.iter()
2249
- .map(|row| (
2250
- row.entity_pk.as_single_string_owned().expect("entity pk"),
2251
- row.snapshot_content.clone()
2252
- ))
2253
- .collect::<Vec<_>>(),
2254
- vec![
2255
- (
2256
- "entity-a".to_string(),
2257
- Some("{\"value\":\"second\"}".to_string())
2258
- ),
2259
- (
2260
- "entity-b".to_string(),
2261
- Some("{\"value\":\"middle\"}".to_string())
2262
- ),
2263
- ]
2264
- );
2265
- }
2266
-
2267
- #[tokio::test]
2268
- async fn scan_limit_applies_after_tombstone_visibility() {
2269
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2270
- let tracked_state = TrackedStateContext::new();
2271
- write_root_for_test(
2272
- &storage,
2273
- &tracked_state,
2274
- "commit-1",
2275
- None,
2276
- &[
2277
- tombstone("entity-a", "change-delete", "commit-1"),
2278
- row("entity-b", "change-live", "commit-1"),
2279
- ],
2280
- )
2281
- .await
2282
- .expect("root should write");
2283
-
2284
- let rows = tracked_state
2285
- .reader(
2286
- storage
2287
- .begin_read(StorageReadOptions::default())
2288
- .expect("read should open"),
2289
- )
2290
- .scan_rows_at_commit(
2291
- "commit-1",
2292
- &TrackedStateScanRequest {
2293
- filter: crate::tracked_state::TrackedStateFilter {
2294
- schema_keys: vec!["test_schema".to_string()],
2295
- ..Default::default()
2296
- },
2297
- limit: Some(1),
2298
- ..Default::default()
2299
- },
2300
- )
2301
- .await
2302
- .expect("limited scan should apply visibility before limit");
2303
-
2304
- assert_eq!(rows.len(), 1);
2305
- assert_eq!(
2306
- rows[0]
2307
- .entity_pk
2308
- .as_single_string_owned()
2309
- .expect("entity pk"),
2310
- "entity-b"
2311
- );
2312
- }
2313
-
2314
- #[tokio::test]
2315
- async fn file_filtered_scan_limit_applies_after_tombstone_visibility() {
2316
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2317
- let tracked_state = TrackedStateContext::new();
2318
- let mut deleted = tombstone("entity-a", "change-delete", "commit-1");
2319
- deleted.file_id = Some("file-a.json".to_string());
2320
- let mut live = row("entity-b", "change-live", "commit-1");
2321
- live.file_id = Some("file-a.json".to_string());
2322
- write_root_for_test(&storage, &tracked_state, "commit-1", None, &[deleted, live])
2323
- .await
2324
- .expect("root should write");
2325
-
2326
- let rows = tracked_state
2327
- .reader(
2328
- storage
2329
- .begin_read(StorageReadOptions::default())
2330
- .expect("read should open"),
2331
- )
2332
- .scan_rows_at_commit(
2333
- "commit-1",
2334
- &TrackedStateScanRequest {
2335
- filter: crate::tracked_state::TrackedStateFilter {
2336
- file_ids: vec![NullableKeyFilter::Value("file-a.json".to_string())],
2337
- ..Default::default()
2338
- },
2339
- read_columns: crate::tracked_state::TrackedStateReadColumns {
2340
- columns: vec!["entity_pk".to_string()],
2341
- },
2342
- limit: Some(1),
2343
- },
2344
- )
2345
- .await
2346
- .expect("limited file scan should apply visibility before limit");
2347
-
2348
- assert_eq!(rows.len(), 1);
2349
- assert_eq!(
2350
- rows[0]
2351
- .entity_pk
2352
- .as_single_string_owned()
2353
- .expect("entity pk"),
2354
- "entity-b"
2355
- );
2356
- }
2357
-
2358
- #[tokio::test]
2359
- async fn reads_resolve_json_snapshot_refs() {
2360
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2361
- let tracked_state = TrackedStateContext::new();
2362
- let large_value = "x".repeat(1536);
2363
- let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
2364
- write_root_for_test(
2365
- &storage,
2366
- &tracked_state,
2367
- "commit-1",
2368
- None,
2369
- std::slice::from_ref(&row),
2370
- )
2371
- .await
2372
- .expect("root should write");
2373
-
2374
- let mut reader = tracked_state.reader(
2375
- storage
2376
- .begin_read(StorageReadOptions::default())
2377
- .expect("read should open"),
2378
- );
2379
- let loaded = reader
2380
- .load_rows_at_commit(
2381
- "commit-1",
2382
- &[TrackedStateKey {
2383
- schema_key: row.schema_key.clone(),
2384
- entity_pk: row.entity_pk.clone(),
2385
- file_id: None,
2386
- }],
2387
- )
2388
- .await
2389
- .expect("row should load")
2390
- .pop()
2391
- .flatten()
2392
- .expect("row should exist");
2393
- let scanned = reader
2394
- .scan_rows_at_commit("commit-1", &test_schema_scan_request())
2395
- .await
2396
- .expect("rows should scan");
2397
-
2398
- assert_eq!(loaded.snapshot_content, row.snapshot_content);
2399
- assert_eq!(scanned[0].snapshot_content, row.snapshot_content);
2400
- }
2401
-
2402
- #[tokio::test]
2403
- async fn commit_root_cache_uses_seen_updated_at_not_change_created_at() {
2404
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2405
- let tracked_state = TrackedStateContext::new();
2406
- let mut row = row("entity-a", "change-a", "commit-1");
2407
- row.created_at = "2026-01-01T00:00:00Z".to_string();
2408
- row.updated_at = "2026-01-02T00:00:00Z".to_string();
2409
- write_root_for_test(
2410
- &storage,
2411
- &tracked_state,
2412
- "commit-1",
2413
- None,
2414
- std::slice::from_ref(&row),
2415
- )
2416
- .await
2417
- .expect("root should write");
2418
-
2419
- let loaded = tracked_state
2420
- .reader(
2421
- storage
2422
- .begin_read(StorageReadOptions::default())
2423
- .expect("read should open"),
2424
- )
2425
- .load_rows_at_commit(
2426
- "commit-1",
2427
- &[TrackedStateKey {
2428
- schema_key: row.schema_key.clone(),
2429
- entity_pk: row.entity_pk.clone(),
2430
- file_id: None,
2431
- }],
2432
- )
2433
- .await
2434
- .expect("row should load")
2435
- .pop()
2436
- .flatten()
2437
- .expect("row should exist");
2438
-
2439
- assert_eq!(loaded.created_at, "2026-01-01T00:00:00Z");
2440
- assert_eq!(loaded.updated_at, "2026-01-02T00:00:00Z");
2441
- }
2442
-
2443
- #[tokio::test]
2444
- async fn updates_preserve_first_visible_created_at_across_rebuild() {
2445
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2446
- let tracked_state = TrackedStateContext::new();
2447
- let mut parent = row("entity-a", "change-parent", "parent");
2448
- parent.created_at = "2026-01-01T00:00:00Z".to_string();
2449
- parent.updated_at = "2026-01-01T00:00:00Z".to_string();
2450
- write_root_for_test(
2451
- &storage,
2452
- &tracked_state,
2453
- "parent",
2454
- None,
2455
- std::slice::from_ref(&parent),
2456
- )
2457
- .await
2458
- .expect("parent root should write");
2459
-
2460
- let mut child = row("entity-a", "change-child", "child");
2461
- child.created_at = "2026-01-02T00:00:00Z".to_string();
2462
- child.updated_at = "2026-01-03T00:00:00Z".to_string();
2463
- write_root_for_test(
2464
- &storage,
2465
- &tracked_state,
2466
- "child",
2467
- Some("parent"),
2468
- std::slice::from_ref(&child),
2469
- )
2470
- .await
2471
- .expect("child root should write");
2472
-
2473
- let key = TrackedStateKey {
2474
- schema_key: child.schema_key.clone(),
2475
- file_id: child.file_id.clone(),
2476
- entity_pk: child.entity_pk.clone(),
2477
- };
2478
- let loaded = tracked_state
2479
- .reader(
2480
- storage
2481
- .begin_read(StorageReadOptions::default())
2482
- .expect("read should open"),
2483
- )
2484
- .load_rows_at_commit("child", std::slice::from_ref(&key))
2485
- .await
2486
- .expect("child row should load")
2487
- .pop()
2488
- .flatten()
2489
- .expect("child row should exist");
2490
- assert_eq!(loaded.created_at, "2026-01-01T00:00:00Z");
2491
- assert_eq!(loaded.updated_at, "2026-01-03T00:00:00Z");
2492
-
2493
- {
2494
- let mut writes = storage.new_write_set();
2495
- writes.delete(
2496
- storage::TRACKED_STATE_COMMIT_ROOT_SPACE,
2497
- crate::storage::StorageKey(bytes::Bytes::copy_from_slice(b"child")),
2498
- );
2499
- storage
2500
- .commit_write_set(writes, StorageWriteOptions::default())
2501
- .expect("child root delete should commit");
2502
- }
2503
- {
2504
- let read = storage
2505
- .begin_read(StorageReadOptions::default())
2506
- .expect("read should open");
2507
- let mut writes = storage.new_write_set();
2508
- tracked_state
2509
- .root_rebuilder(&read, &mut writes)
2510
- .rebuild_commit_root_at("child")
2511
- .await
2512
- .expect("child root should rebuild");
2513
- storage
2514
- .commit_write_set(writes, StorageWriteOptions::default())
2515
- .expect("rebuilt child root should commit");
2516
- }
2517
-
2518
- let rebuilt = tracked_state
2519
- .reader(
2520
- storage
2521
- .begin_read(StorageReadOptions::default())
2522
- .expect("read should open"),
2523
- )
2524
- .load_rows_at_commit("child", &[key])
2525
- .await
2526
- .expect("rebuilt child row should load")
2527
- .pop()
2528
- .flatten()
2529
- .expect("rebuilt child row should exist");
2530
- assert_eq!(rebuilt.created_at, "2026-01-01T00:00:00Z");
2531
- assert_eq!(rebuilt.updated_at, "2026-01-03T00:00:00Z");
2532
- }
2533
-
2534
- #[tokio::test]
2535
- async fn selected_column_scans_do_not_materialize_snapshot_when_snapshot_content_is_omitted() {
2536
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2537
- let tracked_state = TrackedStateContext::new();
2538
- let large_value = "x".repeat(1536);
2539
- let row = row_with_value("entity-a", "change-a", "commit-1", &large_value);
2540
- write_root_for_test(
2541
- &storage,
2542
- &tracked_state,
2543
- "commit-1",
2544
- None,
2545
- std::slice::from_ref(&row),
2546
- )
2547
- .await
2548
- .expect("root should write");
2549
-
2550
- let rows = tracked_state
2551
- .reader(
2552
- storage
2553
- .begin_read(StorageReadOptions::default())
2554
- .expect("read should open"),
2555
- )
2556
- .scan_rows_at_commit(
2557
- "commit-1",
2558
- &TrackedStateScanRequest {
2559
- filter: crate::tracked_state::TrackedStateFilter {
2560
- schema_keys: vec!["test_schema".to_string()],
2561
- ..Default::default()
2562
- },
2563
- read_columns: crate::tracked_state::TrackedStateReadColumns {
2564
- columns: vec!["entity_pk".to_string()],
2565
- },
2566
- ..Default::default()
2567
- },
2568
- )
2569
- .await
2570
- .expect("rows should scan");
2571
-
2572
- assert_eq!(rows.len(), 1);
2573
- assert_eq!(rows[0].snapshot_content, None);
2574
- }
2575
-
2576
- async fn seed_merge_roots(
2577
- base_rows: &[MaterializedTrackedStateRow],
2578
- target_rows: &[MaterializedTrackedStateRow],
2579
- source_rows: &[MaterializedTrackedStateRow],
2580
- ) -> (StorageContext, TrackedStateContext) {
2581
- let storage = StorageContext::new(InMemoryStorageBackend::new());
2582
- let tracked_state = TrackedStateContext::new();
2583
- write_root_for_test(&storage, &tracked_state, "base", None, base_rows)
2584
- .await
2585
- .expect("base root should write");
2586
- write_root_for_test(
2587
- &storage,
2588
- &tracked_state,
2589
- "target",
2590
- Some("base"),
2591
- target_rows,
2592
- )
2593
- .await
2594
- .expect("target root should write");
2595
- write_root_for_test(
2596
- &storage,
2597
- &tracked_state,
2598
- "source",
2599
- Some("base"),
2600
- source_rows,
2601
- )
2602
- .await
2603
- .expect("source root should write");
2604
- (storage, tracked_state)
2605
- }
2606
-
2607
- fn merge_pick_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
2608
- plan.picks
2609
- .iter()
2610
- .map(|entry| {
2611
- entry
2612
- .identity()
2613
- .entity_pk
2614
- .as_single_string_owned()
2615
- .expect("identity")
2616
- })
2617
- .collect()
2618
- }
2619
-
2620
- fn merge_conflict_ids(plan: &TrackedStateMergePlan) -> Vec<String> {
2621
- plan.conflicts
2622
- .iter()
2623
- .map(|entry| {
2624
- entry
2625
- .identity
2626
- .entity_pk
2627
- .as_single_string_owned()
2628
- .expect("identity")
2629
- })
2630
- .collect()
2631
- }
2632
-
2633
- async fn write_root_for_test(
2634
- storage: &StorageContext,
2635
- tracked_state: &TrackedStateContext,
2636
- commit_id: &str,
2637
- parent_commit_id: Option<&str>,
2638
- rows: &[MaterializedTrackedStateRow],
2639
- ) -> Result<(), LixError> {
2640
- let mut read = storage
2641
- .begin_read(StorageReadOptions::default())
2642
- .expect("read should open");
2643
- let mut writes = storage.new_write_set();
2644
- crate::test_support::stage_tracked_root_from_materialized(
2645
- &mut read,
2646
- &mut writes,
2647
- tracked_state,
2648
- commit_id,
2649
- parent_commit_id,
2650
- rows,
2651
- )
2652
- .await?;
2653
- storage.commit_write_set(writes, StorageWriteOptions::default())?;
2654
- Ok(())
2655
- }
2656
-
2657
- async fn delete_root_chunk_for_test(storage: &StorageContext, commit_id: &str) {
2658
- let read = storage
2659
- .begin_read(StorageReadOptions::default())
2660
- .expect("read should open");
2661
- let root_id = storage::load_root(&read, commit_id)
2662
- .await
2663
- .expect("root metadata should load")
2664
- .expect("root metadata should exist");
2665
- let mut writes = storage.new_write_set();
2666
- writes.delete(
2667
- storage::TRACKED_STATE_TREE_CHUNK_SPACE,
2668
- crate::storage::StorageKey(bytes::Bytes::copy_from_slice(root_id.as_bytes())),
2669
- );
2670
- storage
2671
- .commit_write_set(writes, StorageWriteOptions::default())
2672
- .expect("root chunk delete should commit");
2673
- }
2674
-
2675
- async fn corrupt_root_chunk_for_test(storage: &StorageContext, commit_id: &str) {
2676
- let read = storage
2677
- .begin_read(StorageReadOptions::default())
2678
- .expect("read should open");
2679
- let root_id = storage::load_root(&read, commit_id)
2680
- .await
2681
- .expect("root metadata should load")
2682
- .expect("root metadata should exist");
2683
- let mut writes = storage.new_write_set();
2684
- writes.put(
2685
- storage::TRACKED_STATE_TREE_CHUNK_SPACE,
2686
- crate::storage::StorageKey(bytes::Bytes::copy_from_slice(root_id.as_bytes())),
2687
- b"corrupt tracked-state root chunk".as_slice(),
2688
- );
2689
- storage
2690
- .commit_write_set(writes, StorageWriteOptions::default())
2691
- .expect("root chunk corruption should commit");
2692
- }
2693
-
2694
- async fn overwrite_root_without_commit_row_for_test(
2695
- storage: &StorageContext,
2696
- commit_id: &str,
2697
- rows: &[MaterializedTrackedStateRow],
2698
- ) {
2699
- let read = storage
2700
- .begin_read(StorageReadOptions::default())
2701
- .expect("read should open");
2702
- let mut writes = storage.new_write_set();
2703
- let mutations =
2704
- rows.iter()
2705
- .map(|row| {
2706
- let key = TrackedStateKey {
2707
- schema_key: row.schema_key.clone(),
2708
- file_id: row.file_id.clone(),
2709
- entity_pk: row.entity_pk.clone(),
2710
- };
2711
- let value = TrackedStateIndexValue {
2712
- change_id: row.change_id.clone(),
2713
- commit_id: row.commit_id.clone(),
2714
- deleted: row.deleted,
2715
- snapshot_ref: row.snapshot_content.as_ref().map(|content| {
2716
- crate::json_store::JsonRef::for_content(content.as_bytes())
2717
- }),
2718
- metadata_ref: row.metadata.as_ref().map(|metadata| {
2719
- crate::json_store::JsonRef::for_content(metadata.as_bytes())
2720
- }),
2721
- created_at: row.created_at.clone(),
2722
- updated_at: row.updated_at.clone(),
2723
- };
2724
- TrackedStateMutation::put_encoded(
2725
- crate::tracked_state::codec::encode_key(&key),
2726
- crate::tracked_state::codec::encode_value(&value),
2727
- )
2728
- })
2729
- .collect::<Vec<_>>();
2730
- let result = TrackedStateTree::new()
2731
- .apply_mutations(&read, &mut writes, None, mutations, Some(commit_id))
2732
- .await
2733
- .expect("stale root should write");
2734
- storage::stage_commit_root(
2735
- &mut writes,
2736
- &TrackedStateCommitRoot {
2737
- commit_id: commit_id.to_string(),
2738
- root_id: result.root_id,
2739
- parent_roots: Vec::new(),
2740
- changed_key_count: rows.len() as u64,
2741
- row_count_estimate: result.row_count as u64,
2742
- tree_height: result.tree_height as u32,
2743
- primary_chunk_count: result.chunk_count as u64,
2744
- primary_chunk_bytes: result.chunk_bytes as u64,
2745
- },
2746
- )
2747
- .expect("stale metadata should encode");
2748
- storage
2749
- .commit_write_set(writes, StorageWriteOptions::default())
2750
- .expect("stale root overwrite should commit");
2751
- }
2752
-
2753
- fn test_schema_scan_request() -> TrackedStateScanRequest {
2754
- TrackedStateScanRequest {
2755
- filter: crate::tracked_state::TrackedStateFilter {
2756
- schema_keys: vec!["test_schema".to_string()],
2757
- ..Default::default()
2758
- },
2759
- ..Default::default()
2760
- }
2761
- }
2762
-
2763
- fn test_schema_diff_request() -> TrackedStateDiffRequest {
2764
- TrackedStateDiffRequest {
2765
- filter: crate::tracked_state::TrackedStateFilter {
2766
- schema_keys: vec!["test_schema".to_string()],
2767
- ..Default::default()
2768
- },
2769
- }
2770
- }
2771
-
2772
- fn tombstone(entity_pk: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
2773
- let mut row = row(entity_pk, change_id, commit_id);
2774
- row.snapshot_content = None;
2775
- row
2776
- }
2777
-
2778
- fn row(entity_pk: &str, change_id: &str, commit_id: &str) -> MaterializedTrackedStateRow {
2779
- row_with_value(entity_pk, change_id, commit_id, "value")
2780
- }
2781
-
2782
- fn row_with_value(
2783
- entity_pk: &str,
2784
- change_id: &str,
2785
- commit_id: &str,
2786
- value: &str,
2787
- ) -> MaterializedTrackedStateRow {
2788
- MaterializedTrackedStateRow {
2789
- entity_pk: crate::entity_pk::EntityPk::single(entity_pk),
2790
- schema_key: "test_schema".to_string(),
2791
- file_id: None,
2792
- snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
2793
- metadata: None,
2794
- deleted: false,
2795
- created_at: "2026-01-01T00:00:00Z".to_string(),
2796
- updated_at: "2026-01-01T00:00:00Z".to_string(),
2797
- change_id: change_id.to_string(),
2798
- commit_id: commit_id.to_string(),
2799
- }
2800
- }
2801
- }