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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +38 -207
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +38 -71
  16. package/SKILL.md +0 -507
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -833
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -359
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/capabilities.rs +0 -67
  31. package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
  32. package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
  33. package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
  34. package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
  35. package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
  36. package/dist-engine-src/src/backend/conformance/model.rs +0 -28
  37. package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
  38. package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
  39. package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
  40. package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
  41. package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
  42. package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
  43. package/dist-engine-src/src/backend/conformance/write.rs +0 -16
  44. package/dist-engine-src/src/backend/error.rs +0 -94
  45. package/dist-engine-src/src/backend/in_memory.rs +0 -670
  46. package/dist-engine-src/src/backend/mod.rs +0 -39
  47. package/dist-engine-src/src/backend/predicate.rs +0 -80
  48. package/dist-engine-src/src/backend/traits.rs +0 -260
  49. package/dist-engine-src/src/backend/types.rs +0 -239
  50. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  51. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  52. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  53. package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
  54. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  55. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  56. package/dist-engine-src/src/branch/context.rs +0 -40
  57. package/dist-engine-src/src/branch/lifecycle.rs +0 -221
  58. package/dist-engine-src/src/branch/mod.rs +0 -13
  59. package/dist-engine-src/src/branch/refs.rs +0 -321
  60. package/dist-engine-src/src/branch/stage_rows.rs +0 -67
  61. package/dist-engine-src/src/branch/types.rs +0 -21
  62. package/dist-engine-src/src/catalog/context.rs +0 -412
  63. package/dist-engine-src/src/catalog/mod.rs +0 -10
  64. package/dist-engine-src/src/catalog/schema.rs +0 -4
  65. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  66. package/dist-engine-src/src/cel/context.rs +0 -86
  67. package/dist-engine-src/src/cel/error.rs +0 -19
  68. package/dist-engine-src/src/cel/mod.rs +0 -8
  69. package/dist-engine-src/src/cel/provider.rs +0 -9
  70. package/dist-engine-src/src/cel/runtime.rs +0 -167
  71. package/dist-engine-src/src/cel/value.rs +0 -50
  72. package/dist-engine-src/src/changelog/bench_support.rs +0 -785
  73. package/dist-engine-src/src/changelog/change.rs +0 -1
  74. package/dist-engine-src/src/changelog/codec.rs +0 -497
  75. package/dist-engine-src/src/changelog/commit.rs +0 -1
  76. package/dist-engine-src/src/changelog/context.rs +0 -1614
  77. package/dist-engine-src/src/changelog/mod.rs +0 -29
  78. package/dist-engine-src/src/changelog/store.rs +0 -163
  79. package/dist-engine-src/src/changelog/test_support.rs +0 -54
  80. package/dist-engine-src/src/changelog/types.rs +0 -213
  81. package/dist-engine-src/src/commit_graph/context.rs +0 -944
  82. package/dist-engine-src/src/commit_graph/mod.rs +0 -9
  83. package/dist-engine-src/src/commit_graph/types.rs +0 -89
  84. package/dist-engine-src/src/commit_graph/walker.rs +0 -786
  85. package/dist-engine-src/src/common/error.rs +0 -347
  86. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  87. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  88. package/dist-engine-src/src/common/identity.rs +0 -145
  89. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  90. package/dist-engine-src/src/common/metadata.rs +0 -40
  91. package/dist-engine-src/src/common/mod.rs +0 -23
  92. package/dist-engine-src/src/common/types.rs +0 -105
  93. package/dist-engine-src/src/common/wire.rs +0 -222
  94. package/dist-engine-src/src/domain.rs +0 -320
  95. package/dist-engine-src/src/engine.rs +0 -203
  96. package/dist-engine-src/src/entity_pk.rs +0 -402
  97. package/dist-engine-src/src/functions/context.rs +0 -296
  98. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  99. package/dist-engine-src/src/functions/mod.rs +0 -18
  100. package/dist-engine-src/src/functions/provider.rs +0 -130
  101. package/dist-engine-src/src/functions/state.rs +0 -335
  102. package/dist-engine-src/src/functions/types.rs +0 -37
  103. package/dist-engine-src/src/init.rs +0 -692
  104. package/dist-engine-src/src/json_store/compression.rs +0 -77
  105. package/dist-engine-src/src/json_store/context.rs +0 -172
  106. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  107. package/dist-engine-src/src/json_store/mod.rs +0 -38
  108. package/dist-engine-src/src/json_store/store.rs +0 -494
  109. package/dist-engine-src/src/json_store/types.rs +0 -212
  110. package/dist-engine-src/src/lib.rs +0 -92
  111. package/dist-engine-src/src/live_state/context.rs +0 -1883
  112. package/dist-engine-src/src/live_state/mod.rs +0 -21
  113. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  114. package/dist-engine-src/src/live_state/reader.rs +0 -23
  115. package/dist-engine-src/src/live_state/types.rs +0 -231
  116. package/dist-engine-src/src/live_state/visibility.rs +0 -666
  117. package/dist-engine-src/src/plugin/archive.rs +0 -438
  118. package/dist-engine-src/src/plugin/component.rs +0 -183
  119. package/dist-engine-src/src/plugin/install.rs +0 -619
  120. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  121. package/dist-engine-src/src/plugin/materializer.rs +0 -202
  122. package/dist-engine-src/src/plugin/mod.rs +0 -33
  123. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
  124. package/dist-engine-src/src/plugin/storage.rs +0 -74
  125. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  126. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  127. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  128. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  129. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  130. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
  131. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
  132. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  133. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  134. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  135. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  136. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  137. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  138. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  139. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  140. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  141. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  142. package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
  143. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  144. package/dist-engine-src/src/schema/definition.json +0 -187
  145. package/dist-engine-src/src/schema/definition.rs +0 -742
  146. package/dist-engine-src/src/schema/key.rs +0 -138
  147. package/dist-engine-src/src/schema/mod.rs +0 -20
  148. package/dist-engine-src/src/schema/seed.rs +0 -14
  149. package/dist-engine-src/src/schema/tests.rs +0 -780
  150. package/dist-engine-src/src/session/context.rs +0 -1059
  151. package/dist-engine-src/src/session/create_branch.rs +0 -94
  152. package/dist-engine-src/src/session/execute.rs +0 -681
  153. package/dist-engine-src/src/session/merge/analysis.rs +0 -108
  154. package/dist-engine-src/src/session/merge/branch.rs +0 -417
  155. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  156. package/dist-engine-src/src/session/merge/mod.rs +0 -10
  157. package/dist-engine-src/src/session/merge/stats.rs +0 -61
  158. package/dist-engine-src/src/session/mod.rs +0 -30
  159. package/dist-engine-src/src/session/switch_branch.rs +0 -113
  160. package/dist-engine-src/src/session/transaction.rs +0 -557
  161. package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
  162. package/dist-engine-src/src/sql2/bind/error.rs +0 -5
  163. package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
  164. package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
  165. package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
  166. package/dist-engine-src/src/sql2/bind/read.rs +0 -65
  167. package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
  168. package/dist-engine-src/src/sql2/bind/table.rs +0 -273
  169. package/dist-engine-src/src/sql2/bind/write.rs +0 -86
  170. package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
  171. package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
  172. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
  173. package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
  174. package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
  175. package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
  176. package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
  177. package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
  178. package/dist-engine-src/src/sql2/context.rs +0 -317
  179. package/dist-engine-src/src/sql2/dml.rs +0 -148
  180. package/dist-engine-src/src/sql2/error.rs +0 -215
  181. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
  182. package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
  183. package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
  184. package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
  185. package/dist-engine-src/src/sql2/exec/write.rs +0 -661
  186. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
  187. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  188. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  189. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  190. package/dist-engine-src/src/sql2/history_route.rs +0 -661
  191. package/dist-engine-src/src/sql2/mod.rs +0 -52
  192. package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
  193. package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
  194. package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
  195. package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
  196. package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
  197. package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
  198. package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
  199. package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
  200. package/dist-engine-src/src/sql2/plan/write.rs +0 -147
  201. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
  202. package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
  203. package/dist-engine-src/src/sql2/providers/change.rs +0 -445
  204. package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
  205. package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
  206. package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
  207. package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
  208. package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
  209. package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
  210. package/dist-engine-src/src/sql2/providers/history.rs +0 -426
  211. package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
  212. package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
  213. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  214. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  215. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  216. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  217. package/dist-engine-src/src/sql2/session.rs +0 -83
  218. package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
  219. package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
  220. package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
  221. package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
  222. package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
  223. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  224. package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
  225. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  226. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  227. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  228. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  229. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  230. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  231. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  232. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  233. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
  234. package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
  235. package/dist-engine-src/src/storage/conformance.rs +0 -399
  236. package/dist-engine-src/src/storage/context.rs +0 -620
  237. package/dist-engine-src/src/storage/mod.rs +0 -52
  238. package/dist-engine-src/src/storage/point.rs +0 -440
  239. package/dist-engine-src/src/storage/read_scope.rs +0 -67
  240. package/dist-engine-src/src/storage/reader.rs +0 -867
  241. package/dist-engine-src/src/storage/scan.rs +0 -784
  242. package/dist-engine-src/src/storage/spaces.rs +0 -236
  243. package/dist-engine-src/src/storage/stats.rs +0 -80
  244. package/dist-engine-src/src/storage/write_set.rs +0 -962
  245. package/dist-engine-src/src/storage_bench.rs +0 -171
  246. package/dist-engine-src/src/test_support.rs +0 -450
  247. package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
  248. package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
  249. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
  250. package/dist-engine-src/src/tracked_state/context.rs +0 -2801
  251. package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
  252. package/dist-engine-src/src/tracked_state/merge.rs +0 -478
  253. package/dist-engine-src/src/tracked_state/mod.rs +0 -35
  254. package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
  255. package/dist-engine-src/src/tracked_state/storage.rs +0 -427
  256. package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
  257. package/dist-engine-src/src/tracked_state/types.rs +0 -238
  258. package/dist-engine-src/src/transaction/bench_support.rs +0 -407
  259. package/dist-engine-src/src/transaction/commit.rs +0 -1592
  260. package/dist-engine-src/src/transaction/context.rs +0 -1653
  261. package/dist-engine-src/src/transaction/mod.rs +0 -24
  262. package/dist-engine-src/src/transaction/normalization.rs +0 -877
  263. package/dist-engine-src/src/transaction/prep.rs +0 -37
  264. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
  265. package/dist-engine-src/src/transaction/staging.rs +0 -1525
  266. package/dist-engine-src/src/transaction/types.rs +0 -403
  267. package/dist-engine-src/src/transaction/validation.rs +0 -5766
  268. package/dist-engine-src/src/untracked_state/codec.rs +0 -615
  269. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  270. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  271. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  272. package/dist-engine-src/src/untracked_state/storage.rs +0 -898
  273. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  274. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,1592 +0,0 @@
1
- use crate::binary_cas::BinaryCasContext;
2
- use crate::branch::{BranchContext, BranchRefReader};
3
- use crate::changelog::{
4
- ChangeRecord, ChangelogAppend, ChangelogContext, ChangelogWriter, CommitChangeRef,
5
- CommitChangeRefSet, CommitRecord,
6
- };
7
- use crate::entity_pk::EntityPk;
8
- use crate::functions::FunctionContext;
9
- use crate::json_store::{JsonRef, JsonStoreContext, JsonWritePlacementRef, NormalizedJsonRef};
10
- use crate::storage::{StorageRead, StorageWriteSet};
11
- use crate::tracked_state::{TrackedStateContext, TrackedStateDeltaRef};
12
- use crate::transaction::prepare_branch_ref_row;
13
- use crate::transaction::staging::PreparedWriteSet;
14
- use crate::transaction::types::{PreparedStateRow, StagedCommitChangeRef, StagedCommitChangeRefs};
15
- use crate::untracked_state::{
16
- UntrackedStateContext, UntrackedStateIdentity, UntrackedStateIdentityRef, UntrackedStateRowRef,
17
- };
18
- use crate::LixError;
19
- use std::collections::{BTreeMap, BTreeSet};
20
-
21
- type RowIndex = usize;
22
-
23
- /// Commits prepared transaction rows into durable tracked and untracked stores.
24
- ///
25
- /// Providers decode DataFusion DML into hydrated `PreparedStateRow`s. Untracked
26
- /// rows are durable local overlay state and bypass changelog change refs. Tracked
27
- /// rows stage canonical changelog facts, then update the live-state serving
28
- /// commit root. The tracked side of that commit root is a prolly root keyed by
29
- /// the new commit id.
30
- pub(crate) async fn commit_prepared_writes(
31
- binary_cas: &BinaryCasContext,
32
- branch_ctx: &BranchContext,
33
- runtime_functions: Option<&FunctionContext>,
34
- read: &mut (impl StorageRead + Send + Sync),
35
- prepared_writes: PreparedWriteSet,
36
- ) -> Result<StorageWriteSet, LixError> {
37
- let mut writes = StorageWriteSet::new();
38
- let mut json_writer = JsonStoreContext::new().writer();
39
-
40
- if !prepared_writes.file_data_writes.is_empty() {
41
- let mut blob_writer = binary_cas.writer(&mut writes);
42
- for write in &prepared_writes.file_data_writes {
43
- blob_writer.stage_bytes(&write.data)?;
44
- }
45
- }
46
-
47
- let state_rows = prepared_writes.state_rows;
48
- let finalized = finalize_commit_rows(
49
- prepared_writes.commit_change_refs_by_branch,
50
- prepared_writes.extra_commit_parents_by_branch,
51
- branch_ctx,
52
- &*read,
53
- )
54
- .await?;
55
- let commit_rows = finalized.commit_rows;
56
- let branch_heads = finalized.branch_heads;
57
- let tracked_roots = finalized.tracked_roots;
58
- let row_index = index_prepared_rows(&state_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
- && commit_rows.is_empty()
68
- && branch_heads.is_empty()
69
- && writes.is_empty()
70
- {
71
- return Ok(writes);
72
- }
73
-
74
- let staged_commits = stage_changelog_commits(
75
- read,
76
- &mut writes,
77
- &state_rows,
78
- &row_index.tracked_row_indices_by_commit,
79
- &commit_rows,
80
- )
81
- .await?;
82
-
83
- stage_state_json_payloads(
84
- &mut json_writer,
85
- &mut writes,
86
- &state_rows,
87
- &row_index.canonical_row_indices,
88
- )?;
89
-
90
- // The serving commit root is updated in the same backend transaction as the
91
- // changelog append. Tracked rows become prolly mutations under their owning
92
- // commit root; untracked rows remain in the separate local overlay store.
93
- {
94
- let untracked_overlay_delete_identities = existing_untracked_overlay_delete_identities(
95
- &*read,
96
- row_index
97
- .canonical_row_indices
98
- .iter()
99
- .map(|&row_index| untracked_identity_ref_from_state_row(&state_rows[row_index])),
100
- )
101
- .await?;
102
- UntrackedStateContext::new()
103
- .writer(&mut writes)
104
- .stage_rows(
105
- row_index
106
- .untracked_row_indices
107
- .iter()
108
- .map(|&row_index| untracked_row_ref_from_state_row(&state_rows[row_index])),
109
- )?;
110
- UntrackedStateContext::new()
111
- .writer(&mut writes)
112
- .stage_delete_rows(
113
- untracked_overlay_delete_identities
114
- .iter()
115
- .map(UntrackedStateIdentity::as_ref),
116
- )?;
117
- stage_tracked_roots(
118
- read,
119
- &mut writes,
120
- &state_rows,
121
- row_index.tracked_row_indices_by_commit,
122
- tracked_roots,
123
- staged_commits,
124
- )
125
- .await?;
126
- }
127
-
128
- for branch_head in branch_heads {
129
- let canonical_row = prepare_branch_ref_row(
130
- &branch_head.branch_id,
131
- &branch_head.commit_id,
132
- &branch_head.timestamp,
133
- )?;
134
- branch_ctx.stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
135
- }
136
-
137
- Ok(writes)
138
- }
139
-
140
- fn stage_state_json_payloads(
141
- json_writer: &mut crate::json_store::JsonStoreWriter,
142
- writes: &mut StorageWriteSet,
143
- state_rows: &[PreparedStateRow],
144
- row_indices: &[RowIndex],
145
- ) -> Result<(), LixError> {
146
- json_writer.stage_batch(
147
- writes,
148
- JsonWritePlacementRef::OutOfBand,
149
- row_indices
150
- .iter()
151
- .flat_map(|&row_index| json_payloads_from_state_row(&state_rows[row_index])),
152
- )?;
153
- Ok(())
154
- }
155
-
156
- fn json_payloads_from_state_row(
157
- row: &PreparedStateRow,
158
- ) -> impl Iterator<Item = NormalizedJsonRef<'_>> {
159
- row.snapshot
160
- .iter()
161
- .chain(row.metadata.iter())
162
- .map(|json| NormalizedJsonRef::trusted_prehashed(json.normalized.as_ref(), json.json_ref))
163
- }
164
-
165
- async fn existing_untracked_overlay_delete_identities<'a>(
166
- read: &(impl StorageRead + Send + Sync + ?Sized),
167
- identities: impl IntoIterator<Item = UntrackedStateIdentityRef<'a>>,
168
- ) -> Result<Vec<UntrackedStateIdentity>, LixError> {
169
- UntrackedStateContext::new()
170
- .reader(read)
171
- .existing_identities(identities)
172
- .await
173
- }
174
-
175
- struct PreparedRowIndex {
176
- canonical_row_indices: Vec<RowIndex>,
177
- untracked_row_indices: Vec<RowIndex>,
178
- tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
179
- }
180
-
181
- fn index_prepared_rows(rows: &[PreparedStateRow]) -> Result<PreparedRowIndex, LixError> {
182
- let mut canonical_row_indices = Vec::new();
183
- let mut untracked_row_indices = Vec::new();
184
- let mut tracked_row_indices_by_commit = BTreeMap::<String, Vec<RowIndex>>::new();
185
-
186
- for (row_index, row) in rows.iter().enumerate() {
187
- if row.untracked {
188
- untracked_row_indices.push(row_index);
189
- continue;
190
- }
191
- let Some(commit_id) = row.commit_id.as_ref() else {
192
- return Err(LixError::new(
193
- LixError::CODE_INTERNAL_ERROR,
194
- "tracked prepared row is missing commit_id before commit indexing",
195
- ));
196
- };
197
- canonical_row_indices.push(row_index);
198
- tracked_row_indices_by_commit
199
- .entry(commit_id.clone())
200
- .or_default()
201
- .push(row_index);
202
- }
203
-
204
- Ok(PreparedRowIndex {
205
- canonical_row_indices,
206
- untracked_row_indices,
207
- tracked_row_indices_by_commit,
208
- })
209
- }
210
-
211
- #[derive(Clone, Debug)]
212
- struct StagedChangelogCommit {
213
- change_ids: Vec<String>,
214
- selected_change_refs: Vec<StagedCommitChangeRef>,
215
- commit_change_id: String,
216
- commit_created_at: String,
217
- }
218
-
219
- async fn stage_changelog_commits(
220
- read: &mut (impl StorageRead + Send + Sync),
221
- writes: &mut StorageWriteSet,
222
- state_rows: &[PreparedStateRow],
223
- tracked_row_indices_by_commit: &BTreeMap<String, Vec<RowIndex>>,
224
- commit_rows: &[FinalizedCommitRow],
225
- ) -> Result<BTreeMap<String, StagedChangelogCommit>, LixError> {
226
- if commit_rows.is_empty() {
227
- return Ok(BTreeMap::new());
228
- }
229
-
230
- let mut commits = Vec::with_capacity(commit_rows.len());
231
- let mut changes = Vec::new();
232
- let mut commit_change_refs = Vec::with_capacity(commit_rows.len());
233
- let mut staged = BTreeMap::<String, StagedChangelogCommit>::new();
234
- for commit_row in commit_rows {
235
- let state_row_indices = tracked_row_indices_by_commit
236
- .get(&commit_row.commit_id)
237
- .map(Vec::as_slice)
238
- .unwrap_or_default();
239
- let mut refs = Vec::with_capacity(state_row_indices.len());
240
- let mut change_ids =
241
- Vec::with_capacity(state_row_indices.len() + commit_row.selected_change_refs.len());
242
- for &row_index in state_row_indices {
243
- let row = &state_rows[row_index];
244
- let change_id = row.change_id.as_ref().ok_or_else(|| {
245
- LixError::new(
246
- LixError::CODE_INTERNAL_ERROR,
247
- "tracked staged row is missing change_id before changelog append",
248
- )
249
- })?;
250
- refs.push(commit_change_ref_from_state_row(row, change_id));
251
- change_ids.push(change_id.clone());
252
- changes.push(change_record_from_state_row(row)?);
253
- }
254
- for change_ref in &commit_row.selected_change_refs {
255
- refs.push(commit_change_ref_from_selected_change_ref(change_ref));
256
- change_ids.push(change_ref.change_id.clone());
257
- }
258
- commits.push(CommitRecord {
259
- format_version: 1,
260
- commit_id: commit_row.commit_id.clone(),
261
- parent_commit_ids: commit_row.parent_commit_ids.clone(),
262
- change_id: commit_row.change_id.clone(),
263
- author_account_ids: Vec::new(),
264
- created_at: commit_row.created_at.clone(),
265
- });
266
- commit_change_refs.push(CommitChangeRefSet {
267
- commit_id: commit_row.commit_id.clone(),
268
- entries: refs,
269
- });
270
- staged.insert(
271
- commit_row.commit_id.clone(),
272
- StagedChangelogCommit {
273
- change_ids,
274
- selected_change_refs: commit_row.selected_change_refs.clone(),
275
- commit_change_id: commit_row.change_id.clone(),
276
- commit_created_at: commit_row.created_at.clone(),
277
- },
278
- );
279
- }
280
-
281
- let append = ChangelogAppend {
282
- commits,
283
- changes,
284
- commit_change_refs,
285
- };
286
-
287
- let mut writer = ChangelogContext::new().writer(read, writes);
288
- writer.stage_append(append).await?;
289
- stage_commit_row_json_payloads(writes, commit_rows)?;
290
- Ok(staged)
291
- }
292
-
293
- fn change_record_from_state_row(row: &PreparedStateRow) -> Result<ChangeRecord, LixError> {
294
- let Some(change_id) = row.change_id.as_ref() else {
295
- return Err(LixError::new(
296
- LixError::CODE_INTERNAL_ERROR,
297
- "tracked staged row is missing change_id before changelog change construction",
298
- ));
299
- };
300
- Ok(ChangeRecord {
301
- format_version: 1,
302
- change_id: change_id.clone(),
303
- entity_pk: row.entity_pk.clone(),
304
- schema_key: row.schema_key.clone(),
305
- file_id: row.file_id.clone(),
306
- snapshot_ref: row.snapshot.as_ref().map(|snapshot| snapshot.json_ref),
307
- metadata_ref: row.metadata.as_ref().map(|metadata| metadata.json_ref),
308
- created_at: row.updated_at.clone(),
309
- })
310
- }
311
-
312
- fn stage_commit_row_json_payloads(
313
- writes: &mut StorageWriteSet,
314
- commit_rows: &[FinalizedCommitRow],
315
- ) -> Result<(), LixError> {
316
- let snapshots = commit_rows
317
- .iter()
318
- .map(|row| commit_row_snapshot_content(&row.commit_id))
319
- .collect::<Result<Vec<_>, _>>()?;
320
- JsonStoreContext::new().writer().stage_batch(
321
- writes,
322
- JsonWritePlacementRef::OutOfBand,
323
- snapshots
324
- .iter()
325
- .map(|snapshot| NormalizedJsonRef::new(snapshot.as_str())),
326
- )?;
327
- Ok(())
328
- }
329
-
330
- fn commit_row_snapshot_content(commit_id: &str) -> Result<String, LixError> {
331
- serde_json::to_string(&serde_json::json!({
332
- "id": commit_id,
333
- }))
334
- .map_err(|error| {
335
- LixError::new(
336
- LixError::CODE_INTERNAL_ERROR,
337
- format!("failed to encode lix_commit snapshot: {error}"),
338
- )
339
- })
340
- }
341
-
342
- fn commit_change_ref_from_state_row(row: &PreparedStateRow, change_id: &str) -> CommitChangeRef {
343
- CommitChangeRef {
344
- schema_key: row.schema_key.clone(),
345
- file_id: row.file_id.clone(),
346
- entity_pk: row.entity_pk.clone(),
347
- change_id: change_id.to_string(),
348
- }
349
- }
350
-
351
- fn commit_change_ref_from_selected_change_ref(
352
- change_ref: &StagedCommitChangeRef,
353
- ) -> CommitChangeRef {
354
- CommitChangeRef {
355
- schema_key: change_ref.schema_key.clone(),
356
- file_id: change_ref.file_id.clone(),
357
- entity_pk: change_ref.entity_pk.clone(),
358
- change_id: change_ref.change_id.clone(),
359
- }
360
- }
361
-
362
- fn tracked_delta_from_state_row(
363
- row: &PreparedStateRow,
364
- ) -> Result<TrackedStateDeltaRef<'_>, LixError> {
365
- let Some(change_id) = row.change_id.as_deref() else {
366
- return Err(LixError::new(
367
- LixError::CODE_INTERNAL_ERROR,
368
- "tracked staged row is missing change_id before tracked root staging",
369
- ));
370
- };
371
- let Some(commit_id) = row.commit_id.as_deref() else {
372
- return Err(LixError::new(
373
- LixError::CODE_INTERNAL_ERROR,
374
- "tracked staged row is missing commit_id before tracked root staging",
375
- ));
376
- };
377
- Ok(TrackedStateDeltaRef {
378
- schema_key: &row.schema_key,
379
- file_id: row.file_id.as_deref(),
380
- entity_pk: &row.entity_pk,
381
- change_id,
382
- commit_id,
383
- snapshot_ref: row.snapshot.as_ref().map(|snapshot| &snapshot.json_ref),
384
- metadata_ref: row.metadata.as_ref().map(|metadata| &metadata.json_ref),
385
- deleted: row.snapshot.is_none(),
386
- created_at: &row.created_at,
387
- updated_at: &row.updated_at,
388
- })
389
- }
390
-
391
- fn tracked_delta_from_selected_change_ref<'a>(
392
- change_ref: &'a StagedCommitChangeRef,
393
- commit_id: &'a str,
394
- ) -> Result<TrackedStateDeltaRef<'a>, LixError> {
395
- Ok(TrackedStateDeltaRef {
396
- schema_key: &change_ref.schema_key,
397
- file_id: change_ref.file_id.as_deref(),
398
- entity_pk: &change_ref.entity_pk,
399
- change_id: &change_ref.change_id,
400
- commit_id,
401
- snapshot_ref: change_ref.snapshot_ref.as_ref(),
402
- metadata_ref: change_ref.metadata_ref.as_ref(),
403
- deleted: change_ref.deleted,
404
- created_at: &change_ref.created_at,
405
- updated_at: &change_ref.updated_at,
406
- })
407
- }
408
-
409
- async fn stage_tracked_roots(
410
- read: &(impl StorageRead + Send + Sync + ?Sized),
411
- writes: &mut StorageWriteSet,
412
- state_rows: &[PreparedStateRow],
413
- tracked_row_indices_by_commit: BTreeMap<String, Vec<RowIndex>>,
414
- tracked_roots: Vec<PendingTrackedRoot>,
415
- staged_commits: BTreeMap<String, StagedChangelogCommit>,
416
- ) -> Result<(), LixError> {
417
- let tracked_state = TrackedStateContext::new();
418
- let mut tracked_writer = tracked_state.writer(read, writes);
419
- for root in tracked_roots_parent_first(&tracked_roots)? {
420
- let staged = staged_commits.get(&root.commit_id).ok_or_else(|| {
421
- LixError::new(
422
- LixError::CODE_INTERNAL_ERROR,
423
- format!(
424
- "tracked-state root for commit '{}' has no staged changelog facts",
425
- root.commit_id
426
- ),
427
- )
428
- })?;
429
- let state_row_indices = tracked_row_indices_by_commit
430
- .get(&root.commit_id)
431
- .map(Vec::as_slice)
432
- .unwrap_or_default();
433
- if state_row_indices.len() > staged.change_ids.len() {
434
- return Err(LixError::new(
435
- LixError::CODE_INTERNAL_ERROR,
436
- format!(
437
- "commit '{}' has {} tracked rows but only {} changelog changes",
438
- root.commit_id,
439
- state_row_indices.len(),
440
- staged.change_ids.len()
441
- ),
442
- ));
443
- }
444
- let commit_snapshot = commit_row_snapshot_content(&root.commit_id)?;
445
- let commit_snapshot_ref = JsonRef::for_content(commit_snapshot.as_bytes());
446
- let commit_entity_pk = EntityPk::single(root.commit_id.clone());
447
- let mut deltas = state_row_indices
448
- .iter()
449
- .map(|&row_index| tracked_delta_from_state_row(&state_rows[row_index]))
450
- .chain(staged.selected_change_refs.iter().map(|change_ref| {
451
- tracked_delta_from_selected_change_ref(change_ref, &root.commit_id)
452
- }))
453
- .collect::<Result<Vec<_>, _>>()?;
454
- deltas.push(TrackedStateDeltaRef {
455
- schema_key: "lix_commit",
456
- file_id: None,
457
- entity_pk: &commit_entity_pk,
458
- change_id: &staged.commit_change_id,
459
- commit_id: &root.commit_id,
460
- snapshot_ref: Some(&commit_snapshot_ref),
461
- metadata_ref: None,
462
- deleted: false,
463
- created_at: &staged.commit_created_at,
464
- updated_at: &staged.commit_created_at,
465
- });
466
- tracked_writer
467
- .stage_commit_root(&root.commit_id, root.parent_commit_id.as_deref(), deltas)
468
- .await?;
469
- }
470
- let rooted_commit_ids = tracked_roots
471
- .iter()
472
- .map(|root| root.commit_id.as_str())
473
- .collect::<BTreeSet<_>>();
474
- let extra_tracked = tracked_row_indices_by_commit
475
- .keys()
476
- .filter(|commit_id| !rooted_commit_ids.contains(commit_id.as_str()))
477
- .cloned()
478
- .collect::<BTreeSet<_>>();
479
- if !extra_tracked.is_empty() {
480
- let mut commit_ids = tracked_row_indices_by_commit
481
- .keys()
482
- .cloned()
483
- .collect::<Vec<_>>();
484
- commit_ids.sort();
485
- commit_ids.dedup();
486
- return Err(LixError::new(
487
- LixError::CODE_INTERNAL_ERROR,
488
- format!(
489
- "tracked live_state rows have no finalized root metadata for commit ids: {}",
490
- commit_ids.join(", ")
491
- ),
492
- ));
493
- }
494
- if !staged_commits.is_empty() {
495
- let commit_ids = staged_commits
496
- .keys()
497
- .filter(|commit_id| !rooted_commit_ids.contains(commit_id.as_str()))
498
- .cloned()
499
- .collect::<Vec<_>>();
500
- if commit_ids.is_empty() {
501
- return Ok(());
502
- }
503
- return Err(LixError::new(
504
- LixError::CODE_INTERNAL_ERROR,
505
- format!(
506
- "changelog staged commits without tracked root metadata: {}",
507
- commit_ids.join(", ")
508
- ),
509
- ));
510
- }
511
- Ok(())
512
- }
513
-
514
- fn tracked_roots_parent_first(
515
- tracked_roots: &[PendingTrackedRoot],
516
- ) -> Result<Vec<&PendingTrackedRoot>, LixError> {
517
- let mut roots_by_id = BTreeMap::new();
518
- for root in tracked_roots {
519
- if roots_by_id.insert(root.commit_id.as_str(), root).is_some() {
520
- return Err(LixError::unknown(format!(
521
- "cannot stage duplicate tracked_state root '{}'",
522
- root.commit_id
523
- )));
524
- }
525
- }
526
-
527
- let mut ordered = Vec::with_capacity(tracked_roots.len());
528
- let mut visiting = BTreeSet::new();
529
- let mut visited = BTreeSet::new();
530
- for root in tracked_roots {
531
- visit_tracked_root_parent_first(
532
- root.commit_id.as_str(),
533
- &roots_by_id,
534
- &mut visiting,
535
- &mut visited,
536
- &mut ordered,
537
- )?;
538
- }
539
- Ok(ordered)
540
- }
541
-
542
- fn visit_tracked_root_parent_first<'a>(
543
- commit_id: &str,
544
- roots_by_id: &BTreeMap<&'a str, &'a PendingTrackedRoot>,
545
- visiting: &mut BTreeSet<&'a str>,
546
- visited: &mut BTreeSet<&'a str>,
547
- ordered: &mut Vec<&'a PendingTrackedRoot>,
548
- ) -> Result<(), LixError> {
549
- if visited.contains(commit_id) {
550
- return Ok(());
551
- }
552
- let Some(root) = roots_by_id.get(commit_id).copied() else {
553
- return Ok(());
554
- };
555
- if !visiting.insert(root.commit_id.as_str()) {
556
- return Err(LixError::unknown(format!(
557
- "cannot stage tracked_state root '{}' because staged root parents contain a cycle",
558
- root.commit_id
559
- )));
560
- }
561
- if let Some(parent_id) = root.parent_commit_id.as_deref() {
562
- if roots_by_id.contains_key(parent_id) {
563
- visit_tracked_root_parent_first(parent_id, roots_by_id, visiting, visited, ordered)?;
564
- }
565
- }
566
- visiting.remove(root.commit_id.as_str());
567
- visited.insert(root.commit_id.as_str());
568
- ordered.push(root);
569
- Ok(())
570
- }
571
-
572
- fn untracked_row_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateRowRef<'_> {
573
- UntrackedStateRowRef {
574
- entity_pk: &row.entity_pk,
575
- schema_key: &row.schema_key,
576
- file_id: row.file_id.as_deref(),
577
- snapshot_content: row
578
- .snapshot
579
- .as_ref()
580
- .map(|snapshot| snapshot.normalized.as_ref()),
581
- metadata: row
582
- .metadata
583
- .as_ref()
584
- .map(|metadata| metadata.normalized.as_ref()),
585
- created_at: &row.created_at,
586
- updated_at: &row.updated_at,
587
- global: row.global,
588
- branch_id: &row.branch_id,
589
- }
590
- }
591
-
592
- fn untracked_identity_ref_from_state_row(row: &PreparedStateRow) -> UntrackedStateIdentityRef<'_> {
593
- UntrackedStateIdentityRef {
594
- branch_id: &row.branch_id,
595
- schema_key: &row.schema_key,
596
- entity_pk: &row.entity_pk,
597
- file_id: row.file_id.as_deref(),
598
- }
599
- }
600
-
601
- /// Materializes tracked staged change refs into changelog commits.
602
- ///
603
- /// Staging only accumulates `branch_id -> change_ids` because commit ids,
604
- /// parent heads, and commit-row timestamps belong to transaction finalization.
605
- /// The `change_ids` list is the ordered set of canonical changes whose effects
606
- /// the commit introduces relative to its first parent.
607
- /// This function turns those change-ref sets into finalized commit facts.
608
- ///
609
- /// Commit finalization output split by durability target.
610
- ///
611
- /// `commit_rows` are canonical changelog commit facts. tracked_state roots store
612
- /// serving commit roots keyed by the corresponding commit id.
613
- ///
614
- /// `branch_heads` are moving refs. They are written through `BranchContext`,
615
- /// not the canonical changelog.
616
- struct FinalizedCommitRows {
617
- commit_rows: Vec<FinalizedCommitRow>,
618
- branch_heads: Vec<PendingBranchHead>,
619
- tracked_roots: Vec<PendingTrackedRoot>,
620
- }
621
-
622
- struct FinalizedCommitRow {
623
- commit_id: String,
624
- parent_commit_ids: Vec<String>,
625
- created_at: String,
626
- change_id: String,
627
- selected_change_refs: Vec<StagedCommitChangeRef>,
628
- }
629
-
630
- struct PendingBranchHead {
631
- branch_id: String,
632
- commit_id: String,
633
- timestamp: String,
634
- }
635
-
636
- struct PendingTrackedRoot {
637
- commit_id: String,
638
- parent_commit_id: Option<String>,
639
- }
640
-
641
- async fn finalize_commit_rows(
642
- commit_change_refs_by_branch: BTreeMap<String, StagedCommitChangeRefs>,
643
- extra_commit_parents_by_branch: BTreeMap<String, Vec<String>>,
644
- branch_ctx: &BranchContext,
645
- read: &(impl StorageRead + Send + Sync + ?Sized),
646
- ) -> Result<FinalizedCommitRows, LixError> {
647
- let mut commit_rows = Vec::new();
648
- let mut branch_heads = Vec::new();
649
- let mut tracked_roots = Vec::new();
650
-
651
- for (branch_id, change_refs) in commit_change_refs_by_branch {
652
- if change_refs.is_empty() && !change_refs.allow_empty {
653
- continue;
654
- }
655
-
656
- let commit_id = change_refs.commit_id;
657
- let commit_change_id = change_refs.commit_change_id;
658
- let timestamp = change_refs.created_at;
659
- let selected_change_refs = change_refs.selected_change_refs;
660
- let parent_commit_ids = branch_ctx
661
- .ref_reader(read)
662
- .load_head_commit_id(&branch_id)
663
- .await?
664
- .into_iter()
665
- .collect::<Vec<_>>();
666
- let parent_commit_ids = merge_parent_commit_ids(
667
- parent_commit_ids,
668
- extra_commit_parents_by_branch
669
- .get(&branch_id)
670
- .cloned()
671
- .unwrap_or_default(),
672
- );
673
- let parent_commit_id = parent_commit_ids.first().cloned();
674
-
675
- commit_rows.push(FinalizedCommitRow {
676
- commit_id: commit_id.clone(),
677
- parent_commit_ids: parent_commit_ids.clone(),
678
- created_at: timestamp.clone(),
679
- change_id: commit_change_id,
680
- selected_change_refs,
681
- });
682
- branch_heads.push(PendingBranchHead {
683
- branch_id: branch_id.clone(),
684
- commit_id: commit_id.clone(),
685
- timestamp,
686
- });
687
- tracked_roots.push(PendingTrackedRoot {
688
- commit_id,
689
- parent_commit_id,
690
- });
691
- }
692
-
693
- Ok(FinalizedCommitRows {
694
- commit_rows,
695
- branch_heads,
696
- tracked_roots,
697
- })
698
- }
699
-
700
- fn merge_parent_commit_ids(mut base: Vec<String>, extra: Vec<String>) -> Vec<String> {
701
- for parent in extra {
702
- if !base.contains(&parent) {
703
- base.push(parent);
704
- }
705
- }
706
- base
707
- }
708
-
709
- #[cfg(test)]
710
- mod tests {
711
- use std::collections::BTreeMap;
712
- use std::sync::{
713
- atomic::{AtomicUsize, Ordering},
714
- Arc,
715
- };
716
-
717
- use super::*;
718
- use crate::backend::{
719
- Backend, BackendCapabilities, BackendError, BackendWrite, CommitResult, DurableWriteLock,
720
- KeyRange, PutBatch,
721
- };
722
- use crate::branch::BranchContext;
723
- use crate::catalog::SchemaPlanId;
724
- use crate::changelog::ChangelogReader;
725
- use crate::live_state::{LiveStateContext, LiveStateRowRequest};
726
- use crate::storage::{
727
- InMemoryStorageBackend, InMemoryStorageRead, InMemoryStorageWrite, StorageContext,
728
- StorageKey, StorageReadOptions, StorageWriteOptions,
729
- };
730
- use crate::transaction::types::PreparedRowFacts;
731
- use crate::untracked_state::{
732
- MaterializedUntrackedStateRow, UntrackedStateContext, UntrackedStateRowRequest,
733
- };
734
- use crate::NullableKeyFilter;
735
- use crate::GLOBAL_BRANCH_ID;
736
-
737
- const DETERMINISTIC_MODE_KEY: &str = "lix_deterministic_mode";
738
- const DETERMINISTIC_SEQUENCE_KEY: &str = "lix_deterministic_sequence_number";
739
-
740
- fn live_state_context() -> LiveStateContext {
741
- LiveStateContext::new(
742
- crate::tracked_state::TrackedStateContext::new(),
743
- crate::untracked_state::UntrackedStateContext::new(),
744
- crate::commit_graph::CommitGraphContext::new(),
745
- )
746
- }
747
-
748
- #[tokio::test]
749
- async fn commit_staged_writes_appends_changelog_and_updates_commit_root() {
750
- let storage = StorageContext::new(InMemoryStorageBackend::new());
751
- let binary_cas = BinaryCasContext::new();
752
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
753
- let mut read = storage
754
- .begin_read(StorageReadOptions::default())
755
- .expect("read should open");
756
-
757
- let state_rows = vec![tracked_global_row("change-1")];
758
- let writes = commit_prepared_writes(
759
- &binary_cas,
760
- &branch_ctx,
761
- None,
762
- &mut read,
763
- PreparedWriteSet {
764
- insert_identities: BTreeMap::new(),
765
- state_rows,
766
- commit_change_refs_by_branch: BTreeMap::from([(
767
- GLOBAL_BRANCH_ID.to_string(),
768
- change_refs(["change-1"]),
769
- )]),
770
- extra_commit_parents_by_branch: BTreeMap::new(),
771
- file_data_writes: Vec::new(),
772
- },
773
- )
774
- .await
775
- .expect("commit should flush staged rows");
776
- storage
777
- .commit_write_set(writes, StorageWriteOptions::default())
778
- .expect("writes should commit");
779
-
780
- let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
781
- storage
782
- .begin_read(StorageReadOptions::default())
783
- .expect("read should open"),
784
- );
785
- let commits = changelog_reader
786
- .load_commits(crate::changelog::CommitLoadRequest {
787
- commit_ids: &["test-uuid-1".to_string()],
788
- projection: crate::changelog::CommitProjection::Full,
789
- })
790
- .await
791
- .expect("changelog commit should load");
792
- let Some(crate::changelog::CommitLoadEntry::Full {
793
- record,
794
- change_ref_chunks,
795
- }) = commits.entries.into_iter().next().flatten()
796
- else {
797
- panic!("changelog commit should exist");
798
- };
799
- assert_eq!(record.change_id, "test-uuid-2");
800
- assert!(change_ref_chunks
801
- .iter()
802
- .flat_map(|chunk| chunk.entries.iter())
803
- .any(|entry| entry.change_id == "change-1"));
804
- let changes = changelog_reader
805
- .load_changes(crate::changelog::ChangeLoadRequest {
806
- change_ids: &["change-1".to_string(), record.change_id.clone()],
807
- })
808
- .await
809
- .expect("changelog change should load");
810
- let mut loaded_changes = changes.entries.into_iter();
811
- let Some(change) = loaded_changes.next().flatten() else {
812
- panic!("changelog change should exist");
813
- };
814
- assert_eq!(change.change_id, "change-1");
815
- assert_eq!(change.schema_key, "test_schema");
816
- assert!(
817
- loaded_changes.next().flatten().is_none(),
818
- "commit row change is derived from changelog.commit, not stored as changelog.change"
819
- );
820
-
821
- let mut tracked_reader = crate::tracked_state::TrackedStateContext::new().reader(
822
- storage
823
- .begin_read(StorageReadOptions::default())
824
- .expect("read should open"),
825
- );
826
- let commit_rows = tracked_reader
827
- .scan_rows_at_commit(
828
- "test-uuid-1",
829
- &crate::tracked_state::TrackedStateScanRequest {
830
- filter: crate::tracked_state::TrackedStateFilter {
831
- schema_keys: vec!["lix_commit".to_string()],
832
- ..Default::default()
833
- },
834
- ..Default::default()
835
- },
836
- )
837
- .await
838
- .expect("commit root should scan");
839
- assert!(
840
- commit_rows
841
- .iter()
842
- .any(|row| row.change_id == record.change_id && row.snapshot_content.is_some()),
843
- "commit root should surface the derived lix_commit row"
844
- );
845
-
846
- let loaded_head = branch_ctx
847
- .ref_reader(
848
- storage
849
- .begin_read(StorageReadOptions::default())
850
- .expect("read should open"),
851
- )
852
- .load_head_commit_id(GLOBAL_BRANCH_ID)
853
- .await
854
- .expect("branch ref load should succeed");
855
- assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
856
- }
857
-
858
- #[tokio::test]
859
- async fn stage_changelog_commits_orders_staged_parents_before_children() {
860
- let storage = StorageContext::new(InMemoryStorageBackend::new());
861
- let mut writes = StorageWriteSet::new();
862
- let mut read = storage
863
- .begin_read(StorageReadOptions::default())
864
- .expect("read should open");
865
- let mut parent_row = tracked_global_row("parent-change");
866
- parent_row.commit_id = Some("parent-commit".to_string());
867
- let mut child_row = tracked_global_row("child-change");
868
- child_row.commit_id = Some("child-commit".to_string());
869
-
870
- let commits = vec![
871
- FinalizedCommitRow {
872
- commit_id: "child-commit".to_string(),
873
- parent_commit_ids: vec!["parent-commit".to_string()],
874
- created_at: "2026-01-01T00:00:01Z".to_string(),
875
- change_id: "child-commit-change".to_string(),
876
- selected_change_refs: Vec::new(),
877
- },
878
- FinalizedCommitRow {
879
- commit_id: "parent-commit".to_string(),
880
- parent_commit_ids: Vec::new(),
881
- created_at: "2026-01-01T00:00:00Z".to_string(),
882
- change_id: "parent-commit-change".to_string(),
883
- selected_change_refs: Vec::new(),
884
- },
885
- ];
886
- stage_changelog_commits(
887
- &mut read,
888
- &mut writes,
889
- &[parent_row, child_row],
890
- &BTreeMap::from([
891
- ("parent-commit".to_string(), vec![0]),
892
- ("child-commit".to_string(), vec![1]),
893
- ]),
894
- &commits,
895
- )
896
- .await
897
- .expect("child-before-parent input should still stage parent first");
898
- storage
899
- .commit_write_set(writes, StorageWriteOptions::default())
900
- .expect("writes should persist");
901
-
902
- let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
903
- storage
904
- .begin_read(StorageReadOptions::default())
905
- .expect("read should open"),
906
- );
907
- let commits = changelog_reader
908
- .load_commits(crate::changelog::CommitLoadRequest {
909
- commit_ids: &["parent-commit".to_string(), "child-commit".to_string()],
910
- projection: crate::changelog::CommitProjection::Record,
911
- })
912
- .await
913
- .expect("commits should load");
914
- assert!(commits.entries.iter().all(Option::is_some));
915
- }
916
-
917
- #[tokio::test]
918
- async fn commit_with_only_untracked_writes_does_not_create_lix_commit() {
919
- let storage = StorageContext::new(InMemoryStorageBackend::new());
920
- let binary_cas = BinaryCasContext::new();
921
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
922
- let untracked_state = UntrackedStateContext::new();
923
- let mut read = storage
924
- .begin_read(StorageReadOptions::default())
925
- .expect("read should open");
926
-
927
- let state_rows = vec![untracked_global_row("change-untracked")];
928
- let writes = commit_prepared_writes(
929
- &binary_cas,
930
- &branch_ctx,
931
- None,
932
- &mut read,
933
- PreparedWriteSet {
934
- insert_identities: BTreeMap::new(),
935
- state_rows,
936
- commit_change_refs_by_branch: BTreeMap::new(),
937
- extra_commit_parents_by_branch: BTreeMap::new(),
938
- file_data_writes: Vec::new(),
939
- },
940
- )
941
- .await
942
- .expect("commit should flush untracked row");
943
- storage
944
- .commit_write_set(writes, StorageWriteOptions::default())
945
- .expect("writes should commit");
946
-
947
- let loaded = {
948
- let mut untracked_reader = untracked_state.reader(
949
- storage
950
- .begin_read(StorageReadOptions::default())
951
- .expect("read should open"),
952
- );
953
- untracked_reader
954
- .load_row(&UntrackedStateRowRequest {
955
- schema_key: "test_schema".to_string(),
956
- branch_id: GLOBAL_BRANCH_ID.to_string(),
957
- entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
958
- file_id: NullableKeyFilter::Null,
959
- })
960
- .await
961
- }
962
- .expect("untracked row load should succeed")
963
- .expect("untracked row should be persisted");
964
- assert_eq!(
965
- loaded.snapshot_content.as_deref(),
966
- Some("{\"value\":\"untracked\"}")
967
- );
968
- }
969
-
970
- #[tokio::test]
971
- async fn tracked_write_deletes_matching_untracked_overlay() {
972
- let storage = StorageContext::new(InMemoryStorageBackend::new());
973
- let binary_cas = BinaryCasContext::new();
974
- let untracked_state = UntrackedStateContext::new();
975
- let live_state = Arc::new(live_state_context());
976
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
977
-
978
- let mut writes = StorageWriteSet::new();
979
- let staged_row = untracked_global_row("change-untracked");
980
- let canonical_row = crate::test_support::untracked_state_row_from_materialized(
981
- &mut writes,
982
- &MaterializedUntrackedStateRow::from(staged_row),
983
- )
984
- .expect("untracked seed should canonicalize");
985
- untracked_state
986
- .writer(&mut writes)
987
- .stage_rows(std::iter::once(canonical_row.as_ref()))
988
- .expect("untracked seed should write");
989
- storage
990
- .commit_write_set(writes, StorageWriteOptions::default())
991
- .expect("untracked seed should commit");
992
-
993
- let mut read = storage
994
- .begin_read(StorageReadOptions::default())
995
- .expect("read should open");
996
- let state_rows = vec![tracked_global_row("change-tracked")];
997
- let writes = commit_prepared_writes(
998
- &binary_cas,
999
- &branch_ctx,
1000
- None,
1001
- &mut read,
1002
- PreparedWriteSet {
1003
- insert_identities: BTreeMap::new(),
1004
- state_rows,
1005
- commit_change_refs_by_branch: BTreeMap::from([(
1006
- GLOBAL_BRANCH_ID.to_string(),
1007
- change_refs(["change-tracked"]),
1008
- )]),
1009
- extra_commit_parents_by_branch: BTreeMap::new(),
1010
- file_data_writes: Vec::new(),
1011
- },
1012
- )
1013
- .await
1014
- .expect("tracked commit should flush");
1015
- storage
1016
- .commit_write_set(writes, StorageWriteOptions::default())
1017
- .expect("writes should commit");
1018
-
1019
- let untracked = {
1020
- let mut untracked_reader = untracked_state.reader(
1021
- storage
1022
- .begin_read(StorageReadOptions::default())
1023
- .expect("read should open"),
1024
- );
1025
- untracked_reader.load_row(&untracked_request()).await
1026
- }
1027
- .expect("untracked load should succeed");
1028
- assert_eq!(untracked, None);
1029
-
1030
- let visible = live_state
1031
- .reader(
1032
- storage
1033
- .begin_read(StorageReadOptions::default())
1034
- .expect("read should open"),
1035
- )
1036
- .load_row(&live_state_request())
1037
- .await
1038
- .expect("live-state load should succeed")
1039
- .expect("tracked row should be visible");
1040
- assert!(!visible.untracked);
1041
- assert_eq!(visible.change_id.as_deref(), Some("change-tracked"));
1042
- assert_eq!(visible.snapshot_content.as_deref(), Some("{\"value\":1}"));
1043
- }
1044
-
1045
- #[tokio::test]
1046
- async fn commit_staged_writes_applies_cross_subsystem_rows_as_one_backend_batch() {
1047
- let counting_backend = CountingBackend::new();
1048
- let write_batches = counting_backend.write_batches();
1049
- let storage = StorageContext::new(counting_backend);
1050
- let binary_cas = BinaryCasContext::new();
1051
- let live_state = Arc::new(live_state_context());
1052
- let untracked_state = UntrackedStateContext::new();
1053
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1054
- {
1055
- let mut read = storage
1056
- .begin_read(StorageReadOptions::default())
1057
- .expect("seed read should open");
1058
- let mut writes = storage.new_write_set();
1059
- crate::test_support::stage_tracked_root_from_materialized(
1060
- &mut read,
1061
- &mut writes,
1062
- &crate::tracked_state::TrackedStateContext::new(),
1063
- crate::test_support::TEST_EMPTY_ROOT_COMMIT_ID,
1064
- None,
1065
- &[],
1066
- )
1067
- .await
1068
- .expect("empty tracked root should stage");
1069
- let branch_ref_row = crate::transaction::prepare_branch_ref_row(
1070
- GLOBAL_BRANCH_ID,
1071
- crate::test_support::TEST_EMPTY_ROOT_COMMIT_ID,
1072
- "1970-01-01T00:00:00.000Z",
1073
- )
1074
- .expect("global branch ref should stage");
1075
- UntrackedStateContext::new()
1076
- .writer(&mut writes)
1077
- .stage_rows([branch_ref_row.row.as_ref()])
1078
- .expect("global branch ref should stage");
1079
- storage
1080
- .commit_write_set(writes, StorageWriteOptions::default())
1081
- .expect("global branch ref should commit");
1082
- }
1083
- {
1084
- let mut writes = StorageWriteSet::new();
1085
- let mode_snapshot = serde_json::to_string(&serde_json::json!({
1086
- "key": DETERMINISTIC_MODE_KEY,
1087
- "value": { "enabled": true },
1088
- }))
1089
- .expect("mode snapshot should serialize");
1090
- JsonStoreContext::new()
1091
- .writer()
1092
- .stage_batch(
1093
- &mut writes,
1094
- JsonWritePlacementRef::OutOfBand,
1095
- [NormalizedJsonRef::new(mode_snapshot.as_str())],
1096
- )
1097
- .expect("deterministic mode snapshot should stage");
1098
- let row = crate::untracked_state::UntrackedStateRow {
1099
- entity_pk: crate::entity_pk::EntityPk::single(DETERMINISTIC_MODE_KEY),
1100
- schema_key: "lix_key_value".to_string(),
1101
- file_id: None,
1102
- snapshot_content: Some(mode_snapshot.to_string()),
1103
- metadata: None,
1104
- created_at: "2026-01-01T00:00:00Z".to_string(),
1105
- updated_at: "2026-01-01T00:00:00Z".to_string(),
1106
- global: true,
1107
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1108
- };
1109
- UntrackedStateContext::new()
1110
- .writer(&mut writes)
1111
- .stage_rows(std::iter::once(row.as_ref()))
1112
- .expect("deterministic mode should stage");
1113
- storage
1114
- .commit_write_set(writes, StorageWriteOptions::default())
1115
- .expect("deterministic mode should commit");
1116
- }
1117
- write_batches.store(0, Ordering::SeqCst);
1118
- let runtime_functions = {
1119
- let reader = live_state.reader(
1120
- storage
1121
- .begin_read(StorageReadOptions::default())
1122
- .expect("read should open"),
1123
- );
1124
- FunctionContext::prepare(&reader)
1125
- .await
1126
- .expect("runtime context should prepare")
1127
- };
1128
- runtime_functions.provider().call_uuid_v7();
1129
- let mut read = storage
1130
- .begin_read(StorageReadOptions::default())
1131
- .expect("read should open");
1132
-
1133
- let tracked_row = tracked_global_row("change-tracked");
1134
- let mut untracked_row = untracked_global_row("change-untracked");
1135
- untracked_row.entity_pk = crate::entity_pk::EntityPk::single("entity-2");
1136
-
1137
- let writes = commit_prepared_writes(
1138
- &binary_cas,
1139
- &branch_ctx,
1140
- Some(&runtime_functions),
1141
- &mut read,
1142
- PreparedWriteSet {
1143
- insert_identities: BTreeMap::new(),
1144
- state_rows: vec![tracked_row, untracked_row],
1145
- commit_change_refs_by_branch: BTreeMap::from([(
1146
- GLOBAL_BRANCH_ID.to_string(),
1147
- change_refs(["change-tracked"]),
1148
- )]),
1149
- extra_commit_parents_by_branch: BTreeMap::new(),
1150
- file_data_writes: Vec::new(),
1151
- },
1152
- )
1153
- .await
1154
- .expect("cross-subsystem commit should stage and apply");
1155
-
1156
- assert_eq!(
1157
- write_batches.load(Ordering::SeqCst),
1158
- 0,
1159
- "prepared writes should not touch the backend before the write set is committed"
1160
- );
1161
- storage
1162
- .commit_write_set(writes, StorageWriteOptions::default())
1163
- .expect("writes should commit");
1164
- assert_eq!(write_batches.load(Ordering::SeqCst), 1);
1165
-
1166
- let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
1167
- storage
1168
- .begin_read(StorageReadOptions::default())
1169
- .expect("read should open"),
1170
- );
1171
- let commits = changelog_reader
1172
- .load_commits(crate::changelog::CommitLoadRequest {
1173
- commit_ids: &["test-uuid-1".to_string()],
1174
- projection: crate::changelog::CommitProjection::Record,
1175
- })
1176
- .await
1177
- .expect("changelog commit should load");
1178
- let Some(crate::changelog::CommitLoadEntry::Record(commit)) =
1179
- commits.entries.into_iter().next().flatten()
1180
- else {
1181
- panic!("changelog commit should exist");
1182
- };
1183
- assert_eq!(commit.change_id, "test-uuid-2");
1184
- let changes = changelog_reader
1185
- .load_changes(crate::changelog::ChangeLoadRequest {
1186
- change_ids: &["change-tracked".to_string()],
1187
- })
1188
- .await
1189
- .expect("changelog change should load");
1190
- assert!(matches!(
1191
- changes.entries.as_slice(),
1192
- [Some(change)] if change.change_id == "change-tracked"
1193
- ));
1194
-
1195
- let loaded_head = branch_ctx
1196
- .ref_reader(
1197
- storage
1198
- .begin_read(StorageReadOptions::default())
1199
- .expect("read should open"),
1200
- )
1201
- .load_head_commit_id(GLOBAL_BRANCH_ID)
1202
- .await
1203
- .expect("branch ref load should succeed");
1204
- assert_eq!(loaded_head.as_deref(), Some("test-uuid-1"));
1205
-
1206
- let untracked = {
1207
- let mut untracked_reader = untracked_state.reader(
1208
- storage
1209
- .begin_read(StorageReadOptions::default())
1210
- .expect("read should open"),
1211
- );
1212
- untracked_reader
1213
- .load_row(&UntrackedStateRowRequest {
1214
- schema_key: "test_schema".to_string(),
1215
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1216
- entity_pk: crate::entity_pk::EntityPk::single("entity-2"),
1217
- file_id: NullableKeyFilter::Null,
1218
- })
1219
- .await
1220
- }
1221
- .expect("untracked row load should succeed")
1222
- .expect("untracked row should persist");
1223
- assert_eq!(
1224
- untracked.snapshot_content.as_deref(),
1225
- Some("{\"value\":\"untracked\"}")
1226
- );
1227
-
1228
- let sequence_row = live_state
1229
- .reader(
1230
- storage
1231
- .begin_read(StorageReadOptions::default())
1232
- .expect("read should open"),
1233
- )
1234
- .load_row(&LiveStateRowRequest {
1235
- schema_key: "lix_key_value".to_string(),
1236
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1237
- entity_pk: crate::entity_pk::EntityPk::single(DETERMINISTIC_SEQUENCE_KEY),
1238
- file_id: NullableKeyFilter::Null,
1239
- })
1240
- .await
1241
- .expect("deterministic sequence should load")
1242
- .expect("deterministic sequence should persist");
1243
- assert_eq!(
1244
- sequence_row.snapshot_content.as_deref(),
1245
- Some("{\"key\":\"lix_deterministic_sequence_number\",\"value\":0}")
1246
- );
1247
- }
1248
-
1249
- #[tokio::test]
1250
- async fn non_global_tracked_write_creates_one_commit_and_advances_only_touched_branch() {
1251
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1252
- let binary_cas = BinaryCasContext::new();
1253
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1254
- crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "global-before")
1255
- .await;
1256
- crate::test_support::seed_branch_head(storage.clone(), "branch-a", "branch-a-before").await;
1257
-
1258
- let mut read = storage
1259
- .begin_read(StorageReadOptions::default())
1260
- .expect("read should open");
1261
- let state_rows = vec![tracked_branch_row("branch-a", "change-branch-a")];
1262
- let writes = commit_prepared_writes(
1263
- &binary_cas,
1264
- &branch_ctx,
1265
- None,
1266
- &mut read,
1267
- PreparedWriteSet {
1268
- insert_identities: BTreeMap::new(),
1269
- state_rows,
1270
- commit_change_refs_by_branch: BTreeMap::from([(
1271
- "branch-a".to_string(),
1272
- change_refs(["change-branch-a"]),
1273
- )]),
1274
- extra_commit_parents_by_branch: BTreeMap::new(),
1275
- file_data_writes: Vec::new(),
1276
- },
1277
- )
1278
- .await
1279
- .expect("branch commit should flush");
1280
- storage
1281
- .commit_write_set(writes, StorageWriteOptions::default())
1282
- .expect("writes should commit");
1283
-
1284
- let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
1285
- storage
1286
- .begin_read(StorageReadOptions::default())
1287
- .expect("read should open"),
1288
- );
1289
- let commits = changelog_reader
1290
- .load_commits(crate::changelog::CommitLoadRequest {
1291
- commit_ids: &["test-uuid-1".to_string()],
1292
- projection: crate::changelog::CommitProjection::Record,
1293
- })
1294
- .await
1295
- .expect("changelog commit should load");
1296
- let Some(crate::changelog::CommitLoadEntry::Record(commit)) =
1297
- commits.entries.into_iter().next().flatten()
1298
- else {
1299
- panic!("changelog commit should exist");
1300
- };
1301
- assert_eq!(commit.change_id, "test-uuid-2");
1302
- assert_eq!(commit.parent_commit_ids, vec!["branch-a-before"]);
1303
-
1304
- let global_head = branch_ctx
1305
- .ref_reader(
1306
- storage
1307
- .begin_read(StorageReadOptions::default())
1308
- .expect("read should open"),
1309
- )
1310
- .load_head_commit_id(GLOBAL_BRANCH_ID)
1311
- .await
1312
- .expect("global head should load");
1313
- let branch_head = branch_ctx
1314
- .ref_reader(
1315
- storage
1316
- .begin_read(StorageReadOptions::default())
1317
- .expect("read should open"),
1318
- )
1319
- .load_head_commit_id("branch-a")
1320
- .await
1321
- .expect("branch head should load");
1322
- assert_eq!(global_head.as_deref(), Some("global-before"));
1323
- assert_eq!(branch_head.as_deref(), Some("test-uuid-1"));
1324
- }
1325
-
1326
- #[tokio::test]
1327
- async fn finalize_commit_rows_parents_global_commit_to_existing_branch_ref() {
1328
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1329
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1330
- crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "initial-commit")
1331
- .await;
1332
-
1333
- let mut read = storage
1334
- .begin_read(StorageReadOptions::default())
1335
- .expect("read should open");
1336
- let rows = finalize_commit_rows(
1337
- BTreeMap::from([(
1338
- GLOBAL_BRANCH_ID.to_string(),
1339
- change_refs(["change-a", "change-b"]),
1340
- )]),
1341
- BTreeMap::new(),
1342
- &branch_ctx,
1343
- &mut read,
1344
- )
1345
- .await
1346
- .expect("global commit row should finalize");
1347
-
1348
- assert_eq!(rows.commit_rows.len(), 1);
1349
- assert_eq!(rows.branch_heads.len(), 1);
1350
- let row = &rows.commit_rows[0];
1351
- assert_eq!(row.commit_id, "test-uuid-1");
1352
- assert_eq!(row.change_id, "test-uuid-2");
1353
- assert_eq!(row.created_at, "test-timestamp-1");
1354
- assert_eq!(row.parent_commit_ids, vec!["initial-commit"]);
1355
-
1356
- let branch_head = &rows.branch_heads[0];
1357
- assert_eq!(branch_head.branch_id, GLOBAL_BRANCH_ID);
1358
- assert_eq!(branch_head.commit_id, "test-uuid-1");
1359
- }
1360
-
1361
- #[tokio::test]
1362
- async fn finalize_commit_rows_skips_empty_members() {
1363
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1364
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1365
- let mut read = storage
1366
- .begin_read(StorageReadOptions::default())
1367
- .expect("read should open");
1368
- let rows = finalize_commit_rows(
1369
- BTreeMap::from([(
1370
- GLOBAL_BRANCH_ID.to_string(),
1371
- StagedCommitChangeRefs::default(),
1372
- )]),
1373
- BTreeMap::new(),
1374
- &branch_ctx,
1375
- &mut read,
1376
- )
1377
- .await
1378
- .expect("empty change_refs should be ignored");
1379
-
1380
- assert!(rows.commit_rows.is_empty());
1381
- assert!(rows.branch_heads.is_empty());
1382
- }
1383
-
1384
- #[tokio::test]
1385
- async fn finalize_commit_rows_uses_existing_branch_ref_as_parent() {
1386
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1387
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1388
- crate::test_support::seed_branch_head(storage.clone(), GLOBAL_BRANCH_ID, "global-before")
1389
- .await;
1390
- crate::test_support::seed_branch_head(storage.clone(), "branch-a", "previous-commit").await;
1391
-
1392
- let mut read = storage
1393
- .begin_read(StorageReadOptions::default())
1394
- .expect("read should open");
1395
- let rows = finalize_commit_rows(
1396
- BTreeMap::from([("branch-a".to_string(), change_refs(["change-a"]))]),
1397
- BTreeMap::new(),
1398
- &branch_ctx,
1399
- &mut read,
1400
- )
1401
- .await
1402
- .expect("active-branch commit finalization should resolve parent");
1403
-
1404
- assert_eq!(
1405
- rows.commit_rows[0].parent_commit_ids,
1406
- vec!["previous-commit"]
1407
- );
1408
- assert_eq!(rows.branch_heads[0].branch_id, "branch-a");
1409
- }
1410
-
1411
- #[tokio::test]
1412
- async fn finalize_commit_rows_appends_extra_merge_parent_after_target_head() {
1413
- let storage = StorageContext::new(InMemoryStorageBackend::new());
1414
- let branch_ctx = BranchContext::new(Arc::new(UntrackedStateContext::new()));
1415
- crate::test_support::seed_branch_head(storage.clone(), "branch-a", "target-head").await;
1416
-
1417
- let mut read = storage
1418
- .begin_read(StorageReadOptions::default())
1419
- .expect("read should open");
1420
- let rows = finalize_commit_rows(
1421
- BTreeMap::from([("branch-a".to_string(), change_refs(["change-a"]))]),
1422
- BTreeMap::from([("branch-a".to_string(), vec!["source-head".to_string()])]),
1423
- &branch_ctx,
1424
- &mut read,
1425
- )
1426
- .await
1427
- .expect("merge commit finalization should resolve parents");
1428
-
1429
- assert_eq!(
1430
- rows.commit_rows[0].parent_commit_ids,
1431
- vec!["target-head", "source-head"]
1432
- );
1433
- }
1434
-
1435
- fn change_refs<const N: usize>(change_ids: [&str; N]) -> StagedCommitChangeRefs {
1436
- let mut change_refs = StagedCommitChangeRefs::new(
1437
- "test-uuid-1".to_string(),
1438
- "test-uuid-2".to_string(),
1439
- "test-timestamp-1".to_string(),
1440
- );
1441
- for change_id in change_ids {
1442
- change_refs.add_change_id(change_id.to_string());
1443
- }
1444
- change_refs
1445
- }
1446
-
1447
- fn tracked_global_row(change_id: &str) -> PreparedStateRow {
1448
- tracked_branch_row(GLOBAL_BRANCH_ID, change_id)
1449
- }
1450
-
1451
- fn tracked_branch_row(branch_id: &str, change_id: &str) -> PreparedStateRow {
1452
- PreparedStateRow {
1453
- schema_plan_id: SchemaPlanId::for_test(0),
1454
- facts: PreparedRowFacts::default(),
1455
- entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
1456
- schema_key: "test_schema".to_string(),
1457
- file_id: None,
1458
- snapshot: Some(
1459
- crate::transaction::types::stage_json_from_value(
1460
- crate::transaction::types::TransactionJson::from_value_for_test(
1461
- serde_json::json!({ "value": 1 }),
1462
- ),
1463
- "test tracked row snapshot",
1464
- )
1465
- .expect("test snapshot should stage"),
1466
- ),
1467
- metadata: None,
1468
- origin: None,
1469
- created_at: "2026-01-01T00:00:00Z".to_string(),
1470
- updated_at: "2026-01-01T00:00:00Z".to_string(),
1471
- global: branch_id == GLOBAL_BRANCH_ID,
1472
- change_id: Some(change_id.to_string()),
1473
- commit_id: Some("test-uuid-1".to_string()),
1474
- untracked: false,
1475
- branch_id: branch_id.to_string(),
1476
- }
1477
- }
1478
-
1479
- fn untracked_global_row(change_id: &str) -> PreparedStateRow {
1480
- let mut row = tracked_global_row(change_id);
1481
- row.snapshot = Some(
1482
- crate::transaction::types::stage_json_from_value(
1483
- crate::transaction::types::TransactionJson::from_value_for_test(
1484
- serde_json::json!({ "value": "untracked" }),
1485
- ),
1486
- "test untracked row snapshot",
1487
- )
1488
- .expect("test snapshot should stage"),
1489
- );
1490
- PreparedStateRow {
1491
- change_id: None,
1492
- commit_id: None,
1493
- untracked: true,
1494
- ..row
1495
- }
1496
- }
1497
-
1498
- fn untracked_request() -> UntrackedStateRowRequest {
1499
- UntrackedStateRowRequest {
1500
- schema_key: "test_schema".to_string(),
1501
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1502
- entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
1503
- file_id: NullableKeyFilter::Null,
1504
- }
1505
- }
1506
-
1507
- fn live_state_request() -> LiveStateRowRequest {
1508
- LiveStateRowRequest {
1509
- schema_key: "test_schema".to_string(),
1510
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1511
- entity_pk: crate::entity_pk::EntityPk::single("entity-1"),
1512
- file_id: NullableKeyFilter::Null,
1513
- }
1514
- }
1515
-
1516
- struct CountingBackend {
1517
- inner: InMemoryStorageBackend,
1518
- write_batches: Arc<AtomicUsize>,
1519
- }
1520
-
1521
- impl CountingBackend {
1522
- fn new() -> Self {
1523
- Self {
1524
- inner: InMemoryStorageBackend::new(),
1525
- write_batches: Arc::new(AtomicUsize::new(0)),
1526
- }
1527
- }
1528
-
1529
- fn write_batches(&self) -> Arc<AtomicUsize> {
1530
- Arc::clone(&self.write_batches)
1531
- }
1532
- }
1533
-
1534
- impl Backend for CountingBackend {
1535
- type Read<'a>
1536
- = InMemoryStorageRead
1537
- where
1538
- Self: 'a;
1539
-
1540
- type Write<'a>
1541
- = CountingWrite
1542
- where
1543
- Self: 'a;
1544
-
1545
- fn capabilities(&self) -> BackendCapabilities {
1546
- self.inner.capabilities()
1547
- }
1548
-
1549
- fn begin_read(&self, opts: StorageReadOptions) -> Result<Self::Read<'_>, BackendError> {
1550
- self.inner.begin_read(opts)
1551
- }
1552
-
1553
- fn begin_write(&self, opts: StorageWriteOptions) -> Result<Self::Write<'_>, BackendError> {
1554
- Ok(CountingWrite {
1555
- inner: self.inner.begin_write(opts)?,
1556
- write_batches: Arc::clone(&self.write_batches),
1557
- })
1558
- }
1559
-
1560
- fn durable_write_lock(&self) -> DurableWriteLock {
1561
- self.inner.durable_write_lock()
1562
- }
1563
- }
1564
-
1565
- struct CountingWrite {
1566
- inner: InMemoryStorageWrite,
1567
- write_batches: Arc<AtomicUsize>,
1568
- }
1569
-
1570
- impl BackendWrite for CountingWrite {
1571
- fn put_many(&mut self, entries: PutBatch) -> Result<(), BackendError> {
1572
- self.inner.put_many(entries)
1573
- }
1574
-
1575
- fn delete_many(&mut self, keys: &[StorageKey]) -> Result<(), BackendError> {
1576
- self.inner.delete_many(keys)
1577
- }
1578
-
1579
- fn delete_range(&mut self, range: KeyRange) -> Result<(), BackendError> {
1580
- self.inner.delete_range(range)
1581
- }
1582
-
1583
- fn commit(self) -> Result<CommitResult, BackendError> {
1584
- self.write_batches.fetch_add(1, Ordering::SeqCst);
1585
- self.inner.commit()
1586
- }
1587
-
1588
- fn rollback(self) -> Result<(), BackendError> {
1589
- self.inner.rollback()
1590
- }
1591
- }
1592
- }