@lix-js/sdk 0.6.0-preview.3 → 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 (235) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +105 -65
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +30 -6
  5. package/dist/engine-wasm/wasm/lix_engine.js +187 -117
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +14 -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 +42 -28
  11. package/dist/open-lix.js +49 -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 +819 -124
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +260 -57
  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 +19 -16
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +557 -0
  104. package/dist-engine-src/src/sql2/bind/classify.rs +102 -0
  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} +98 -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 +4 -5
  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 +30 -24
  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 -109
  218. package/dist-engine-src/src/sql2/classify.rs +0 -182
  219. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  220. package/dist-engine-src/src/sql2/execute.rs +0 -3440
  221. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  222. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  223. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -166
  224. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -25
  225. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  226. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  227. package/dist-engine-src/src/storage/types.rs +0 -501
  228. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  229. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  230. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  231. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  232. package/dist-engine-src/src/version/mod.rs +0 -13
  233. package/dist-engine-src/src/version/refs.rs +0 -330
  234. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  235. package/dist-engine-src/src/version/types.rs +0 -21
@@ -1,122 +1,271 @@
1
1
  use std::collections::BTreeMap;
2
2
 
3
- use crate::live_state::{LiveStateRowIdentity, MaterializedLiveStateRow};
4
- use crate::GLOBAL_VERSION_ID;
3
+ use crate::live_state::{
4
+ LiveStateReader, LiveStateRowIdentity, LiveStateScanRequest, MaterializedLiveStateRow,
5
+ };
6
+ use crate::LixError;
7
+ use crate::GLOBAL_BRANCH_ID;
5
8
 
6
- /// Expands a version-scoped storage read so global candidates are available for
9
+ #[derive(Clone, Debug, Eq, PartialEq)]
10
+ pub(crate) struct VisibilityRequest {
11
+ pub(crate) branch_scope: VisibilityBranchScope,
12
+ pub(crate) include_tombstones: bool,
13
+ pub(crate) limit: Option<usize>,
14
+ }
15
+
16
+ #[derive(Clone, Debug, Eq, PartialEq)]
17
+ pub(crate) enum VisibilityBranchScope {
18
+ BranchIds { branch_ids: Vec<String> },
19
+ }
20
+
21
+ pub(crate) trait StagedLiveStateRows {
22
+ fn staged_rows(
23
+ &self,
24
+ request: &LiveStateScanRequest,
25
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError>;
26
+ }
27
+
28
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29
+ enum OverlayTier {
30
+ BaseGlobal,
31
+ StagedGlobal,
32
+ BaseBranch,
33
+ StagedBranch,
34
+ }
35
+
36
+ /// Expands a branch-scoped storage read so global candidates are available for
7
37
  /// the visibility overlay.
8
- pub(crate) fn expanded_version_ids(version_ids: &[String]) -> Vec<String> {
9
- if version_ids.is_empty() {
38
+ pub(crate) fn expanded_branch_ids(branch_ids: &[String]) -> Vec<String> {
39
+ if branch_ids.is_empty() {
10
40
  return Vec::new();
11
41
  }
12
42
 
13
- let mut expanded = version_ids.to_vec();
14
- if version_ids
43
+ let mut expanded = branch_ids.to_vec();
44
+ if branch_ids
15
45
  .iter()
16
- .any(|version_id| version_id != GLOBAL_VERSION_ID)
46
+ .any(|branch_id| branch_id != GLOBAL_BRANCH_ID)
17
47
  && !expanded
18
48
  .iter()
19
- .any(|version_id| version_id == GLOBAL_VERSION_ID)
49
+ .any(|branch_id| branch_id == GLOBAL_BRANCH_ID)
20
50
  {
21
- expanded.push(GLOBAL_VERSION_ID.to_string());
51
+ expanded.push(GLOBAL_BRANCH_ID.to_string());
22
52
  }
23
53
  expanded
24
54
  }
25
55
 
56
+ pub(crate) fn resolve_visible_rows(
57
+ base_rows: Vec<MaterializedLiveStateRow>,
58
+ staged_rows: Vec<MaterializedLiveStateRow>,
59
+ request: &VisibilityRequest,
60
+ ) -> Vec<MaterializedLiveStateRow> {
61
+ let requested_branch_ids = requested_branch_ids(&request.branch_scope);
62
+ resolve_live_state_rows(
63
+ base_rows,
64
+ staged_rows,
65
+ &requested_branch_ids,
66
+ request.include_tombstones,
67
+ request.limit,
68
+ )
69
+ }
70
+
71
+ pub(crate) async fn overlay_scan_rows<S>(
72
+ base: &dyn LiveStateReader,
73
+ staged: &S,
74
+ request: &LiveStateScanRequest,
75
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError>
76
+ where
77
+ S: StagedLiveStateRows + ?Sized,
78
+ {
79
+ let mut candidate_request = request.clone();
80
+ candidate_request.limit = None;
81
+ candidate_request.filter.include_tombstones = true;
82
+ candidate_request.filter.branch_ids = expanded_branch_ids(&request.filter.branch_ids);
83
+ let staged_rows = staged.staged_rows(&candidate_request)?;
84
+ let rows = base.scan_rows(&candidate_request).await?;
85
+ Ok(resolve_visible_rows(
86
+ rows,
87
+ staged_rows,
88
+ &VisibilityRequest {
89
+ branch_scope: VisibilityBranchScope::BranchIds {
90
+ branch_ids: request.filter.branch_ids.clone(),
91
+ },
92
+ include_tombstones: request.filter.include_tombstones,
93
+ limit: request.limit,
94
+ },
95
+ ))
96
+ }
97
+
26
98
  /// Resolves raw tracked/untracked candidates into the rows visible for a scan.
27
99
  ///
28
- /// Global rows are projected into each requested version scope, but keep
29
- /// `global = true`. Version-scoped rows win over projected global rows for the
100
+ /// Global rows are projected into each requested branch scope, but keep
101
+ /// `global = true`. Branch-scoped rows win over projected global rows for the
30
102
  /// same identity. Tombstones participate in winning and are filtered only after
31
103
  /// visibility is resolved. This projection is a read concern; constraint
32
104
  /// validation remains exact storage-scope local unless a validator explicitly
33
105
  /// opts into overlay semantics.
34
- pub(crate) fn resolve_scan_rows(
106
+ fn resolve_scan_rows(
35
107
  rows: Vec<MaterializedLiveStateRow>,
36
- requested_version_ids: &[String],
108
+ requested_branch_ids: &[String],
109
+ include_tombstones: bool,
110
+ ) -> Vec<MaterializedLiveStateRow> {
111
+ let mut rows = project_global_rows_into_requested_branches(rows, requested_branch_ids);
112
+ if !include_tombstones {
113
+ rows.retain(|row| !row.deleted);
114
+ }
115
+ rows
116
+ }
117
+
118
+ fn resolve_live_state_rows(
119
+ base_rows: Vec<MaterializedLiveStateRow>,
120
+ staged_rows: Vec<MaterializedLiveStateRow>,
121
+ requested_branch_ids: &[String],
37
122
  include_tombstones: bool,
123
+ limit: Option<usize>,
38
124
  ) -> Vec<MaterializedLiveStateRow> {
39
- let mut rows = project_global_rows_into_requested_versions(rows, requested_version_ids);
125
+ let base_rows = resolve_scan_rows(base_rows, requested_branch_ids, true);
126
+ let staged_rows = resolve_scan_rows(staged_rows, requested_branch_ids, true);
127
+ let mut rows_by_identity =
128
+ BTreeMap::<LiveStateRowIdentity, (OverlayTier, MaterializedLiveStateRow)>::new();
129
+
130
+ for row in base_rows {
131
+ let tier = if row.global {
132
+ OverlayTier::BaseGlobal
133
+ } else {
134
+ OverlayTier::BaseBranch
135
+ };
136
+ insert_overlay_row(&mut rows_by_identity, tier, row);
137
+ }
138
+ for row in staged_rows {
139
+ let tier = if row.global {
140
+ OverlayTier::StagedGlobal
141
+ } else {
142
+ OverlayTier::StagedBranch
143
+ };
144
+ insert_overlay_row(&mut rows_by_identity, tier, row);
145
+ }
146
+
147
+ let mut rows = rows_by_identity
148
+ .into_values()
149
+ .map(|(_, row)| row)
150
+ .collect::<Vec<_>>();
40
151
  if !include_tombstones {
41
152
  rows.retain(|row| !row.deleted);
42
153
  }
154
+ if let Some(limit) = limit {
155
+ rows.truncate(limit);
156
+ }
43
157
  rows
44
158
  }
45
159
 
46
- /// Resolves a row loaded through a concrete storage version into the row visible
47
- /// to the requested version scope.
48
- pub(crate) fn project_loaded_row(
49
- mut row: MaterializedLiveStateRow,
50
- requested_version_id: &str,
51
- matched_version_id: &str,
52
- ) -> MaterializedLiveStateRow {
53
- if row.global && requested_version_id != GLOBAL_VERSION_ID {
54
- row.version_id = requested_version_id.to_string();
55
- } else if matched_version_id == GLOBAL_VERSION_ID && requested_version_id != GLOBAL_VERSION_ID {
56
- row.version_id = requested_version_id.to_string();
57
- }
58
- row
160
+ fn requested_branch_ids(branch_scope: &VisibilityBranchScope) -> Vec<String> {
161
+ match branch_scope {
162
+ VisibilityBranchScope::BranchIds { branch_ids } => branch_ids.clone(),
163
+ }
59
164
  }
60
165
 
61
- fn project_global_rows_into_requested_versions(
166
+ fn project_global_rows_into_requested_branches(
62
167
  rows: Vec<MaterializedLiveStateRow>,
63
- requested_version_ids: &[String],
168
+ requested_branch_ids: &[String],
64
169
  ) -> Vec<MaterializedLiveStateRow> {
65
- if requested_version_ids.is_empty() {
66
- return rows;
170
+ if requested_branch_ids.is_empty() {
171
+ return dedupe_rows(rows);
67
172
  }
68
173
 
69
174
  let mut rows_by_identity = BTreeMap::<LiveStateRowIdentity, MaterializedLiveStateRow>::new();
70
- for requested_version_id in requested_version_ids {
175
+ for requested_branch_id in requested_branch_ids {
71
176
  for row in &rows {
72
- if row.version_id == GLOBAL_VERSION_ID {
177
+ if row.branch_id == GLOBAL_BRANCH_ID {
73
178
  let mut projected = row.clone();
74
- projected.version_id = requested_version_id.clone();
75
- rows_by_identity.insert(LiveStateRowIdentity::from_row(&projected), projected);
179
+ projected.branch_id = requested_branch_id.clone();
180
+ insert_row_preferring_untracked(&mut rows_by_identity, projected);
76
181
  }
77
182
  }
183
+ let mut branch_rows_by_identity =
184
+ BTreeMap::<LiveStateRowIdentity, MaterializedLiveStateRow>::new();
78
185
  for row in rows
79
186
  .iter()
80
- .filter(|row| row.version_id == *requested_version_id)
187
+ .filter(|row| row.branch_id == *requested_branch_id)
81
188
  {
82
- rows_by_identity.insert(LiveStateRowIdentity::from_row(row), row.clone());
189
+ insert_row_preferring_untracked(&mut branch_rows_by_identity, row.clone());
83
190
  }
191
+ rows_by_identity.extend(branch_rows_by_identity);
84
192
  }
85
193
 
86
194
  rows_by_identity.into_values().collect()
87
195
  }
88
196
 
197
+ fn dedupe_rows(rows: Vec<MaterializedLiveStateRow>) -> Vec<MaterializedLiveStateRow> {
198
+ let mut rows_by_identity = BTreeMap::<LiveStateRowIdentity, MaterializedLiveStateRow>::new();
199
+ for row in rows {
200
+ insert_row_preferring_untracked(&mut rows_by_identity, row);
201
+ }
202
+ rows_by_identity.into_values().collect()
203
+ }
204
+
205
+ fn insert_overlay_row(
206
+ rows_by_identity: &mut BTreeMap<LiveStateRowIdentity, (OverlayTier, MaterializedLiveStateRow)>,
207
+ tier: OverlayTier,
208
+ row: MaterializedLiveStateRow,
209
+ ) {
210
+ let identity = LiveStateRowIdentity::from_row(&row);
211
+ match rows_by_identity.get(&identity) {
212
+ Some((existing_tier, _)) if *existing_tier > tier => {}
213
+ Some((existing_tier, existing))
214
+ if *existing_tier == tier && existing.untracked && !row.untracked => {}
215
+ _ => {
216
+ rows_by_identity.insert(identity, (tier, row));
217
+ }
218
+ }
219
+ }
220
+
221
+ fn insert_row_preferring_untracked(
222
+ rows_by_identity: &mut BTreeMap<LiveStateRowIdentity, MaterializedLiveStateRow>,
223
+ row: MaterializedLiveStateRow,
224
+ ) {
225
+ let identity = LiveStateRowIdentity::from_row(&row);
226
+ match rows_by_identity.get(&identity) {
227
+ Some(existing) if existing.untracked && !row.untracked => {}
228
+ _ => {
229
+ rows_by_identity.insert(identity, row);
230
+ }
231
+ }
232
+ }
233
+
89
234
  #[cfg(test)]
90
235
  mod tests {
91
236
  use super::*;
237
+ use crate::entity_pk::EntityPk;
238
+ use crate::live_state::LiveStateRowRequest;
239
+ use async_trait::async_trait;
92
240
 
93
241
  #[test]
94
- fn expands_requested_version_with_global_candidates() {
242
+ fn expands_requested_branch_with_global_candidates() {
95
243
  assert_eq!(
96
- expanded_version_ids(&["version-a".to_string()]),
97
- vec!["version-a".to_string(), "global".to_string()]
244
+ expanded_branch_ids(&["branch-a".to_string()]),
245
+ vec!["branch-a".to_string(), "global".to_string()]
98
246
  );
99
247
  assert_eq!(
100
- expanded_version_ids(&["global".to_string()]),
248
+ expanded_branch_ids(&["global".to_string()]),
101
249
  vec!["global".to_string()]
102
250
  );
103
251
  }
104
252
 
105
253
  #[test]
106
- fn scan_projects_global_row_into_requested_version() {
254
+ fn committed_scan_projects_global_row_into_requested_branch() {
107
255
  let rows = resolve_scan_rows(
108
256
  vec![row_at(
109
257
  "global",
258
+ "entity",
110
259
  "global-value",
111
260
  true,
112
261
  Some("change-global"),
113
262
  )],
114
- &["version-a".to_string()],
263
+ &["branch-a".to_string()],
115
264
  false,
116
265
  );
117
266
 
118
267
  assert_eq!(rows.len(), 1);
119
- assert_eq!(rows[0].version_id, "version-a");
268
+ assert_eq!(rows[0].branch_id, "branch-a");
120
269
  assert!(rows[0].global);
121
270
  assert_eq!(
122
271
  rows[0].snapshot_content.as_deref(),
@@ -125,75 +274,327 @@ mod tests {
125
274
  }
126
275
 
127
276
  #[test]
128
- fn scan_prefers_requested_version_row_over_projected_global_row() {
277
+ fn committed_scan_prefers_requested_branch_row_over_projected_global_row() {
129
278
  let rows = resolve_scan_rows(
130
279
  vec![
131
- row_at("global", "global-value", true, Some("change-global")),
132
- row_at("version-a", "version-value", false, Some("change-version")),
280
+ row_at(
281
+ "global",
282
+ "entity",
283
+ "global-value",
284
+ true,
285
+ Some("change-global"),
286
+ ),
287
+ row_at(
288
+ "branch-a",
289
+ "entity",
290
+ "branch-value",
291
+ false,
292
+ Some("change-branch"),
293
+ ),
133
294
  ],
134
- &["version-a".to_string()],
295
+ &["branch-a".to_string()],
135
296
  false,
136
297
  );
137
298
 
138
299
  assert_eq!(rows.len(), 1);
139
- assert_eq!(rows[0].version_id, "version-a");
300
+ assert_eq!(rows[0].branch_id, "branch-a");
140
301
  assert!(!rows[0].global);
141
302
  assert_eq!(
142
303
  rows[0].snapshot_content.as_deref(),
143
- Some("{\"value\":\"version-value\"}")
304
+ Some("{\"value\":\"branch-value\"}")
144
305
  );
145
306
  }
146
307
 
147
308
  #[test]
148
- fn version_tombstone_hides_global_row_after_visibility_resolution() {
309
+ fn empty_branch_filter_dedupes_duplicate_base_rows() {
310
+ let mut tracked = row_at(
311
+ "branch-a",
312
+ "entity",
313
+ "tracked",
314
+ false,
315
+ Some("change-tracked"),
316
+ );
317
+ tracked.untracked = false;
318
+ let mut untracked = row_at("branch-a", "entity", "untracked", false, None);
319
+ untracked.untracked = true;
320
+
321
+ let rows = resolve_scan_rows(vec![tracked, untracked], &[], false);
322
+
323
+ assert_eq!(rows.len(), 1);
324
+ assert!(rows[0].untracked);
325
+ assert_eq!(
326
+ rows[0].snapshot_content.as_deref(),
327
+ Some("{\"value\":\"untracked\"}")
328
+ );
329
+ }
330
+
331
+ #[test]
332
+ fn empty_branch_filter_dedupes_duplicate_base_and_staged_overlay_identity() {
333
+ let base = row_at("branch-a", "entity", "base", false, Some("change-base"));
334
+ let staged = row_at("branch-a", "entity", "staged", false, Some("change-staged"));
335
+
336
+ let rows = resolve_live_state_rows(vec![base], vec![staged], &[], false, None);
337
+
338
+ assert_eq!(rows.len(), 1);
339
+ assert_eq!(
340
+ rows[0].snapshot_content.as_deref(),
341
+ Some("{\"value\":\"staged\"}")
342
+ );
343
+ }
344
+
345
+ #[test]
346
+ fn branch_tombstone_hides_global_row_after_visibility_resolution() {
149
347
  let rows = resolve_scan_rows(
150
348
  vec![
151
- row_at("global", "global-value", true, Some("change-global")),
152
- tombstone_at("version-a", false, Some("change-tombstone")),
349
+ row_at(
350
+ "global",
351
+ "entity",
352
+ "global-value",
353
+ true,
354
+ Some("change-global"),
355
+ ),
356
+ tombstone_at("branch-a", "entity", false, Some("change-tombstone")),
153
357
  ],
154
- &["version-a".to_string()],
358
+ &["branch-a".to_string()],
155
359
  false,
156
360
  );
157
361
 
158
362
  assert!(rows.is_empty());
159
363
  }
160
364
 
365
+ #[test]
366
+ fn overlay_prefers_staged_untracked_over_staged_tracked_for_same_visible_identity() {
367
+ let mut tracked = row_at(
368
+ "branch-a",
369
+ "entity",
370
+ "tracked",
371
+ false,
372
+ Some("change-tracked"),
373
+ );
374
+ tracked.untracked = false;
375
+ let mut untracked = row_at("branch-a", "entity", "untracked", false, None);
376
+ untracked.untracked = true;
377
+
378
+ let rows = resolve_live_state_rows(
379
+ Vec::new(),
380
+ vec![untracked.clone(), tracked.clone()],
381
+ &["branch-a".to_string()],
382
+ false,
383
+ None,
384
+ );
385
+
386
+ assert_eq!(rows.len(), 1);
387
+ assert!(rows[0].untracked);
388
+ assert_eq!(
389
+ rows[0].snapshot_content.as_deref(),
390
+ Some("{\"value\":\"untracked\"}")
391
+ );
392
+
393
+ let rows = resolve_live_state_rows(
394
+ Vec::new(),
395
+ vec![tracked, untracked],
396
+ &["branch-a".to_string()],
397
+ false,
398
+ None,
399
+ );
400
+
401
+ assert_eq!(rows.len(), 1);
402
+ assert!(rows[0].untracked);
403
+ assert_eq!(
404
+ rows[0].snapshot_content.as_deref(),
405
+ Some("{\"value\":\"untracked\"}")
406
+ );
407
+ }
408
+
409
+ #[test]
410
+ fn overlay_prefers_staged_tracked_over_base_untracked_for_same_visible_identity() {
411
+ let mut base = row_at("branch-a", "entity", "base-untracked", false, None);
412
+ base.untracked = true;
413
+ let mut staged = row_at(
414
+ "branch-a",
415
+ "entity",
416
+ "staged-tracked",
417
+ false,
418
+ Some("change-staged"),
419
+ );
420
+ staged.untracked = false;
421
+
422
+ let rows = resolve_live_state_rows(
423
+ vec![base],
424
+ vec![staged],
425
+ &["branch-a".to_string()],
426
+ false,
427
+ None,
428
+ );
429
+
430
+ assert_eq!(rows.len(), 1);
431
+ assert!(!rows[0].untracked);
432
+ assert_eq!(
433
+ rows[0].snapshot_content.as_deref(),
434
+ Some("{\"value\":\"staged-tracked\"}")
435
+ );
436
+ }
437
+
438
+ #[test]
439
+ fn staged_global_tombstone_hides_projected_base_global_row() {
440
+ let mut base = row_at("branch-a", "entity", "base", true, Some("change-base"));
441
+ base.global = true;
442
+
443
+ let rows = resolve_live_state_rows(
444
+ vec![base],
445
+ vec![tombstone_at(
446
+ "global",
447
+ "entity",
448
+ true,
449
+ Some("change-staged"),
450
+ )],
451
+ &["branch-a".to_string()],
452
+ false,
453
+ None,
454
+ );
455
+
456
+ assert!(rows.is_empty());
457
+ }
458
+
459
+ #[test]
460
+ fn base_branch_tombstone_hides_staged_global_row() {
461
+ let base = tombstone_at("branch-a", "entity", false, Some("change-base"));
462
+ let staged = row_at("global", "entity", "staged", true, Some("change-staged"));
463
+
464
+ let rows = resolve_live_state_rows(
465
+ vec![base],
466
+ vec![staged],
467
+ &["branch-a".to_string()],
468
+ false,
469
+ None,
470
+ );
471
+
472
+ assert!(rows.is_empty());
473
+ }
474
+
475
+ #[test]
476
+ fn base_tracked_branch_tombstone_hides_staged_untracked_global_row() {
477
+ let mut base = tombstone_at("branch-a", "entity", false, Some("change-base"));
478
+ base.untracked = false;
479
+ let mut staged = row_at("global", "entity", "staged", true, None);
480
+ staged.untracked = true;
481
+
482
+ let rows = resolve_live_state_rows(
483
+ vec![base],
484
+ vec![staged],
485
+ &["branch-a".to_string()],
486
+ false,
487
+ None,
488
+ );
489
+
490
+ assert!(rows.is_empty());
491
+ }
492
+
493
+ #[test]
494
+ fn staged_branch_row_overrides_base_branch_tombstone() {
495
+ let base = tombstone_at("branch-a", "entity", false, Some("change-base"));
496
+ let staged = row_at("branch-a", "entity", "staged", false, Some("change-staged"));
497
+
498
+ let rows = resolve_live_state_rows(
499
+ vec![base],
500
+ vec![staged],
501
+ &["branch-a".to_string()],
502
+ false,
503
+ None,
504
+ );
505
+
506
+ assert_eq!(rows.len(), 1);
507
+ assert!(!rows[0].deleted);
508
+ }
509
+
161
510
  #[test]
162
511
  fn tombstone_can_be_returned_when_requested() {
163
512
  let rows = resolve_scan_rows(
164
513
  vec![
165
- row_at("global", "global-value", true, Some("change-global")),
166
- tombstone_at("version-a", false, Some("change-tombstone")),
514
+ row_at(
515
+ "global",
516
+ "entity",
517
+ "global-value",
518
+ true,
519
+ Some("change-global"),
520
+ ),
521
+ tombstone_at("branch-a", "entity", false, Some("change-tombstone")),
167
522
  ],
168
- &["version-a".to_string()],
523
+ &["branch-a".to_string()],
169
524
  true,
170
525
  );
171
526
 
172
527
  assert_eq!(rows.len(), 1);
173
- assert_eq!(rows[0].version_id, "version-a");
528
+ assert_eq!(rows[0].branch_id, "branch-a");
174
529
  assert_eq!(rows[0].snapshot_content, None);
175
530
  }
176
531
 
177
532
  #[test]
178
- fn loaded_global_row_is_projected_into_requested_version() {
179
- let row = project_loaded_row(
180
- row_at("global", "global-value", true, Some("change-global")),
181
- "version-a",
182
- "global",
533
+ fn resolve_visible_rows_maps_branch_scope_and_applies_limit() {
534
+ let request = VisibilityRequest {
535
+ branch_scope: VisibilityBranchScope::BranchIds {
536
+ branch_ids: vec!["branch-a".to_string()],
537
+ },
538
+ include_tombstones: false,
539
+ limit: Some(1),
540
+ };
541
+ let rows = resolve_visible_rows(
542
+ vec![
543
+ row_at("branch-a", "a", "A", false, Some("change-a")),
544
+ row_at("branch-a", "b", "B", false, Some("change-b")),
545
+ ],
546
+ Vec::new(),
547
+ &request,
183
548
  );
184
549
 
185
- assert_eq!(row.version_id, "version-a");
186
- assert!(row.global);
550
+ assert_eq!(rows.len(), 1);
551
+ }
552
+
553
+ #[tokio::test]
554
+ async fn overlay_scan_fetches_base_global_candidates_for_staged_only_branch_scope() {
555
+ let base = ExistingGlobalOnlyReader {
556
+ rows: vec![row_at(
557
+ "global",
558
+ "entity",
559
+ "global-value",
560
+ true,
561
+ Some("change-global"),
562
+ )],
563
+ };
564
+ let staged = EmptyStagedRows;
565
+
566
+ let rows = overlay_scan_rows(
567
+ &base,
568
+ &staged,
569
+ &LiveStateScanRequest {
570
+ filter: crate::live_state::LiveStateFilter {
571
+ branch_ids: vec!["staged-branch".to_string()],
572
+ ..Default::default()
573
+ },
574
+ ..Default::default()
575
+ },
576
+ )
577
+ .await
578
+ .expect("overlay scan should succeed");
579
+
580
+ assert_eq!(rows.len(), 1);
581
+ assert_eq!(rows[0].branch_id, "staged-branch");
582
+ assert!(rows[0].global);
583
+ assert_eq!(
584
+ rows[0].snapshot_content.as_deref(),
585
+ Some("{\"value\":\"global-value\"}")
586
+ );
187
587
  }
188
588
 
189
589
  fn row_at(
190
- version_id: &str,
590
+ branch_id: &str,
591
+ entity_pk: &str,
191
592
  value: &str,
192
593
  global: bool,
193
594
  change_id: Option<&str>,
194
595
  ) -> MaterializedLiveStateRow {
195
596
  MaterializedLiveStateRow {
196
- entity_id: crate::entity_identity::EntityIdentity::single("entity"),
597
+ entity_pk: EntityPk::single(entity_pk),
197
598
  schema_key: "schema".to_string(),
198
599
  file_id: None,
199
600
  snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
@@ -205,19 +606,61 @@ mod tests {
205
606
  change_id: change_id.map(str::to_string),
206
607
  commit_id: Some("commit".to_string()),
207
608
  untracked: false,
208
- version_id: version_id.to_string(),
609
+ branch_id: branch_id.to_string(),
209
610
  }
210
611
  }
211
612
 
212
613
  fn tombstone_at(
213
- version_id: &str,
614
+ branch_id: &str,
615
+ entity_pk: &str,
214
616
  global: bool,
215
617
  change_id: Option<&str>,
216
618
  ) -> MaterializedLiveStateRow {
217
619
  MaterializedLiveStateRow {
218
620
  snapshot_content: None,
219
621
  deleted: true,
220
- ..row_at(version_id, "ignored", global, change_id)
622
+ ..row_at(branch_id, entity_pk, "ignored", global, change_id)
623
+ }
624
+ }
625
+
626
+ struct EmptyStagedRows;
627
+
628
+ impl StagedLiveStateRows for EmptyStagedRows {
629
+ fn staged_rows(
630
+ &self,
631
+ _request: &LiveStateScanRequest,
632
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
633
+ Ok(Vec::new())
634
+ }
635
+ }
636
+
637
+ struct ExistingGlobalOnlyReader {
638
+ rows: Vec<MaterializedLiveStateRow>,
639
+ }
640
+
641
+ #[async_trait]
642
+ impl LiveStateReader for ExistingGlobalOnlyReader {
643
+ async fn scan_rows(
644
+ &self,
645
+ request: &LiveStateScanRequest,
646
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
647
+ if request
648
+ .filter
649
+ .branch_ids
650
+ .iter()
651
+ .any(|branch_id| branch_id == GLOBAL_BRANCH_ID)
652
+ {
653
+ Ok(self.rows.clone())
654
+ } else {
655
+ Ok(Vec::new())
656
+ }
657
+ }
658
+
659
+ async fn load_row(
660
+ &self,
661
+ _request: &LiveStateRowRequest,
662
+ ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
663
+ Ok(None)
221
664
  }
222
665
  }
223
666
  }