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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,1653 +0,0 @@
1
- use std::collections::{BTreeMap, BTreeSet};
2
- use std::sync::atomic::{AtomicUsize, Ordering};
3
- use std::sync::Arc;
4
-
5
- use async_trait::async_trait;
6
- use serde_json::Value as JsonValue;
7
-
8
- use crate::binary_cas::{BinaryCasContext, BlobBytesBatch, BlobHash};
9
- use crate::branch::{BranchContext, BranchRefReader};
10
- use crate::catalog::CatalogContext;
11
- use crate::commit_graph::{CommitGraphContext, CommitGraphStoreReader};
12
- use crate::domain::Domain;
13
- use crate::entity_pk::EntityPk;
14
- use crate::functions::{FunctionContext, FunctionProviderHandle};
15
- use crate::live_state::overlay_scan_rows;
16
- use crate::live_state::{
17
- LiveStateContext, LiveStateRowRequest, LiveStateScanRequest, MaterializedLiveStateRow,
18
- };
19
- use crate::session::{SessionMode, WORKSPACE_BRANCH_KEY};
20
- use crate::sql2::SqlWriteExecutionContext;
21
- use crate::sql2::{
22
- ChangelogQuerySource, HistoryQuerySource, SqlChangelogQuerySource, SqlExecutionContext,
23
- SqlHistoryQuerySource,
24
- };
25
- use crate::storage::{
26
- InMemoryStorageBackend, StorageBackend, StorageReadOptions, StorageWriteOptions,
27
- StorageWriteSetStats,
28
- };
29
- use crate::storage::{StorageContext, StorageRead, StorageReadScope, StorageWriteSet};
30
- use crate::tracked_state::{TrackedStateContext, TrackedStateStoreReader};
31
- use crate::transaction::commit;
32
- use crate::transaction::normalization::{
33
- normalize_transaction_write_row, remember_pending_registered_schema,
34
- NormalizedTransactionWriteRow, REGISTERED_SCHEMA_KEY,
35
- };
36
- use crate::transaction::prepare_branch_ref_row;
37
- use crate::transaction::schema_resolver::TransactionSchemaResolver;
38
- use crate::transaction::staging::{PreparedWriteSet, TransactionWriteBuffer};
39
- use crate::transaction::types::{
40
- stage_json_from_value, PreparedStateRow, PreparedTransactionWrite, StagedCommitChangeRef,
41
- TransactionFileData, TransactionJson, TransactionWrite, TransactionWriteMode,
42
- TransactionWriteOutcome, TransactionWriteRow,
43
- };
44
- use crate::transaction::validation::{validate_prepared_writes, TransactionValidationInput};
45
- use crate::GLOBAL_BRANCH_ID;
46
- use crate::{LixError, NullableKeyFilter};
47
-
48
- #[derive(Debug, Clone, PartialEq, Eq, Default)]
49
- pub(crate) struct TransactionCommitOutcome {
50
- pub(crate) storage_stats: StorageWriteSetStats,
51
- }
52
-
53
- /// One execution-scoped transaction capability for engine write paths.
54
- ///
55
- /// This is intentionally not a session-wide kitchen sink. It owns the backend
56
- /// write transaction for one `SessionContext::execute(...)` call and projects
57
- /// accepted SQL/provider writes back into the SQL DAG through an engine-local live-state
58
- /// overlay.
59
- ///
60
- /// Transaction invariant: this is the capability for engine operations
61
- /// that may write. Write-relevant reads must be exposed from this transaction,
62
- /// after the backend write transaction has begun, rather than from session-level
63
- /// helpers.
64
- pub(crate) struct Transaction<B: StorageBackend = InMemoryStorageBackend> {
65
- active_branch_id: String,
66
- live_state: Arc<LiveStateContext>,
67
- tracked_state: Arc<TrackedStateContext>,
68
- binary_cas: Arc<BinaryCasContext>,
69
- branch_ctx: Arc<BranchContext>,
70
- catalog_context: Arc<CatalogContext>,
71
- schema_resolver: TransactionSchemaResolver,
72
- staged_writes: Arc<TransactionWriteBuffer>,
73
- staged_storage_writes: StorageWriteSet,
74
- storage: StorageContext<B>,
75
- sql_schema_cache: SqlSchemaCache,
76
- functions: FunctionProviderHandle,
77
- commit_boundary: Option<TransactionCommitBoundary>,
78
- }
79
-
80
- #[derive(Default)]
81
- struct SqlSchemaCache {
82
- visible_schemas: Option<Vec<JsonValue>>,
83
- }
84
-
85
- impl SqlSchemaCache {
86
- fn is_prepared(&self) -> bool {
87
- self.visible_schemas.is_some()
88
- }
89
-
90
- fn prepare(&mut self, visible_schemas: Vec<JsonValue>) {
91
- self.visible_schemas = Some(visible_schemas);
92
- }
93
-
94
- fn visible_schemas(&self) -> Result<&[JsonValue], LixError> {
95
- self.visible_schemas.as_deref().ok_or_else(|| {
96
- LixError::new(
97
- LixError::CODE_INTERNAL_ERROR,
98
- "SQL visible schemas were requested before SQL transaction context preparation",
99
- )
100
- })
101
- }
102
- }
103
-
104
- #[derive(Clone)]
105
- pub(crate) struct TransactionCommitBoundary {
106
- state: CommitBoundaryState,
107
- pre_commit_check: Arc<dyn Fn() -> Result<(), LixError> + Send + Sync>,
108
- }
109
-
110
- impl TransactionCommitBoundary {
111
- pub(crate) fn new(
112
- state: CommitBoundaryState,
113
- pre_commit_check: Arc<dyn Fn() -> Result<(), LixError> + Send + Sync>,
114
- ) -> Self {
115
- Self {
116
- state,
117
- pre_commit_check,
118
- }
119
- }
120
-
121
- fn begin(&self) -> CommitBoundaryGuard {
122
- self.state.begin()
123
- }
124
-
125
- fn check(&self) -> Result<(), LixError> {
126
- (self.pre_commit_check)()
127
- }
128
-
129
- fn commit<T>(&self, commit: impl FnOnce() -> Result<T, LixError>) -> Result<T, LixError> {
130
- let _gate = self.state.lock_durable_commit();
131
- self.check()?;
132
- commit()
133
- }
134
- }
135
-
136
- #[derive(Clone)]
137
- pub(crate) struct CommitBoundaryState {
138
- active_count: Arc<AtomicUsize>,
139
- durable_commit_gate: Arc<std::sync::Mutex<()>>,
140
- watch: tokio::sync::watch::Sender<usize>,
141
- }
142
-
143
- impl CommitBoundaryState {
144
- pub(crate) fn new() -> Self {
145
- let (watch, _) = tokio::sync::watch::channel(0);
146
- Self {
147
- active_count: Arc::new(AtomicUsize::new(0)),
148
- durable_commit_gate: Arc::new(std::sync::Mutex::new(())),
149
- watch,
150
- }
151
- }
152
-
153
- pub(crate) fn begin(&self) -> CommitBoundaryGuard {
154
- let previous = self.active_count.fetch_add(1, Ordering::SeqCst);
155
- self.watch.send_replace(previous + 1);
156
- CommitBoundaryGuard {
157
- state: self.clone(),
158
- }
159
- }
160
-
161
- pub(crate) fn active_count(&self) -> usize {
162
- self.active_count.load(Ordering::SeqCst)
163
- }
164
-
165
- pub(crate) fn is_active(&self) -> bool {
166
- self.active_count() > 0
167
- }
168
-
169
- pub(crate) fn subscribe(&self) -> tokio::sync::watch::Receiver<usize> {
170
- self.watch.subscribe()
171
- }
172
-
173
- pub(crate) fn lock_durable_commit(&self) -> std::sync::MutexGuard<'_, ()> {
174
- self.durable_commit_gate
175
- .lock()
176
- .expect("commit boundary durable commit gate should not poison")
177
- }
178
-
179
- pub(crate) fn try_lock_durable_commit(&self) -> Option<std::sync::MutexGuard<'_, ()>> {
180
- match self.durable_commit_gate.try_lock() {
181
- Ok(guard) => Some(guard),
182
- Err(std::sync::TryLockError::WouldBlock) => None,
183
- Err(std::sync::TryLockError::Poisoned(_)) => {
184
- panic!("commit boundary durable commit gate should not poison")
185
- }
186
- }
187
- }
188
- }
189
-
190
- pub(crate) struct CommitBoundaryGuard {
191
- state: CommitBoundaryState,
192
- }
193
-
194
- impl Drop for CommitBoundaryGuard {
195
- fn drop(&mut self) {
196
- let remaining = self.state.active_count.fetch_sub(1, Ordering::SeqCst) - 1;
197
- self.state.watch.send_replace(remaining);
198
- }
199
- }
200
-
201
- pub(crate) fn begin_commit_boundary(
202
- boundary: Option<&TransactionCommitBoundary>,
203
- ) -> Option<CommitBoundaryGuard> {
204
- let boundary = boundary?;
205
- Some(boundary.begin())
206
- }
207
-
208
- fn check_commit_boundary(boundary: Option<&TransactionCommitBoundary>) -> Result<(), LixError> {
209
- if let Some(boundary) = boundary {
210
- boundary.check()?;
211
- }
212
- Ok(())
213
- }
214
-
215
- pub(crate) fn commit_at_boundary<T>(
216
- boundary: Option<&TransactionCommitBoundary>,
217
- commit: impl FnOnce() -> Result<T, LixError>,
218
- ) -> Result<T, LixError> {
219
- match boundary {
220
- Some(boundary) => boundary.commit(commit),
221
- None => commit(),
222
- }
223
- }
224
-
225
- impl<B> Transaction<B>
226
- where
227
- B: StorageBackend + Clone + Send + Sync + 'static,
228
- for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
229
- for<'backend> B::Write<'backend>: Send,
230
- {
231
- /// Opens a backend write transaction and creates an execution-scoped
232
- /// staging area for SQL/provider hooks.
233
- async fn open(
234
- mode: &SessionMode,
235
- storage: StorageContext<B>,
236
- live_state: Arc<LiveStateContext>,
237
- tracked_state: Arc<TrackedStateContext>,
238
- binary_cas: Arc<BinaryCasContext>,
239
- branch_ctx: Arc<BranchContext>,
240
- catalog_context: Arc<CatalogContext>,
241
- ) -> Result<OpenTransaction<B>, LixError> {
242
- let read = storage.begin_read(StorageReadOptions::default())?;
243
- let setup_result = async {
244
- let active_branch_id =
245
- resolve_active_branch_id(mode, live_state.as_ref(), branch_ctx.as_ref(), &read)
246
- .await?;
247
- let runtime_functions = {
248
- let runtime_live_state = live_state.reader(&read);
249
- FunctionContext::prepare(&runtime_live_state).await?
250
- };
251
- let functions = runtime_functions.provider();
252
- let schema_facts = {
253
- let visible_live_state = live_state.reader(&read);
254
- catalog_context
255
- .schema_facts_for_domain(
256
- &visible_live_state,
257
- &Domain::schema_catalog(active_branch_id.clone(), true),
258
- )
259
- .await?
260
- };
261
- Ok::<_, LixError>((active_branch_id, runtime_functions, functions, schema_facts))
262
- }
263
- .await;
264
- let (active_branch_id, runtime_functions, functions, schema_facts) = match setup_result {
265
- Ok(result) => result,
266
- Err(error) => {
267
- return Err(error);
268
- }
269
- };
270
- let mut schema_resolver = TransactionSchemaResolver::new(Arc::clone(&catalog_context));
271
- schema_resolver.remember_schema_facts(
272
- &Domain::schema_catalog(active_branch_id.clone(), true),
273
- schema_facts,
274
- );
275
- let staged_writes = Arc::new(TransactionWriteBuffer::new(functions.clone()));
276
- Ok(OpenTransaction {
277
- transaction: Self {
278
- active_branch_id,
279
- live_state,
280
- tracked_state,
281
- binary_cas,
282
- branch_ctx,
283
- catalog_context,
284
- schema_resolver,
285
- staged_writes,
286
- staged_storage_writes: StorageWriteSet::new(),
287
- storage,
288
- sql_schema_cache: SqlSchemaCache::default(),
289
- functions,
290
- commit_boundary: None,
291
- },
292
- runtime_functions,
293
- })
294
- }
295
-
296
- /// Commits prepared writes, runtime function state, and the backend transaction.
297
- ///
298
- /// Commit owns the execution boundary: prepared rows become changelog
299
- /// facts, branch-ref updates, and visible live_state rows before the
300
- /// backend transaction is committed.
301
- #[allow(dead_code)]
302
- pub(crate) async fn commit(
303
- self,
304
- runtime_functions: &FunctionContext,
305
- ) -> Result<TransactionCommitOutcome, LixError> {
306
- let mut transaction = self;
307
- let commit_boundary = transaction.commit_boundary.clone();
308
- let prepared_writes = match transaction.staged_writes.drain() {
309
- Ok(prepared_writes) => prepared_writes,
310
- Err(error) => {
311
- return Err(error);
312
- }
313
- };
314
- let _commit_guard = begin_commit_boundary(commit_boundary.as_ref());
315
- check_commit_boundary(commit_boundary.as_ref())?;
316
- if let Err(error) = transaction
317
- .validate_prepared_writes_by_branch(&prepared_writes)
318
- .await
319
- {
320
- return Err(error);
321
- }
322
- let mut read = transaction
323
- .storage
324
- .begin_read(StorageReadOptions::default())?;
325
- let mut writes = match commit::commit_prepared_writes(
326
- &transaction.binary_cas,
327
- transaction.branch_ctx.as_ref(),
328
- Some(runtime_functions),
329
- &mut read,
330
- prepared_writes,
331
- )
332
- .await
333
- {
334
- Ok(writes) => writes,
335
- Err(error) => return Err(error),
336
- };
337
- writes.extend(transaction.staged_storage_writes);
338
- let storage_stats = commit_at_boundary(commit_boundary.as_ref(), || {
339
- let (_commit, stats) = transaction
340
- .storage
341
- .commit_write_set(writes, StorageWriteOptions::default())?;
342
- Ok(stats)
343
- })?;
344
- Ok(TransactionCommitOutcome { storage_stats })
345
- }
346
-
347
- pub(crate) fn attach_commit_boundary(&mut self, boundary: TransactionCommitBoundary) {
348
- self.commit_boundary = Some(boundary);
349
- }
350
-
351
- /// Rolls back the backend transaction.
352
- ///
353
- /// This is the explicit failure path for a write execution. Dropping the
354
- /// buffered transaction without commit is not the API we want callers to
355
- /// rely on.
356
- #[allow(dead_code)]
357
- pub(crate) async fn rollback(self) -> Result<(), LixError> {
358
- Ok(())
359
- }
360
-
361
- /// Stages one decoded write batch into this transaction.
362
- ///
363
- /// This is the programmatic write entrypoint used by non-SQL APIs. The
364
- /// transaction still owns preparation from `TransactionWriteRow` into
365
- /// `PreparedStateRow`, so generated timestamps, change ids, commit ids, and
366
- /// commit change refs stay in one place.
367
- #[allow(dead_code)]
368
- pub(crate) async fn stage_write(
369
- &mut self,
370
- write: TransactionWrite,
371
- ) -> Result<TransactionWriteOutcome, LixError> {
372
- require_valid_transaction_write_storage_scopes(&write)?;
373
- #[cfg(feature = "storage-benches")]
374
- {
375
- crate::storage_bench::record_transaction_rows_staged(transaction_write_row_count(
376
- &write,
377
- ));
378
- crate::storage_bench::record_transaction_untracked_rows(
379
- transaction_write_untracked_row_count(&write),
380
- );
381
- }
382
- self.require_existing_transaction_write_branch_ids(&write)
383
- .await?;
384
- let write = self.prepare_transaction_write(write).await?;
385
- self.staged_writes.stage_write(write)
386
- }
387
-
388
- async fn prepare_transaction_write(
389
- &mut self,
390
- write: TransactionWrite,
391
- ) -> Result<PreparedTransactionWrite, LixError> {
392
- Ok(match write {
393
- TransactionWrite::Rows { mode, rows } => PreparedTransactionWrite::Rows {
394
- mode,
395
- rows: self.prepare_transaction_rows(rows).await?,
396
- },
397
- TransactionWrite::RowsWithFileData {
398
- mode,
399
- rows,
400
- file_data,
401
- count,
402
- } => PreparedTransactionWrite::RowsWithFileData {
403
- mode,
404
- rows: self.prepare_transaction_rows(rows).await?,
405
- file_data,
406
- count,
407
- },
408
- })
409
- }
410
-
411
- async fn prepare_transaction_rows(
412
- &mut self,
413
- rows: Vec<TransactionWriteRow>,
414
- ) -> Result<Vec<PreparedStateRow>, LixError> {
415
- let row_count = rows.len();
416
- let staged = self.staged_writes.staging_overlay()?;
417
- let read = self.storage.begin_read(StorageReadOptions::default())?;
418
- let live_state = self.live_state.reader(&read);
419
- let mut rows_by_scope = BTreeMap::<Domain, Vec<(usize, TransactionWriteRow)>>::new();
420
- for (index, row) in rows.into_iter().enumerate() {
421
- rows_by_scope
422
- .entry(Domain::schema_catalog(
423
- row.schema_scope_branch_id().to_string(),
424
- row.untracked,
425
- ))
426
- .or_default()
427
- .push((index, row));
428
- }
429
-
430
- let mut prepared_rows = Vec::with_capacity(row_count);
431
- prepared_rows.resize_with(row_count, || None);
432
- for (domain, rows) in rows_by_scope {
433
- let functions = self.functions.clone();
434
- let catalog = self
435
- .schema_resolver
436
- .catalog_for_row_normalization(&live_state, &staged, &domain)
437
- .await?;
438
- for (_, row) in &rows {
439
- if row.schema_key != REGISTERED_SCHEMA_KEY {
440
- continue;
441
- }
442
- if row.file_id.is_some() {
443
- return Err(LixError::new(
444
- LixError::CODE_SCHEMA_DEFINITION,
445
- "lix_registered_schema rows must not be scoped to a file",
446
- )
447
- .with_hint("Schema definitions are scoped by branch and durability only; write them with null file_id."));
448
- }
449
- remember_pending_registered_schema(
450
- row.snapshot.as_ref().map(TransactionJson::value),
451
- Domain::schema_catalog(row.schema_scope_branch_id().to_string(), row.untracked),
452
- catalog,
453
- )?;
454
- }
455
- let normalized_rows = rows
456
- .into_iter()
457
- .map(|(index, row)| {
458
- normalize_transaction_write_row(row, catalog, functions.clone())
459
- .map(|row| (index, row))
460
- })
461
- .collect::<Result<Vec<_>, _>>()?;
462
- for (index, row) in normalized_rows {
463
- prepared_rows[index] = Some(prepare_state_row(row, &functions)?);
464
- }
465
- }
466
- Ok(prepared_rows
467
- .into_iter()
468
- .map(|row| {
469
- row.expect("every row should be prepared exactly once by schema scope grouping")
470
- })
471
- .collect())
472
- }
473
-
474
- async fn validate_prepared_writes_by_branch(
475
- &mut self,
476
- prepared_writes: &PreparedWriteSet,
477
- ) -> Result<(), LixError> {
478
- let validation_index = prepared_writes.validation_index();
479
- for scope in validation_index.schema_scopes() {
480
- #[cfg(feature = "storage-benches")]
481
- crate::storage_bench::record_transaction_validation_branch();
482
- let branch_prepared_writes = validation_index.validation_set_for_schema_scope(scope);
483
- let read = self.storage.begin_read(StorageReadOptions::default())?;
484
- let live_state = self.live_state.reader(&read);
485
- let schema_catalog = self
486
- .schema_resolver
487
- .catalog_for_validation(&live_state, scope)
488
- .await?;
489
- validate_prepared_writes(TransactionValidationInput::new(
490
- &branch_prepared_writes,
491
- &schema_catalog,
492
- &live_state,
493
- ))
494
- .await?;
495
- }
496
- Ok(())
497
- }
498
-
499
- /// Convenience helper for programmatic APIs that only stage state rows.
500
- #[allow(dead_code)]
501
- pub(crate) async fn stage_rows(
502
- &mut self,
503
- rows: Vec<TransactionWriteRow>,
504
- ) -> Result<TransactionWriteOutcome, LixError> {
505
- self.stage_write(TransactionWrite::Rows {
506
- mode: TransactionWriteMode::Replace,
507
- rows,
508
- })
509
- .await
510
- }
511
-
512
- async fn require_existing_transaction_write_branch_ids(
513
- &mut self,
514
- write: &TransactionWrite,
515
- ) -> Result<(), LixError> {
516
- let branch_ids = transaction_write_branch_ids(write);
517
- let read = self.storage.begin_read(StorageReadOptions::default())?;
518
- let reader = self.branch_ctx.ref_reader(&read);
519
- for branch_id in branch_ids {
520
- if branch_id == GLOBAL_BRANCH_ID {
521
- continue;
522
- }
523
- if reader.load_head_commit_id(&branch_id).await?.is_none() {
524
- return Err(LixError::branch_not_found(
525
- branch_id,
526
- "stage_write",
527
- "target",
528
- ));
529
- }
530
- }
531
- Ok(())
532
- }
533
-
534
- /// Returns the active branch resolved inside this write transaction.
535
- pub(crate) fn active_branch_id(&self) -> &str {
536
- &self.active_branch_id
537
- }
538
-
539
- /// Returns this transaction's prepared runtime functions.
540
- pub(crate) fn functions(&self) -> FunctionProviderHandle {
541
- self.functions.clone()
542
- }
543
-
544
- pub(crate) async fn sql_read_execution_context(
545
- &mut self,
546
- ) -> Result<TransactionSqlReadExecutionContext<B::Read<'_>>, LixError> {
547
- self.prepare_sql_visible_schemas().await?;
548
- self.sql_read_execution_context_from_cached_schemas()
549
- }
550
-
551
- fn sql_read_execution_context_from_cached_schemas(
552
- &self,
553
- ) -> Result<TransactionSqlReadExecutionContext<B::Read<'_>>, LixError> {
554
- let read_store = self.storage.begin_read(StorageReadOptions::default())?;
555
- let staged = self.staged_writes.staging_overlay()?;
556
- Ok(TransactionSqlReadExecutionContext {
557
- active_branch_id: self.active_branch_id.clone(),
558
- read_store,
559
- live_state: Arc::clone(&self.live_state),
560
- binary_cas: Arc::clone(&self.binary_cas),
561
- branch_ctx: Arc::clone(&self.branch_ctx),
562
- visible_schemas: self.cached_visible_schemas()?.to_vec(),
563
- functions: self.functions.clone(),
564
- staged,
565
- })
566
- }
567
-
568
- pub(crate) async fn prepare_sql_visible_schemas(&mut self) -> Result<(), LixError> {
569
- if self.sql_schema_cache.is_prepared() {
570
- return Ok(());
571
- }
572
- let read = self.storage.begin_read(StorageReadOptions::default())?;
573
- let live_state = self.live_state.reader(&read);
574
- let visible_schemas = self
575
- .catalog_context
576
- .schema_jsons_for_sql_read_planning(&live_state, &self.active_branch_id)
577
- .await?;
578
- self.sql_schema_cache.prepare(visible_schemas);
579
- Ok(())
580
- }
581
-
582
- fn cached_visible_schemas(&self) -> Result<&[JsonValue], LixError> {
583
- self.sql_schema_cache.visible_schemas()
584
- }
585
-
586
- /// Advances a branch ref without staging tracked rows.
587
- ///
588
- /// Fast-forward merges use this path because the commit graph already
589
- /// contains the source head; the target ref only needs to move to it.
590
- pub(crate) async fn advance_branch_ref(
591
- &mut self,
592
- branch_id: &str,
593
- commit_id: &str,
594
- ) -> Result<(), LixError> {
595
- let timestamp = self.functions.call_timestamp();
596
- let canonical_row = prepare_branch_ref_row(branch_id, commit_id, &timestamp)?;
597
- self.branch_ctx
598
- .stage_canonical_ref_rows(&mut self.staged_storage_writes, &[canonical_row.row])
599
- }
600
-
601
- pub(crate) fn stage_merge_commit(
602
- &self,
603
- branch_id: String,
604
- source_parent_commit_id: String,
605
- selected_changes: impl IntoIterator<Item = StagedCommitChangeRef>,
606
- ) -> Result<String, LixError> {
607
- let commit_id = self
608
- .staged_writes
609
- .stage_selected_commit_change_refs(branch_id.clone(), selected_changes)?;
610
- self.staged_writes
611
- .add_commit_parent(branch_id, source_parent_commit_id)?;
612
- Ok(commit_id)
613
- }
614
-
615
- /// Creates a branch-ref reader scoped to this write transaction.
616
- pub(crate) fn branch_ref_reader(&mut self) -> impl BranchRefReader + '_ {
617
- let read = self
618
- .storage
619
- .begin_read(StorageReadOptions::default())
620
- .expect("open transaction read scope");
621
- self.branch_ctx.ref_reader(read)
622
- }
623
-
624
- /// Creates a tracked-state reader scoped to this write transaction.
625
- pub(crate) fn tracked_state_reader(
626
- &mut self,
627
- ) -> TrackedStateStoreReader<StorageReadScope<B::Read<'_>>> {
628
- let read = self
629
- .storage
630
- .begin_read(StorageReadOptions::default())
631
- .expect("open transaction read scope");
632
- self.tracked_state.reader(read)
633
- }
634
-
635
- /// Creates a commit-graph reader scoped to this write transaction.
636
- pub(crate) fn commit_graph_reader(
637
- &mut self,
638
- ) -> CommitGraphStoreReader<StorageReadScope<B::Read<'_>>> {
639
- let read = self
640
- .storage
641
- .begin_read(StorageReadOptions::default())
642
- .expect("open transaction read scope");
643
- CommitGraphContext::new().reader(read)
644
- }
645
- }
646
-
647
- pub(crate) struct TransactionSqlReadExecutionContext<R> {
648
- active_branch_id: String,
649
- read_store: StorageReadScope<R>,
650
- live_state: Arc<LiveStateContext>,
651
- binary_cas: Arc<BinaryCasContext>,
652
- branch_ctx: Arc<BranchContext>,
653
- visible_schemas: Vec<JsonValue>,
654
- functions: FunctionProviderHandle,
655
- staged: crate::transaction::staging::PreparedStateRowOverlay,
656
- }
657
-
658
- impl<R> TransactionSqlReadExecutionContext<R>
659
- where
660
- R: crate::storage::StorageBackendRead,
661
- {
662
- pub(crate) fn close(self) -> Result<(), LixError> {
663
- self.read_store.close().map_err(Into::into)
664
- }
665
- }
666
-
667
- impl<R> SqlExecutionContext for TransactionSqlReadExecutionContext<R>
668
- where
669
- R: crate::storage::StorageBackendRead + Clone + Send + Sync + 'static,
670
- {
671
- type ReadStore = StorageReadScope<R>;
672
-
673
- fn active_branch_id(&self) -> &str {
674
- &self.active_branch_id
675
- }
676
-
677
- fn live_state(&self) -> Arc<dyn crate::live_state::LiveStateReader> {
678
- Arc::new(TransactionReadLiveStateReader {
679
- base: self.live_state.reader(self.read_store.clone()),
680
- staged: self.staged.clone(),
681
- })
682
- }
683
-
684
- fn functions(&self) -> FunctionProviderHandle {
685
- self.functions.clone()
686
- }
687
-
688
- fn history_query_source(&self) -> SqlHistoryQuerySource<Self::ReadStore> {
689
- HistoryQuerySource {
690
- json_reader: crate::json_store::JsonStoreContext::new().reader(self.read_store.store()),
691
- }
692
- }
693
-
694
- fn changelog_query_source(&self) -> SqlChangelogQuerySource<Self::ReadStore> {
695
- ChangelogQuerySource {
696
- store: self.read_store.clone(),
697
- json_reader: crate::json_store::JsonStoreContext::new().reader(self.read_store.store()),
698
- }
699
- }
700
-
701
- fn commit_graph(&self) -> Box<dyn crate::commit_graph::CommitGraphReader> {
702
- Box::new(CommitGraphContext::new().reader(self.read_store.clone()))
703
- }
704
-
705
- fn branch_ref(&self) -> Arc<dyn BranchRefReader> {
706
- Arc::new(self.branch_ctx.ref_reader(self.read_store.clone()))
707
- }
708
-
709
- fn blob_reader(&self) -> Arc<dyn crate::binary_cas::BlobDataReader> {
710
- Arc::new(self.binary_cas.reader(self.read_store.clone()))
711
- }
712
-
713
- fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
714
- Ok(self.visible_schemas.clone())
715
- }
716
- }
717
-
718
- struct TransactionReadLiveStateReader<R> {
719
- base: crate::live_state::LiveStateStoreReader<StorageReadScope<R>>,
720
- staged: crate::transaction::staging::PreparedStateRowOverlay,
721
- }
722
-
723
- #[async_trait]
724
- impl<R> crate::live_state::LiveStateReader for TransactionReadLiveStateReader<R>
725
- where
726
- R: crate::storage::StorageBackendRead + Clone + Send + Sync,
727
- {
728
- async fn scan_rows(
729
- &self,
730
- request: &LiveStateScanRequest,
731
- ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
732
- overlay_scan_rows(&self.base, &self.staged, request).await
733
- }
734
-
735
- async fn load_row(
736
- &self,
737
- request: &LiveStateRowRequest,
738
- ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
739
- Ok(self
740
- .scan_rows(&LiveStateScanRequest {
741
- filter: crate::live_state::LiveStateFilter {
742
- schema_keys: vec![request.schema_key.clone()],
743
- entity_pks: vec![request.entity_pk.clone()],
744
- branch_ids: vec![request.branch_id.clone()],
745
- file_ids: vec![request.file_id.clone()],
746
- ..Default::default()
747
- },
748
- limit: Some(1),
749
- ..Default::default()
750
- })
751
- .await?
752
- .into_iter()
753
- .next())
754
- }
755
- }
756
-
757
- fn prepare_state_row(
758
- normalized: NormalizedTransactionWriteRow,
759
- functions: &FunctionProviderHandle,
760
- ) -> Result<PreparedStateRow, LixError> {
761
- let NormalizedTransactionWriteRow {
762
- row,
763
- snapshot,
764
- schema_plan_id,
765
- facts,
766
- } = normalized;
767
- let updated_at = row.updated_at.unwrap_or_else(|| functions.call_timestamp());
768
- let snapshot = snapshot
769
- .map(|value| stage_json_from_value(value, "prepared row snapshot_content"))
770
- .transpose()?;
771
- let metadata = row
772
- .metadata
773
- .map(|value| stage_json_from_value(value, "prepared row metadata"))
774
- .transpose()?;
775
- Ok(PreparedStateRow {
776
- schema_plan_id,
777
- facts,
778
- entity_pk: row.entity_pk.ok_or_else(|| {
779
- LixError::new(
780
- "LIX_ERROR_UNKNOWN",
781
- "normalized transaction write row is missing entity_pk",
782
- )
783
- })?,
784
- schema_key: row.schema_key,
785
- file_id: row.file_id,
786
- snapshot,
787
- metadata,
788
- origin: row.origin,
789
- created_at: row.created_at.unwrap_or_else(|| updated_at.clone()),
790
- updated_at,
791
- global: row.global,
792
- change_id: if row.untracked {
793
- row.change_id
794
- } else {
795
- Some(row.change_id.unwrap_or_else(|| functions.call_uuid_v7()))
796
- },
797
- commit_id: row.commit_id,
798
- untracked: row.untracked,
799
- branch_id: row.branch_id,
800
- })
801
- }
802
-
803
- pub(crate) struct OpenTransaction<B: StorageBackend = InMemoryStorageBackend> {
804
- pub(crate) transaction: Transaction<B>,
805
- pub(crate) runtime_functions: FunctionContext,
806
- }
807
-
808
- pub(crate) async fn open_transaction<B>(
809
- mode: &SessionMode,
810
- storage: StorageContext<B>,
811
- live_state: Arc<LiveStateContext>,
812
- tracked_state: Arc<TrackedStateContext>,
813
- binary_cas: Arc<BinaryCasContext>,
814
- branch_ctx: Arc<BranchContext>,
815
- catalog_context: Arc<CatalogContext>,
816
- ) -> Result<OpenTransaction<B>, LixError>
817
- where
818
- B: StorageBackend + Clone + Send + Sync + 'static,
819
- for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
820
- for<'backend> B::Write<'backend>: Send,
821
- {
822
- Transaction::open(
823
- mode,
824
- storage,
825
- live_state,
826
- tracked_state,
827
- binary_cas,
828
- branch_ctx,
829
- catalog_context,
830
- )
831
- .await
832
- }
833
-
834
- #[async_trait]
835
- impl<B> SqlWriteExecutionContext for Transaction<B>
836
- where
837
- B: StorageBackend + Clone + Send + Sync + 'static,
838
- for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
839
- for<'backend> B::Write<'backend>: Send,
840
- {
841
- fn active_branch_id(&self) -> &str {
842
- &self.active_branch_id
843
- }
844
-
845
- fn functions(&self) -> FunctionProviderHandle {
846
- self.functions.clone()
847
- }
848
-
849
- fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
850
- Ok(self.cached_visible_schemas()?.to_vec())
851
- }
852
-
853
- async fn load_bytes_many(&mut self, hashes: &[BlobHash]) -> Result<BlobBytesBatch, LixError> {
854
- let read = self.storage.begin_read(StorageReadOptions::default())?;
855
- self.binary_cas.reader(&read).load_bytes_many(hashes).await
856
- }
857
-
858
- async fn scan_live_state(
859
- &mut self,
860
- request: &LiveStateScanRequest,
861
- ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
862
- let staged = self.staged_writes.staging_overlay()?;
863
- let read = self.storage.begin_read(StorageReadOptions::default())?;
864
- let base = self.live_state.reader(&read);
865
- overlay_scan_rows(&base, &staged, request).await
866
- }
867
-
868
- async fn load_branch_head(&mut self, branch_id: &str) -> Result<Option<String>, LixError> {
869
- let read = self.storage.begin_read(StorageReadOptions::default())?;
870
- let result = self
871
- .branch_ctx
872
- .ref_reader(&read)
873
- .load_head_commit_id(branch_id)
874
- .await;
875
- result
876
- }
877
-
878
- async fn stage_write(
879
- &mut self,
880
- write: TransactionWrite,
881
- ) -> Result<TransactionWriteOutcome, LixError> {
882
- Transaction::stage_write(self, write).await
883
- }
884
- }
885
-
886
- fn transaction_write_branch_ids(write: &TransactionWrite) -> BTreeSet<String> {
887
- match write {
888
- TransactionWrite::Rows { rows, .. } => transaction_write_row_branch_ids(rows),
889
- TransactionWrite::RowsWithFileData {
890
- rows, file_data, ..
891
- } => transaction_write_row_branch_ids(rows)
892
- .into_iter()
893
- .chain(stage_file_data_branch_ids(file_data))
894
- .collect(),
895
- }
896
- }
897
-
898
- #[cfg(feature = "storage-benches")]
899
- fn transaction_write_row_count(write: &TransactionWrite) -> usize {
900
- match write {
901
- TransactionWrite::Rows { rows, .. } => rows.len(),
902
- TransactionWrite::RowsWithFileData { rows, .. } => rows.len(),
903
- }
904
- }
905
-
906
- #[cfg(feature = "storage-benches")]
907
- fn transaction_write_untracked_row_count(write: &TransactionWrite) -> usize {
908
- match write {
909
- TransactionWrite::Rows { rows, .. } => rows.iter().filter(|row| row.untracked).count(),
910
- TransactionWrite::RowsWithFileData { rows, .. } => {
911
- rows.iter().filter(|row| row.untracked).count()
912
- }
913
- }
914
- }
915
-
916
- fn require_valid_transaction_write_storage_scopes(
917
- write: &TransactionWrite,
918
- ) -> Result<(), LixError> {
919
- match write {
920
- TransactionWrite::Rows { rows, .. } => {
921
- require_valid_transaction_write_row_storage_scopes(rows)
922
- }
923
- TransactionWrite::RowsWithFileData { rows, .. } => {
924
- require_valid_transaction_write_row_storage_scopes(rows)
925
- }
926
- }
927
- }
928
-
929
- fn require_valid_transaction_write_row_storage_scopes(
930
- rows: &[TransactionWriteRow],
931
- ) -> Result<(), LixError> {
932
- for row in rows {
933
- require_valid_storage_scope(row.branch_id.as_str(), row.global)?;
934
- }
935
- Ok(())
936
- }
937
-
938
- fn require_valid_storage_scope(branch_id: &str, global: bool) -> Result<(), LixError> {
939
- if global != (branch_id == GLOBAL_BRANCH_ID) {
940
- return Err(LixError::new(
941
- LixError::CODE_INVALID_STORAGE_SCOPE,
942
- format!("invalid storage scope: branch_id='{branch_id}', global={global}"),
943
- ));
944
- }
945
- Ok(())
946
- }
947
-
948
- fn transaction_write_row_branch_ids(rows: &[TransactionWriteRow]) -> BTreeSet<String> {
949
- rows.iter().map(|row| row.branch_id.clone()).collect()
950
- }
951
-
952
- fn stage_file_data_branch_ids(file_data: &[TransactionFileData]) -> BTreeSet<String> {
953
- file_data
954
- .iter()
955
- .map(|write| write.branch_id.clone())
956
- .collect()
957
- }
958
-
959
- async fn resolve_active_branch_id(
960
- mode: &SessionMode,
961
- live_state: &LiveStateContext,
962
- branch_ctx: &BranchContext,
963
- read: &(impl StorageRead + Send + Sync + ?Sized),
964
- ) -> Result<String, LixError> {
965
- match mode {
966
- SessionMode::Pinned { branch_id } => Ok(branch_id.clone()),
967
- SessionMode::Workspace => load_workspace_branch_id(live_state, branch_ctx, read).await,
968
- }
969
- }
970
-
971
- async fn load_workspace_branch_id(
972
- live_state: &LiveStateContext,
973
- branch_ctx: &BranchContext,
974
- read: &(impl StorageRead + Send + Sync + ?Sized),
975
- ) -> Result<String, LixError> {
976
- let row = live_state
977
- .reader(read)
978
- .load_row(&LiveStateRowRequest {
979
- schema_key: "lix_key_value".to_string(),
980
- branch_id: GLOBAL_BRANCH_ID.to_string(),
981
- entity_pk: EntityPk::single(WORKSPACE_BRANCH_KEY),
982
- file_id: NullableKeyFilter::Null,
983
- })
984
- .await?
985
- .ok_or_else(|| {
986
- LixError::new(
987
- "LIX_ERROR_UNKNOWN",
988
- "workspace branch selector is missing lix_key_value:lix_workspace_branch_id",
989
- )
990
- })?;
991
- let snapshot_content = row.snapshot_content.as_deref().ok_or_else(|| {
992
- LixError::new(
993
- "LIX_ERROR_UNKNOWN",
994
- "workspace branch selector is missing snapshot_content",
995
- )
996
- })?;
997
- let snapshot = serde_json::from_str::<JsonValue>(snapshot_content).map_err(|error| {
998
- LixError::new(
999
- "LIX_ERROR_UNKNOWN",
1000
- format!("workspace branch selector snapshot is invalid JSON: {error}"),
1001
- )
1002
- })?;
1003
- let branch_id = snapshot
1004
- .get("value")
1005
- .and_then(JsonValue::as_str)
1006
- .filter(|value| !value.is_empty())
1007
- .ok_or_else(|| {
1008
- LixError::new(
1009
- "LIX_ERROR_UNKNOWN",
1010
- "workspace branch selector value must be a non-empty string",
1011
- )
1012
- })?
1013
- .to_string();
1014
-
1015
- let head = branch_ctx
1016
- .ref_reader(read)
1017
- .load_head_commit_id(&branch_id)
1018
- .await?;
1019
- if head.is_none() {
1020
- return Err(LixError::branch_not_found(
1021
- branch_id,
1022
- "load_workspace_branch_id",
1023
- "workspace_selector",
1024
- ));
1025
- }
1026
-
1027
- Ok(branch_id)
1028
- }
1029
-
1030
- #[cfg(test)]
1031
- mod tests {
1032
- use std::sync::Arc;
1033
-
1034
- use serde_json::json;
1035
-
1036
- use super::*;
1037
- use crate::branch::BranchContext;
1038
- use crate::changelog::ChangelogReader;
1039
- use crate::storage::{InMemoryStorageBackend, StorageReadOptions};
1040
- use crate::tracked_state::{TrackedStateKey, TrackedStateScanRequest};
1041
- use crate::transaction::types::TransactionJson;
1042
- use crate::untracked_state::{UntrackedStateContext, UntrackedStateRowRequest};
1043
- use crate::NullableKeyFilter;
1044
- use crate::GLOBAL_BRANCH_ID;
1045
-
1046
- fn live_state_context() -> LiveStateContext {
1047
- LiveStateContext::new(
1048
- crate::tracked_state::TrackedStateContext::new(),
1049
- crate::untracked_state::UntrackedStateContext::new(),
1050
- crate::commit_graph::CommitGraphContext::new(),
1051
- )
1052
- }
1053
-
1054
- const SCHEMA_FIXTURE_COMMIT_ID: &str = "schema-fixture-commit";
1055
-
1056
- #[tokio::test]
1057
- async fn stage_rows_routes_tracked_and_untracked_rows_without_sql() {
1058
- let backend = InMemoryStorageBackend::new();
1059
- let storage = StorageContext::new(backend.clone());
1060
- let live_state = Arc::new(live_state_context());
1061
- seed_visible_schema_rows(storage.clone()).await;
1062
- let binary_cas = Arc::new(BinaryCasContext::new());
1063
- let tracked_state = Arc::new(crate::tracked_state::TrackedStateContext::new());
1064
- let branch_ctx = Arc::new(BranchContext::new(Arc::new(UntrackedStateContext::new())));
1065
- let catalog_context = Arc::new(CatalogContext::new());
1066
- let opened = open_transaction(
1067
- &SessionMode::Pinned {
1068
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1069
- },
1070
- storage.clone(),
1071
- Arc::clone(&live_state),
1072
- Arc::clone(&tracked_state),
1073
- Arc::clone(&binary_cas),
1074
- Arc::clone(&branch_ctx),
1075
- Arc::clone(&catalog_context),
1076
- )
1077
- .await
1078
- .expect("transaction should open");
1079
- let mut transaction = opened.transaction;
1080
- let runtime_functions = opened.runtime_functions;
1081
-
1082
- transaction
1083
- .stage_rows(vec![
1084
- key_value_stage_row("tracked-programmatic", "tracked", false),
1085
- key_value_stage_row("untracked-programmatic", "untracked", true),
1086
- ])
1087
- .await
1088
- .expect("programmatic rows should stage");
1089
- transaction
1090
- .commit(&runtime_functions)
1091
- .await
1092
- .expect("transaction should commit");
1093
-
1094
- let tracked_row = live_state
1095
- .reader(
1096
- storage
1097
- .begin_read(StorageReadOptions::default())
1098
- .expect("read should open"),
1099
- )
1100
- .load_row(&LiveStateRowRequest {
1101
- schema_key: "lix_key_value".to_string(),
1102
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1103
- entity_pk: crate::entity_pk::EntityPk::single("tracked-programmatic"),
1104
- file_id: NullableKeyFilter::Null,
1105
- })
1106
- .await
1107
- .expect("tracked row should load")
1108
- .expect("tracked row should exist");
1109
- let tracked_change_id = tracked_row
1110
- .change_id
1111
- .as_ref()
1112
- .expect("tracked row should have a change id")
1113
- .clone();
1114
- let mut changelog_reader = crate::changelog::ChangelogContext::new().reader(
1115
- storage
1116
- .begin_read(StorageReadOptions::default())
1117
- .expect("read should open"),
1118
- );
1119
- let changes = changelog_reader
1120
- .load_changes(crate::changelog::ChangeLoadRequest {
1121
- change_ids: &[tracked_change_id],
1122
- })
1123
- .await
1124
- .expect("changelog should load tracked change");
1125
- assert!(
1126
- matches!(
1127
- changes.entries.as_slice(),
1128
- [Some(change)]
1129
- if change.entity_pk.as_single_string_owned().as_deref()
1130
- == Ok("tracked-programmatic")
1131
- ),
1132
- "tracked staged row should be appended to changelog"
1133
- );
1134
-
1135
- let head_commit_id = branch_ctx
1136
- .ref_reader(
1137
- storage
1138
- .begin_read(StorageReadOptions::default())
1139
- .expect("read should open"),
1140
- )
1141
- .load_head_commit_id(GLOBAL_BRANCH_ID)
1142
- .await
1143
- .expect("branch ref should load")
1144
- .expect("tracked commit should advance the global branch ref");
1145
-
1146
- let tracked_row = crate::tracked_state::TrackedStateContext::new()
1147
- .reader(
1148
- storage
1149
- .begin_read(StorageReadOptions::default())
1150
- .expect("read should open"),
1151
- )
1152
- .load_rows_at_commit(
1153
- &head_commit_id,
1154
- &[TrackedStateKey {
1155
- schema_key: "lix_key_value".to_string(),
1156
- entity_pk: crate::entity_pk::EntityPk::single("tracked-programmatic"),
1157
- file_id: None,
1158
- }],
1159
- )
1160
- .await
1161
- .expect("tracked state should load")
1162
- .pop()
1163
- .flatten()
1164
- .expect("tracked row should be present in tracked state");
1165
- assert_eq!(tracked_row.commit_id, head_commit_id);
1166
- assert_eq!(
1167
- tracked_row.snapshot_content.as_deref(),
1168
- Some(r#"{"key":"tracked-programmatic","value":"tracked"}"#)
1169
- );
1170
-
1171
- let untracked_row = crate::untracked_state::UntrackedStateContext::new()
1172
- .reader(
1173
- storage
1174
- .begin_read(StorageReadOptions::default())
1175
- .expect("read should open"),
1176
- )
1177
- .load_row(&UntrackedStateRowRequest {
1178
- schema_key: "lix_key_value".to_string(),
1179
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1180
- entity_pk: crate::entity_pk::EntityPk::single("untracked-programmatic"),
1181
- file_id: NullableKeyFilter::Null,
1182
- })
1183
- .await
1184
- .expect("untracked state should load")
1185
- .expect("untracked row should be present in untracked state");
1186
- assert_eq!(
1187
- untracked_row.snapshot_content.as_deref(),
1188
- Some(r#"{"key":"untracked-programmatic","value":"untracked"}"#)
1189
- );
1190
-
1191
- let live_untracked_row = live_state
1192
- .reader(
1193
- storage
1194
- .begin_read(StorageReadOptions::default())
1195
- .expect("read should open"),
1196
- )
1197
- .load_row(&crate::live_state::LiveStateRowRequest {
1198
- schema_key: "lix_key_value".to_string(),
1199
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1200
- entity_pk: crate::entity_pk::EntityPk::single("untracked-programmatic"),
1201
- file_id: NullableKeyFilter::Null,
1202
- })
1203
- .await
1204
- .expect("live state should load")
1205
- .expect("untracked row should be visible through live state");
1206
- assert!(live_untracked_row.untracked);
1207
- assert!(live_untracked_row.global);
1208
- assert_eq!(live_untracked_row.branch_id, GLOBAL_BRANCH_ID);
1209
-
1210
- let tracked_rows = crate::tracked_state::TrackedStateContext::new()
1211
- .reader(
1212
- storage
1213
- .begin_read(StorageReadOptions::default())
1214
- .expect("read should open"),
1215
- )
1216
- .scan_rows_at_commit(&head_commit_id, &TrackedStateScanRequest::default())
1217
- .await
1218
- .expect("tracked state should scan");
1219
- assert!(
1220
- tracked_rows
1221
- .iter()
1222
- .all(|row| row.entity_pk.as_single_string_owned().as_deref()
1223
- != Ok("untracked-programmatic")),
1224
- "untracked staged rows should not be written into tracked state"
1225
- );
1226
- }
1227
-
1228
- #[tokio::test]
1229
- async fn commit_validates_staged_rows_before_persistence() {
1230
- let backend = InMemoryStorageBackend::new();
1231
- let storage = StorageContext::new(backend.clone());
1232
- let live_state = Arc::new(live_state_context());
1233
- seed_visible_schema_rows(storage.clone()).await;
1234
- let binary_cas = Arc::new(BinaryCasContext::new());
1235
- let branch_ctx = Arc::new(BranchContext::new(Arc::new(UntrackedStateContext::new())));
1236
- let catalog_context = Arc::new(CatalogContext::new());
1237
- let opened = open_transaction(
1238
- &SessionMode::Pinned {
1239
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1240
- },
1241
- storage.clone(),
1242
- Arc::clone(&live_state),
1243
- Arc::new(crate::tracked_state::TrackedStateContext::new()),
1244
- Arc::clone(&binary_cas),
1245
- Arc::clone(&branch_ctx),
1246
- Arc::clone(&catalog_context),
1247
- )
1248
- .await
1249
- .expect("transaction should open");
1250
- let mut transaction = opened.transaction;
1251
- let runtime_functions = opened.runtime_functions;
1252
-
1253
- let mut invalid_row = key_value_stage_row("invalid-programmatic", "invalid", false);
1254
- invalid_row.snapshot = Some(TransactionJson::from_value_for_test(
1255
- json!({"key": "invalid-programmatic"}),
1256
- ));
1257
- transaction
1258
- .stage_rows(vec![invalid_row])
1259
- .await
1260
- .expect("invalid row should still reach commit validation");
1261
-
1262
- let error = transaction
1263
- .commit(&runtime_functions)
1264
- .await
1265
- .expect_err("validation should reject before persistence");
1266
- assert!(
1267
- error.message.contains("snapshot_content validation failed"),
1268
- "validation error should explain the rejected schema data: {error:?}"
1269
- );
1270
-
1271
- let head = branch_ctx
1272
- .ref_reader(
1273
- storage
1274
- .begin_read(StorageReadOptions::default())
1275
- .expect("read should open"),
1276
- )
1277
- .load_head_commit_id(GLOBAL_BRANCH_ID)
1278
- .await
1279
- .expect("branch ref should load after failed commit");
1280
- assert_eq!(
1281
- head.as_deref(),
1282
- Some(SCHEMA_FIXTURE_COMMIT_ID),
1283
- "validation failure must not advance the branch ref"
1284
- );
1285
- }
1286
-
1287
- #[tokio::test]
1288
- async fn commit_rejects_non_object_metadata_without_sql() {
1289
- let backend = InMemoryStorageBackend::new();
1290
- let storage = StorageContext::new(backend.clone());
1291
- let (live_state, _binary_cas, branch_ref, runtime_functions, mut transaction) =
1292
- open_test_transaction(&backend).await;
1293
-
1294
- let mut row = key_value_stage_row("invalid-metadata", "value", false);
1295
- row.metadata = Some(TransactionJson::from_value_for_test(json!("not-an-object")));
1296
- transaction
1297
- .stage_rows(vec![row])
1298
- .await
1299
- .expect("row should stage before metadata validation");
1300
-
1301
- let error = transaction
1302
- .commit(&runtime_functions)
1303
- .await
1304
- .expect_err("non-object metadata should fail commit validation");
1305
-
1306
- assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
1307
- assert!(
1308
- error.message.contains("metadata") && error.message.contains("JSON object"),
1309
- "error should explain metadata object validation: {error:?}"
1310
- );
1311
- assert_no_persistence_after_validation_failure(
1312
- storage.clone(),
1313
- &live_state,
1314
- &branch_ref,
1315
- "invalid-metadata",
1316
- )
1317
- .await;
1318
- }
1319
-
1320
- #[tokio::test]
1321
- async fn stage_rows_rejects_unknown_schema_key_without_sql() {
1322
- let backend = InMemoryStorageBackend::new();
1323
- let (_live_state, _binary_cas, _branch_ref, _runtime_functions, mut transaction) =
1324
- open_test_transaction(&backend).await;
1325
-
1326
- let mut row = key_value_stage_row("unknown-schema", "value", false);
1327
- row.schema_key = "missing_schema".to_string();
1328
-
1329
- let error = transaction
1330
- .stage_rows(vec![row])
1331
- .await
1332
- .expect_err("unknown schema should be rejected while staging");
1333
-
1334
- assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
1335
- assert!(
1336
- error
1337
- .message
1338
- .contains("schema 'missing_schema' is not visible"),
1339
- "error should explain missing schema visibility: {error:?}"
1340
- );
1341
- }
1342
-
1343
- #[tokio::test]
1344
- async fn stage_rows_rejects_missing_branch_without_sql() {
1345
- let backend = InMemoryStorageBackend::new();
1346
- let (_live_state, _binary_cas, _branch_ref, _runtime_functions, mut transaction) =
1347
- open_test_transaction(&backend).await;
1348
-
1349
- let mut row = key_value_stage_row("ghost-branch-row", "value", false);
1350
- row.branch_id = "ghost-branch".to_string();
1351
- row.global = false;
1352
-
1353
- let error = transaction
1354
- .stage_rows(vec![row])
1355
- .await
1356
- .expect_err("missing branch should be rejected before staging");
1357
-
1358
- assert_eq!(error.code, LixError::CODE_BRANCH_NOT_FOUND);
1359
- assert!(
1360
- error
1361
- .message
1362
- .contains("branch 'ghost-branch' was not found"),
1363
- "error should explain missing branch: {error:?}"
1364
- );
1365
- }
1366
-
1367
- #[tokio::test]
1368
- async fn stage_rows_rejects_invalid_storage_scope_without_sql() {
1369
- let backend = InMemoryStorageBackend::new();
1370
- let (_live_state, _binary_cas, _branch_ref, _runtime_functions, mut transaction) =
1371
- open_test_transaction(&backend).await;
1372
-
1373
- let mut row = key_value_stage_row("invalid-storage-scope", "value", false);
1374
- row.branch_id = GLOBAL_BRANCH_ID.to_string();
1375
- row.global = false;
1376
-
1377
- let error = transaction
1378
- .stage_rows(vec![row])
1379
- .await
1380
- .expect_err("invalid storage scope should be rejected before staging");
1381
-
1382
- assert_eq!(error.code, LixError::CODE_INVALID_STORAGE_SCOPE);
1383
- assert!(
1384
- error.message.contains("branch_id='global', global=false"),
1385
- "error should explain invalid storage scope: {error:?}"
1386
- );
1387
- }
1388
-
1389
- #[tokio::test]
1390
- async fn stage_rows_rejects_invalid_snapshot_json_without_sql() {
1391
- let backend = InMemoryStorageBackend::new();
1392
- let (_live_state, _binary_cas, _branch_ref, _runtime_functions, mut transaction) =
1393
- open_test_transaction(&backend).await;
1394
-
1395
- let mut row = key_value_stage_row("invalid-json", "value", false);
1396
- row.snapshot = Some(TransactionJson::from_value_for_test(json!("not-an-object")));
1397
-
1398
- let error = transaction
1399
- .stage_rows(vec![row])
1400
- .await
1401
- .expect_err("non-object snapshot should be rejected while staging");
1402
-
1403
- assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
1404
- assert!(
1405
- error.message.contains("must be a JSON object"),
1406
- "error should explain invalid snapshot shape: {error:?}"
1407
- );
1408
- }
1409
-
1410
- #[tokio::test]
1411
- async fn commit_rejects_snapshot_that_violates_json_schema_without_sql() {
1412
- let backend = InMemoryStorageBackend::new();
1413
- let storage = StorageContext::new(backend.clone());
1414
- let (live_state, _binary_cas, branch_ref, runtime_functions, mut transaction) =
1415
- open_test_transaction(&backend).await;
1416
-
1417
- let mut row = key_value_stage_row("schema-mismatch", "value", false);
1418
- row.snapshot = Some(TransactionJson::from_value_for_test(
1419
- json!({"key": "schema-mismatch"}),
1420
- ));
1421
- transaction
1422
- .stage_rows(vec![row])
1423
- .await
1424
- .expect("row should stage before JSON Schema validation");
1425
-
1426
- let error = transaction
1427
- .commit(&runtime_functions)
1428
- .await
1429
- .expect_err("JSON Schema mismatch should fail commit validation");
1430
-
1431
- assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
1432
- assert!(
1433
- error.message.contains("snapshot_content validation failed"),
1434
- "error should explain JSON Schema validation: {error:?}"
1435
- );
1436
- assert_no_persistence_after_validation_failure(
1437
- storage.clone(),
1438
- &live_state,
1439
- &branch_ref,
1440
- "schema-mismatch",
1441
- )
1442
- .await;
1443
- }
1444
-
1445
- #[tokio::test]
1446
- async fn stage_rows_rejects_malformed_registered_schema_without_sql() {
1447
- let backend = InMemoryStorageBackend::new();
1448
- let (_live_state, _binary_cas, _branch_ref, _runtime_functions, mut transaction) =
1449
- open_test_transaction(&backend).await;
1450
-
1451
- let mut row = key_value_stage_row("malformed-registered-schema", "value", false);
1452
- row.schema_key = "lix_registered_schema".to_string();
1453
- row.snapshot = Some(TransactionJson::from_value_for_test(json!({
1454
- "value": {
1455
- "x-lix-key": "malformed_registered_schema",
1456
- "x-lix-primary-key": ["id"],
1457
- "type": "object",
1458
- "properties": {
1459
- "id": { "type": "string" }
1460
- },
1461
- "required": ["id"],
1462
- "additionalProperties": false
1463
- }
1464
- })));
1465
- row.entity_pk = None;
1466
-
1467
- let error = transaction
1468
- .stage_rows(vec![row])
1469
- .await
1470
- .expect_err("malformed registered schema should be rejected while staging");
1471
-
1472
- assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
1473
- assert!(
1474
- error.message.contains("x-lix-primary-key"),
1475
- "error should explain malformed registered schema: {error:?}"
1476
- );
1477
- }
1478
-
1479
- #[tokio::test]
1480
- async fn stage_rows_rejects_primary_key_entity_pk_mismatch_without_sql() {
1481
- let backend = InMemoryStorageBackend::new();
1482
- let (_live_state, _binary_cas, _branch_ref, _runtime_functions, mut transaction) =
1483
- open_test_transaction(&backend).await;
1484
-
1485
- let mut row = key_value_stage_row("right-id", "value", false);
1486
- row.entity_pk = Some(crate::entity_pk::EntityPk::single("wrong-id"));
1487
-
1488
- let error = transaction
1489
- .stage_rows(vec![row])
1490
- .await
1491
- .expect_err("entity pk mismatch should be rejected while staging");
1492
-
1493
- assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
1494
- assert!(
1495
- error
1496
- .message
1497
- .contains("does not match x-lix-primary-key derived entity_pk"),
1498
- "error should explain entity pk mismatch: {error:?}"
1499
- );
1500
- }
1501
-
1502
- async fn open_test_transaction(
1503
- backend: &InMemoryStorageBackend,
1504
- ) -> (
1505
- Arc<LiveStateContext>,
1506
- Arc<BinaryCasContext>,
1507
- Arc<BranchContext>,
1508
- FunctionContext,
1509
- Transaction,
1510
- ) {
1511
- let storage = StorageContext::new(backend.clone());
1512
- let live_state = Arc::new(live_state_context());
1513
- seed_visible_schema_rows(storage.clone()).await;
1514
- let binary_cas = Arc::new(BinaryCasContext::new());
1515
- let branch_ctx = Arc::new(BranchContext::new(Arc::new(UntrackedStateContext::new())));
1516
- let catalog_context = Arc::new(CatalogContext::new());
1517
- let opened = open_transaction(
1518
- &SessionMode::Pinned {
1519
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1520
- },
1521
- storage,
1522
- Arc::clone(&live_state),
1523
- Arc::new(crate::tracked_state::TrackedStateContext::new()),
1524
- Arc::clone(&binary_cas),
1525
- Arc::clone(&branch_ctx),
1526
- catalog_context,
1527
- )
1528
- .await
1529
- .expect("transaction should open");
1530
- let transaction = opened.transaction;
1531
- let runtime_functions = opened.runtime_functions;
1532
-
1533
- (
1534
- live_state,
1535
- binary_cas,
1536
- branch_ctx,
1537
- runtime_functions,
1538
- transaction,
1539
- )
1540
- }
1541
-
1542
- async fn seed_visible_schema_rows(storage: StorageContext) {
1543
- let mut writes = StorageWriteSet::new();
1544
- let rows = crate::schema::seed_schema_definitions()
1545
- .into_iter()
1546
- .map(|schema| {
1547
- let key = crate::schema::schema_key_from_definition(schema)
1548
- .expect("seed schema key should derive");
1549
- let snapshot_content = json!({ "value": schema }).to_string();
1550
- crate::tracked_state::MaterializedTrackedStateRow {
1551
- entity_pk: crate::schema::registered_schema_entity_pk(&key.schema_key)
1552
- .expect("registered schema identity should derive"),
1553
- schema_key: "lix_registered_schema".to_string(),
1554
- file_id: None,
1555
- snapshot_content: Some(snapshot_content),
1556
- metadata: None,
1557
- deleted: false,
1558
- created_at: "1970-01-01T00:00:00.000Z".to_string(),
1559
- updated_at: "1970-01-01T00:00:00.000Z".to_string(),
1560
- change_id: format!("schema-fixture-{}", key.schema_key),
1561
- commit_id: SCHEMA_FIXTURE_COMMIT_ID.to_string(),
1562
- }
1563
- })
1564
- .collect::<Vec<_>>();
1565
- let branch_ref_row = prepare_branch_ref_row(
1566
- GLOBAL_BRANCH_ID,
1567
- SCHEMA_FIXTURE_COMMIT_ID,
1568
- "1970-01-01T00:00:00.000Z",
1569
- )
1570
- .expect("schema fixture branch ref should stage");
1571
- let mut read = storage
1572
- .begin_read(crate::storage::StorageReadOptions::default())
1573
- .expect("schema fixture read should open");
1574
- crate::test_support::stage_tracked_root_from_materialized(
1575
- &mut read,
1576
- &mut writes,
1577
- &crate::tracked_state::TrackedStateContext::new(),
1578
- SCHEMA_FIXTURE_COMMIT_ID,
1579
- None,
1580
- &rows,
1581
- )
1582
- .await
1583
- .expect("schema fixture rows should stage");
1584
- crate::untracked_state::UntrackedStateContext::new()
1585
- .writer(&mut writes)
1586
- .stage_rows([branch_ref_row.row.as_ref()])
1587
- .expect("schema fixture branch ref should stage");
1588
- storage
1589
- .commit_write_set(writes, crate::storage::StorageWriteOptions::default())
1590
- .expect("schema fixture transaction should commit");
1591
- }
1592
-
1593
- async fn assert_no_persistence_after_validation_failure(
1594
- storage: StorageContext,
1595
- live_state: &LiveStateContext,
1596
- branch_ctx: &BranchContext,
1597
- rejected_entity_pk: &str,
1598
- ) {
1599
- let head = branch_ctx
1600
- .ref_reader(
1601
- storage
1602
- .begin_read(StorageReadOptions::default())
1603
- .expect("read should open"),
1604
- )
1605
- .load_head_commit_id(GLOBAL_BRANCH_ID)
1606
- .await
1607
- .expect("branch ref should load after failed commit");
1608
- assert_eq!(
1609
- head.as_deref(),
1610
- Some(SCHEMA_FIXTURE_COMMIT_ID),
1611
- "validation failure must not advance the branch ref"
1612
- );
1613
- let row = live_state
1614
- .reader(
1615
- storage
1616
- .begin_read(StorageReadOptions::default())
1617
- .expect("read should open"),
1618
- )
1619
- .load_row(&crate::live_state::LiveStateRowRequest {
1620
- schema_key: "lix_key_value".to_string(),
1621
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1622
- entity_pk: crate::entity_pk::EntityPk::single(rejected_entity_pk),
1623
- file_id: NullableKeyFilter::Null,
1624
- })
1625
- .await
1626
- .expect("live state should load after failed commit");
1627
- assert_eq!(
1628
- row, None,
1629
- "validation failure must happen before live-state persistence"
1630
- );
1631
- }
1632
-
1633
- fn key_value_stage_row(key: &str, value: &str, untracked: bool) -> TransactionWriteRow {
1634
- TransactionWriteRow {
1635
- entity_pk: Some(crate::entity_pk::EntityPk::single(key)),
1636
- schema_key: "lix_key_value".to_string(),
1637
- file_id: None,
1638
- snapshot: Some(TransactionJson::from_value_for_test(json!({
1639
- "key": key,
1640
- "value": value,
1641
- }))),
1642
- metadata: None,
1643
- origin: None,
1644
- created_at: None,
1645
- updated_at: None,
1646
- global: true,
1647
- change_id: None,
1648
- commit_id: None,
1649
- untracked,
1650
- branch_id: GLOBAL_BRANCH_ID.to_string(),
1651
- }
1652
- }
1653
- }