@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.
- package/SKILL.md +305 -320
- package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
- package/dist/engine-wasm/wasm/lix_engine.js +9 -13
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
- package/dist/open-lix.d.ts +103 -14
- package/dist/open-lix.js +3 -0
- package/dist/sqlite/index.js +99 -22
- package/dist-engine-src/README.md +18 -0
- package/dist-engine-src/src/backend/kv.rs +358 -0
- package/dist-engine-src/src/backend/mod.rs +12 -0
- package/dist-engine-src/src/backend/testing.rs +658 -0
- package/dist-engine-src/src/backend/types.rs +96 -0
- package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
- package/dist-engine-src/src/binary_cas/codec.rs +346 -0
- package/dist-engine-src/src/binary_cas/context.rs +139 -0
- package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
- package/dist-engine-src/src/binary_cas/mod.rs +11 -0
- package/dist-engine-src/src/binary_cas/types.rs +127 -0
- package/dist-engine-src/src/cel/context.rs +86 -0
- package/dist-engine-src/src/cel/error.rs +19 -0
- package/dist-engine-src/src/cel/mod.rs +8 -0
- package/dist-engine-src/src/cel/provider.rs +9 -0
- package/dist-engine-src/src/cel/runtime.rs +167 -0
- package/dist-engine-src/src/cel/value.rs +50 -0
- package/dist-engine-src/src/changelog/codec.rs +321 -0
- package/dist-engine-src/src/changelog/context.rs +92 -0
- package/dist-engine-src/src/changelog/materialization.rs +121 -0
- package/dist-engine-src/src/changelog/mod.rs +13 -0
- package/dist-engine-src/src/changelog/reader.rs +20 -0
- package/dist-engine-src/src/changelog/storage.rs +220 -0
- package/dist-engine-src/src/changelog/types.rs +38 -0
- package/dist-engine-src/src/commit_graph/context.rs +1588 -0
- package/dist-engine-src/src/commit_graph/mod.rs +12 -0
- package/dist-engine-src/src/commit_graph/types.rs +145 -0
- package/dist-engine-src/src/commit_graph/walker.rs +780 -0
- package/dist-engine-src/src/common/error.rs +313 -0
- package/dist-engine-src/src/common/fingerprint.rs +3 -0
- package/dist-engine-src/src/common/fs_path.rs +1336 -0
- package/dist-engine-src/src/common/identity.rs +135 -0
- package/dist-engine-src/src/common/metadata.rs +35 -0
- package/dist-engine-src/src/common/mod.rs +23 -0
- package/dist-engine-src/src/common/types.rs +105 -0
- package/dist-engine-src/src/common/wire.rs +222 -0
- package/dist-engine-src/src/engine.rs +239 -0
- package/dist-engine-src/src/entity_identity.rs +285 -0
- package/dist-engine-src/src/functions/context.rs +327 -0
- package/dist-engine-src/src/functions/deterministic.rs +113 -0
- package/dist-engine-src/src/functions/mod.rs +18 -0
- package/dist-engine-src/src/functions/provider.rs +130 -0
- package/dist-engine-src/src/functions/state.rs +363 -0
- package/dist-engine-src/src/functions/types.rs +37 -0
- package/dist-engine-src/src/init.rs +505 -0
- package/dist-engine-src/src/json_store/compression.rs +77 -0
- package/dist-engine-src/src/json_store/context.rs +129 -0
- package/dist-engine-src/src/json_store/encoded.rs +15 -0
- package/dist-engine-src/src/json_store/mod.rs +9 -0
- package/dist-engine-src/src/json_store/store.rs +236 -0
- package/dist-engine-src/src/json_store/types.rs +52 -0
- package/dist-engine-src/src/lib.rs +61 -0
- package/dist-engine-src/src/live_state/context.rs +2241 -0
- package/dist-engine-src/src/live_state/mod.rs +15 -0
- package/dist-engine-src/src/live_state/overlay.rs +75 -0
- package/dist-engine-src/src/live_state/reader.rs +23 -0
- package/dist-engine-src/src/live_state/types.rs +239 -0
- package/dist-engine-src/src/live_state/visibility.rs +218 -0
- package/dist-engine-src/src/plugin/archive.rs +441 -0
- package/dist-engine-src/src/plugin/component.rs +183 -0
- package/dist-engine-src/src/plugin/install.rs +637 -0
- package/dist-engine-src/src/plugin/manifest.rs +516 -0
- package/dist-engine-src/src/plugin/materializer.rs +477 -0
- package/dist-engine-src/src/plugin/mod.rs +33 -0
- package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
- package/dist-engine-src/src/plugin/storage.rs +74 -0
- package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
- package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
- package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
- package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
- package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
- package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
- package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
- package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
- package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
- package/dist-engine-src/src/schema/definition.json +157 -0
- package/dist-engine-src/src/schema/definition.rs +636 -0
- package/dist-engine-src/src/schema/key.rs +206 -0
- package/dist-engine-src/src/schema/mod.rs +20 -0
- package/dist-engine-src/src/schema/seed.rs +14 -0
- package/dist-engine-src/src/schema/tests.rs +739 -0
- package/dist-engine-src/src/schema_registry.rs +294 -0
- package/dist-engine-src/src/session/context.rs +366 -0
- package/dist-engine-src/src/session/create_version.rs +80 -0
- package/dist-engine-src/src/session/execute.rs +447 -0
- package/dist-engine-src/src/session/merge/analysis.rs +102 -0
- package/dist-engine-src/src/session/merge/apply.rs +23 -0
- package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
- package/dist-engine-src/src/session/merge/mod.rs +11 -0
- package/dist-engine-src/src/session/merge/stats.rs +65 -0
- package/dist-engine-src/src/session/merge/version.rs +437 -0
- package/dist-engine-src/src/session/mod.rs +25 -0
- package/dist-engine-src/src/session/switch_version.rs +121 -0
- package/dist-engine-src/src/sql2/change_provider.rs +337 -0
- package/dist-engine-src/src/sql2/classify.rs +147 -0
- package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
- package/dist-engine-src/src/sql2/context.rs +307 -0
- package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
- package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
- package/dist-engine-src/src/sql2/dml.rs +148 -0
- package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
- package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
- package/dist-engine-src/src/sql2/error.rs +196 -0
- package/dist-engine-src/src/sql2/execute.rs +3379 -0
- package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
- package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
- package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
- package/dist-engine-src/src/sql2/history_projection.rs +80 -0
- package/dist-engine-src/src/sql2/history_provider.rs +418 -0
- package/dist-engine-src/src/sql2/history_route.rs +643 -0
- package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
- package/dist-engine-src/src/sql2/mod.rs +43 -0
- package/dist-engine-src/src/sql2/read_only.rs +65 -0
- package/dist-engine-src/src/sql2/record_batch.rs +17 -0
- package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
- package/dist-engine-src/src/sql2/runtime.rs +60 -0
- package/dist-engine-src/src/sql2/session.rs +135 -0
- package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
- package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
- package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
- package/dist-engine-src/src/sql2/version_scope.rs +394 -0
- package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
- package/dist-engine-src/src/storage/context.rs +356 -0
- package/dist-engine-src/src/storage/mod.rs +14 -0
- package/dist-engine-src/src/storage/read_scope.rs +88 -0
- package/dist-engine-src/src/storage/types.rs +501 -0
- package/dist-engine-src/src/storage_bench.rs +3406 -0
- package/dist-engine-src/src/test_support.rs +81 -0
- package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
- package/dist-engine-src/src/tracked_state/codec.rs +747 -0
- package/dist-engine-src/src/tracked_state/context.rs +983 -0
- package/dist-engine-src/src/tracked_state/diff.rs +494 -0
- package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
- package/dist-engine-src/src/tracked_state/merge.rs +474 -0
- package/dist-engine-src/src/tracked_state/mod.rs +31 -0
- package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
- package/dist-engine-src/src/tracked_state/storage.rs +243 -0
- package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
- package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
- package/dist-engine-src/src/tracked_state/types.rs +61 -0
- package/dist-engine-src/src/transaction/commit.rs +1224 -0
- package/dist-engine-src/src/transaction/context.rs +1307 -0
- package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
- package/dist-engine-src/src/transaction/mod.rs +11 -0
- package/dist-engine-src/src/transaction/normalization.rs +1026 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
- package/dist-engine-src/src/transaction/staging.rs +1436 -0
- package/dist-engine-src/src/transaction/types.rs +351 -0
- package/dist-engine-src/src/transaction/validation.rs +4811 -0
- package/dist-engine-src/src/untracked_state/codec.rs +363 -0
- package/dist-engine-src/src/untracked_state/context.rs +82 -0
- package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
- package/dist-engine-src/src/untracked_state/mod.rs +17 -0
- package/dist-engine-src/src/untracked_state/storage.rs +348 -0
- package/dist-engine-src/src/untracked_state/types.rs +96 -0
- package/dist-engine-src/src/version/context.rs +52 -0
- package/dist-engine-src/src/version/mod.rs +12 -0
- package/dist-engine-src/src/version/refs.rs +421 -0
- package/dist-engine-src/src/version/stage_rows.rs +71 -0
- package/dist-engine-src/src/version/types.rs +21 -0
- package/dist-engine-src/src/wasm/mod.rs +60 -0
- 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
|
+
}
|