@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,1525 +0,0 @@
1
- use std::collections::{BTreeMap, HashMap};
2
- use std::sync::{Arc, Mutex};
3
-
4
- use crate::catalog::SchemaPlanId;
5
- use crate::domain::{Domain, DomainRowIdentity};
6
- use crate::entity_pk::EntityPk;
7
- use crate::functions::{FunctionProvider, FunctionProviderHandle};
8
- #[cfg(test)]
9
- use crate::live_state::LiveStateRowRequest;
10
- use crate::live_state::{LiveStateScanRequest, MaterializedLiveStateRow};
11
- #[cfg(test)]
12
- use crate::transaction::types::{stage_json_from_value, TransactionJson};
13
- use crate::transaction::types::{
14
- LogicalPrimaryKey, PreparedTransactionWrite, StagedCommitChangeRef, TransactionFileData,
15
- TransactionWriteMode, TransactionWriteOperation, TransactionWriteOrigin,
16
- TransactionWriteOutcome,
17
- };
18
- use crate::transaction::types::{PreparedStateRow, StagedCommitChangeRefs};
19
- use crate::GLOBAL_BRANCH_ID;
20
- use crate::{LixError, NullableKeyFilter};
21
-
22
- /// Transaction-local write buffer after transaction-boundary preparation.
23
- ///
24
- /// This is the engine seam between SQL execution and transaction ownership:
25
- /// write frontends pass decoded `TransactionWriteRow`s to `Transaction`, the
26
- /// transaction prepares them into stable `PreparedStateRow`s, reads build a
27
- /// `PreparedStateRowOverlay` from those rows, and commit drains the same rows.
28
- pub(crate) struct TransactionWriteBuffer {
29
- functions: FunctionProviderHandle,
30
- rows: Mutex<Vec<Option<PreparedStateRow>>>,
31
- by_identity: Mutex<HashMap<PreparedStateRowIdentity, RowSlot>>,
32
- insert_identities: Mutex<BTreeMap<PreparedStateRowIdentity, Option<TransactionWriteOrigin>>>,
33
- commit_change_refs_by_branch: Mutex<BTreeMap<String, StagedCommitChangeRefs>>,
34
- extra_commit_parents_by_branch: Mutex<BTreeMap<String, Vec<String>>>,
35
- file_data_writes: Mutex<Vec<TransactionFileData>>,
36
- }
37
-
38
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
39
- pub(crate) enum RowSlot {
40
- State(usize),
41
- }
42
-
43
- /// Drained prepared transaction writes ready for commit.
44
- pub(crate) struct PreparedWriteSet {
45
- pub(crate) state_rows: Vec<PreparedStateRow>,
46
- pub(crate) insert_identities:
47
- BTreeMap<PreparedStateRowIdentity, Option<TransactionWriteOrigin>>,
48
- pub(crate) commit_change_refs_by_branch: BTreeMap<String, StagedCommitChangeRefs>,
49
- pub(crate) extra_commit_parents_by_branch: BTreeMap<String, Vec<String>>,
50
- pub(crate) file_data_writes: Vec<TransactionFileData>,
51
- }
52
-
53
- pub(crate) struct PreparedWriteValidationSet<'a> {
54
- rows: Vec<PreparedValidationRow<'a>>,
55
- constraint_rows: Vec<PreparedValidationRow<'a>>,
56
- insert_identities: Vec<(
57
- &'a PreparedStateRowIdentity,
58
- Option<&'a TransactionWriteOrigin>,
59
- )>,
60
- }
61
-
62
- pub(crate) struct PreparedWriteValidationIndex<'a> {
63
- rows_by_schema_scope: BTreeMap<Domain, Vec<PreparedValidationRow<'a>>>,
64
- insert_identities_by_schema_scope: BTreeMap<
65
- Domain,
66
- Vec<(
67
- &'a PreparedStateRowIdentity,
68
- Option<&'a TransactionWriteOrigin>,
69
- )>,
70
- >,
71
- }
72
-
73
- #[derive(Clone, Copy)]
74
- pub(crate) enum PreparedValidationRow<'a> {
75
- State(&'a PreparedStateRow),
76
- }
77
-
78
- impl<'a> PreparedValidationRow<'a> {
79
- pub(crate) fn entity_pk(&self) -> &EntityPk {
80
- match self {
81
- Self::State(row) => &row.entity_pk,
82
- }
83
- }
84
-
85
- pub(crate) fn schema_plan_id(&self) -> SchemaPlanId {
86
- match self {
87
- Self::State(row) => row.schema_plan_id,
88
- }
89
- }
90
-
91
- pub(crate) fn schema_key(&self) -> &str {
92
- match self {
93
- Self::State(row) => &row.schema_key,
94
- }
95
- }
96
-
97
- pub(crate) fn file_id(&self) -> &Option<String> {
98
- match self {
99
- Self::State(row) => &row.file_id,
100
- }
101
- }
102
-
103
- #[cfg(test)]
104
- pub(crate) fn snapshot_content(&self) -> Option<&str> {
105
- match self {
106
- Self::State(row) => row
107
- .snapshot
108
- .as_ref()
109
- .map(|snapshot| snapshot.normalized.as_ref()),
110
- }
111
- }
112
-
113
- pub(crate) fn snapshot_json(self) -> Option<&'a serde_json::Value> {
114
- match self {
115
- Self::State(row) => row
116
- .snapshot
117
- .as_ref()
118
- .map(|snapshot| snapshot.value.as_ref()),
119
- }
120
- }
121
-
122
- pub(crate) fn metadata_json(self) -> Option<&'a serde_json::Value> {
123
- match self {
124
- Self::State(row) => row
125
- .metadata
126
- .as_ref()
127
- .map(|metadata| metadata.value.as_ref()),
128
- }
129
- }
130
-
131
- pub(crate) fn is_tombstone(&self) -> bool {
132
- match self {
133
- Self::State(row) => row.snapshot.is_none(),
134
- }
135
- }
136
-
137
- pub(crate) fn untracked(&self) -> bool {
138
- match self {
139
- Self::State(row) => row.untracked,
140
- }
141
- }
142
-
143
- pub(crate) fn branch_id(&self) -> &str {
144
- match self {
145
- Self::State(row) => &row.branch_id,
146
- }
147
- }
148
-
149
- pub(crate) fn domain(&self) -> Domain {
150
- Domain::exact_file(
151
- self.branch_id().to_string(),
152
- self.untracked(),
153
- self.file_id().clone(),
154
- )
155
- }
156
-
157
- pub(crate) fn domain_row_identity(&self) -> DomainRowIdentity {
158
- DomainRowIdentity::in_domain(
159
- self.domain(),
160
- self.schema_key().to_string(),
161
- self.entity_pk().clone(),
162
- )
163
- }
164
- }
165
-
166
- impl<'a> PreparedWriteValidationIndex<'a> {
167
- pub(crate) fn schema_scopes(&self) -> impl Iterator<Item = &Domain> {
168
- self.rows_by_schema_scope.keys()
169
- }
170
-
171
- pub(crate) fn validation_set_for_schema_scope(
172
- &self,
173
- schema_scope: &Domain,
174
- ) -> PreparedWriteValidationSet<'a> {
175
- let constraint_rows = self
176
- .rows_by_schema_scope
177
- .iter()
178
- .flat_map(|(target_scope, rows)| {
179
- rows.iter().copied().filter(move |row| {
180
- schema_scope.validation_scope_contains_constraint_domain(target_scope)
181
- || (row.is_tombstone()
182
- && target_scope.tombstone_domain_affects_validation_scope(schema_scope))
183
- })
184
- })
185
- .collect();
186
- PreparedWriteValidationSet {
187
- rows: self
188
- .rows_by_schema_scope
189
- .get(schema_scope)
190
- .cloned()
191
- .unwrap_or_default(),
192
- constraint_rows,
193
- insert_identities: self
194
- .insert_identities_by_schema_scope
195
- .get(schema_scope)
196
- .cloned()
197
- .unwrap_or_default(),
198
- }
199
- }
200
- }
201
-
202
- impl<'a> PreparedWriteValidationSet<'a> {
203
- pub(crate) fn rows(&self) -> impl Iterator<Item = PreparedValidationRow<'a>> + '_ {
204
- self.rows.iter().copied()
205
- }
206
-
207
- pub(crate) fn constraint_rows(&self) -> impl Iterator<Item = PreparedValidationRow<'a>> + '_ {
208
- self.constraint_rows.iter().copied()
209
- }
210
-
211
- pub(crate) fn insert_identities(
212
- &self,
213
- ) -> impl Iterator<Item = (&PreparedStateRowIdentity, Option<&TransactionWriteOrigin>)> {
214
- self.insert_identities
215
- .iter()
216
- .map(|(identity, origin)| (*identity, *origin))
217
- }
218
- }
219
-
220
- impl PreparedWriteSet {
221
- #[cfg(test)]
222
- pub(crate) fn validation_rows(&self) -> impl Iterator<Item = PreparedValidationRow<'_>> + '_ {
223
- self.state_rows.iter().map(PreparedValidationRow::State)
224
- }
225
-
226
- pub(crate) fn validation_index(&self) -> PreparedWriteValidationIndex<'_> {
227
- let mut rows_by_schema_scope = BTreeMap::<Domain, Vec<PreparedValidationRow<'_>>>::new();
228
- for row in &self.state_rows {
229
- let row = PreparedValidationRow::State(row);
230
- rows_by_schema_scope
231
- .entry(row.domain().schema_catalog_domain())
232
- .or_default()
233
- .push(row);
234
- }
235
- let mut insert_identities_by_schema_scope = BTreeMap::<
236
- Domain,
237
- Vec<(&PreparedStateRowIdentity, Option<&TransactionWriteOrigin>)>,
238
- >::new();
239
- for (identity, origin) in &self.insert_identities {
240
- insert_identities_by_schema_scope
241
- .entry(identity.domain().schema_catalog_domain())
242
- .or_default()
243
- .push((identity, origin.as_ref()));
244
- }
245
-
246
- PreparedWriteValidationIndex {
247
- rows_by_schema_scope,
248
- insert_identities_by_schema_scope,
249
- }
250
- }
251
-
252
- #[cfg(test)]
253
- pub(crate) fn validation_set_for_tests(&self) -> PreparedWriteValidationSet<'_> {
254
- let rows: Vec<_> = self.validation_rows().collect();
255
- let insert_identities = self
256
- .insert_identities
257
- .iter()
258
- .map(|(identity, origin)| (identity, origin.as_ref()))
259
- .collect();
260
- PreparedWriteValidationSet {
261
- constraint_rows: rows.clone(),
262
- rows,
263
- insert_identities,
264
- }
265
- }
266
- }
267
-
268
- impl TransactionWriteBuffer {
269
- pub(crate) fn new(functions: FunctionProviderHandle) -> Self {
270
- Self {
271
- functions,
272
- rows: Mutex::new(Vec::new()),
273
- by_identity: Mutex::new(HashMap::new()),
274
- insert_identities: Mutex::new(BTreeMap::new()),
275
- commit_change_refs_by_branch: Mutex::new(BTreeMap::new()),
276
- extra_commit_parents_by_branch: Mutex::new(BTreeMap::new()),
277
- file_data_writes: Mutex::new(Vec::new()),
278
- }
279
- }
280
-
281
- /// Drains staged writes for commit.
282
- pub(crate) fn drain(&self) -> Result<PreparedWriteSet, LixError> {
283
- let mut rows_guard = self.rows.lock().map_err(|_| {
284
- LixError::new(
285
- "LIX_ERROR_UNKNOWN",
286
- "failed to acquire transaction staged writes lock",
287
- )
288
- })?;
289
- let mut by_identity_guard = self.by_identity.lock().map_err(|_| {
290
- LixError::new(
291
- "LIX_ERROR_UNKNOWN",
292
- "failed to acquire transaction staged identity index lock",
293
- )
294
- })?;
295
- let mut file_data_guard = self.file_data_writes.lock().map_err(|_| {
296
- LixError::new(
297
- "LIX_ERROR_UNKNOWN",
298
- "failed to acquire transaction staged file data lock",
299
- )
300
- })?;
301
- let mut insert_identities_guard = self.insert_identities.lock().map_err(|_| {
302
- LixError::new(
303
- "LIX_ERROR_UNKNOWN",
304
- "failed to acquire transaction staged insert identity lock",
305
- )
306
- })?;
307
- let mut commit_change_refs_guard =
308
- self.commit_change_refs_by_branch.lock().map_err(|_| {
309
- LixError::new(
310
- "LIX_ERROR_UNKNOWN",
311
- "failed to acquire transaction staged commit change refs lock",
312
- )
313
- })?;
314
- let mut extra_parents_guard = self.extra_commit_parents_by_branch.lock().map_err(|_| {
315
- LixError::new(
316
- "LIX_ERROR_UNKNOWN",
317
- "failed to acquire transaction staged extra commit parents lock",
318
- )
319
- })?;
320
- let result = Ok(PreparedWriteSet {
321
- state_rows: std::mem::take(&mut *rows_guard)
322
- .into_iter()
323
- .flatten()
324
- .collect(),
325
- insert_identities: std::mem::take(&mut *insert_identities_guard),
326
- commit_change_refs_by_branch: std::mem::take(&mut *commit_change_refs_guard),
327
- extra_commit_parents_by_branch: std::mem::take(&mut *extra_parents_guard),
328
- file_data_writes: std::mem::take(&mut *file_data_guard),
329
- });
330
- by_identity_guard.clear();
331
- result
332
- }
333
-
334
- /// Records an additional parent for the commit generated for `branch_id`.
335
- ///
336
- /// Normal writes parent the new commit to the branch's previous head.
337
- /// Merges add the source branch head as an extra parent so the commit graph
338
- /// preserves branch ancestry while tracked-state roots still apply source
339
- /// rows onto the target root.
340
- pub(crate) fn add_commit_parent(
341
- &self,
342
- branch_id: String,
343
- parent_commit_id: String,
344
- ) -> Result<(), LixError> {
345
- let mut guard = self.extra_commit_parents_by_branch.lock().map_err(|_| {
346
- LixError::new(
347
- "LIX_ERROR_UNKNOWN",
348
- "failed to acquire transaction staged extra commit parents lock",
349
- )
350
- })?;
351
- let parents = guard.entry(branch_id).or_default();
352
- if !parents.contains(&parent_commit_id) {
353
- parents.push(parent_commit_id);
354
- }
355
- Ok(())
356
- }
357
-
358
- pub(crate) fn stage_selected_commit_change_refs(
359
- &self,
360
- branch_id: String,
361
- selected_change_refs: impl IntoIterator<Item = StagedCommitChangeRef>,
362
- ) -> Result<String, LixError> {
363
- let mut functions = self.functions.clone();
364
- let mut guard = self.commit_change_refs_by_branch.lock().map_err(|_| {
365
- LixError::new(
366
- "LIX_ERROR_UNKNOWN",
367
- "failed to acquire transaction staged commit change refs lock",
368
- )
369
- })?;
370
- let change_refs = guard.entry(branch_id).or_insert_with(|| {
371
- StagedCommitChangeRefs::new(
372
- functions.uuid_v7(),
373
- functions.uuid_v7(),
374
- functions.timestamp(),
375
- )
376
- });
377
- change_refs.allow_empty();
378
- for change_ref in selected_change_refs {
379
- change_refs.add_selected_change_ref(change_ref);
380
- }
381
- Ok(change_refs.commit_id.clone())
382
- }
383
-
384
- /// Builds the transaction-local read overlay from currently staged writes.
385
- pub(crate) fn staging_overlay(self: &Arc<Self>) -> Result<PreparedStateRowOverlay, LixError> {
386
- Ok(PreparedStateRowOverlay {
387
- staged_writes: Arc::clone(self),
388
- })
389
- }
390
-
391
- /// Stages one prepared write batch into this transaction.
392
- ///
393
- /// Frontends hand raw `TransactionWriteRow`s to `Transaction`; normalization prepares
394
- /// stable `PreparedStateRow`s before this method indexes them for transaction-
395
- /// local reads and commit routing.
396
- pub(crate) fn stage_write(
397
- &self,
398
- write: PreparedTransactionWrite,
399
- ) -> Result<TransactionWriteOutcome, LixError> {
400
- let (mode, count) = match &write {
401
- PreparedTransactionWrite::Rows { mode, rows } => (Some(*mode), rows.len() as u64),
402
- PreparedTransactionWrite::RowsWithFileData { mode, count, .. } => (Some(*mode), *count),
403
- };
404
- let mut functions = self.functions.clone();
405
- let (rows, file_data_writes) = self.state_rows_from_stage_write(write);
406
- reject_duplicate_present_rows_in_batch(&rows)?;
407
- let mut guard = self.rows.lock().map_err(|_| {
408
- LixError::new(
409
- "LIX_ERROR_UNKNOWN",
410
- "failed to acquire transaction staged writes lock",
411
- )
412
- })?;
413
- let mut by_identity_guard = self.by_identity.lock().map_err(|_| {
414
- LixError::new(
415
- "LIX_ERROR_UNKNOWN",
416
- "failed to acquire transaction staged identity index lock",
417
- )
418
- })?;
419
- let mut commit_change_refs_guard =
420
- self.commit_change_refs_by_branch.lock().map_err(|_| {
421
- LixError::new(
422
- "LIX_ERROR_UNKNOWN",
423
- "failed to acquire transaction staged commit change refs lock",
424
- )
425
- })?;
426
- let mut insert_identities_guard = self.insert_identities.lock().map_err(|_| {
427
- LixError::new(
428
- "LIX_ERROR_UNKNOWN",
429
- "failed to acquire transaction staged insert identity lock",
430
- )
431
- })?;
432
- for mut row in rows {
433
- if row.global && row.branch_id != GLOBAL_BRANCH_ID {
434
- return Err(LixError::new(
435
- LixError::CODE_INVALID_PARAM,
436
- "global staged rows must use the global branch id",
437
- ));
438
- }
439
- let identity = PreparedStateRowIdentity::from(&row);
440
- if mode == Some(TransactionWriteMode::Insert)
441
- && by_identity_guard.contains_key(&identity)
442
- {
443
- return Err(duplicate_insert_identity_error(&row));
444
- }
445
- let existing_slot = by_identity_guard.remove(&identity);
446
- if let Some(RowSlot::State(index)) = existing_slot {
447
- if let Some(previous) = guard.get_mut(index).and_then(Option::take) {
448
- remove_row_from_commit_change_refs(&mut commit_change_refs_guard, &previous);
449
- }
450
- }
451
- add_row_to_commit_change_refs(&mut commit_change_refs_guard, &mut row, &mut functions);
452
- let identity = PreparedStateRowIdentity::from(&row);
453
- if mode == Some(TransactionWriteMode::Insert) {
454
- insert_identities_guard.insert(identity.clone(), row.origin.clone());
455
- }
456
- let slot = match existing_slot {
457
- Some(RowSlot::State(index)) => {
458
- guard[index] = Some(row);
459
- RowSlot::State(index)
460
- }
461
- _ => {
462
- let index = guard.len();
463
- guard.push(Some(row));
464
- RowSlot::State(index)
465
- }
466
- };
467
- by_identity_guard.insert(identity, slot);
468
- }
469
- if !file_data_writes.is_empty() {
470
- self.file_data_writes
471
- .lock()
472
- .map_err(|_| {
473
- LixError::new(
474
- "LIX_ERROR_UNKNOWN",
475
- "failed to acquire transaction staged file data lock",
476
- )
477
- })?
478
- .extend(file_data_writes);
479
- }
480
- Ok(TransactionWriteOutcome { count })
481
- }
482
-
483
- fn state_rows_from_stage_write(
484
- &self,
485
- write: PreparedTransactionWrite,
486
- ) -> (Vec<PreparedStateRow>, Vec<TransactionFileData>) {
487
- let mut state_rows = Vec::new();
488
- let mut file_data_writes = Vec::new();
489
- match write {
490
- PreparedTransactionWrite::Rows { rows, .. } => {
491
- state_rows.extend(rows);
492
- }
493
- PreparedTransactionWrite::RowsWithFileData {
494
- rows, file_data, ..
495
- } => {
496
- state_rows.extend(rows);
497
- file_data_writes.extend(file_data);
498
- }
499
- }
500
- (state_rows, file_data_writes)
501
- }
502
- }
503
-
504
- /// Read overlay derived from staged transaction writes.
505
- #[derive(Clone)]
506
- pub(crate) struct PreparedStateRowOverlay {
507
- staged_writes: Arc<TransactionWriteBuffer>,
508
- }
509
-
510
- pub(crate) struct StagedScanParts {
511
- pub(crate) rows: Vec<MaterializedLiveStateRow>,
512
- }
513
-
514
- impl PreparedStateRowOverlay {
515
- /// Returns staged rows visible for a scan request.
516
- #[cfg(test)]
517
- pub(crate) fn scan(
518
- &self,
519
- request: &LiveStateScanRequest,
520
- ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
521
- Ok(crate::live_state::resolve_visible_rows(
522
- self.scan_parts(request)?.rows,
523
- Vec::new(),
524
- &crate::live_state::VisibilityRequest {
525
- branch_scope: crate::live_state::VisibilityBranchScope::BranchIds {
526
- branch_ids: request.filter.branch_ids.clone(),
527
- },
528
- include_tombstones: request.filter.include_tombstones,
529
- limit: None,
530
- },
531
- ))
532
- }
533
-
534
- /// Returns staged rows and base-row identities hidden by staged rows in one pass.
535
- ///
536
- /// Tombstones hide base rows even when the request does not include
537
- /// tombstone rows in the visible result set.
538
- pub(crate) fn scan_parts(
539
- &self,
540
- request: &LiveStateScanRequest,
541
- ) -> Result<StagedScanParts, LixError> {
542
- if matches!(
543
- request.filter.rows,
544
- crate::live_state::LiveStateRowFilter::None
545
- ) {
546
- return Ok(StagedScanParts { rows: Vec::new() });
547
- }
548
-
549
- let rows_guard = self.staged_writes.rows.lock().map_err(|_| {
550
- LixError::new(
551
- "LIX_ERROR_UNKNOWN",
552
- "failed to acquire transaction staged writes lock",
553
- )
554
- })?;
555
- let by_identity_guard = self.staged_writes.by_identity.lock().map_err(|_| {
556
- LixError::new(
557
- "LIX_ERROR_UNKNOWN",
558
- "failed to acquire transaction staged identity index lock",
559
- )
560
- })?;
561
-
562
- let mut rows = Vec::new();
563
- for slot in by_identity_guard.values() {
564
- match *slot {
565
- RowSlot::State(index) => {
566
- let Some(row) = rows_guard.get(index).and_then(Option::as_ref) else {
567
- continue;
568
- };
569
- if !staged_row_identity_matches_scan(row, request) {
570
- continue;
571
- }
572
- rows.push(MaterializedLiveStateRow::from(row));
573
- }
574
- }
575
- }
576
- Ok(StagedScanParts { rows })
577
- }
578
-
579
- /// Returns a staged exact-row answer, if this transaction has one.
580
- #[cfg(test)]
581
- pub(crate) fn load_exact(&self, request: &LiveStateRowRequest) -> Option<StagedExactRow> {
582
- let untracked_identity = PreparedStateRowIdentity::from_exact_request(request, true)?;
583
- if let Some(row) = self.load_state_slot(&untracked_identity) {
584
- return Some(if row.snapshot.is_none() {
585
- StagedExactRow::Tombstone
586
- } else {
587
- StagedExactRow::Row(MaterializedLiveStateRow::from(&row))
588
- });
589
- }
590
-
591
- let identity = PreparedStateRowIdentity::from_exact_request(request, false)?;
592
- if let Some(row) = self.load_state_slot(&identity) {
593
- return Some(if row.snapshot.is_none() {
594
- StagedExactRow::Tombstone
595
- } else {
596
- StagedExactRow::Row(MaterializedLiveStateRow::from(&row))
597
- });
598
- }
599
- None
600
- }
601
-
602
- #[cfg(test)]
603
- fn load_state_slot(&self, identity: &PreparedStateRowIdentity) -> Option<PreparedStateRow> {
604
- let rows_guard = self.staged_writes.rows.lock().ok()?;
605
- let by_identity_guard = self.staged_writes.by_identity.lock().ok()?;
606
- let Some(RowSlot::State(index)) = by_identity_guard.get(identity).copied() else {
607
- return None;
608
- };
609
- rows_guard.get(index)?.as_ref().cloned()
610
- }
611
- }
612
-
613
- impl crate::live_state::StagedLiveStateRows for PreparedStateRowOverlay {
614
- fn staged_rows(
615
- &self,
616
- request: &LiveStateScanRequest,
617
- ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
618
- Ok(self.scan_parts(request)?.rows)
619
- }
620
- }
621
-
622
- #[cfg(test)]
623
- pub(crate) enum StagedExactRow {
624
- Row(MaterializedLiveStateRow),
625
- Tombstone,
626
- }
627
-
628
- #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
629
- pub(crate) struct PreparedStateRowIdentity {
630
- untracked: bool,
631
- schema_key: String,
632
- entity_pk: crate::entity_pk::EntityPk,
633
- file_id: Option<String>,
634
- branch_id: String,
635
- }
636
-
637
- impl PreparedStateRowIdentity {
638
- fn from_staged_row(row: &PreparedStateRow) -> Self {
639
- Self {
640
- untracked: row.untracked,
641
- schema_key: row.schema_key.clone(),
642
- entity_pk: row.entity_pk.clone(),
643
- file_id: row.file_id.clone(),
644
- branch_id: row.branch_id.clone(),
645
- }
646
- }
647
-
648
- #[cfg(test)]
649
- fn from_exact_request(request: &LiveStateRowRequest, untracked: bool) -> Option<Self> {
650
- let file_id = match &request.file_id {
651
- NullableKeyFilter::Null => None,
652
- NullableKeyFilter::Value(value) => Some(value.clone()),
653
- // Exact overlay lookup requires a concrete row identity.
654
- NullableKeyFilter::Any => return None,
655
- };
656
- Some(Self {
657
- untracked,
658
- schema_key: request.schema_key.clone(),
659
- entity_pk: request.entity_pk.clone(),
660
- file_id,
661
- branch_id: request.branch_id.clone(),
662
- })
663
- }
664
-
665
- pub(crate) fn schema_key(&self) -> &str {
666
- &self.schema_key
667
- }
668
-
669
- pub(crate) fn entity_pk(&self) -> &crate::entity_pk::EntityPk {
670
- &self.entity_pk
671
- }
672
-
673
- pub(crate) fn domain(&self) -> Domain {
674
- Domain::exact_file(self.branch_id.clone(), self.untracked, self.file_id.clone())
675
- }
676
- }
677
-
678
- impl From<&PreparedStateRow> for PreparedStateRowIdentity {
679
- fn from(row: &PreparedStateRow) -> Self {
680
- Self::from_staged_row(row)
681
- }
682
- }
683
-
684
- impl From<&MaterializedLiveStateRow> for PreparedStateRowIdentity {
685
- fn from(row: &MaterializedLiveStateRow) -> Self {
686
- Self {
687
- untracked: row.untracked,
688
- schema_key: row.schema_key.clone(),
689
- entity_pk: row.entity_pk.clone(),
690
- file_id: row.file_id.clone(),
691
- branch_id: row.branch_id.clone(),
692
- }
693
- }
694
- }
695
-
696
- fn reject_duplicate_present_rows_in_batch(rows: &[PreparedStateRow]) -> Result<(), LixError> {
697
- let mut pending_present_rows = BTreeMap::<PreparedStateRowIdentity, &PreparedStateRow>::new();
698
- for row in rows {
699
- let identity = PreparedStateRowIdentity::from(row);
700
- if row.snapshot.is_none() {
701
- pending_present_rows.remove(&identity);
702
- continue;
703
- }
704
- if let Some(previous) = pending_present_rows.insert(identity, row) {
705
- return Err(duplicate_staged_present_row_error(row, previous));
706
- }
707
- }
708
- Ok(())
709
- }
710
-
711
- fn duplicate_staged_present_row_error(
712
- row: &PreparedStateRow,
713
- previous: &PreparedStateRow,
714
- ) -> LixError {
715
- let message = logical_primary_key_violation_message(row.origin.as_ref())
716
- .unwrap_or_else(|| {
717
- format!(
718
- "primary-key constraint violation on schema '{}': duplicate staged rows for entity_pk '{}' in branch '{}'",
719
- row.schema_key,
720
- previous
721
- .entity_pk
722
- .as_json_array_text()
723
- .unwrap_or_else(|_| "<invalid entity_pk>".to_string()),
724
- row.branch_id
725
- )
726
- });
727
- LixError::new(LixError::CODE_UNIQUE, message)
728
- }
729
-
730
- pub(crate) fn duplicate_insert_identity_message(
731
- schema_key: &str,
732
- entity_pk: &crate::entity_pk::EntityPk,
733
- branch_id: Option<&str>,
734
- origin: Option<&TransactionWriteOrigin>,
735
- ) -> String {
736
- if let Some(message) = logical_primary_key_violation_message(origin) {
737
- return message;
738
- }
739
- let entity_pk = entity_pk
740
- .as_json_array_text()
741
- .unwrap_or_else(|_| "<invalid entity_pk>".to_string());
742
- match branch_id {
743
- Some(branch_id) => format!(
744
- "primary-key constraint violation on schema '{schema_key}': INSERT would duplicate entity_pk '{entity_pk}' in branch '{branch_id}'"
745
- ),
746
- None => format!(
747
- "primary-key constraint violation on schema '{schema_key}': INSERT would duplicate entity_pk '{entity_pk}'"
748
- ),
749
- }
750
- }
751
-
752
- fn duplicate_insert_identity_error(row: &PreparedStateRow) -> LixError {
753
- let message = duplicate_insert_identity_message(
754
- &row.schema_key,
755
- &row.entity_pk,
756
- Some(&row.branch_id),
757
- row.origin.as_ref(),
758
- );
759
- LixError::new(LixError::CODE_UNIQUE, message)
760
- }
761
-
762
- fn logical_primary_key_violation_message(
763
- origin: Option<&TransactionWriteOrigin>,
764
- ) -> Option<String> {
765
- let origin = origin?;
766
- if origin.operation != TransactionWriteOperation::Insert {
767
- return None;
768
- }
769
- let primary_key = origin.primary_key.as_ref()?;
770
- Some(format!(
771
- "primary-key constraint violation on table '{}': INSERT would duplicate {}",
772
- origin.surface,
773
- format_logical_primary_key(primary_key)
774
- ))
775
- }
776
-
777
- fn format_logical_primary_key(primary_key: &LogicalPrimaryKey) -> String {
778
- primary_key
779
- .columns
780
- .iter()
781
- .enumerate()
782
- .map(|(index, column)| {
783
- let value = primary_key
784
- .values
785
- .get(index)
786
- .map(String::as_str)
787
- .unwrap_or("<missing>");
788
- format!("{column} '{value}'")
789
- })
790
- .collect::<Vec<_>>()
791
- .join(", ")
792
- }
793
-
794
- fn add_row_to_commit_change_refs(
795
- change_refs_by_branch: &mut BTreeMap<String, StagedCommitChangeRefs>,
796
- row: &mut PreparedStateRow,
797
- functions: &mut dyn FunctionProvider,
798
- ) {
799
- if row.untracked {
800
- return;
801
- }
802
- let change_id = row
803
- .change_id
804
- .clone()
805
- .expect("tracked staged rows must carry change_id for commit change refs");
806
- let change_refs = change_refs_by_branch
807
- .entry(row.branch_id.clone())
808
- .or_insert_with(|| {
809
- StagedCommitChangeRefs::new(
810
- functions.uuid_v7(),
811
- functions.uuid_v7(),
812
- functions.timestamp(),
813
- )
814
- });
815
- row.commit_id = Some(change_refs.commit_id.clone());
816
- change_refs.add_change_id(change_id);
817
- }
818
-
819
- fn remove_row_from_commit_change_refs(
820
- change_refs_by_branch: &mut BTreeMap<String, StagedCommitChangeRefs>,
821
- row: &PreparedStateRow,
822
- ) {
823
- if row.untracked {
824
- return;
825
- }
826
- let Some(change_refs) = change_refs_by_branch.get_mut(&row.branch_id) else {
827
- return;
828
- };
829
- let Some(change_id) = row.change_id.as_deref() else {
830
- return;
831
- };
832
- change_refs.remove_change_id(change_id);
833
- if change_refs.is_empty() {
834
- change_refs_by_branch.remove(&row.branch_id);
835
- }
836
- }
837
-
838
- fn staged_row_identity_matches_scan(
839
- row: &PreparedStateRow,
840
- request: &LiveStateScanRequest,
841
- ) -> bool {
842
- if !request.filter.schema_keys.is_empty()
843
- && !request.filter.schema_keys.contains(&row.schema_key)
844
- {
845
- return false;
846
- }
847
- if !request.filter.entity_pks.is_empty() && !request.filter.entity_pks.contains(&row.entity_pk)
848
- {
849
- return false;
850
- }
851
- if !staged_branch_matches_scan(&row.branch_id, request) {
852
- return false;
853
- }
854
- if request
855
- .filter
856
- .untracked
857
- .is_some_and(|untracked| row.untracked != untracked)
858
- {
859
- return false;
860
- }
861
- nullable_key_matches_filters(&row.file_id, &request.filter.file_ids)
862
- }
863
-
864
- fn nullable_key_matches_filters(
865
- value: &Option<String>,
866
- filters: &[NullableKeyFilter<String>],
867
- ) -> bool {
868
- filters.is_empty()
869
- || filters
870
- .iter()
871
- .any(|filter| nullable_key_matches_filter(value, filter))
872
- }
873
-
874
- fn staged_branch_matches_scan(branch_id: &str, request: &LiveStateScanRequest) -> bool {
875
- request.filter.branch_ids.is_empty()
876
- || request
877
- .filter
878
- .branch_ids
879
- .iter()
880
- .any(|requested| requested == branch_id)
881
- || (branch_id == GLOBAL_BRANCH_ID
882
- && request
883
- .filter
884
- .branch_ids
885
- .iter()
886
- .any(|requested| requested != GLOBAL_BRANCH_ID))
887
- }
888
-
889
- fn nullable_key_matches_filter(value: &Option<String>, filter: &NullableKeyFilter<String>) -> bool {
890
- match filter {
891
- NullableKeyFilter::Any => true,
892
- NullableKeyFilter::Null => value.is_none(),
893
- NullableKeyFilter::Value(expected) => value.as_ref() == Some(expected),
894
- }
895
- }
896
-
897
- #[cfg(test)]
898
- mod tests {
899
- use super::*;
900
- use crate::functions::SharedFunctionProvider;
901
- use crate::live_state::{LiveStateFilter, LiveStateRowRequest};
902
-
903
- #[tokio::test]
904
- async fn staging_overlay_uses_last_staged_row_for_exact_load() {
905
- let staged_writes = test_staged_writes();
906
-
907
- staged_writes
908
- .stage_write(PreparedTransactionWrite::Rows {
909
- mode: TransactionWriteMode::Replace,
910
- rows: vec![state_row("sql2-duplicate-key", "first")],
911
- })
912
- .expect("initial row should stage");
913
- staged_writes
914
- .stage_write(PreparedTransactionWrite::Rows {
915
- mode: TransactionWriteMode::Replace,
916
- rows: vec![state_row("sql2-duplicate-key", "second")],
917
- })
918
- .expect("staging rows should succeed");
919
-
920
- let overlay = staged_writes
921
- .staging_overlay()
922
- .expect("overlay should build from staged rows");
923
- let row = overlay
924
- .load_exact(&LiveStateRowRequest {
925
- schema_key: "lix_key_value".to_string(),
926
- branch_id: "global".to_string(),
927
- entity_pk: crate::entity_pk::EntityPk::single("sql2-duplicate-key"),
928
- file_id: NullableKeyFilter::Null,
929
- })
930
- .expect("staged row should be visible");
931
-
932
- let StagedExactRow::Row(row) = row else {
933
- panic!("latest staged row should not be a tombstone");
934
- };
935
- assert_eq!(
936
- row.snapshot_content.as_deref(),
937
- Some("{\"key\":\"sql2-duplicate-key\",\"value\":\"second\"}")
938
- );
939
- }
940
-
941
- #[tokio::test]
942
- async fn staging_overlay_scan_returns_only_latest_row_per_identity() {
943
- let staged_writes = test_staged_writes();
944
-
945
- staged_writes
946
- .stage_write(PreparedTransactionWrite::Rows {
947
- mode: TransactionWriteMode::Replace,
948
- rows: vec![state_row("sql2-duplicate-key", "first")],
949
- })
950
- .expect("initial row should stage");
951
- staged_writes
952
- .stage_write(PreparedTransactionWrite::Rows {
953
- mode: TransactionWriteMode::Replace,
954
- rows: vec![state_row("sql2-duplicate-key", "second")],
955
- })
956
- .expect("staging rows should succeed");
957
-
958
- let overlay = staged_writes
959
- .staging_overlay()
960
- .expect("overlay should build from staged rows");
961
- let rows = overlay
962
- .scan(&scan_request_for_key("sql2-duplicate-key", false))
963
- .expect("overlay scan should succeed");
964
-
965
- assert_eq!(rows.len(), 1);
966
- assert_eq!(
967
- rows[0].snapshot_content.as_deref(),
968
- Some("{\"key\":\"sql2-duplicate-key\",\"value\":\"second\"}")
969
- );
970
- }
971
-
972
- #[tokio::test]
973
- async fn staging_overlay_delete_hides_prior_staged_insert() {
974
- let staged_writes = test_staged_writes();
975
-
976
- staged_writes
977
- .stage_write(PreparedTransactionWrite::Rows {
978
- mode: TransactionWriteMode::Replace,
979
- rows: vec![
980
- state_row("sql2-delete-key", "visible"),
981
- tombstone_row("sql2-delete-key"),
982
- ],
983
- })
984
- .expect("staging rows should succeed");
985
-
986
- let overlay = staged_writes
987
- .staging_overlay()
988
- .expect("overlay should build from staged rows");
989
- let exact = overlay
990
- .load_exact(&exact_request_for_key("sql2-delete-key"))
991
- .expect("staged tombstone should answer exact load");
992
- assert!(matches!(exact, StagedExactRow::Tombstone));
993
- assert!(overlay
994
- .scan(&scan_request_for_key("sql2-delete-key", false))
995
- .expect("overlay scan should succeed")
996
- .is_empty());
997
-
998
- let tombstones = overlay
999
- .scan(&scan_request_for_key("sql2-delete-key", true))
1000
- .expect("overlay scan should succeed");
1001
- assert_eq!(tombstones.len(), 1);
1002
- assert_eq!(tombstones[0].snapshot_content, None);
1003
- }
1004
-
1005
- #[tokio::test]
1006
- async fn staging_overlay_insert_after_delete_resurrects_row() {
1007
- let staged_writes = test_staged_writes();
1008
-
1009
- staged_writes
1010
- .stage_write(PreparedTransactionWrite::Rows {
1011
- mode: TransactionWriteMode::Replace,
1012
- rows: vec![
1013
- tombstone_row("sql2-resurrect-key"),
1014
- state_row("sql2-resurrect-key", "visible-again"),
1015
- ],
1016
- })
1017
- .expect("staging rows should succeed");
1018
-
1019
- let overlay = staged_writes
1020
- .staging_overlay()
1021
- .expect("overlay should build from staged rows");
1022
- let exact = overlay
1023
- .load_exact(&exact_request_for_key("sql2-resurrect-key"))
1024
- .expect("staged row should answer exact load");
1025
-
1026
- let StagedExactRow::Row(row) = exact else {
1027
- panic!("latest staged row should be visible");
1028
- };
1029
- assert_eq!(
1030
- row.snapshot_content.as_deref(),
1031
- Some("{\"key\":\"sql2-resurrect-key\",\"value\":\"visible-again\"}")
1032
- );
1033
- assert_eq!(
1034
- overlay
1035
- .scan(&scan_request_for_key("sql2-resurrect-key", false))
1036
- .expect("overlay scan should succeed")
1037
- .len(),
1038
- 1
1039
- );
1040
- }
1041
-
1042
- #[tokio::test]
1043
- async fn staged_writes_drain_returns_coalesced_latest_rows() {
1044
- let staged_writes = test_staged_writes();
1045
-
1046
- staged_writes
1047
- .stage_write(PreparedTransactionWrite::Rows {
1048
- mode: TransactionWriteMode::Replace,
1049
- rows: vec![
1050
- state_row("sql2-key-a", "first"),
1051
- state_row("sql2-key-b", "only"),
1052
- ],
1053
- })
1054
- .expect("initial rows should stage");
1055
- staged_writes
1056
- .stage_write(PreparedTransactionWrite::Rows {
1057
- mode: TransactionWriteMode::Replace,
1058
- rows: vec![state_row("sql2-key-a", "second")],
1059
- })
1060
- .expect("staging rows should succeed");
1061
-
1062
- let drained = staged_writes.drain().expect("drain should succeed");
1063
-
1064
- assert_eq!(drained.state_rows.len(), 2);
1065
- assert!(drained.state_rows.iter().any(|row| {
1066
- row.entity_pk == crate::entity_pk::EntityPk::single("sql2-key-a")
1067
- && row
1068
- .snapshot
1069
- .as_ref()
1070
- .map(|snapshot| snapshot.normalized.as_ref())
1071
- == Some("{\"key\":\"sql2-key-a\",\"value\":\"second\"}")
1072
- }));
1073
- assert!(drained.state_rows.iter().any(|row| {
1074
- row.entity_pk == crate::entity_pk::EntityPk::single("sql2-key-b")
1075
- && row
1076
- .snapshot
1077
- .as_ref()
1078
- .map(|snapshot| snapshot.normalized.as_ref())
1079
- == Some("{\"key\":\"sql2-key-b\",\"value\":\"only\"}")
1080
- }));
1081
- }
1082
-
1083
- #[tokio::test]
1084
- async fn staged_writes_drain_preserves_file_data_payloads() {
1085
- let staged_writes = test_staged_writes();
1086
-
1087
- staged_writes
1088
- .stage_write(PreparedTransactionWrite::RowsWithFileData {
1089
- mode: TransactionWriteMode::Replace,
1090
- rows: vec![state_row("file-readme", "descriptor")],
1091
- file_data: vec![TransactionFileData {
1092
- file_id: "file-readme".to_string(),
1093
- branch_id: "global".to_string(),
1094
- untracked: true,
1095
- data: b"hello".to_vec(),
1096
- }],
1097
- count: 1,
1098
- })
1099
- .expect("staging rows with file data should succeed");
1100
-
1101
- let drained = staged_writes.drain().expect("drain should succeed");
1102
-
1103
- assert_eq!(drained.state_rows.len(), 1);
1104
- assert_eq!(drained.file_data_writes.len(), 1);
1105
- assert_eq!(drained.file_data_writes[0].file_id, "file-readme");
1106
- assert_eq!(drained.file_data_writes[0].data, b"hello");
1107
- }
1108
-
1109
- #[tokio::test]
1110
- async fn staged_writes_track_commit_members_for_tracked_global_rows() {
1111
- let staged_writes = test_staged_writes();
1112
-
1113
- staged_writes
1114
- .stage_write(PreparedTransactionWrite::Rows {
1115
- mode: TransactionWriteMode::Replace,
1116
- rows: vec![state_row("tracked-key", "value").with_tracked()],
1117
- })
1118
- .expect("tracked global row should stage");
1119
-
1120
- let drained = staged_writes.drain().expect("drain should succeed");
1121
- let change_refs = drained
1122
- .commit_change_refs_by_branch
1123
- .get("global")
1124
- .expect("global commit change_refs should exist");
1125
- assert_eq!(
1126
- change_refs.change_ids.iter().cloned().collect::<Vec<_>>(),
1127
- vec!["test-change-id".to_string()]
1128
- );
1129
- }
1130
-
1131
- #[tokio::test]
1132
- async fn staged_writes_do_not_track_untracked_rows_as_commit_members() {
1133
- let staged_writes = test_staged_writes();
1134
-
1135
- staged_writes
1136
- .stage_write(PreparedTransactionWrite::Rows {
1137
- mode: TransactionWriteMode::Replace,
1138
- rows: vec![state_row("untracked-key", "value")],
1139
- })
1140
- .expect("untracked row should stage");
1141
-
1142
- let drained = staged_writes.drain().expect("drain should succeed");
1143
- assert!(drained.commit_change_refs_by_branch.is_empty());
1144
- }
1145
-
1146
- #[tokio::test]
1147
- async fn staged_writes_replace_commit_member_on_tracked_overwrite() {
1148
- let staged_writes = test_staged_writes();
1149
-
1150
- staged_writes
1151
- .stage_write(PreparedTransactionWrite::Rows {
1152
- mode: TransactionWriteMode::Replace,
1153
- rows: vec![state_row("overwrite-key", "first")
1154
- .with_tracked()
1155
- .with_change_id("change-first")],
1156
- })
1157
- .expect("initial tracked row should stage");
1158
- staged_writes
1159
- .stage_write(PreparedTransactionWrite::Rows {
1160
- mode: TransactionWriteMode::Replace,
1161
- rows: vec![state_row("overwrite-key", "second")
1162
- .with_tracked()
1163
- .with_change_id("change-second")],
1164
- })
1165
- .expect("tracked overwrite should stage");
1166
-
1167
- let drained = staged_writes.drain().expect("drain should succeed");
1168
- let change_refs = drained
1169
- .commit_change_refs_by_branch
1170
- .get("global")
1171
- .expect("global commit change_refs should exist");
1172
- assert_eq!(
1173
- change_refs.change_ids.iter().cloned().collect::<Vec<_>>(),
1174
- vec!["change-second".to_string()]
1175
- );
1176
- }
1177
-
1178
- #[tokio::test]
1179
- async fn staged_writes_keep_tracked_and_untracked_domains_separate() {
1180
- let staged_writes = test_staged_writes();
1181
-
1182
- staged_writes
1183
- .stage_write(PreparedTransactionWrite::Rows {
1184
- mode: TransactionWriteMode::Replace,
1185
- rows: vec![
1186
- state_row("tracked-to-untracked-key", "tracked")
1187
- .with_tracked()
1188
- .with_change_id("change-tracked"),
1189
- state_row("tracked-to-untracked-key", "untracked")
1190
- .with_change_id("change-untracked"),
1191
- ],
1192
- })
1193
- .expect("untracked overwrite should stage");
1194
-
1195
- let drained = staged_writes.drain().expect("drain should succeed");
1196
- assert_eq!(drained.state_rows.len(), 2);
1197
- assert!(drained
1198
- .state_rows
1199
- .iter()
1200
- .any(|row| { row.change_id.as_deref() == Some("change-tracked") && !row.untracked }));
1201
- assert!(drained
1202
- .state_rows
1203
- .iter()
1204
- .any(|row| { row.change_id.as_deref() == Some("change-untracked") && row.untracked }));
1205
- let change_refs = drained
1206
- .commit_change_refs_by_branch
1207
- .get("global")
1208
- .expect("tracked commit member should remain in tracked domain");
1209
- assert_eq!(
1210
- change_refs.change_ids.iter().cloned().collect::<Vec<_>>(),
1211
- vec!["change-tracked".to_string()]
1212
- );
1213
- }
1214
-
1215
- #[tokio::test]
1216
- async fn staged_writes_reject_duplicate_present_rows_in_one_batch() {
1217
- let staged_writes = test_staged_writes();
1218
-
1219
- let error = staged_writes
1220
- .stage_write(PreparedTransactionWrite::Rows {
1221
- mode: TransactionWriteMode::Replace,
1222
- rows: vec![
1223
- state_row("duplicate-present-key", "first"),
1224
- state_row("duplicate-present-key", "second"),
1225
- ],
1226
- })
1227
- .expect_err("same-batch duplicate present rows should fail");
1228
-
1229
- assert_eq!(error.code, LixError::CODE_UNIQUE);
1230
- assert!(
1231
- error.message.contains("primary-key constraint violation"),
1232
- "error should explain the duplicate primary key: {error:?}"
1233
- );
1234
- }
1235
-
1236
- #[tokio::test]
1237
- async fn staged_writes_insert_keeps_tracked_and_untracked_rows_as_distinct_identities() {
1238
- let staged_writes = test_staged_writes();
1239
-
1240
- staged_writes
1241
- .stage_write(PreparedTransactionWrite::Rows {
1242
- mode: TransactionWriteMode::Insert,
1243
- rows: vec![
1244
- state_row("shared-domain-key", "tracked").with_tracked(),
1245
- state_row("shared-domain-key", "untracked"),
1246
- ],
1247
- })
1248
- .expect("tracked and untracked rows are distinct domain identities");
1249
-
1250
- let drained = staged_writes.drain().expect("drain should succeed");
1251
- assert_eq!(drained.state_rows.len(), 2);
1252
- assert!(drained.state_rows.iter().any(|row| {
1253
- row.entity_pk == crate::entity_pk::EntityPk::single("shared-domain-key")
1254
- && !row.untracked
1255
- }));
1256
- assert!(drained.state_rows.iter().any(|row| {
1257
- row.entity_pk == crate::entity_pk::EntityPk::single("shared-domain-key")
1258
- && row.untracked
1259
- }));
1260
- }
1261
-
1262
- #[tokio::test]
1263
- async fn staged_writes_track_active_branch_members_separately() {
1264
- let staged_writes = test_staged_writes();
1265
-
1266
- staged_writes
1267
- .stage_write(PreparedTransactionWrite::Rows {
1268
- mode: TransactionWriteMode::Replace,
1269
- rows: vec![state_row("active-branch-key", "value")
1270
- .with_tracked()
1271
- .with_branch("branch-a")],
1272
- })
1273
- .expect("active-branch tracked staging should accumulate change_refs");
1274
-
1275
- let drained = staged_writes.drain().expect("drain should succeed");
1276
- let change_refs = drained
1277
- .commit_change_refs_by_branch
1278
- .get("branch-a")
1279
- .expect("active-branch commit change_refs should exist");
1280
- assert_eq!(
1281
- change_refs.change_ids.iter().cloned().collect::<Vec<_>>(),
1282
- vec!["test-change-id".to_string()]
1283
- );
1284
- }
1285
-
1286
- #[tokio::test]
1287
- async fn staged_writes_reject_global_rows_with_non_global_branch_id() {
1288
- let staged_writes = test_staged_writes();
1289
-
1290
- let error = staged_writes
1291
- .stage_write(PreparedTransactionWrite::Rows {
1292
- mode: TransactionWriteMode::Replace,
1293
- rows: vec![{
1294
- let mut row = state_row("invalid-global-key", "value");
1295
- row.branch_id = "branch-a".to_string();
1296
- row
1297
- }],
1298
- })
1299
- .expect_err("global row with non-global branch should fail");
1300
-
1301
- assert!(error
1302
- .message
1303
- .contains("global staged rows must use the global branch id"));
1304
- }
1305
-
1306
- #[tokio::test]
1307
- async fn staging_overlay_identity_matches_live_state_conflict_key() {
1308
- let staged_writes = test_staged_writes();
1309
-
1310
- staged_writes
1311
- .stage_write(PreparedTransactionWrite::Rows {
1312
- mode: TransactionWriteMode::Replace,
1313
- rows: vec![state_row("shared-entity", "base")],
1314
- })
1315
- .expect("initial same-identity row should stage");
1316
- staged_writes
1317
- .stage_write(PreparedTransactionWrite::Rows {
1318
- mode: TransactionWriteMode::Replace,
1319
- rows: vec![
1320
- state_row("shared-entity", "base"),
1321
- state_row("shared-entity", "other-branch").with_branch("branch-b"),
1322
- state_row("shared-entity", "other-schema").with_schema("other_schema"),
1323
- state_row("shared-entity", "other-file").with_file_id("file-a"),
1324
- state_row("shared-entity", "tracked").with_tracked(),
1325
- ],
1326
- })
1327
- .expect("staging rows should succeed");
1328
-
1329
- let overlay = staged_writes
1330
- .staging_overlay()
1331
- .expect("overlay should build from staged rows");
1332
- let rows = overlay
1333
- .scan(&LiveStateScanRequest {
1334
- filter: LiveStateFilter {
1335
- entity_pks: vec![crate::entity_pk::EntityPk::single("shared-entity")],
1336
- include_tombstones: true,
1337
- ..LiveStateFilter::default()
1338
- },
1339
- ..LiveStateScanRequest::default()
1340
- })
1341
- .expect("overlay scan should succeed");
1342
-
1343
- assert_eq!(rows.len(), 4);
1344
- assert_eq!(
1345
- rows.iter()
1346
- .filter(
1347
- |row| row.entity_pk == crate::entity_pk::EntityPk::single("shared-entity")
1348
- && row.branch_id == "global"
1349
- && row.schema_key == "lix_key_value"
1350
- && row.file_id.is_none()
1351
- )
1352
- .count(),
1353
- 1
1354
- );
1355
- assert!(rows.iter().any(|row| {
1356
- row.snapshot_content.as_deref()
1357
- == Some("{\"key\":\"shared-entity\",\"value\":\"base\"}")
1358
- }));
1359
- }
1360
-
1361
- #[tokio::test]
1362
- async fn staged_writes_use_injected_function_provider_for_commit_metadata() {
1363
- let staged_writes = test_staged_writes();
1364
-
1365
- staged_writes
1366
- .stage_write(PreparedTransactionWrite::Rows {
1367
- mode: TransactionWriteMode::Replace,
1368
- rows: vec![state_row("sql2-functions-key", "value").with_tracked()],
1369
- })
1370
- .expect("staging rows should succeed");
1371
-
1372
- let drained = staged_writes.drain().expect("drain should succeed");
1373
- let change_refs = drained
1374
- .commit_change_refs_by_branch
1375
- .get("global")
1376
- .expect("global commit change_refs should exist");
1377
- assert_eq!(change_refs.commit_id, "test-uuid-1");
1378
- assert_eq!(change_refs.commit_change_id, "test-uuid-2");
1379
- assert_eq!(change_refs.created_at, "test-timestamp-1");
1380
- }
1381
-
1382
- #[tokio::test]
1383
- async fn staged_writes_stamp_tracked_rows_with_commit_id_during_staging() {
1384
- let staged_writes = test_staged_writes();
1385
-
1386
- staged_writes
1387
- .stage_write(PreparedTransactionWrite::Rows {
1388
- mode: TransactionWriteMode::Replace,
1389
- rows: vec![state_row("tracked-commit-key", "value").with_tracked()],
1390
- })
1391
- .expect("tracked row should stage");
1392
-
1393
- let drained = staged_writes.drain().expect("drain should succeed");
1394
- assert_eq!(drained.state_rows.len(), 1);
1395
- assert_eq!(
1396
- drained.state_rows[0].commit_id.as_deref(),
1397
- Some("test-uuid-1")
1398
- );
1399
- assert_eq!(
1400
- drained
1401
- .commit_change_refs_by_branch
1402
- .get("global")
1403
- .expect("global commit change_refs should exist")
1404
- .commit_id,
1405
- "test-uuid-1"
1406
- );
1407
- }
1408
-
1409
- fn test_staged_writes() -> Arc<TransactionWriteBuffer> {
1410
- Arc::new(TransactionWriteBuffer::new(SharedFunctionProvider::new(
1411
- Box::new(TestFunctionProvider::default()) as Box<dyn FunctionProvider + Send>,
1412
- )))
1413
- }
1414
-
1415
- #[derive(Default)]
1416
- struct TestFunctionProvider {
1417
- uuid_count: usize,
1418
- timestamp_count: usize,
1419
- }
1420
-
1421
- impl FunctionProvider for TestFunctionProvider {
1422
- fn uuid_v7(&mut self) -> String {
1423
- self.uuid_count += 1;
1424
- format!("test-uuid-{}", self.uuid_count)
1425
- }
1426
-
1427
- fn timestamp(&mut self) -> String {
1428
- self.timestamp_count += 1;
1429
- format!("test-timestamp-{}", self.timestamp_count)
1430
- }
1431
- }
1432
-
1433
- fn state_row(key: &str, value: &str) -> PreparedStateRow {
1434
- let snapshot = stage_json_from_value(
1435
- TransactionJson::from_value_for_test(serde_json::json!({ "key": key, "value": value })),
1436
- "test staged row snapshot_content",
1437
- )
1438
- .expect("test snapshot should prepare");
1439
- PreparedStateRow {
1440
- schema_plan_id: SchemaPlanId::for_test(0),
1441
- facts: crate::transaction::types::PreparedRowFacts::default(),
1442
- entity_pk: crate::entity_pk::EntityPk::single(key),
1443
- schema_key: "lix_key_value".to_string(),
1444
- file_id: None,
1445
- snapshot: Some(snapshot),
1446
- metadata: None,
1447
- origin: None,
1448
- created_at: "test-created-at".to_string(),
1449
- updated_at: "test-updated-at".to_string(),
1450
- global: true,
1451
- change_id: None,
1452
- commit_id: None,
1453
- untracked: true,
1454
- branch_id: "global".to_string(),
1455
- }
1456
- }
1457
-
1458
- fn tombstone_row(key: &str) -> PreparedStateRow {
1459
- let mut row = state_row(key, "deleted");
1460
- row.snapshot = None;
1461
- row
1462
- }
1463
-
1464
- fn exact_request_for_key(key: &str) -> LiveStateRowRequest {
1465
- LiveStateRowRequest {
1466
- schema_key: "lix_key_value".to_string(),
1467
- branch_id: "global".to_string(),
1468
- entity_pk: crate::entity_pk::EntityPk::single(key),
1469
- file_id: NullableKeyFilter::Null,
1470
- }
1471
- }
1472
-
1473
- fn scan_request_for_key(key: &str, include_tombstones: bool) -> LiveStateScanRequest {
1474
- LiveStateScanRequest {
1475
- filter: LiveStateFilter {
1476
- schema_keys: vec!["lix_key_value".to_string()],
1477
- entity_pks: vec![crate::entity_pk::EntityPk::single(key)],
1478
- branch_ids: vec!["global".to_string()],
1479
- file_ids: vec![NullableKeyFilter::Null],
1480
- include_tombstones,
1481
- ..LiveStateFilter::default()
1482
- },
1483
- ..LiveStateScanRequest::default()
1484
- }
1485
- }
1486
-
1487
- trait StateRowTestExt {
1488
- fn with_schema(self, schema_key: &str) -> Self;
1489
- fn with_file_id(self, file_id: &str) -> Self;
1490
- fn with_tracked(self) -> Self;
1491
- fn with_branch(self, branch_id: &str) -> Self;
1492
- fn with_change_id(self, change_id: &str) -> Self;
1493
- }
1494
-
1495
- impl StateRowTestExt for PreparedStateRow {
1496
- fn with_schema(mut self, schema_key: &str) -> Self {
1497
- self.schema_key = schema_key.to_string();
1498
- self
1499
- }
1500
-
1501
- fn with_file_id(mut self, file_id: &str) -> Self {
1502
- self.file_id = Some(file_id.to_string());
1503
- self
1504
- }
1505
-
1506
- fn with_tracked(mut self) -> Self {
1507
- self.untracked = false;
1508
- if self.change_id.is_none() {
1509
- self.change_id = Some("test-change-id".to_string());
1510
- }
1511
- self
1512
- }
1513
-
1514
- fn with_branch(mut self, branch_id: &str) -> Self {
1515
- self.branch_id = branch_id.to_string();
1516
- self.global = branch_id == GLOBAL_BRANCH_ID;
1517
- self
1518
- }
1519
-
1520
- fn with_change_id(mut self, change_id: &str) -> Self {
1521
- self.change_id = Some(change_id.to_string());
1522
- self
1523
- }
1524
- }
1525
- }