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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/SKILL.md +4 -5
  2. package/dist/engine-wasm/wasm/lix_engine.js +1 -1
  3. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  4. package/dist/generated/builtin-schemas.d.ts +87 -162
  5. package/dist/generated/builtin-schemas.js +139 -236
  6. package/dist/open-lix.d.ts +1 -1
  7. package/dist-engine-src/src/binary_cas/types.rs +0 -6
  8. package/dist-engine-src/src/catalog/context.rs +412 -0
  9. package/dist-engine-src/src/catalog/mod.rs +10 -0
  10. package/dist-engine-src/src/catalog/schema.rs +4 -0
  11. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  12. package/dist-engine-src/src/cel/mod.rs +1 -1
  13. package/dist-engine-src/src/cel/provider.rs +1 -1
  14. package/dist-engine-src/src/commit_graph/context.rs +328 -1015
  15. package/dist-engine-src/src/commit_graph/mod.rs +2 -3
  16. package/dist-engine-src/src/commit_graph/types.rs +7 -43
  17. package/dist-engine-src/src/commit_graph/walker.rs +57 -81
  18. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  19. package/dist-engine-src/src/commit_store/context.rs +944 -0
  20. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  21. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  22. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  23. package/dist-engine-src/src/commit_store/types.rs +215 -0
  24. package/dist-engine-src/src/common/identity.rs +15 -5
  25. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  26. package/dist-engine-src/src/common/metadata.rs +17 -12
  27. package/dist-engine-src/src/common/mod.rs +5 -5
  28. package/dist-engine-src/src/domain.rs +324 -0
  29. package/dist-engine-src/src/engine.rs +29 -43
  30. package/dist-engine-src/src/entity_identity.rs +238 -118
  31. package/dist-engine-src/src/functions/context.rs +17 -52
  32. package/dist-engine-src/src/functions/deterministic.rs +1 -1
  33. package/dist-engine-src/src/functions/mod.rs +1 -1
  34. package/dist-engine-src/src/functions/provider.rs +4 -4
  35. package/dist-engine-src/src/functions/state.rs +39 -66
  36. package/dist-engine-src/src/functions/types.rs +1 -1
  37. package/dist-engine-src/src/init.rs +204 -151
  38. package/dist-engine-src/src/json_store/context.rs +354 -60
  39. package/dist-engine-src/src/json_store/encoded.rs +6 -6
  40. package/dist-engine-src/src/json_store/mod.rs +4 -1
  41. package/dist-engine-src/src/json_store/store.rs +884 -11
  42. package/dist-engine-src/src/json_store/types.rs +166 -1
  43. package/dist-engine-src/src/lib.rs +10 -9
  44. package/dist-engine-src/src/live_state/context.rs +608 -830
  45. package/dist-engine-src/src/live_state/mod.rs +3 -3
  46. package/dist-engine-src/src/live_state/overlay.rs +7 -7
  47. package/dist-engine-src/src/live_state/reader.rs +5 -5
  48. package/dist-engine-src/src/live_state/types.rs +19 -36
  49. package/dist-engine-src/src/live_state/visibility.rs +19 -14
  50. package/dist-engine-src/src/plugin/archive.rs +3 -6
  51. package/dist-engine-src/src/plugin/install.rs +0 -18
  52. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
  53. package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
  54. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
  55. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
  56. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
  57. package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
  58. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
  59. package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
  60. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
  61. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
  62. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
  63. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
  64. package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
  65. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  66. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
  67. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
  68. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
  69. package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
  70. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  71. package/dist-engine-src/src/schema/definition.json +47 -17
  72. package/dist-engine-src/src/schema/definition.rs +202 -96
  73. package/dist-engine-src/src/schema/key.rs +9 -77
  74. package/dist-engine-src/src/schema/mod.rs +4 -4
  75. package/dist-engine-src/src/schema/tests.rs +133 -92
  76. package/dist-engine-src/src/session/context.rs +40 -42
  77. package/dist-engine-src/src/session/create_version.rs +22 -14
  78. package/dist-engine-src/src/session/execute.rs +45 -14
  79. package/dist-engine-src/src/session/merge/apply.rs +4 -4
  80. package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
  81. package/dist-engine-src/src/session/merge/stats.rs +1 -1
  82. package/dist-engine-src/src/session/merge/version.rs +35 -45
  83. package/dist-engine-src/src/session/mod.rs +4 -2
  84. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  85. package/dist-engine-src/src/session/switch_version.rs +16 -28
  86. package/dist-engine-src/src/sql2/change_provider.rs +14 -20
  87. package/dist-engine-src/src/sql2/classify.rs +61 -26
  88. package/dist-engine-src/src/sql2/context.rs +22 -18
  89. package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
  90. package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
  91. package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
  92. package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
  93. package/dist-engine-src/src/sql2/error.rs +21 -1
  94. package/dist-engine-src/src/sql2/execute.rs +325 -264
  95. package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
  96. package/dist-engine-src/src/sql2/file_provider.rs +533 -108
  97. package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
  98. package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
  99. package/dist-engine-src/src/sql2/history_projection.rs +3 -27
  100. package/dist-engine-src/src/sql2/history_provider.rs +11 -17
  101. package/dist-engine-src/src/sql2/history_route.rs +22 -8
  102. package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
  103. package/dist-engine-src/src/sql2/mod.rs +6 -3
  104. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  105. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  106. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  107. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  108. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  109. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  110. package/dist-engine-src/src/sql2/read_only.rs +10 -12
  111. package/dist-engine-src/src/sql2/session.rs +7 -10
  112. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  113. package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
  114. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  115. package/dist-engine-src/src/sql2/version_provider.rs +46 -31
  116. package/dist-engine-src/src/sql2/version_scope.rs +4 -4
  117. package/dist-engine-src/src/storage_bench.rs +1782 -325
  118. package/dist-engine-src/src/test_support.rs +183 -36
  119. package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
  120. package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
  121. package/dist-engine-src/src/tracked_state/context.rs +1155 -271
  122. package/dist-engine-src/src/tracked_state/diff.rs +249 -57
  123. package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
  124. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  125. package/dist-engine-src/src/tracked_state/merge.rs +37 -19
  126. package/dist-engine-src/src/tracked_state/mod.rs +8 -7
  127. package/dist-engine-src/src/tracked_state/storage.rs +138 -6
  128. package/dist-engine-src/src/tracked_state/tree.rs +695 -252
  129. package/dist-engine-src/src/tracked_state/types.rs +176 -6
  130. package/dist-engine-src/src/transaction/commit.rs +695 -435
  131. package/dist-engine-src/src/transaction/context.rs +551 -310
  132. package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
  133. package/dist-engine-src/src/transaction/mod.rs +2 -0
  134. package/dist-engine-src/src/transaction/normalization.rs +311 -447
  135. package/dist-engine-src/src/transaction/prep.rs +37 -0
  136. package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
  137. package/dist-engine-src/src/transaction/staging.rs +701 -406
  138. package/dist-engine-src/src/transaction/types.rs +231 -122
  139. package/dist-engine-src/src/transaction/validation.rs +2717 -1698
  140. package/dist-engine-src/src/untracked_state/codec.rs +40 -96
  141. package/dist-engine-src/src/untracked_state/context.rs +21 -5
  142. package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
  143. package/dist-engine-src/src/untracked_state/mod.rs +3 -5
  144. package/dist-engine-src/src/untracked_state/storage.rs +105 -57
  145. package/dist-engine-src/src/untracked_state/types.rs +63 -13
  146. package/dist-engine-src/src/version/context.rs +1 -13
  147. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  148. package/dist-engine-src/src/version/mod.rs +3 -2
  149. package/dist-engine-src/src/version/refs.rs +12 -103
  150. package/dist-engine-src/src/version/stage_rows.rs +15 -19
  151. package/package.json +1 -1
  152. package/dist-engine-src/src/changelog/codec.rs +0 -321
  153. package/dist-engine-src/src/changelog/context.rs +0 -92
  154. package/dist-engine-src/src/changelog/materialization.rs +0 -121
  155. package/dist-engine-src/src/changelog/mod.rs +0 -13
  156. package/dist-engine-src/src/changelog/reader.rs +0 -20
  157. package/dist-engine-src/src/changelog/storage.rs +0 -220
  158. package/dist-engine-src/src/changelog/types.rs +0 -38
  159. package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
  160. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
  161. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
  162. package/dist-engine-src/src/schema_registry.rs +0 -294
  163. package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
  164. package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
  165. 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 engine2. The string projection exists
12
- /// only for SQL/canonical boundaries that still expose a single `entity_id`.
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<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),
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 string value at index {index} must not be empty"
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, number, or boolean"
50
+ "primary-key value at index {index} must be a JSON string"
71
51
  ),
72
52
  Self::InvalidEncodedEntityIdentity => {
73
- write!(formatter, "encoded entity identity is invalid")
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![EntityIdentityPart::String(value.into())],
65
+ parts: vec![value.into()],
83
66
  }
84
67
  }
85
68
 
86
69
  #[cfg(test)]
87
- pub(crate) fn tuple(parts: Vec<EntityIdentityPart>) -> Result<Self, EntityIdentityError> {
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(EntityIdentityPart::from_json_value(value, index)?);
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 as_string(&self) -> Result<String, LixError> {
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
- if let [EntityIdentityPart::String(value)] = self.parts.as_slice() {
124
- return Ok(value.clone());
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 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)
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 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);
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
- Ok(Self::single(entity_id))
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
- 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
+ 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 single_string_identity_projects_to_plain_entity_id() {
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.as_string().expect("projection should work"),
214
+ identity.as_single_string().expect("projection should work"),
184
215
  "plain-id"
185
216
  );
186
217
  }
187
218
 
188
219
  #[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");
220
+ fn single_identity_projects_to_json_array_entity_id() {
221
+ let identity = EntityIdentity::single("plain-id");
197
222
 
198
- assert!(encoded.starts_with(COMPOSITE_ENTITY_ID_PREFIX));
199
- assert!(!encoded.contains("namespace~key"));
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 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");
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
- let encoded = identity.as_string().expect("projection should work");
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::from_string(&encoded).expect("decode should work"),
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 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");
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.as_string().expect("left should encode"),
234
- right.as_string().expect("right should encode")
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 from_string_treats_plain_string_as_single_string_identity() {
240
- assert_eq!(
241
- EntityIdentity::single("plain-id"),
242
- EntityIdentity::single("plain-id")
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
- "index": 7,
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::json_store::JsonStoreWriter;
6
- use crate::live_state::{LiveStateReader, LiveStateWriter};
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 engine2-owned provider used by SQL and transaction staging.
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<S>(
58
+ pub(crate) async fn stage_persist_if_needed(
60
59
  &self,
61
- writer: &mut LiveStateWriter<S>,
62
60
  writes: &mut StorageWriteSet,
63
- json_writer: &mut JsonStoreWriter,
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::{LiveStateContext, LiveStateRow};
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(crate::changelog::ChangelogContext::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 mut tx = storage
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 row = LiveStateRow {
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
- let mut writes = StorageWriteSet::new();
313
- let mut json_writer = crate::json_store::JsonStoreContext::new().writer();
314
- {
315
- let mut writer = live_state.writer(tx.as_mut());
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 engine2 execution.
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