@lix-js/sdk 0.6.0-preview.4 → 0.6.0

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