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

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 (196) hide show
  1. package/README.md +9 -0
  2. package/SKILL.md +468 -0
  3. package/dist/engine-wasm/index.d.ts +15 -11
  4. package/dist/engine-wasm/index.js +105 -38
  5. package/dist/engine-wasm/wasm/lix_engine.d.ts +14 -2
  6. package/dist/engine-wasm/wasm/lix_engine.js +18 -17
  7. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  8. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +2 -1
  9. package/dist/generated/builtin-schemas.d.ts +31 -41
  10. package/dist/generated/builtin-schemas.js +52 -56
  11. package/dist/open-lix.d.ts +141 -24
  12. package/dist/open-lix.js +199 -35
  13. package/dist/sqlite/index.js +99 -22
  14. package/dist-engine-src/README.md +18 -0
  15. package/dist-engine-src/src/backend/kv.rs +358 -0
  16. package/dist-engine-src/src/backend/mod.rs +12 -0
  17. package/dist-engine-src/src/backend/testing.rs +658 -0
  18. package/dist-engine-src/src/backend/types.rs +96 -0
  19. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  20. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  21. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  22. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  23. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  24. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  25. package/dist-engine-src/src/cel/context.rs +86 -0
  26. package/dist-engine-src/src/cel/error.rs +19 -0
  27. package/dist-engine-src/src/cel/mod.rs +8 -0
  28. package/dist-engine-src/src/cel/provider.rs +9 -0
  29. package/dist-engine-src/src/cel/runtime.rs +167 -0
  30. package/dist-engine-src/src/cel/value.rs +50 -0
  31. package/dist-engine-src/src/changelog/codec.rs +321 -0
  32. package/dist-engine-src/src/changelog/context.rs +92 -0
  33. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  34. package/dist-engine-src/src/changelog/mod.rs +13 -0
  35. package/dist-engine-src/src/changelog/reader.rs +20 -0
  36. package/dist-engine-src/src/changelog/storage.rs +220 -0
  37. package/dist-engine-src/src/changelog/types.rs +38 -0
  38. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  39. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  40. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  41. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  42. package/dist-engine-src/src/common/error.rs +313 -0
  43. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  44. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  45. package/dist-engine-src/src/common/identity.rs +135 -0
  46. package/dist-engine-src/src/common/metadata.rs +35 -0
  47. package/dist-engine-src/src/common/mod.rs +23 -0
  48. package/dist-engine-src/src/common/types.rs +105 -0
  49. package/dist-engine-src/src/common/wire.rs +222 -0
  50. package/dist-engine-src/src/engine.rs +239 -0
  51. package/dist-engine-src/src/entity_identity.rs +285 -0
  52. package/dist-engine-src/src/functions/context.rs +327 -0
  53. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  54. package/dist-engine-src/src/functions/mod.rs +18 -0
  55. package/dist-engine-src/src/functions/provider.rs +130 -0
  56. package/dist-engine-src/src/functions/state.rs +363 -0
  57. package/dist-engine-src/src/functions/types.rs +37 -0
  58. package/dist-engine-src/src/init.rs +505 -0
  59. package/dist-engine-src/src/json_store/compression.rs +77 -0
  60. package/dist-engine-src/src/json_store/context.rs +129 -0
  61. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  62. package/dist-engine-src/src/json_store/mod.rs +9 -0
  63. package/dist-engine-src/src/json_store/store.rs +236 -0
  64. package/dist-engine-src/src/json_store/types.rs +52 -0
  65. package/dist-engine-src/src/lib.rs +61 -0
  66. package/dist-engine-src/src/live_state/context.rs +2241 -0
  67. package/dist-engine-src/src/live_state/mod.rs +15 -0
  68. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  69. package/dist-engine-src/src/live_state/reader.rs +23 -0
  70. package/dist-engine-src/src/live_state/types.rs +239 -0
  71. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  72. package/dist-engine-src/src/plugin/archive.rs +441 -0
  73. package/dist-engine-src/src/plugin/component.rs +183 -0
  74. package/dist-engine-src/src/plugin/install.rs +637 -0
  75. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  76. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  77. package/dist-engine-src/src/plugin/mod.rs +33 -0
  78. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  79. package/dist-engine-src/src/plugin/storage.rs +74 -0
  80. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  81. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  82. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  83. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  84. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  85. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  86. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  87. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  88. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  89. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  90. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  91. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  92. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  93. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  94. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  95. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  96. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  97. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  98. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  99. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  100. package/dist-engine-src/src/schema/definition.json +157 -0
  101. package/dist-engine-src/src/schema/definition.rs +636 -0
  102. package/dist-engine-src/src/schema/key.rs +206 -0
  103. package/dist-engine-src/src/schema/mod.rs +20 -0
  104. package/dist-engine-src/src/schema/seed.rs +14 -0
  105. package/dist-engine-src/src/schema/tests.rs +739 -0
  106. package/dist-engine-src/src/schema_registry.rs +294 -0
  107. package/dist-engine-src/src/session/context.rs +366 -0
  108. package/dist-engine-src/src/session/create_version.rs +80 -0
  109. package/dist-engine-src/src/session/execute.rs +447 -0
  110. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  111. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  112. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  113. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  114. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  115. package/dist-engine-src/src/session/merge/version.rs +437 -0
  116. package/dist-engine-src/src/session/mod.rs +25 -0
  117. package/dist-engine-src/src/session/switch_version.rs +121 -0
  118. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  119. package/dist-engine-src/src/sql2/classify.rs +147 -0
  120. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  121. package/dist-engine-src/src/sql2/context.rs +307 -0
  122. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  123. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  124. package/dist-engine-src/src/sql2/dml.rs +148 -0
  125. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  126. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  127. package/dist-engine-src/src/sql2/error.rs +196 -0
  128. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  129. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  130. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  131. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  132. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  133. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  134. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  135. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  136. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  137. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  138. package/dist-engine-src/src/sql2/mod.rs +43 -0
  139. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  140. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  141. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  142. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  143. package/dist-engine-src/src/sql2/session.rs +135 -0
  144. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  148. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  149. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  150. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  151. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  152. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  153. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  154. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  155. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  156. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  157. package/dist-engine-src/src/storage/context.rs +356 -0
  158. package/dist-engine-src/src/storage/mod.rs +14 -0
  159. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  160. package/dist-engine-src/src/storage/types.rs +501 -0
  161. package/dist-engine-src/src/storage_bench.rs +3406 -0
  162. package/dist-engine-src/src/test_support.rs +81 -0
  163. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  164. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  165. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  166. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  167. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  168. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  169. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  170. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  171. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  172. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  173. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  174. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  175. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  176. package/dist-engine-src/src/transaction/context.rs +1307 -0
  177. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  178. package/dist-engine-src/src/transaction/mod.rs +11 -0
  179. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  180. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  181. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  182. package/dist-engine-src/src/transaction/types.rs +351 -0
  183. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  184. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  185. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  186. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  187. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  188. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  189. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  190. package/dist-engine-src/src/version/context.rs +52 -0
  191. package/dist-engine-src/src/version/mod.rs +12 -0
  192. package/dist-engine-src/src/version/refs.rs +421 -0
  193. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  194. package/dist-engine-src/src/version/types.rs +21 -0
  195. package/dist-engine-src/src/wasm/mod.rs +60 -0
  196. package/package.json +68 -63
@@ -0,0 +1,394 @@
1
+ use std::collections::BTreeSet;
2
+
3
+ use datafusion::error::DataFusionError;
4
+ use datafusion::logical_expr::expr::InList;
5
+ use datafusion::logical_expr::{BinaryExpr, Expr, Operator};
6
+ use datafusion::scalar::ScalarValue;
7
+
8
+ use crate::version::VersionRefReader;
9
+ use crate::LixError;
10
+ use crate::GLOBAL_VERSION_ID;
11
+
12
+ /// Version scope requested by a SQL surface.
13
+ ///
14
+ /// Active surfaces read through one session version. By-version surfaces either
15
+ /// read explicitly filtered versions or, without a version predicate, enumerate
16
+ /// every visible version scope before handing the request to live_state.
17
+ pub(crate) enum SqlVersionScope {
18
+ Active(String),
19
+ Explicit(Vec<String>),
20
+ AllVisible,
21
+ }
22
+
23
+ #[derive(Debug, Clone, PartialEq, Eq)]
24
+ pub(crate) enum VersionBinding {
25
+ Active { version_id: String },
26
+ Explicit,
27
+ }
28
+
29
+ #[derive(Debug, Clone, PartialEq, Eq)]
30
+ pub(crate) struct WriteVersionScope {
31
+ pub(crate) version_id: String,
32
+ pub(crate) global: bool,
33
+ }
34
+
35
+ impl VersionBinding {
36
+ pub(crate) fn active(version_id: impl Into<String>) -> Self {
37
+ Self::Active {
38
+ version_id: version_id.into(),
39
+ }
40
+ }
41
+
42
+ pub(crate) fn explicit() -> Self {
43
+ Self::Explicit
44
+ }
45
+
46
+ pub(crate) fn active_version_id(&self) -> Option<&str> {
47
+ match self {
48
+ Self::Active { version_id } => Some(version_id),
49
+ Self::Explicit => None,
50
+ }
51
+ }
52
+
53
+ pub(crate) fn require_active_version_id(&self, operation: &str) -> Result<String, LixError> {
54
+ match self {
55
+ Self::Active { version_id } => Ok(version_id.clone()),
56
+ Self::Explicit => Err(LixError::new(
57
+ "LIX_ERROR_UNKNOWN",
58
+ format!("{operation} is only supported for active-version SQL surfaces"),
59
+ )),
60
+ }
61
+ }
62
+ }
63
+
64
+ pub(crate) fn resolve_write_version_scope(
65
+ explicit_global: Option<bool>,
66
+ explicit_version_id: Option<String>,
67
+ fallback_version_id: Option<&str>,
68
+ operation: &str,
69
+ surface: &str,
70
+ ) -> Result<WriteVersionScope, DataFusionError> {
71
+ if explicit_global == Some(true) {
72
+ if explicit_version_id
73
+ .as_deref()
74
+ .is_some_and(|version_id| version_id != GLOBAL_VERSION_ID)
75
+ {
76
+ return Err(DataFusionError::Execution(format!(
77
+ "{surface} cannot set lixcol_global=true with non-global lixcol_version_id"
78
+ )));
79
+ }
80
+ return Ok(WriteVersionScope {
81
+ version_id: GLOBAL_VERSION_ID.to_string(),
82
+ global: true,
83
+ });
84
+ }
85
+
86
+ let version_id = explicit_version_id
87
+ .or_else(|| fallback_version_id.map(ToOwned::to_owned))
88
+ .ok_or_else(|| {
89
+ DataFusionError::Execution(format!("{operation} requires lixcol_version_id"))
90
+ })?;
91
+ if explicit_global == Some(false) && version_id == GLOBAL_VERSION_ID {
92
+ return Err(DataFusionError::Execution(format!(
93
+ "{surface} cannot set lixcol_global=false with global lixcol_version_id"
94
+ )));
95
+ }
96
+ Ok(WriteVersionScope {
97
+ global: explicit_global.unwrap_or(version_id == GLOBAL_VERSION_ID),
98
+ version_id,
99
+ })
100
+ }
101
+
102
+ impl SqlVersionScope {
103
+ pub(crate) fn from_provider(
104
+ binding: &VersionBinding,
105
+ requested_version_ids: Vec<String>,
106
+ ) -> Self {
107
+ match binding {
108
+ VersionBinding::Active { version_id } => Self::Active(version_id.clone()),
109
+ VersionBinding::Explicit if requested_version_ids.is_empty() => Self::AllVisible,
110
+ VersionBinding::Explicit => Self::Explicit(requested_version_ids),
111
+ }
112
+ }
113
+ }
114
+
115
+ pub(crate) async fn resolve_sql_version_scope(
116
+ version_ref: &dyn VersionRefReader,
117
+ scope: SqlVersionScope,
118
+ ) -> Result<Vec<String>, LixError> {
119
+ match scope {
120
+ SqlVersionScope::Active(version_id) => Ok(vec![version_id]),
121
+ SqlVersionScope::Explicit(version_ids) => Ok(version_ids),
122
+ SqlVersionScope::AllVisible => visible_version_ids(version_ref).await,
123
+ }
124
+ }
125
+
126
+ pub(crate) async fn resolve_provider_version_ids(
127
+ version_ref: &dyn VersionRefReader,
128
+ binding: &VersionBinding,
129
+ requested_version_ids: Vec<String>,
130
+ ) -> Result<Vec<String>, LixError> {
131
+ resolve_sql_version_scope(
132
+ version_ref,
133
+ SqlVersionScope::from_provider(binding, requested_version_ids),
134
+ )
135
+ .await
136
+ }
137
+
138
+ pub(crate) fn explicit_version_ids_from_dml_filters(filters: &[Expr]) -> Vec<String> {
139
+ filters
140
+ .iter()
141
+ .flat_map(version_ids_from_filter)
142
+ .collect::<BTreeSet<_>>()
143
+ .into_iter()
144
+ .collect()
145
+ }
146
+
147
+ fn version_ids_from_filter(expr: &Expr) -> Vec<String> {
148
+ match expr {
149
+ Expr::BinaryExpr(binary_expr) if binary_expr.op == Operator::And => {
150
+ let mut values = version_ids_from_filter(&binary_expr.left);
151
+ values.extend(version_ids_from_filter(&binary_expr.right));
152
+ values
153
+ }
154
+ Expr::BinaryExpr(binary_expr) => version_id_from_binary_filter(binary_expr)
155
+ .map(|value| vec![value])
156
+ .unwrap_or_default(),
157
+ Expr::InList(in_list) => version_ids_from_in_list_filter(in_list).unwrap_or_default(),
158
+ _ => Vec::new(),
159
+ }
160
+ }
161
+
162
+ fn version_id_from_binary_filter(binary_expr: &BinaryExpr) -> Option<String> {
163
+ if binary_expr.op != Operator::Eq {
164
+ return None;
165
+ }
166
+
167
+ version_id_from_column_literal_filter(&binary_expr.left, &binary_expr.right)
168
+ .or_else(|| version_id_from_column_literal_filter(&binary_expr.right, &binary_expr.left))
169
+ }
170
+
171
+ fn version_ids_from_in_list_filter(in_list: &InList) -> Option<Vec<String>> {
172
+ if in_list.negated {
173
+ return None;
174
+ }
175
+ let Expr::Column(column) = in_list.expr.as_ref() else {
176
+ return None;
177
+ };
178
+ if column.name != "lixcol_version_id" {
179
+ return None;
180
+ }
181
+
182
+ let values = in_list
183
+ .list
184
+ .iter()
185
+ .map(string_expr_literal)
186
+ .collect::<Option<Vec<_>>>()?;
187
+ if values.is_empty() {
188
+ return None;
189
+ }
190
+ Some(values)
191
+ }
192
+
193
+ fn version_id_from_column_literal_filter(
194
+ column_expr: &Expr,
195
+ literal_expr: &Expr,
196
+ ) -> Option<String> {
197
+ let Expr::Column(column) = column_expr else {
198
+ return None;
199
+ };
200
+ if column.name != "lixcol_version_id" {
201
+ return None;
202
+ }
203
+ string_expr_literal(literal_expr)
204
+ }
205
+
206
+ fn string_expr_literal(expr: &Expr) -> Option<String> {
207
+ let Expr::Literal(literal, _) = expr else {
208
+ return None;
209
+ };
210
+ match literal {
211
+ ScalarValue::Utf8(Some(value))
212
+ | ScalarValue::Utf8View(Some(value))
213
+ | ScalarValue::LargeUtf8(Some(value)) => Some(value.clone()),
214
+ _ => None,
215
+ }
216
+ }
217
+
218
+ async fn visible_version_ids(version_ref: &dyn VersionRefReader) -> Result<Vec<String>, LixError> {
219
+ let mut version_ids = version_ref
220
+ .scan_heads()
221
+ .await?
222
+ .into_iter()
223
+ .map(|head| head.version_id)
224
+ .collect::<BTreeSet<_>>();
225
+ version_ids.insert(GLOBAL_VERSION_ID.to_string());
226
+ Ok(version_ids.into_iter().collect())
227
+ }
228
+
229
+ #[cfg(test)]
230
+ mod tests {
231
+ use async_trait::async_trait;
232
+
233
+ use super::*;
234
+ use crate::version::VersionHead;
235
+
236
+ #[tokio::test]
237
+ async fn active_scope_uses_session_version() {
238
+ let version_ref = RowsVersionRefReader::new(Vec::new());
239
+ let ids =
240
+ resolve_provider_version_ids(&version_ref, &VersionBinding::active("main"), Vec::new())
241
+ .await
242
+ .expect("scope should resolve");
243
+
244
+ assert_eq!(ids, vec!["main".to_string()]);
245
+ }
246
+
247
+ #[tokio::test]
248
+ async fn explicit_scope_keeps_requested_versions() {
249
+ let version_ref = RowsVersionRefReader::new(Vec::new());
250
+ let ids = resolve_provider_version_ids(
251
+ &version_ref,
252
+ &VersionBinding::explicit(),
253
+ vec!["version-a".to_string(), "global".to_string()],
254
+ )
255
+ .await
256
+ .expect("scope should resolve");
257
+
258
+ assert_eq!(ids, vec!["version-a".to_string(), "global".to_string()]);
259
+ }
260
+
261
+ #[tokio::test]
262
+ async fn all_visible_scope_loads_version_refs_and_global() {
263
+ let version_ref = RowsVersionRefReader::new(vec![
264
+ VersionHead {
265
+ version_id: "version-b".to_string(),
266
+ commit_id: "commit-version-b".to_string(),
267
+ },
268
+ VersionHead {
269
+ version_id: "version-a".to_string(),
270
+ commit_id: "commit-version-a".to_string(),
271
+ },
272
+ ]);
273
+ let ids =
274
+ resolve_provider_version_ids(&version_ref, &VersionBinding::explicit(), Vec::new())
275
+ .await
276
+ .expect("scope should resolve");
277
+
278
+ assert_eq!(
279
+ ids,
280
+ vec![
281
+ "global".to_string(),
282
+ "version-a".to_string(),
283
+ "version-b".to_string(),
284
+ ]
285
+ );
286
+ }
287
+
288
+ #[test]
289
+ fn write_scope_uses_fallback_version_when_version_is_implicit() {
290
+ let scope = resolve_write_version_scope(
291
+ None,
292
+ None,
293
+ Some("active-version"),
294
+ "INSERT into surface",
295
+ "surface",
296
+ )
297
+ .expect("scope should resolve");
298
+
299
+ assert_eq!(
300
+ scope,
301
+ WriteVersionScope {
302
+ version_id: "active-version".to_string(),
303
+ global: false,
304
+ }
305
+ );
306
+ }
307
+
308
+ #[test]
309
+ fn write_scope_requires_version_without_fallback() {
310
+ let error = resolve_write_version_scope(None, None, None, "INSERT into surface", "surface")
311
+ .expect_err("missing version should be rejected");
312
+
313
+ assert!(error
314
+ .to_string()
315
+ .contains("INSERT into surface requires lixcol_version_id"));
316
+ }
317
+
318
+ #[test]
319
+ fn write_scope_derives_global_from_global_version_id() {
320
+ let scope = resolve_write_version_scope(
321
+ None,
322
+ Some(GLOBAL_VERSION_ID.to_string()),
323
+ None,
324
+ "INSERT into surface",
325
+ "surface",
326
+ )
327
+ .expect("scope should resolve");
328
+
329
+ assert_eq!(
330
+ scope,
331
+ WriteVersionScope {
332
+ version_id: GLOBAL_VERSION_ID.to_string(),
333
+ global: true,
334
+ }
335
+ );
336
+ }
337
+
338
+ #[test]
339
+ fn write_scope_rejects_non_global_with_global_version_id() {
340
+ let error = resolve_write_version_scope(
341
+ Some(false),
342
+ Some(GLOBAL_VERSION_ID.to_string()),
343
+ None,
344
+ "INSERT into surface",
345
+ "surface",
346
+ )
347
+ .expect_err("conflicting global/version scope should be rejected");
348
+
349
+ assert!(error
350
+ .to_string()
351
+ .contains("surface cannot set lixcol_global=false with global lixcol_version_id"));
352
+ }
353
+
354
+ #[test]
355
+ fn write_scope_rejects_global_with_non_global_version_id() {
356
+ let error = resolve_write_version_scope(
357
+ Some(true),
358
+ Some("version-a".to_string()),
359
+ None,
360
+ "INSERT into surface",
361
+ "surface",
362
+ )
363
+ .expect_err("conflicting global/version scope should be rejected");
364
+
365
+ assert!(error
366
+ .to_string()
367
+ .contains("surface cannot set lixcol_global=true with non-global lixcol_version_id"));
368
+ }
369
+
370
+ struct RowsVersionRefReader {
371
+ heads: Vec<VersionHead>,
372
+ }
373
+
374
+ impl RowsVersionRefReader {
375
+ fn new(heads: Vec<VersionHead>) -> Self {
376
+ Self { heads }
377
+ }
378
+ }
379
+
380
+ #[async_trait]
381
+ impl VersionRefReader for RowsVersionRefReader {
382
+ async fn load_head(&self, version_id: &str) -> Result<Option<VersionHead>, LixError> {
383
+ Ok(self
384
+ .heads
385
+ .iter()
386
+ .find(|head| head.version_id == version_id)
387
+ .cloned())
388
+ }
389
+
390
+ async fn scan_heads(&self) -> Result<Vec<VersionHead>, LixError> {
391
+ Ok(self.heads.clone())
392
+ }
393
+ }
394
+ }