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

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 (169) hide show
  1. package/SKILL.md +46 -8
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +25 -1
  3. package/dist/engine-wasm/wasm/lix_engine.js +60 -2
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +5 -0
  6. package/dist/generated/builtin-schemas.d.ts +87 -162
  7. package/dist/generated/builtin-schemas.js +139 -236
  8. package/dist/open-lix.d.ts +10 -3
  9. package/dist/open-lix.js +39 -0
  10. package/dist-engine-src/src/binary_cas/types.rs +0 -6
  11. package/dist-engine-src/src/catalog/context.rs +412 -0
  12. package/dist-engine-src/src/catalog/mod.rs +10 -0
  13. package/dist-engine-src/src/catalog/schema.rs +4 -0
  14. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  15. package/dist-engine-src/src/cel/mod.rs +1 -1
  16. package/dist-engine-src/src/cel/provider.rs +1 -1
  17. package/dist-engine-src/src/commit_graph/context.rs +328 -1015
  18. package/dist-engine-src/src/commit_graph/mod.rs +2 -3
  19. package/dist-engine-src/src/commit_graph/types.rs +7 -43
  20. package/dist-engine-src/src/commit_graph/walker.rs +57 -81
  21. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  22. package/dist-engine-src/src/commit_store/context.rs +944 -0
  23. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  24. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  25. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  26. package/dist-engine-src/src/commit_store/types.rs +215 -0
  27. package/dist-engine-src/src/common/identity.rs +15 -5
  28. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  29. package/dist-engine-src/src/common/metadata.rs +17 -12
  30. package/dist-engine-src/src/common/mod.rs +5 -5
  31. package/dist-engine-src/src/domain.rs +324 -0
  32. package/dist-engine-src/src/engine.rs +29 -43
  33. package/dist-engine-src/src/entity_identity.rs +238 -118
  34. package/dist-engine-src/src/functions/context.rs +17 -52
  35. package/dist-engine-src/src/functions/deterministic.rs +1 -1
  36. package/dist-engine-src/src/functions/mod.rs +1 -1
  37. package/dist-engine-src/src/functions/provider.rs +4 -4
  38. package/dist-engine-src/src/functions/state.rs +39 -66
  39. package/dist-engine-src/src/functions/types.rs +1 -1
  40. package/dist-engine-src/src/init.rs +204 -151
  41. package/dist-engine-src/src/json_store/context.rs +354 -60
  42. package/dist-engine-src/src/json_store/encoded.rs +6 -6
  43. package/dist-engine-src/src/json_store/mod.rs +4 -1
  44. package/dist-engine-src/src/json_store/store.rs +884 -11
  45. package/dist-engine-src/src/json_store/types.rs +166 -1
  46. package/dist-engine-src/src/lib.rs +11 -10
  47. package/dist-engine-src/src/live_state/context.rs +608 -830
  48. package/dist-engine-src/src/live_state/mod.rs +3 -3
  49. package/dist-engine-src/src/live_state/overlay.rs +7 -7
  50. package/dist-engine-src/src/live_state/reader.rs +5 -5
  51. package/dist-engine-src/src/live_state/types.rs +19 -36
  52. package/dist-engine-src/src/live_state/visibility.rs +19 -14
  53. package/dist-engine-src/src/plugin/archive.rs +3 -6
  54. package/dist-engine-src/src/plugin/install.rs +0 -18
  55. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
  56. package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
  57. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
  58. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
  59. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
  60. package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
  61. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
  62. package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
  63. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
  64. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
  65. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
  66. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
  67. package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
  68. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  69. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
  70. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
  71. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
  72. package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
  73. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  74. package/dist-engine-src/src/schema/definition.json +47 -17
  75. package/dist-engine-src/src/schema/definition.rs +202 -96
  76. package/dist-engine-src/src/schema/key.rs +9 -77
  77. package/dist-engine-src/src/schema/mod.rs +4 -4
  78. package/dist-engine-src/src/schema/tests.rs +133 -92
  79. package/dist-engine-src/src/session/context.rs +86 -48
  80. package/dist-engine-src/src/session/create_version.rs +22 -14
  81. package/dist-engine-src/src/session/execute.rs +117 -23
  82. package/dist-engine-src/src/session/merge/apply.rs +4 -4
  83. package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
  84. package/dist-engine-src/src/session/merge/stats.rs +1 -1
  85. package/dist-engine-src/src/session/merge/version.rs +35 -45
  86. package/dist-engine-src/src/session/mod.rs +9 -7
  87. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  88. package/dist-engine-src/src/session/switch_version.rs +17 -28
  89. package/dist-engine-src/src/session/transaction.rs +76 -0
  90. package/dist-engine-src/src/sql2/change_provider.rs +14 -20
  91. package/dist-engine-src/src/sql2/classify.rs +75 -48
  92. package/dist-engine-src/src/sql2/context.rs +22 -18
  93. package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
  94. package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
  95. package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
  96. package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
  97. package/dist-engine-src/src/sql2/error.rs +24 -5
  98. package/dist-engine-src/src/sql2/execute.rs +426 -272
  99. package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
  100. package/dist-engine-src/src/sql2/file_provider.rs +533 -108
  101. package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
  102. package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
  103. package/dist-engine-src/src/sql2/history_projection.rs +3 -27
  104. package/dist-engine-src/src/sql2/history_provider.rs +11 -17
  105. package/dist-engine-src/src/sql2/history_route.rs +22 -8
  106. package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
  107. package/dist-engine-src/src/sql2/mod.rs +8 -4
  108. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  109. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  110. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  111. package/dist-engine-src/src/sql2/public_bind/dml.rs +172 -0
  112. package/dist-engine-src/src/sql2/public_bind/mod.rs +26 -0
  113. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  114. package/dist-engine-src/src/sql2/read_only.rs +10 -12
  115. package/dist-engine-src/src/sql2/session.rs +7 -10
  116. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  117. package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
  118. package/dist-engine-src/src/sql2/udfs/public_call.rs +238 -0
  119. package/dist-engine-src/src/sql2/version_provider.rs +46 -31
  120. package/dist-engine-src/src/sql2/version_scope.rs +4 -4
  121. package/dist-engine-src/src/storage_bench.rs +1782 -325
  122. package/dist-engine-src/src/test_support.rs +183 -36
  123. package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
  124. package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
  125. package/dist-engine-src/src/tracked_state/context.rs +1155 -271
  126. package/dist-engine-src/src/tracked_state/diff.rs +249 -57
  127. package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
  128. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  129. package/dist-engine-src/src/tracked_state/merge.rs +37 -19
  130. package/dist-engine-src/src/tracked_state/mod.rs +8 -7
  131. package/dist-engine-src/src/tracked_state/storage.rs +138 -6
  132. package/dist-engine-src/src/tracked_state/tree.rs +695 -252
  133. package/dist-engine-src/src/tracked_state/types.rs +176 -6
  134. package/dist-engine-src/src/transaction/commit.rs +695 -435
  135. package/dist-engine-src/src/transaction/context.rs +551 -310
  136. package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
  137. package/dist-engine-src/src/transaction/mod.rs +2 -0
  138. package/dist-engine-src/src/transaction/normalization.rs +311 -447
  139. package/dist-engine-src/src/transaction/prep.rs +37 -0
  140. package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
  141. package/dist-engine-src/src/transaction/staging.rs +701 -406
  142. package/dist-engine-src/src/transaction/types.rs +231 -122
  143. package/dist-engine-src/src/transaction/validation.rs +2717 -1698
  144. package/dist-engine-src/src/untracked_state/codec.rs +40 -96
  145. package/dist-engine-src/src/untracked_state/context.rs +21 -5
  146. package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
  147. package/dist-engine-src/src/untracked_state/mod.rs +3 -5
  148. package/dist-engine-src/src/untracked_state/storage.rs +105 -57
  149. package/dist-engine-src/src/untracked_state/types.rs +63 -13
  150. package/dist-engine-src/src/version/context.rs +1 -13
  151. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  152. package/dist-engine-src/src/version/mod.rs +3 -2
  153. package/dist-engine-src/src/version/refs.rs +12 -103
  154. package/dist-engine-src/src/version/stage_rows.rs +15 -19
  155. package/package.json +1 -1
  156. package/dist-engine-src/src/changelog/codec.rs +0 -321
  157. package/dist-engine-src/src/changelog/context.rs +0 -92
  158. package/dist-engine-src/src/changelog/materialization.rs +0 -121
  159. package/dist-engine-src/src/changelog/mod.rs +0 -13
  160. package/dist-engine-src/src/changelog/reader.rs +0 -20
  161. package/dist-engine-src/src/changelog/storage.rs +0 -220
  162. package/dist-engine-src/src/changelog/types.rs +0 -38
  163. package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
  164. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
  165. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
  166. package/dist-engine-src/src/schema_registry.rs +0 -294
  167. package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
  168. package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
  169. package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
@@ -1,32 +1,38 @@
1
- use std::collections::BTreeSet;
1
+ use std::collections::{BTreeMap, BTreeSet};
2
2
  use std::sync::Arc;
3
3
 
4
4
  use async_trait::async_trait;
5
5
  use serde_json::Value as JsonValue;
6
6
 
7
7
  use crate::binary_cas::{BinaryCasContext, BlobBytesBatch, BlobHash};
8
- use crate::changelog::ChangelogContext;
8
+ use crate::catalog::CatalogContext;
9
9
  use crate::commit_graph::{CommitGraphContext, CommitGraphStoreReader};
10
+ use crate::commit_store::CommitStoreContext;
11
+ use crate::domain::Domain;
10
12
  use crate::entity_identity::EntityIdentity;
11
13
  use crate::functions::{FunctionContext, FunctionProviderHandle};
12
- use crate::json_store::JsonStoreContext;
13
14
  use crate::live_state::{
14
- LiveStateContext, LiveStateRow, LiveStateRowRequest, LiveStateScanRequest,
15
+ LiveStateContext, LiveStateRowRequest, LiveStateScanRequest, MaterializedLiveStateRow,
15
16
  };
16
- use crate::schema_registry::SchemaRegistry;
17
17
  use crate::session::{SessionMode, WORKSPACE_VERSION_KEY};
18
18
  use crate::sql2::SqlWriteExecutionContext;
19
19
  use crate::storage::{StorageContext, StorageWriteSet, StorageWriteTransaction};
20
20
  use crate::tracked_state::{TrackedStateContext, TrackedStateStoreReader};
21
21
  use crate::transaction::commit;
22
22
  use crate::transaction::live_state_overlay::overlay_scan_rows;
23
- use crate::transaction::normalization::normalize_stage_row;
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;
24
28
  use crate::transaction::schema_resolver::TransactionSchemaResolver;
25
- use crate::transaction::staging::{StagedWriteSet, TransactionStagedWrites};
29
+ use crate::transaction::staging::{PreparedWriteSet, TransactionWriteBuffer};
26
30
  use crate::transaction::types::{
27
- StageFileData, StageRow, StageWrite, StageWriteMode, StageWriteOutcome,
31
+ stage_json_from_value, PreparedAdoptedStateRow, PreparedRowFacts, PreparedStateRow,
32
+ PreparedTransactionWrite, TransactionAdoptedChange, TransactionFileData, TransactionJson,
33
+ TransactionWrite, TransactionWriteMode, TransactionWriteOutcome, TransactionWriteRow,
28
34
  };
29
- use crate::transaction::validation::{validate_staged_writes, TransactionValidationInput};
35
+ use crate::transaction::validation::{validate_prepared_writes, TransactionValidationInput};
30
36
  use crate::version::{VersionContext, VersionRefReader};
31
37
  use crate::GLOBAL_VERSION_ID;
32
38
  use crate::{LixError, NullableKeyFilter};
@@ -34,14 +40,14 @@ use crate::{LixError, NullableKeyFilter};
34
40
  #[derive(Debug, Clone, PartialEq, Eq, Default)]
35
41
  pub(crate) struct TransactionCommitOutcome;
36
42
 
37
- /// One execution-scoped transaction capability for engine2 write paths.
43
+ /// One execution-scoped transaction capability for engine write paths.
38
44
  ///
39
45
  /// This is intentionally not a session-wide kitchen sink. It owns the backend
40
46
  /// write transaction for one `SessionContext::execute(...)` call and projects
41
- /// staged SQL writes back into the SQL DAG through an engine2-local live-state
47
+ /// accepted SQL/provider writes back into the SQL DAG through an engine-local live-state
42
48
  /// overlay.
43
49
  ///
44
- /// Transaction invariant: this is the capability for engine2 operations
50
+ /// Transaction invariant: this is the capability for engine operations
45
51
  /// that may write. Write-relevant reads must be exposed from this transaction,
46
52
  /// after the backend write transaction has begun, rather than from session-level
47
53
  /// helpers.
@@ -50,10 +56,10 @@ pub(crate) struct Transaction {
50
56
  live_state: Arc<LiveStateContext>,
51
57
  tracked_state: Arc<TrackedStateContext>,
52
58
  binary_cas: Arc<BinaryCasContext>,
53
- changelog: Arc<ChangelogContext>,
59
+ commit_store: Arc<CommitStoreContext>,
54
60
  version_ctx: Arc<VersionContext>,
55
61
  schema_resolver: TransactionSchemaResolver,
56
- staged_writes: Arc<TransactionStagedWrites>,
62
+ staged_writes: Arc<TransactionWriteBuffer>,
57
63
  storage_transaction: Box<dyn StorageWriteTransaction + Send + Sync + 'static>,
58
64
  visible_schemas: Vec<JsonValue>,
59
65
  functions: FunctionProviderHandle,
@@ -68,9 +74,9 @@ impl Transaction {
68
74
  live_state: Arc<LiveStateContext>,
69
75
  tracked_state: Arc<TrackedStateContext>,
70
76
  binary_cas: Arc<BinaryCasContext>,
71
- changelog: Arc<ChangelogContext>,
77
+ commit_store: Arc<CommitStoreContext>,
72
78
  version_ctx: Arc<VersionContext>,
73
- schema_registry: Arc<SchemaRegistry>,
79
+ catalog_context: Arc<CatalogContext>,
74
80
  ) -> Result<OpenTransaction, LixError> {
75
81
  let mut storage_transaction = storage.begin_write_transaction().await?;
76
82
  let setup_result = async {
@@ -88,8 +94,17 @@ impl Transaction {
88
94
  let functions = runtime_functions.provider();
89
95
  let visible_schemas = {
90
96
  let visible_live_state = live_state.reader(storage_transaction.as_mut());
91
- schema_registry
92
- .visible_schemas(&visible_live_state, &active_version_id)
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
+ )
93
108
  .await?
94
109
  };
95
110
  Ok::<_, LixError>((
@@ -97,28 +112,31 @@ impl Transaction {
97
112
  runtime_functions,
98
113
  functions,
99
114
  visible_schemas,
115
+ schema_facts,
100
116
  ))
101
117
  }
102
118
  .await;
103
- let (active_version_id, runtime_functions, functions, visible_schemas) = match setup_result
104
- {
105
- Ok(result) => result,
106
- Err(error) => {
107
- let _ = storage_transaction.rollback().await;
108
- return Err(error);
109
- }
110
- };
111
- let mut schema_resolver = TransactionSchemaResolver::new(schema_registry);
112
- schema_resolver
113
- .remember_visible_schemas(active_version_id.clone(), visible_schemas.clone())?;
114
- let staged_writes = Arc::new(TransactionStagedWrites::new(functions.clone()));
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()));
115
133
  Ok(OpenTransaction {
116
134
  transaction: Self {
117
135
  active_version_id,
118
136
  live_state,
119
137
  tracked_state,
120
138
  binary_cas,
121
- changelog,
139
+ commit_store,
122
140
  version_ctx,
123
141
  schema_resolver,
124
142
  staged_writes,
@@ -130,34 +148,36 @@ impl Transaction {
130
148
  })
131
149
  }
132
150
 
133
- /// Commits staged writes, runtime function state, and the backend transaction.
151
+ /// Commits prepared writes, runtime function state, and the backend transaction.
134
152
  ///
135
- /// Commit owns the execution boundary: provider-staged rows become
136
- /// changelog facts, `lix_commit` rows, version-ref updates, and visible
137
- /// live_state rows before the backend transaction is committed.
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.
138
156
  pub(crate) async fn commit(
139
157
  mut self,
140
158
  runtime_functions: &FunctionContext,
141
159
  ) -> Result<TransactionCommitOutcome, LixError> {
142
- let staged_writes = match self.staged_writes.drain() {
143
- Ok(staged_writes) => staged_writes,
160
+ let prepared_writes = match self.staged_writes.drain() {
161
+ Ok(prepared_writes) => prepared_writes,
144
162
  Err(error) => {
145
163
  let _ = self.storage_transaction.rollback().await;
146
164
  return Err(error);
147
165
  }
148
166
  };
149
- if let Err(error) = self.validate_staged_writes_by_version(&staged_writes).await {
167
+ if let Err(error) = self
168
+ .validate_prepared_writes_by_version(&prepared_writes)
169
+ .await
170
+ {
150
171
  let _ = self.storage_transaction.rollback().await;
151
172
  return Err(error);
152
173
  }
153
- if let Err(error) = commit::commit_staged_writes(
174
+ if let Err(error) = commit::commit_prepared_writes(
154
175
  &self.binary_cas,
155
- &self.changelog,
156
- &self.live_state,
176
+ &self.commit_store,
157
177
  self.version_ctx.as_ref(),
158
178
  Some(runtime_functions),
159
179
  self.storage_transaction.as_mut(),
160
- staged_writes,
180
+ prepared_writes,
161
181
  )
162
182
  .await
163
183
  {
@@ -181,75 +201,226 @@ impl Transaction {
181
201
  /// Stages one decoded write batch into this transaction.
182
202
  ///
183
203
  /// This is the programmatic write entrypoint used by non-SQL APIs. The
184
- /// transaction still owns hydration from `StageRow` into `StagedStateRow`,
185
- /// so generated timestamps, change ids, commit ids, and commit membership
186
- /// stay in one place.
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.
187
207
  #[allow(dead_code)]
188
208
  pub(crate) async fn stage_write(
189
209
  &mut self,
190
- write: StageWrite,
191
- ) -> Result<StageWriteOutcome, LixError> {
192
- require_valid_stage_write_storage_scopes(&write)?;
193
- self.require_existing_stage_write_version_ids(&write)
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)
194
223
  .await?;
195
- let write = self.normalize_stage_write(write).await?;
224
+ let write = self.prepare_transaction_write(write).await?;
196
225
  self.staged_writes.stage_write(write)
197
226
  }
198
227
 
199
- async fn normalize_stage_write(&mut self, write: StageWrite) -> Result<StageWrite, LixError> {
228
+ async fn prepare_transaction_write(
229
+ &mut self,
230
+ write: TransactionWrite,
231
+ ) -> Result<PreparedTransactionWrite, LixError> {
200
232
  Ok(match write {
201
- StageWrite::Rows { mode, rows } => StageWrite::Rows {
233
+ TransactionWrite::Rows { mode, rows } => PreparedTransactionWrite::Rows {
202
234
  mode,
203
- rows: self.normalize_stage_rows(rows).await?,
235
+ rows: self.prepare_transaction_rows(rows).await?,
204
236
  },
205
- StageWrite::RowsWithFileData {
237
+ TransactionWrite::RowsWithFileData {
206
238
  mode,
207
239
  rows,
208
240
  file_data,
209
241
  count,
210
- } => StageWrite::RowsWithFileData {
242
+ } => PreparedTransactionWrite::RowsWithFileData {
211
243
  mode,
212
- rows: self.normalize_stage_rows(rows).await?,
244
+ rows: self.prepare_transaction_rows(rows).await?,
213
245
  file_data,
214
246
  count,
215
247
  },
216
- StageWrite::AdoptedChanges { changes } => StageWrite::AdoptedChanges { changes },
248
+ TransactionWrite::AdoptedChanges { changes } => {
249
+ PreparedTransactionWrite::AdoptedChanges {
250
+ rows: self.prepare_adopted_changes(changes).await?,
251
+ }
252
+ }
217
253
  })
218
254
  }
219
255
 
220
- async fn normalize_stage_rows(
256
+ async fn prepare_transaction_rows(
221
257
  &mut self,
222
- rows: Vec<StageRow>,
223
- ) -> Result<Vec<StageRow>, LixError> {
224
- let mut normalized_rows = Vec::with_capacity(rows.len());
225
- for row in rows {
226
- let version_id = row.schema_scope_version_id().to_string();
227
- let staged = self.staged_writes.staging_overlay()?;
228
- let live_state = self.live_state.reader(self.storage_transaction.as_mut());
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();
229
278
  let catalog = self
230
279
  .schema_resolver
231
- .catalog_for_row_normalization(&live_state, staged, &version_id)
280
+ .catalog_for_row_normalization(&live_state, &staged, &domain)
232
281
  .await?;
233
- let row = normalize_stage_row(row, catalog, self.functions.clone())?;
234
- normalized_rows.push(row);
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
+ }
235
312
  }
236
- Ok(normalized_rows)
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())
237
319
  }
238
320
 
239
- async fn validate_staged_writes_by_version(
321
+ async fn prepare_adopted_changes(
240
322
  &mut self,
241
- staged_writes: &StagedWriteSet,
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,
242
411
  ) -> Result<(), LixError> {
243
- for version_id in staged_write_validation_version_ids(staged_writes) {
244
- let version_staged_writes =
245
- staged_write_set_for_schema_scope(staged_writes, &version_id);
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);
246
417
  let live_state = self.live_state.reader(self.storage_transaction.as_mut());
247
418
  let schema_catalog = self
248
419
  .schema_resolver
249
- .catalog_for_validation(&live_state, staged_writes, &version_id)
420
+ .catalog_for_validation(&live_state, scope)
250
421
  .await?;
251
- validate_staged_writes(TransactionValidationInput::new(
252
- &version_staged_writes,
422
+ validate_prepared_writes(TransactionValidationInput::new(
423
+ &version_prepared_writes,
253
424
  &schema_catalog,
254
425
  &live_state,
255
426
  ))
@@ -262,20 +433,20 @@ impl Transaction {
262
433
  #[allow(dead_code)]
263
434
  pub(crate) async fn stage_rows(
264
435
  &mut self,
265
- rows: Vec<StageRow>,
266
- ) -> Result<StageWriteOutcome, LixError> {
267
- self.stage_write(StageWrite::Rows {
268
- mode: StageWriteMode::Replace,
436
+ rows: Vec<TransactionWriteRow>,
437
+ ) -> Result<TransactionWriteOutcome, LixError> {
438
+ self.stage_write(TransactionWrite::Rows {
439
+ mode: TransactionWriteMode::Replace,
269
440
  rows,
270
441
  })
271
442
  .await
272
443
  }
273
444
 
274
- async fn require_existing_stage_write_version_ids(
445
+ async fn require_existing_transaction_write_version_ids(
275
446
  &mut self,
276
- write: &StageWrite,
447
+ write: &TransactionWrite,
277
448
  ) -> Result<(), LixError> {
278
- let version_ids = stage_write_version_ids(write);
449
+ let version_ids = transaction_write_version_ids(write);
279
450
  let reader = self
280
451
  .version_ctx
281
452
  .ref_reader(self.storage_transaction.as_mut());
@@ -329,18 +500,9 @@ impl Transaction {
329
500
  ) -> Result<(), LixError> {
330
501
  let timestamp = self.functions.call_timestamp();
331
502
  let mut writes = StorageWriteSet::new();
332
- let canonical_row = {
333
- let mut json_writer = JsonStoreContext::new().writer();
334
- self.version_ctx.canonical_ref_row(
335
- &mut writes,
336
- &mut json_writer,
337
- version_id,
338
- commit_id,
339
- &timestamp,
340
- )?
341
- };
503
+ let canonical_row = prepare_version_ref_row(version_id, commit_id, &timestamp)?;
342
504
  self.version_ctx
343
- .stage_canonical_ref_rows(&mut writes, &[canonical_row])?;
505
+ .stage_canonical_ref_rows(&mut writes, &[canonical_row.row])?;
344
506
  writes
345
507
  .apply(&mut self.storage_transaction.as_mut())
346
508
  .await
@@ -375,11 +537,129 @@ impl Transaction {
375
537
  pub(crate) fn commit_graph_reader(
376
538
  &mut self,
377
539
  ) -> CommitGraphStoreReader<&mut dyn StorageWriteTransaction> {
378
- CommitGraphContext::new(self.changelog.as_ref().clone())
379
- .reader(self.storage_transaction.as_mut())
540
+ CommitGraphContext::new().reader(self.storage_transaction.as_mut())
380
541
  }
381
542
  }
382
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
+
383
663
  pub(crate) struct OpenTransaction {
384
664
  pub(crate) transaction: Transaction,
385
665
  pub(crate) runtime_functions: FunctionContext,
@@ -391,9 +671,9 @@ pub(crate) async fn open_transaction(
391
671
  live_state: Arc<LiveStateContext>,
392
672
  tracked_state: Arc<TrackedStateContext>,
393
673
  binary_cas: Arc<BinaryCasContext>,
394
- changelog: Arc<ChangelogContext>,
674
+ commit_store: Arc<CommitStoreContext>,
395
675
  version_ctx: Arc<VersionContext>,
396
- schema_registry: Arc<SchemaRegistry>,
676
+ catalog_context: Arc<CatalogContext>,
397
677
  ) -> Result<OpenTransaction, LixError> {
398
678
  Transaction::open(
399
679
  mode,
@@ -401,9 +681,9 @@ pub(crate) async fn open_transaction(
401
681
  live_state,
402
682
  tracked_state,
403
683
  binary_cas,
404
- changelog,
684
+ commit_store,
405
685
  version_ctx,
406
- schema_registry,
686
+ catalog_context,
407
687
  )
408
688
  .await
409
689
  }
@@ -432,7 +712,7 @@ impl SqlWriteExecutionContext for Transaction {
432
712
  async fn scan_live_state(
433
713
  &mut self,
434
714
  request: &LiveStateScanRequest,
435
- ) -> Result<Vec<LiveStateRow>, LixError> {
715
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
436
716
  let staged = self.staged_writes.staging_overlay()?;
437
717
  let base = self.live_state.reader(self.storage_transaction.as_mut());
438
718
  overlay_scan_rows(&base, &staged, request).await
@@ -445,115 +725,67 @@ impl SqlWriteExecutionContext for Transaction {
445
725
  .await
446
726
  }
447
727
 
448
- async fn stage_write(&mut self, write: StageWrite) -> Result<StageWriteOutcome, LixError> {
728
+ async fn stage_write(
729
+ &mut self,
730
+ write: TransactionWrite,
731
+ ) -> Result<TransactionWriteOutcome, LixError> {
449
732
  Transaction::stage_write(self, write).await
450
733
  }
451
734
  }
452
735
 
453
- fn stage_write_version_ids(write: &StageWrite) -> BTreeSet<String> {
736
+ fn transaction_write_version_ids(write: &TransactionWrite) -> BTreeSet<String> {
454
737
  match write {
455
- StageWrite::Rows { rows, .. } => stage_row_version_ids(rows),
456
- StageWrite::RowsWithFileData {
738
+ TransactionWrite::Rows { rows, .. } => transaction_write_row_version_ids(rows),
739
+ TransactionWrite::RowsWithFileData {
457
740
  rows, file_data, ..
458
- } => stage_row_version_ids(rows)
741
+ } => transaction_write_row_version_ids(rows)
459
742
  .into_iter()
460
743
  .chain(stage_file_data_version_ids(file_data))
461
744
  .collect(),
462
- StageWrite::AdoptedChanges { changes } => changes
745
+ TransactionWrite::AdoptedChanges { changes } => changes
463
746
  .iter()
464
747
  .map(|change| change.version_id.clone())
465
748
  .collect(),
466
749
  }
467
750
  }
468
751
 
469
- fn staged_write_validation_version_ids(staged_writes: &StagedWriteSet) -> BTreeSet<String> {
470
- staged_writes
471
- .state_rows
472
- .iter()
473
- .map(|row| row.schema_scope_version_id().to_string())
474
- .chain(
475
- staged_writes
476
- .adopted_rows
477
- .iter()
478
- .map(|row| row.schema_scope_version_id().to_string()),
479
- )
480
- .collect()
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
+ }
481
759
  }
482
760
 
483
- fn staged_write_set_for_schema_scope(
484
- staged_writes: &StagedWriteSet,
485
- schema_scope_version_id: &str,
486
- ) -> StagedWriteSet {
487
- StagedWriteSet {
488
- state_rows: staged_writes
489
- .state_rows
490
- .iter()
491
- .filter(|row| row.schema_scope_version_id() == schema_scope_version_id)
492
- .cloned()
493
- .collect(),
494
- adopted_rows: staged_writes
495
- .adopted_rows
496
- .iter()
497
- .filter(|row| row.schema_scope_version_id() == schema_scope_version_id)
498
- .cloned()
499
- .collect(),
500
- insert_identities: staged_writes
501
- .insert_identities
502
- .iter()
503
- .filter(|(identity, _)| {
504
- let identity_schema_scope = if identity.version_id == GLOBAL_VERSION_ID {
505
- GLOBAL_VERSION_ID
506
- } else {
507
- identity.version_id.as_str()
508
- };
509
- identity_schema_scope == schema_scope_version_id
510
- })
511
- .map(|(identity, origin)| (identity.clone(), origin.clone()))
512
- .collect(),
513
- commit_members_by_version: staged_writes
514
- .commit_members_by_version
515
- .iter()
516
- .filter(|(member_version_id, _)| {
517
- let member_schema_scope = if member_version_id.as_str() == GLOBAL_VERSION_ID {
518
- GLOBAL_VERSION_ID
519
- } else {
520
- member_version_id.as_str()
521
- };
522
- member_schema_scope == schema_scope_version_id
523
- })
524
- .map(|(member_version_id, members)| (member_version_id.clone(), members.clone()))
525
- .collect(),
526
- extra_commit_parents_by_version: staged_writes
527
- .extra_commit_parents_by_version
528
- .iter()
529
- .filter(|(parent_version_id, _)| parent_version_id.as_str() == schema_scope_version_id)
530
- .map(|(parent_version_id, parents)| (parent_version_id.clone(), parents.clone()))
531
- .collect(),
532
- file_data_writes: staged_writes
533
- .file_data_writes
534
- .iter()
535
- .filter(|write| {
536
- let write_schema_scope = if write.version_id == GLOBAL_VERSION_ID {
537
- GLOBAL_VERSION_ID
538
- } else {
539
- write.version_id.as_str()
540
- };
541
- write_schema_scope == schema_scope_version_id
542
- })
543
- .cloned()
544
- .collect(),
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,
545
769
  }
546
770
  }
547
771
 
548
- fn require_valid_stage_write_storage_scopes(write: &StageWrite) -> Result<(), LixError> {
772
+ fn require_valid_transaction_write_storage_scopes(
773
+ write: &TransactionWrite,
774
+ ) -> Result<(), LixError> {
549
775
  match write {
550
- StageWrite::Rows { rows, .. } => require_valid_stage_row_storage_scopes(rows),
551
- StageWrite::RowsWithFileData { rows, .. } => require_valid_stage_row_storage_scopes(rows),
552
- StageWrite::AdoptedChanges { .. } => Ok(()),
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(()),
553
783
  }
554
784
  }
555
785
 
556
- fn require_valid_stage_row_storage_scopes(rows: &[StageRow]) -> Result<(), LixError> {
786
+ fn require_valid_transaction_write_row_storage_scopes(
787
+ rows: &[TransactionWriteRow],
788
+ ) -> Result<(), LixError> {
557
789
  for row in rows {
558
790
  require_valid_storage_scope(row.version_id.as_str(), row.global)?;
559
791
  }
@@ -570,11 +802,11 @@ fn require_valid_storage_scope(version_id: &str, global: bool) -> Result<(), Lix
570
802
  Ok(())
571
803
  }
572
804
 
573
- fn stage_row_version_ids(rows: &[StageRow]) -> BTreeSet<String> {
805
+ fn transaction_write_row_version_ids(rows: &[TransactionWriteRow]) -> BTreeSet<String> {
574
806
  rows.iter().map(|row| row.version_id.clone()).collect()
575
807
  }
576
808
 
577
- fn stage_file_data_version_ids(file_data: &[StageFileData]) -> BTreeSet<String> {
809
+ fn stage_file_data_version_ids(file_data: &[TransactionFileData]) -> BTreeSet<String> {
578
810
  file_data
579
811
  .iter()
580
812
  .map(|write| write.version_id.clone())
@@ -662,8 +894,9 @@ mod tests {
662
894
 
663
895
  use super::*;
664
896
  use crate::backend::testing::UnitTestBackend;
665
- use crate::changelog::ChangelogScanRequest;
897
+ use crate::commit_store::{ChangeScanRequest, CommitStoreContext};
666
898
  use crate::tracked_state::{TrackedStateRowRequest, TrackedStateScanRequest};
899
+ use crate::transaction::types::TransactionJson;
667
900
  use crate::untracked_state::{UntrackedStateContext, UntrackedStateRowRequest};
668
901
  use crate::version::VersionContext;
669
902
  use crate::Backend;
@@ -674,20 +907,23 @@ mod tests {
674
907
  LiveStateContext::new(
675
908
  crate::tracked_state::TrackedStateContext::new(),
676
909
  crate::untracked_state::UntrackedStateContext::new(),
677
- crate::commit_graph::CommitGraphContext::new(crate::changelog::ChangelogContext::new()),
910
+ crate::commit_graph::CommitGraphContext::new(),
678
911
  )
679
912
  }
680
913
 
914
+ const SCHEMA_FIXTURE_COMMIT_ID: &str = "schema-fixture-commit";
915
+
681
916
  #[tokio::test]
682
917
  async fn stage_rows_routes_tracked_and_untracked_rows_without_sql() {
683
918
  let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
684
919
  let storage = StorageContext::new(Arc::clone(&backend));
685
920
  let live_state = Arc::new(live_state_context());
686
- seed_visible_schema_rows(storage.clone(), &live_state).await;
921
+ seed_visible_schema_rows(storage.clone()).await;
687
922
  let binary_cas = Arc::new(BinaryCasContext::new());
688
- let changelog = Arc::new(ChangelogContext::new());
923
+ let changelog = Arc::new(CommitStoreContext::new());
924
+ let commit_store = Arc::new(CommitStoreContext::new());
689
925
  let version_ctx = Arc::new(VersionContext::new(Arc::new(UntrackedStateContext::new())));
690
- let schema_registry = Arc::new(SchemaRegistry::new());
926
+ let catalog_context = Arc::new(CatalogContext::new());
691
927
  let opened = open_transaction(
692
928
  &SessionMode::Pinned {
693
929
  version_id: GLOBAL_VERSION_ID.to_string(),
@@ -696,9 +932,9 @@ mod tests {
696
932
  Arc::clone(&live_state),
697
933
  Arc::new(crate::tracked_state::TrackedStateContext::new()),
698
934
  Arc::clone(&binary_cas),
699
- Arc::clone(&changelog),
935
+ Arc::clone(&commit_store),
700
936
  Arc::clone(&version_ctx),
701
- Arc::clone(&schema_registry),
937
+ Arc::clone(&catalog_context),
702
938
  )
703
939
  .await
704
940
  .expect("transaction should open");
@@ -719,20 +955,25 @@ mod tests {
719
955
 
720
956
  let changes = changelog
721
957
  .reader(storage.clone())
722
- .scan_changes(&ChangelogScanRequest::default())
958
+ .scan_changes(&ChangeScanRequest::default())
723
959
  .await
724
960
  .expect("changelog should scan");
725
961
  assert!(
726
- changes
727
- .iter()
728
- .any(|change| change.entity_id.as_string().as_deref() == Ok("tracked-programmatic")),
962
+ changes.iter().any(|change| change
963
+ .record
964
+ .entity_id
965
+ .as_single_string_owned()
966
+ .as_deref()
967
+ == Ok("tracked-programmatic")),
729
968
  "tracked staged row should be appended to changelog"
730
969
  );
731
970
  assert!(
732
- !changes
733
- .iter()
734
- .any(|change| change.entity_id.as_string().as_deref()
735
- == Ok("untracked-programmatic")),
971
+ !changes.iter().any(|change| change
972
+ .record
973
+ .entity_id
974
+ .as_single_string_owned()
975
+ .as_deref()
976
+ == Ok("untracked-programmatic")),
736
977
  "untracked staged row must not be appended to changelog"
737
978
  );
738
979
 
@@ -745,18 +986,20 @@ mod tests {
745
986
 
746
987
  let tracked_row = crate::tracked_state::TrackedStateContext::new()
747
988
  .reader(storage.clone())
748
- .load_row_at_commit(
989
+ .load_rows_at_commit(
749
990
  &head_commit_id,
750
- &TrackedStateRowRequest {
991
+ &[TrackedStateRowRequest {
751
992
  schema_key: "lix_key_value".to_string(),
752
993
  entity_id: crate::entity_identity::EntityIdentity::single(
753
994
  "tracked-programmatic",
754
995
  ),
755
996
  file_id: NullableKeyFilter::Null,
756
- },
997
+ }],
757
998
  )
758
999
  .await
759
1000
  .expect("tracked state should load")
1001
+ .pop()
1002
+ .flatten()
760
1003
  .expect("tracked row should be present in tracked state");
761
1004
  assert_eq!(tracked_row.commit_id, head_commit_id);
762
1005
  assert_eq!(
@@ -803,7 +1046,8 @@ mod tests {
803
1046
  assert!(
804
1047
  tracked_rows
805
1048
  .iter()
806
- .all(|row| row.entity_id.as_string().as_deref() != Ok("untracked-programmatic")),
1049
+ .all(|row| row.entity_id.as_single_string_owned().as_deref()
1050
+ != Ok("untracked-programmatic")),
807
1051
  "untracked staged rows should not be written into tracked state"
808
1052
  );
809
1053
  }
@@ -813,11 +1057,12 @@ mod tests {
813
1057
  let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
814
1058
  let storage = StorageContext::new(Arc::clone(&backend));
815
1059
  let live_state = Arc::new(live_state_context());
816
- seed_visible_schema_rows(storage.clone(), &live_state).await;
1060
+ seed_visible_schema_rows(storage.clone()).await;
817
1061
  let binary_cas = Arc::new(BinaryCasContext::new());
818
- let changelog = Arc::new(ChangelogContext::new());
1062
+ let changelog = Arc::new(CommitStoreContext::new());
1063
+ let commit_store = Arc::new(CommitStoreContext::new());
819
1064
  let version_ctx = Arc::new(VersionContext::new(Arc::new(UntrackedStateContext::new())));
820
- let schema_registry = Arc::new(SchemaRegistry::new());
1065
+ let catalog_context = Arc::new(CatalogContext::new());
821
1066
  let opened = open_transaction(
822
1067
  &SessionMode::Pinned {
823
1068
  version_id: GLOBAL_VERSION_ID.to_string(),
@@ -826,9 +1071,9 @@ mod tests {
826
1071
  Arc::clone(&live_state),
827
1072
  Arc::new(crate::tracked_state::TrackedStateContext::new()),
828
1073
  Arc::clone(&binary_cas),
829
- Arc::clone(&changelog),
1074
+ Arc::clone(&commit_store),
830
1075
  Arc::clone(&version_ctx),
831
- Arc::clone(&schema_registry),
1076
+ Arc::clone(&catalog_context),
832
1077
  )
833
1078
  .await
834
1079
  .expect("transaction should open");
@@ -836,7 +1081,9 @@ mod tests {
836
1081
  let runtime_functions = opened.runtime_functions;
837
1082
 
838
1083
  let mut invalid_row = key_value_stage_row("invalid-programmatic", "invalid", false);
839
- invalid_row.snapshot_content = Some("{\"key\":\"invalid-programmatic\"}".to_string());
1084
+ invalid_row.snapshot = Some(TransactionJson::from_value_for_test(
1085
+ json!({"key": "invalid-programmatic"}),
1086
+ ));
840
1087
  transaction
841
1088
  .stage_rows(vec![invalid_row])
842
1089
  .await
@@ -853,11 +1100,16 @@ mod tests {
853
1100
 
854
1101
  let changes = changelog
855
1102
  .reader(storage.clone())
856
- .scan_changes(&ChangelogScanRequest::default())
1103
+ .scan_changes(&ChangeScanRequest::default())
857
1104
  .await
858
1105
  .expect("changelog should scan after failed commit");
859
1106
  assert!(
860
- changes.is_empty(),
1107
+ changes.iter().all(|change| change
1108
+ .record
1109
+ .entity_id
1110
+ .as_single_string_owned()
1111
+ .as_deref()
1112
+ != Ok("invalid-programmatic")),
861
1113
  "validation failure must happen before changelog persistence"
862
1114
  );
863
1115
  let head = version_ctx
@@ -866,8 +1118,9 @@ mod tests {
866
1118
  .await
867
1119
  .expect("version ref should load after failed commit");
868
1120
  assert_eq!(
869
- head, None,
870
- "validation failure must happen before version-ref persistence"
1121
+ head.as_deref(),
1122
+ Some(SCHEMA_FIXTURE_COMMIT_ID),
1123
+ "validation failure must not advance the version ref"
871
1124
  );
872
1125
  }
873
1126
 
@@ -879,7 +1132,7 @@ mod tests {
879
1132
  open_test_transaction(&backend).await;
880
1133
 
881
1134
  let mut row = key_value_stage_row("invalid-metadata", "value", false);
882
- row.metadata = Some(json!("not-an-object"));
1135
+ row.metadata = Some(TransactionJson::from_value_for_test(json!("not-an-object")));
883
1136
  transaction
884
1137
  .stage_rows(vec![row])
885
1138
  .await
@@ -900,6 +1153,7 @@ mod tests {
900
1153
  &live_state,
901
1154
  &changelog,
902
1155
  &version_ref,
1156
+ "invalid-metadata",
903
1157
  )
904
1158
  .await;
905
1159
  }
@@ -928,7 +1182,7 @@ mod tests {
928
1182
  assert!(
929
1183
  error
930
1184
  .message
931
- .contains("schema 'missing_schema' version '1' is not visible"),
1185
+ .contains("schema 'missing_schema' is not visible"),
932
1186
  "error should explain missing schema visibility: {error:?}"
933
1187
  );
934
1188
  }
@@ -991,35 +1245,6 @@ mod tests {
991
1245
  );
992
1246
  }
993
1247
 
994
- #[tokio::test]
995
- async fn stage_rows_rejects_unknown_schema_version_without_sql() {
996
- let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
997
- let (
998
- _live_state,
999
- _binary_cas,
1000
- _changelog,
1001
- _version_ref,
1002
- _runtime_functions,
1003
- mut transaction,
1004
- ) = open_test_transaction(&backend).await;
1005
-
1006
- let mut row = key_value_stage_row("unknown-version", "value", false);
1007
- row.schema_version = "999".to_string();
1008
-
1009
- let error = transaction
1010
- .stage_rows(vec![row])
1011
- .await
1012
- .expect_err("unknown schema version should be rejected while staging");
1013
-
1014
- assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
1015
- assert!(
1016
- error
1017
- .message
1018
- .contains("schema 'lix_key_value' version '999' is not visible"),
1019
- "error should explain missing schema version visibility: {error:?}"
1020
- );
1021
- }
1022
-
1023
1248
  #[tokio::test]
1024
1249
  async fn stage_rows_rejects_invalid_snapshot_json_without_sql() {
1025
1250
  let backend: Arc<dyn Backend + Send + Sync> = Arc::new(UnitTestBackend::new());
@@ -1033,17 +1258,17 @@ mod tests {
1033
1258
  ) = open_test_transaction(&backend).await;
1034
1259
 
1035
1260
  let mut row = key_value_stage_row("invalid-json", "value", false);
1036
- row.snapshot_content = Some("{".to_string());
1261
+ row.snapshot = Some(TransactionJson::from_value_for_test(json!("not-an-object")));
1037
1262
 
1038
1263
  let error = transaction
1039
1264
  .stage_rows(vec![row])
1040
1265
  .await
1041
- .expect_err("invalid JSON should be rejected while staging");
1266
+ .expect_err("non-object snapshot should be rejected while staging");
1042
1267
 
1043
1268
  assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
1044
1269
  assert!(
1045
- error.message.contains("invalid JSON"),
1046
- "error should explain invalid JSON: {error:?}"
1270
+ error.message.contains("must be a JSON object"),
1271
+ "error should explain invalid snapshot shape: {error:?}"
1047
1272
  );
1048
1273
  }
1049
1274
 
@@ -1055,7 +1280,9 @@ mod tests {
1055
1280
  open_test_transaction(&backend).await;
1056
1281
 
1057
1282
  let mut row = key_value_stage_row("schema-mismatch", "value", false);
1058
- row.snapshot_content = Some(r#"{"key":"schema-mismatch"}"#.to_string());
1283
+ row.snapshot = Some(TransactionJson::from_value_for_test(
1284
+ json!({"key": "schema-mismatch"}),
1285
+ ));
1059
1286
  transaction
1060
1287
  .stage_rows(vec![row])
1061
1288
  .await
@@ -1076,6 +1303,7 @@ mod tests {
1076
1303
  &live_state,
1077
1304
  &changelog,
1078
1305
  &version_ref,
1306
+ "schema-mismatch",
1079
1307
  )
1080
1308
  .await;
1081
1309
  }
@@ -1094,14 +1322,18 @@ mod tests {
1094
1322
 
1095
1323
  let mut row = key_value_stage_row("malformed-registered-schema", "value", false);
1096
1324
  row.schema_key = "lix_registered_schema".to_string();
1097
- row.snapshot_content = Some(
1098
- json!({
1099
- "value": {
1100
- "x-lix-key": "malformed_registered_schema"
1101
- }
1102
- })
1103
- .to_string(),
1104
- );
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
+ })));
1105
1337
  row.entity_id = None;
1106
1338
 
1107
1339
  let error = transaction
@@ -1109,10 +1341,9 @@ mod tests {
1109
1341
  .await
1110
1342
  .expect_err("malformed registered schema should be rejected while staging");
1111
1343
 
1112
- assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
1344
+ assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
1113
1345
  assert!(
1114
- error.message.contains("x-lix-version")
1115
- || error.message.contains("primary-key pointer"),
1346
+ error.message.contains("x-lix-primary-key"),
1116
1347
  "error should explain malformed registered schema: {error:?}"
1117
1348
  );
1118
1349
  }
@@ -1151,18 +1382,19 @@ mod tests {
1151
1382
  ) -> (
1152
1383
  Arc<LiveStateContext>,
1153
1384
  Arc<BinaryCasContext>,
1154
- Arc<ChangelogContext>,
1385
+ Arc<CommitStoreContext>,
1155
1386
  Arc<VersionContext>,
1156
1387
  FunctionContext,
1157
1388
  Transaction,
1158
1389
  ) {
1159
1390
  let storage = StorageContext::new(Arc::clone(backend));
1160
1391
  let live_state = Arc::new(live_state_context());
1161
- seed_visible_schema_rows(storage.clone(), &live_state).await;
1392
+ seed_visible_schema_rows(storage.clone()).await;
1162
1393
  let binary_cas = Arc::new(BinaryCasContext::new());
1163
- let changelog = Arc::new(ChangelogContext::new());
1394
+ let changelog = Arc::new(CommitStoreContext::new());
1395
+ let commit_store = Arc::new(CommitStoreContext::new());
1164
1396
  let version_ctx = Arc::new(VersionContext::new(Arc::new(UntrackedStateContext::new())));
1165
- let schema_registry = Arc::new(SchemaRegistry::new());
1397
+ let catalog_context = Arc::new(CatalogContext::new());
1166
1398
  let opened = open_transaction(
1167
1399
  &SessionMode::Pinned {
1168
1400
  version_id: GLOBAL_VERSION_ID.to_string(),
@@ -1171,9 +1403,9 @@ mod tests {
1171
1403
  Arc::clone(&live_state),
1172
1404
  Arc::new(crate::tracked_state::TrackedStateContext::new()),
1173
1405
  Arc::clone(&binary_cas),
1174
- Arc::clone(&changelog),
1406
+ Arc::clone(&commit_store),
1175
1407
  Arc::clone(&version_ctx),
1176
- schema_registry,
1408
+ catalog_context,
1177
1409
  )
1178
1410
  .await
1179
1411
  .expect("transaction should open");
@@ -1190,46 +1422,52 @@ mod tests {
1190
1422
  )
1191
1423
  }
1192
1424
 
1193
- async fn seed_visible_schema_rows(storage: StorageContext, live_state: &LiveStateContext) {
1425
+ async fn seed_visible_schema_rows(storage: StorageContext) {
1426
+ let mut writes = StorageWriteSet::new();
1194
1427
  let rows = crate::schema::seed_schema_definitions()
1195
1428
  .into_iter()
1196
1429
  .map(|schema| {
1197
1430
  let key = crate::schema::schema_key_from_definition(schema)
1198
1431
  .expect("seed schema key should derive");
1199
- LiveStateRow {
1200
- entity_id: crate::schema::registered_schema_entity_id(
1201
- &key.schema_key,
1202
- &key.schema_version,
1203
- )
1204
- .expect("registered schema identity 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"),
1205
1436
  schema_key: "lix_registered_schema".to_string(),
1206
1437
  file_id: None,
1207
- version_id: GLOBAL_VERSION_ID.to_string(),
1208
- schema_version: "1".to_string(),
1209
- snapshot_content: Some(json!({ "value": schema }).to_string()),
1438
+ snapshot_content: Some(snapshot_content),
1210
1439
  metadata: None,
1440
+ deleted: false,
1211
1441
  created_at: "1970-01-01T00:00:00.000Z".to_string(),
1212
1442
  updated_at: "1970-01-01T00:00:00.000Z".to_string(),
1213
- change_id: None,
1214
- commit_id: None,
1215
- untracked: true,
1216
- global: true,
1443
+ change_id: format!("schema-fixture-{}", key.schema_key),
1444
+ commit_id: SCHEMA_FIXTURE_COMMIT_ID.to_string(),
1217
1445
  }
1218
1446
  })
1219
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");
1220
1454
  let mut storage_transaction = storage
1221
1455
  .begin_write_transaction()
1222
1456
  .await
1223
1457
  .expect("schema fixture transaction should open");
1224
- let mut writes = StorageWriteSet::new();
1225
- let mut json_writer = JsonStoreContext::new().writer();
1226
- {
1227
- let mut writer = live_state.writer(storage_transaction.as_mut());
1228
- writer
1229
- .stage_rows(&mut writes, &mut json_writer, &rows)
1230
- .await
1231
- .expect("schema fixture rows should stage");
1232
- }
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");
1233
1471
  writes
1234
1472
  .apply(&mut storage_transaction.as_mut())
1235
1473
  .await
@@ -1243,16 +1481,22 @@ mod tests {
1243
1481
  async fn assert_no_persistence_after_validation_failure(
1244
1482
  storage: StorageContext,
1245
1483
  live_state: &LiveStateContext,
1246
- changelog: &ChangelogContext,
1484
+ changelog: &CommitStoreContext,
1247
1485
  version_ctx: &VersionContext,
1486
+ rejected_entity_id: &str,
1248
1487
  ) {
1249
1488
  let changes = changelog
1250
1489
  .reader(storage.clone())
1251
- .scan_changes(&ChangelogScanRequest::default())
1490
+ .scan_changes(&ChangeScanRequest::default())
1252
1491
  .await
1253
1492
  .expect("changelog should scan after failed commit");
1254
1493
  assert!(
1255
- changes.is_empty(),
1494
+ changes.iter().all(|change| change
1495
+ .record
1496
+ .entity_id
1497
+ .as_single_string_owned()
1498
+ .as_deref()
1499
+ != Ok(rejected_entity_id)),
1256
1500
  "validation failure must happen before changelog persistence"
1257
1501
  );
1258
1502
  let head = version_ctx
@@ -1261,15 +1505,16 @@ mod tests {
1261
1505
  .await
1262
1506
  .expect("version ref should load after failed commit");
1263
1507
  assert_eq!(
1264
- head, None,
1265
- "validation failure must happen before version-ref persistence"
1508
+ head.as_deref(),
1509
+ Some(SCHEMA_FIXTURE_COMMIT_ID),
1510
+ "validation failure must not advance the version ref"
1266
1511
  );
1267
1512
  let row = live_state
1268
1513
  .reader(storage)
1269
1514
  .load_row(&crate::live_state::LiveStateRowRequest {
1270
1515
  schema_key: "lix_key_value".to_string(),
1271
1516
  version_id: GLOBAL_VERSION_ID.to_string(),
1272
- entity_id: crate::entity_identity::EntityIdentity::single("schema-mismatch"),
1517
+ entity_id: crate::entity_identity::EntityIdentity::single(rejected_entity_id),
1273
1518
  file_id: NullableKeyFilter::Null,
1274
1519
  })
1275
1520
  .await
@@ -1280,21 +1525,17 @@ mod tests {
1280
1525
  );
1281
1526
  }
1282
1527
 
1283
- fn key_value_stage_row(key: &str, value: &str, untracked: bool) -> StageRow {
1284
- StageRow {
1528
+ fn key_value_stage_row(key: &str, value: &str, untracked: bool) -> TransactionWriteRow {
1529
+ TransactionWriteRow {
1285
1530
  entity_id: Some(crate::entity_identity::EntityIdentity::single(key)),
1286
1531
  schema_key: "lix_key_value".to_string(),
1287
1532
  file_id: None,
1288
- snapshot_content: Some(
1289
- json!({
1290
- "key": key,
1291
- "value": value,
1292
- })
1293
- .to_string(),
1294
- ),
1533
+ snapshot: Some(TransactionJson::from_value_for_test(json!({
1534
+ "key": key,
1535
+ "value": value,
1536
+ }))),
1295
1537
  metadata: None,
1296
1538
  origin: None,
1297
- schema_version: "1".to_string(),
1298
1539
  created_at: None,
1299
1540
  updated_at: None,
1300
1541
  global: true,