@lix-js/sdk 0.6.0-preview.1 → 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 (205) hide show
  1. package/SKILL.md +304 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/generated/builtin-schemas.d.ts +87 -162
  7. package/dist/generated/builtin-schemas.js +139 -236
  8. package/dist/open-lix.d.ts +103 -14
  9. package/dist/open-lix.js +3 -0
  10. package/dist/sqlite/index.js +99 -22
  11. package/dist-engine-src/README.md +18 -0
  12. package/dist-engine-src/src/backend/kv.rs +358 -0
  13. package/dist-engine-src/src/backend/mod.rs +12 -0
  14. package/dist-engine-src/src/backend/testing.rs +658 -0
  15. package/dist-engine-src/src/backend/types.rs +96 -0
  16. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  17. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  18. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  19. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  20. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  21. package/dist-engine-src/src/binary_cas/types.rs +121 -0
  22. package/dist-engine-src/src/catalog/context.rs +412 -0
  23. package/dist-engine-src/src/catalog/mod.rs +10 -0
  24. package/dist-engine-src/src/catalog/schema.rs +4 -0
  25. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  26. package/dist-engine-src/src/cel/context.rs +86 -0
  27. package/dist-engine-src/src/cel/error.rs +19 -0
  28. package/dist-engine-src/src/cel/mod.rs +8 -0
  29. package/dist-engine-src/src/cel/provider.rs +9 -0
  30. package/dist-engine-src/src/cel/runtime.rs +167 -0
  31. package/dist-engine-src/src/cel/value.rs +50 -0
  32. package/dist-engine-src/src/commit_graph/context.rs +901 -0
  33. package/dist-engine-src/src/commit_graph/mod.rs +11 -0
  34. package/dist-engine-src/src/commit_graph/types.rs +109 -0
  35. package/dist-engine-src/src/commit_graph/walker.rs +756 -0
  36. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  37. package/dist-engine-src/src/commit_store/context.rs +944 -0
  38. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  39. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  40. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  41. package/dist-engine-src/src/commit_store/types.rs +215 -0
  42. package/dist-engine-src/src/common/error.rs +313 -0
  43. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  44. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  45. package/dist-engine-src/src/common/identity.rs +145 -0
  46. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  47. package/dist-engine-src/src/common/metadata.rs +40 -0
  48. package/dist-engine-src/src/common/mod.rs +23 -0
  49. package/dist-engine-src/src/common/types.rs +105 -0
  50. package/dist-engine-src/src/common/wire.rs +222 -0
  51. package/dist-engine-src/src/domain.rs +324 -0
  52. package/dist-engine-src/src/engine.rs +225 -0
  53. package/dist-engine-src/src/entity_identity.rs +405 -0
  54. package/dist-engine-src/src/functions/context.rs +292 -0
  55. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  56. package/dist-engine-src/src/functions/mod.rs +18 -0
  57. package/dist-engine-src/src/functions/provider.rs +130 -0
  58. package/dist-engine-src/src/functions/state.rs +336 -0
  59. package/dist-engine-src/src/functions/types.rs +37 -0
  60. package/dist-engine-src/src/init.rs +558 -0
  61. package/dist-engine-src/src/json_store/compression.rs +77 -0
  62. package/dist-engine-src/src/json_store/context.rs +423 -0
  63. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  64. package/dist-engine-src/src/json_store/mod.rs +12 -0
  65. package/dist-engine-src/src/json_store/store.rs +1109 -0
  66. package/dist-engine-src/src/json_store/types.rs +217 -0
  67. package/dist-engine-src/src/lib.rs +62 -0
  68. package/dist-engine-src/src/live_state/context.rs +2019 -0
  69. package/dist-engine-src/src/live_state/mod.rs +15 -0
  70. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  71. package/dist-engine-src/src/live_state/reader.rs +23 -0
  72. package/dist-engine-src/src/live_state/types.rs +222 -0
  73. package/dist-engine-src/src/live_state/visibility.rs +223 -0
  74. package/dist-engine-src/src/plugin/archive.rs +438 -0
  75. package/dist-engine-src/src/plugin/component.rs +183 -0
  76. package/dist-engine-src/src/plugin/install.rs +619 -0
  77. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  78. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  79. package/dist-engine-src/src/plugin/mod.rs +33 -0
  80. package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
  81. package/dist-engine-src/src/plugin/storage.rs +74 -0
  82. package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
  83. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  84. package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
  85. package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
  86. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
  87. package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
  89. package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
  90. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
  91. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
  92. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
  93. package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
  94. package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
  95. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  96. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
  97. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
  98. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
  99. package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
  100. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  101. package/dist-engine-src/src/schema/definition.json +187 -0
  102. package/dist-engine-src/src/schema/definition.rs +742 -0
  103. package/dist-engine-src/src/schema/key.rs +138 -0
  104. package/dist-engine-src/src/schema/mod.rs +20 -0
  105. package/dist-engine-src/src/schema/seed.rs +14 -0
  106. package/dist-engine-src/src/schema/tests.rs +780 -0
  107. package/dist-engine-src/src/session/context.rs +364 -0
  108. package/dist-engine-src/src/session/create_version.rs +88 -0
  109. package/dist-engine-src/src/session/execute.rs +478 -0
  110. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  111. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  112. package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
  113. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  114. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  115. package/dist-engine-src/src/session/merge/version.rs +427 -0
  116. package/dist-engine-src/src/session/mod.rs +27 -0
  117. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  118. package/dist-engine-src/src/session/switch_version.rs +109 -0
  119. package/dist-engine-src/src/sql2/change_provider.rs +331 -0
  120. package/dist-engine-src/src/sql2/classify.rs +182 -0
  121. package/dist-engine-src/src/sql2/context.rs +311 -0
  122. package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
  123. package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
  124. package/dist-engine-src/src/sql2/dml.rs +148 -0
  125. package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
  126. package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
  127. package/dist-engine-src/src/sql2/error.rs +216 -0
  128. package/dist-engine-src/src/sql2/execute.rs +3440 -0
  129. package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
  130. package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
  131. package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
  132. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  133. package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
  134. package/dist-engine-src/src/sql2/history_projection.rs +56 -0
  135. package/dist-engine-src/src/sql2/history_provider.rs +412 -0
  136. package/dist-engine-src/src/sql2/history_route.rs +657 -0
  137. package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
  138. package/dist-engine-src/src/sql2/mod.rs +46 -0
  139. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  140. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  141. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  142. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  143. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  144. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  145. package/dist-engine-src/src/sql2/read_only.rs +63 -0
  146. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  147. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  148. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  149. package/dist-engine-src/src/sql2/session.rs +132 -0
  150. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  151. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  152. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  153. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  154. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  155. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  156. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  157. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  158. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  159. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  160. package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
  161. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  162. package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
  163. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  164. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  165. package/dist-engine-src/src/storage/context.rs +356 -0
  166. package/dist-engine-src/src/storage/mod.rs +14 -0
  167. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  168. package/dist-engine-src/src/storage/types.rs +501 -0
  169. package/dist-engine-src/src/storage_bench.rs +4863 -0
  170. package/dist-engine-src/src/test_support.rs +228 -0
  171. package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
  172. package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
  173. package/dist-engine-src/src/tracked_state/context.rs +1867 -0
  174. package/dist-engine-src/src/tracked_state/diff.rs +686 -0
  175. package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
  176. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  177. package/dist-engine-src/src/tracked_state/merge.rs +492 -0
  178. package/dist-engine-src/src/tracked_state/mod.rs +32 -0
  179. package/dist-engine-src/src/tracked_state/storage.rs +375 -0
  180. package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
  181. package/dist-engine-src/src/tracked_state/types.rs +231 -0
  182. package/dist-engine-src/src/transaction/commit.rs +1484 -0
  183. package/dist-engine-src/src/transaction/context.rs +1548 -0
  184. package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
  185. package/dist-engine-src/src/transaction/mod.rs +13 -0
  186. package/dist-engine-src/src/transaction/normalization.rs +890 -0
  187. package/dist-engine-src/src/transaction/prep.rs +37 -0
  188. package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
  189. package/dist-engine-src/src/transaction/staging.rs +1731 -0
  190. package/dist-engine-src/src/transaction/types.rs +460 -0
  191. package/dist-engine-src/src/transaction/validation.rs +5830 -0
  192. package/dist-engine-src/src/untracked_state/codec.rs +307 -0
  193. package/dist-engine-src/src/untracked_state/context.rs +98 -0
  194. package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
  195. package/dist-engine-src/src/untracked_state/mod.rs +15 -0
  196. package/dist-engine-src/src/untracked_state/storage.rs +396 -0
  197. package/dist-engine-src/src/untracked_state/types.rs +146 -0
  198. package/dist-engine-src/src/version/context.rs +40 -0
  199. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  200. package/dist-engine-src/src/version/mod.rs +13 -0
  201. package/dist-engine-src/src/version/refs.rs +330 -0
  202. package/dist-engine-src/src/version/stage_rows.rs +67 -0
  203. package/dist-engine-src/src/version/types.rs +21 -0
  204. package/dist-engine-src/src/wasm/mod.rs +60 -0
  205. package/package.json +68 -64
@@ -0,0 +1,405 @@
1
+ use serde_json::Value as JsonValue;
2
+
3
+ use crate::common::json_pointer_get;
4
+ use crate::LixError;
5
+
6
+ /// Logical entity identity derived from a schema primary key.
7
+ ///
8
+ /// Keep this as typed tuple data inside engine. SQL `entity_id` surfaces
9
+ /// should use the JSON-array projection.
10
+ #[derive(
11
+ Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
12
+ )]
13
+ pub(crate) struct EntityIdentity {
14
+ pub(crate) parts: Vec<String>,
15
+ }
16
+
17
+ #[derive(Debug, Clone, PartialEq, Eq)]
18
+ pub(crate) enum EntityIdentityError {
19
+ EmptyPrimaryKey,
20
+ EmptyPrimaryKeyPath { index: usize },
21
+ EmptyPrimaryKeyValue { index: usize },
22
+ MissingPrimaryKeyValue { index: usize },
23
+ UnsupportedPrimaryKeyValue { index: usize },
24
+ InvalidEncodedEntityIdentity,
25
+ }
26
+
27
+ impl std::fmt::Display for EntityIdentityError {
28
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29
+ match self {
30
+ Self::EmptyPrimaryKey => {
31
+ write!(formatter, "primary key must contain at least one path")
32
+ }
33
+ Self::EmptyPrimaryKeyPath { index } => {
34
+ write!(
35
+ formatter,
36
+ "primary-key path at index {index} must not be empty"
37
+ )
38
+ }
39
+ Self::EmptyPrimaryKeyValue { index } => {
40
+ write!(
41
+ formatter,
42
+ "primary-key value at index {index} must not be empty"
43
+ )
44
+ }
45
+ Self::MissingPrimaryKeyValue { index } => {
46
+ write!(formatter, "primary-key value at index {index} is missing")
47
+ }
48
+ Self::UnsupportedPrimaryKeyValue { index } => write!(
49
+ formatter,
50
+ "primary-key value at index {index} must be a JSON string"
51
+ ),
52
+ Self::InvalidEncodedEntityIdentity => {
53
+ write!(
54
+ formatter,
55
+ "encoded entity identity must be a non-empty JSON array of strings"
56
+ )
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ impl EntityIdentity {
63
+ pub(crate) fn single(value: impl Into<String>) -> Self {
64
+ Self {
65
+ parts: vec![value.into()],
66
+ }
67
+ }
68
+
69
+ #[cfg(test)]
70
+ pub(crate) fn tuple(parts: Vec<String>) -> Result<Self, EntityIdentityError> {
71
+ if parts.is_empty() {
72
+ return Err(EntityIdentityError::EmptyPrimaryKey);
73
+ }
74
+ if let Some((index, _)) = parts.iter().enumerate().find(|(_, part)| part.is_empty()) {
75
+ return Err(EntityIdentityError::EmptyPrimaryKeyValue { index });
76
+ }
77
+ Ok(Self { parts })
78
+ }
79
+
80
+ pub(crate) fn from_primary_key_paths(
81
+ snapshot: &JsonValue,
82
+ primary_key_paths: &[Vec<String>],
83
+ ) -> Result<Self, EntityIdentityError> {
84
+ if primary_key_paths.is_empty() {
85
+ return Err(EntityIdentityError::EmptyPrimaryKey);
86
+ }
87
+
88
+ let mut parts = Vec::with_capacity(primary_key_paths.len());
89
+ for (index, path) in primary_key_paths.iter().enumerate() {
90
+ if path.is_empty() {
91
+ return Err(EntityIdentityError::EmptyPrimaryKeyPath { index });
92
+ }
93
+ let Some(value) = json_pointer_get(snapshot, path) else {
94
+ return Err(EntityIdentityError::MissingPrimaryKeyValue { index });
95
+ };
96
+ parts.push(string_part_from_json_value(value, index)?);
97
+ }
98
+
99
+ Ok(Self { parts })
100
+ }
101
+
102
+ pub(crate) fn as_json_array_value(&self) -> Result<JsonValue, LixError> {
103
+ if self.parts.is_empty() {
104
+ return Err(LixError::unknown(
105
+ "entity identity must contain at least one primary-key part",
106
+ ));
107
+ }
108
+
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
+ ));
128
+ }
129
+
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",
136
+ ))
137
+ }
138
+
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);
157
+ }
158
+
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 })
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));
196
+ }
197
+ JsonValue::Object(canonical)
198
+ }
199
+ _ => value.clone(),
200
+ }
201
+ }
202
+
203
+ #[cfg(test)]
204
+ mod tests {
205
+ use serde_json::json;
206
+
207
+ use super::*;
208
+
209
+ #[test]
210
+ fn single_string_identity_projects_to_single_string() {
211
+ let identity = EntityIdentity::single("plain-id");
212
+
213
+ assert_eq!(
214
+ identity.as_single_string().expect("projection should work"),
215
+ "plain-id"
216
+ );
217
+ }
218
+
219
+ #[test]
220
+ fn single_identity_projects_to_json_array_entity_id() {
221
+ let identity = EntityIdentity::single("plain-id");
222
+
223
+ assert_eq!(
224
+ identity
225
+ .as_json_array_text()
226
+ .expect("projection should work"),
227
+ "[\"plain-id\"]"
228
+ );
229
+ }
230
+
231
+ #[test]
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");
235
+
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");
251
+
252
+ assert_eq!(
253
+ EntityIdentity::from_json_array_text(&encoded).expect("decode should work"),
254
+ identity
255
+ );
256
+ }
257
+
258
+ #[test]
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");
280
+
281
+ assert_ne!(
282
+ left.as_json_array_text().expect("left should encode"),
283
+ right.as_json_array_text().expect("right should encode")
284
+ );
285
+ }
286
+
287
+ #[test]
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")
305
+ );
306
+ }
307
+
308
+ #[test]
309
+ fn from_primary_key_paths_derives_ordered_parts() {
310
+ let snapshot = json!({
311
+ "namespace": "messages",
312
+ "locale": "en"
313
+ });
314
+
315
+ let identity = EntityIdentity::from_primary_key_paths(
316
+ &snapshot,
317
+ &[vec!["namespace".to_string()], vec!["locale".to_string()]],
318
+ )
319
+ .expect("primary key should derive");
320
+
321
+ assert_eq!(
322
+ identity,
323
+ EntityIdentity {
324
+ parts: vec!["messages".to_string(), "en".to_string()],
325
+ }
326
+ );
327
+ }
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
+
396
+ #[test]
397
+ fn from_primary_key_paths_rejects_missing_parts() {
398
+ let snapshot = json!({ "id": "a" });
399
+
400
+ assert_eq!(
401
+ EntityIdentity::from_primary_key_paths(&snapshot, &[vec!["missing".to_string()]]),
402
+ Err(EntityIdentityError::MissingPrimaryKeyValue { index: 0 })
403
+ );
404
+ }
405
+ }
@@ -0,0 +1,292 @@
1
+ use crate::functions::{
2
+ state, DeterministicFunctionProvider, DeterministicSequence, FunctionProvider,
3
+ FunctionProviderHandle, SharedFunctionProvider, SystemFunctionProvider,
4
+ };
5
+ use crate::live_state::LiveStateReader;
6
+ use crate::storage::StorageWriteSet;
7
+ use crate::LixError;
8
+
9
+ /// Execution-scoped runtime function context.
10
+ ///
11
+ /// Lower layers should only receive function providers. This context owns the
12
+ /// lifecycle at the session/transaction boundary: prepare the right function
13
+ /// source before execution and persist deterministic sequence progress after
14
+ /// successful execution.
15
+ pub(crate) struct FunctionContext {
16
+ functions: FunctionProviderHandle,
17
+ bookkeeping_timestamp: String,
18
+ }
19
+
20
+ impl FunctionContext {
21
+ /// Prepares the runtime function provider for one execution.
22
+ ///
23
+ /// If deterministic mode is absent or disabled, the context uses system
24
+ /// functions. If enabled, it starts from the persisted sequence + 1.
25
+ pub(crate) async fn prepare(live_state: &dyn LiveStateReader) -> Result<Self, LixError> {
26
+ let mode = state::load_mode(live_state).await?;
27
+ let mut bookkeeping_functions = SystemFunctionProvider;
28
+ let bookkeeping_timestamp = bookkeeping_functions.timestamp();
29
+ if !mode.enabled {
30
+ return Ok(Self {
31
+ functions: SharedFunctionProvider::new(
32
+ Box::new(SystemFunctionProvider) as Box<dyn FunctionProvider + Send>
33
+ ),
34
+ bookkeeping_timestamp,
35
+ });
36
+ }
37
+
38
+ let sequence = state::load_sequence(live_state).await?;
39
+ Ok(Self {
40
+ functions: SharedFunctionProvider::new(Box::new(DeterministicFunctionProvider::new(
41
+ sequence.next_sequence(),
42
+ mode.timestamp_shuffle,
43
+ ))
44
+ as Box<dyn FunctionProvider + Send>),
45
+ bookkeeping_timestamp,
46
+ })
47
+ }
48
+
49
+ /// Returns the engine-owned provider used by SQL and transaction staging.
50
+ pub(crate) fn provider(&self) -> FunctionProviderHandle {
51
+ self.functions.clone()
52
+ }
53
+
54
+ /// Persists deterministic sequence progress if this execution used any.
55
+ ///
56
+ /// System functions report no sequence state, so this is a no-op when
57
+ /// deterministic mode is disabled.
58
+ pub(crate) async fn stage_persist_if_needed(
59
+ &self,
60
+ writes: &mut StorageWriteSet,
61
+ ) -> Result<(), LixError> {
62
+ let Some(highest_seen) = self.functions.deterministic_sequence_persist_highest_seen()
63
+ else {
64
+ return Ok(());
65
+ };
66
+ state::stage_sequence(
67
+ writes,
68
+ DeterministicSequence { highest_seen },
69
+ &self.bookkeeping_timestamp,
70
+ )
71
+ .await
72
+ }
73
+ }
74
+
75
+ #[cfg(test)]
76
+ mod tests {
77
+ use std::sync::Arc;
78
+
79
+ use crate::backend::testing::UnitTestBackend;
80
+ use crate::functions::state::{DETERMINISTIC_MODE_KEY, DETERMINISTIC_SEQUENCE_KEY};
81
+ use crate::functions::{state::load_sequence, DeterministicSequence};
82
+ use crate::live_state::LiveStateContext;
83
+ use crate::storage::StorageContext;
84
+ use crate::GLOBAL_VERSION_ID;
85
+
86
+ use super::*;
87
+
88
+ fn live_state_context() -> LiveStateContext {
89
+ LiveStateContext::new(
90
+ crate::tracked_state::TrackedStateContext::new(),
91
+ crate::untracked_state::UntrackedStateContext::new(),
92
+ crate::commit_graph::CommitGraphContext::new(),
93
+ )
94
+ }
95
+
96
+ #[tokio::test]
97
+ async fn prepare_uses_system_functions_when_mode_missing() {
98
+ let backend = Arc::new(UnitTestBackend::new());
99
+ let storage = StorageContext::new(backend.clone());
100
+ let live_state = live_state_context();
101
+ let reader = live_state.reader(storage.clone());
102
+
103
+ let context = FunctionContext::prepare(&reader)
104
+ .await
105
+ .expect("runtime context should prepare");
106
+
107
+ assert_eq!(
108
+ context
109
+ .provider()
110
+ .deterministic_sequence_persist_highest_seen(),
111
+ None
112
+ );
113
+ }
114
+
115
+ #[tokio::test]
116
+ async fn prepare_starts_deterministic_functions_at_sequence_zero() {
117
+ let backend = Arc::new(UnitTestBackend::new());
118
+ let storage = StorageContext::new(backend.clone());
119
+ let live_state = live_state_context();
120
+ crate::test_support::seed_global_version_head(storage.clone()).await;
121
+ write_key_value(
122
+ storage.clone(),
123
+ DETERMINISTIC_MODE_KEY,
124
+ serde_json::json!({
125
+ "enabled": true,
126
+ }),
127
+ )
128
+ .await;
129
+
130
+ let reader = live_state.reader(storage.clone());
131
+ let context = FunctionContext::prepare(&reader)
132
+ .await
133
+ .expect("runtime context should prepare");
134
+ let functions = context.provider();
135
+
136
+ assert_eq!(
137
+ functions.call_uuid_v7(),
138
+ "01920000-0000-7000-8000-000000000000"
139
+ );
140
+ assert_eq!(functions.call_timestamp(), "1970-01-01T00:00:00.001Z");
141
+ assert_eq!(
142
+ context
143
+ .provider()
144
+ .deterministic_sequence_persist_highest_seen(),
145
+ Some(1)
146
+ );
147
+ }
148
+
149
+ #[tokio::test]
150
+ async fn prepare_continues_from_persisted_sequence() {
151
+ let backend = Arc::new(UnitTestBackend::new());
152
+ let storage = StorageContext::new(backend.clone());
153
+ let live_state = live_state_context();
154
+ crate::test_support::seed_global_version_head(storage.clone()).await;
155
+ write_key_value(
156
+ storage.clone(),
157
+ DETERMINISTIC_MODE_KEY,
158
+ serde_json::json!({
159
+ "enabled": true,
160
+ }),
161
+ )
162
+ .await;
163
+ write_key_value(
164
+ storage.clone(),
165
+ DETERMINISTIC_SEQUENCE_KEY,
166
+ serde_json::json!(41),
167
+ )
168
+ .await;
169
+
170
+ let reader = live_state.reader(storage.clone());
171
+ let context = FunctionContext::prepare(&reader)
172
+ .await
173
+ .expect("runtime context should prepare");
174
+ let functions = context.provider();
175
+
176
+ assert_eq!(
177
+ functions.call_uuid_v7(),
178
+ "01920000-0000-7000-8000-00000000002a"
179
+ );
180
+ assert_eq!(
181
+ context
182
+ .provider()
183
+ .deterministic_sequence_persist_highest_seen(),
184
+ Some(42)
185
+ );
186
+ }
187
+
188
+ #[tokio::test]
189
+ async fn persist_if_needed_writes_sequence_when_deterministic_functions_advanced() {
190
+ let backend = Arc::new(UnitTestBackend::new());
191
+ let storage = StorageContext::new(backend.clone());
192
+ let live_state = live_state_context();
193
+ crate::test_support::seed_global_version_head(storage.clone()).await;
194
+ write_key_value(
195
+ storage.clone(),
196
+ DETERMINISTIC_MODE_KEY,
197
+ serde_json::json!({
198
+ "enabled": true,
199
+ }),
200
+ )
201
+ .await;
202
+
203
+ let context = {
204
+ let reader = live_state.reader(storage.clone());
205
+ FunctionContext::prepare(&reader)
206
+ .await
207
+ .expect("runtime context should prepare")
208
+ };
209
+ context.provider().call_uuid_v7();
210
+
211
+ let mut tx = storage
212
+ .begin_write_transaction()
213
+ .await
214
+ .expect("transaction should open");
215
+ let mut writes = StorageWriteSet::new();
216
+ context
217
+ .stage_persist_if_needed(&mut writes)
218
+ .await
219
+ .expect("sequence should stage");
220
+ writes
221
+ .apply(&mut tx.as_mut())
222
+ .await
223
+ .expect("sequence should apply");
224
+ tx.commit().await.expect("transaction should commit");
225
+
226
+ let reader = live_state.reader(storage.clone());
227
+ let sequence = load_sequence(&reader).await.expect("sequence should load");
228
+ assert_eq!(sequence, DeterministicSequence { highest_seen: 0 });
229
+ }
230
+
231
+ #[tokio::test]
232
+ async fn persist_if_needed_is_noop_for_system_functions() {
233
+ let backend = Arc::new(UnitTestBackend::new());
234
+ let storage = StorageContext::new(backend.clone());
235
+ let live_state = live_state_context();
236
+ let reader = live_state.reader(storage.clone());
237
+ let context = FunctionContext::prepare(&reader)
238
+ .await
239
+ .expect("runtime context should prepare");
240
+
241
+ let tx = storage
242
+ .begin_write_transaction()
243
+ .await
244
+ .expect("transaction should open");
245
+ let mut writes = StorageWriteSet::new();
246
+ context
247
+ .stage_persist_if_needed(&mut writes)
248
+ .await
249
+ .expect("persist should no-op");
250
+ assert!(writes.is_empty());
251
+ tx.commit().await.expect("transaction should commit");
252
+
253
+ let reader = live_state.reader(storage.clone());
254
+ let sequence = load_sequence(&reader)
255
+ .await
256
+ .expect("missing sequence should load");
257
+ assert_eq!(sequence, DeterministicSequence::uninitialized());
258
+ }
259
+
260
+ async fn write_key_value(storage: StorageContext, key: &str, value: serde_json::Value) {
261
+ let mut tx = storage
262
+ .begin_write_transaction()
263
+ .await
264
+ .expect("transaction should open");
265
+ let snapshot_content = serde_json::to_string(&serde_json::json!({
266
+ "key": key,
267
+ "value": value,
268
+ }))
269
+ .expect("snapshot should serialize");
270
+ let mut writes = StorageWriteSet::new();
271
+ let row = crate::untracked_state::UntrackedStateRow {
272
+ entity_id: crate::entity_identity::EntityIdentity::single(key),
273
+ schema_key: "lix_key_value".to_string(),
274
+ file_id: None,
275
+ snapshot_content: Some(snapshot_content),
276
+ metadata: None,
277
+ created_at: "1970-01-01T00:00:00.000Z".to_string(),
278
+ updated_at: "1970-01-01T00:00:00.000Z".to_string(),
279
+ global: true,
280
+ version_id: GLOBAL_VERSION_ID.to_string(),
281
+ };
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");
286
+ writes
287
+ .apply(&mut tx.as_mut())
288
+ .await
289
+ .expect("test key-value should apply");
290
+ tx.commit().await.expect("transaction should commit");
291
+ }
292
+ }