@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.3

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 (205) hide show
  1. package/SKILL.md +304 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/generated/builtin-schemas.d.ts +87 -162
  7. package/dist/generated/builtin-schemas.js +139 -236
  8. package/dist/open-lix.d.ts +103 -14
  9. package/dist/open-lix.js +3 -0
  10. package/dist/sqlite/index.js +99 -22
  11. package/dist-engine-src/README.md +18 -0
  12. package/dist-engine-src/src/backend/kv.rs +358 -0
  13. package/dist-engine-src/src/backend/mod.rs +12 -0
  14. package/dist-engine-src/src/backend/testing.rs +658 -0
  15. package/dist-engine-src/src/backend/types.rs +96 -0
  16. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  17. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  18. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  19. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  20. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  21. package/dist-engine-src/src/binary_cas/types.rs +121 -0
  22. package/dist-engine-src/src/catalog/context.rs +412 -0
  23. package/dist-engine-src/src/catalog/mod.rs +10 -0
  24. package/dist-engine-src/src/catalog/schema.rs +4 -0
  25. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  26. package/dist-engine-src/src/cel/context.rs +86 -0
  27. package/dist-engine-src/src/cel/error.rs +19 -0
  28. package/dist-engine-src/src/cel/mod.rs +8 -0
  29. package/dist-engine-src/src/cel/provider.rs +9 -0
  30. package/dist-engine-src/src/cel/runtime.rs +167 -0
  31. package/dist-engine-src/src/cel/value.rs +50 -0
  32. package/dist-engine-src/src/commit_graph/context.rs +901 -0
  33. package/dist-engine-src/src/commit_graph/mod.rs +11 -0
  34. package/dist-engine-src/src/commit_graph/types.rs +109 -0
  35. package/dist-engine-src/src/commit_graph/walker.rs +756 -0
  36. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  37. package/dist-engine-src/src/commit_store/context.rs +944 -0
  38. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  39. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  40. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  41. package/dist-engine-src/src/commit_store/types.rs +215 -0
  42. package/dist-engine-src/src/common/error.rs +313 -0
  43. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  44. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  45. package/dist-engine-src/src/common/identity.rs +145 -0
  46. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  47. package/dist-engine-src/src/common/metadata.rs +40 -0
  48. package/dist-engine-src/src/common/mod.rs +23 -0
  49. package/dist-engine-src/src/common/types.rs +105 -0
  50. package/dist-engine-src/src/common/wire.rs +222 -0
  51. package/dist-engine-src/src/domain.rs +324 -0
  52. package/dist-engine-src/src/engine.rs +225 -0
  53. package/dist-engine-src/src/entity_identity.rs +405 -0
  54. package/dist-engine-src/src/functions/context.rs +292 -0
  55. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  56. package/dist-engine-src/src/functions/mod.rs +18 -0
  57. package/dist-engine-src/src/functions/provider.rs +130 -0
  58. package/dist-engine-src/src/functions/state.rs +336 -0
  59. package/dist-engine-src/src/functions/types.rs +37 -0
  60. package/dist-engine-src/src/init.rs +558 -0
  61. package/dist-engine-src/src/json_store/compression.rs +77 -0
  62. package/dist-engine-src/src/json_store/context.rs +423 -0
  63. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  64. package/dist-engine-src/src/json_store/mod.rs +12 -0
  65. package/dist-engine-src/src/json_store/store.rs +1109 -0
  66. package/dist-engine-src/src/json_store/types.rs +217 -0
  67. package/dist-engine-src/src/lib.rs +62 -0
  68. package/dist-engine-src/src/live_state/context.rs +2019 -0
  69. package/dist-engine-src/src/live_state/mod.rs +15 -0
  70. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  71. package/dist-engine-src/src/live_state/reader.rs +23 -0
  72. package/dist-engine-src/src/live_state/types.rs +222 -0
  73. package/dist-engine-src/src/live_state/visibility.rs +223 -0
  74. package/dist-engine-src/src/plugin/archive.rs +438 -0
  75. package/dist-engine-src/src/plugin/component.rs +183 -0
  76. package/dist-engine-src/src/plugin/install.rs +619 -0
  77. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  78. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  79. package/dist-engine-src/src/plugin/mod.rs +33 -0
  80. package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
  81. package/dist-engine-src/src/plugin/storage.rs +74 -0
  82. package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
  83. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  84. package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
  85. package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
  86. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
  87. package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
  89. package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
  90. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
  91. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
  92. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
  93. package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
  94. package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
  95. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  96. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
  97. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
  98. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
  99. package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
  100. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  101. package/dist-engine-src/src/schema/definition.json +187 -0
  102. package/dist-engine-src/src/schema/definition.rs +742 -0
  103. package/dist-engine-src/src/schema/key.rs +138 -0
  104. package/dist-engine-src/src/schema/mod.rs +20 -0
  105. package/dist-engine-src/src/schema/seed.rs +14 -0
  106. package/dist-engine-src/src/schema/tests.rs +780 -0
  107. package/dist-engine-src/src/session/context.rs +364 -0
  108. package/dist-engine-src/src/session/create_version.rs +88 -0
  109. package/dist-engine-src/src/session/execute.rs +478 -0
  110. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  111. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  112. package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
  113. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  114. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  115. package/dist-engine-src/src/session/merge/version.rs +427 -0
  116. package/dist-engine-src/src/session/mod.rs +27 -0
  117. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  118. package/dist-engine-src/src/session/switch_version.rs +109 -0
  119. package/dist-engine-src/src/sql2/change_provider.rs +331 -0
  120. package/dist-engine-src/src/sql2/classify.rs +182 -0
  121. package/dist-engine-src/src/sql2/context.rs +311 -0
  122. package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
  123. package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
  124. package/dist-engine-src/src/sql2/dml.rs +148 -0
  125. package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
  126. package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
  127. package/dist-engine-src/src/sql2/error.rs +216 -0
  128. package/dist-engine-src/src/sql2/execute.rs +3440 -0
  129. package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
  130. package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
  131. package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
  132. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  133. package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
  134. package/dist-engine-src/src/sql2/history_projection.rs +56 -0
  135. package/dist-engine-src/src/sql2/history_provider.rs +412 -0
  136. package/dist-engine-src/src/sql2/history_route.rs +657 -0
  137. package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
  138. package/dist-engine-src/src/sql2/mod.rs +46 -0
  139. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  140. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  141. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  142. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  143. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  144. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  145. package/dist-engine-src/src/sql2/read_only.rs +63 -0
  146. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  147. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  148. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  149. package/dist-engine-src/src/sql2/session.rs +132 -0
  150. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  151. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  152. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  153. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  154. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  155. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  156. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  157. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  158. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  159. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  160. package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
  161. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  162. package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
  163. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  164. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  165. package/dist-engine-src/src/storage/context.rs +356 -0
  166. package/dist-engine-src/src/storage/mod.rs +14 -0
  167. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  168. package/dist-engine-src/src/storage/types.rs +501 -0
  169. package/dist-engine-src/src/storage_bench.rs +4863 -0
  170. package/dist-engine-src/src/test_support.rs +228 -0
  171. package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
  172. package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
  173. package/dist-engine-src/src/tracked_state/context.rs +1867 -0
  174. package/dist-engine-src/src/tracked_state/diff.rs +686 -0
  175. package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
  176. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  177. package/dist-engine-src/src/tracked_state/merge.rs +492 -0
  178. package/dist-engine-src/src/tracked_state/mod.rs +32 -0
  179. package/dist-engine-src/src/tracked_state/storage.rs +375 -0
  180. package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
  181. package/dist-engine-src/src/tracked_state/types.rs +231 -0
  182. package/dist-engine-src/src/transaction/commit.rs +1484 -0
  183. package/dist-engine-src/src/transaction/context.rs +1548 -0
  184. package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
  185. package/dist-engine-src/src/transaction/mod.rs +13 -0
  186. package/dist-engine-src/src/transaction/normalization.rs +890 -0
  187. package/dist-engine-src/src/transaction/prep.rs +37 -0
  188. package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
  189. package/dist-engine-src/src/transaction/staging.rs +1731 -0
  190. package/dist-engine-src/src/transaction/types.rs +460 -0
  191. package/dist-engine-src/src/transaction/validation.rs +5830 -0
  192. package/dist-engine-src/src/untracked_state/codec.rs +307 -0
  193. package/dist-engine-src/src/untracked_state/context.rs +98 -0
  194. package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
  195. package/dist-engine-src/src/untracked_state/mod.rs +15 -0
  196. package/dist-engine-src/src/untracked_state/storage.rs +396 -0
  197. package/dist-engine-src/src/untracked_state/types.rs +146 -0
  198. package/dist-engine-src/src/version/context.rs +40 -0
  199. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  200. package/dist-engine-src/src/version/mod.rs +13 -0
  201. package/dist-engine-src/src/version/refs.rs +330 -0
  202. package/dist-engine-src/src/version/stage_rows.rs +67 -0
  203. package/dist-engine-src/src/version/types.rs +21 -0
  204. package/dist-engine-src/src/wasm/mod.rs +60 -0
  205. package/package.json +68 -64
@@ -0,0 +1,1484 @@
1
+ use crate::binary_cas::BinaryCasContext;
2
+ use crate::commit_store::{ChangeRef, CommitDraftRef, CommitStoreContext, StagedCommitStoreCommit};
3
+ use crate::functions::FunctionContext;
4
+ use crate::json_store::{JsonStoreContext, JsonWritePlacementRef, NormalizedJsonRef};
5
+ use crate::storage::{StorageReader, StorageWriteSet, StorageWriteTransaction};
6
+ use crate::tracked_state::{TrackedStateContext, TrackedStateDeltaRef};
7
+ use crate::transaction::prepare_version_ref_row;
8
+ use crate::transaction::staging::PreparedWriteSet;
9
+ use crate::transaction::types::{PreparedAdoptedStateRow, PreparedStateRow, StagedCommitMembers};
10
+ use crate::untracked_state::{
11
+ UntrackedStateContext, UntrackedStateIdentity, UntrackedStateIdentityRef, UntrackedStateRowRef,
12
+ };
13
+ use crate::version::{VersionContext, VersionRefReader};
14
+ use crate::LixError;
15
+ use std::collections::BTreeMap;
16
+
17
+ type RowIndex = usize;
18
+ type AdoptedRowIndex = usize;
19
+
20
+ /// Commits prepared transaction rows into durable tracked and untracked stores.
21
+ ///
22
+ /// Providers decode DataFusion DML into hydrated `PreparedStateRow`s. Untracked
23
+ /// rows are durable local overlay state and bypass commit-store rows. Tracked
24
+ /// rows stage canonical commit-store facts, then update the live-state serving
25
+ /// projection. The tracked side of that projection is a prolly root keyed by
26
+ /// the new commit id.
27
+ pub(crate) async fn commit_prepared_writes(
28
+ binary_cas: &BinaryCasContext,
29
+ commit_store: &CommitStoreContext,
30
+ version_ctx: &VersionContext,
31
+ runtime_functions: Option<&FunctionContext>,
32
+ transaction: &mut (impl StorageWriteTransaction + ?Sized),
33
+ prepared_writes: PreparedWriteSet,
34
+ ) -> Result<(), LixError> {
35
+ let mut writes = StorageWriteSet::new();
36
+ let mut json_writer = JsonStoreContext::new().writer();
37
+
38
+ if !prepared_writes.file_data_writes.is_empty() {
39
+ let mut blob_writer = binary_cas.writer(&mut writes);
40
+ for write in &prepared_writes.file_data_writes {
41
+ blob_writer.stage_bytes(&write.data)?;
42
+ }
43
+ }
44
+
45
+ let state_rows = prepared_writes.state_rows;
46
+ let adopted_rows = prepared_writes.adopted_rows;
47
+ let finalized = finalize_commit_rows(
48
+ prepared_writes.commit_members_by_version,
49
+ prepared_writes.extra_commit_parents_by_version,
50
+ version_ctx,
51
+ transaction,
52
+ )
53
+ .await?;
54
+ let commit_rows = finalized.commit_rows;
55
+ let version_heads = finalized.version_heads;
56
+ let tracked_roots = finalized.tracked_roots;
57
+ let row_index = index_prepared_rows(&state_rows)?;
58
+ let adopted_index = index_adopted_rows(&adopted_rows);
59
+
60
+ if let Some(runtime_functions) = runtime_functions {
61
+ runtime_functions
62
+ .stage_persist_if_needed(&mut writes)
63
+ .await?;
64
+ }
65
+
66
+ if state_rows.is_empty()
67
+ && adopted_rows.is_empty()
68
+ && commit_rows.is_empty()
69
+ && version_heads.is_empty()
70
+ && writes.is_empty()
71
+ {
72
+ return Ok(());
73
+ }
74
+
75
+ let staged_commits = stage_commit_store_commits(
76
+ commit_store,
77
+ transaction,
78
+ &mut writes,
79
+ &state_rows,
80
+ &row_index.tracked_row_indices_by_commit,
81
+ &adopted_rows,
82
+ &adopted_index.tracked_row_indices_by_commit,
83
+ &commit_rows,
84
+ )
85
+ .await?;
86
+
87
+ let json_pack_indexes_by_commit = stage_prepared_json_payloads(
88
+ &mut json_writer,
89
+ &mut writes,
90
+ &state_rows,
91
+ &row_index.tracked_row_indices_by_commit,
92
+ &staged_commits,
93
+ &row_index.untracked_row_indices,
94
+ )?;
95
+
96
+ // The serving projection is updated in the same backend transaction as the
97
+ // commit-store append. Tracked rows become prolly mutations under their owning
98
+ // commit root; untracked rows remain in the separate local overlay store.
99
+ {
100
+ let untracked_overlay_delete_identities = existing_untracked_overlay_delete_identities(
101
+ transaction,
102
+ row_index
103
+ .canonical_row_indices
104
+ .iter()
105
+ .map(|&row_index| untracked_identity_ref_from_state_row(&state_rows[row_index]))
106
+ .chain(
107
+ adopted_rows
108
+ .iter()
109
+ .map(untracked_identity_ref_from_adopted_row),
110
+ ),
111
+ )
112
+ .await?;
113
+ UntrackedStateContext::new()
114
+ .writer(&mut writes)
115
+ .stage_rows(
116
+ row_index
117
+ .untracked_row_indices
118
+ .iter()
119
+ .map(|&row_index| untracked_row_ref_from_state_row(&state_rows[row_index])),
120
+ )?;
121
+ UntrackedStateContext::new()
122
+ .writer(&mut writes)
123
+ .stage_delete_rows(
124
+ untracked_overlay_delete_identities
125
+ .iter()
126
+ .map(UntrackedStateIdentity::as_ref),
127
+ );
128
+ stage_tracked_roots(
129
+ transaction,
130
+ &mut writes,
131
+ &state_rows,
132
+ row_index.tracked_row_indices_by_commit,
133
+ &adopted_rows,
134
+ adopted_index.tracked_row_indices_by_commit,
135
+ tracked_roots,
136
+ staged_commits,
137
+ json_pack_indexes_by_commit,
138
+ )
139
+ .await?;
140
+ }
141
+
142
+ for version_head in version_heads {
143
+ let canonical_row = prepare_version_ref_row(
144
+ &version_head.version_id,
145
+ &version_head.commit_id,
146
+ &version_head.timestamp,
147
+ )?;
148
+ version_ctx.stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
149
+ }
150
+
151
+ writes.apply(transaction).await?;
152
+ Ok(())
153
+ }
154
+
155
+ fn stage_prepared_json_payloads(
156
+ json_writer: &mut crate::json_store::JsonStoreWriter,
157
+ writes: &mut StorageWriteSet,
158
+ state_rows: &[PreparedStateRow],
159
+ tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
160
+ staged_commits: &BTreeMap<String, StagedCommitStoreCommit>,
161
+ untracked_row_indices: &[RowIndex],
162
+ ) -> Result<BTreeMap<String, BTreeMap<u32, std::collections::HashMap<[u8; 32], usize>>>, LixError> {
163
+ let mut pack_indexes_by_commit = BTreeMap::new();
164
+ for (commit_id, row_indices) in tracked_row_indices_by_commit {
165
+ let staged_commit = staged_commits.get(commit_id).ok_or_else(|| {
166
+ LixError::new(
167
+ LixError::CODE_INTERNAL_ERROR,
168
+ format!("commit '{commit_id}' has tracked JSON rows but no staged commit-store locators"),
169
+ )
170
+ })?;
171
+ if row_indices.len() != staged_commit.authored_locators.len() {
172
+ return Err(LixError::new(
173
+ LixError::CODE_INTERNAL_ERROR,
174
+ format!(
175
+ "commit '{commit_id}' has {} tracked JSON rows but {} authored locators",
176
+ row_indices.len(),
177
+ staged_commit.authored_locators.len()
178
+ ),
179
+ ));
180
+ }
181
+ let mut row_indices_by_pack = BTreeMap::<u32, Vec<RowIndex>>::new();
182
+ for (&row_index, locator) in row_indices.iter().zip(&staged_commit.authored_locators) {
183
+ row_indices_by_pack
184
+ .entry(locator.source_pack_id)
185
+ .or_default()
186
+ .push(row_index);
187
+ }
188
+ for (pack_id, pack_row_indices) in row_indices_by_pack {
189
+ let report = json_writer.stage_batch_report(
190
+ writes,
191
+ JsonWritePlacementRef::CommitPack { commit_id, pack_id },
192
+ pack_row_indices
193
+ .iter()
194
+ .flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
195
+ )?;
196
+ pack_indexes_by_commit
197
+ .entry(commit_id.clone())
198
+ .or_insert_with(BTreeMap::new)
199
+ .insert(pack_id, report.pack_indexes);
200
+ }
201
+ }
202
+ json_writer.stage_batch(
203
+ writes,
204
+ JsonWritePlacementRef::OutOfBand,
205
+ untracked_row_indices
206
+ .iter()
207
+ .flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
208
+ )?;
209
+ Ok(pack_indexes_by_commit)
210
+ }
211
+
212
+ fn json_payloads_from_state_row(
213
+ row: &PreparedStateRow,
214
+ ) -> impl Iterator<Item = NormalizedJsonRef<'_>> {
215
+ row.snapshot
216
+ .iter()
217
+ .chain(row.metadata.iter())
218
+ .map(|json| NormalizedJsonRef::trusted_prehashed(json.normalized.as_ref(), json.json_ref))
219
+ }
220
+
221
+ async fn existing_untracked_overlay_delete_identities<'a>(
222
+ transaction: &mut (impl StorageReader + ?Sized),
223
+ identities: impl IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
224
+ ) -> Result<Vec<UntrackedStateIdentity>, LixError> {
225
+ UntrackedStateContext::new()
226
+ .reader(transaction)
227
+ .existing_identities(identities)
228
+ .await
229
+ }
230
+
231
+ struct PreparedRowIndex {
232
+ canonical_row_indices: Vec<RowIndex>,
233
+ untracked_row_indices: Vec<RowIndex>,
234
+ tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
235
+ }
236
+
237
+ struct PreparedAdoptedRowIndex {
238
+ tracked_row_indices_by_commit: BTreeMap<String, Vec<AdoptedRowIndex>>,
239
+ }
240
+
241
+ fn index_prepared_rows(rows: &[PreparedStateRow]) -> Result<PreparedRowIndex, LixError> {
242
+ let mut canonical_row_indices = Vec::new();
243
+ let mut untracked_row_indices = Vec::new();
244
+ let mut tracked_row_indices_by_commit = BTreeMap::<String, Vec<RowIndex>>::new();
245
+
246
+ for (row_index, row) in rows.iter().enumerate() {
247
+ if row.untracked {
248
+ untracked_row_indices.push(row_index);
249
+ continue;
250
+ }
251
+ let Some(commit_id) = row.commit_id.as_ref() else {
252
+ return Err(LixError::new(
253
+ LixError::CODE_INTERNAL_ERROR,
254
+ "tracked prepared row is missing commit_id before commit indexing",
255
+ ));
256
+ };
257
+ canonical_row_indices.push(row_index);
258
+ tracked_row_indices_by_commit
259
+ .entry(commit_id.clone())
260
+ .or_default()
261
+ .push(row_index);
262
+ }
263
+
264
+ Ok(PreparedRowIndex {
265
+ canonical_row_indices,
266
+ untracked_row_indices,
267
+ tracked_row_indices_by_commit,
268
+ })
269
+ }
270
+
271
+ fn index_adopted_rows(rows: &[PreparedAdoptedStateRow]) -> PreparedAdoptedRowIndex {
272
+ let mut tracked_row_indices_by_commit = BTreeMap::<String, Vec<AdoptedRowIndex>>::new();
273
+ for (row_index, row) in rows.iter().enumerate() {
274
+ tracked_row_indices_by_commit
275
+ .entry(row.commit_id.clone())
276
+ .or_default()
277
+ .push(row_index);
278
+ }
279
+ PreparedAdoptedRowIndex {
280
+ tracked_row_indices_by_commit,
281
+ }
282
+ }
283
+
284
+ async fn stage_commit_store_commits(
285
+ commit_store: &CommitStoreContext,
286
+ transaction: &mut (impl StorageReader + ?Sized),
287
+ writes: &mut StorageWriteSet,
288
+ state_rows: &[PreparedStateRow],
289
+ tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
290
+ adopted_rows: &[PreparedAdoptedStateRow],
291
+ adopted_row_indices_by_commit: &BTreeMap<String, Vec<AdoptedRowIndex>>,
292
+ commit_rows: &[FinalizedCommitRow],
293
+ ) -> Result<BTreeMap<String, StagedCommitStoreCommit>, LixError> {
294
+ let mut commits = Vec::with_capacity(commit_rows.len());
295
+ let mut commit_ids = Vec::with_capacity(commit_rows.len());
296
+ for commit_row in commit_rows {
297
+ let state_row_indices = tracked_row_indices_by_commit
298
+ .get(&commit_row.commit_id)
299
+ .map(Vec::as_slice)
300
+ .unwrap_or_default();
301
+ let adopted_row_indices = adopted_row_indices_by_commit
302
+ .get(&commit_row.commit_id)
303
+ .map(Vec::as_slice)
304
+ .unwrap_or_default();
305
+ let mut authored_changes = Vec::with_capacity(state_row_indices.len());
306
+ for &row_index in state_row_indices {
307
+ authored_changes.push(change_ref_from_state_row(&state_rows[row_index])?);
308
+ }
309
+ let mut adopted_changes = Vec::with_capacity(adopted_row_indices.len());
310
+ for &row_index in adopted_row_indices {
311
+ adopted_changes.push(change_ref_from_adopted_row(&adopted_rows[row_index]));
312
+ }
313
+
314
+ let commit = CommitDraftRef {
315
+ id: &commit_row.commit_id,
316
+ change_id: &commit_row.change_id,
317
+ parent_ids: &commit_row.parent_commit_ids,
318
+ author_account_ids: &[],
319
+ created_at: &commit_row.created_at,
320
+ };
321
+ commit_ids.push(commit_row.commit_id.clone());
322
+ commits.push((commit, authored_changes, adopted_changes));
323
+ }
324
+ let staged = commit_store
325
+ .writer(transaction, writes)
326
+ .stage_tracked_commit_drafts(commits)
327
+ .await?;
328
+ if staged.len() != commit_ids.len() {
329
+ return Err(LixError::new(
330
+ LixError::CODE_INTERNAL_ERROR,
331
+ format!(
332
+ "commit-store staged {} commits for {} finalized commit rows",
333
+ staged.len(),
334
+ commit_ids.len()
335
+ ),
336
+ ));
337
+ }
338
+ Ok(commit_ids.into_iter().zip(staged).collect())
339
+ }
340
+
341
+ fn change_ref_from_state_row(row: &PreparedStateRow) -> Result<ChangeRef<'_>, LixError> {
342
+ let Some(change_id) = row.change_id.as_deref() else {
343
+ return Err(LixError::new(
344
+ "LIX_ERROR_UNKNOWN",
345
+ "tracked staged row is missing change_id before commit-store append",
346
+ ));
347
+ };
348
+
349
+ Ok(ChangeRef {
350
+ id: change_id,
351
+ entity_id: &row.entity_id,
352
+ schema_key: &row.schema_key,
353
+ file_id: row.file_id.as_deref(),
354
+ snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
355
+ metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
356
+ created_at: &row.updated_at,
357
+ })
358
+ }
359
+
360
+ fn change_ref_from_adopted_row(row: &PreparedAdoptedStateRow) -> ChangeRef<'_> {
361
+ ChangeRef {
362
+ id: &row.change_id,
363
+ entity_id: &row.entity_id,
364
+ schema_key: &row.schema_key,
365
+ file_id: row.file_id.as_deref(),
366
+ snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
367
+ metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
368
+ created_at: &row.updated_at,
369
+ }
370
+ }
371
+
372
+ async fn stage_tracked_roots(
373
+ transaction: &mut (impl StorageReader + ?Sized),
374
+ writes: &mut StorageWriteSet,
375
+ state_rows: &[PreparedStateRow],
376
+ mut tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
377
+ adopted_rows: &[PreparedAdoptedStateRow],
378
+ mut adopted_row_indices_by_commit: BTreeMap<String, Vec<AdoptedRowIndex>>,
379
+ tracked_roots: Vec<PendingTrackedRoot>,
380
+ mut staged_commits: BTreeMap<String, StagedCommitStoreCommit>,
381
+ json_pack_indexes_by_commit: BTreeMap<
382
+ String,
383
+ BTreeMap<u32, std::collections::HashMap<[u8; 32], usize>>,
384
+ >,
385
+ ) -> Result<(), LixError> {
386
+ let tracked_state = TrackedStateContext::new();
387
+ let mut writer = tracked_state.writer(transaction, writes);
388
+ for root in tracked_roots {
389
+ let staged = staged_commits.remove(&root.commit_id).ok_or_else(|| {
390
+ LixError::new(
391
+ LixError::CODE_INTERNAL_ERROR,
392
+ format!(
393
+ "tracked-state root for commit '{}' has no staged commit-store locators",
394
+ root.commit_id
395
+ ),
396
+ )
397
+ })?;
398
+ let state_row_indices = tracked_row_indices_by_commit
399
+ .remove(&root.commit_id)
400
+ .unwrap_or_default();
401
+ let adopted_row_indices = adopted_row_indices_by_commit
402
+ .remove(&root.commit_id)
403
+ .unwrap_or_default();
404
+ if state_row_indices.len() != staged.authored_locators.len() {
405
+ return Err(LixError::new(
406
+ LixError::CODE_INTERNAL_ERROR,
407
+ format!(
408
+ "commit '{}' has {} tracked authored rows but {} commit-store authored locators",
409
+ root.commit_id,
410
+ state_row_indices.len(),
411
+ staged.authored_locators.len()
412
+ ),
413
+ ));
414
+ }
415
+ if adopted_row_indices.len() != staged.adopted_locators.len() {
416
+ return Err(LixError::new(
417
+ LixError::CODE_INTERNAL_ERROR,
418
+ format!(
419
+ "commit '{}' has {} tracked adopted rows but {} commit-store adopted locators",
420
+ root.commit_id,
421
+ adopted_row_indices.len(),
422
+ staged.adopted_locators.len()
423
+ ),
424
+ ));
425
+ }
426
+ let authored_changes = state_row_indices
427
+ .iter()
428
+ .map(|&row_index| change_ref_from_state_row(&state_rows[row_index]))
429
+ .collect::<Result<Vec<_>, _>>()?;
430
+ let adopted_changes = adopted_row_indices
431
+ .iter()
432
+ .map(|&row_index| change_ref_from_adopted_row(&adopted_rows[row_index]))
433
+ .collect::<Vec<_>>();
434
+ let authored_updated_at = state_row_indices
435
+ .iter()
436
+ .map(|&row_index| state_rows[row_index].updated_at.as_str())
437
+ .collect::<Vec<_>>();
438
+ let authored_created_at = state_row_indices
439
+ .iter()
440
+ .map(|&row_index| state_rows[row_index].created_at.as_str())
441
+ .collect::<Vec<_>>();
442
+ let adopted_updated_at = adopted_row_indices
443
+ .iter()
444
+ .map(|&row_index| adopted_rows[row_index].updated_at.as_str())
445
+ .collect::<Vec<_>>();
446
+ let adopted_created_at = adopted_row_indices
447
+ .iter()
448
+ .map(|&row_index| adopted_rows[row_index].created_at.as_str())
449
+ .collect::<Vec<_>>();
450
+ let mut deltas = Vec::with_capacity(authored_changes.len() + adopted_changes.len());
451
+ deltas.extend(
452
+ authored_changes
453
+ .iter()
454
+ .zip(&staged.authored_locators)
455
+ .zip(authored_created_at)
456
+ .zip(authored_updated_at)
457
+ .map(
458
+ |(((change, locator), created_at), updated_at)| TrackedStateDeltaRef {
459
+ change: *change,
460
+ locator: locator.as_ref(),
461
+ created_at,
462
+ updated_at,
463
+ },
464
+ ),
465
+ );
466
+ deltas.extend(
467
+ adopted_changes
468
+ .iter()
469
+ .zip(&staged.adopted_locators)
470
+ .zip(adopted_created_at)
471
+ .zip(adopted_updated_at)
472
+ .map(
473
+ |(((change, locator), created_at), updated_at)| TrackedStateDeltaRef {
474
+ change: *change,
475
+ locator: locator.as_ref(),
476
+ created_at,
477
+ updated_at,
478
+ },
479
+ ),
480
+ );
481
+ if let Some(indexes) = json_pack_indexes_by_commit
482
+ .get(&root.commit_id)
483
+ .and_then(|packs| packs.get(&0))
484
+ {
485
+ writer
486
+ .stage_delta_with_json_pack_indexes(
487
+ &root.commit_id,
488
+ root.parent_commit_id.as_deref(),
489
+ &deltas,
490
+ crate::tracked_state::DeltaJsonPackIndexesRef {
491
+ commit_id: &root.commit_id,
492
+ pack_id: 0,
493
+ indexes,
494
+ },
495
+ )
496
+ .await?;
497
+ } else {
498
+ writer
499
+ .stage_delta(&root.commit_id, root.parent_commit_id.as_deref(), &deltas)
500
+ .await?;
501
+ }
502
+ }
503
+ if !tracked_row_indices_by_commit.is_empty() || !adopted_row_indices_by_commit.is_empty() {
504
+ let mut commit_ids = tracked_row_indices_by_commit
505
+ .keys()
506
+ .chain(adopted_row_indices_by_commit.keys())
507
+ .cloned()
508
+ .collect::<Vec<_>>();
509
+ commit_ids.sort();
510
+ commit_ids.dedup();
511
+ return Err(LixError::new(
512
+ LixError::CODE_INTERNAL_ERROR,
513
+ format!(
514
+ "tracked live_state rows have no finalized root metadata for commit ids: {}",
515
+ commit_ids.join(", ")
516
+ ),
517
+ ));
518
+ }
519
+ if !staged_commits.is_empty() {
520
+ let commit_ids = staged_commits.keys().cloned().collect::<Vec<_>>();
521
+ return Err(LixError::new(
522
+ LixError::CODE_INTERNAL_ERROR,
523
+ format!(
524
+ "commit-store staged commits without tracked root metadata: {}",
525
+ commit_ids.join(", ")
526
+ ),
527
+ ));
528
+ }
529
+ Ok(())
530
+ }
531
+
532
+ fn untracked_row_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateRowRef<'_> {
533
+ UntrackedStateRowRef {
534
+ entity_id: &row.entity_id,
535
+ schema_key: &row.schema_key,
536
+ file_id: row.file_id.as_deref(),
537
+ snapshot_content: row
538
+ .snapshot
539
+ .as_ref()
540
+ .map(|snapshot| snapshot.normalized.as_ref()),
541
+ metadata: row
542
+ .metadata
543
+ .as_ref()
544
+ .map(|metadata| metadata.normalized.as_ref()),
545
+ created_at: &row.created_at,
546
+ updated_at: &row.updated_at,
547
+ global: row.global,
548
+ version_id: &row.version_id,
549
+ }
550
+ }
551
+
552
+ fn untracked_identity_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateIdentityRef<'_> {
553
+ UntrackedStateIdentityRef {
554
+ version_id: &row.version_id,
555
+ schema_key: &row.schema_key,
556
+ entity_id: &row.entity_id,
557
+ file_id: row.file_id.as_deref(),
558
+ }
559
+ }
560
+
561
+ fn untracked_identity_ref_from_adopted_row(
562
+ row: &PreparedAdoptedStateRow,
563
+ ) -> UntrackedStateIdentityRef<'_> {
564
+ UntrackedStateIdentityRef {
565
+ version_id: &row.version_id,
566
+ schema_key: &row.schema_key,
567
+ entity_id: &row.entity_id,
568
+ file_id: row.file_id.as_deref(),
569
+ }
570
+ }
571
+
572
+ /// Materializes tracked staged membership into commit-store commits.
573
+ ///
574
+ /// Staging only accumulates `version_id -> change_ids` because commit ids,
575
+ /// parent heads, and commit-row timestamps belong to transaction finalization.
576
+ /// The `change_ids` list is the ordered set of canonical changes whose effects
577
+ /// the commit introduces relative to its first parent; merge commits may later
578
+ /// populate this list with existing source-parent changes instead of copied
579
+ /// change payloads.
580
+ /// This function turns those membership sets into finalized commit facts.
581
+ ///
582
+ /// Commit finalization output split by durability target.
583
+ ///
584
+ /// `commit_rows` are canonical commit-store facts. live_state later projects
585
+ /// commit SQL surfaces from commit_store; tracked_state roots do not store
586
+ /// commit graph facts.
587
+ ///
588
+ /// `version_heads` are moving refs. They are written through `VersionContext`,
589
+ /// not the canonical commit store.
590
+ struct FinalizedCommitRows {
591
+ commit_rows: Vec<FinalizedCommitRow>,
592
+ version_heads: Vec<PendingVersionHead>,
593
+ tracked_roots: Vec<PendingTrackedRoot>,
594
+ }
595
+
596
+ struct FinalizedCommitRow {
597
+ commit_id: String,
598
+ parent_commit_ids: Vec<String>,
599
+ created_at: String,
600
+ change_id: String,
601
+ }
602
+
603
+ struct PendingVersionHead {
604
+ version_id: String,
605
+ commit_id: String,
606
+ timestamp: String,
607
+ }
608
+
609
+ struct PendingTrackedRoot {
610
+ commit_id: String,
611
+ parent_commit_id: Option<String>,
612
+ }
613
+
614
+ async fn finalize_commit_rows(
615
+ commit_members_by_version: BTreeMap<String, StagedCommitMembers>,
616
+ extra_commit_parents_by_version: BTreeMap<String, Vec<String>>,
617
+ version_ctx: &VersionContext,
618
+ transaction: &mut (impl StorageReader + ?Sized),
619
+ ) -> Result<FinalizedCommitRows, LixError> {
620
+ let mut commit_rows = Vec::new();
621
+ let mut version_heads = Vec::new();
622
+ let mut tracked_roots = Vec::new();
623
+
624
+ for (version_id, members) in commit_members_by_version {
625
+ if members.is_empty() && !members.allow_empty {
626
+ continue;
627
+ }
628
+
629
+ let commit_id = members.commit_id;
630
+ let commit_change_id = members.commit_change_id;
631
+ let timestamp = members.created_at;
632
+ let _change_ids = members.change_ids;
633
+ let parent_commit_ids = version_ctx
634
+ .ref_reader(&mut *transaction)
635
+ .load_head_commit_id(&version_id)
636
+ .await?
637
+ .into_iter()
638
+ .collect::<Vec<_>>();
639
+ let parent_commit_ids = merge_parent_commit_ids(
640
+ parent_commit_ids,
641
+ extra_commit_parents_by_version
642
+ .get(&version_id)
643
+ .cloned()
644
+ .unwrap_or_default(),
645
+ );
646
+ let parent_commit_id = parent_commit_ids.first().cloned();
647
+
648
+ commit_rows.push(FinalizedCommitRow {
649
+ commit_id: commit_id.clone(),
650
+ parent_commit_ids: parent_commit_ids.clone(),
651
+ created_at: timestamp.clone(),
652
+ change_id: commit_change_id,
653
+ });
654
+ version_heads.push(PendingVersionHead {
655
+ version_id: version_id.clone(),
656
+ commit_id: commit_id.clone(),
657
+ timestamp,
658
+ });
659
+ tracked_roots.push(PendingTrackedRoot {
660
+ commit_id,
661
+ parent_commit_id,
662
+ });
663
+ }
664
+
665
+ Ok(FinalizedCommitRows {
666
+ commit_rows,
667
+ version_heads,
668
+ tracked_roots,
669
+ })
670
+ }
671
+
672
+ fn merge_parent_commit_ids(mut base: Vec<String>, extra: Vec<String>) -> Vec<String> {
673
+ for parent in extra {
674
+ if !base.contains(&parent) {
675
+ base.push(parent);
676
+ }
677
+ }
678
+ base
679
+ }
680
+
681
+ #[cfg(test)]
682
+ mod tests {
683
+ use std::collections::BTreeMap;
684
+ use std::sync::{
685
+ atomic::{AtomicUsize, Ordering},
686
+ Arc,
687
+ };
688
+
689
+ use super::*;
690
+ use crate::backend::{
691
+ testing::UnitTestBackend, Backend, BackendKvEntryPage, BackendKvExistsBatch,
692
+ BackendKvGetRequest, BackendKvKeyPage, BackendKvScanRequest, BackendKvValueBatch,
693
+ BackendKvValuePage, BackendKvWriteBatch, BackendKvWriteStats, BackendReadTransaction,
694
+ BackendWriteTransaction,
695
+ };
696
+ use crate::catalog::SchemaPlanId;
697
+ use crate::commit_store::{ChangeIndexEntry, ChangeLocator};
698
+ use crate::live_state::{LiveStateContext, LiveStateRowRequest};
699
+ use crate::storage::StorageContext;
700
+ use crate::transaction::types::PreparedRowFacts;
701
+ use crate::untracked_state::{
702
+ MaterializedUntrackedStateRow, UntrackedStateContext, UntrackedStateRowRequest,
703
+ };
704
+ use crate::version::VersionContext;
705
+ use crate::NullableKeyFilter;
706
+ use crate::GLOBAL_VERSION_ID;
707
+ use async_trait::async_trait;
708
+
709
+ const DETERMINISTIC_MODE_KEY: &str = "lix_deterministic_mode";
710
+ const DETERMINISTIC_SEQUENCE_KEY: &str = "lix_deterministic_sequence_number";
711
+
712
+ fn live_state_context() -> LiveStateContext {
713
+ LiveStateContext::new(
714
+ crate::tracked_state::TrackedStateContext::new(),
715
+ crate::untracked_state::UntrackedStateContext::new(),
716
+ crate::commit_graph::CommitGraphContext::new(),
717
+ )
718
+ }
719
+
720
+ #[tokio::test]
721
+ async fn commit_staged_writes_appends_commit_store_and_updates_serving_projection() {
722
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
723
+ let storage = StorageContext::new(Arc::clone(&backend));
724
+ let binary_cas = BinaryCasContext::new();
725
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
726
+ let mut transaction = storage
727
+ .begin_write_transaction()
728
+ .await
729
+ .expect("transaction should open");
730
+
731
+ let state_rows = vec![tracked_global_row("change-1")];
732
+ commit_prepared_writes(
733
+ &binary_cas,
734
+ &crate::commit_store::CommitStoreContext::new(),
735
+ &version_ctx,
736
+ None,
737
+ transaction.as_mut(),
738
+ PreparedWriteSet {
739
+ insert_identities: BTreeMap::new(),
740
+ state_rows,
741
+ adopted_rows: Vec::new(),
742
+ commit_members_by_version: BTreeMap::from([(
743
+ GLOBAL_VERSION_ID.to_string(),
744
+ members(["change-1"]),
745
+ )]),
746
+ extra_commit_parents_by_version: BTreeMap::new(),
747
+ file_data_writes: Vec::new(),
748
+ },
749
+ )
750
+ .await
751
+ .expect("commit should flush staged rows");
752
+ transaction
753
+ .commit()
754
+ .await
755
+ .expect("commit should persist kv");
756
+
757
+ let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
758
+ let commit = commit_reader
759
+ .load_commit("test-uuid-1")
760
+ .await
761
+ .expect("commit-store commit should load")
762
+ .expect("commit-store commit should exist");
763
+ assert_eq!(commit.change_id, "test-uuid-2");
764
+ assert_eq!(commit.change_pack_count, 1);
765
+ assert_eq!(commit.membership_pack_count, 0);
766
+ let index_entries = commit_reader
767
+ .load_change_index_entries(&["change-1".to_string(), "test-uuid-2".to_string()])
768
+ .await
769
+ .expect("commit-store change index should load");
770
+ assert_eq!(
771
+ index_entries,
772
+ vec![
773
+ Some(ChangeIndexEntry::PackedChange {
774
+ locator: ChangeLocator {
775
+ source_commit_id: "test-uuid-1".to_string(),
776
+ source_pack_id: 0,
777
+ source_ordinal: 0,
778
+ change_id: "change-1".to_string(),
779
+ },
780
+ }),
781
+ Some(ChangeIndexEntry::CommitHeader {
782
+ commit_id: "test-uuid-1".to_string(),
783
+ change_id: "test-uuid-2".to_string(),
784
+ }),
785
+ ]
786
+ );
787
+ let change_pack = commit_reader
788
+ .load_change_pack("test-uuid-1", 0)
789
+ .await
790
+ .expect("commit-store change pack should load")
791
+ .expect("commit-store change pack should exist");
792
+ assert_eq!(change_pack.len(), 1);
793
+ assert_eq!(change_pack[0].id, "change-1");
794
+ assert_eq!(change_pack[0].schema_key, "test_schema");
795
+
796
+ let loaded_head = version_ctx
797
+ .ref_reader(storage.clone())
798
+ .load_head_commit_id(GLOBAL_VERSION_ID)
799
+ .await
800
+ .expect("version ref load should succeed");
801
+ assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
802
+ }
803
+
804
+ #[tokio::test]
805
+ async fn commit_with_only_untracked_writes_does_not_create_lix_commit() {
806
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
807
+ let storage = StorageContext::new(Arc::clone(&backend));
808
+ let binary_cas = BinaryCasContext::new();
809
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
810
+ let untracked_state = UntrackedStateContext::new();
811
+ let mut transaction = storage
812
+ .begin_write_transaction()
813
+ .await
814
+ .expect("transaction should open");
815
+
816
+ let state_rows = vec![untracked_global_row("change-untracked")];
817
+ commit_prepared_writes(
818
+ &binary_cas,
819
+ &crate::commit_store::CommitStoreContext::new(),
820
+ &version_ctx,
821
+ None,
822
+ transaction.as_mut(),
823
+ PreparedWriteSet {
824
+ insert_identities: BTreeMap::new(),
825
+ state_rows,
826
+ adopted_rows: Vec::new(),
827
+ commit_members_by_version: BTreeMap::new(),
828
+ extra_commit_parents_by_version: BTreeMap::new(),
829
+ file_data_writes: Vec::new(),
830
+ },
831
+ )
832
+ .await
833
+ .expect("commit should flush untracked row");
834
+ transaction
835
+ .commit()
836
+ .await
837
+ .expect("commit should persist kv");
838
+
839
+ let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
840
+ let index_entries = commit_reader
841
+ .load_change_index_entries(&["change-untracked".to_string()])
842
+ .await
843
+ .expect("commit-store change index should load");
844
+ assert_eq!(index_entries, vec![None]);
845
+
846
+ let loaded = {
847
+ let mut untracked_reader = untracked_state.reader(storage.clone());
848
+ untracked_reader
849
+ .load_row(&UntrackedStateRowRequest {
850
+ schema_key: "test_schema".to_string(),
851
+ version_id: GLOBAL_VERSION_ID.to_string(),
852
+ entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
853
+ file_id: NullableKeyFilter::Null,
854
+ })
855
+ .await
856
+ }
857
+ .expect("untracked row load should succeed")
858
+ .expect("untracked row should be persisted");
859
+ assert_eq!(
860
+ loaded.snapshot_content.as_deref(),
861
+ Some("{\"value\":\"untracked\"}")
862
+ );
863
+ }
864
+
865
+ #[tokio::test]
866
+ async fn tracked_write_deletes_matching_untracked_overlay() {
867
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
868
+ let storage = StorageContext::new(Arc::clone(&backend));
869
+ let binary_cas = BinaryCasContext::new();
870
+ let untracked_state = UntrackedStateContext::new();
871
+ let live_state = Arc::new(live_state_context());
872
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
873
+
874
+ let mut seed_transaction = storage
875
+ .begin_write_transaction()
876
+ .await
877
+ .expect("seed transaction should open");
878
+ let mut writes = StorageWriteSet::new();
879
+ let staged_row = untracked_global_row("change-untracked");
880
+ let canonical_row = crate::test_support::untracked_state_row_from_materialized(
881
+ &mut writes,
882
+ &MaterializedUntrackedStateRow::from(staged_row),
883
+ )
884
+ .expect("untracked seed should canonicalize");
885
+ untracked_state
886
+ .writer(&mut writes)
887
+ .stage_rows(std::iter::once(canonical_row.as_ref()))
888
+ .expect("untracked seed should write");
889
+ writes
890
+ .apply(&mut seed_transaction.as_mut())
891
+ .await
892
+ .expect("untracked seed should apply");
893
+ seed_transaction
894
+ .commit()
895
+ .await
896
+ .expect("seed transaction should persist");
897
+
898
+ let mut transaction = storage
899
+ .begin_write_transaction()
900
+ .await
901
+ .expect("transaction should open");
902
+ let state_rows = vec![tracked_global_row("change-tracked")];
903
+ commit_prepared_writes(
904
+ &binary_cas,
905
+ &crate::commit_store::CommitStoreContext::new(),
906
+ &version_ctx,
907
+ None,
908
+ transaction.as_mut(),
909
+ PreparedWriteSet {
910
+ insert_identities: BTreeMap::new(),
911
+ state_rows,
912
+ adopted_rows: Vec::new(),
913
+ commit_members_by_version: BTreeMap::from([(
914
+ GLOBAL_VERSION_ID.to_string(),
915
+ members(["change-tracked"]),
916
+ )]),
917
+ extra_commit_parents_by_version: BTreeMap::new(),
918
+ file_data_writes: Vec::new(),
919
+ },
920
+ )
921
+ .await
922
+ .expect("tracked commit should flush");
923
+ transaction
924
+ .commit()
925
+ .await
926
+ .expect("commit should persist kv");
927
+
928
+ let untracked = {
929
+ let mut untracked_reader = untracked_state.reader(storage.clone());
930
+ untracked_reader.load_row(&untracked_request()).await
931
+ }
932
+ .expect("untracked load should succeed");
933
+ assert_eq!(untracked, None);
934
+
935
+ let visible = live_state
936
+ .reader(storage.clone())
937
+ .load_row(&live_state_request())
938
+ .await
939
+ .expect("live-state load should succeed")
940
+ .expect("tracked row should be visible");
941
+ assert!(!visible.untracked);
942
+ assert_eq!(visible.change_id.as_deref(), Some("change-tracked"));
943
+ assert_eq!(visible.snapshot_content.as_deref(), Some("{\"value\":1}"));
944
+ }
945
+
946
+ #[tokio::test]
947
+ async fn commit_staged_writes_applies_cross_subsystem_rows_as_one_backend_batch() {
948
+ let counting_backend = Arc::new(CountingBackend::new());
949
+ let write_batches = counting_backend.write_batches();
950
+ let backend: Arc<dyn Backend + Send + Sync> = counting_backend;
951
+ let storage = StorageContext::new(backend);
952
+ let binary_cas = BinaryCasContext::new();
953
+ let live_state = Arc::new(live_state_context());
954
+ let untracked_state = UntrackedStateContext::new();
955
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
956
+ crate::test_support::seed_global_version_head(storage.clone()).await;
957
+ {
958
+ let mut seed_transaction = storage
959
+ .begin_write_transaction()
960
+ .await
961
+ .expect("seed transaction should open");
962
+ let mut writes = StorageWriteSet::new();
963
+ let mode_snapshot = serde_json::to_string(&serde_json::json!({
964
+ "key": DETERMINISTIC_MODE_KEY,
965
+ "value": { "enabled": true },
966
+ }))
967
+ .expect("mode snapshot should serialize");
968
+ JsonStoreContext::new()
969
+ .writer()
970
+ .stage_batch(
971
+ &mut writes,
972
+ JsonWritePlacementRef::OutOfBand,
973
+ [NormalizedJsonRef::new(mode_snapshot.as_str())],
974
+ )
975
+ .expect("deterministic mode snapshot should stage");
976
+ let row = crate::untracked_state::UntrackedStateRow {
977
+ entity_id: crate::entity_identity::EntityIdentity::single(DETERMINISTIC_MODE_KEY),
978
+ schema_key: "lix_key_value".to_string(),
979
+ file_id: None,
980
+ snapshot_content: Some(mode_snapshot.to_string()),
981
+ metadata: None,
982
+ created_at: "2026-01-01T00:00:00Z".to_string(),
983
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
984
+ global: true,
985
+ version_id: GLOBAL_VERSION_ID.to_string(),
986
+ };
987
+ UntrackedStateContext::new()
988
+ .writer(&mut writes)
989
+ .stage_rows(std::iter::once(row.as_ref()))
990
+ .expect("deterministic mode should stage");
991
+ writes
992
+ .apply(&mut seed_transaction.as_mut())
993
+ .await
994
+ .expect("deterministic mode should apply");
995
+ seed_transaction
996
+ .commit()
997
+ .await
998
+ .expect("seed transaction should persist");
999
+ }
1000
+ write_batches.store(0, Ordering::SeqCst);
1001
+ let runtime_functions = {
1002
+ let reader = live_state.reader(storage.clone());
1003
+ FunctionContext::prepare(&reader)
1004
+ .await
1005
+ .expect("runtime context should prepare")
1006
+ };
1007
+ runtime_functions.provider().call_uuid_v7();
1008
+ let mut transaction = storage
1009
+ .begin_write_transaction()
1010
+ .await
1011
+ .expect("transaction should open");
1012
+
1013
+ let tracked_row = tracked_global_row("change-tracked");
1014
+ let mut untracked_row = untracked_global_row("change-untracked");
1015
+ untracked_row.entity_id = crate::entity_identity::EntityIdentity::single("entity-2");
1016
+
1017
+ commit_prepared_writes(
1018
+ &binary_cas,
1019
+ &crate::commit_store::CommitStoreContext::new(),
1020
+ &version_ctx,
1021
+ Some(&runtime_functions),
1022
+ transaction.as_mut(),
1023
+ PreparedWriteSet {
1024
+ insert_identities: BTreeMap::new(),
1025
+ state_rows: vec![tracked_row, untracked_row],
1026
+ adopted_rows: Vec::new(),
1027
+ commit_members_by_version: BTreeMap::from([(
1028
+ GLOBAL_VERSION_ID.to_string(),
1029
+ members(["change-tracked"]),
1030
+ )]),
1031
+ extra_commit_parents_by_version: BTreeMap::new(),
1032
+ file_data_writes: Vec::new(),
1033
+ },
1034
+ )
1035
+ .await
1036
+ .expect("cross-subsystem commit should stage and apply");
1037
+
1038
+ assert_eq!(
1039
+ write_batches.load(Ordering::SeqCst),
1040
+ 1,
1041
+ "tracked, json, untracked, commit-store, and version refs must apply as one backend write batch"
1042
+ );
1043
+
1044
+ transaction
1045
+ .commit()
1046
+ .await
1047
+ .expect("commit should persist kv");
1048
+ assert_eq!(write_batches.load(Ordering::SeqCst), 1);
1049
+
1050
+ let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
1051
+ let commit = commit_reader
1052
+ .load_commit("test-uuid-1")
1053
+ .await
1054
+ .expect("commit-store commit should load")
1055
+ .expect("commit-store commit should exist");
1056
+ assert_eq!(commit.change_id, "test-uuid-2");
1057
+ let index_entries = commit_reader
1058
+ .load_change_index_entries(&["change-tracked".to_string()])
1059
+ .await
1060
+ .expect("commit-store change index should load");
1061
+ assert!(matches!(
1062
+ index_entries.as_slice(),
1063
+ [Some(ChangeIndexEntry::PackedChange { .. })]
1064
+ ));
1065
+
1066
+ let loaded_head = version_ctx
1067
+ .ref_reader(storage.clone())
1068
+ .load_head_commit_id(GLOBAL_VERSION_ID)
1069
+ .await
1070
+ .expect("version ref load should succeed");
1071
+ assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
1072
+
1073
+ let untracked = {
1074
+ let mut untracked_reader = untracked_state.reader(storage.clone());
1075
+ untracked_reader
1076
+ .load_row(&UntrackedStateRowRequest {
1077
+ schema_key: "test_schema".to_string(),
1078
+ version_id: GLOBAL_VERSION_ID.to_string(),
1079
+ entity_id: crate::entity_identity::EntityIdentity::single("entity-2"),
1080
+ file_id: NullableKeyFilter::Null,
1081
+ })
1082
+ .await
1083
+ }
1084
+ .expect("untracked row load should succeed")
1085
+ .expect("untracked row should persist");
1086
+ assert_eq!(
1087
+ untracked.snapshot_content.as_deref(),
1088
+ Some("{\"value\":\"untracked\"}")
1089
+ );
1090
+
1091
+ let sequence_row = live_state
1092
+ .reader(storage.clone())
1093
+ .load_row(&LiveStateRowRequest {
1094
+ schema_key: "lix_key_value".to_string(),
1095
+ version_id: GLOBAL_VERSION_ID.to_string(),
1096
+ entity_id: crate::entity_identity::EntityIdentity::single(
1097
+ DETERMINISTIC_SEQUENCE_KEY,
1098
+ ),
1099
+ file_id: NullableKeyFilter::Null,
1100
+ })
1101
+ .await
1102
+ .expect("deterministic sequence should load")
1103
+ .expect("deterministic sequence should persist");
1104
+ assert_eq!(
1105
+ sequence_row.snapshot_content.as_deref(),
1106
+ Some("{\"key\":\"lix_deterministic_sequence_number\",\"value\":0}")
1107
+ );
1108
+ }
1109
+
1110
+ #[tokio::test]
1111
+ async fn non_global_tracked_write_creates_one_commit_and_advances_only_touched_version() {
1112
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1113
+ let storage = StorageContext::new(Arc::clone(&backend));
1114
+ let binary_cas = BinaryCasContext::new();
1115
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1116
+ crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
1117
+ .await;
1118
+ crate::test_support::seed_version_head(storage.clone(), "version-a", "version-a-before")
1119
+ .await;
1120
+
1121
+ let mut transaction = storage
1122
+ .begin_write_transaction()
1123
+ .await
1124
+ .expect("transaction should open");
1125
+ let state_rows = vec![tracked_version_row("version-a", "change-version-a")];
1126
+ commit_prepared_writes(
1127
+ &binary_cas,
1128
+ &crate::commit_store::CommitStoreContext::new(),
1129
+ &version_ctx,
1130
+ None,
1131
+ transaction.as_mut(),
1132
+ PreparedWriteSet {
1133
+ insert_identities: BTreeMap::new(),
1134
+ state_rows,
1135
+ adopted_rows: Vec::new(),
1136
+ commit_members_by_version: BTreeMap::from([(
1137
+ "version-a".to_string(),
1138
+ members(["change-version-a"]),
1139
+ )]),
1140
+ extra_commit_parents_by_version: BTreeMap::new(),
1141
+ file_data_writes: Vec::new(),
1142
+ },
1143
+ )
1144
+ .await
1145
+ .expect("version commit should flush");
1146
+ transaction
1147
+ .commit()
1148
+ .await
1149
+ .expect("commit should persist kv");
1150
+
1151
+ let commit_reader = crate::commit_store::CommitStoreContext::new().reader(storage.clone());
1152
+ let commit = commit_reader
1153
+ .load_commit("test-uuid-1")
1154
+ .await
1155
+ .expect("commit-store commit should load")
1156
+ .expect("commit-store commit should exist");
1157
+ assert_eq!(commit.change_id, "test-uuid-2");
1158
+ assert_eq!(commit.parent_ids, vec!["version-a-before"]);
1159
+ let index_entries = commit_reader
1160
+ .load_change_index_entries(&["change-version-a".to_string()])
1161
+ .await
1162
+ .expect("commit-store change index should load");
1163
+ assert!(matches!(
1164
+ index_entries.as_slice(),
1165
+ [Some(ChangeIndexEntry::PackedChange { .. })]
1166
+ ));
1167
+
1168
+ let global_head = version_ctx
1169
+ .ref_reader(storage.clone())
1170
+ .load_head_commit_id(GLOBAL_VERSION_ID)
1171
+ .await
1172
+ .expect("global head should load");
1173
+ let version_head = version_ctx
1174
+ .ref_reader(storage.clone())
1175
+ .load_head_commit_id("version-a")
1176
+ .await
1177
+ .expect("version head should load");
1178
+ assert_eq!(global_head.as_deref(), Some("global-before"));
1179
+ assert_eq!(version_head.as_deref(), Some("test-uuid-1"));
1180
+ }
1181
+
1182
+ #[tokio::test]
1183
+ async fn finalize_commit_rows_parents_global_commit_to_existing_version_ref() {
1184
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1185
+ let storage = StorageContext::new(Arc::clone(&backend));
1186
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1187
+ crate::test_support::seed_version_head(
1188
+ storage.clone(),
1189
+ GLOBAL_VERSION_ID,
1190
+ "initial-commit",
1191
+ )
1192
+ .await;
1193
+
1194
+ let mut transaction = storage
1195
+ .begin_write_transaction()
1196
+ .await
1197
+ .expect("transaction should open");
1198
+ let rows = finalize_commit_rows(
1199
+ BTreeMap::from([(
1200
+ GLOBAL_VERSION_ID.to_string(),
1201
+ members(["change-a", "change-b"]),
1202
+ )]),
1203
+ BTreeMap::new(),
1204
+ &version_ctx,
1205
+ transaction.as_mut(),
1206
+ )
1207
+ .await
1208
+ .expect("global commit row should finalize");
1209
+
1210
+ assert_eq!(rows.commit_rows.len(), 1);
1211
+ assert_eq!(rows.version_heads.len(), 1);
1212
+ let row = &rows.commit_rows[0];
1213
+ assert_eq!(row.commit_id, "test-uuid-1");
1214
+ assert_eq!(row.change_id, "test-uuid-2");
1215
+ assert_eq!(row.created_at, "test-timestamp-1");
1216
+ assert_eq!(row.parent_commit_ids, vec!["initial-commit"]);
1217
+
1218
+ let version_head = &rows.version_heads[0];
1219
+ assert_eq!(version_head.version_id, GLOBAL_VERSION_ID);
1220
+ assert_eq!(version_head.commit_id, "test-uuid-1");
1221
+ }
1222
+
1223
+ #[tokio::test]
1224
+ async fn finalize_commit_rows_skips_empty_members() {
1225
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1226
+ let storage = StorageContext::new(Arc::clone(&backend));
1227
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1228
+ let mut transaction = storage
1229
+ .begin_write_transaction()
1230
+ .await
1231
+ .expect("transaction should open");
1232
+ let rows = finalize_commit_rows(
1233
+ BTreeMap::from([(
1234
+ GLOBAL_VERSION_ID.to_string(),
1235
+ StagedCommitMembers::default(),
1236
+ )]),
1237
+ BTreeMap::new(),
1238
+ &version_ctx,
1239
+ transaction.as_mut(),
1240
+ )
1241
+ .await
1242
+ .expect("empty members should be ignored");
1243
+
1244
+ assert!(rows.commit_rows.is_empty());
1245
+ assert!(rows.version_heads.is_empty());
1246
+ }
1247
+
1248
+ #[tokio::test]
1249
+ async fn finalize_commit_rows_uses_existing_version_ref_as_parent() {
1250
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1251
+ let storage = StorageContext::new(Arc::clone(&backend));
1252
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1253
+ crate::test_support::seed_version_head(storage.clone(), GLOBAL_VERSION_ID, "global-before")
1254
+ .await;
1255
+ crate::test_support::seed_version_head(storage.clone(), "version-a", "previous-commit")
1256
+ .await;
1257
+
1258
+ let mut transaction = storage
1259
+ .begin_write_transaction()
1260
+ .await
1261
+ .expect("transaction should open");
1262
+ let rows = finalize_commit_rows(
1263
+ BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
1264
+ BTreeMap::new(),
1265
+ &version_ctx,
1266
+ transaction.as_mut(),
1267
+ )
1268
+ .await
1269
+ .expect("active-version commit finalization should resolve parent");
1270
+
1271
+ assert_eq!(
1272
+ rows.commit_rows[0].parent_commit_ids,
1273
+ vec!["previous-commit"]
1274
+ );
1275
+ assert_eq!(rows.version_heads[0].version_id, "version-a");
1276
+ }
1277
+
1278
+ #[tokio::test]
1279
+ async fn finalize_commit_rows_appends_extra_merge_parent_after_target_head() {
1280
+ let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
1281
+ let storage = StorageContext::new(Arc::clone(&backend));
1282
+ let version_ctx = VersionContext::new(Arc::new(UntrackedStateContext::new()));
1283
+ crate::test_support::seed_version_head(storage.clone(), "version-a", "target-head").await;
1284
+
1285
+ let mut transaction = storage
1286
+ .begin_write_transaction()
1287
+ .await
1288
+ .expect("transaction should open");
1289
+ let rows = finalize_commit_rows(
1290
+ BTreeMap::from([("version-a".to_string(), members(["change-a"]))]),
1291
+ BTreeMap::from([("version-a".to_string(), vec!["source-head".to_string()])]),
1292
+ &version_ctx,
1293
+ transaction.as_mut(),
1294
+ )
1295
+ .await
1296
+ .expect("merge commit finalization should resolve parents");
1297
+
1298
+ assert_eq!(
1299
+ rows.commit_rows[0].parent_commit_ids,
1300
+ vec!["target-head", "source-head"]
1301
+ );
1302
+ }
1303
+
1304
+ fn members<const N: usize>(change_ids: [&str; N]) -> StagedCommitMembers {
1305
+ let mut members = StagedCommitMembers::new(
1306
+ "test-uuid-1".to_string(),
1307
+ "test-uuid-2".to_string(),
1308
+ "test-timestamp-1".to_string(),
1309
+ );
1310
+ for change_id in change_ids {
1311
+ members.add_change_id(change_id.to_string());
1312
+ }
1313
+ members
1314
+ }
1315
+
1316
+ fn tracked_global_row(change_id: &str) -> PreparedStateRow {
1317
+ tracked_version_row(GLOBAL_VERSION_ID, change_id)
1318
+ }
1319
+
1320
+ fn tracked_version_row(version_id: &str, change_id: &str) -> PreparedStateRow {
1321
+ PreparedStateRow {
1322
+ schema_plan_id: SchemaPlanId::for_test(0),
1323
+ facts: PreparedRowFacts::default(),
1324
+ entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
1325
+ schema_key: "test_schema".to_string(),
1326
+ file_id: None,
1327
+ snapshot: Some(
1328
+ crate::transaction::types::stage_json_from_value(
1329
+ crate::transaction::types::TransactionJson::from_value_for_test(
1330
+ serde_json::json!({ "value": 1 }),
1331
+ ),
1332
+ "test tracked row snapshot",
1333
+ )
1334
+ .expect("test snapshot should stage"),
1335
+ ),
1336
+ metadata: None,
1337
+ origin: None,
1338
+ created_at: "2026-01-01T00:00:00Z".to_string(),
1339
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
1340
+ global: version_id == GLOBAL_VERSION_ID,
1341
+ change_id: Some(change_id.to_string()),
1342
+ commit_id: Some("test-uuid-1".to_string()),
1343
+ untracked: false,
1344
+ version_id: version_id.to_string(),
1345
+ }
1346
+ }
1347
+
1348
+ fn untracked_global_row(change_id: &str) -> PreparedStateRow {
1349
+ let mut row = tracked_global_row(change_id);
1350
+ row.snapshot = Some(
1351
+ crate::transaction::types::stage_json_from_value(
1352
+ crate::transaction::types::TransactionJson::from_value_for_test(
1353
+ serde_json::json!({ "value": "untracked" }),
1354
+ ),
1355
+ "test untracked row snapshot",
1356
+ )
1357
+ .expect("test snapshot should stage"),
1358
+ );
1359
+ PreparedStateRow {
1360
+ change_id: None,
1361
+ commit_id: None,
1362
+ untracked: true,
1363
+ ..row
1364
+ }
1365
+ }
1366
+
1367
+ fn untracked_request() -> UntrackedStateRowRequest {
1368
+ UntrackedStateRowRequest {
1369
+ schema_key: "test_schema".to_string(),
1370
+ version_id: GLOBAL_VERSION_ID.to_string(),
1371
+ entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
1372
+ file_id: NullableKeyFilter::Null,
1373
+ }
1374
+ }
1375
+
1376
+ fn live_state_request() -> LiveStateRowRequest {
1377
+ LiveStateRowRequest {
1378
+ schema_key: "test_schema".to_string(),
1379
+ version_id: GLOBAL_VERSION_ID.to_string(),
1380
+ entity_id: crate::entity_identity::EntityIdentity::single("entity-1"),
1381
+ file_id: NullableKeyFilter::Null,
1382
+ }
1383
+ }
1384
+
1385
+ struct CountingBackend {
1386
+ inner: UnitTestBackend,
1387
+ write_batches: Arc<AtomicUsize>,
1388
+ }
1389
+
1390
+ impl CountingBackend {
1391
+ fn new() -> Self {
1392
+ Self {
1393
+ inner: UnitTestBackend::new(),
1394
+ write_batches: Arc::new(AtomicUsize::new(0)),
1395
+ }
1396
+ }
1397
+
1398
+ fn write_batches(&self) -> Arc<AtomicUsize> {
1399
+ Arc::clone(&self.write_batches)
1400
+ }
1401
+ }
1402
+
1403
+ #[async_trait]
1404
+ impl Backend for CountingBackend {
1405
+ async fn begin_read_transaction(
1406
+ &self,
1407
+ ) -> Result<Box<dyn BackendReadTransaction + Send + Sync + 'static>, LixError> {
1408
+ self.inner.begin_read_transaction().await
1409
+ }
1410
+
1411
+ async fn begin_write_transaction(
1412
+ &self,
1413
+ ) -> Result<Box<dyn BackendWriteTransaction + Send + Sync + 'static>, LixError> {
1414
+ Ok(Box::new(CountingWriteTransaction {
1415
+ inner: self.inner.begin_write_transaction().await?,
1416
+ write_batches: Arc::clone(&self.write_batches),
1417
+ }))
1418
+ }
1419
+ }
1420
+
1421
+ struct CountingWriteTransaction {
1422
+ inner: Box<dyn BackendWriteTransaction + Send + Sync + 'static>,
1423
+ write_batches: Arc<AtomicUsize>,
1424
+ }
1425
+
1426
+ #[async_trait]
1427
+ impl BackendReadTransaction for CountingWriteTransaction {
1428
+ async fn get_values(
1429
+ &mut self,
1430
+ request: BackendKvGetRequest,
1431
+ ) -> Result<BackendKvValueBatch, LixError> {
1432
+ self.inner.get_values(request).await
1433
+ }
1434
+
1435
+ async fn exists_many(
1436
+ &mut self,
1437
+ request: BackendKvGetRequest,
1438
+ ) -> Result<BackendKvExistsBatch, LixError> {
1439
+ self.inner.exists_many(request).await
1440
+ }
1441
+
1442
+ async fn scan_keys(
1443
+ &mut self,
1444
+ request: BackendKvScanRequest,
1445
+ ) -> Result<BackendKvKeyPage, LixError> {
1446
+ self.inner.scan_keys(request).await
1447
+ }
1448
+
1449
+ async fn scan_values(
1450
+ &mut self,
1451
+ request: BackendKvScanRequest,
1452
+ ) -> Result<BackendKvValuePage, LixError> {
1453
+ self.inner.scan_values(request).await
1454
+ }
1455
+
1456
+ async fn scan_entries(
1457
+ &mut self,
1458
+ request: BackendKvScanRequest,
1459
+ ) -> Result<BackendKvEntryPage, LixError> {
1460
+ self.inner.scan_entries(request).await
1461
+ }
1462
+
1463
+ async fn rollback(self: Box<Self>) -> Result<(), LixError> {
1464
+ let Self { inner, .. } = *self;
1465
+ inner.rollback().await
1466
+ }
1467
+ }
1468
+
1469
+ #[async_trait]
1470
+ impl BackendWriteTransaction for CountingWriteTransaction {
1471
+ async fn write_kv_batch(
1472
+ &mut self,
1473
+ batch: BackendKvWriteBatch,
1474
+ ) -> Result<BackendKvWriteStats, LixError> {
1475
+ self.write_batches.fetch_add(1, Ordering::SeqCst);
1476
+ self.inner.write_kv_batch(batch).await
1477
+ }
1478
+
1479
+ async fn commit(self: Box<Self>) -> Result<(), LixError> {
1480
+ let Self { inner, .. } = *self;
1481
+ inner.commit().await
1482
+ }
1483
+ }
1484
+ }