@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,11 @@
1
+ mod chunking;
2
+ mod codec;
3
+ mod context;
4
+ pub(crate) mod kv;
5
+ mod types;
6
+
7
+ pub(crate) use context::{BinaryCasContext, BlobDataReader};
8
+ pub(crate) use types::{
9
+ BlobBytesBatch, BlobExistsBatch, BlobHash, BlobLayout, BlobMetadata, BlobMetadataBatch,
10
+ BlobWrite, BlobWriteReceipt,
11
+ };
@@ -0,0 +1,121 @@
1
+ use crate::binary_cas::codec::{binary_blob_hash_bytes, hash_bytes_to_hex, hash_hex_to_bytes};
2
+ use crate::LixError;
3
+
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
5
+ pub(crate) struct BlobHash([u8; 32]);
6
+
7
+ impl BlobHash {
8
+ pub(crate) fn from_bytes(bytes: [u8; 32]) -> Self {
9
+ Self(bytes)
10
+ }
11
+
12
+ pub(crate) fn from_content(content: &[u8]) -> Self {
13
+ Self(binary_blob_hash_bytes(content))
14
+ }
15
+
16
+ pub(crate) fn from_hex(hash_hex: &str) -> Result<Self, LixError> {
17
+ Ok(Self(hash_hex_to_bytes(hash_hex, "binary CAS blob")?))
18
+ }
19
+
20
+ pub(crate) fn to_hex(self) -> String {
21
+ hash_bytes_to_hex(&self.0)
22
+ }
23
+
24
+ pub(crate) fn as_bytes(&self) -> &[u8; 32] {
25
+ &self.0
26
+ }
27
+
28
+ pub(crate) fn into_bytes(self) -> [u8; 32] {
29
+ self.0
30
+ }
31
+ }
32
+
33
+ #[derive(Debug, Clone, PartialEq, Eq)]
34
+ pub(crate) enum BlobLayout {
35
+ Empty,
36
+ SingleChunk { chunk_hash: BlobHash },
37
+ Chunked { chunk_count: u32 },
38
+ }
39
+
40
+ #[derive(Debug, Clone, PartialEq, Eq)]
41
+ pub(crate) struct BlobMetadata {
42
+ pub(crate) hash: BlobHash,
43
+ pub(crate) size_bytes: u64,
44
+ pub(crate) layout: BlobLayout,
45
+ }
46
+
47
+ #[derive(Debug, Clone, PartialEq, Eq)]
48
+ pub(crate) struct BlobExistsBatch {
49
+ entries: Vec<bool>,
50
+ }
51
+
52
+ impl BlobExistsBatch {
53
+ pub(crate) fn new(entries: Vec<bool>) -> Self {
54
+ Self { entries }
55
+ }
56
+
57
+ #[allow(dead_code)]
58
+ pub(crate) fn get(&self, index: usize) -> bool {
59
+ self.entries.get(index).copied().unwrap_or(false)
60
+ }
61
+
62
+ #[allow(dead_code)]
63
+ pub(crate) fn into_vec(self) -> Vec<bool> {
64
+ self.entries
65
+ }
66
+ }
67
+
68
+ #[derive(Debug, Clone, PartialEq, Eq)]
69
+ pub(crate) struct BlobMetadataBatch {
70
+ entries: Vec<Option<BlobMetadata>>,
71
+ }
72
+
73
+ impl BlobMetadataBatch {
74
+ pub(crate) fn new(entries: Vec<Option<BlobMetadata>>) -> Self {
75
+ Self { entries }
76
+ }
77
+
78
+ #[allow(dead_code)]
79
+ pub(crate) fn get(&self, index: usize) -> Option<&BlobMetadata> {
80
+ self.entries.get(index).and_then(Option::as_ref)
81
+ }
82
+
83
+ pub(crate) fn into_vec(self) -> Vec<Option<BlobMetadata>> {
84
+ self.entries
85
+ }
86
+ }
87
+
88
+ #[derive(Debug, Clone, PartialEq, Eq)]
89
+ pub(crate) struct BlobBytesBatch {
90
+ entries: Vec<Option<Vec<u8>>>,
91
+ }
92
+
93
+ impl BlobBytesBatch {
94
+ pub(crate) fn new(entries: Vec<Option<Vec<u8>>>) -> Self {
95
+ Self { entries }
96
+ }
97
+
98
+ #[allow(dead_code)]
99
+ pub(crate) fn get(&self, index: usize) -> Option<&[u8]> {
100
+ self.entries
101
+ .get(index)
102
+ .and_then(Option::as_ref)
103
+ .map(Vec::as_slice)
104
+ }
105
+
106
+ pub(crate) fn into_vec(self) -> Vec<Option<Vec<u8>>> {
107
+ self.entries
108
+ }
109
+ }
110
+
111
+ #[derive(Debug, Clone, Copy)]
112
+ pub(crate) struct BlobWrite<'a> {
113
+ pub(crate) bytes: &'a [u8],
114
+ }
115
+
116
+ #[derive(Debug, Clone, PartialEq, Eq)]
117
+ pub(crate) struct BlobWriteReceipt {
118
+ pub(crate) hash: BlobHash,
119
+ pub(crate) size_bytes: u64,
120
+ pub(crate) layout: BlobLayout,
121
+ }
@@ -0,0 +1,412 @@
1
+ use std::collections::BTreeMap;
2
+
3
+ use serde_json::Value as JsonValue;
4
+
5
+ use crate::catalog::SchemaCatalogFact;
6
+ use crate::domain::{committed_row_is_exact_version_scoped, Domain};
7
+ use crate::live_state::MaterializedLiveStateRow;
8
+ use crate::live_state::{LiveStateFilter, LiveStateReader, LiveStateScanRequest};
9
+ use crate::schema::schema_key_from_definition;
10
+ use crate::{LixError, NullableKeyFilter};
11
+
12
+ const REGISTERED_SCHEMA_KEY: &str = "lix_registered_schema";
13
+
14
+ /// Engine schema visibility boundary.
15
+ ///
16
+ /// SQL planning receives a schema snapshot from live state. System schemas are
17
+ /// seeded as ordinary `lix_registered_schema` rows during initialization, so
18
+ /// runtime schema visibility has one source of truth.
19
+ pub(crate) struct CatalogContext;
20
+
21
+ impl CatalogContext {
22
+ pub(crate) fn new() -> Self {
23
+ Self
24
+ }
25
+
26
+ /// Loads schema definitions for SQL surface planning at `version_id`.
27
+ ///
28
+ /// SQL surfaces are a read-planning projection over the active untracked
29
+ /// schema catalog. Validation must use `schema_facts_for_domain` instead so
30
+ /// schema durability remains explicit.
31
+ pub(crate) async fn schema_jsons_for_sql_read_planning<R>(
32
+ &self,
33
+ live_state: &R,
34
+ version_id: &str,
35
+ ) -> Result<Vec<JsonValue>, LixError>
36
+ where
37
+ R: LiveStateReader + ?Sized,
38
+ {
39
+ let facts = self
40
+ .schema_facts_for_domain(live_state, &Domain::schema_catalog(version_id, true))
41
+ .await?;
42
+ let mut schemas = BTreeMap::<String, JsonValue>::new();
43
+ for fact in facts {
44
+ let schema_key = fact.catalog_key().schema_key.clone();
45
+ if schemas
46
+ .insert(schema_key.clone(), fact.schema().clone())
47
+ .is_some()
48
+ {
49
+ return Err(LixError::new(
50
+ LixError::CODE_SCHEMA_DEFINITION,
51
+ format!(
52
+ "SQL surface schema '{}' is visible from more than one schema catalog fact",
53
+ schema_key
54
+ ),
55
+ )
56
+ .with_hint("SQL entity surfaces are named by schema_key. Keep exactly one visible schema per schema_key for SQL planning."));
57
+ }
58
+ }
59
+ Ok(schemas.into_values().collect())
60
+ }
61
+
62
+ /// Loads schema facts reachable from a row domain.
63
+ pub(crate) async fn schema_facts_for_domain<R>(
64
+ &self,
65
+ live_state: &R,
66
+ domain: &Domain,
67
+ ) -> Result<Vec<SchemaCatalogFact>, LixError>
68
+ where
69
+ R: LiveStateReader + ?Sized,
70
+ {
71
+ let mut facts = Vec::new();
72
+ for schema_domain in domain.schema_catalog_domains() {
73
+ let rows = live_state
74
+ .scan_rows(&LiveStateScanRequest {
75
+ filter: LiveStateFilter {
76
+ schema_keys: vec![REGISTERED_SCHEMA_KEY.to_string()],
77
+ version_ids: vec![schema_domain.version_id().to_string()],
78
+ file_ids: vec![NullableKeyFilter::Null],
79
+ untracked: Some(schema_domain.untracked()),
80
+ include_tombstones: false,
81
+ ..LiveStateFilter::default()
82
+ },
83
+ ..LiveStateScanRequest::default()
84
+ })
85
+ .await?;
86
+ for row in rows
87
+ .into_iter()
88
+ .filter(|row| row_belongs_to_schema_catalog_domain(row, &schema_domain))
89
+ {
90
+ let Some((key, schema)) = decode_registered_schema_row(&row)? else {
91
+ continue;
92
+ };
93
+ facts.push(SchemaCatalogFact::new(schema_domain.clone(), key, schema));
94
+ }
95
+ }
96
+ Ok(facts)
97
+ }
98
+ }
99
+
100
+ fn row_belongs_to_schema_catalog_domain(row: &MaterializedLiveStateRow, domain: &Domain) -> bool {
101
+ row.schema_key == REGISTERED_SCHEMA_KEY
102
+ && row.file_id.is_none()
103
+ && row.snapshot_content.is_some()
104
+ && row.version_id == domain.version_id()
105
+ && row.untracked == domain.untracked()
106
+ && committed_row_is_exact_version_scoped(row, domain.version_id())
107
+ }
108
+
109
+ fn decode_registered_schema_row(
110
+ row: &MaterializedLiveStateRow,
111
+ ) -> Result<Option<(crate::schema::SchemaKey, JsonValue)>, LixError> {
112
+ if row.schema_key != REGISTERED_SCHEMA_KEY {
113
+ return Err(LixError::new(
114
+ "LIX_ERROR_UNKNOWN",
115
+ format!(
116
+ "expected lix_registered_schema row, got schema_key={}",
117
+ row.schema_key
118
+ ),
119
+ ));
120
+ }
121
+
122
+ let Some(snapshot_content) = row.snapshot_content.as_deref() else {
123
+ return Ok(None);
124
+ };
125
+
126
+ let snapshot: JsonValue = serde_json::from_str(snapshot_content).map_err(|err| {
127
+ LixError::new(
128
+ "LIX_ERROR_UNKNOWN",
129
+ format!("invalid registered schema snapshot JSON: {err}"),
130
+ )
131
+ })?;
132
+ let schema = snapshot.get("value").cloned().ok_or_else(|| {
133
+ LixError::new(
134
+ "LIX_ERROR_UNKNOWN",
135
+ "registered schema snapshot missing value",
136
+ )
137
+ })?;
138
+ let key = schema_key_from_definition(&schema)?;
139
+ Ok(Some((key, schema)))
140
+ }
141
+
142
+ #[cfg(test)]
143
+ mod tests {
144
+ use async_trait::async_trait;
145
+ use serde_json::json;
146
+
147
+ use super::*;
148
+ use crate::live_state::LiveStateRowRequest;
149
+ use crate::GLOBAL_VERSION_ID;
150
+
151
+ #[tokio::test]
152
+ async fn visible_schemas_are_loaded_from_registered_schema_rows() {
153
+ let context = CatalogContext::new();
154
+
155
+ let schemas = context
156
+ .schema_jsons_for_sql_read_planning(
157
+ &RowsLiveStateReader::new(vec![
158
+ registered_schema_row("lix_registered_schema"),
159
+ registered_schema_row("lix_key_value"),
160
+ ]),
161
+ "global",
162
+ )
163
+ .await
164
+ .expect("schema visibility should load");
165
+
166
+ assert!(schemas.iter().any(|schema| {
167
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("lix_registered_schema")
168
+ }));
169
+ assert!(schemas.iter().any(|schema| {
170
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("lix_key_value")
171
+ }));
172
+ }
173
+
174
+ #[tokio::test]
175
+ async fn visible_schemas_include_registered_schema_rows() {
176
+ let context = CatalogContext::new();
177
+
178
+ let schemas = context
179
+ .schema_jsons_for_sql_read_planning(
180
+ &RowsLiveStateReader::new(vec![registered_schema_row("engine_dynamic_schema")]),
181
+ "global",
182
+ )
183
+ .await
184
+ .expect("schema visibility should load");
185
+
186
+ assert!(schemas.iter().any(|schema| {
187
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("engine_dynamic_schema")
188
+ }));
189
+ }
190
+
191
+ #[tokio::test]
192
+ async fn sql_read_planning_rejects_multiple_visible_schemas_for_same_surface() {
193
+ let context = CatalogContext::new();
194
+ let error = context
195
+ .schema_jsons_for_sql_read_planning(
196
+ &RowsLiveStateReader::new(vec![
197
+ registered_schema_row("engine_dynamic_schema"),
198
+ registered_schema_row("engine_dynamic_schema"),
199
+ ]),
200
+ "global",
201
+ )
202
+ .await
203
+ .expect_err("SQL surfaces must not choose a schema identity implicitly");
204
+
205
+ assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
206
+ assert!(error.message.contains("SQL surface schema"));
207
+ }
208
+
209
+ #[tokio::test]
210
+ async fn tracked_domain_sees_tracked_seed_schemas_but_not_user_untracked_schemas() {
211
+ let context = CatalogContext::new();
212
+ let mut seed_schema = registered_schema_row("lix_key_value");
213
+ seed_schema.untracked = false;
214
+
215
+ let facts = context
216
+ .schema_facts_for_domain(
217
+ &RowsLiveStateReader::new(vec![
218
+ seed_schema,
219
+ registered_schema_row("engine_dynamic_schema"),
220
+ ]),
221
+ &Domain::schema_catalog("global", false),
222
+ )
223
+ .await
224
+ .expect("schema visibility should load");
225
+ let schemas = facts
226
+ .iter()
227
+ .map(SchemaCatalogFact::schema)
228
+ .collect::<Vec<_>>();
229
+
230
+ assert!(schemas.iter().any(|schema| {
231
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("lix_key_value")
232
+ }));
233
+ assert!(!schemas.iter().any(|schema| {
234
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("engine_dynamic_schema")
235
+ }));
236
+ }
237
+
238
+ #[tokio::test]
239
+ async fn tracked_domain_does_not_see_untracked_seed_schemas() {
240
+ let context = CatalogContext::new();
241
+
242
+ let facts = context
243
+ .schema_facts_for_domain(
244
+ &RowsLiveStateReader::new(vec![registered_schema_row("lix_key_value")]),
245
+ &Domain::schema_catalog("global", false),
246
+ )
247
+ .await
248
+ .expect("schema visibility should load");
249
+ let schemas = facts
250
+ .iter()
251
+ .map(SchemaCatalogFact::schema)
252
+ .collect::<Vec<_>>();
253
+
254
+ assert!(!schemas.iter().any(|schema| {
255
+ schema.get("x-lix-key").and_then(JsonValue::as_str) == Some("lix_key_value")
256
+ }));
257
+ }
258
+
259
+ #[tokio::test]
260
+ async fn visible_schemas_ignore_projected_global_schema_rows_for_version_scope() {
261
+ let context = CatalogContext::new();
262
+ let mut global_only = registered_schema_row("global_only_schema");
263
+ global_only.global = true;
264
+ global_only.version_id = "main".to_string();
265
+
266
+ let schemas = context
267
+ .schema_jsons_for_sql_read_planning(
268
+ &RowsLiveStateReader::new(vec![global_only]),
269
+ "main",
270
+ )
271
+ .await
272
+ .expect("schema visibility should load");
273
+
274
+ assert!(schemas.is_empty());
275
+ }
276
+
277
+ #[tokio::test]
278
+ async fn schema_facts_post_filter_non_catalog_rows_even_if_reader_returns_them() {
279
+ let context = CatalogContext::new();
280
+ let valid_schema = registered_schema_row("valid_schema");
281
+ let mut file_scoped_schema = registered_schema_row("file_scoped_schema");
282
+ file_scoped_schema.file_id = Some("file-a".to_string());
283
+ let mut tombstoned_schema = registered_schema_row("tombstoned_schema");
284
+ tombstoned_schema.snapshot_content = None;
285
+
286
+ let facts = context
287
+ .schema_facts_for_domain(
288
+ &RowsLiveStateReader::new(vec![
289
+ valid_schema,
290
+ file_scoped_schema,
291
+ tombstoned_schema,
292
+ ]),
293
+ &Domain::schema_catalog("global", true),
294
+ )
295
+ .await
296
+ .expect("schema facts should load");
297
+ let schema_keys = facts
298
+ .iter()
299
+ .filter_map(|fact| fact.schema().get("x-lix-key").and_then(JsonValue::as_str))
300
+ .collect::<Vec<_>>();
301
+
302
+ assert_eq!(schema_keys, vec!["valid_schema"]);
303
+ }
304
+
305
+ #[tokio::test]
306
+ async fn visible_schemas_are_empty_when_no_schema_rows_are_visible() {
307
+ let context = CatalogContext::new();
308
+
309
+ let schemas = context
310
+ .schema_jsons_for_sql_read_planning(&RowsLiveStateReader::new(Vec::new()), "global")
311
+ .await
312
+ .expect("schema visibility should load");
313
+
314
+ assert!(schemas.is_empty());
315
+ }
316
+
317
+ struct RowsLiveStateReader {
318
+ rows: Vec<MaterializedLiveStateRow>,
319
+ }
320
+
321
+ impl RowsLiveStateReader {
322
+ fn new(rows: Vec<MaterializedLiveStateRow>) -> Self {
323
+ Self { rows }
324
+ }
325
+ }
326
+
327
+ #[async_trait]
328
+ impl LiveStateReader for RowsLiveStateReader {
329
+ async fn scan_rows(
330
+ &self,
331
+ request: &LiveStateScanRequest,
332
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
333
+ Ok(self
334
+ .rows
335
+ .iter()
336
+ .filter(|row| {
337
+ request.filter.schema_keys.is_empty()
338
+ || request.filter.schema_keys.contains(&row.schema_key)
339
+ })
340
+ .filter(|row| {
341
+ request.filter.version_ids.is_empty()
342
+ || request.filter.version_ids.contains(&row.version_id)
343
+ })
344
+ .filter(|row| {
345
+ request
346
+ .filter
347
+ .untracked
348
+ .is_none_or(|untracked| row.untracked == untracked)
349
+ })
350
+ .cloned()
351
+ .collect())
352
+ }
353
+
354
+ async fn load_row(
355
+ &self,
356
+ request: &LiveStateRowRequest,
357
+ ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
358
+ Ok(self
359
+ .rows
360
+ .iter()
361
+ .find(|row| {
362
+ row.schema_key == request.schema_key
363
+ && row.version_id == request.version_id
364
+ && row.entity_id == request.entity_id
365
+ })
366
+ .cloned())
367
+ }
368
+ }
369
+
370
+ fn registered_schema_row(schema_key: &str) -> MaterializedLiveStateRow {
371
+ MaterializedLiveStateRow {
372
+ entity_id: registered_schema_entity_id(schema_key),
373
+ file_id: None,
374
+ schema_key: REGISTERED_SCHEMA_KEY.to_string(),
375
+ version_id: GLOBAL_VERSION_ID.to_string(),
376
+ metadata: None,
377
+ deleted: false,
378
+ change_id: Some("change-registered-schema".to_string()),
379
+ commit_id: None,
380
+ global: true,
381
+ untracked: true,
382
+ created_at: "2026-04-23T00:00:00Z".to_string(),
383
+ updated_at: "2026-04-23T01:00:00Z".to_string(),
384
+ snapshot_content: Some(
385
+ json!({
386
+ "value": {
387
+ "x-lix-key": schema_key,
388
+ "type": "object",
389
+ "properties": {
390
+ "id": { "type": "string" }
391
+ },
392
+ "required": ["id"],
393
+ "additionalProperties": false
394
+ }
395
+ })
396
+ .to_string(),
397
+ ),
398
+ }
399
+ }
400
+
401
+ fn registered_schema_entity_id(schema_key: &str) -> crate::entity_identity::EntityIdentity {
402
+ crate::entity_identity::EntityIdentity::from_primary_key_paths(
403
+ &json!({
404
+ "value": {
405
+ "x-lix-key": schema_key,
406
+ }
407
+ }),
408
+ &[vec!["value".to_string(), "x-lix-key".to_string()]],
409
+ )
410
+ .expect("registered schema identity should derive")
411
+ }
412
+ }
@@ -0,0 +1,10 @@
1
+ mod context;
2
+ mod schema;
3
+ mod snapshot;
4
+
5
+ pub(crate) use context::CatalogContext;
6
+ pub(crate) use schema::{
7
+ ForeignKeyPlan, SchemaCatalogFact, SchemaCatalogKey, SchemaPlan, SchemaPlanId,
8
+ StateForeignKeyPlan,
9
+ };
10
+ pub(crate) use snapshot::{CatalogSnapshot, StateDeleteReferencePlan};
@@ -0,0 +1,4 @@
1
+ pub(crate) use super::snapshot::{
2
+ ForeignKeyPlan, SchemaCatalogFact, SchemaCatalogKey, SchemaPlan, SchemaPlanId,
3
+ StateForeignKeyPlan,
4
+ };