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

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