@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,31 +1,31 @@
1
1
  use std::sync::Arc;
2
2
 
3
- use crate::storage::{StorageReader, StorageWriteSet};
3
+ use crate::storage::{StorageRead, StorageWriteSet};
4
4
  use crate::untracked_state::{UntrackedStateContext, UntrackedStateRow};
5
5
 
6
- use super::refs::VersionRefContext;
7
- use super::VersionRefReader;
6
+ use super::refs::BranchRefContext;
7
+ use super::BranchRefReader;
8
8
 
9
- /// Aggregate entrypoint for version-domain services.
9
+ /// Aggregate entrypoint for branch-domain services.
10
10
  ///
11
11
  /// Today this owns the moving-ref subsystem. Descriptor helpers are re-exported
12
- /// by `version`; future version APIs can grow here without making session or
12
+ /// by `branch`; future branch APIs can grow here without making session or
13
13
  /// SQL code depend directly on ref storage details.
14
- pub(crate) struct VersionContext {
15
- refs: Arc<VersionRefContext>,
14
+ pub(crate) struct BranchContext {
15
+ refs: Arc<BranchRefContext>,
16
16
  }
17
17
 
18
- impl VersionContext {
18
+ impl BranchContext {
19
19
  pub(crate) fn new(untracked_state: Arc<UntrackedStateContext>) -> Self {
20
20
  Self {
21
- refs: Arc::new(VersionRefContext::new(untracked_state)),
21
+ refs: Arc::new(BranchRefContext::new(untracked_state)),
22
22
  }
23
23
  }
24
24
 
25
- /// Creates a version-ref reader over a caller-provided KV store.
26
- pub(crate) fn ref_reader<S>(&self, store: S) -> impl VersionRefReader
25
+ /// Creates a branch-ref reader over a caller-provided KV store.
26
+ pub(crate) fn ref_reader<S>(&self, store: S) -> impl BranchRefReader
27
27
  where
28
- S: StorageReader + Send,
28
+ S: StorageRead + Send + Sync,
29
29
  {
30
30
  self.refs.reader(store)
31
31
  }
@@ -0,0 +1,221 @@
1
+ use crate::commit_graph::{CommitGraphCommit, CommitGraphReader};
2
+ use crate::common::validate_non_empty_identity_value;
3
+ use crate::LixError;
4
+
5
+ use super::{BranchHead, BranchRefReader};
6
+
7
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
+ pub(crate) enum BranchOperation {
9
+ CreateBranch,
10
+ SwitchBranch,
11
+ MergeBranch,
12
+ MergeBranchPreview,
13
+ LoadWorkspaceSelector,
14
+ }
15
+
16
+ impl BranchOperation {
17
+ pub(crate) fn label(self) -> &'static str {
18
+ match self {
19
+ Self::CreateBranch => "create_branch",
20
+ Self::SwitchBranch => "switch_branch",
21
+ Self::MergeBranch => "merge_branch",
22
+ Self::MergeBranchPreview => "merge_branch_preview",
23
+ Self::LoadWorkspaceSelector => "load_workspace_branch_id",
24
+ }
25
+ }
26
+ }
27
+
28
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29
+ pub(crate) enum BranchReferenceRole {
30
+ Source,
31
+ Target,
32
+ WorkspaceSelector,
33
+ CommitSource,
34
+ }
35
+
36
+ impl BranchReferenceRole {
37
+ pub(crate) fn label(self) -> &'static str {
38
+ match self {
39
+ Self::Source => "source",
40
+ Self::Target => "target",
41
+ Self::WorkspaceSelector => "workspace_selector",
42
+ Self::CommitSource => "commit_source",
43
+ }
44
+ }
45
+ }
46
+
47
+ /// Shared domain service for resolving public branch references.
48
+ ///
49
+ /// Built-in branch schemas describe row shape. This service owns semantic
50
+ /// ref validation: non-empty ids, global sentinel handling, and missing refs.
51
+ pub(crate) struct BranchLifecycle<'a> {
52
+ refs: &'a dyn BranchRefReader,
53
+ }
54
+
55
+ impl<'a> BranchLifecycle<'a> {
56
+ pub(crate) fn new(refs: &'a dyn BranchRefReader) -> Self {
57
+ Self { refs }
58
+ }
59
+
60
+ pub(crate) fn require_non_empty_id(
61
+ branch_id: &str,
62
+ operation: BranchOperation,
63
+ role: BranchReferenceRole,
64
+ ) -> Result<(), LixError> {
65
+ require_non_empty_public_id("branch_id", branch_id, operation, role)
66
+ }
67
+
68
+ pub(crate) async fn require_existing_commit(
69
+ commit_graph: &mut dyn CommitGraphReader,
70
+ commit_id: &str,
71
+ operation: BranchOperation,
72
+ role: BranchReferenceRole,
73
+ ) -> Result<CommitGraphCommit, LixError> {
74
+ require_non_empty_public_id("commit_id", commit_id, operation, role)?;
75
+ commit_graph
76
+ .load_commit(commit_id)
77
+ .await?
78
+ .ok_or_else(|| LixError::commit_not_found(commit_id, operation.label(), role.label()))
79
+ }
80
+
81
+ pub(crate) async fn require_existing_ref(
82
+ &self,
83
+ branch_id: &str,
84
+ operation: BranchOperation,
85
+ role: BranchReferenceRole,
86
+ ) -> Result<BranchHead, LixError> {
87
+ Self::require_non_empty_id(branch_id, operation, role)?;
88
+ self.require_existing_stored_ref(branch_id, operation, role)
89
+ .await
90
+ }
91
+
92
+ pub(crate) async fn require_existing_commit_id(
93
+ &self,
94
+ branch_id: &str,
95
+ operation: BranchOperation,
96
+ role: BranchReferenceRole,
97
+ ) -> Result<String, LixError> {
98
+ Ok(self
99
+ .require_existing_ref(branch_id, operation, role)
100
+ .await?
101
+ .commit_id)
102
+ }
103
+
104
+ async fn require_existing_stored_ref(
105
+ &self,
106
+ branch_id: &str,
107
+ operation: BranchOperation,
108
+ role: BranchReferenceRole,
109
+ ) -> Result<BranchHead, LixError> {
110
+ self.refs
111
+ .load_head(branch_id)
112
+ .await?
113
+ .ok_or_else(|| LixError::branch_not_found(branch_id, operation.label(), role.label()))
114
+ }
115
+ }
116
+
117
+ fn require_non_empty_public_id(
118
+ label: &str,
119
+ value: &str,
120
+ operation: BranchOperation,
121
+ role: BranchReferenceRole,
122
+ ) -> Result<(), LixError> {
123
+ validate_non_empty_identity_value(label, value)
124
+ .map(|_| ())
125
+ .map_err(|_| {
126
+ LixError::new(
127
+ LixError::CODE_INVALID_PARAM,
128
+ format!(
129
+ "{} {} {label} must be non-empty",
130
+ operation.label(),
131
+ role.label()
132
+ ),
133
+ )
134
+ })
135
+ }
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use async_trait::async_trait;
140
+
141
+ use super::*;
142
+
143
+ #[tokio::test]
144
+ async fn require_existing_ref_returns_head() {
145
+ let reader = RowsBranchRefReader::new(vec![BranchHead {
146
+ branch_id: "branch-a".to_string(),
147
+ commit_id: "commit-a".to_string(),
148
+ }]);
149
+ let lifecycle = BranchLifecycle::new(&reader);
150
+
151
+ let head = lifecycle
152
+ .require_existing_ref(
153
+ "branch-a",
154
+ BranchOperation::SwitchBranch,
155
+ BranchReferenceRole::Target,
156
+ )
157
+ .await
158
+ .expect("branch should resolve");
159
+
160
+ assert_eq!(head.commit_id, "commit-a");
161
+ }
162
+
163
+ #[tokio::test]
164
+ async fn require_existing_ref_rejects_empty_id_as_invalid_param() {
165
+ let reader = RowsBranchRefReader::new(Vec::new());
166
+ let lifecycle = BranchLifecycle::new(&reader);
167
+
168
+ let error = lifecycle
169
+ .require_existing_ref(
170
+ "",
171
+ BranchOperation::SwitchBranch,
172
+ BranchReferenceRole::Target,
173
+ )
174
+ .await
175
+ .expect_err("empty branch id should be rejected before lookup");
176
+
177
+ assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
178
+ }
179
+
180
+ #[tokio::test]
181
+ async fn require_existing_ref_reports_missing_branch() {
182
+ let reader = RowsBranchRefReader::new(Vec::new());
183
+ let lifecycle = BranchLifecycle::new(&reader);
184
+
185
+ let error = lifecycle
186
+ .require_existing_ref(
187
+ "missing",
188
+ BranchOperation::SwitchBranch,
189
+ BranchReferenceRole::Target,
190
+ )
191
+ .await
192
+ .expect_err("missing branch should be rejected");
193
+
194
+ assert_eq!(error.code, LixError::CODE_BRANCH_NOT_FOUND);
195
+ }
196
+
197
+ struct RowsBranchRefReader {
198
+ heads: Vec<BranchHead>,
199
+ }
200
+
201
+ impl RowsBranchRefReader {
202
+ fn new(heads: Vec<BranchHead>) -> Self {
203
+ Self { heads }
204
+ }
205
+ }
206
+
207
+ #[async_trait]
208
+ impl BranchRefReader for RowsBranchRefReader {
209
+ async fn load_head(&self, branch_id: &str) -> Result<Option<BranchHead>, LixError> {
210
+ Ok(self
211
+ .heads
212
+ .iter()
213
+ .find(|head| head.branch_id == branch_id)
214
+ .cloned())
215
+ }
216
+
217
+ async fn scan_heads(&self) -> Result<Vec<BranchHead>, LixError> {
218
+ Ok(self.heads.clone())
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,13 @@
1
+ mod context;
2
+ mod lifecycle;
3
+ mod refs;
4
+ mod stage_rows;
5
+ mod types;
6
+
7
+ pub(crate) use context::BranchContext;
8
+ pub(crate) use lifecycle::{BranchLifecycle, BranchOperation, BranchReferenceRole};
9
+ pub(crate) use stage_rows::{
10
+ branch_descriptor_stage_row, branch_descriptor_tombstone_row, branch_ref_stage_row,
11
+ branch_ref_tombstone_row, BRANCH_DESCRIPTOR_SCHEMA_KEY, BRANCH_REF_SCHEMA_KEY,
12
+ };
13
+ pub(crate) use types::{BranchHead, BranchRefReader};
@@ -0,0 +1,321 @@
1
+ use std::sync::Arc;
2
+
3
+ use tokio::sync::Mutex;
4
+
5
+ use crate::branch::BRANCH_REF_SCHEMA_KEY;
6
+ use crate::branch::{BranchHead, BranchRefReader};
7
+ use crate::entity_pk::EntityPk;
8
+ use crate::storage::{StorageRead, StorageWriteSet};
9
+ use crate::untracked_state::{
10
+ MaterializedUntrackedStateRow, UntrackedStateContext, UntrackedStateFilter, UntrackedStateRow,
11
+ UntrackedStateRowRequest, UntrackedStateScanRequest,
12
+ };
13
+ use crate::GLOBAL_BRANCH_ID;
14
+ use crate::{LixError, NullableKeyFilter};
15
+
16
+ /// Typed access to moving branch heads stored in untracked state.
17
+ ///
18
+ /// Branch refs are one of the inputs used by live_state visibility, so this
19
+ /// context deliberately bypasses live_state and reads the underlying untracked
20
+ /// rows directly. That keeps the dependency acyclic:
21
+ /// untracked_state -> branch_ref -> live_state.
22
+ pub(super) struct BranchRefContext {
23
+ untracked_state: Arc<UntrackedStateContext>,
24
+ }
25
+
26
+ impl BranchRefContext {
27
+ pub(super) fn new(untracked_state: Arc<UntrackedStateContext>) -> Self {
28
+ Self { untracked_state }
29
+ }
30
+
31
+ /// Creates a branch-ref reader over a caller-provided KV store.
32
+ pub(super) fn reader<S>(&self, store: S) -> BranchRefStoreReader<S>
33
+ where
34
+ S: StorageRead + Send + Sync,
35
+ {
36
+ BranchRefStoreReader {
37
+ untracked_state: Arc::clone(&self.untracked_state),
38
+ store: Mutex::new(store),
39
+ }
40
+ }
41
+
42
+ /// Creates a branch-ref writer over a transaction-local storage write set.
43
+ pub(super) fn writer<'a>(&self, writes: &'a mut StorageWriteSet) -> BranchRefWriter<'a> {
44
+ BranchRefWriter {
45
+ untracked_state: Arc::clone(&self.untracked_state),
46
+ writes,
47
+ }
48
+ }
49
+ }
50
+
51
+ /// Read side for branch heads.
52
+ pub(super) struct BranchRefStoreReader<S>
53
+ where
54
+ S: StorageRead + Send + Sync,
55
+ {
56
+ untracked_state: Arc<UntrackedStateContext>,
57
+ store: Mutex<S>,
58
+ }
59
+
60
+ impl<S> BranchRefStoreReader<S>
61
+ where
62
+ S: StorageRead + Send + Sync,
63
+ {
64
+ pub(crate) async fn load_head(&self, branch_id: &str) -> Result<Option<BranchHead>, LixError> {
65
+ let store = self.store.lock().await;
66
+ let Some(row) = self
67
+ .untracked_state
68
+ .reader(&*store)
69
+ .load_row(&UntrackedStateRowRequest {
70
+ schema_key: BRANCH_REF_SCHEMA_KEY.to_string(),
71
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
72
+ entity_pk: EntityPk::single(branch_id),
73
+ file_id: NullableKeyFilter::Null,
74
+ })
75
+ .await?
76
+ else {
77
+ return Ok(None);
78
+ };
79
+
80
+ decode_branch_head(branch_id, &row)
81
+ }
82
+
83
+ pub(crate) async fn load_head_commit_id(
84
+ &self,
85
+ branch_id: &str,
86
+ ) -> Result<Option<String>, LixError> {
87
+ Ok(self.load_head(branch_id).await?.map(|head| head.commit_id))
88
+ }
89
+
90
+ pub(crate) async fn scan_heads(&self) -> Result<Vec<BranchHead>, LixError> {
91
+ let store = self.store.lock().await;
92
+ let rows = self
93
+ .untracked_state
94
+ .reader(&*store)
95
+ .scan_rows(&UntrackedStateScanRequest {
96
+ filter: UntrackedStateFilter {
97
+ schema_keys: vec![BRANCH_REF_SCHEMA_KEY.to_string()],
98
+ branch_ids: vec![GLOBAL_BRANCH_ID.to_string()],
99
+ ..UntrackedStateFilter::default()
100
+ },
101
+ ..UntrackedStateScanRequest::default()
102
+ })
103
+ .await?;
104
+ let mut heads = rows
105
+ .iter()
106
+ .map(|row| {
107
+ let branch_id = row.entity_pk.as_single_string_owned()?;
108
+ decode_branch_head(&branch_id, row)
109
+ })
110
+ .collect::<Result<Vec<_>, _>>()?
111
+ .into_iter()
112
+ .flatten()
113
+ .collect::<Vec<_>>();
114
+ heads.sort_by(|left, right| left.branch_id.cmp(&right.branch_id));
115
+ Ok(heads)
116
+ }
117
+ }
118
+
119
+ #[async_trait::async_trait]
120
+ impl<S> BranchRefReader for BranchRefStoreReader<S>
121
+ where
122
+ S: StorageRead + Send + Sync,
123
+ {
124
+ async fn load_head(&self, branch_id: &str) -> Result<Option<BranchHead>, LixError> {
125
+ BranchRefStoreReader::load_head(self, branch_id).await
126
+ }
127
+
128
+ async fn load_head_commit_id(&self, branch_id: &str) -> Result<Option<String>, LixError> {
129
+ BranchRefStoreReader::load_head_commit_id(self, branch_id).await
130
+ }
131
+
132
+ async fn scan_heads(&self) -> Result<Vec<BranchHead>, LixError> {
133
+ BranchRefStoreReader::scan_heads(self).await
134
+ }
135
+ }
136
+
137
+ /// Write side for moving branch heads.
138
+ pub(super) struct BranchRefWriter<'a> {
139
+ untracked_state: Arc<UntrackedStateContext>,
140
+ writes: &'a mut StorageWriteSet,
141
+ }
142
+
143
+ impl BranchRefWriter<'_> {
144
+ pub(crate) fn stage_rows(&mut self, rows: &[UntrackedStateRow]) -> Result<(), LixError> {
145
+ self.untracked_state
146
+ .writer(self.writes)
147
+ .stage_rows(rows.iter().map(|row| row.as_ref()))
148
+ }
149
+ }
150
+
151
+ fn decode_branch_head(
152
+ requested_branch_id: &str,
153
+ row: &MaterializedUntrackedStateRow,
154
+ ) -> Result<Option<BranchHead>, LixError> {
155
+ let Some(snapshot_content) = row.snapshot_content.as_deref() else {
156
+ return Ok(None);
157
+ };
158
+ let snapshot =
159
+ serde_json::from_str::<serde_json::Value>(snapshot_content).map_err(|error| {
160
+ LixError::new(
161
+ "LIX_ERROR_UNKNOWN",
162
+ format!("engine branch-ref snapshot parse failed: {error}"),
163
+ )
164
+ })?;
165
+ let commit_id = snapshot
166
+ .get("commit_id")
167
+ .and_then(serde_json::Value::as_str)
168
+ .ok_or_else(|| {
169
+ LixError::new(
170
+ "LIX_ERROR_UNKNOWN",
171
+ format!("branch ref for branch '{requested_branch_id}' is missing commit_id"),
172
+ )
173
+ })?;
174
+ Ok(Some(BranchHead {
175
+ branch_id: requested_branch_id.to_string(),
176
+ commit_id: commit_id.to_string(),
177
+ }))
178
+ }
179
+
180
+ #[cfg(test)]
181
+ mod tests {
182
+ use std::sync::Arc;
183
+
184
+ use crate::storage::{InMemoryStorageBackend, StorageReadOptions, StorageWriteOptions};
185
+ use crate::storage::{StorageContext, StorageWriteSet};
186
+ use crate::transaction::prepare_branch_ref_row;
187
+ use crate::untracked_state::{UntrackedStateContext, UntrackedStateRowRequest};
188
+
189
+ use super::*;
190
+
191
+ #[tokio::test]
192
+ async fn load_head_returns_none_when_missing() {
193
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
194
+ let branch_ref = test_branch_ref();
195
+ let read = storage
196
+ .begin_read(StorageReadOptions::default())
197
+ .expect("read should open");
198
+
199
+ let head = branch_ref
200
+ .reader(read)
201
+ .load_head("missing-branch")
202
+ .await
203
+ .expect("missing branch ref should load cleanly");
204
+
205
+ assert_eq!(head, None);
206
+ }
207
+
208
+ #[tokio::test]
209
+ async fn advance_head_writes_untracked_global_ref() {
210
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
211
+ let branch_ref = BranchRefContext::new(Arc::new(UntrackedStateContext::new()));
212
+
213
+ let mut writes = storage.new_write_set();
214
+ stage_branch_head(
215
+ &branch_ref,
216
+ &mut writes,
217
+ "branch-a",
218
+ "commit-a",
219
+ "2026-01-01T00:00:00Z",
220
+ )
221
+ .expect("branch head should advance");
222
+ storage
223
+ .commit_write_set(writes, StorageWriteOptions::default())
224
+ .expect("branch head should commit");
225
+
226
+ let read = storage
227
+ .begin_read(StorageReadOptions::default())
228
+ .expect("read should open");
229
+ let head = branch_ref
230
+ .reader(read)
231
+ .load_head("branch-a")
232
+ .await
233
+ .expect("branch head should load")
234
+ .expect("branch head should exist");
235
+ assert_eq!(head.branch_id, "branch-a");
236
+ assert_eq!(head.commit_id, "commit-a");
237
+
238
+ let read = storage
239
+ .begin_read(StorageReadOptions::default())
240
+ .expect("read should open");
241
+ let mut reader = UntrackedStateContext::new().reader(read);
242
+ let row = reader
243
+ .load_row(&UntrackedStateRowRequest {
244
+ schema_key: BRANCH_REF_SCHEMA_KEY.to_string(),
245
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
246
+ entity_pk: crate::entity_pk::EntityPk::single("branch-a"),
247
+ file_id: NullableKeyFilter::Null,
248
+ })
249
+ .await
250
+ .expect("branch-ref row should load")
251
+ .expect("branch-ref row should exist");
252
+ assert!(row.global);
253
+ assert_eq!(row.created_at, "2026-01-01T00:00:00Z");
254
+ assert_eq!(row.updated_at, "2026-01-01T00:00:00Z");
255
+ }
256
+
257
+ #[tokio::test]
258
+ async fn scan_heads_returns_sorted_branch_heads() {
259
+ let storage = StorageContext::new(InMemoryStorageBackend::new());
260
+ let branch_ref = test_branch_ref();
261
+
262
+ let mut writes = storage.new_write_set();
263
+ stage_branch_head(
264
+ &branch_ref,
265
+ &mut writes,
266
+ "branch-b",
267
+ "commit-b",
268
+ "2026-01-01T00:00:00Z",
269
+ )
270
+ .expect("branch-b should advance");
271
+ stage_branch_head(
272
+ &branch_ref,
273
+ &mut writes,
274
+ "branch-a",
275
+ "commit-a",
276
+ "2026-01-01T00:00:00Z",
277
+ )
278
+ .expect("branch-a should advance");
279
+ storage
280
+ .commit_write_set(writes, StorageWriteOptions::default())
281
+ .expect("branch heads should commit");
282
+
283
+ let read = storage
284
+ .begin_read(StorageReadOptions::default())
285
+ .expect("read should open");
286
+ let heads = branch_ref
287
+ .reader(read)
288
+ .scan_heads()
289
+ .await
290
+ .expect("heads should scan");
291
+
292
+ assert_eq!(
293
+ heads,
294
+ vec![
295
+ BranchHead {
296
+ branch_id: "branch-a".to_string(),
297
+ commit_id: "commit-a".to_string(),
298
+ },
299
+ BranchHead {
300
+ branch_id: "branch-b".to_string(),
301
+ commit_id: "commit-b".to_string(),
302
+ },
303
+ ]
304
+ );
305
+ }
306
+
307
+ fn test_branch_ref() -> BranchRefContext {
308
+ BranchRefContext::new(Arc::new(UntrackedStateContext::new()))
309
+ }
310
+
311
+ fn stage_branch_head(
312
+ branch_ref: &BranchRefContext,
313
+ writes: &mut StorageWriteSet,
314
+ branch_id: &str,
315
+ commit_id: &str,
316
+ timestamp: &str,
317
+ ) -> Result<(), LixError> {
318
+ let canonical_row = prepare_branch_ref_row(branch_id, commit_id, timestamp)?;
319
+ branch_ref.writer(writes).stage_rows(&[canonical_row.row])
320
+ }
321
+ }
@@ -0,0 +1,67 @@
1
+ use serde_json::json;
2
+
3
+ use crate::entity_pk::EntityPk;
4
+ use crate::transaction::types::{TransactionJson, TransactionWriteRow};
5
+ use crate::GLOBAL_BRANCH_ID;
6
+
7
+ pub(crate) const BRANCH_DESCRIPTOR_SCHEMA_KEY: &str = "lix_branch_descriptor";
8
+ pub(crate) const BRANCH_REF_SCHEMA_KEY: &str = "lix_branch_ref";
9
+
10
+ pub(crate) fn branch_descriptor_stage_row(
11
+ branch_id: &str,
12
+ name: &str,
13
+ hidden: bool,
14
+ ) -> TransactionWriteRow {
15
+ TransactionWriteRow {
16
+ entity_pk: Some(EntityPk::single(branch_id)),
17
+ schema_key: BRANCH_DESCRIPTOR_SCHEMA_KEY.to_string(),
18
+ file_id: None,
19
+ snapshot: Some(TransactionJson::from_value_unchecked(json!({
20
+ "id": branch_id,
21
+ "name": name,
22
+ "hidden": hidden,
23
+ }))),
24
+ metadata: None,
25
+ origin: None,
26
+ created_at: None,
27
+ updated_at: None,
28
+ global: true,
29
+ change_id: None,
30
+ commit_id: None,
31
+ untracked: false,
32
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
33
+ }
34
+ }
35
+
36
+ pub(crate) fn branch_ref_stage_row(branch_id: &str, commit_id: &str) -> TransactionWriteRow {
37
+ TransactionWriteRow {
38
+ entity_pk: Some(EntityPk::single(branch_id)),
39
+ schema_key: BRANCH_REF_SCHEMA_KEY.to_string(),
40
+ file_id: None,
41
+ snapshot: Some(TransactionJson::from_value_unchecked(json!({
42
+ "id": branch_id,
43
+ "commit_id": commit_id,
44
+ }))),
45
+ metadata: None,
46
+ origin: None,
47
+ created_at: None,
48
+ updated_at: None,
49
+ global: true,
50
+ change_id: None,
51
+ commit_id: None,
52
+ untracked: true,
53
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
54
+ }
55
+ }
56
+
57
+ pub(crate) fn branch_descriptor_tombstone_row(branch_id: &str) -> TransactionWriteRow {
58
+ let mut row = branch_descriptor_stage_row(branch_id, "", false);
59
+ row.snapshot = None;
60
+ row
61
+ }
62
+
63
+ pub(crate) fn branch_ref_tombstone_row(branch_id: &str) -> TransactionWriteRow {
64
+ let mut row = branch_ref_stage_row(branch_id, "");
65
+ row.snapshot = None;
66
+ row
67
+ }
@@ -0,0 +1,21 @@
1
+ /// Current changelog head for a branch.
2
+ #[derive(Debug, Clone, PartialEq, Eq)]
3
+ pub(crate) struct BranchHead {
4
+ pub(crate) branch_id: String,
5
+ pub(crate) commit_id: String,
6
+ }
7
+
8
+ /// Typed reader for moving branch heads.
9
+ #[async_trait::async_trait]
10
+ pub(crate) trait BranchRefReader: Send + Sync {
11
+ async fn load_head(&self, branch_id: &str) -> Result<Option<BranchHead>, crate::LixError>;
12
+
13
+ async fn load_head_commit_id(
14
+ &self,
15
+ branch_id: &str,
16
+ ) -> Result<Option<String>, crate::LixError> {
17
+ Ok(self.load_head(branch_id).await?.map(|head| head.commit_id))
18
+ }
19
+
20
+ async fn scan_heads(&self) -> Result<Vec<BranchHead>, crate::LixError>;
21
+ }