@lix-js/sdk 0.6.0-preview.1 → 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 (191) hide show
  1. package/SKILL.md +305 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/open-lix.d.ts +103 -14
  7. package/dist/open-lix.js +3 -0
  8. package/dist/sqlite/index.js +99 -22
  9. package/dist-engine-src/README.md +18 -0
  10. package/dist-engine-src/src/backend/kv.rs +358 -0
  11. package/dist-engine-src/src/backend/mod.rs +12 -0
  12. package/dist-engine-src/src/backend/testing.rs +658 -0
  13. package/dist-engine-src/src/backend/types.rs +96 -0
  14. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  15. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  16. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  17. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  18. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  19. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  20. package/dist-engine-src/src/cel/context.rs +86 -0
  21. package/dist-engine-src/src/cel/error.rs +19 -0
  22. package/dist-engine-src/src/cel/mod.rs +8 -0
  23. package/dist-engine-src/src/cel/provider.rs +9 -0
  24. package/dist-engine-src/src/cel/runtime.rs +167 -0
  25. package/dist-engine-src/src/cel/value.rs +50 -0
  26. package/dist-engine-src/src/changelog/codec.rs +321 -0
  27. package/dist-engine-src/src/changelog/context.rs +92 -0
  28. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  29. package/dist-engine-src/src/changelog/mod.rs +13 -0
  30. package/dist-engine-src/src/changelog/reader.rs +20 -0
  31. package/dist-engine-src/src/changelog/storage.rs +220 -0
  32. package/dist-engine-src/src/changelog/types.rs +38 -0
  33. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  34. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  35. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  36. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  37. package/dist-engine-src/src/common/error.rs +313 -0
  38. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  39. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  40. package/dist-engine-src/src/common/identity.rs +135 -0
  41. package/dist-engine-src/src/common/metadata.rs +35 -0
  42. package/dist-engine-src/src/common/mod.rs +23 -0
  43. package/dist-engine-src/src/common/types.rs +105 -0
  44. package/dist-engine-src/src/common/wire.rs +222 -0
  45. package/dist-engine-src/src/engine.rs +239 -0
  46. package/dist-engine-src/src/entity_identity.rs +285 -0
  47. package/dist-engine-src/src/functions/context.rs +327 -0
  48. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  49. package/dist-engine-src/src/functions/mod.rs +18 -0
  50. package/dist-engine-src/src/functions/provider.rs +130 -0
  51. package/dist-engine-src/src/functions/state.rs +363 -0
  52. package/dist-engine-src/src/functions/types.rs +37 -0
  53. package/dist-engine-src/src/init.rs +505 -0
  54. package/dist-engine-src/src/json_store/compression.rs +77 -0
  55. package/dist-engine-src/src/json_store/context.rs +129 -0
  56. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  57. package/dist-engine-src/src/json_store/mod.rs +9 -0
  58. package/dist-engine-src/src/json_store/store.rs +236 -0
  59. package/dist-engine-src/src/json_store/types.rs +52 -0
  60. package/dist-engine-src/src/lib.rs +61 -0
  61. package/dist-engine-src/src/live_state/context.rs +2241 -0
  62. package/dist-engine-src/src/live_state/mod.rs +15 -0
  63. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  64. package/dist-engine-src/src/live_state/reader.rs +23 -0
  65. package/dist-engine-src/src/live_state/types.rs +239 -0
  66. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  67. package/dist-engine-src/src/plugin/archive.rs +441 -0
  68. package/dist-engine-src/src/plugin/component.rs +183 -0
  69. package/dist-engine-src/src/plugin/install.rs +637 -0
  70. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  71. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  72. package/dist-engine-src/src/plugin/mod.rs +33 -0
  73. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  74. package/dist-engine-src/src/plugin/storage.rs +74 -0
  75. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  76. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  77. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  78. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  79. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  80. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  81. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  82. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  85. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  86. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  87. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  89. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  90. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  91. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  92. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  93. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  94. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  95. package/dist-engine-src/src/schema/definition.json +157 -0
  96. package/dist-engine-src/src/schema/definition.rs +636 -0
  97. package/dist-engine-src/src/schema/key.rs +206 -0
  98. package/dist-engine-src/src/schema/mod.rs +20 -0
  99. package/dist-engine-src/src/schema/seed.rs +14 -0
  100. package/dist-engine-src/src/schema/tests.rs +739 -0
  101. package/dist-engine-src/src/schema_registry.rs +294 -0
  102. package/dist-engine-src/src/session/context.rs +366 -0
  103. package/dist-engine-src/src/session/create_version.rs +80 -0
  104. package/dist-engine-src/src/session/execute.rs +447 -0
  105. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  106. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  107. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  108. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  109. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  110. package/dist-engine-src/src/session/merge/version.rs +437 -0
  111. package/dist-engine-src/src/session/mod.rs +25 -0
  112. package/dist-engine-src/src/session/switch_version.rs +121 -0
  113. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  114. package/dist-engine-src/src/sql2/classify.rs +147 -0
  115. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  116. package/dist-engine-src/src/sql2/context.rs +307 -0
  117. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  118. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  119. package/dist-engine-src/src/sql2/dml.rs +148 -0
  120. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  121. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  122. package/dist-engine-src/src/sql2/error.rs +196 -0
  123. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  124. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  125. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  126. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  127. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  128. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  129. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  130. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  131. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  132. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  133. package/dist-engine-src/src/sql2/mod.rs +43 -0
  134. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  135. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  136. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  137. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  138. package/dist-engine-src/src/sql2/session.rs +135 -0
  139. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  140. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  141. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  142. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  143. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  144. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  148. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  149. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  150. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  151. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  152. package/dist-engine-src/src/storage/context.rs +356 -0
  153. package/dist-engine-src/src/storage/mod.rs +14 -0
  154. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  155. package/dist-engine-src/src/storage/types.rs +501 -0
  156. package/dist-engine-src/src/storage_bench.rs +3406 -0
  157. package/dist-engine-src/src/test_support.rs +81 -0
  158. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  159. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  160. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  161. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  162. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  163. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  164. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  165. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  166. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  167. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  168. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  169. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  170. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  171. package/dist-engine-src/src/transaction/context.rs +1307 -0
  172. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  173. package/dist-engine-src/src/transaction/mod.rs +11 -0
  174. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  175. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  176. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  177. package/dist-engine-src/src/transaction/types.rs +351 -0
  178. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  179. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  180. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  181. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  182. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  183. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  184. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  185. package/dist-engine-src/src/version/context.rs +52 -0
  186. package/dist-engine-src/src/version/mod.rs +12 -0
  187. package/dist-engine-src/src/version/refs.rs +421 -0
  188. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  189. package/dist-engine-src/src/version/types.rs +21 -0
  190. package/dist-engine-src/src/wasm/mod.rs +60 -0
  191. package/package.json +68 -64
@@ -0,0 +1,294 @@
1
+ use std::collections::BTreeMap;
2
+
3
+ use serde_json::Value as JsonValue;
4
+
5
+ use crate::live_state::LiveStateRow;
6
+ use crate::live_state::{LiveStateFilter, LiveStateReader, LiveStateScanRequest};
7
+ use crate::schema::schema_key_from_definition;
8
+ use crate::{LixError, NullableKeyFilter, GLOBAL_VERSION_ID};
9
+
10
+ const REGISTERED_SCHEMA_KEY: &str = "lix_registered_schema";
11
+
12
+ /// Engine2 schema visibility boundary.
13
+ ///
14
+ /// SQL planning receives a schema snapshot from live state. System schemas are
15
+ /// seeded as ordinary `lix_registered_schema` rows during initialization, so
16
+ /// runtime schema visibility has one source of truth.
17
+ pub(crate) struct SchemaRegistry;
18
+
19
+ impl SchemaRegistry {
20
+ pub(crate) fn new() -> Self {
21
+ Self
22
+ }
23
+
24
+ /// Loads schema definitions visible for SQL planning at `version_id`.
25
+ pub(crate) async fn visible_schemas<R>(
26
+ &self,
27
+ live_state: &R,
28
+ version_id: &str,
29
+ ) -> Result<Vec<JsonValue>, LixError>
30
+ where
31
+ R: LiveStateReader + ?Sized,
32
+ {
33
+ let mut schemas = BTreeMap::new();
34
+ for row in live_state
35
+ .scan_rows(&LiveStateScanRequest {
36
+ filter: LiveStateFilter {
37
+ schema_keys: vec![REGISTERED_SCHEMA_KEY.to_string()],
38
+ version_ids: vec![version_id.to_string()],
39
+ file_ids: vec![NullableKeyFilter::Null],
40
+ include_tombstones: false,
41
+ ..LiveStateFilter::default()
42
+ },
43
+ ..LiveStateScanRequest::default()
44
+ })
45
+ .await?
46
+ .into_iter()
47
+ .filter(|row| version_scoped_schema_row_is_visible(row, version_id))
48
+ {
49
+ let Some((key, schema)) = decode_registered_schema_row(&row)? else {
50
+ continue;
51
+ };
52
+ upsert_latest_schema(&mut schemas, key, schema);
53
+ }
54
+ Ok(schemas.into_values().map(|(_, schema)| schema).collect())
55
+ }
56
+ }
57
+
58
+ fn version_scoped_schema_row_is_visible(row: &LiveStateRow, requested_version_id: &str) -> bool {
59
+ requested_version_id == GLOBAL_VERSION_ID || !row.global
60
+ }
61
+
62
+ fn upsert_latest_schema(
63
+ schemas: &mut BTreeMap<String, (crate::schema::SchemaKey, JsonValue)>,
64
+ key: crate::schema::SchemaKey,
65
+ schema: JsonValue,
66
+ ) {
67
+ let should_replace = schemas
68
+ .get(&key.schema_key)
69
+ .is_none_or(|(existing, _)| !schema_key_is_older(&key, existing));
70
+ if should_replace {
71
+ schemas.insert(key.schema_key.clone(), (key, schema));
72
+ }
73
+ }
74
+
75
+ fn schema_key_is_older(
76
+ candidate: &crate::schema::SchemaKey,
77
+ existing: &crate::schema::SchemaKey,
78
+ ) -> bool {
79
+ match (candidate.version_number(), existing.version_number()) {
80
+ (Some(candidate_version), Some(existing_version)) => candidate_version < existing_version,
81
+ _ => candidate.schema_version < existing.schema_version,
82
+ }
83
+ }
84
+
85
+ fn decode_registered_schema_row(
86
+ row: &LiveStateRow,
87
+ ) -> Result<Option<(crate::schema::SchemaKey, JsonValue)>, LixError> {
88
+ if row.schema_key != REGISTERED_SCHEMA_KEY {
89
+ return Err(LixError::new(
90
+ "LIX_ERROR_UNKNOWN",
91
+ format!(
92
+ "expected lix_registered_schema row, got schema_key={}",
93
+ row.schema_key
94
+ ),
95
+ ));
96
+ }
97
+
98
+ let Some(snapshot_content) = row.snapshot_content.as_deref() else {
99
+ return Ok(None);
100
+ };
101
+
102
+ let snapshot: JsonValue = serde_json::from_str(snapshot_content).map_err(|err| {
103
+ LixError::new(
104
+ "LIX_ERROR_UNKNOWN",
105
+ format!("invalid registered schema snapshot JSON: {err}"),
106
+ )
107
+ })?;
108
+ let schema = snapshot.get("value").cloned().ok_or_else(|| {
109
+ LixError::new(
110
+ "LIX_ERROR_UNKNOWN",
111
+ "registered schema snapshot missing value",
112
+ )
113
+ })?;
114
+ let key = schema_key_from_definition(&schema)?;
115
+ Ok(Some((key, schema)))
116
+ }
117
+
118
+ #[cfg(test)]
119
+ mod tests {
120
+ use async_trait::async_trait;
121
+ use serde_json::json;
122
+
123
+ use super::*;
124
+ use crate::live_state::LiveStateRowRequest;
125
+ use crate::GLOBAL_VERSION_ID;
126
+
127
+ #[tokio::test]
128
+ async fn visible_schemas_are_loaded_from_registered_schema_rows() {
129
+ let registry = SchemaRegistry::new();
130
+
131
+ let schemas = registry
132
+ .visible_schemas(
133
+ &RowsLiveStateReader::new(vec![
134
+ registered_schema_row("lix_registered_schema", "1"),
135
+ registered_schema_row("lix_key_value", "1"),
136
+ ]),
137
+ "global",
138
+ )
139
+ .await
140
+ .expect("schema visibility should load");
141
+
142
+ assert!(schemas.iter().any(|schema| {
143
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("lix_registered_schema")
144
+ }));
145
+ assert!(schemas.iter().any(|schema| {
146
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("lix_key_value")
147
+ }));
148
+ }
149
+
150
+ #[tokio::test]
151
+ async fn visible_schemas_include_registered_schema_rows() {
152
+ let registry = SchemaRegistry::new();
153
+
154
+ let schemas = registry
155
+ .visible_schemas(
156
+ &RowsLiveStateReader::new(vec![registered_schema_row(
157
+ "engine2_dynamic_schema",
158
+ "1",
159
+ )]),
160
+ "global",
161
+ )
162
+ .await
163
+ .expect("schema visibility should load");
164
+
165
+ assert!(schemas.iter().any(|schema| {
166
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("engine2_dynamic_schema")
167
+ }));
168
+ }
169
+
170
+ #[tokio::test]
171
+ async fn visible_schemas_ignore_projected_global_schema_rows_for_version_scope() {
172
+ let registry = SchemaRegistry::new();
173
+ let mut global_only = registered_schema_row("global_only_schema", "1");
174
+ global_only.global = true;
175
+ global_only.version_id = "main".to_string();
176
+
177
+ let schemas = registry
178
+ .visible_schemas(&RowsLiveStateReader::new(vec![global_only]), "main")
179
+ .await
180
+ .expect("schema visibility should load");
181
+
182
+ assert!(schemas.is_empty());
183
+ }
184
+
185
+ #[tokio::test]
186
+ async fn visible_schemas_are_empty_when_no_schema_rows_are_visible() {
187
+ let registry = SchemaRegistry::new();
188
+
189
+ let schemas = registry
190
+ .visible_schemas(&RowsLiveStateReader::new(Vec::new()), "global")
191
+ .await
192
+ .expect("schema visibility should load");
193
+
194
+ assert!(schemas.is_empty());
195
+ }
196
+
197
+ struct RowsLiveStateReader {
198
+ rows: Vec<LiveStateRow>,
199
+ }
200
+
201
+ impl RowsLiveStateReader {
202
+ fn new(rows: Vec<LiveStateRow>) -> Self {
203
+ Self { rows }
204
+ }
205
+ }
206
+
207
+ #[async_trait]
208
+ impl LiveStateReader for RowsLiveStateReader {
209
+ async fn scan_rows(
210
+ &self,
211
+ request: &LiveStateScanRequest,
212
+ ) -> Result<Vec<LiveStateRow>, LixError> {
213
+ Ok(self
214
+ .rows
215
+ .iter()
216
+ .filter(|row| {
217
+ request.filter.schema_keys.is_empty()
218
+ || request.filter.schema_keys.contains(&row.schema_key)
219
+ })
220
+ .filter(|row| {
221
+ request.filter.version_ids.is_empty()
222
+ || request.filter.version_ids.contains(&row.version_id)
223
+ })
224
+ .cloned()
225
+ .collect())
226
+ }
227
+
228
+ async fn load_row(
229
+ &self,
230
+ request: &LiveStateRowRequest,
231
+ ) -> Result<Option<LiveStateRow>, LixError> {
232
+ Ok(self
233
+ .rows
234
+ .iter()
235
+ .find(|row| {
236
+ row.schema_key == request.schema_key
237
+ && row.version_id == request.version_id
238
+ && row.entity_id == request.entity_id
239
+ })
240
+ .cloned())
241
+ }
242
+ }
243
+
244
+ fn registered_schema_row(schema_key: &str, schema_version: &str) -> LiveStateRow {
245
+ LiveStateRow {
246
+ entity_id: registered_schema_entity_id(schema_key, schema_version),
247
+ file_id: None,
248
+ schema_key: REGISTERED_SCHEMA_KEY.to_string(),
249
+ schema_version: "1".to_string(),
250
+ version_id: GLOBAL_VERSION_ID.to_string(),
251
+ metadata: None,
252
+ change_id: Some("change-registered-schema".to_string()),
253
+ commit_id: None,
254
+ global: true,
255
+ untracked: true,
256
+ created_at: "2026-04-23T00:00:00Z".to_string(),
257
+ updated_at: "2026-04-23T01:00:00Z".to_string(),
258
+ snapshot_content: Some(
259
+ json!({
260
+ "value": {
261
+ "x-lix-key": schema_key,
262
+ "x-lix-version": schema_version,
263
+ "type": "object",
264
+ "properties": {
265
+ "id": { "type": "string" }
266
+ },
267
+ "required": ["id"],
268
+ "additionalProperties": false
269
+ }
270
+ })
271
+ .to_string(),
272
+ ),
273
+ }
274
+ }
275
+
276
+ fn registered_schema_entity_id(
277
+ schema_key: &str,
278
+ schema_version: &str,
279
+ ) -> crate::entity_identity::EntityIdentity {
280
+ crate::entity_identity::EntityIdentity::from_primary_key_paths(
281
+ &json!({
282
+ "value": {
283
+ "x-lix-key": schema_key,
284
+ "x-lix-version": schema_version,
285
+ }
286
+ }),
287
+ &[
288
+ vec!["value".to_string(), "x-lix-key".to_string()],
289
+ vec!["value".to_string(), "x-lix-version".to_string()],
290
+ ],
291
+ )
292
+ .expect("registered schema identity should derive")
293
+ }
294
+ }
@@ -0,0 +1,366 @@
1
+ use std::future::Future;
2
+ use std::pin::Pin;
3
+ use std::sync::atomic::{AtomicBool, Ordering};
4
+ use std::sync::Arc;
5
+
6
+ use serde_json::Value as JsonValue;
7
+
8
+ use crate::binary_cas::{BinaryCasContext, BlobDataReader};
9
+ use crate::changelog::ChangelogContext;
10
+ use crate::commit_graph::{CommitGraphContext, CommitGraphReader};
11
+ use crate::entity_identity::EntityIdentity;
12
+ use crate::functions::FunctionProviderHandle;
13
+ use crate::json_store::JsonStoreContext;
14
+ use crate::live_state::{LiveStateContext, LiveStateReader, LiveStateRowRequest};
15
+ use crate::schema_registry::SchemaRegistry;
16
+ use crate::sql2::{ChangelogQuerySource, SqlChangelogQuerySource, SqlExecutionContext};
17
+ use crate::storage::{
18
+ ScopedStorageReader, StorageContext, StorageReadScope, StorageReadTransaction, StorageReader,
19
+ };
20
+ use crate::tracked_state::TrackedStateContext;
21
+ use crate::transaction::{open_transaction, Transaction};
22
+ use crate::version::{VersionContext, VersionRefReader};
23
+ use crate::GLOBAL_VERSION_ID;
24
+ use crate::{LixError, NullableKeyFilter};
25
+
26
+ pub(crate) const WORKSPACE_VERSION_KEY: &str = "lix_workspace_version_id";
27
+
28
+ #[derive(Clone)]
29
+ pub(crate) enum SessionMode {
30
+ Pinned { version_id: String },
31
+ Workspace,
32
+ }
33
+
34
+ /// Session-context state for engine2 execution.
35
+ ///
36
+ /// A session context pins the active version selector and shared execution
37
+ /// services. Each call to `execute(...)` projects this state into a read-only
38
+ /// SQL context or a transaction-owned write context.
39
+ ///
40
+ /// Write transaction invariant: any engine2 operation that may write must enter
41
+ /// through `SessionContext::with_write_transaction`. Reads that influence writes
42
+ /// are only available from that transaction capability, not from session-level
43
+ /// helpers.
44
+ #[derive(Clone)]
45
+ pub struct SessionContext {
46
+ pub(super) mode: SessionMode,
47
+ pub(super) storage: StorageContext,
48
+ pub(super) live_state: Arc<LiveStateContext>,
49
+ pub(super) tracked_state: Arc<TrackedStateContext>,
50
+ pub(super) binary_cas: Arc<BinaryCasContext>,
51
+ pub(super) changelog: Arc<ChangelogContext>,
52
+ pub(super) version_ctx: Arc<VersionContext>,
53
+ pub(super) schema_registry: Arc<SchemaRegistry>,
54
+ closed: Arc<AtomicBool>,
55
+ }
56
+
57
+ impl SessionContext {
58
+ pub(crate) async fn open_workspace(
59
+ storage: StorageContext,
60
+ live_state: Arc<LiveStateContext>,
61
+ tracked_state: Arc<TrackedStateContext>,
62
+ binary_cas: Arc<BinaryCasContext>,
63
+ changelog: Arc<ChangelogContext>,
64
+ version_ctx: Arc<VersionContext>,
65
+ schema_registry: Arc<SchemaRegistry>,
66
+ ) -> Result<Self, LixError> {
67
+ let session = Self::new(
68
+ SessionMode::Workspace,
69
+ storage,
70
+ live_state,
71
+ tracked_state,
72
+ binary_cas,
73
+ changelog,
74
+ version_ctx,
75
+ schema_registry,
76
+ );
77
+ session.active_version_id().await?;
78
+ Ok(session)
79
+ }
80
+
81
+ pub(crate) async fn open(
82
+ active_version_id: String,
83
+ storage: StorageContext,
84
+ live_state: Arc<LiveStateContext>,
85
+ tracked_state: Arc<TrackedStateContext>,
86
+ binary_cas: Arc<BinaryCasContext>,
87
+ changelog: Arc<ChangelogContext>,
88
+ version_ctx: Arc<VersionContext>,
89
+ schema_registry: Arc<SchemaRegistry>,
90
+ ) -> Result<Self, LixError> {
91
+ Ok(Self::new(
92
+ SessionMode::Pinned {
93
+ version_id: active_version_id,
94
+ },
95
+ storage,
96
+ live_state,
97
+ tracked_state,
98
+ binary_cas,
99
+ changelog,
100
+ version_ctx,
101
+ schema_registry,
102
+ ))
103
+ }
104
+
105
+ pub(super) fn new(
106
+ mode: SessionMode,
107
+ storage: StorageContext,
108
+ live_state: Arc<LiveStateContext>,
109
+ tracked_state: Arc<TrackedStateContext>,
110
+ binary_cas: Arc<BinaryCasContext>,
111
+ changelog: Arc<ChangelogContext>,
112
+ version_ctx: Arc<VersionContext>,
113
+ schema_registry: Arc<SchemaRegistry>,
114
+ ) -> Self {
115
+ Self::new_with_closed(
116
+ mode,
117
+ storage,
118
+ live_state,
119
+ tracked_state,
120
+ binary_cas,
121
+ changelog,
122
+ version_ctx,
123
+ schema_registry,
124
+ Arc::new(AtomicBool::new(false)),
125
+ )
126
+ }
127
+
128
+ pub(super) fn new_with_closed(
129
+ mode: SessionMode,
130
+ storage: StorageContext,
131
+ live_state: Arc<LiveStateContext>,
132
+ tracked_state: Arc<TrackedStateContext>,
133
+ binary_cas: Arc<BinaryCasContext>,
134
+ changelog: Arc<ChangelogContext>,
135
+ version_ctx: Arc<VersionContext>,
136
+ schema_registry: Arc<SchemaRegistry>,
137
+ closed: Arc<AtomicBool>,
138
+ ) -> Self {
139
+ Self {
140
+ mode,
141
+ storage,
142
+ live_state,
143
+ tracked_state,
144
+ binary_cas,
145
+ changelog,
146
+ version_ctx,
147
+ schema_registry,
148
+ closed,
149
+ }
150
+ }
151
+
152
+ /// Releases this logical session handle. This is a lifecycle boundary only:
153
+ /// successful writes are committed before their operation returns.
154
+ pub async fn close(&self) -> Result<(), LixError> {
155
+ self.closed.store(true, Ordering::SeqCst);
156
+ Ok(())
157
+ }
158
+
159
+ pub fn is_closed(&self) -> bool {
160
+ self.closed.load(Ordering::SeqCst)
161
+ }
162
+
163
+ pub(crate) fn closed_flag(&self) -> Arc<AtomicBool> {
164
+ Arc::clone(&self.closed)
165
+ }
166
+
167
+ pub(crate) fn ensure_open(&self) -> Result<(), LixError> {
168
+ if self.is_closed() {
169
+ return Err(closed_error());
170
+ }
171
+ Ok(())
172
+ }
173
+
174
+ /// Resolves the version this session should operate on right now.
175
+ ///
176
+ /// This is a read-path helper. Write flows must resolve the active version
177
+ /// through the transaction capability so the read is scoped to the
178
+ /// same backend transaction as the writes it influences.
179
+ ///
180
+ /// Pinned sessions are pure in-memory views over one version. Workspace
181
+ /// sessions read the shared workspace selector from untracked global
182
+ /// `lix_key_value` state so multiple open app sessions can observe the same
183
+ /// active workspace version.
184
+ pub async fn active_version_id(&self) -> Result<String, LixError> {
185
+ let mut transaction = self.storage.begin_read_transaction().await?;
186
+ let result = self
187
+ .active_version_id_from_reader(transaction.as_mut())
188
+ .await;
189
+ match result {
190
+ Ok(version_id) => {
191
+ transaction.rollback().await?;
192
+ Ok(version_id)
193
+ }
194
+ Err(error) => {
195
+ let _ = transaction.rollback().await;
196
+ Err(error)
197
+ }
198
+ }
199
+ }
200
+
201
+ pub(super) async fn active_version_id_from_reader<S>(
202
+ &self,
203
+ reader: &mut S,
204
+ ) -> Result<String, LixError>
205
+ where
206
+ S: StorageReader + ?Sized,
207
+ {
208
+ self.ensure_open()?;
209
+ match &self.mode {
210
+ SessionMode::Pinned { version_id } => Ok(version_id.clone()),
211
+ SessionMode::Workspace => self.load_workspace_version_id(reader).await,
212
+ }
213
+ }
214
+
215
+ async fn load_workspace_version_id<S>(&self, reader: &mut S) -> Result<String, LixError>
216
+ where
217
+ S: StorageReader + ?Sized,
218
+ {
219
+ let row = self
220
+ .live_state
221
+ .reader(&mut *reader)
222
+ .load_row(&LiveStateRowRequest {
223
+ schema_key: "lix_key_value".to_string(),
224
+ version_id: GLOBAL_VERSION_ID.to_string(),
225
+ entity_id: EntityIdentity::single(WORKSPACE_VERSION_KEY),
226
+ file_id: NullableKeyFilter::Null,
227
+ })
228
+ .await?
229
+ .ok_or_else(|| {
230
+ LixError::new(
231
+ "LIX_ERROR_UNKNOWN",
232
+ "workspace version selector is missing lix_key_value:lix_workspace_version_id",
233
+ )
234
+ })?;
235
+ let snapshot_content = row.snapshot_content.as_deref().ok_or_else(|| {
236
+ LixError::new(
237
+ "LIX_ERROR_UNKNOWN",
238
+ "workspace version selector is missing snapshot_content",
239
+ )
240
+ })?;
241
+ let snapshot = serde_json::from_str::<JsonValue>(snapshot_content).map_err(|error| {
242
+ LixError::new(
243
+ "LIX_ERROR_UNKNOWN",
244
+ format!("workspace version selector snapshot is invalid JSON: {error}"),
245
+ )
246
+ })?;
247
+ let version_id = snapshot
248
+ .get("value")
249
+ .and_then(JsonValue::as_str)
250
+ .filter(|value| !value.is_empty())
251
+ .ok_or_else(|| {
252
+ LixError::new(
253
+ "LIX_ERROR_UNKNOWN",
254
+ "workspace version selector value must be a non-empty string",
255
+ )
256
+ })?
257
+ .to_string();
258
+
259
+ let head = self
260
+ .version_ctx
261
+ .ref_reader(&mut *reader)
262
+ .load_head_commit_id(&version_id)
263
+ .await?;
264
+ if head.is_none() {
265
+ return Err(LixError::version_not_found(
266
+ version_id,
267
+ "load_workspace_version_id",
268
+ "workspace_selector",
269
+ ));
270
+ }
271
+
272
+ Ok(version_id)
273
+ }
274
+
275
+ pub(crate) async fn with_write_transaction<T, F>(&self, f: F) -> Result<T, LixError>
276
+ where
277
+ F: for<'tx> FnOnce(
278
+ &'tx mut Transaction,
279
+ ) -> Pin<Box<dyn Future<Output = Result<T, LixError>> + 'tx>>,
280
+ {
281
+ self.ensure_open()?;
282
+ let opened = open_transaction(
283
+ &self.mode,
284
+ self.storage.clone(),
285
+ Arc::clone(&self.live_state),
286
+ Arc::clone(&self.tracked_state),
287
+ Arc::clone(&self.binary_cas),
288
+ Arc::clone(&self.changelog),
289
+ Arc::clone(&self.version_ctx),
290
+ Arc::clone(&self.schema_registry),
291
+ )
292
+ .await?;
293
+ let mut transaction = opened.transaction;
294
+ let runtime_functions = opened.runtime_functions;
295
+
296
+ match f(&mut transaction).await {
297
+ Ok(value) => {
298
+ transaction.commit(&runtime_functions).await?;
299
+ Ok(value)
300
+ }
301
+ Err(error) => {
302
+ let _ = transaction.rollback().await;
303
+ Err(error)
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ fn closed_error() -> LixError {
310
+ LixError::new(LixError::CODE_CLOSED, "Lix handle is closed")
311
+ .with_hint("Open a new Lix handle before calling this method.")
312
+ }
313
+
314
+ /// Read-only SQL execution context derived from a session.
315
+ ///
316
+ /// Write statements re-plan against `Transaction`; this context intentionally
317
+ /// has no write stager.
318
+ pub(super) struct SessionSqlExecutionContext<'a> {
319
+ pub(super) active_version_id: &'a str,
320
+ pub(super) read_store:
321
+ ScopedStorageReader<Box<dyn StorageReadTransaction + Send + Sync + 'static>>,
322
+ pub(super) live_state: Arc<LiveStateContext>,
323
+ pub(super) binary_cas: Arc<BinaryCasContext>,
324
+ pub(super) changelog: Arc<ChangelogContext>,
325
+ pub(super) version_ctx: Arc<VersionContext>,
326
+ pub(super) visible_schemas: Vec<JsonValue>,
327
+ pub(super) functions: FunctionProviderHandle,
328
+ }
329
+
330
+ impl SqlExecutionContext for SessionSqlExecutionContext<'_> {
331
+ fn active_version_id(&self) -> &str {
332
+ self.active_version_id
333
+ }
334
+
335
+ fn live_state(&self) -> Arc<dyn LiveStateReader> {
336
+ Arc::new(self.live_state.reader(self.read_store.clone())) as Arc<dyn LiveStateReader>
337
+ }
338
+
339
+ fn changelog_query_source(&self) -> SqlChangelogQuerySource {
340
+ let read_scope = StorageReadScope::new(self.read_store.clone());
341
+ ChangelogQuerySource {
342
+ changelog_reader: Arc::new(self.changelog.reader(read_scope.store())),
343
+ json_reader: JsonStoreContext::new().reader(read_scope.store()),
344
+ }
345
+ }
346
+
347
+ fn commit_graph(&self) -> Box<dyn CommitGraphReader> {
348
+ Box::new(CommitGraphContext::new(ChangelogContext::new()).reader(self.read_store.clone()))
349
+ }
350
+
351
+ fn version_ref(&self) -> Arc<dyn VersionRefReader> {
352
+ Arc::new(self.version_ctx.ref_reader(self.read_store.clone()))
353
+ }
354
+
355
+ fn functions(&self) -> FunctionProviderHandle {
356
+ self.functions.clone()
357
+ }
358
+
359
+ fn blob_reader(&self) -> Arc<dyn BlobDataReader> {
360
+ Arc::new(self.binary_cas.reader(self.read_store.clone())) as Arc<dyn BlobDataReader>
361
+ }
362
+
363
+ fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
364
+ Ok(self.visible_schemas.clone())
365
+ }
366
+ }