@lix-js/sdk 0.6.0-preview.4 → 0.6.0-preview.5

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 (234) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +65 -64
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
  5. package/dist/engine-wasm/wasm/lix_engine.js +130 -118
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
  8. package/dist/generated/builtin-schemas.d.ts +69 -69
  9. package/dist/generated/builtin-schemas.js +94 -94
  10. package/dist/open-lix.d.ts +33 -26
  11. package/dist/open-lix.js +10 -10
  12. package/dist/sqlite/index.js +86 -30
  13. package/dist-engine-src/README.md +3 -3
  14. package/dist-engine-src/src/backend/capabilities.rs +67 -0
  15. package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
  16. package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
  17. package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
  18. package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
  19. package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
  20. package/dist-engine-src/src/backend/conformance/model.rs +28 -0
  21. package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
  22. package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
  23. package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
  24. package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
  25. package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
  26. package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
  27. package/dist-engine-src/src/backend/conformance/write.rs +16 -0
  28. package/dist-engine-src/src/backend/error.rs +94 -0
  29. package/dist-engine-src/src/backend/in_memory.rs +670 -0
  30. package/dist-engine-src/src/backend/mod.rs +36 -9
  31. package/dist-engine-src/src/backend/predicate.rs +80 -0
  32. package/dist-engine-src/src/backend/traits.rs +260 -0
  33. package/dist-engine-src/src/backend/types.rs +224 -81
  34. package/dist-engine-src/src/binary_cas/context.rs +8 -8
  35. package/dist-engine-src/src/binary_cas/kv.rs +234 -259
  36. package/dist-engine-src/src/{version → branch}/context.rs +12 -12
  37. package/dist-engine-src/src/branch/lifecycle.rs +221 -0
  38. package/dist-engine-src/src/branch/mod.rs +13 -0
  39. package/dist-engine-src/src/branch/refs.rs +321 -0
  40. package/dist-engine-src/src/branch/stage_rows.rs +67 -0
  41. package/dist-engine-src/src/branch/types.rs +21 -0
  42. package/dist-engine-src/src/catalog/context.rs +18 -18
  43. package/dist-engine-src/src/catalog/snapshot.rs +8 -8
  44. package/dist-engine-src/src/changelog/bench_support.rs +785 -0
  45. package/dist-engine-src/src/changelog/change.rs +1 -0
  46. package/dist-engine-src/src/changelog/codec.rs +497 -0
  47. package/dist-engine-src/src/changelog/commit.rs +1 -0
  48. package/dist-engine-src/src/changelog/context.rs +1614 -0
  49. package/dist-engine-src/src/changelog/mod.rs +29 -0
  50. package/dist-engine-src/src/changelog/store.rs +163 -0
  51. package/dist-engine-src/src/changelog/test_support.rs +54 -0
  52. package/dist-engine-src/src/changelog/types.rs +213 -0
  53. package/dist-engine-src/src/commit_graph/context.rs +317 -274
  54. package/dist-engine-src/src/commit_graph/mod.rs +2 -4
  55. package/dist-engine-src/src/commit_graph/types.rs +22 -42
  56. package/dist-engine-src/src/commit_graph/walker.rs +133 -103
  57. package/dist-engine-src/src/common/error.rs +52 -18
  58. package/dist-engine-src/src/common/identity.rs +2 -2
  59. package/dist-engine-src/src/common/mod.rs +1 -1
  60. package/dist-engine-src/src/domain.rs +42 -46
  61. package/dist-engine-src/src/engine.rs +74 -96
  62. package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
  63. package/dist-engine-src/src/functions/context.rs +56 -52
  64. package/dist-engine-src/src/functions/state.rs +51 -52
  65. package/dist-engine-src/src/init.rs +288 -154
  66. package/dist-engine-src/src/json_store/context.rs +15 -266
  67. package/dist-engine-src/src/json_store/mod.rs +26 -0
  68. package/dist-engine-src/src/json_store/store.rs +103 -718
  69. package/dist-engine-src/src/json_store/types.rs +4 -9
  70. package/dist-engine-src/src/lib.rs +49 -19
  71. package/dist-engine-src/src/live_state/context.rs +654 -790
  72. package/dist-engine-src/src/live_state/mod.rs +9 -3
  73. package/dist-engine-src/src/live_state/overlay.rs +4 -4
  74. package/dist-engine-src/src/live_state/types.rs +30 -21
  75. package/dist-engine-src/src/live_state/visibility.rs +514 -71
  76. package/dist-engine-src/src/plugin/install.rs +48 -48
  77. package/dist-engine-src/src/plugin/manifest.rs +7 -7
  78. package/dist-engine-src/src/plugin/materializer.rs +0 -275
  79. package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
  80. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
  81. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
  82. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
  85. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
  86. package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
  87. package/dist-engine-src/src/schema/compatibility.rs +11 -11
  88. package/dist-engine-src/src/schema/definition.json +2 -2
  89. package/dist-engine-src/src/schema/definition.rs +5 -5
  90. package/dist-engine-src/src/schema/key.rs +3 -3
  91. package/dist-engine-src/src/schema/mod.rs +1 -1
  92. package/dist-engine-src/src/schema/tests.rs +18 -18
  93. package/dist-engine-src/src/session/context.rs +803 -148
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +223 -83
  96. package/dist-engine-src/src/session/merge/analysis.rs +9 -3
  97. package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
  98. package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
  99. package/dist-engine-src/src/session/merge/mod.rs +5 -6
  100. package/dist-engine-src/src/session/merge/stats.rs +7 -11
  101. package/dist-engine-src/src/session/mod.rs +15 -12
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +495 -14
  104. package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
  105. package/dist-engine-src/src/sql2/bind/error.rs +5 -0
  106. package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
  107. package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
  108. package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
  109. package/dist-engine-src/src/sql2/bind/read.rs +65 -0
  110. package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
  111. package/dist-engine-src/src/sql2/bind/table.rs +273 -0
  112. package/dist-engine-src/src/sql2/bind/write.rs +86 -0
  113. package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
  114. package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
  115. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
  116. package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
  117. package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
  118. package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
  119. package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
  120. package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
  121. package/dist-engine-src/src/sql2/context.rs +36 -30
  122. package/dist-engine-src/src/sql2/error.rs +1 -1
  123. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
  124. package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
  125. package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
  126. package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
  127. package/dist-engine-src/src/sql2/exec/write.rs +661 -0
  128. package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
  129. package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
  130. package/dist-engine-src/src/sql2/history_projection.rs +8 -8
  131. package/dist-engine-src/src/sql2/history_route.rs +35 -31
  132. package/dist-engine-src/src/sql2/mod.rs +28 -23
  133. package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
  134. package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
  135. package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
  136. package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
  137. package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
  138. package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
  139. package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
  140. package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
  141. package/dist-engine-src/src/sql2/plan/write.rs +147 -0
  142. package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
  143. package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
  144. package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
  145. package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
  146. package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
  147. package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
  148. package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
  149. package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
  150. package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
  151. package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
  152. package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
  153. package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
  154. package/dist-engine-src/src/sql2/read_only.rs +2 -2
  155. package/dist-engine-src/src/sql2/session.rs +47 -96
  156. package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
  157. package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
  158. package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
  159. package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
  160. package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
  161. package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
  162. package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
  163. package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
  164. package/dist-engine-src/src/storage/conformance.rs +399 -0
  165. package/dist-engine-src/src/storage/context.rs +552 -288
  166. package/dist-engine-src/src/storage/mod.rs +48 -10
  167. package/dist-engine-src/src/storage/point.rs +440 -0
  168. package/dist-engine-src/src/storage/read_scope.rs +43 -64
  169. package/dist-engine-src/src/storage/reader.rs +867 -0
  170. package/dist-engine-src/src/storage/scan.rs +784 -0
  171. package/dist-engine-src/src/storage/spaces.rs +236 -0
  172. package/dist-engine-src/src/storage/stats.rs +80 -0
  173. package/dist-engine-src/src/storage/write_set.rs +962 -0
  174. package/dist-engine-src/src/storage_bench.rs +136 -4828
  175. package/dist-engine-src/src/test_support.rs +360 -138
  176. package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
  177. package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
  178. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
  179. package/dist-engine-src/src/tracked_state/context.rs +1927 -993
  180. package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
  181. package/dist-engine-src/src/tracked_state/merge.rs +74 -88
  182. package/dist-engine-src/src/tracked_state/mod.rs +19 -16
  183. package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
  184. package/dist-engine-src/src/tracked_state/storage.rs +243 -191
  185. package/dist-engine-src/src/tracked_state/tree.rs +247 -371
  186. package/dist-engine-src/src/tracked_state/types.rs +49 -42
  187. package/dist-engine-src/src/transaction/bench_support.rs +407 -0
  188. package/dist-engine-src/src/transaction/commit.rs +821 -713
  189. package/dist-engine-src/src/transaction/context.rs +705 -600
  190. package/dist-engine-src/src/transaction/mod.rs +13 -2
  191. package/dist-engine-src/src/transaction/normalization.rs +63 -76
  192. package/dist-engine-src/src/transaction/prep.rs +13 -13
  193. package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
  194. package/dist-engine-src/src/transaction/staging.rs +228 -434
  195. package/dist-engine-src/src/transaction/types.rs +41 -98
  196. package/dist-engine-src/src/transaction/validation.rs +382 -446
  197. package/dist-engine-src/src/untracked_state/codec.rs +337 -29
  198. package/dist-engine-src/src/untracked_state/context.rs +7 -7
  199. package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
  200. package/dist-engine-src/src/untracked_state/mod.rs +1 -1
  201. package/dist-engine-src/src/untracked_state/storage.rs +659 -157
  202. package/dist-engine-src/src/untracked_state/types.rs +21 -21
  203. package/package.json +71 -68
  204. package/dist-engine-src/src/backend/kv.rs +0 -358
  205. package/dist-engine-src/src/backend/testing.rs +0 -658
  206. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  207. package/dist-engine-src/src/commit_store/context.rs +0 -944
  208. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  209. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  210. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  211. package/dist-engine-src/src/commit_store/types.rs +0 -215
  212. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  213. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  214. package/dist-engine-src/src/session/create_version.rs +0 -88
  215. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  216. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  217. package/dist-engine-src/src/session/switch_version.rs +0 -110
  218. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  219. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  220. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  221. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  222. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  223. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  224. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  225. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  226. package/dist-engine-src/src/storage/types.rs +0 -501
  227. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  228. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  229. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  230. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  231. package/dist-engine-src/src/version/mod.rs +0 -13
  232. package/dist-engine-src/src/version/refs.rs +0 -330
  233. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  234. package/dist-engine-src/src/version/types.rs +0 -21
@@ -0,0 +1,273 @@
1
+ use datafusion::sql::sqlparser::ast::ObjectName;
2
+
3
+ use crate::LixError;
4
+
5
+ use super::super::catalog::{PublicCatalog, PublicSurfaceContract};
6
+ use super::expr::BoundColumnRef;
7
+ use super::write::BoundWriteOp;
8
+
9
+ #[derive(Clone, Debug, Eq, PartialEq)]
10
+ pub(crate) struct BoundTable {
11
+ pub(crate) name: String,
12
+ pub(crate) surface: PublicSurfaceContract,
13
+ }
14
+
15
+ pub(crate) fn bind_exact_table_name(name: &ObjectName) -> Result<String, LixError> {
16
+ if name.0.len() != 1 {
17
+ return Err(super::error::unsupported(
18
+ "qualified SQL table names are not supported",
19
+ ));
20
+ }
21
+ name.0
22
+ .first()
23
+ .and_then(|part| part.as_ident())
24
+ .map(|ident| {
25
+ if ident.quote_style.is_some() {
26
+ ident.value.clone()
27
+ } else {
28
+ ident.value.to_ascii_lowercase()
29
+ }
30
+ })
31
+ .ok_or_else(|| super::error::unsupported("unsupported SQL table name"))
32
+ }
33
+
34
+ pub(crate) fn bind_public_table(
35
+ catalog: &PublicCatalog,
36
+ name: &ObjectName,
37
+ ) -> Result<BoundTable, LixError> {
38
+ let table_name = bind_exact_table_name(name)?;
39
+ let surface = catalog.require_surface(&table_name)?.clone();
40
+ Ok(BoundTable {
41
+ name: table_name,
42
+ surface,
43
+ })
44
+ }
45
+
46
+ pub(crate) fn require_public_column<'a>(
47
+ table: &'a BoundTable,
48
+ column_name: &str,
49
+ ) -> Result<&'a super::super::catalog::PublicColumn, LixError> {
50
+ if table.surface.public_column(column_name).is_some() {
51
+ return Ok(table
52
+ .surface
53
+ .public_column(column_name)
54
+ .expect("checked public column"));
55
+ }
56
+ if table.surface.column(column_name).is_some() {
57
+ return Err(LixError::new(
58
+ LixError::CODE_COLUMN_NOT_FOUND,
59
+ format!(
60
+ "column '{column_name}' is not part of public SQL surface '{}'",
61
+ table.name
62
+ ),
63
+ ));
64
+ }
65
+ Err(LixError::new(
66
+ LixError::CODE_COLUMN_NOT_FOUND,
67
+ format!(
68
+ "column '{column_name}' does not exist on SQL table '{}'",
69
+ table.name
70
+ ),
71
+ ))
72
+ }
73
+
74
+ pub(crate) fn require_writable_column(
75
+ table: &BoundTable,
76
+ column_name: &str,
77
+ op: BoundWriteOp,
78
+ ) -> Result<BoundColumnRef, LixError> {
79
+ let column = require_public_column(table, column_name)?;
80
+ let allowed = match op {
81
+ BoundWriteOp::Insert => column.is_insertable(),
82
+ BoundWriteOp::Update => column.is_updatable(),
83
+ BoundWriteOp::Delete => false,
84
+ };
85
+ if !allowed {
86
+ if table.name == "lix_branch" && column_name == "id" && op == BoundWriteOp::Update {
87
+ return Err(super::error::unsupported(
88
+ "UPDATE lix_branch cannot change immutable column 'id'",
89
+ ));
90
+ }
91
+ return Err(super::error::unsupported(format!(
92
+ "column '{column_name}' is not writable on SQL table '{}'",
93
+ table.name
94
+ )));
95
+ }
96
+ Ok(BoundColumnRef {
97
+ table: table.name.clone(),
98
+ column_id: column.id,
99
+ name: column.name.clone(),
100
+ })
101
+ }
102
+
103
+ pub(crate) fn bind_public_column_ref(
104
+ table: &BoundTable,
105
+ column_name: &str,
106
+ ) -> Result<BoundColumnRef, LixError> {
107
+ let column = require_public_column(table, column_name)?;
108
+ Ok(BoundColumnRef {
109
+ table: table.name.clone(),
110
+ column_id: column.id,
111
+ name: column.name.clone(),
112
+ })
113
+ }
114
+
115
+ #[cfg(test)]
116
+ mod tests {
117
+ use datafusion::sql::sqlparser::ast::{SetExpr, Statement, TableFactor};
118
+ use datafusion::sql::sqlparser::dialect::GenericDialect;
119
+ use datafusion::sql::sqlparser::parser::Parser;
120
+ use serde_json::json;
121
+
122
+ use super::*;
123
+ use crate::sql2::catalog::PublicSurfaceKind;
124
+
125
+ #[test]
126
+ fn rejects_qualified_table_name_even_when_leaf_exists() {
127
+ let catalog = catalog();
128
+ let error = bind_public_table(&catalog, &table_name("SELECT * FROM foo.lix_state"))
129
+ .expect_err("qualified table should be rejected");
130
+
131
+ assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
132
+ }
133
+
134
+ #[test]
135
+ fn rejects_unknown_table_name() {
136
+ let catalog = catalog();
137
+ let error = bind_public_table(&catalog, &table_name("SELECT * FROM missing"))
138
+ .expect_err("unknown table should be rejected");
139
+
140
+ assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
141
+ assert!(error.message.contains("unknown SQL table 'missing'"));
142
+ }
143
+
144
+ #[test]
145
+ fn base_entity_table_does_not_expose_branch_column() {
146
+ let catalog = catalog();
147
+ let table = bind_public_table(&catalog, &table_name("SELECT * FROM test_state_schema"))
148
+ .expect("base entity table should bind");
149
+
150
+ assert!(matches!(
151
+ table.surface.kind,
152
+ PublicSurfaceKind::EntityBase { .. }
153
+ ));
154
+ assert!(require_public_column(&table, "name").is_ok());
155
+ let error = require_public_column(&table, "lixcol_branch_id")
156
+ .expect_err("base entity surface should not expose branch column");
157
+ assert!(error.message.contains("does not exist"));
158
+ }
159
+
160
+ #[test]
161
+ fn by_branch_entity_exposes_lixcol_branch_id_without_branch_id_alias() {
162
+ let catalog = catalog();
163
+ let table = bind_public_table(
164
+ &catalog,
165
+ &table_name("SELECT * FROM test_state_schema_by_branch"),
166
+ )
167
+ .expect("by-branch entity table should bind");
168
+
169
+ assert!(matches!(
170
+ table.surface.kind,
171
+ PublicSurfaceKind::EntityByBranch { .. }
172
+ ));
173
+ assert!(require_public_column(&table, "lixcol_branch_id").is_ok());
174
+ let error = require_public_column(&table, "branch_id")
175
+ .expect_err("by-branch entity surface should not alias branch_id");
176
+ assert!(error.message.contains("does not exist"));
177
+ }
178
+
179
+ #[test]
180
+ fn quoted_table_names_are_case_sensitive() {
181
+ let catalog = catalog();
182
+
183
+ bind_public_table(&catalog, &table_name("SELECT * FROM \"lix_state\""))
184
+ .expect("quoted exact case should bind");
185
+ let error = bind_public_table(&catalog, &table_name("SELECT * FROM \"LIX_STATE\""))
186
+ .expect_err("quoted mixed case should not be folded");
187
+
188
+ assert!(error.message.contains("unknown SQL table 'LIX_STATE'"));
189
+ }
190
+
191
+ #[test]
192
+ fn hidden_columns_cannot_bind_as_public_columns() {
193
+ let catalog = catalog();
194
+ let table = bind_public_table(&catalog, &table_name("SELECT * FROM lix_file"))
195
+ .expect("lix_file should bind");
196
+
197
+ let error = require_public_column(&table, "lixcol_schema_key")
198
+ .expect_err("hidden column should not bind");
199
+ assert!(error.message.contains("not part of public SQL surface"));
200
+ }
201
+
202
+ #[test]
203
+ fn catalog_rejects_duplicate_public_surface_names() {
204
+ let error = PublicCatalog::from_visible_schemas(&[json!({
205
+ "x-lix-key": "lix_file",
206
+ "properties": {
207
+ "id": { "type": "string" }
208
+ }
209
+ })])
210
+ .expect_err("system table collisions should be rejected");
211
+
212
+ assert_eq!(error.code, LixError::CODE_SCHEMA_DEFINITION);
213
+ assert!(error
214
+ .message
215
+ .contains("duplicate public SQL surface 'lix_file'"));
216
+ }
217
+
218
+ #[test]
219
+ fn catalog_uses_validated_entity_surface_derivation() {
220
+ let catalog = PublicCatalog::from_visible_schemas(&[json!({
221
+ "x-lix-key": "bad_entity",
222
+ "properties": {
223
+ "value": { "type": "null" }
224
+ }
225
+ })])
226
+ .expect("invalid entity schemas should match provider behavior and be skipped");
227
+
228
+ assert!(catalog.surface("bad_entity").is_none());
229
+ }
230
+
231
+ #[test]
232
+ fn dynamic_entity_history_surface_uses_provider_history_column_names() {
233
+ let catalog = catalog();
234
+ let table = bind_public_table(
235
+ &catalog,
236
+ &table_name("SELECT * FROM test_state_schema_history"),
237
+ )
238
+ .expect("entity history surface should bind");
239
+
240
+ assert!(matches!(
241
+ table.surface.kind,
242
+ PublicSurfaceKind::EntityHistory { .. }
243
+ ));
244
+ assert!(require_public_column(&table, "lixcol_entity_pk").is_ok());
245
+ assert!(require_public_column(&table, "lixcol_snapshot_content").is_ok());
246
+ }
247
+
248
+ fn catalog() -> PublicCatalog {
249
+ PublicCatalog::from_visible_schemas(&[json!({
250
+ "x-lix-key": "test_state_schema",
251
+ "properties": {
252
+ "id": { "type": "string" },
253
+ "name": { "type": "string" },
254
+ "lixcol_internal": { "type": "string" }
255
+ }
256
+ })])
257
+ .expect("test catalog")
258
+ }
259
+
260
+ fn table_name(sql: &str) -> ObjectName {
261
+ let mut statements = Parser::parse_sql(&GenericDialect {}, sql).expect("parse SQL");
262
+ let Some(Statement::Query(query)) = statements.pop() else {
263
+ panic!("expected query");
264
+ };
265
+ let SetExpr::Select(select) = query.body.as_ref() else {
266
+ panic!("expected select");
267
+ };
268
+ let TableFactor::Table { name, .. } = &select.from[0].relation else {
269
+ panic!("expected table factor");
270
+ };
271
+ name.clone()
272
+ }
273
+ }
@@ -0,0 +1,86 @@
1
+ use super::expr::{BoundColumnRef, BoundExpr, BoundParamRef};
2
+ use super::read::BoundRead;
3
+ use crate::sql2::plan::branch_scope::BranchScope;
4
+ use crate::sql2::plan::predicate::BoundPredicate;
5
+ use std::collections::BTreeMap;
6
+
7
+ #[derive(Clone, Debug, Eq, PartialEq)]
8
+ pub(crate) struct BoundWrite {
9
+ pub(crate) target: BoundWriteTarget,
10
+ pub(crate) op: BoundWriteOp,
11
+ pub(crate) input: BoundWriteInput,
12
+ pub(crate) predicate: BoundPredicate,
13
+ pub(crate) assignments: Vec<BoundAssignment>,
14
+ pub(crate) params: BoundParamMap,
15
+ pub(crate) branch_scope: BranchScope,
16
+ }
17
+
18
+ #[derive(Clone, Debug, Eq, PartialEq)]
19
+ pub(crate) enum BoundWriteTarget {
20
+ LixState,
21
+ LixStateByBranch,
22
+ Entity(EntityWriteSurface),
23
+ File(FileWriteSurface),
24
+ Directory(DirectoryWriteSurface),
25
+ Branch,
26
+ }
27
+
28
+ #[derive(Clone, Debug, Eq, PartialEq)]
29
+ pub(crate) enum EntityWriteSurface {
30
+ Base { schema_key: String },
31
+ ByBranch { schema_key: String },
32
+ }
33
+
34
+ #[derive(Clone, Debug, Eq, PartialEq)]
35
+ pub(crate) enum FileWriteSurface {
36
+ Base,
37
+ ByBranch,
38
+ }
39
+
40
+ #[derive(Clone, Debug, Eq, PartialEq)]
41
+ pub(crate) enum DirectoryWriteSurface {
42
+ Base,
43
+ ByBranch,
44
+ }
45
+
46
+ #[derive(Clone, Debug, Eq, PartialEq)]
47
+ pub(crate) enum BoundWriteOp {
48
+ Insert,
49
+ Update,
50
+ Delete,
51
+ }
52
+
53
+ #[derive(Clone, Debug, Eq, PartialEq)]
54
+ pub(crate) enum BoundWriteInput {
55
+ Values(BoundInsertValues),
56
+ Query {
57
+ query: Box<BoundRead>,
58
+ columns: Vec<BoundColumnRef>,
59
+ },
60
+ None,
61
+ }
62
+
63
+ #[derive(Clone, Debug, Eq, PartialEq)]
64
+ pub(crate) struct BoundInsertValues {
65
+ pub(crate) columns: Vec<BoundColumnRef>,
66
+ pub(crate) rows: Vec<Vec<BoundExpr>>,
67
+ }
68
+
69
+ impl BoundInsertValues {
70
+ pub(crate) fn column_index(&self, column_name: &str) -> Option<usize> {
71
+ self.columns
72
+ .iter()
73
+ .position(|column| column.name == column_name)
74
+ }
75
+ }
76
+
77
+ #[derive(Clone, Debug, Eq, PartialEq)]
78
+ pub(crate) struct BoundAssignment {
79
+ pub(crate) column: BoundColumnRef,
80
+ pub(crate) value: BoundExpr,
81
+ }
82
+
83
+ #[derive(Clone, Debug, Default, Eq, PartialEq)]
84
+ pub(crate) struct BoundParamMap {
85
+ pub(crate) params: BTreeMap<usize, BoundParamRef>,
86
+ }