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

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