@lix-js/sdk 0.6.0-preview.2 → 0.6.0-preview.4
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 +46 -8
- package/dist/engine-wasm/wasm/lix_engine.d.ts +25 -1
- package/dist/engine-wasm/wasm/lix_engine.js +60 -2
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +5 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +10 -3
- package/dist/open-lix.js +39 -0
- package/dist-engine-src/src/binary_cas/types.rs +0 -6
- package/dist-engine-src/src/catalog/context.rs +412 -0
- package/dist-engine-src/src/catalog/mod.rs +10 -0
- package/dist-engine-src/src/catalog/schema.rs +4 -0
- package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
- package/dist-engine-src/src/cel/mod.rs +1 -1
- package/dist-engine-src/src/cel/provider.rs +1 -1
- package/dist-engine-src/src/commit_graph/context.rs +328 -1015
- package/dist-engine-src/src/commit_graph/mod.rs +2 -3
- package/dist-engine-src/src/commit_graph/types.rs +7 -43
- package/dist-engine-src/src/commit_graph/walker.rs +57 -81
- package/dist-engine-src/src/commit_store/codec.rs +887 -0
- package/dist-engine-src/src/commit_store/context.rs +944 -0
- package/dist-engine-src/src/commit_store/materialization.rs +84 -0
- package/dist-engine-src/src/commit_store/mod.rs +16 -0
- package/dist-engine-src/src/commit_store/storage.rs +600 -0
- package/dist-engine-src/src/commit_store/types.rs +215 -0
- package/dist-engine-src/src/common/identity.rs +15 -5
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +17 -12
- package/dist-engine-src/src/common/mod.rs +5 -5
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +29 -43
- package/dist-engine-src/src/entity_identity.rs +238 -118
- package/dist-engine-src/src/functions/context.rs +17 -52
- package/dist-engine-src/src/functions/deterministic.rs +1 -1
- package/dist-engine-src/src/functions/mod.rs +1 -1
- package/dist-engine-src/src/functions/provider.rs +4 -4
- package/dist-engine-src/src/functions/state.rs +39 -66
- package/dist-engine-src/src/functions/types.rs +1 -1
- package/dist-engine-src/src/init.rs +204 -151
- package/dist-engine-src/src/json_store/context.rs +354 -60
- package/dist-engine-src/src/json_store/encoded.rs +6 -6
- package/dist-engine-src/src/json_store/mod.rs +4 -1
- package/dist-engine-src/src/json_store/store.rs +884 -11
- package/dist-engine-src/src/json_store/types.rs +166 -1
- package/dist-engine-src/src/lib.rs +11 -10
- package/dist-engine-src/src/live_state/context.rs +608 -830
- package/dist-engine-src/src/live_state/mod.rs +3 -3
- package/dist-engine-src/src/live_state/overlay.rs +7 -7
- package/dist-engine-src/src/live_state/reader.rs +5 -5
- package/dist-engine-src/src/live_state/types.rs +19 -36
- package/dist-engine-src/src/live_state/visibility.rs +19 -14
- package/dist-engine-src/src/plugin/archive.rs +3 -6
- package/dist-engine-src/src/plugin/install.rs +0 -18
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
- package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +47 -17
- package/dist-engine-src/src/schema/definition.rs +202 -96
- package/dist-engine-src/src/schema/key.rs +9 -77
- package/dist-engine-src/src/schema/mod.rs +4 -4
- package/dist-engine-src/src/schema/tests.rs +133 -92
- package/dist-engine-src/src/session/context.rs +86 -48
- package/dist-engine-src/src/session/create_version.rs +22 -14
- package/dist-engine-src/src/session/execute.rs +117 -23
- package/dist-engine-src/src/session/merge/apply.rs +4 -4
- package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
- package/dist-engine-src/src/session/merge/stats.rs +1 -1
- package/dist-engine-src/src/session/merge/version.rs +35 -45
- package/dist-engine-src/src/session/mod.rs +9 -7
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +17 -28
- package/dist-engine-src/src/session/transaction.rs +76 -0
- package/dist-engine-src/src/sql2/change_provider.rs +14 -20
- package/dist-engine-src/src/sql2/classify.rs +75 -48
- package/dist-engine-src/src/sql2/context.rs +22 -18
- package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
- package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
- package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
- package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
- package/dist-engine-src/src/sql2/error.rs +24 -5
- package/dist-engine-src/src/sql2/execute.rs +426 -272
- package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
- package/dist-engine-src/src/sql2/file_provider.rs +533 -108
- package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
- package/dist-engine-src/src/sql2/history_projection.rs +3 -27
- package/dist-engine-src/src/sql2/history_provider.rs +11 -17
- package/dist-engine-src/src/sql2/history_route.rs +22 -8
- package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
- package/dist-engine-src/src/sql2/mod.rs +8 -4
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
- package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
- package/dist-engine-src/src/sql2/public_bind/dml.rs +172 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +26 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +10 -12
- package/dist-engine-src/src/sql2/session.rs +7 -10
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
- package/dist-engine-src/src/sql2/udfs/public_call.rs +238 -0
- package/dist-engine-src/src/sql2/version_provider.rs +46 -31
- package/dist-engine-src/src/sql2/version_scope.rs +4 -4
- package/dist-engine-src/src/storage_bench.rs +1782 -325
- package/dist-engine-src/src/test_support.rs +183 -36
- package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
- package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
- package/dist-engine-src/src/tracked_state/context.rs +1155 -271
- package/dist-engine-src/src/tracked_state/diff.rs +249 -57
- package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +37 -19
- package/dist-engine-src/src/tracked_state/mod.rs +8 -7
- package/dist-engine-src/src/tracked_state/storage.rs +138 -6
- package/dist-engine-src/src/tracked_state/tree.rs +695 -252
- package/dist-engine-src/src/tracked_state/types.rs +176 -6
- package/dist-engine-src/src/transaction/commit.rs +695 -435
- package/dist-engine-src/src/transaction/context.rs +551 -310
- package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
- package/dist-engine-src/src/transaction/mod.rs +2 -0
- package/dist-engine-src/src/transaction/normalization.rs +311 -447
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
- package/dist-engine-src/src/transaction/staging.rs +701 -406
- package/dist-engine-src/src/transaction/types.rs +231 -122
- package/dist-engine-src/src/transaction/validation.rs +2717 -1698
- package/dist-engine-src/src/untracked_state/codec.rs +40 -96
- package/dist-engine-src/src/untracked_state/context.rs +21 -5
- package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
- package/dist-engine-src/src/untracked_state/mod.rs +3 -5
- package/dist-engine-src/src/untracked_state/storage.rs +105 -57
- package/dist-engine-src/src/untracked_state/types.rs +63 -13
- package/dist-engine-src/src/version/context.rs +1 -13
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +3 -2
- package/dist-engine-src/src/version/refs.rs +12 -103
- package/dist-engine-src/src/version/stage_rows.rs +15 -19
- package/package.json +1 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -321
- package/dist-engine-src/src/changelog/context.rs +0 -92
- package/dist-engine-src/src/changelog/materialization.rs +0 -121
- package/dist-engine-src/src/changelog/mod.rs +0 -13
- package/dist-engine-src/src/changelog/reader.rs +0 -20
- package/dist-engine-src/src/changelog/storage.rs +0 -220
- package/dist-engine-src/src/changelog/types.rs +0 -38
- package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
- package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
- package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
- package/dist-engine-src/src/schema_registry.rs +0 -294
- package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
- package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
- package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
|
@@ -1,39 +1,25 @@
|
|
|
1
|
-
use base64::Engine as _;
|
|
2
1
|
use serde_json::Value as JsonValue;
|
|
3
2
|
|
|
4
3
|
use crate::common::json_pointer_get;
|
|
5
4
|
use crate::LixError;
|
|
6
5
|
|
|
7
|
-
const COMPOSITE_ENTITY_ID_PREFIX: &str = "pk:v1:";
|
|
8
|
-
|
|
9
6
|
/// Logical entity identity derived from a schema primary key.
|
|
10
7
|
///
|
|
11
|
-
/// Keep this as typed tuple data inside
|
|
12
|
-
///
|
|
8
|
+
/// Keep this as typed tuple data inside engine. SQL `entity_id` surfaces
|
|
9
|
+
/// should use the JSON-array projection.
|
|
13
10
|
#[derive(
|
|
14
11
|
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
|
15
12
|
)]
|
|
16
13
|
pub(crate) struct EntityIdentity {
|
|
17
|
-
pub(crate) parts: Vec<
|
|
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),
|
|
14
|
+
pub(crate) parts: Vec<String>,
|
|
28
15
|
}
|
|
29
16
|
|
|
30
17
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
31
18
|
pub(crate) enum EntityIdentityError {
|
|
32
19
|
EmptyPrimaryKey,
|
|
33
20
|
EmptyPrimaryKeyPath { index: usize },
|
|
34
|
-
MissingPrimaryKeyValue { index: usize },
|
|
35
|
-
NullPrimaryKeyValue { index: usize },
|
|
36
21
|
EmptyPrimaryKeyValue { index: usize },
|
|
22
|
+
MissingPrimaryKeyValue { index: usize },
|
|
37
23
|
UnsupportedPrimaryKeyValue { index: usize },
|
|
38
24
|
InvalidEncodedEntityIdentity,
|
|
39
25
|
}
|
|
@@ -50,27 +36,24 @@ impl std::fmt::Display for EntityIdentityError {
|
|
|
50
36
|
"primary-key path at index {index} must not be empty"
|
|
51
37
|
)
|
|
52
38
|
}
|
|
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
39
|
Self::EmptyPrimaryKeyValue { index } => {
|
|
63
40
|
write!(
|
|
64
41
|
formatter,
|
|
65
|
-
"primary-key
|
|
42
|
+
"primary-key value at index {index} must not be empty"
|
|
66
43
|
)
|
|
67
44
|
}
|
|
45
|
+
Self::MissingPrimaryKeyValue { index } => {
|
|
46
|
+
write!(formatter, "primary-key value at index {index} is missing")
|
|
47
|
+
}
|
|
68
48
|
Self::UnsupportedPrimaryKeyValue { index } => write!(
|
|
69
49
|
formatter,
|
|
70
|
-
"primary-key value at index {index} must be a string
|
|
50
|
+
"primary-key value at index {index} must be a JSON string"
|
|
71
51
|
),
|
|
72
52
|
Self::InvalidEncodedEntityIdentity => {
|
|
73
|
-
write!(
|
|
53
|
+
write!(
|
|
54
|
+
formatter,
|
|
55
|
+
"encoded entity identity must be a non-empty JSON array of strings"
|
|
56
|
+
)
|
|
74
57
|
}
|
|
75
58
|
}
|
|
76
59
|
}
|
|
@@ -79,15 +62,18 @@ impl std::fmt::Display for EntityIdentityError {
|
|
|
79
62
|
impl EntityIdentity {
|
|
80
63
|
pub(crate) fn single(value: impl Into<String>) -> Self {
|
|
81
64
|
Self {
|
|
82
|
-
parts: vec![
|
|
65
|
+
parts: vec![value.into()],
|
|
83
66
|
}
|
|
84
67
|
}
|
|
85
68
|
|
|
86
69
|
#[cfg(test)]
|
|
87
|
-
pub(crate) fn tuple(parts: Vec<
|
|
70
|
+
pub(crate) fn tuple(parts: Vec<String>) -> Result<Self, EntityIdentityError> {
|
|
88
71
|
if parts.is_empty() {
|
|
89
72
|
return Err(EntityIdentityError::EmptyPrimaryKey);
|
|
90
73
|
}
|
|
74
|
+
if let Some((index, _)) = parts.iter().enumerate().find(|(_, part)| part.is_empty()) {
|
|
75
|
+
return Err(EntityIdentityError::EmptyPrimaryKeyValue { index });
|
|
76
|
+
}
|
|
91
77
|
Ok(Self { parts })
|
|
92
78
|
}
|
|
93
79
|
|
|
@@ -107,65 +93,110 @@ impl EntityIdentity {
|
|
|
107
93
|
let Some(value) = json_pointer_get(snapshot, path) else {
|
|
108
94
|
return Err(EntityIdentityError::MissingPrimaryKeyValue { index });
|
|
109
95
|
};
|
|
110
|
-
parts.push(
|
|
96
|
+
parts.push(string_part_from_json_value(value, index)?);
|
|
111
97
|
}
|
|
112
98
|
|
|
113
99
|
Ok(Self { parts })
|
|
114
100
|
}
|
|
115
101
|
|
|
116
|
-
pub(crate) fn
|
|
102
|
+
pub(crate) fn as_json_array_value(&self) -> Result<JsonValue, LixError> {
|
|
117
103
|
if self.parts.is_empty() {
|
|
118
104
|
return Err(LixError::unknown(
|
|
119
105
|
"entity identity must contain at least one primary-key part",
|
|
120
106
|
));
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
Ok(JsonValue::Array(
|
|
110
|
+
self.parts
|
|
111
|
+
.iter()
|
|
112
|
+
.map(|part| JsonValue::String(part.clone()))
|
|
113
|
+
.collect(),
|
|
114
|
+
))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pub(crate) fn as_json_array_text(&self) -> Result<String, LixError> {
|
|
118
|
+
serde_json::to_string(&self.as_json_array_value()?).map_err(|error| {
|
|
119
|
+
LixError::unknown(format!("failed to encode entity id as JSON: {error}"))
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pub(crate) fn as_single_string(&self) -> Result<&str, LixError> {
|
|
124
|
+
if self.parts.is_empty() {
|
|
125
|
+
return Err(LixError::unknown(
|
|
126
|
+
"entity identity must contain at least one primary-key part",
|
|
127
|
+
));
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"{COMPOSITE_ENTITY_ID_PREFIX}{}",
|
|
134
|
-
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload)
|
|
130
|
+
if let [value] = self.parts.as_slice() {
|
|
131
|
+
return Ok(value.as_str());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Err(LixError::unknown(
|
|
135
|
+
"entity identity is not a single string primary-key tuple",
|
|
135
136
|
))
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
pub(crate) fn
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
pub(crate) fn as_single_string_owned(&self) -> Result<String, LixError> {
|
|
140
|
+
Ok(self.as_single_string()?.to_owned())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub(crate) fn from_json_array_text(entity_id: &str) -> Result<Self, EntityIdentityError> {
|
|
144
|
+
let value = serde_json::from_str::<JsonValue>(entity_id)
|
|
145
|
+
.map_err(|_| EntityIdentityError::InvalidEncodedEntityIdentity)?;
|
|
146
|
+
Self::from_json_array_value(&value)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub(crate) fn from_json_array_value(
|
|
150
|
+
entity_id: &JsonValue,
|
|
151
|
+
) -> Result<Self, EntityIdentityError> {
|
|
152
|
+
let JsonValue::Array(values) = entity_id else {
|
|
153
|
+
return Err(EntityIdentityError::InvalidEncodedEntityIdentity);
|
|
154
|
+
};
|
|
155
|
+
if values.is_empty() {
|
|
156
|
+
return Err(EntityIdentityError::EmptyPrimaryKey);
|
|
149
157
|
}
|
|
150
158
|
|
|
151
|
-
|
|
159
|
+
let mut parts = Vec::with_capacity(values.len());
|
|
160
|
+
for (index, value) in values.iter().enumerate() {
|
|
161
|
+
parts.push(string_part_from_json_value(value, index)?);
|
|
162
|
+
}
|
|
163
|
+
Ok(Self { parts })
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
166
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
fn string_part_from_json_value(
|
|
168
|
+
value: &JsonValue,
|
|
169
|
+
index: usize,
|
|
170
|
+
) -> Result<String, EntityIdentityError> {
|
|
171
|
+
match value {
|
|
172
|
+
JsonValue::String(value) if value.is_empty() => {
|
|
173
|
+
Err(EntityIdentityError::EmptyPrimaryKeyValue { index })
|
|
174
|
+
}
|
|
175
|
+
JsonValue::String(value) => Ok(value.clone()),
|
|
176
|
+
_ => Err(EntityIdentityError::UnsupportedPrimaryKeyValue { index }),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
pub(crate) fn canonical_json_text(value: &JsonValue) -> serde_json::Result<String> {
|
|
181
|
+
serde_json::to_string(&canonical_json_value(value))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn canonical_json_value(value: &JsonValue) -> JsonValue {
|
|
185
|
+
match value {
|
|
186
|
+
JsonValue::Array(values) => {
|
|
187
|
+
JsonValue::Array(values.iter().map(canonical_json_value).collect())
|
|
188
|
+
}
|
|
189
|
+
JsonValue::Object(object) => {
|
|
190
|
+
let mut entries = object.iter().collect::<Vec<_>>();
|
|
191
|
+
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
|
|
192
|
+
|
|
193
|
+
let mut canonical = serde_json::Map::new();
|
|
194
|
+
for (key, value) in entries {
|
|
195
|
+
canonical.insert(key.clone(), canonical_json_value(value));
|
|
167
196
|
}
|
|
197
|
+
JsonValue::Object(canonical)
|
|
168
198
|
}
|
|
199
|
+
_ => value.clone(),
|
|
169
200
|
}
|
|
170
201
|
}
|
|
171
202
|
|
|
@@ -176,70 +207,101 @@ mod tests {
|
|
|
176
207
|
use super::*;
|
|
177
208
|
|
|
178
209
|
#[test]
|
|
179
|
-
fn
|
|
210
|
+
fn single_string_identity_projects_to_single_string() {
|
|
180
211
|
let identity = EntityIdentity::single("plain-id");
|
|
181
212
|
|
|
182
213
|
assert_eq!(
|
|
183
|
-
identity.
|
|
214
|
+
identity.as_single_string().expect("projection should work"),
|
|
184
215
|
"plain-id"
|
|
185
216
|
);
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
#[test]
|
|
189
|
-
fn
|
|
190
|
-
let identity = EntityIdentity::
|
|
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");
|
|
220
|
+
fn single_identity_projects_to_json_array_entity_id() {
|
|
221
|
+
let identity = EntityIdentity::single("plain-id");
|
|
197
222
|
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
assert_eq!(
|
|
224
|
+
identity
|
|
225
|
+
.as_json_array_text()
|
|
226
|
+
.expect("projection should work"),
|
|
227
|
+
"[\"plain-id\"]"
|
|
228
|
+
);
|
|
200
229
|
}
|
|
201
230
|
|
|
202
231
|
#[test]
|
|
203
|
-
fn
|
|
204
|
-
let identity = EntityIdentity::tuple(vec![
|
|
205
|
-
|
|
206
|
-
EntityIdentityPart::Number("42".to_string()),
|
|
207
|
-
EntityIdentityPart::Bool(true),
|
|
208
|
-
])
|
|
209
|
-
.expect("tuple identity");
|
|
232
|
+
fn composite_identity_projects_to_json_array_entity_id() {
|
|
233
|
+
let identity = EntityIdentity::tuple(vec!["namespace".to_string(), "42".to_string()])
|
|
234
|
+
.expect("tuple identity");
|
|
210
235
|
|
|
211
|
-
|
|
236
|
+
assert_eq!(
|
|
237
|
+
identity
|
|
238
|
+
.as_json_array_text()
|
|
239
|
+
.expect("projection should work"),
|
|
240
|
+
"[\"namespace\",\"42\"]"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[test]
|
|
245
|
+
fn entity_id_json_array_roundtrips() {
|
|
246
|
+
let identity = EntityIdentity::tuple(vec!["namespace".to_string(), "42".to_string()])
|
|
247
|
+
.expect("tuple identity");
|
|
248
|
+
let encoded = identity
|
|
249
|
+
.as_json_array_text()
|
|
250
|
+
.expect("projection should work");
|
|
212
251
|
|
|
213
252
|
assert_eq!(
|
|
214
|
-
EntityIdentity::
|
|
253
|
+
EntityIdentity::from_json_array_text(&encoded).expect("decode should work"),
|
|
215
254
|
identity
|
|
216
255
|
);
|
|
217
256
|
}
|
|
218
257
|
|
|
219
258
|
#[test]
|
|
220
|
-
fn
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
259
|
+
fn entity_id_json_array_rejects_empty_string_part() {
|
|
260
|
+
assert_eq!(
|
|
261
|
+
EntityIdentity::from_json_array_text("[\"\"]"),
|
|
262
|
+
Err(EntityIdentityError::EmptyPrimaryKeyValue { index: 0 })
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn tuple_rejects_empty_string_part() {
|
|
268
|
+
assert_eq!(
|
|
269
|
+
EntityIdentity::tuple(vec!["namespace".to_string(), "".to_string()]),
|
|
270
|
+
Err(EntityIdentityError::EmptyPrimaryKeyValue { index: 1 })
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn entity_id_json_array_does_not_collide_on_delimiter_like_values() {
|
|
276
|
+
let left = EntityIdentity::tuple(vec!["a~b".to_string(), "c".to_string()])
|
|
277
|
+
.expect("left tuple identity");
|
|
278
|
+
let right = EntityIdentity::tuple(vec!["a".to_string(), "b~c".to_string()])
|
|
279
|
+
.expect("right tuple identity");
|
|
231
280
|
|
|
232
281
|
assert_ne!(
|
|
233
|
-
left.
|
|
234
|
-
right.
|
|
282
|
+
left.as_json_array_text().expect("left should encode"),
|
|
283
|
+
right.as_json_array_text().expect("right should encode")
|
|
235
284
|
);
|
|
236
285
|
}
|
|
237
286
|
|
|
238
287
|
#[test]
|
|
239
|
-
fn
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
288
|
+
fn composite_identity_rejects_single_string_projection() {
|
|
289
|
+
let identity = EntityIdentity::tuple(vec!["namespace".to_string(), "42".to_string()])
|
|
290
|
+
.expect("tuple identity");
|
|
291
|
+
|
|
292
|
+
assert!(identity.as_single_string().is_err());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn composite_identity_does_not_collide_on_delimiter_like_values() {
|
|
297
|
+
let left = EntityIdentity::tuple(vec!["a~b".to_string(), "1".to_string()])
|
|
298
|
+
.expect("left tuple identity");
|
|
299
|
+
let right = EntityIdentity::tuple(vec!["a".to_string(), "b~1".to_string()])
|
|
300
|
+
.expect("right tuple identity");
|
|
301
|
+
|
|
302
|
+
assert_ne!(
|
|
303
|
+
left.as_json_array_text().expect("left should encode"),
|
|
304
|
+
right.as_json_array_text().expect("right should encode")
|
|
243
305
|
);
|
|
244
306
|
}
|
|
245
307
|
|
|
@@ -247,32 +309,90 @@ mod tests {
|
|
|
247
309
|
fn from_primary_key_paths_derives_ordered_parts() {
|
|
248
310
|
let snapshot = json!({
|
|
249
311
|
"namespace": "messages",
|
|
250
|
-
"
|
|
251
|
-
"active": true
|
|
312
|
+
"locale": "en"
|
|
252
313
|
});
|
|
253
314
|
|
|
254
315
|
let identity = EntityIdentity::from_primary_key_paths(
|
|
255
316
|
&snapshot,
|
|
256
|
-
&[
|
|
257
|
-
vec!["namespace".to_string()],
|
|
258
|
-
vec!["index".to_string()],
|
|
259
|
-
vec!["active".to_string()],
|
|
260
|
-
],
|
|
317
|
+
&[vec!["namespace".to_string()], vec!["locale".to_string()]],
|
|
261
318
|
)
|
|
262
319
|
.expect("primary key should derive");
|
|
263
320
|
|
|
264
321
|
assert_eq!(
|
|
265
322
|
identity,
|
|
266
323
|
EntityIdentity {
|
|
267
|
-
parts: vec![
|
|
268
|
-
EntityIdentityPart::String("messages".to_string()),
|
|
269
|
-
EntityIdentityPart::Number("7".to_string()),
|
|
270
|
-
EntityIdentityPart::Bool(true),
|
|
271
|
-
],
|
|
324
|
+
parts: vec!["messages".to_string(), "en".to_string()],
|
|
272
325
|
}
|
|
273
326
|
);
|
|
274
327
|
}
|
|
275
328
|
|
|
329
|
+
#[test]
|
|
330
|
+
fn entity_id_json_array_rejects_non_string_parts() {
|
|
331
|
+
assert_eq!(
|
|
332
|
+
EntityIdentity::from_json_array_text("[\"namespace\",42]"),
|
|
333
|
+
Err(EntityIdentityError::UnsupportedPrimaryKeyValue { index: 1 })
|
|
334
|
+
);
|
|
335
|
+
assert_eq!(
|
|
336
|
+
EntityIdentity::from_json_array_text("[\"namespace\",null]"),
|
|
337
|
+
Err(EntityIdentityError::UnsupportedPrimaryKeyValue { index: 1 })
|
|
338
|
+
);
|
|
339
|
+
assert_eq!(
|
|
340
|
+
EntityIdentity::from_json_array_text("[[\"nested\"]]"),
|
|
341
|
+
Err(EntityIdentityError::UnsupportedPrimaryKeyValue { index: 0 })
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[test]
|
|
346
|
+
fn from_primary_key_paths_rejects_non_string_parts() {
|
|
347
|
+
let snapshot = json!({
|
|
348
|
+
"namespace": "messages",
|
|
349
|
+
"index": 7
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
assert_eq!(
|
|
353
|
+
EntityIdentity::from_primary_key_paths(
|
|
354
|
+
&snapshot,
|
|
355
|
+
&[vec!["namespace".to_string()], vec!["index".to_string()],],
|
|
356
|
+
),
|
|
357
|
+
Err(EntityIdentityError::UnsupportedPrimaryKeyValue { index: 1 })
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn from_primary_key_paths_rejects_empty_string_parts() {
|
|
363
|
+
let snapshot = json!({
|
|
364
|
+
"namespace": "messages",
|
|
365
|
+
"id": ""
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
assert_eq!(
|
|
369
|
+
EntityIdentity::from_primary_key_paths(
|
|
370
|
+
&snapshot,
|
|
371
|
+
&[vec!["namespace".to_string()], vec!["id".to_string()],],
|
|
372
|
+
),
|
|
373
|
+
Err(EntityIdentityError::EmptyPrimaryKeyValue { index: 1 })
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[test]
|
|
378
|
+
fn from_primary_key_paths_rejects_nested_json_parts() {
|
|
379
|
+
let snapshot = json!({
|
|
380
|
+
"entity_id": ["welcome.title", "en"],
|
|
381
|
+
"schema_key": "message"
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
assert_eq!(
|
|
385
|
+
EntityIdentity::from_primary_key_paths(
|
|
386
|
+
&snapshot,
|
|
387
|
+
&[
|
|
388
|
+
vec!["entity_id".to_string()],
|
|
389
|
+
vec!["schema_key".to_string()],
|
|
390
|
+
],
|
|
391
|
+
),
|
|
392
|
+
Err(EntityIdentityError::UnsupportedPrimaryKeyValue { index: 0 })
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
276
396
|
#[test]
|
|
277
397
|
fn from_primary_key_paths_rejects_missing_parts() {
|
|
278
398
|
let snapshot = json!({ "id": "a" });
|
|
@@ -2,9 +2,8 @@ use crate::functions::{
|
|
|
2
2
|
state, DeterministicFunctionProvider, DeterministicSequence, FunctionProvider,
|
|
3
3
|
FunctionProviderHandle, SharedFunctionProvider, SystemFunctionProvider,
|
|
4
4
|
};
|
|
5
|
-
use crate::
|
|
6
|
-
use crate::
|
|
7
|
-
use crate::storage::{StorageReader, StorageWriteSet};
|
|
5
|
+
use crate::live_state::LiveStateReader;
|
|
6
|
+
use crate::storage::StorageWriteSet;
|
|
8
7
|
use crate::LixError;
|
|
9
8
|
|
|
10
9
|
/// Execution-scoped runtime function context.
|
|
@@ -47,7 +46,7 @@ impl FunctionContext {
|
|
|
47
46
|
})
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
/// Returns the
|
|
49
|
+
/// Returns the engine-owned provider used by SQL and transaction staging.
|
|
51
50
|
pub(crate) fn provider(&self) -> FunctionProviderHandle {
|
|
52
51
|
self.functions.clone()
|
|
53
52
|
}
|
|
@@ -56,23 +55,16 @@ impl FunctionContext {
|
|
|
56
55
|
///
|
|
57
56
|
/// System functions report no sequence state, so this is a no-op when
|
|
58
57
|
/// deterministic mode is disabled.
|
|
59
|
-
pub(crate) async fn stage_persist_if_needed
|
|
58
|
+
pub(crate) async fn stage_persist_if_needed(
|
|
60
59
|
&self,
|
|
61
|
-
writer: &mut LiveStateWriter<S>,
|
|
62
60
|
writes: &mut StorageWriteSet,
|
|
63
|
-
|
|
64
|
-
) -> Result<(), LixError>
|
|
65
|
-
where
|
|
66
|
-
S: StorageReader,
|
|
67
|
-
{
|
|
61
|
+
) -> Result<(), LixError> {
|
|
68
62
|
let Some(highest_seen) = self.functions.deterministic_sequence_persist_highest_seen()
|
|
69
63
|
else {
|
|
70
64
|
return Ok(());
|
|
71
65
|
};
|
|
72
66
|
state::stage_sequence(
|
|
73
|
-
writer,
|
|
74
67
|
writes,
|
|
75
|
-
json_writer,
|
|
76
68
|
DeterministicSequence { highest_seen },
|
|
77
69
|
&self.bookkeeping_timestamp,
|
|
78
70
|
)
|
|
@@ -87,7 +79,7 @@ mod tests {
|
|
|
87
79
|
use crate::backend::testing::UnitTestBackend;
|
|
88
80
|
use crate::functions::state::{DETERMINISTIC_MODE_KEY, DETERMINISTIC_SEQUENCE_KEY};
|
|
89
81
|
use crate::functions::{state::load_sequence, DeterministicSequence};
|
|
90
|
-
use crate::live_state::
|
|
82
|
+
use crate::live_state::LiveStateContext;
|
|
91
83
|
use crate::storage::StorageContext;
|
|
92
84
|
use crate::GLOBAL_VERSION_ID;
|
|
93
85
|
|
|
@@ -97,7 +89,7 @@ mod tests {
|
|
|
97
89
|
LiveStateContext::new(
|
|
98
90
|
crate::tracked_state::TrackedStateContext::new(),
|
|
99
91
|
crate::untracked_state::UntrackedStateContext::new(),
|
|
100
|
-
crate::commit_graph::CommitGraphContext::new(
|
|
92
|
+
crate::commit_graph::CommitGraphContext::new(),
|
|
101
93
|
)
|
|
102
94
|
}
|
|
103
95
|
|
|
@@ -128,7 +120,6 @@ mod tests {
|
|
|
128
120
|
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
129
121
|
write_key_value(
|
|
130
122
|
storage.clone(),
|
|
131
|
-
&live_state,
|
|
132
123
|
DETERMINISTIC_MODE_KEY,
|
|
133
124
|
serde_json::json!({
|
|
134
125
|
"enabled": true,
|
|
@@ -163,7 +154,6 @@ mod tests {
|
|
|
163
154
|
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
164
155
|
write_key_value(
|
|
165
156
|
storage.clone(),
|
|
166
|
-
&live_state,
|
|
167
157
|
DETERMINISTIC_MODE_KEY,
|
|
168
158
|
serde_json::json!({
|
|
169
159
|
"enabled": true,
|
|
@@ -172,7 +162,6 @@ mod tests {
|
|
|
172
162
|
.await;
|
|
173
163
|
write_key_value(
|
|
174
164
|
storage.clone(),
|
|
175
|
-
&live_state,
|
|
176
165
|
DETERMINISTIC_SEQUENCE_KEY,
|
|
177
166
|
serde_json::json!(41),
|
|
178
167
|
)
|
|
@@ -204,7 +193,6 @@ mod tests {
|
|
|
204
193
|
crate::test_support::seed_global_version_head(storage.clone()).await;
|
|
205
194
|
write_key_value(
|
|
206
195
|
storage.clone(),
|
|
207
|
-
&live_state,
|
|
208
196
|
DETERMINISTIC_MODE_KEY,
|
|
209
197
|
serde_json::json!({
|
|
210
198
|
"enabled": true,
|
|
@@ -225,13 +213,8 @@ mod tests {
|
|
|
225
213
|
.await
|
|
226
214
|
.expect("transaction should open");
|
|
227
215
|
let mut writes = StorageWriteSet::new();
|
|
228
|
-
let mut json_writer = crate::json_store::JsonStoreContext::new().writer();
|
|
229
216
|
context
|
|
230
|
-
.stage_persist_if_needed(
|
|
231
|
-
&mut live_state.writer(tx.as_mut()),
|
|
232
|
-
&mut writes,
|
|
233
|
-
&mut json_writer,
|
|
234
|
-
)
|
|
217
|
+
.stage_persist_if_needed(&mut writes)
|
|
235
218
|
.await
|
|
236
219
|
.expect("sequence should stage");
|
|
237
220
|
writes
|
|
@@ -255,18 +238,13 @@ mod tests {
|
|
|
255
238
|
.await
|
|
256
239
|
.expect("runtime context should prepare");
|
|
257
240
|
|
|
258
|
-
let
|
|
241
|
+
let tx = storage
|
|
259
242
|
.begin_write_transaction()
|
|
260
243
|
.await
|
|
261
244
|
.expect("transaction should open");
|
|
262
245
|
let mut writes = StorageWriteSet::new();
|
|
263
|
-
let mut json_writer = crate::json_store::JsonStoreContext::new().writer();
|
|
264
246
|
context
|
|
265
|
-
.stage_persist_if_needed(
|
|
266
|
-
&mut live_state.writer(tx.as_mut()),
|
|
267
|
-
&mut writes,
|
|
268
|
-
&mut json_writer,
|
|
269
|
-
)
|
|
247
|
+
.stage_persist_if_needed(&mut writes)
|
|
270
248
|
.await
|
|
271
249
|
.expect("persist should no-op");
|
|
272
250
|
assert!(writes.is_empty());
|
|
@@ -279,12 +257,7 @@ mod tests {
|
|
|
279
257
|
assert_eq!(sequence, DeterministicSequence::uninitialized());
|
|
280
258
|
}
|
|
281
259
|
|
|
282
|
-
async fn write_key_value(
|
|
283
|
-
storage: StorageContext,
|
|
284
|
-
live_state: &LiveStateContext,
|
|
285
|
-
key: &str,
|
|
286
|
-
value: serde_json::Value,
|
|
287
|
-
) {
|
|
260
|
+
async fn write_key_value(storage: StorageContext, key: &str, value: serde_json::Value) {
|
|
288
261
|
let mut tx = storage
|
|
289
262
|
.begin_write_transaction()
|
|
290
263
|
.await
|
|
@@ -294,30 +267,22 @@ mod tests {
|
|
|
294
267
|
"value": value,
|
|
295
268
|
}))
|
|
296
269
|
.expect("snapshot should serialize");
|
|
297
|
-
let
|
|
270
|
+
let mut writes = StorageWriteSet::new();
|
|
271
|
+
let row = crate::untracked_state::UntrackedStateRow {
|
|
298
272
|
entity_id: crate::entity_identity::EntityIdentity::single(key),
|
|
299
273
|
schema_key: "lix_key_value".to_string(),
|
|
300
274
|
file_id: None,
|
|
301
275
|
snapshot_content: Some(snapshot_content),
|
|
302
276
|
metadata: None,
|
|
303
|
-
schema_version: "1".to_string(),
|
|
304
277
|
created_at: "1970-01-01T00:00:00.000Z".to_string(),
|
|
305
278
|
updated_at: "1970-01-01T00:00:00.000Z".to_string(),
|
|
306
279
|
global: true,
|
|
307
|
-
change_id: None,
|
|
308
|
-
commit_id: None,
|
|
309
|
-
untracked: true,
|
|
310
280
|
version_id: GLOBAL_VERSION_ID.to_string(),
|
|
311
281
|
};
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
writer
|
|
317
|
-
.stage_rows(&mut writes, &mut json_writer, &[row])
|
|
318
|
-
.await
|
|
319
|
-
.expect("test key-value should stage");
|
|
320
|
-
}
|
|
282
|
+
crate::untracked_state::UntrackedStateContext::new()
|
|
283
|
+
.writer(&mut writes)
|
|
284
|
+
.stage_rows(std::iter::once(row.as_ref()))
|
|
285
|
+
.expect("test key-value should stage");
|
|
321
286
|
writes
|
|
322
287
|
.apply(&mut tx.as_mut())
|
|
323
288
|
.await
|
|
@@ -2,7 +2,7 @@ use crate::functions::FunctionProvider;
|
|
|
2
2
|
|
|
3
3
|
const DETERMINISTIC_UUID_COUNTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
|
|
4
4
|
|
|
5
|
-
/// Deterministic function provider for
|
|
5
|
+
/// Deterministic function provider for engine execution.
|
|
6
6
|
///
|
|
7
7
|
/// The provider is pure runtime state: it does not load or persist the sequence
|
|
8
8
|
/// itself. Session/transaction code owns that boundary so tests can decide when
|