@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.
- package/README.md +9 -0
- package/SKILL.md +468 -0
- package/dist/engine-wasm/index.d.ts +15 -11
- package/dist/engine-wasm/index.js +105 -38
- package/dist/engine-wasm/wasm/lix_engine.d.ts +14 -2
- package/dist/engine-wasm/wasm/lix_engine.js +18 -17
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +2 -1
- package/dist/generated/builtin-schemas.d.ts +31 -41
- package/dist/generated/builtin-schemas.js +52 -56
- package/dist/open-lix.d.ts +141 -24
- package/dist/open-lix.js +199 -35
- 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 -63
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
3
|
+
use crate::binary_cas::BinaryCasContext;
|
|
4
|
+
use crate::changelog::ChangelogContext;
|
|
5
|
+
use crate::commit_graph::CommitGraphContext;
|
|
6
|
+
use crate::entity_identity::EntityIdentity;
|
|
7
|
+
use crate::init::InitReceipt;
|
|
8
|
+
use crate::json_store::JsonStoreContext;
|
|
9
|
+
use crate::live_state::LiveStateContext;
|
|
10
|
+
use crate::live_state::LiveStateRowRequest;
|
|
11
|
+
use crate::schema_registry::SchemaRegistry;
|
|
12
|
+
use crate::session::SessionContext;
|
|
13
|
+
use crate::storage::{StorageContext, StorageWriteSet};
|
|
14
|
+
use crate::tracked_state::TrackedStateContext;
|
|
15
|
+
use crate::untracked_state::UntrackedStateContext;
|
|
16
|
+
use crate::version::{VersionContext, VersionRefReader};
|
|
17
|
+
use crate::GLOBAL_VERSION_ID;
|
|
18
|
+
use crate::{Backend, LixError, NullableKeyFilter};
|
|
19
|
+
|
|
20
|
+
#[derive(Clone)]
|
|
21
|
+
pub struct Engine {
|
|
22
|
+
storage: StorageContext,
|
|
23
|
+
tracked_state: Arc<TrackedStateContext>,
|
|
24
|
+
live_state: Arc<LiveStateContext>,
|
|
25
|
+
version_ctx: Arc<VersionContext>,
|
|
26
|
+
binary_cas: Arc<BinaryCasContext>,
|
|
27
|
+
changelog: Arc<ChangelogContext>,
|
|
28
|
+
schema_registry: Arc<SchemaRegistry>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Engine {
|
|
32
|
+
/// Seeds an empty backend with the engine2 repository bootstrap facts.
|
|
33
|
+
///
|
|
34
|
+
/// Initialization is a storage lifecycle operation, separate from runtime
|
|
35
|
+
/// construction. Call this before `Engine::new(...)` for a brand-new
|
|
36
|
+
/// backend.
|
|
37
|
+
pub async fn initialize(
|
|
38
|
+
backend: Box<dyn Backend + Send + Sync>,
|
|
39
|
+
) -> Result<InitReceipt, LixError> {
|
|
40
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::from(backend);
|
|
41
|
+
let storage = StorageContext::new(backend);
|
|
42
|
+
let changelog = ChangelogContext::new();
|
|
43
|
+
let commit_graph = CommitGraphContext::new(changelog);
|
|
44
|
+
let tracked_state = TrackedStateContext::new();
|
|
45
|
+
let untracked_state = UntrackedStateContext::new();
|
|
46
|
+
let live_state = LiveStateContext::new(tracked_state, untracked_state, commit_graph);
|
|
47
|
+
|
|
48
|
+
crate::init::initialize(storage, &changelog, &live_state).await
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Creates a clean DataFusion-first engine over an initialized backend.
|
|
52
|
+
///
|
|
53
|
+
/// SessionContext, execution, and transaction overlays are layered below the
|
|
54
|
+
/// instance instead of being hidden behind a legacy boot path.
|
|
55
|
+
pub async fn new(backend: Box<dyn Backend + Send + Sync>) -> Result<Self, LixError> {
|
|
56
|
+
let backend: Arc<dyn Backend + Send + Sync> = Arc::from(backend);
|
|
57
|
+
let storage = StorageContext::new(backend);
|
|
58
|
+
|
|
59
|
+
let tracked_state = Arc::new(TrackedStateContext::new());
|
|
60
|
+
let untracked_state = Arc::new(UntrackedStateContext::new());
|
|
61
|
+
let changelog = Arc::new(ChangelogContext::new());
|
|
62
|
+
let commit_graph = CommitGraphContext::new(changelog.as_ref().clone());
|
|
63
|
+
let live_state = Arc::new(LiveStateContext::new(
|
|
64
|
+
tracked_state.as_ref().clone(),
|
|
65
|
+
*untracked_state,
|
|
66
|
+
commit_graph,
|
|
67
|
+
));
|
|
68
|
+
let version_ctx = Arc::new(VersionContext::new(Arc::clone(&untracked_state)));
|
|
69
|
+
assert_initialized(storage.clone(), live_state.as_ref()).await?;
|
|
70
|
+
|
|
71
|
+
// SessionContext::execute later projects these stable state contexts into one
|
|
72
|
+
// execution-scoped SQL context, optionally wrapped by a transaction
|
|
73
|
+
// overlay for writes.
|
|
74
|
+
|
|
75
|
+
Ok(Self {
|
|
76
|
+
binary_cas: Arc::new(BinaryCasContext::new()),
|
|
77
|
+
changelog,
|
|
78
|
+
storage,
|
|
79
|
+
tracked_state,
|
|
80
|
+
live_state,
|
|
81
|
+
version_ctx,
|
|
82
|
+
schema_registry: Arc::new(SchemaRegistry::new()),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub(crate) fn storage(&self) -> StorageContext {
|
|
87
|
+
self.storage.clone()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[cfg(test)]
|
|
91
|
+
pub(crate) fn tracked_state(&self) -> Arc<TrackedStateContext> {
|
|
92
|
+
Arc::clone(&self.tracked_state)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Loads the current commit head for a version.
|
|
96
|
+
///
|
|
97
|
+
/// This is the public engine-level form of the typed `version_ref` context:
|
|
98
|
+
/// callers should not need to know that version heads are represented as
|
|
99
|
+
/// untracked `lix_version_ref` rows in live_state.
|
|
100
|
+
pub async fn load_version_head_commit_id(
|
|
101
|
+
&self,
|
|
102
|
+
version_id: &str,
|
|
103
|
+
) -> Result<Option<String>, LixError> {
|
|
104
|
+
let mut transaction = self.storage.begin_read_transaction().await?;
|
|
105
|
+
let result = self
|
|
106
|
+
.version_ctx
|
|
107
|
+
.ref_reader(transaction.as_mut())
|
|
108
|
+
.load_head_commit_id(version_id)
|
|
109
|
+
.await;
|
|
110
|
+
match result {
|
|
111
|
+
Ok(result) => {
|
|
112
|
+
transaction.rollback().await?;
|
|
113
|
+
Ok(result)
|
|
114
|
+
}
|
|
115
|
+
Err(error) => {
|
|
116
|
+
let _ = transaction.rollback().await;
|
|
117
|
+
Err(error)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pub async fn open_session(
|
|
123
|
+
&self,
|
|
124
|
+
active_version_id: impl Into<String>,
|
|
125
|
+
) -> Result<SessionContext, LixError> {
|
|
126
|
+
SessionContext::open(
|
|
127
|
+
active_version_id.into(),
|
|
128
|
+
self.storage(),
|
|
129
|
+
Arc::clone(&self.live_state),
|
|
130
|
+
Arc::clone(&self.tracked_state),
|
|
131
|
+
Arc::clone(&self.binary_cas),
|
|
132
|
+
Arc::clone(&self.changelog),
|
|
133
|
+
Arc::clone(&self.version_ctx),
|
|
134
|
+
Arc::clone(&self.schema_registry),
|
|
135
|
+
)
|
|
136
|
+
.await
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pub async fn open_workspace_session(&self) -> Result<SessionContext, LixError> {
|
|
140
|
+
SessionContext::open_workspace(
|
|
141
|
+
self.storage(),
|
|
142
|
+
Arc::clone(&self.live_state),
|
|
143
|
+
Arc::clone(&self.tracked_state),
|
|
144
|
+
Arc::clone(&self.binary_cas),
|
|
145
|
+
Arc::clone(&self.changelog),
|
|
146
|
+
Arc::clone(&self.version_ctx),
|
|
147
|
+
Arc::clone(&self.schema_registry),
|
|
148
|
+
)
|
|
149
|
+
.await
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Rebuilds the tracked serving projection for one version from changelog.
|
|
153
|
+
///
|
|
154
|
+
/// This is intentionally an engine-level operation: callers should not need
|
|
155
|
+
/// to know which KV namespaces back changelog, commit graph, or tracked
|
|
156
|
+
/// state. The current version head is read from the live-state facade so
|
|
157
|
+
/// rebuild uses the same moving-ref visibility as normal execution.
|
|
158
|
+
pub async fn rebuild_tracked_state_for_version(
|
|
159
|
+
&self,
|
|
160
|
+
version_id: &str,
|
|
161
|
+
) -> Result<(), LixError> {
|
|
162
|
+
let head_commit_id = self
|
|
163
|
+
.load_version_head_commit_id(version_id)
|
|
164
|
+
.await?
|
|
165
|
+
.ok_or_else(|| {
|
|
166
|
+
LixError::version_not_found(
|
|
167
|
+
version_id.to_string(),
|
|
168
|
+
"rebuild_tracked_state_for_version",
|
|
169
|
+
"target",
|
|
170
|
+
)
|
|
171
|
+
})?;
|
|
172
|
+
let commit_graph = CommitGraphContext::new(ChangelogContext::new());
|
|
173
|
+
let storage = self.storage();
|
|
174
|
+
let mut read_transaction = storage.begin_read_transaction().await?;
|
|
175
|
+
let mut transaction = storage.begin_write_transaction().await?;
|
|
176
|
+
let mut writes = StorageWriteSet::new();
|
|
177
|
+
let mut json_writer = JsonStoreContext::new().writer();
|
|
178
|
+
let rebuild_result = self
|
|
179
|
+
.tracked_state
|
|
180
|
+
.rebuild_state_at_commit(
|
|
181
|
+
&commit_graph,
|
|
182
|
+
read_transaction.as_mut(),
|
|
183
|
+
transaction.as_mut(),
|
|
184
|
+
&mut writes,
|
|
185
|
+
&mut json_writer,
|
|
186
|
+
&head_commit_id,
|
|
187
|
+
)
|
|
188
|
+
.await;
|
|
189
|
+
if let Err(error) = rebuild_result {
|
|
190
|
+
let _ = read_transaction.rollback().await;
|
|
191
|
+
let _ = transaction.rollback().await;
|
|
192
|
+
return Err(error);
|
|
193
|
+
}
|
|
194
|
+
if let Err(error) = read_transaction.rollback().await {
|
|
195
|
+
let _ = transaction.rollback().await;
|
|
196
|
+
return Err(error);
|
|
197
|
+
}
|
|
198
|
+
if let Err(error) = writes.apply(&mut transaction.as_mut()).await {
|
|
199
|
+
let _ = transaction.rollback().await;
|
|
200
|
+
return Err(error);
|
|
201
|
+
}
|
|
202
|
+
transaction.commit().await
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async fn assert_initialized(
|
|
207
|
+
storage: StorageContext,
|
|
208
|
+
live_state: &LiveStateContext,
|
|
209
|
+
) -> Result<(), LixError> {
|
|
210
|
+
let mut transaction = storage.begin_read_transaction().await?;
|
|
211
|
+
let reader = live_state.reader(transaction.as_mut());
|
|
212
|
+
let result = reader
|
|
213
|
+
.load_row(&LiveStateRowRequest {
|
|
214
|
+
schema_key: "lix_key_value".to_string(),
|
|
215
|
+
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
216
|
+
entity_id: EntityIdentity::single("lix_id"),
|
|
217
|
+
file_id: NullableKeyFilter::Null,
|
|
218
|
+
})
|
|
219
|
+
.await;
|
|
220
|
+
let initialized = match result {
|
|
221
|
+
Ok(row) => {
|
|
222
|
+
transaction.rollback().await?;
|
|
223
|
+
row.is_some()
|
|
224
|
+
}
|
|
225
|
+
Err(error) => {
|
|
226
|
+
let _ = transaction.rollback().await;
|
|
227
|
+
return Err(error);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if initialized {
|
|
232
|
+
return Ok(());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
Err(LixError::new(
|
|
236
|
+
"LIX_ERROR_NOT_INITIALIZED",
|
|
237
|
+
"engine2 backend is not initialized; call Engine::initialize(...) before Engine::new(...)",
|
|
238
|
+
))
|
|
239
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
use base64::Engine as _;
|
|
2
|
+
use serde_json::Value as JsonValue;
|
|
3
|
+
|
|
4
|
+
use crate::common::json_pointer_get;
|
|
5
|
+
use crate::LixError;
|
|
6
|
+
|
|
7
|
+
const COMPOSITE_ENTITY_ID_PREFIX: &str = "pk:v1:";
|
|
8
|
+
|
|
9
|
+
/// Logical entity identity derived from a schema primary key.
|
|
10
|
+
///
|
|
11
|
+
/// Keep this as typed tuple data inside engine2. The string projection exists
|
|
12
|
+
/// only for SQL/canonical boundaries that still expose a single `entity_id`.
|
|
13
|
+
#[derive(
|
|
14
|
+
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
|
15
|
+
)]
|
|
16
|
+
pub(crate) struct EntityIdentity {
|
|
17
|
+
pub(crate) parts: Vec<EntityIdentityPart>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(
|
|
21
|
+
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
|
22
|
+
)]
|
|
23
|
+
#[serde(tag = "type", content = "value")]
|
|
24
|
+
pub(crate) enum EntityIdentityPart {
|
|
25
|
+
String(String),
|
|
26
|
+
Bool(bool),
|
|
27
|
+
Number(String),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
31
|
+
pub(crate) enum EntityIdentityError {
|
|
32
|
+
EmptyPrimaryKey,
|
|
33
|
+
EmptyPrimaryKeyPath { index: usize },
|
|
34
|
+
MissingPrimaryKeyValue { index: usize },
|
|
35
|
+
NullPrimaryKeyValue { index: usize },
|
|
36
|
+
EmptyPrimaryKeyValue { index: usize },
|
|
37
|
+
UnsupportedPrimaryKeyValue { index: usize },
|
|
38
|
+
InvalidEncodedEntityIdentity,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl std::fmt::Display for EntityIdentityError {
|
|
42
|
+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
43
|
+
match self {
|
|
44
|
+
Self::EmptyPrimaryKey => {
|
|
45
|
+
write!(formatter, "primary key must contain at least one path")
|
|
46
|
+
}
|
|
47
|
+
Self::EmptyPrimaryKeyPath { index } => {
|
|
48
|
+
write!(
|
|
49
|
+
formatter,
|
|
50
|
+
"primary-key path at index {index} must not be empty"
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
Self::MissingPrimaryKeyValue { index } => {
|
|
54
|
+
write!(formatter, "primary-key value at index {index} is missing")
|
|
55
|
+
}
|
|
56
|
+
Self::NullPrimaryKeyValue { index } => {
|
|
57
|
+
write!(
|
|
58
|
+
formatter,
|
|
59
|
+
"primary-key value at index {index} must not be null"
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
Self::EmptyPrimaryKeyValue { index } => {
|
|
63
|
+
write!(
|
|
64
|
+
formatter,
|
|
65
|
+
"primary-key string value at index {index} must not be empty"
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
Self::UnsupportedPrimaryKeyValue { index } => write!(
|
|
69
|
+
formatter,
|
|
70
|
+
"primary-key value at index {index} must be a string, number, or boolean"
|
|
71
|
+
),
|
|
72
|
+
Self::InvalidEncodedEntityIdentity => {
|
|
73
|
+
write!(formatter, "encoded entity identity is invalid")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl EntityIdentity {
|
|
80
|
+
pub(crate) fn single(value: impl Into<String>) -> Self {
|
|
81
|
+
Self {
|
|
82
|
+
parts: vec![EntityIdentityPart::String(value.into())],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[cfg(test)]
|
|
87
|
+
pub(crate) fn tuple(parts: Vec<EntityIdentityPart>) -> Result<Self, EntityIdentityError> {
|
|
88
|
+
if parts.is_empty() {
|
|
89
|
+
return Err(EntityIdentityError::EmptyPrimaryKey);
|
|
90
|
+
}
|
|
91
|
+
Ok(Self { parts })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub(crate) fn from_primary_key_paths(
|
|
95
|
+
snapshot: &JsonValue,
|
|
96
|
+
primary_key_paths: &[Vec<String>],
|
|
97
|
+
) -> Result<Self, EntityIdentityError> {
|
|
98
|
+
if primary_key_paths.is_empty() {
|
|
99
|
+
return Err(EntityIdentityError::EmptyPrimaryKey);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let mut parts = Vec::with_capacity(primary_key_paths.len());
|
|
103
|
+
for (index, path) in primary_key_paths.iter().enumerate() {
|
|
104
|
+
if path.is_empty() {
|
|
105
|
+
return Err(EntityIdentityError::EmptyPrimaryKeyPath { index });
|
|
106
|
+
}
|
|
107
|
+
let Some(value) = json_pointer_get(snapshot, path) else {
|
|
108
|
+
return Err(EntityIdentityError::MissingPrimaryKeyValue { index });
|
|
109
|
+
};
|
|
110
|
+
parts.push(EntityIdentityPart::from_json_value(value, index)?);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Ok(Self { parts })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub(crate) fn as_string(&self) -> Result<String, LixError> {
|
|
117
|
+
if self.parts.is_empty() {
|
|
118
|
+
return Err(LixError::unknown(
|
|
119
|
+
"entity identity must contain at least one primary-key part",
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if let [EntityIdentityPart::String(value)] = self.parts.as_slice() {
|
|
124
|
+
return Ok(value.clone());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let payload = serde_json::to_vec(self).map_err(|error| {
|
|
128
|
+
LixError::unknown(format!(
|
|
129
|
+
"failed to encode composite entity identity: {error}"
|
|
130
|
+
))
|
|
131
|
+
})?;
|
|
132
|
+
Ok(format!(
|
|
133
|
+
"{COMPOSITE_ENTITY_ID_PREFIX}{}",
|
|
134
|
+
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload)
|
|
135
|
+
))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub(crate) fn from_string(entity_id: &str) -> Result<Self, EntityIdentityError> {
|
|
139
|
+
if let Some(encoded) = entity_id.strip_prefix(COMPOSITE_ENTITY_ID_PREFIX) {
|
|
140
|
+
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
141
|
+
.decode(encoded)
|
|
142
|
+
.map_err(|_| EntityIdentityError::InvalidEncodedEntityIdentity)?;
|
|
143
|
+
let identity = serde_json::from_slice::<Self>(&payload)
|
|
144
|
+
.map_err(|_| EntityIdentityError::InvalidEncodedEntityIdentity)?;
|
|
145
|
+
if identity.parts.is_empty() {
|
|
146
|
+
return Err(EntityIdentityError::InvalidEncodedEntityIdentity);
|
|
147
|
+
}
|
|
148
|
+
return Ok(identity);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Ok(Self::single(entity_id))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
impl EntityIdentityPart {
|
|
156
|
+
fn from_json_value(value: &JsonValue, index: usize) -> Result<Self, EntityIdentityError> {
|
|
157
|
+
match value {
|
|
158
|
+
JsonValue::Null => Err(EntityIdentityError::NullPrimaryKeyValue { index }),
|
|
159
|
+
JsonValue::String(value) if value.is_empty() => {
|
|
160
|
+
Err(EntityIdentityError::EmptyPrimaryKeyValue { index })
|
|
161
|
+
}
|
|
162
|
+
JsonValue::String(value) => Ok(Self::String(value.clone())),
|
|
163
|
+
JsonValue::Bool(value) => Ok(Self::Bool(*value)),
|
|
164
|
+
JsonValue::Number(value) => Ok(Self::Number(value.to_string())),
|
|
165
|
+
JsonValue::Array(_) | JsonValue::Object(_) => {
|
|
166
|
+
Err(EntityIdentityError::UnsupportedPrimaryKeyValue { index })
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[cfg(test)]
|
|
173
|
+
mod tests {
|
|
174
|
+
use serde_json::json;
|
|
175
|
+
|
|
176
|
+
use super::*;
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn single_string_identity_projects_to_plain_entity_id() {
|
|
180
|
+
let identity = EntityIdentity::single("plain-id");
|
|
181
|
+
|
|
182
|
+
assert_eq!(
|
|
183
|
+
identity.as_string().expect("projection should work"),
|
|
184
|
+
"plain-id"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn composite_identity_projects_to_versioned_opaque_entity_id() {
|
|
190
|
+
let identity = EntityIdentity::tuple(vec![
|
|
191
|
+
EntityIdentityPart::String("namespace".to_string()),
|
|
192
|
+
EntityIdentityPart::String("key".to_string()),
|
|
193
|
+
])
|
|
194
|
+
.expect("tuple identity");
|
|
195
|
+
|
|
196
|
+
let encoded = identity.as_string().expect("projection should work");
|
|
197
|
+
|
|
198
|
+
assert!(encoded.starts_with(COMPOSITE_ENTITY_ID_PREFIX));
|
|
199
|
+
assert!(!encoded.contains("namespace~key"));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn composite_identity_roundtrips_from_string() {
|
|
204
|
+
let identity = EntityIdentity::tuple(vec![
|
|
205
|
+
EntityIdentityPart::String("namespace".to_string()),
|
|
206
|
+
EntityIdentityPart::Number("42".to_string()),
|
|
207
|
+
EntityIdentityPart::Bool(true),
|
|
208
|
+
])
|
|
209
|
+
.expect("tuple identity");
|
|
210
|
+
|
|
211
|
+
let encoded = identity.as_string().expect("projection should work");
|
|
212
|
+
|
|
213
|
+
assert_eq!(
|
|
214
|
+
EntityIdentity::from_string(&encoded).expect("decode should work"),
|
|
215
|
+
identity
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn composite_identity_does_not_collide_on_delimiter_like_values() {
|
|
221
|
+
let left = EntityIdentity::tuple(vec![
|
|
222
|
+
EntityIdentityPart::String("a~b".to_string()),
|
|
223
|
+
EntityIdentityPart::String("1".to_string()),
|
|
224
|
+
])
|
|
225
|
+
.expect("left tuple identity");
|
|
226
|
+
let right = EntityIdentity::tuple(vec![
|
|
227
|
+
EntityIdentityPart::String("a".to_string()),
|
|
228
|
+
EntityIdentityPart::String("b~1".to_string()),
|
|
229
|
+
])
|
|
230
|
+
.expect("right tuple identity");
|
|
231
|
+
|
|
232
|
+
assert_ne!(
|
|
233
|
+
left.as_string().expect("left should encode"),
|
|
234
|
+
right.as_string().expect("right should encode")
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn from_string_treats_plain_string_as_single_string_identity() {
|
|
240
|
+
assert_eq!(
|
|
241
|
+
EntityIdentity::single("plain-id"),
|
|
242
|
+
EntityIdentity::single("plain-id")
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn from_primary_key_paths_derives_ordered_parts() {
|
|
248
|
+
let snapshot = json!({
|
|
249
|
+
"namespace": "messages",
|
|
250
|
+
"index": 7,
|
|
251
|
+
"active": true
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
let identity = EntityIdentity::from_primary_key_paths(
|
|
255
|
+
&snapshot,
|
|
256
|
+
&[
|
|
257
|
+
vec!["namespace".to_string()],
|
|
258
|
+
vec!["index".to_string()],
|
|
259
|
+
vec!["active".to_string()],
|
|
260
|
+
],
|
|
261
|
+
)
|
|
262
|
+
.expect("primary key should derive");
|
|
263
|
+
|
|
264
|
+
assert_eq!(
|
|
265
|
+
identity,
|
|
266
|
+
EntityIdentity {
|
|
267
|
+
parts: vec![
|
|
268
|
+
EntityIdentityPart::String("messages".to_string()),
|
|
269
|
+
EntityIdentityPart::Number("7".to_string()),
|
|
270
|
+
EntityIdentityPart::Bool(true),
|
|
271
|
+
],
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#[test]
|
|
277
|
+
fn from_primary_key_paths_rejects_missing_parts() {
|
|
278
|
+
let snapshot = json!({ "id": "a" });
|
|
279
|
+
|
|
280
|
+
assert_eq!(
|
|
281
|
+
EntityIdentity::from_primary_key_paths(&snapshot, &[vec!["missing".to_string()]]),
|
|
282
|
+
Err(EntityIdentityError::MissingPrimaryKeyValue { index: 0 })
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|