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