@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,890 @@
1
+ use std::sync::Arc;
2
+
3
+ use serde_json::{Map as JsonMap, Value as JsonValue};
4
+
5
+ use crate::catalog::{CatalogSnapshot, SchemaPlan, SchemaPlanId};
6
+ use crate::common::format_json_pointer;
7
+ use crate::common::normalize_path_segment;
8
+ use crate::domain::Domain;
9
+ use crate::entity_identity::{EntityIdentity, EntityIdentityError};
10
+ use crate::functions::FunctionProviderHandle;
11
+ use crate::schema::{
12
+ is_seed_schema_key, schema_from_registered_snapshot, validate_lix_schema,
13
+ validate_lix_schema_definition,
14
+ };
15
+ use crate::transaction::types::{PreparedRowFacts, TransactionJson, TransactionWriteRow};
16
+ use crate::LixError;
17
+
18
+ pub(crate) const REGISTERED_SCHEMA_KEY: &str = "lix_registered_schema";
19
+ const DIRECTORY_DESCRIPTOR_SCHEMA_KEY: &str = "lix_directory_descriptor";
20
+ const FILE_DESCRIPTOR_SCHEMA_KEY: &str = "lix_file_descriptor";
21
+
22
+ #[derive(Debug, Clone, PartialEq, Eq)]
23
+ pub(crate) struct NormalizedTransactionWriteRow {
24
+ pub(crate) row: TransactionWriteRow,
25
+ pub(crate) snapshot: Option<TransactionJson>,
26
+ pub(crate) schema_plan_id: SchemaPlanId,
27
+ pub(crate) facts: PreparedRowFacts,
28
+ }
29
+
30
+ /// Normalizes one incoming row into a row with final snapshot/entity identity.
31
+ ///
32
+ /// This is the canonical schema-semantics boundary for transaction writes. It owns
33
+ /// schema default application, primary-key identity derivation, and explicit
34
+ /// identity mismatch validation. SQL providers should not pre-derive primary
35
+ /// keys for schemas that can be normalized here; they should pass decoded
36
+ /// snapshots and let this layer complete them.
37
+ ///
38
+ /// This function intentionally does not assign timestamps, change ids, or
39
+ /// commit ids; those are prepared-row fields assigned after semantic
40
+ /// normalization has produced the final identity.
41
+ pub(crate) fn normalize_transaction_write_row(
42
+ mut row: TransactionWriteRow,
43
+ schema_catalog: &mut CatalogSnapshot,
44
+ functions: FunctionProviderHandle,
45
+ ) -> Result<NormalizedTransactionWriteRow, LixError> {
46
+ validate_transaction_write_row_schema_identity(&row)?;
47
+
48
+ let Some((schema_plan_id, schema_plan)) = schema_catalog.plan_for_key(&row.schema_key) else {
49
+ return Err(LixError::new(
50
+ LixError::CODE_SCHEMA_DEFINITION,
51
+ format!(
52
+ "schema '{}' is not visible to this transaction",
53
+ row.schema_key
54
+ ),
55
+ ));
56
+ };
57
+
58
+ let normalized_snapshot = if let Some(snapshot) = row.snapshot.take() {
59
+ let (mut snapshot, normalized) = snapshot_object_from_transaction_json(snapshot, &row)?;
60
+ let defaults_changed = apply_defaults(&mut snapshot, schema_plan, &row, functions)?;
61
+ let descriptor_changed = normalize_filesystem_descriptor_snapshot(&row, &mut snapshot)?;
62
+ let snapshot = JsonValue::Object(snapshot);
63
+ row.entity_id = Some(resolve_entity_id(&row, schema_plan, &snapshot)?);
64
+ if defaults_changed || descriptor_changed {
65
+ Some(TransactionJson::from_value(
66
+ snapshot,
67
+ "normalized transaction snapshot_content",
68
+ )?)
69
+ } else {
70
+ Some(TransactionJson::from_parts(Arc::new(snapshot), normalized))
71
+ }
72
+ } else if row.entity_id.is_none() {
73
+ return Err(LixError::new(
74
+ LixError::CODE_SCHEMA_VALIDATION,
75
+ format!(
76
+ "tombstone for schema '{}' requires entity_id",
77
+ row.schema_key
78
+ ),
79
+ ));
80
+ } else {
81
+ None
82
+ };
83
+
84
+ if row.schema_key == REGISTERED_SCHEMA_KEY {
85
+ if row.file_id.is_some() {
86
+ return Err(LixError::new(
87
+ LixError::CODE_SCHEMA_DEFINITION,
88
+ "lix_registered_schema rows must not be scoped to a file",
89
+ )
90
+ .with_hint("Schema definitions are scoped by version and durability only; write them with null file_id."));
91
+ }
92
+ let schema_domain =
93
+ Domain::schema_catalog(row.schema_scope_version_id().to_string(), row.untracked);
94
+ remember_pending_registered_schema(
95
+ normalized_snapshot.as_ref().map(TransactionJson::value),
96
+ schema_domain,
97
+ schema_catalog,
98
+ )?;
99
+ }
100
+
101
+ Ok(NormalizedTransactionWriteRow {
102
+ row,
103
+ snapshot: normalized_snapshot,
104
+ schema_plan_id,
105
+ facts: PreparedRowFacts::default(),
106
+ })
107
+ }
108
+
109
+ fn validate_transaction_write_row_schema_identity(
110
+ row: &TransactionWriteRow,
111
+ ) -> Result<(), LixError> {
112
+ if row.schema_key.is_empty() {
113
+ return Err(LixError::new(
114
+ LixError::CODE_UNKNOWN,
115
+ "engine transaction staging requires non-empty schema_key",
116
+ ));
117
+ }
118
+ Ok(())
119
+ }
120
+
121
+ fn snapshot_object_from_transaction_json(
122
+ snapshot: TransactionJson,
123
+ row: &TransactionWriteRow,
124
+ ) -> Result<(JsonMap<String, JsonValue>, Arc<str>), LixError> {
125
+ let (snapshot, normalized) = snapshot.into_parts();
126
+ let snapshot = match Arc::try_unwrap(snapshot) {
127
+ Ok(snapshot) => snapshot,
128
+ Err(snapshot) => snapshot.as_ref().clone(),
129
+ };
130
+ match snapshot {
131
+ JsonValue::Object(snapshot) => Ok((snapshot, normalized)),
132
+ _ => Err(LixError::new(
133
+ LixError::CODE_SCHEMA_VALIDATION,
134
+ format!(
135
+ "snapshot_content for schema '{}' must be a JSON object",
136
+ row.schema_key
137
+ ),
138
+ )),
139
+ }
140
+ }
141
+
142
+ fn apply_defaults(
143
+ snapshot: &mut JsonMap<String, JsonValue>,
144
+ schema_plan: &SchemaPlan,
145
+ row: &TransactionWriteRow,
146
+ functions: FunctionProviderHandle,
147
+ ) -> Result<bool, LixError> {
148
+ schema_plan
149
+ .defaults
150
+ .apply(snapshot, functions, &row.schema_key)
151
+ }
152
+
153
+ fn normalize_filesystem_descriptor_snapshot(
154
+ row: &TransactionWriteRow,
155
+ snapshot: &mut JsonMap<String, JsonValue>,
156
+ ) -> Result<bool, LixError> {
157
+ match row.schema_key.as_str() {
158
+ DIRECTORY_DESCRIPTOR_SCHEMA_KEY => normalize_directory_descriptor_snapshot(row, snapshot),
159
+ FILE_DESCRIPTOR_SCHEMA_KEY => normalize_file_descriptor_snapshot(row, snapshot),
160
+ _ => Ok(false),
161
+ }
162
+ }
163
+
164
+ fn normalize_directory_descriptor_snapshot(
165
+ row: &TransactionWriteRow,
166
+ snapshot: &mut JsonMap<String, JsonValue>,
167
+ ) -> Result<bool, LixError> {
168
+ let Some(name) = optional_string_field(snapshot, "name", row)? else {
169
+ return Ok(false);
170
+ };
171
+ let normalized_name = normalize_path_segment(name)?;
172
+ if name == normalized_name {
173
+ return Ok(false);
174
+ }
175
+ snapshot.insert("name".to_string(), JsonValue::String(normalized_name));
176
+ Ok(true)
177
+ }
178
+
179
+ fn normalize_file_descriptor_snapshot(
180
+ row: &TransactionWriteRow,
181
+ snapshot: &mut JsonMap<String, JsonValue>,
182
+ ) -> Result<bool, LixError> {
183
+ let Some(name) = optional_string_field(snapshot, "name", row)? else {
184
+ return Ok(false);
185
+ };
186
+ let normalized_name = normalize_path_segment(name)?;
187
+ if name == normalized_name {
188
+ return Ok(false);
189
+ }
190
+ snapshot.insert("name".to_string(), JsonValue::String(normalized_name));
191
+ Ok(true)
192
+ }
193
+
194
+ fn optional_string_field<'a>(
195
+ snapshot: &'a JsonMap<String, JsonValue>,
196
+ field: &str,
197
+ row: &TransactionWriteRow,
198
+ ) -> Result<Option<&'a str>, LixError> {
199
+ let Some(value) = snapshot.get(field) else {
200
+ return Ok(None);
201
+ };
202
+ value.as_str().map(Some).ok_or_else(|| {
203
+ LixError::new(
204
+ LixError::CODE_SCHEMA_VALIDATION,
205
+ format!(
206
+ "snapshot_content for schema '{}' field '{}' must be a string",
207
+ row.schema_key, field
208
+ ),
209
+ )
210
+ })
211
+ }
212
+
213
+ fn resolve_entity_id(
214
+ row: &TransactionWriteRow,
215
+ schema_plan: &SchemaPlan,
216
+ snapshot: &JsonValue,
217
+ ) -> Result<EntityIdentity, LixError> {
218
+ let Some(primary_key_paths) = schema_plan.primary_key.as_ref() else {
219
+ return row.entity_id.clone().ok_or_else(|| {
220
+ LixError::new(
221
+ LixError::CODE_SCHEMA_VALIDATION,
222
+ format!(
223
+ "write for schema '{}' requires entity_id because the schema has no x-lix-primary-key",
224
+ row.schema_key
225
+ ),
226
+ )
227
+ });
228
+ };
229
+ let derived = EntityIdentity::from_primary_key_paths(snapshot, primary_key_paths)
230
+ .map_err(|error| entity_id_derivation_error(row, primary_key_paths, error))?;
231
+ if let Some(entity_id) = row.entity_id.as_ref() {
232
+ if entity_id != &derived {
233
+ return Err(LixError::new(
234
+ LixError::CODE_SCHEMA_VALIDATION,
235
+ format!(
236
+ "entity_id '{}' does not match x-lix-primary-key derived entity_id '{}' for schema '{}'",
237
+ entity_id.as_json_array_text()?, derived.as_json_array_text()?, row.schema_key
238
+ ),
239
+ ));
240
+ }
241
+ }
242
+ Ok(derived)
243
+ }
244
+
245
+ fn entity_id_derivation_error(
246
+ row: &TransactionWriteRow,
247
+ primary_key_paths: &[Vec<String>],
248
+ error: EntityIdentityError,
249
+ ) -> LixError {
250
+ let detail = match error {
251
+ EntityIdentityError::EmptyPrimaryKey => "empty x-lix-primary-key".to_string(),
252
+ EntityIdentityError::EmptyPrimaryKeyPath { index } => {
253
+ format!("empty x-lix-primary-key pointer at index {index}")
254
+ }
255
+ EntityIdentityError::EmptyPrimaryKeyValue { index } => {
256
+ let pointer = primary_key_paths
257
+ .get(index)
258
+ .map(|path| format_json_pointer(path))
259
+ .unwrap_or_else(|| format!("index {index}"));
260
+ format!("empty value at primary-key pointer '{pointer}'")
261
+ }
262
+ EntityIdentityError::MissingPrimaryKeyValue { index } => {
263
+ let pointer = format_json_pointer(&primary_key_paths[index]);
264
+ format!("missing value at primary-key pointer '{pointer}'")
265
+ }
266
+ EntityIdentityError::UnsupportedPrimaryKeyValue { index } => {
267
+ let pointer = format_json_pointer(&primary_key_paths[index]);
268
+ format!("non-string value at primary-key pointer '{pointer}'")
269
+ }
270
+ EntityIdentityError::InvalidEncodedEntityIdentity => {
271
+ "invalid encoded entity identity".to_string()
272
+ }
273
+ };
274
+ LixError::new(
275
+ LixError::CODE_SCHEMA_VALIDATION,
276
+ format!(
277
+ "failed to derive entity_id for schema '{}': {detail}",
278
+ row.schema_key
279
+ ),
280
+ )
281
+ }
282
+
283
+ pub(crate) fn remember_pending_registered_schema(
284
+ snapshot: Option<&JsonValue>,
285
+ domain: Domain,
286
+ schema_catalog: &mut CatalogSnapshot,
287
+ ) -> Result<(), LixError> {
288
+ let Some(snapshot) = snapshot else {
289
+ return Err(LixError::new(
290
+ LixError::CODE_SCHEMA_DEFINITION,
291
+ "lix_registered_schema rows cannot be deleted yet; schema deletion is not supported",
292
+ ));
293
+ };
294
+ if let Some(schema) = snapshot.get("value") {
295
+ validate_lix_schema_definition(schema)?;
296
+ }
297
+ {
298
+ let registered_schema_definition = schema_catalog
299
+ .schema(REGISTERED_SCHEMA_KEY)
300
+ .ok_or_else(|| {
301
+ LixError::new(
302
+ LixError::CODE_SCHEMA_DEFINITION,
303
+ "lix_registered_schema schema is not visible to this transaction",
304
+ )
305
+ })?;
306
+ validate_lix_schema(registered_schema_definition, &snapshot)?;
307
+ }
308
+ let (key, schema) = schema_from_registered_snapshot(&snapshot)?;
309
+ if is_seed_schema_key(&key.schema_key) {
310
+ return Err(LixError::new(
311
+ LixError::CODE_SCHEMA_DEFINITION,
312
+ format!(
313
+ "schema '{}' is a system schema and cannot be registered at runtime",
314
+ key.schema_key
315
+ ),
316
+ ));
317
+ }
318
+ validate_lix_schema_definition(&schema)?;
319
+ schema_catalog.insert_schema_for_domain(domain, key, schema)?;
320
+ Ok(())
321
+ }
322
+
323
+ #[cfg(test)]
324
+ mod tests {
325
+ use serde_json::json;
326
+
327
+ use super::*;
328
+ use crate::functions::{FunctionProvider, SharedFunctionProvider};
329
+ use crate::schema::seed_schema_definition;
330
+
331
+ #[test]
332
+ fn normalization_derives_entity_id_from_primary_key() {
333
+ let mut catalog = catalog_with(vec![schema_with_default_id()]);
334
+ let row = TransactionWriteRow {
335
+ entity_id: None,
336
+ schema_key: "normalization_schema".to_string(),
337
+ snapshot: Some(snapshot_json(
338
+ r#"{"id":"entity-from-snapshot","value":"hello"}"#,
339
+ )),
340
+ ..base_stage_row()
341
+ };
342
+
343
+ let row =
344
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
345
+
346
+ assert_eq!(
347
+ row.row.entity_id.as_ref(),
348
+ Some(&crate::entity_identity::EntityIdentity::single(
349
+ "entity-from-snapshot"
350
+ ))
351
+ );
352
+ }
353
+
354
+ #[test]
355
+ fn normalization_applies_json_and_cel_defaults_before_identity_derivation() {
356
+ let mut catalog = catalog_with(vec![schema_with_default_id()]);
357
+ let row = TransactionWriteRow {
358
+ entity_id: None,
359
+ schema_key: "normalization_schema".to_string(),
360
+ snapshot: Some(snapshot_json(r#"{}"#)),
361
+ ..base_stage_row()
362
+ };
363
+
364
+ let row =
365
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
366
+ let snapshot = normalized_snapshot(&row);
367
+
368
+ assert_eq!(
369
+ row.row.entity_id.as_ref(),
370
+ Some(&crate::entity_identity::EntityIdentity::single(
371
+ "uuid-default"
372
+ ))
373
+ );
374
+ assert_eq!(snapshot["id"], "uuid-default");
375
+ assert_eq!(snapshot["value"], "literal-default");
376
+ }
377
+
378
+ #[test]
379
+ fn normalization_applies_cel_defaults_from_snapshot_context() {
380
+ let mut catalog = catalog_with(vec![schema_with_cel_field_default()]);
381
+ let row = TransactionWriteRow {
382
+ entity_id: None,
383
+ schema_key: "cel_field_default_schema".to_string(),
384
+ snapshot: Some(snapshot_json(r#"{"id":"entity-1","name":"Sample"}"#)),
385
+ ..base_stage_row()
386
+ };
387
+
388
+ let row =
389
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
390
+ let snapshot = normalized_snapshot(&row);
391
+
392
+ assert_eq!(snapshot["slug"], "Sample-slug");
393
+ }
394
+
395
+ #[test]
396
+ fn normalization_x_lix_default_overrides_json_default() {
397
+ let mut catalog = catalog_with(vec![schema_with_overridden_default()]);
398
+ let row = TransactionWriteRow {
399
+ entity_id: None,
400
+ schema_key: "overridden_default_schema".to_string(),
401
+ snapshot: Some(snapshot_json(r#"{"id":"entity-1"}"#)),
402
+ ..base_stage_row()
403
+ };
404
+
405
+ let row =
406
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
407
+ let snapshot = normalized_snapshot(&row);
408
+
409
+ assert_eq!(snapshot["status"], "computed");
410
+ }
411
+
412
+ #[test]
413
+ fn normalization_does_not_overwrite_explicit_null_with_default() {
414
+ let mut catalog = catalog_with(vec![schema_with_nullable_default()]);
415
+ let row = TransactionWriteRow {
416
+ entity_id: None,
417
+ schema_key: "nullable_default_schema".to_string(),
418
+ snapshot: Some(snapshot_json(r#"{"id":"entity-1","status":null}"#)),
419
+ ..base_stage_row()
420
+ };
421
+
422
+ let row =
423
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
424
+ let snapshot = normalized_snapshot(&row);
425
+
426
+ assert_eq!(snapshot["status"], JsonValue::Null);
427
+ }
428
+
429
+ #[test]
430
+ fn normalization_applies_timestamp_function_default() {
431
+ let mut catalog = catalog_with(vec![schema_with_timestamp_default()]);
432
+ let row = TransactionWriteRow {
433
+ entity_id: None,
434
+ schema_key: "timestamp_default_schema".to_string(),
435
+ snapshot: Some(snapshot_json(r#"{"id":"entity-1"}"#)),
436
+ ..base_stage_row()
437
+ };
438
+
439
+ let row =
440
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
441
+ let snapshot = normalized_snapshot(&row);
442
+
443
+ assert_eq!(snapshot["created_at"], "1970-01-01T00:00:00.000Z");
444
+ }
445
+
446
+ #[test]
447
+ fn normalization_surfaces_cel_default_errors() {
448
+ let mut catalog = catalog_with(vec![schema_with_unknown_cel_default()]);
449
+ let row = TransactionWriteRow {
450
+ entity_id: None,
451
+ schema_key: "unknown_cel_default_schema".to_string(),
452
+ snapshot: Some(snapshot_json(r#"{"id":"entity-1"}"#)),
453
+ ..base_stage_row()
454
+ };
455
+
456
+ let error = normalize_transaction_write_row(row, &mut catalog, functions())
457
+ .expect_err("default should fail");
458
+
459
+ assert!(error.message.contains("failed to evaluate x-lix-default"));
460
+ assert!(error.message.contains("unknown_cel_default_schema.slug"));
461
+ }
462
+
463
+ #[test]
464
+ fn normalization_rejects_entity_id_that_disagrees_with_primary_key() {
465
+ let mut catalog = catalog_with(vec![schema_with_default_id()]);
466
+ let row = TransactionWriteRow {
467
+ entity_id: Some(crate::entity_identity::EntityIdentity::single("wrong-id")),
468
+ schema_key: "normalization_schema".to_string(),
469
+ snapshot: Some(snapshot_json(r#"{"id":"right-id","value":"hello"}"#)),
470
+ ..base_stage_row()
471
+ };
472
+
473
+ let error = normalize_transaction_write_row(row, &mut catalog, functions())
474
+ .expect_err("id mismatch fails");
475
+
476
+ assert!(error
477
+ .message
478
+ .contains("does not match x-lix-primary-key derived entity_id"));
479
+ }
480
+
481
+ #[test]
482
+ fn normalization_derives_json_array_entity_id_for_composite_primary_key() {
483
+ let mut catalog = catalog_with(vec![composite_key_schema()]);
484
+ let row = TransactionWriteRow {
485
+ entity_id: None,
486
+ schema_key: "composite_key_schema".to_string(),
487
+ snapshot: Some(snapshot_json(r#"{"namespace":"a~b","key":"1"}"#)),
488
+ ..base_stage_row()
489
+ };
490
+
491
+ let row =
492
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
493
+ let entity_id = row.row.entity_id.expect("composite entity id");
494
+ let projected_entity_id = entity_id
495
+ .as_json_array_text()
496
+ .expect("entity id should project");
497
+
498
+ assert_eq!(projected_entity_id, "[\"a~b\",\"1\"]");
499
+ }
500
+
501
+ #[test]
502
+ fn normalization_rejects_non_string_primary_key_values() {
503
+ let mut catalog = catalog_with(vec![composite_key_schema()]);
504
+ let row = TransactionWriteRow {
505
+ entity_id: None,
506
+ schema_key: "composite_key_schema".to_string(),
507
+ snapshot: Some(snapshot_json(r#"{"namespace":"a~b","key":1}"#)),
508
+ ..base_stage_row()
509
+ };
510
+
511
+ let error = normalize_transaction_write_row(row, &mut catalog, functions())
512
+ .expect_err("non-string primary key values should fail");
513
+
514
+ assert_eq!(error.code, LixError::CODE_SCHEMA_VALIDATION);
515
+ assert!(error
516
+ .message
517
+ .contains("non-string value at primary-key pointer '/key'"));
518
+ }
519
+
520
+ #[test]
521
+ fn normalization_validates_explicit_composite_entity_id_against_projection() {
522
+ let mut catalog = catalog_with(vec![composite_key_schema()]);
523
+ let snapshot = json!({
524
+ "namespace": "a~b",
525
+ "key": "1",
526
+ });
527
+ let derived = EntityIdentity::from_primary_key_paths(
528
+ &snapshot,
529
+ &[vec!["namespace".to_string()], vec!["key".to_string()]],
530
+ )
531
+ .expect("identity should derive");
532
+ let row = TransactionWriteRow {
533
+ entity_id: Some(derived.clone()),
534
+ schema_key: "composite_key_schema".to_string(),
535
+ snapshot: Some(transaction_json(snapshot.clone())),
536
+ ..base_stage_row()
537
+ };
538
+
539
+ let row =
540
+ normalize_transaction_write_row(row, &mut catalog, functions()).expect("normalize row");
541
+
542
+ assert_eq!(row.row.entity_id.as_ref(), Some(&derived));
543
+ }
544
+
545
+ #[test]
546
+ fn normalization_makes_pending_registered_schema_visible_to_later_rows() {
547
+ let mut catalog = catalog_with(vec![seed_schema_definition(REGISTERED_SCHEMA_KEY)
548
+ .expect("registered schema builtin")
549
+ .clone()]);
550
+ let registered = TransactionWriteRow {
551
+ entity_id: None,
552
+ schema_key: REGISTERED_SCHEMA_KEY.to_string(),
553
+ snapshot: Some(transaction_json(json!({
554
+ "value": dynamic_schema_definition(),
555
+ }))),
556
+ ..base_stage_row()
557
+ };
558
+
559
+ normalize_transaction_write_row(registered, &mut catalog, functions())
560
+ .expect("register schema");
561
+
562
+ let dynamic = TransactionWriteRow {
563
+ entity_id: None,
564
+ schema_key: "dynamic_schema".to_string(),
565
+ snapshot: Some(snapshot_json(r#"{"id":"dynamic-1"}"#)),
566
+ ..base_stage_row()
567
+ };
568
+ let dynamic = normalize_transaction_write_row(dynamic, &mut catalog, functions())
569
+ .expect("dynamic row");
570
+
571
+ assert_eq!(
572
+ dynamic.row.entity_id.as_ref(),
573
+ Some(&crate::entity_identity::EntityIdentity::single("dynamic-1"))
574
+ );
575
+ }
576
+
577
+ #[test]
578
+ fn normalization_canonicalizes_filesystem_descriptor_segments() {
579
+ let mut catalog = catalog_with(vec![
580
+ builtin_schema(FILE_DESCRIPTOR_SCHEMA_KEY),
581
+ builtin_schema(DIRECTORY_DESCRIPTOR_SCHEMA_KEY),
582
+ ]);
583
+
584
+ let file = TransactionWriteRow {
585
+ entity_id: None,
586
+ schema_key: FILE_DESCRIPTOR_SCHEMA_KEY.to_string(),
587
+ snapshot: Some(transaction_json(json!({
588
+ "id": "file-cafe",
589
+ "directory_id": null,
590
+ "name": "Cafe\u{301}.txt",
591
+ }))),
592
+ global: false,
593
+ ..base_stage_row()
594
+ };
595
+ let file = normalize_transaction_write_row(file, &mut catalog, functions())
596
+ .expect("normalize file");
597
+ let file_snapshot = normalized_snapshot(&file);
598
+ assert_eq!(file_snapshot["name"], "Café.txt");
599
+
600
+ let directory = TransactionWriteRow {
601
+ entity_id: None,
602
+ schema_key: DIRECTORY_DESCRIPTOR_SCHEMA_KEY.to_string(),
603
+ snapshot: Some(transaction_json(json!({
604
+ "id": "dir-cafe",
605
+ "parent_id": null,
606
+ "name": "Cafe\u{301}",
607
+ }))),
608
+ global: false,
609
+ ..base_stage_row()
610
+ };
611
+ let directory = normalize_transaction_write_row(directory, &mut catalog, functions())
612
+ .expect("normalize directory");
613
+ let directory_snapshot = normalized_snapshot(&directory);
614
+ assert_eq!(directory_snapshot["name"], "Café");
615
+ }
616
+
617
+ #[test]
618
+ fn normalization_rejects_invalid_filesystem_descriptor_segments() {
619
+ let mut catalog = catalog_with(vec![
620
+ builtin_schema(FILE_DESCRIPTOR_SCHEMA_KEY),
621
+ builtin_schema(DIRECTORY_DESCRIPTOR_SCHEMA_KEY),
622
+ ]);
623
+
624
+ let dot_segment = normalize_transaction_write_row(
625
+ TransactionWriteRow {
626
+ entity_id: None,
627
+ schema_key: FILE_DESCRIPTOR_SCHEMA_KEY.to_string(),
628
+ snapshot: Some(transaction_json(json!({
629
+ "id": "file-dotdot",
630
+ "directory_id": null,
631
+ "name": "..",
632
+ }))),
633
+ global: false,
634
+ ..base_stage_row()
635
+ },
636
+ &mut catalog,
637
+ functions(),
638
+ )
639
+ .expect_err("file descriptor name should reject dot segments");
640
+ assert_eq!(dot_segment.code, "LIX_ERROR_PATH_DOT_SEGMENT");
641
+
642
+ let bidi = normalize_transaction_write_row(
643
+ TransactionWriteRow {
644
+ entity_id: None,
645
+ schema_key: FILE_DESCRIPTOR_SCHEMA_KEY.to_string(),
646
+ snapshot: Some(transaction_json(json!({
647
+ "id": "file-bidi",
648
+ "directory_id": null,
649
+ "name": "safe\u{202E}txt",
650
+ }))),
651
+ global: false,
652
+ ..base_stage_row()
653
+ },
654
+ &mut catalog,
655
+ functions(),
656
+ )
657
+ .expect_err("file descriptor name should reject bidi formatting characters");
658
+ assert_eq!(bidi.code, "LIX_ERROR_PATH_INVALID_SEGMENT_CODE_POINT");
659
+
660
+ let zero_width = normalize_transaction_write_row(
661
+ TransactionWriteRow {
662
+ entity_id: None,
663
+ schema_key: DIRECTORY_DESCRIPTOR_SCHEMA_KEY.to_string(),
664
+ snapshot: Some(transaction_json(json!({
665
+ "id": "dir-zero-width",
666
+ "parent_id": null,
667
+ "name": "zero\u{200D}width",
668
+ }))),
669
+ global: false,
670
+ ..base_stage_row()
671
+ },
672
+ &mut catalog,
673
+ functions(),
674
+ )
675
+ .expect_err("directory descriptor name should reject zero-width characters");
676
+ assert_eq!(zero_width.code, "LIX_ERROR_PATH_INVALID_SEGMENT_CODE_POINT");
677
+ }
678
+
679
+ #[test]
680
+ fn normalization_keeps_file_descriptor_name_opaque() {
681
+ let mut catalog = catalog_with(vec![builtin_schema(FILE_DESCRIPTOR_SCHEMA_KEY)]);
682
+
683
+ let row = normalize_transaction_write_row(
684
+ TransactionWriteRow {
685
+ entity_id: None,
686
+ schema_key: FILE_DESCRIPTOR_SCHEMA_KEY.to_string(),
687
+ snapshot: Some(transaction_json(json!({
688
+ "id": "file-opaque-name",
689
+ "directory_id": null,
690
+ "name": "foo.bar",
691
+ }))),
692
+ global: false,
693
+ ..base_stage_row()
694
+ },
695
+ &mut catalog,
696
+ functions(),
697
+ )
698
+ .expect("file descriptor name should be an opaque basename");
699
+
700
+ let snapshot = normalized_snapshot(&row);
701
+ assert_eq!(snapshot["name"], "foo.bar");
702
+ }
703
+
704
+ fn normalized_snapshot(row: &NormalizedTransactionWriteRow) -> &JsonValue {
705
+ row.snapshot
706
+ .as_ref()
707
+ .expect("normalized test row should have a snapshot")
708
+ .value()
709
+ }
710
+
711
+ fn catalog_with(schemas: Vec<JsonValue>) -> CatalogSnapshot {
712
+ let mut visible_schemas = schemas;
713
+ if visible_schemas.iter().any(|schema| {
714
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some(FILE_DESCRIPTOR_SCHEMA_KEY)
715
+ }) && !visible_schemas.iter().any(|schema| {
716
+ schema.get("x-lix-key").and_then(JsonValue::as_str)
717
+ == Some(DIRECTORY_DESCRIPTOR_SCHEMA_KEY)
718
+ }) {
719
+ visible_schemas.push(builtin_schema(DIRECTORY_DESCRIPTOR_SCHEMA_KEY));
720
+ }
721
+ CatalogSnapshot::from_visible_schemas(&visible_schemas).expect("catalog")
722
+ }
723
+
724
+ fn builtin_schema(schema_key: &str) -> JsonValue {
725
+ seed_schema_definition(schema_key)
726
+ .unwrap_or_else(|| panic!("{schema_key} builtin schema should exist"))
727
+ .clone()
728
+ }
729
+
730
+ fn transaction_json(value: JsonValue) -> TransactionJson {
731
+ TransactionJson::from_value_for_test(value)
732
+ }
733
+
734
+ fn snapshot_json(value: &str) -> TransactionJson {
735
+ transaction_json(serde_json::from_str(value).expect("test snapshot should parse"))
736
+ }
737
+
738
+ fn base_stage_row() -> TransactionWriteRow {
739
+ TransactionWriteRow {
740
+ entity_id: Some(crate::entity_identity::EntityIdentity::single("entity-1")),
741
+ schema_key: "normalization_schema".to_string(),
742
+ file_id: None,
743
+ snapshot: Some(snapshot_json(r#"{"id":"entity-1","value":"hello"}"#)),
744
+ metadata: None,
745
+ origin: None,
746
+ created_at: None,
747
+ updated_at: None,
748
+ global: true,
749
+ change_id: None,
750
+ commit_id: None,
751
+ untracked: false,
752
+ version_id: crate::GLOBAL_VERSION_ID.to_string(),
753
+ }
754
+ }
755
+
756
+ fn schema_with_default_id() -> JsonValue {
757
+ json!({
758
+ "x-lix-key": "normalization_schema",
759
+ "x-lix-primary-key": ["/id"],
760
+ "type": "object",
761
+ "properties": {
762
+ "id": { "type": "string", "x-lix-default": "lix_uuid_v7()" },
763
+ "value": { "type": "string", "default": "literal-default" }
764
+ },
765
+ "required": ["id", "value"],
766
+ "additionalProperties": false
767
+ })
768
+ }
769
+
770
+ fn schema_with_cel_field_default() -> JsonValue {
771
+ json!({
772
+ "x-lix-key": "cel_field_default_schema",
773
+ "x-lix-primary-key": ["/id"],
774
+ "type": "object",
775
+ "properties": {
776
+ "id": { "type": "string" },
777
+ "name": { "type": "string" },
778
+ "slug": { "type": "string", "x-lix-default": "name + '-slug'" }
779
+ },
780
+ "required": ["id", "name"],
781
+ "additionalProperties": false
782
+ })
783
+ }
784
+
785
+ fn schema_with_overridden_default() -> JsonValue {
786
+ json!({
787
+ "x-lix-key": "overridden_default_schema",
788
+ "x-lix-primary-key": ["/id"],
789
+ "type": "object",
790
+ "properties": {
791
+ "id": { "type": "string" },
792
+ "status": {
793
+ "type": "string",
794
+ "default": "literal",
795
+ "x-lix-default": "'computed'"
796
+ }
797
+ },
798
+ "required": ["id"],
799
+ "additionalProperties": false
800
+ })
801
+ }
802
+
803
+ fn schema_with_nullable_default() -> JsonValue {
804
+ json!({
805
+ "x-lix-key": "nullable_default_schema",
806
+ "x-lix-primary-key": ["/id"],
807
+ "type": "object",
808
+ "properties": {
809
+ "id": { "type": "string" },
810
+ "status": {
811
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
812
+ "x-lix-default": "'computed'"
813
+ }
814
+ },
815
+ "required": ["id"],
816
+ "additionalProperties": false
817
+ })
818
+ }
819
+
820
+ fn schema_with_timestamp_default() -> JsonValue {
821
+ json!({
822
+ "x-lix-key": "timestamp_default_schema",
823
+ "x-lix-primary-key": ["/id"],
824
+ "type": "object",
825
+ "properties": {
826
+ "id": { "type": "string" },
827
+ "created_at": { "type": "string", "x-lix-default": "lix_timestamp()" }
828
+ },
829
+ "required": ["id"],
830
+ "additionalProperties": false
831
+ })
832
+ }
833
+
834
+ fn schema_with_unknown_cel_default() -> JsonValue {
835
+ json!({
836
+ "x-lix-key": "unknown_cel_default_schema",
837
+ "x-lix-primary-key": ["/id"],
838
+ "type": "object",
839
+ "properties": {
840
+ "id": { "type": "string" },
841
+ "slug": { "type": "string", "x-lix-default": "missing_var + '-slug'" }
842
+ },
843
+ "required": ["id"],
844
+ "additionalProperties": false
845
+ })
846
+ }
847
+
848
+ fn composite_key_schema() -> JsonValue {
849
+ json!({
850
+ "x-lix-key": "composite_key_schema",
851
+ "x-lix-primary-key": ["/namespace", "/key"],
852
+ "type": "object",
853
+ "properties": {
854
+ "namespace": { "type": "string" },
855
+ "key": { "type": "string" }
856
+ },
857
+ "required": ["namespace", "key"],
858
+ "additionalProperties": false
859
+ })
860
+ }
861
+
862
+ fn dynamic_schema_definition() -> JsonValue {
863
+ json!({
864
+ "x-lix-key": "dynamic_schema",
865
+ "x-lix-primary-key": ["/id"],
866
+ "type": "object",
867
+ "properties": {
868
+ "id": { "type": "string" }
869
+ },
870
+ "required": ["id"],
871
+ "additionalProperties": false
872
+ })
873
+ }
874
+
875
+ fn functions() -> FunctionProviderHandle {
876
+ SharedFunctionProvider::new(Box::new(FixedFunctions) as Box<dyn FunctionProvider + Send>)
877
+ }
878
+
879
+ struct FixedFunctions;
880
+
881
+ impl FunctionProvider for FixedFunctions {
882
+ fn uuid_v7(&mut self) -> String {
883
+ "uuid-default".to_string()
884
+ }
885
+
886
+ fn timestamp(&mut self) -> String {
887
+ "1970-01-01T00:00:00.000Z".to_string()
888
+ }
889
+ }
890
+ }