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

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 (274) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +38 -207
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +38 -71
  16. package/SKILL.md +0 -507
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -833
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -359
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/capabilities.rs +0 -67
  31. package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
  32. package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
  33. package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
  34. package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
  35. package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
  36. package/dist-engine-src/src/backend/conformance/model.rs +0 -28
  37. package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
  38. package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
  39. package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
  40. package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
  41. package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
  42. package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
  43. package/dist-engine-src/src/backend/conformance/write.rs +0 -16
  44. package/dist-engine-src/src/backend/error.rs +0 -94
  45. package/dist-engine-src/src/backend/in_memory.rs +0 -670
  46. package/dist-engine-src/src/backend/mod.rs +0 -39
  47. package/dist-engine-src/src/backend/predicate.rs +0 -80
  48. package/dist-engine-src/src/backend/traits.rs +0 -260
  49. package/dist-engine-src/src/backend/types.rs +0 -239
  50. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  51. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  52. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  53. package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
  54. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  55. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  56. package/dist-engine-src/src/branch/context.rs +0 -40
  57. package/dist-engine-src/src/branch/lifecycle.rs +0 -221
  58. package/dist-engine-src/src/branch/mod.rs +0 -13
  59. package/dist-engine-src/src/branch/refs.rs +0 -321
  60. package/dist-engine-src/src/branch/stage_rows.rs +0 -67
  61. package/dist-engine-src/src/branch/types.rs +0 -21
  62. package/dist-engine-src/src/catalog/context.rs +0 -412
  63. package/dist-engine-src/src/catalog/mod.rs +0 -10
  64. package/dist-engine-src/src/catalog/schema.rs +0 -4
  65. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  66. package/dist-engine-src/src/cel/context.rs +0 -86
  67. package/dist-engine-src/src/cel/error.rs +0 -19
  68. package/dist-engine-src/src/cel/mod.rs +0 -8
  69. package/dist-engine-src/src/cel/provider.rs +0 -9
  70. package/dist-engine-src/src/cel/runtime.rs +0 -167
  71. package/dist-engine-src/src/cel/value.rs +0 -50
  72. package/dist-engine-src/src/changelog/bench_support.rs +0 -785
  73. package/dist-engine-src/src/changelog/change.rs +0 -1
  74. package/dist-engine-src/src/changelog/codec.rs +0 -497
  75. package/dist-engine-src/src/changelog/commit.rs +0 -1
  76. package/dist-engine-src/src/changelog/context.rs +0 -1614
  77. package/dist-engine-src/src/changelog/mod.rs +0 -29
  78. package/dist-engine-src/src/changelog/store.rs +0 -163
  79. package/dist-engine-src/src/changelog/test_support.rs +0 -54
  80. package/dist-engine-src/src/changelog/types.rs +0 -213
  81. package/dist-engine-src/src/commit_graph/context.rs +0 -944
  82. package/dist-engine-src/src/commit_graph/mod.rs +0 -9
  83. package/dist-engine-src/src/commit_graph/types.rs +0 -89
  84. package/dist-engine-src/src/commit_graph/walker.rs +0 -786
  85. package/dist-engine-src/src/common/error.rs +0 -347
  86. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  87. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  88. package/dist-engine-src/src/common/identity.rs +0 -145
  89. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  90. package/dist-engine-src/src/common/metadata.rs +0 -40
  91. package/dist-engine-src/src/common/mod.rs +0 -23
  92. package/dist-engine-src/src/common/types.rs +0 -105
  93. package/dist-engine-src/src/common/wire.rs +0 -222
  94. package/dist-engine-src/src/domain.rs +0 -320
  95. package/dist-engine-src/src/engine.rs +0 -203
  96. package/dist-engine-src/src/entity_pk.rs +0 -402
  97. package/dist-engine-src/src/functions/context.rs +0 -296
  98. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  99. package/dist-engine-src/src/functions/mod.rs +0 -18
  100. package/dist-engine-src/src/functions/provider.rs +0 -130
  101. package/dist-engine-src/src/functions/state.rs +0 -335
  102. package/dist-engine-src/src/functions/types.rs +0 -37
  103. package/dist-engine-src/src/init.rs +0 -692
  104. package/dist-engine-src/src/json_store/compression.rs +0 -77
  105. package/dist-engine-src/src/json_store/context.rs +0 -172
  106. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  107. package/dist-engine-src/src/json_store/mod.rs +0 -38
  108. package/dist-engine-src/src/json_store/store.rs +0 -494
  109. package/dist-engine-src/src/json_store/types.rs +0 -212
  110. package/dist-engine-src/src/lib.rs +0 -92
  111. package/dist-engine-src/src/live_state/context.rs +0 -1883
  112. package/dist-engine-src/src/live_state/mod.rs +0 -21
  113. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  114. package/dist-engine-src/src/live_state/reader.rs +0 -23
  115. package/dist-engine-src/src/live_state/types.rs +0 -231
  116. package/dist-engine-src/src/live_state/visibility.rs +0 -666
  117. package/dist-engine-src/src/plugin/archive.rs +0 -438
  118. package/dist-engine-src/src/plugin/component.rs +0 -183
  119. package/dist-engine-src/src/plugin/install.rs +0 -619
  120. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  121. package/dist-engine-src/src/plugin/materializer.rs +0 -202
  122. package/dist-engine-src/src/plugin/mod.rs +0 -33
  123. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
  124. package/dist-engine-src/src/plugin/storage.rs +0 -74
  125. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  126. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  127. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  128. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  129. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  130. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
  131. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
  132. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  133. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  134. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  135. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  136. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  137. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  138. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  139. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  140. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  141. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  142. package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
  143. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  144. package/dist-engine-src/src/schema/definition.json +0 -187
  145. package/dist-engine-src/src/schema/definition.rs +0 -742
  146. package/dist-engine-src/src/schema/key.rs +0 -138
  147. package/dist-engine-src/src/schema/mod.rs +0 -20
  148. package/dist-engine-src/src/schema/seed.rs +0 -14
  149. package/dist-engine-src/src/schema/tests.rs +0 -780
  150. package/dist-engine-src/src/session/context.rs +0 -1059
  151. package/dist-engine-src/src/session/create_branch.rs +0 -94
  152. package/dist-engine-src/src/session/execute.rs +0 -681
  153. package/dist-engine-src/src/session/merge/analysis.rs +0 -108
  154. package/dist-engine-src/src/session/merge/branch.rs +0 -417
  155. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  156. package/dist-engine-src/src/session/merge/mod.rs +0 -10
  157. package/dist-engine-src/src/session/merge/stats.rs +0 -61
  158. package/dist-engine-src/src/session/mod.rs +0 -30
  159. package/dist-engine-src/src/session/switch_branch.rs +0 -113
  160. package/dist-engine-src/src/session/transaction.rs +0 -557
  161. package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
  162. package/dist-engine-src/src/sql2/bind/error.rs +0 -5
  163. package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
  164. package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
  165. package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
  166. package/dist-engine-src/src/sql2/bind/read.rs +0 -65
  167. package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
  168. package/dist-engine-src/src/sql2/bind/table.rs +0 -273
  169. package/dist-engine-src/src/sql2/bind/write.rs +0 -86
  170. package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
  171. package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
  172. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
  173. package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
  174. package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
  175. package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
  176. package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
  177. package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
  178. package/dist-engine-src/src/sql2/context.rs +0 -317
  179. package/dist-engine-src/src/sql2/dml.rs +0 -148
  180. package/dist-engine-src/src/sql2/error.rs +0 -215
  181. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
  182. package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
  183. package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
  184. package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
  185. package/dist-engine-src/src/sql2/exec/write.rs +0 -661
  186. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
  187. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  188. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  189. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  190. package/dist-engine-src/src/sql2/history_route.rs +0 -661
  191. package/dist-engine-src/src/sql2/mod.rs +0 -52
  192. package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
  193. package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
  194. package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
  195. package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
  196. package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
  197. package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
  198. package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
  199. package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
  200. package/dist-engine-src/src/sql2/plan/write.rs +0 -147
  201. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
  202. package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
  203. package/dist-engine-src/src/sql2/providers/change.rs +0 -445
  204. package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
  205. package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
  206. package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
  207. package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
  208. package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
  209. package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
  210. package/dist-engine-src/src/sql2/providers/history.rs +0 -426
  211. package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
  212. package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
  213. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  214. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  215. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  216. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  217. package/dist-engine-src/src/sql2/session.rs +0 -83
  218. package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
  219. package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
  220. package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
  221. package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
  222. package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
  223. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  224. package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
  225. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  226. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  227. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  228. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  229. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  230. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  231. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  232. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  233. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
  234. package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
  235. package/dist-engine-src/src/storage/conformance.rs +0 -399
  236. package/dist-engine-src/src/storage/context.rs +0 -620
  237. package/dist-engine-src/src/storage/mod.rs +0 -52
  238. package/dist-engine-src/src/storage/point.rs +0 -440
  239. package/dist-engine-src/src/storage/read_scope.rs +0 -67
  240. package/dist-engine-src/src/storage/reader.rs +0 -867
  241. package/dist-engine-src/src/storage/scan.rs +0 -784
  242. package/dist-engine-src/src/storage/spaces.rs +0 -236
  243. package/dist-engine-src/src/storage/stats.rs +0 -80
  244. package/dist-engine-src/src/storage/write_set.rs +0 -962
  245. package/dist-engine-src/src/storage_bench.rs +0 -171
  246. package/dist-engine-src/src/test_support.rs +0 -450
  247. package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
  248. package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
  249. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
  250. package/dist-engine-src/src/tracked_state/context.rs +0 -2801
  251. package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
  252. package/dist-engine-src/src/tracked_state/merge.rs +0 -478
  253. package/dist-engine-src/src/tracked_state/mod.rs +0 -35
  254. package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
  255. package/dist-engine-src/src/tracked_state/storage.rs +0 -427
  256. package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
  257. package/dist-engine-src/src/tracked_state/types.rs +0 -238
  258. package/dist-engine-src/src/transaction/bench_support.rs +0 -407
  259. package/dist-engine-src/src/transaction/commit.rs +0 -1592
  260. package/dist-engine-src/src/transaction/context.rs +0 -1653
  261. package/dist-engine-src/src/transaction/mod.rs +0 -24
  262. package/dist-engine-src/src/transaction/normalization.rs +0 -877
  263. package/dist-engine-src/src/transaction/prep.rs +0 -37
  264. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
  265. package/dist-engine-src/src/transaction/staging.rs +0 -1525
  266. package/dist-engine-src/src/transaction/types.rs +0 -403
  267. package/dist-engine-src/src/transaction/validation.rs +0 -5766
  268. package/dist-engine-src/src/untracked_state/codec.rs +0 -615
  269. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  270. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  271. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  272. package/dist-engine-src/src/untracked_state/storage.rs +0 -898
  273. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  274. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,2236 +0,0 @@
1
- use std::collections::{BTreeMap, BTreeSet};
2
- use std::ops::ControlFlow;
3
-
4
- use datafusion::sql::parser::Statement as DataFusionStatement;
5
- use datafusion::sql::sqlparser::ast::{
6
- AssignmentTarget, BinaryOperator, Delete, Expr, FromTable, Function, FunctionArg,
7
- FunctionArgExpr, FunctionArguments, Insert, ObjectName, ObjectNamePart, Query, SetExpr,
8
- Statement as SqlStatement, TableFactor, TableObject, TableWithJoins, UnaryOperator, Update,
9
- Value, Visit, Visitor,
10
- };
11
- use serde_json::Value as JsonValue;
12
-
13
- use crate::sql2::catalog::{PublicCatalog, PublicSurfaceContract, PublicSurfaceKind};
14
- use crate::sql2::plan::branch_scope::BranchScope;
15
- use crate::sql2::plan::predicate::BoundPredicate;
16
- use crate::LixError;
17
- use crate::GLOBAL_BRANCH_ID;
18
-
19
- use super::expr::{BoundExpr, BoundLiteral, BoundParamRef};
20
- use super::read::BoundRead;
21
- use super::table::{
22
- bind_public_column_ref, bind_public_table, require_writable_column, BoundTable,
23
- };
24
- use super::write::{
25
- BoundAssignment, BoundInsertValues, BoundParamMap, BoundWrite, BoundWriteInput, BoundWriteOp,
26
- BoundWriteTarget, DirectoryWriteSurface, EntityWriteSurface, FileWriteSurface,
27
- };
28
-
29
- pub(crate) fn bind_statement(
30
- statement: &DataFusionStatement,
31
- visible_schemas: &[JsonValue],
32
- active_branch_id: &str,
33
- ) -> Result<BoundWrite, LixError> {
34
- let catalog = PublicCatalog::from_visible_schemas(visible_schemas)?;
35
- match statement {
36
- DataFusionStatement::Statement(statement) => {
37
- bind_sql_statement(statement, &catalog, active_branch_id)
38
- }
39
- DataFusionStatement::Explain(_) => Err(super::error::unsupported(
40
- "EXPLAIN statements are not supported by SQL write binding",
41
- )),
42
- _ => Err(super::error::unsupported(format!(
43
- "SQL statement is not supported by Lix SQL: {statement}"
44
- ))),
45
- }
46
- }
47
-
48
- fn bind_sql_statement(
49
- statement: &SqlStatement,
50
- catalog: &PublicCatalog,
51
- active_branch_id: &str,
52
- ) -> Result<BoundWrite, LixError> {
53
- match statement {
54
- SqlStatement::Insert(insert) => bind_insert_bound(insert, catalog, active_branch_id),
55
- SqlStatement::Update(update) => bind_update_bound(update, catalog, active_branch_id),
56
- SqlStatement::Delete(delete) => bind_delete_bound(delete, catalog, active_branch_id),
57
- SqlStatement::Explain { .. } => Err(super::error::unsupported(
58
- "EXPLAIN statements are not supported by SQL write binding",
59
- )),
60
- _ => Err(super::error::unsupported(
61
- "sql2 bound statement pipeline is not wired yet",
62
- )),
63
- }
64
- }
65
-
66
- pub(super) fn bind_insert_bound(
67
- insert: &Insert,
68
- catalog: &PublicCatalog,
69
- active_branch_id: &str,
70
- ) -> Result<BoundWrite, LixError> {
71
- let mut params = ParamBinder::default();
72
- reject_unsupported_insert_clauses(insert)?;
73
- let TableObject::TableName(name) = &insert.table else {
74
- return Err(super::error::unsupported("unsupported INSERT target"));
75
- };
76
- let table = bind_public_table(catalog, name)?;
77
- require_write_capability(&table.surface, BoundWriteOp::Insert)?;
78
- if insert.columns.is_empty() {
79
- return Err(super::error::unsupported(
80
- "INSERT requires an explicit public column list",
81
- ));
82
- }
83
- let mut target_columns = BTreeSet::new();
84
- let mut columns = Vec::new();
85
- for column in &insert.columns {
86
- let column_name = normalize_identifier(column);
87
- reject_duplicate_target_column(&mut target_columns, &column_name)?;
88
- columns.push(require_writable_column(
89
- &table,
90
- &column_name,
91
- BoundWriteOp::Insert,
92
- )?);
93
- }
94
- let input = bind_insert_input(
95
- &table.surface.kind,
96
- &columns,
97
- insert.source.as_deref(),
98
- &mut params,
99
- )?;
100
- let branch_scope = bind_write_branch_scope(
101
- &table.surface.kind,
102
- &input,
103
- &BoundPredicate::True,
104
- active_branch_id,
105
- )?;
106
- Ok(BoundWrite {
107
- target: bound_write_target(&table.surface.kind),
108
- op: BoundWriteOp::Insert,
109
- input,
110
- predicate: BoundPredicate::True,
111
- assignments: Vec::new(),
112
- params: params.into_map(),
113
- branch_scope,
114
- })
115
- }
116
-
117
- pub(super) fn bind_update_bound(
118
- update: &Update,
119
- catalog: &PublicCatalog,
120
- active_branch_id: &str,
121
- ) -> Result<BoundWrite, LixError> {
122
- let mut params = ParamBinder::default();
123
- reject_unsupported_update_clauses(update)?;
124
- let table = bind_table_with_joins(catalog, &update.table)?;
125
- require_write_capability(&table.surface, BoundWriteOp::Update)?;
126
- let mut target_columns = BTreeSet::new();
127
- let mut assignments = Vec::new();
128
- for assignment in &update.assignments {
129
- let column = bind_assignment_target(&table, &assignment.target)?;
130
- reject_duplicate_target_column(&mut target_columns, &column.name)?;
131
- assignments.push(BoundAssignment {
132
- column,
133
- value: bind_expr(&table, &assignment.value, &mut params)?,
134
- });
135
- }
136
- let predicate = bind_optional_predicate(&table, update.selection.as_ref(), &mut params)?;
137
- let branch_scope = bind_write_branch_scope(
138
- &table.surface.kind,
139
- &BoundWriteInput::None,
140
- &predicate,
141
- active_branch_id,
142
- )?;
143
- Ok(BoundWrite {
144
- target: bound_write_target(&table.surface.kind),
145
- op: BoundWriteOp::Update,
146
- input: BoundWriteInput::None,
147
- predicate,
148
- assignments,
149
- params: params.into_map(),
150
- branch_scope,
151
- })
152
- }
153
-
154
- pub(super) fn bind_delete_bound(
155
- delete: &Delete,
156
- catalog: &PublicCatalog,
157
- active_branch_id: &str,
158
- ) -> Result<BoundWrite, LixError> {
159
- let mut params = ParamBinder::default();
160
- reject_unsupported_delete_clauses(delete)?;
161
- let table = bind_delete_target(catalog, &delete.from)?;
162
- require_write_capability(&table.surface, BoundWriteOp::Delete)?;
163
- let predicate = bind_optional_predicate(&table, delete.selection.as_ref(), &mut params)?;
164
- let branch_scope = bind_write_branch_scope(
165
- &table.surface.kind,
166
- &BoundWriteInput::None,
167
- &predicate,
168
- active_branch_id,
169
- )?;
170
- Ok(BoundWrite {
171
- target: bound_write_target(&table.surface.kind),
172
- op: BoundWriteOp::Delete,
173
- input: BoundWriteInput::None,
174
- predicate,
175
- assignments: Vec::new(),
176
- params: params.into_map(),
177
- branch_scope,
178
- })
179
- }
180
-
181
- fn reject_unsupported_insert_clauses(insert: &Insert) -> Result<(), LixError> {
182
- if insert.optimizer_hint.is_some() {
183
- return Err(super::error::unsupported(
184
- "INSERT optimizer hints are not supported",
185
- ));
186
- }
187
- if insert.or.is_some() {
188
- return Err(super::error::unsupported(
189
- "INSERT conflict clauses are not supported",
190
- ));
191
- }
192
- if insert.ignore {
193
- return Err(super::error::unsupported("INSERT IGNORE is not supported"));
194
- }
195
- if insert.table_alias.is_some() {
196
- return Err(super::error::unsupported(
197
- "INSERT target aliases are not supported",
198
- ));
199
- }
200
- if insert.overwrite {
201
- return Err(super::error::unsupported(
202
- "INSERT OVERWRITE is not supported",
203
- ));
204
- }
205
- if !insert.assignments.is_empty() {
206
- return Err(super::error::unsupported("INSERT ... SET is not supported"));
207
- }
208
- if insert.partitioned.is_some() || !insert.after_columns.is_empty() {
209
- return Err(super::error::unsupported(
210
- "partitioned INSERT is not supported",
211
- ));
212
- }
213
- if insert.on.is_some() {
214
- return Err(super::error::unsupported(
215
- "INSERT ON clauses are not supported",
216
- ));
217
- }
218
- if insert.returning.is_some() {
219
- return Err(super::error::unsupported(
220
- "INSERT RETURNING is not supported",
221
- ));
222
- }
223
- if insert.replace_into {
224
- return Err(super::error::unsupported("REPLACE INTO is not supported"));
225
- }
226
- if insert.priority.is_some() {
227
- return Err(super::error::unsupported(
228
- "INSERT priority clauses are not supported",
229
- ));
230
- }
231
- if insert.insert_alias.is_some() {
232
- return Err(super::error::unsupported(
233
- "INSERT row aliases are not supported",
234
- ));
235
- }
236
- if insert.settings.is_some() || insert.format_clause.is_some() {
237
- return Err(super::error::unsupported(
238
- "INSERT settings and format clauses are not supported",
239
- ));
240
- }
241
- Ok(())
242
- }
243
-
244
- fn reject_unsupported_update_clauses(update: &Update) -> Result<(), LixError> {
245
- if update.optimizer_hint.is_some() {
246
- return Err(super::error::unsupported(
247
- "UPDATE optimizer hints are not supported",
248
- ));
249
- }
250
- if update.from.is_some() {
251
- return Err(super::error::unsupported("UPDATE FROM is not supported"));
252
- }
253
- if update.returning.is_some() {
254
- return Err(super::error::unsupported(
255
- "UPDATE RETURNING is not supported",
256
- ));
257
- }
258
- if update.or.is_some() {
259
- return Err(super::error::unsupported(
260
- "UPDATE conflict clauses are not supported",
261
- ));
262
- }
263
- if update.limit.is_some() {
264
- return Err(super::error::unsupported("UPDATE LIMIT is not supported"));
265
- }
266
- Ok(())
267
- }
268
-
269
- fn reject_unsupported_delete_clauses(delete: &Delete) -> Result<(), LixError> {
270
- if delete.optimizer_hint.is_some() {
271
- return Err(super::error::unsupported(
272
- "DELETE optimizer hints are not supported",
273
- ));
274
- }
275
- if !delete.tables.is_empty() {
276
- return Err(super::error::unsupported(
277
- "multi-table DELETE is not supported",
278
- ));
279
- }
280
- if delete.using.is_some() {
281
- return Err(super::error::unsupported("DELETE USING is not supported"));
282
- }
283
- if delete.returning.is_some() {
284
- return Err(super::error::unsupported(
285
- "DELETE RETURNING is not supported",
286
- ));
287
- }
288
- if !delete.order_by.is_empty() {
289
- return Err(super::error::unsupported(
290
- "DELETE ORDER BY is not supported",
291
- ));
292
- }
293
- if delete.limit.is_some() {
294
- return Err(super::error::unsupported("DELETE LIMIT is not supported"));
295
- }
296
- Ok(())
297
- }
298
-
299
- fn bind_table_with_joins(
300
- catalog: &PublicCatalog,
301
- table: &TableWithJoins,
302
- ) -> Result<BoundTable, LixError> {
303
- if !table.joins.is_empty() {
304
- return Err(super::error::unsupported(
305
- "joined DML targets are not supported",
306
- ));
307
- }
308
- let TableFactor::Table {
309
- name,
310
- alias,
311
- args,
312
- with_hints,
313
- with_ordinality,
314
- partitions,
315
- json_path,
316
- sample,
317
- index_hints,
318
- ..
319
- } = &table.relation
320
- else {
321
- return Err(super::error::unsupported("unsupported DML target"));
322
- };
323
- if alias.is_some() {
324
- return Err(super::error::unsupported(
325
- "DML target aliases are not supported",
326
- ));
327
- }
328
- if args.is_some()
329
- || !with_hints.is_empty()
330
- || *with_ordinality
331
- || !partitions.is_empty()
332
- || json_path.is_some()
333
- || sample.is_some()
334
- || !index_hints.is_empty()
335
- {
336
- return Err(super::error::unsupported(
337
- "DML target table modifiers are not supported",
338
- ));
339
- }
340
- bind_public_table(catalog, name)
341
- }
342
-
343
- fn bind_delete_target(catalog: &PublicCatalog, from: &FromTable) -> Result<BoundTable, LixError> {
344
- let tables = match from {
345
- FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => tables,
346
- };
347
- if tables.len() != 1 {
348
- return Err(super::error::unsupported(
349
- "DELETE requires exactly one target table",
350
- ));
351
- }
352
- bind_table_with_joins(catalog, &tables[0])
353
- }
354
-
355
- fn bind_assignment_target(
356
- table: &BoundTable,
357
- target: &AssignmentTarget,
358
- ) -> Result<super::expr::BoundColumnRef, LixError> {
359
- match target {
360
- AssignmentTarget::ColumnName(name) => {
361
- let column_name = bind_exact_column_name(name)?;
362
- require_writable_column(table, &column_name, BoundWriteOp::Update)
363
- }
364
- AssignmentTarget::Tuple(_) => Err(super::error::unsupported(
365
- "tuple UPDATE assignments are not supported",
366
- )),
367
- }
368
- }
369
-
370
- fn bind_insert_input(
371
- surface_kind: &PublicSurfaceKind,
372
- columns: &[super::expr::BoundColumnRef],
373
- source: Option<&Query>,
374
- params: &mut ParamBinder,
375
- ) -> Result<BoundWriteInput, LixError> {
376
- let Some(source) = source else {
377
- return Err(super::error::unsupported("INSERT source is required"));
378
- };
379
- reject_unsupported_insert_query_clauses(source)?;
380
- let SetExpr::Values(values) = source.body.as_ref() else {
381
- if matches!(
382
- surface_kind,
383
- PublicSurfaceKind::EntityBase { .. } | PublicSurfaceKind::EntityByBranch { .. }
384
- ) {
385
- return Err(super::error::unsupported(
386
- "INSERT ... SELECT is not supported for entity SQL surfaces yet",
387
- ));
388
- }
389
- if columns
390
- .iter()
391
- .any(|column| column.table == "lix_file" && column.name == "data")
392
- {
393
- return Err(LixError::new(
394
- LixError::CODE_TYPE_MISMATCH,
395
- "lix_file.data expects binary data",
396
- )
397
- .with_hint("Use X'...' or a binary parameter for file contents."));
398
- }
399
- let statement =
400
- DataFusionStatement::Statement(Box::new(SqlStatement::Query(Box::new(source.clone()))));
401
- super::read::bind_read_statement(&source.to_string(), &statement)?;
402
- bind_query_params(source, params)?;
403
- return Ok(BoundWriteInput::Query {
404
- query: Box::new(BoundRead {
405
- query: Box::new(source.clone()),
406
- }),
407
- columns: columns.to_vec(),
408
- });
409
- };
410
- let mut rows = Vec::with_capacity(values.rows.len());
411
- for row in &values.rows {
412
- if row.len() != columns.len() {
413
- return Err(super::error::unsupported(format!(
414
- "INSERT has {} target columns but row has {} values",
415
- columns.len(),
416
- row.len()
417
- )));
418
- }
419
- rows.push(
420
- row.iter()
421
- .map(|value| bind_insert_value_expr(value, params))
422
- .collect::<Result<Vec<_>, LixError>>()?,
423
- );
424
- }
425
- Ok(BoundWriteInput::Values(BoundInsertValues {
426
- columns: columns.to_vec(),
427
- rows,
428
- }))
429
- }
430
-
431
- fn bind_query_params(query: &Query, params: &mut ParamBinder) -> Result<(), LixError> {
432
- let mut visitor = QueryParamVisitor { params };
433
- match query.visit(&mut visitor) {
434
- ControlFlow::Continue(()) => Ok(()),
435
- ControlFlow::Break(error) => Err(*error),
436
- }
437
- }
438
-
439
- struct QueryParamVisitor<'a> {
440
- params: &'a mut ParamBinder,
441
- }
442
-
443
- impl Visitor for QueryParamVisitor<'_> {
444
- type Break = Box<LixError>;
445
-
446
- fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<Self::Break> {
447
- let Expr::Value(value) = expr else {
448
- return ControlFlow::Continue(());
449
- };
450
- let Value::Placeholder(name) = &value.value else {
451
- return ControlFlow::Continue(());
452
- };
453
- match self.params.bind(name) {
454
- Ok(_) => ControlFlow::Continue(()),
455
- Err(error) => ControlFlow::Break(Box::new(error)),
456
- }
457
- }
458
- }
459
-
460
- fn bind_insert_value_expr(expr: &Expr, params: &mut ParamBinder) -> Result<BoundExpr, LixError> {
461
- match expr {
462
- Expr::Value(value) => bind_value(&value.value, params),
463
- Expr::Nested(expr) => bind_insert_value_expr(expr, params),
464
- Expr::UnaryOp {
465
- op: UnaryOperator::Minus,
466
- expr,
467
- } => bind_negative_number_expr(expr),
468
- Expr::Function(function) => bind_insert_value_function(function, params),
469
- _ => Err(super::error::unsupported(format!(
470
- "unsupported INSERT VALUES expression '{expr}'"
471
- ))),
472
- }
473
- }
474
-
475
- fn reject_unsupported_insert_query_clauses(source: &Query) -> Result<(), LixError> {
476
- if source.with.is_some()
477
- || source.order_by.is_some()
478
- || source.limit_clause.is_some()
479
- || source.fetch.is_some()
480
- || !source.locks.is_empty()
481
- || source.for_clause.is_some()
482
- || source.settings.is_some()
483
- || source.format_clause.is_some()
484
- || !source.pipe_operators.is_empty()
485
- {
486
- return Err(super::error::unsupported(
487
- "INSERT VALUES query clauses are not supported",
488
- ));
489
- }
490
- Ok(())
491
- }
492
-
493
- fn bind_optional_predicate(
494
- table: &BoundTable,
495
- expr: Option<&Expr>,
496
- params: &mut ParamBinder,
497
- ) -> Result<BoundPredicate, LixError> {
498
- match expr {
499
- Some(expr) => bind_predicate(table, expr, params),
500
- None => Ok(BoundPredicate::True),
501
- }
502
- }
503
-
504
- fn bind_predicate(
505
- table: &BoundTable,
506
- expr: &Expr,
507
- params: &mut ParamBinder,
508
- ) -> Result<BoundPredicate, LixError> {
509
- match expr {
510
- Expr::BinaryOp { left, op, right } if *op == BinaryOperator::And => {
511
- let mut predicates = Vec::new();
512
- flatten_and_predicate(table, left, params, &mut predicates)?;
513
- flatten_and_predicate(table, right, params, &mut predicates)?;
514
- Ok(BoundPredicate::And(predicates))
515
- }
516
- Expr::BinaryOp { left, op, right } if *op == BinaryOperator::Or => {
517
- let mut predicates = Vec::new();
518
- flatten_or_predicate(table, left, params, &mut predicates)?;
519
- flatten_or_predicate(table, right, params, &mut predicates)?;
520
- Ok(BoundPredicate::Or(predicates))
521
- }
522
- Expr::BinaryOp { left, op, right } if *op == BinaryOperator::Eq => Ok(BoundPredicate::Eq(
523
- bind_expr(table, left, params)?,
524
- bind_expr(table, right, params)?,
525
- )),
526
- Expr::IsNull(expr) => Ok(BoundPredicate::IsNull(bind_expr(table, expr, params)?)),
527
- Expr::IsNotNull(expr) => Ok(BoundPredicate::IsNotNull(bind_expr(table, expr, params)?)),
528
- Expr::InList {
529
- expr,
530
- list,
531
- negated,
532
- } => {
533
- if *negated {
534
- return Err(super::error::unsupported(
535
- "NOT IN predicates are not supported",
536
- ));
537
- }
538
- Ok(BoundPredicate::In {
539
- expr: bind_expr(table, expr, params)?,
540
- values: list
541
- .iter()
542
- .map(|value| bind_expr(table, value, params))
543
- .collect::<Result<Vec<_>, _>>()?,
544
- })
545
- }
546
- Expr::Value(value) if value.value == Value::Boolean(true) => Ok(BoundPredicate::True),
547
- Expr::Value(value) if value.value == Value::Boolean(false) => Ok(BoundPredicate::False),
548
- _ => Err(super::error::unsupported(format!(
549
- "unsupported SQL predicate '{expr}'"
550
- ))),
551
- }
552
- }
553
-
554
- fn flatten_and_predicate(
555
- table: &BoundTable,
556
- expr: &Expr,
557
- params: &mut ParamBinder,
558
- predicates: &mut Vec<BoundPredicate>,
559
- ) -> Result<(), LixError> {
560
- match bind_predicate(table, expr, params)? {
561
- BoundPredicate::And(items) => predicates.extend(items),
562
- predicate => predicates.push(predicate),
563
- }
564
- Ok(())
565
- }
566
-
567
- fn flatten_or_predicate(
568
- table: &BoundTable,
569
- expr: &Expr,
570
- params: &mut ParamBinder,
571
- predicates: &mut Vec<BoundPredicate>,
572
- ) -> Result<(), LixError> {
573
- match bind_predicate(table, expr, params)? {
574
- BoundPredicate::Or(items) => predicates.extend(items),
575
- predicate => predicates.push(predicate),
576
- }
577
- Ok(())
578
- }
579
-
580
- fn bind_expr(
581
- table: &BoundTable,
582
- expr: &Expr,
583
- params: &mut ParamBinder,
584
- ) -> Result<BoundExpr, LixError> {
585
- match expr {
586
- Expr::Identifier(ident) => {
587
- let column_name = normalize_identifier(ident);
588
- Ok(BoundExpr::Column(bind_public_column_ref(
589
- table,
590
- &column_name,
591
- )?))
592
- }
593
- Expr::CompoundIdentifier(idents) if idents.len() == 2 => {
594
- let table_name = normalize_identifier(&idents[0]);
595
- if table_name != table.name {
596
- return Err(super::error::unsupported(format!(
597
- "unknown SQL table qualifier '{table_name}'"
598
- )));
599
- }
600
- let column_name = normalize_identifier(&idents[1]);
601
- Ok(BoundExpr::Column(bind_public_column_ref(
602
- table,
603
- &column_name,
604
- )?))
605
- }
606
- Expr::Value(value) => bind_value(&value.value, params),
607
- Expr::Nested(expr) => bind_expr(table, expr, params),
608
- Expr::UnaryOp {
609
- op: UnaryOperator::Minus,
610
- expr,
611
- } => bind_negative_number_expr(expr),
612
- Expr::Function(function) => bind_function_expr(table, function, params),
613
- _ => Err(super::error::unsupported(format!(
614
- "unsupported SQL expression '{expr}'"
615
- ))),
616
- }
617
- }
618
-
619
- fn bind_value(value: &Value, params: &mut ParamBinder) -> Result<BoundExpr, LixError> {
620
- match value {
621
- Value::Null => Ok(BoundExpr::Literal(BoundLiteral::Null)),
622
- Value::Boolean(value) => Ok(BoundExpr::Literal(BoundLiteral::Bool(*value))),
623
- Value::SingleQuotedString(value) | Value::DoubleQuotedString(value) => {
624
- Ok(BoundExpr::Literal(BoundLiteral::Text(value.clone())))
625
- }
626
- Value::HexStringLiteral(value) => decode_hex_literal(value),
627
- Value::Number(value, _) => bind_number_literal(value),
628
- Value::Placeholder(name) => Ok(BoundExpr::Param(params.bind(name)?)),
629
- _ => Err(super::error::unsupported(format!(
630
- "unsupported SQL literal '{value}'"
631
- ))),
632
- }
633
- }
634
-
635
- fn bind_number_literal(value: &str) -> Result<BoundExpr, LixError> {
636
- if let Ok(value) = value.parse::<i64>() {
637
- return Ok(BoundExpr::Literal(BoundLiteral::Integer(value)));
638
- }
639
- let value = value
640
- .parse::<f64>()
641
- .map_err(|_| super::error::unsupported(format!("unsupported numeric literal '{value}'")))?;
642
- let Some(number) = serde_json::Number::from_f64(value) else {
643
- return Err(super::error::unsupported(
644
- "unsupported non-finite numeric literal",
645
- ));
646
- };
647
- Ok(BoundExpr::Literal(BoundLiteral::Json(JsonValue::Number(
648
- number,
649
- ))))
650
- }
651
-
652
- fn bind_negative_number_expr(expr: &Expr) -> Result<BoundExpr, LixError> {
653
- let Expr::Value(value) = expr else {
654
- return Err(super::error::unsupported(format!(
655
- "unsupported negative SQL expression '-{expr}'"
656
- )));
657
- };
658
- let Value::Number(value, _) = &value.value else {
659
- return Err(super::error::unsupported(format!(
660
- "unsupported negative SQL literal '-{}'",
661
- value.value
662
- )));
663
- };
664
- bind_number_literal(&format!("-{value}"))
665
- }
666
-
667
- fn decode_hex_literal(value: &str) -> Result<BoundExpr, LixError> {
668
- if value.len() % 2 != 0 {
669
- return Err(super::error::unsupported(format!(
670
- "hex literal has odd length '{value}'"
671
- )));
672
- }
673
- let bytes = value
674
- .as_bytes()
675
- .chunks_exact(2)
676
- .map(|chunk| {
677
- let high = hex_digit(chunk[0])?;
678
- let low = hex_digit(chunk[1])?;
679
- Ok((high << 4) | low)
680
- })
681
- .collect::<Result<Vec<_>, LixError>>()?;
682
- Ok(BoundExpr::Literal(BoundLiteral::Blob(bytes)))
683
- }
684
-
685
- fn bind_insert_value_function(
686
- function: &Function,
687
- params: &mut ParamBinder,
688
- ) -> Result<BoundExpr, LixError> {
689
- if let Some(value) = bind_insert_lix_json_literal(function)? {
690
- return Ok(value);
691
- }
692
- bind_function(function, params, |expr, params| {
693
- bind_insert_value_expr(expr, params)
694
- })
695
- }
696
-
697
- fn bind_insert_lix_json_literal(function: &Function) -> Result<Option<BoundExpr>, LixError> {
698
- reject_unsupported_function_modifiers(function)?;
699
- let name = bind_lix_function_name(function)?;
700
- if name != "lix_json" {
701
- return Ok(None);
702
- }
703
- let raw_args = function_args(&function.args)?;
704
- validate_bound_function_arity(&name, raw_args.len())?;
705
- let Expr::Value(value) = raw_args[0] else {
706
- return Ok(None);
707
- };
708
- let raw = match &value.value {
709
- Value::SingleQuotedString(value) | Value::DoubleQuotedString(value) => value,
710
- _ => return Ok(None),
711
- };
712
- let value = serde_json::from_str(raw).map_err(|error| {
713
- LixError::new(
714
- LixError::CODE_TYPE_MISMATCH,
715
- format!("lix_json argument is not valid JSON: {error}"),
716
- )
717
- })?;
718
- Ok(Some(BoundExpr::Literal(BoundLiteral::Json(value))))
719
- }
720
-
721
- fn bind_function_expr(
722
- table: &BoundTable,
723
- function: &Function,
724
- params: &mut ParamBinder,
725
- ) -> Result<BoundExpr, LixError> {
726
- bind_function(function, params, |expr, params| {
727
- bind_expr(table, expr, params)
728
- })
729
- }
730
-
731
- fn bind_function(
732
- function: &Function,
733
- params: &mut ParamBinder,
734
- mut bind_arg_expr: impl FnMut(&Expr, &mut ParamBinder) -> Result<BoundExpr, LixError>,
735
- ) -> Result<BoundExpr, LixError> {
736
- reject_unsupported_function_modifiers(function)?;
737
- let name = bind_lix_function_name(function)?;
738
- let raw_args = function_args(&function.args)?;
739
- validate_text_encoding_literal(&name, &raw_args)?;
740
- let args = raw_args
741
- .iter()
742
- .map(|arg| bind_arg_expr(arg, params))
743
- .collect::<Result<Vec<_>, _>>()?;
744
- validate_bound_function_arity(&name, args.len())?;
745
- Ok(BoundExpr::Function { name, args })
746
- }
747
-
748
- fn reject_unsupported_function_modifiers(function: &Function) -> Result<(), LixError> {
749
- if function.uses_odbc_syntax
750
- || !matches!(function.parameters, FunctionArguments::None)
751
- || function.filter.is_some()
752
- || function.null_treatment.is_some()
753
- || function.over.is_some()
754
- || !function.within_group.is_empty()
755
- {
756
- return Err(super::error::unsupported(
757
- "SQL function modifiers are not supported by bound writes",
758
- ));
759
- }
760
- if let FunctionArguments::List(list) = &function.args {
761
- if list.duplicate_treatment.is_some() || !list.clauses.is_empty() {
762
- return Err(super::error::unsupported(
763
- "SQL function argument modifiers are not supported by bound writes",
764
- ));
765
- }
766
- }
767
- Ok(())
768
- }
769
-
770
- fn validate_bound_function_arity(name: &str, actual: usize) -> Result<(), LixError> {
771
- match name {
772
- "lix_json" => expect_exact_function_arity(name, actual, 1),
773
- "lix_empty_blob" => expect_exact_function_arity(name, actual, 0),
774
- "lix_timestamp" => expect_exact_function_arity(name, actual, 0),
775
- "lix_uuid_v7" => expect_exact_function_arity(name, actual, 0),
776
- "lix_active_branch_commit_id" => expect_exact_function_arity(name, actual, 0),
777
- "lix_json_get" | "lix_json_get_text" => expect_min_function_arity(name, actual, 2),
778
- "lix_text_encode" | "lix_text_decode" => {
779
- if (1..=2).contains(&actual) {
780
- Ok(())
781
- } else {
782
- Err(super::error::unsupported(format!(
783
- "{name} requires 1 or 2 arguments"
784
- )))
785
- }
786
- }
787
- _ => Err(super::error::unsupported(format!(
788
- "unsupported SQL function '{name}'"
789
- ))),
790
- }
791
- }
792
-
793
- fn validate_text_encoding_literal(name: &str, args: &[&Expr]) -> Result<(), LixError> {
794
- if !matches!(name, "lix_text_encode" | "lix_text_decode") || args.len() < 2 {
795
- return Ok(());
796
- }
797
- let Expr::Value(value) = args[1] else {
798
- return Ok(());
799
- };
800
- let Some(encoding) = string_literal_value(&value.value) else {
801
- return Ok(());
802
- };
803
- let normalized = encoding.trim().to_ascii_uppercase().replace('-', "");
804
- if normalized == "UTF8" {
805
- Ok(())
806
- } else {
807
- Err(super::error::unsupported(format!(
808
- "{name} only supports UTF8 encoding, got '{encoding}'"
809
- )))
810
- }
811
- }
812
-
813
- fn expect_exact_function_arity(name: &str, actual: usize, expected: usize) -> Result<(), LixError> {
814
- if actual != expected {
815
- return Err(super::error::unsupported(format!(
816
- "{name} requires exactly {expected} argument"
817
- )));
818
- }
819
- Ok(())
820
- }
821
-
822
- fn expect_min_function_arity(name: &str, actual: usize, minimum: usize) -> Result<(), LixError> {
823
- if actual < minimum {
824
- return Err(super::error::unsupported(format!(
825
- "{name} requires at least {minimum} arguments"
826
- )));
827
- }
828
- Ok(())
829
- }
830
-
831
- fn string_literal_value(value: &Value) -> Option<&str> {
832
- match value {
833
- Value::SingleQuotedString(value)
834
- | Value::DoubleQuotedString(value)
835
- | Value::TripleSingleQuotedString(value)
836
- | Value::TripleDoubleQuotedString(value)
837
- | Value::EscapedStringLiteral(value)
838
- | Value::UnicodeStringLiteral(value)
839
- | Value::NationalStringLiteral(value)
840
- | Value::SingleQuotedRawStringLiteral(value)
841
- | Value::DoubleQuotedRawStringLiteral(value)
842
- | Value::TripleSingleQuotedRawStringLiteral(value)
843
- | Value::TripleDoubleQuotedRawStringLiteral(value) => Some(value.as_str()),
844
- Value::DollarQuotedString(value) => Some(value.value.as_str()),
845
- _ => None,
846
- }
847
- }
848
-
849
- fn bind_lix_function_name(function: &Function) -> Result<String, LixError> {
850
- if function.name.0.len() != 1 {
851
- return Err(super::error::unsupported(
852
- "qualified SQL function names are not supported by bound writes",
853
- ));
854
- }
855
- let Some(ObjectNamePart::Identifier(ident)) = function.name.0.first() else {
856
- return Err(super::error::unsupported(
857
- "unsupported SQL function name in bound write",
858
- ));
859
- };
860
- let name = if ident.quote_style.is_some() {
861
- ident.value.clone()
862
- } else {
863
- ident.value.to_ascii_lowercase()
864
- };
865
- match name.as_str() {
866
- "lix_json"
867
- | "lix_json_get"
868
- | "lix_json_get_text"
869
- | "lix_empty_blob"
870
- | "lix_timestamp"
871
- | "lix_uuid_v7"
872
- | "lix_active_branch_commit_id"
873
- | "lix_text_encode"
874
- | "lix_text_decode" => Ok(name),
875
- _ => Err(super::error::unsupported(format!(
876
- "unsupported SQL function '{name}'"
877
- ))),
878
- }
879
- }
880
-
881
- fn function_args(args: &FunctionArguments) -> Result<Vec<&Expr>, LixError> {
882
- let FunctionArguments::List(list) = args else {
883
- return Err(super::error::unsupported(
884
- "only ordinary SQL function argument lists are supported",
885
- ));
886
- };
887
- list.args
888
- .iter()
889
- .map(|arg| match arg {
890
- FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => Ok(expr),
891
- _ => Err(super::error::unsupported(
892
- "named, wildcard, and qualified function arguments are not supported",
893
- )),
894
- })
895
- .collect()
896
- }
897
-
898
- fn hex_digit(byte: u8) -> Result<u8, LixError> {
899
- match byte {
900
- b'0'..=b'9' => Ok(byte - b'0'),
901
- b'a'..=b'f' => Ok(byte - b'a' + 10),
902
- b'A'..=b'F' => Ok(byte - b'A' + 10),
903
- _ => Err(super::error::unsupported(format!(
904
- "invalid hex literal digit '{}'",
905
- byte as char
906
- ))),
907
- }
908
- }
909
-
910
- fn bind_exact_column_name(name: &ObjectName) -> Result<String, LixError> {
911
- if name.0.len() != 1 {
912
- return Err(super::error::unsupported(
913
- "qualified SQL column names are not supported",
914
- ));
915
- }
916
- name.0
917
- .first()
918
- .and_then(|part| part.as_ident())
919
- .map(normalize_identifier)
920
- .ok_or_else(|| super::error::unsupported("unsupported SQL column name"))
921
- }
922
-
923
- fn normalize_identifier(ident: &datafusion::sql::sqlparser::ast::Ident) -> String {
924
- if ident.quote_style.is_some() {
925
- ident.value.clone()
926
- } else {
927
- ident.value.to_ascii_lowercase()
928
- }
929
- }
930
-
931
- fn reject_duplicate_target_column(
932
- target_columns: &mut BTreeSet<String>,
933
- column_name: &str,
934
- ) -> Result<(), LixError> {
935
- if target_columns.insert(column_name.to_string()) {
936
- Ok(())
937
- } else {
938
- Err(LixError::new(
939
- LixError::CODE_INVALID_PARAM,
940
- format!("duplicate write target column '{column_name}'"),
941
- ))
942
- }
943
- }
944
-
945
- fn require_write_capability(
946
- surface: &PublicSurfaceContract,
947
- op: BoundWriteOp,
948
- ) -> Result<(), LixError> {
949
- let allowed = match op {
950
- BoundWriteOp::Insert => surface.capabilities.insert,
951
- BoundWriteOp::Update => surface.capabilities.update,
952
- BoundWriteOp::Delete => surface.capabilities.delete,
953
- };
954
- if allowed {
955
- Ok(())
956
- } else {
957
- let mut error = LixError::new(
958
- LixError::CODE_READ_ONLY,
959
- format!("DML cannot write read-only SQL table '{}'", surface.name),
960
- );
961
- if matches!(
962
- surface.kind,
963
- PublicSurfaceKind::EntityHistory { .. }
964
- | PublicSurfaceKind::FileHistory
965
- | PublicSurfaceKind::DirectoryHistory
966
- | PublicSurfaceKind::History
967
- ) {
968
- error = error.with_hint("History views are query-only.");
969
- }
970
- Err(error)
971
- }
972
- }
973
-
974
- fn bound_write_target(kind: &PublicSurfaceKind) -> BoundWriteTarget {
975
- match kind {
976
- PublicSurfaceKind::LixState => BoundWriteTarget::LixState,
977
- PublicSurfaceKind::LixStateByBranch => BoundWriteTarget::LixStateByBranch,
978
- PublicSurfaceKind::EntityBase { schema_key } => {
979
- BoundWriteTarget::Entity(EntityWriteSurface::Base {
980
- schema_key: schema_key.clone(),
981
- })
982
- }
983
- PublicSurfaceKind::EntityByBranch { schema_key } => {
984
- BoundWriteTarget::Entity(EntityWriteSurface::ByBranch {
985
- schema_key: schema_key.clone(),
986
- })
987
- }
988
- PublicSurfaceKind::File => BoundWriteTarget::File(FileWriteSurface::Base),
989
- PublicSurfaceKind::FileByBranch => BoundWriteTarget::File(FileWriteSurface::ByBranch),
990
- PublicSurfaceKind::Directory => BoundWriteTarget::Directory(DirectoryWriteSurface::Base),
991
- PublicSurfaceKind::DirectoryByBranch => {
992
- BoundWriteTarget::Directory(DirectoryWriteSurface::ByBranch)
993
- }
994
- PublicSurfaceKind::Branch => BoundWriteTarget::Branch,
995
- PublicSurfaceKind::EntityHistory { .. }
996
- | PublicSurfaceKind::FileHistory
997
- | PublicSurfaceKind::DirectoryHistory
998
- | PublicSurfaceKind::Change
999
- | PublicSurfaceKind::History => {
1000
- unreachable!("write capability checked before target binding")
1001
- }
1002
- }
1003
- }
1004
-
1005
- fn bind_write_branch_scope(
1006
- kind: &PublicSurfaceKind,
1007
- input: &BoundWriteInput,
1008
- predicate: &BoundPredicate,
1009
- active_branch_id: &str,
1010
- ) -> Result<BranchScope, LixError> {
1011
- let Some(branch_column) = by_branch_column_name(kind) else {
1012
- return bind_base_write_branch_scope(kind, input, predicate, active_branch_id);
1013
- };
1014
- let branch_selector = match input {
1015
- BoundWriteInput::Values(values) => {
1016
- let mut selector = BranchSelector::Missing;
1017
- if let Some(column_index) = values.column_index(branch_column) {
1018
- for row in &values.rows {
1019
- let value = &row[column_index];
1020
- selector = selector.union(value_branch_selector(value)?);
1021
- }
1022
- }
1023
- selector
1024
- }
1025
- BoundWriteInput::None => predicate_branch_selector(predicate, branch_column)?,
1026
- BoundWriteInput::Query { .. } => Err(super::error::unsupported(
1027
- "INSERT ... SELECT by-branch writes are not supported",
1028
- ))?,
1029
- };
1030
- if matches!(kind, PublicSurfaceKind::LixStateByBranch) {
1031
- let global_selector = match input {
1032
- BoundWriteInput::Values(values) => {
1033
- let mut selector = GlobalSelector::Missing;
1034
- let global_column_index = values.column_index("global");
1035
- for row in &values.rows {
1036
- selector =
1037
- selector.union(insert_row_global_selector(global_column_index, row)?);
1038
- }
1039
- selector
1040
- }
1041
- BoundWriteInput::None => predicate_global_selector(predicate)?,
1042
- BoundWriteInput::Query { .. } => GlobalSelector::Missing,
1043
- };
1044
- return lix_state_by_branch_scope(input, branch_column, branch_selector, global_selector);
1045
- }
1046
- by_branch_scope(input, branch_column, branch_selector)
1047
- }
1048
-
1049
- fn by_branch_scope(
1050
- input: &BoundWriteInput,
1051
- branch_column: &str,
1052
- selector: BranchSelector,
1053
- ) -> Result<BranchScope, LixError> {
1054
- match (input, selector) {
1055
- (_, selector) if selector.is_empty() => Ok(BranchScope::Empty),
1056
- (BoundWriteInput::Values(_), BranchSelector::Missing) => Err(super::error::unsupported(
1057
- format!("INSERT into by-branch SQL table requires explicit '{branch_column}'"),
1058
- )),
1059
- (BoundWriteInput::Values(_), BranchSelector::Static(branch_ids)) => {
1060
- Ok(BranchScope::Explicit { branch_ids })
1061
- }
1062
- (
1063
- BoundWriteInput::Values(_),
1064
- BranchSelector::Dynamic {
1065
- branch_ids,
1066
- param_indexes,
1067
- },
1068
- ) => Ok(BranchScope::ExplicitDynamic {
1069
- branch_ids,
1070
- param_indexes,
1071
- }),
1072
- (BoundWriteInput::None, BranchSelector::Missing) => Err(super::error::unsupported(
1073
- format!("by-branch SQL writes require an explicit '{branch_column}' predicate"),
1074
- )),
1075
- (BoundWriteInput::None, BranchSelector::Static(branch_ids)) => {
1076
- Ok(BranchScope::ExplicitRequired { branch_ids })
1077
- }
1078
- (
1079
- BoundWriteInput::None,
1080
- BranchSelector::Dynamic {
1081
- branch_ids,
1082
- param_indexes,
1083
- },
1084
- ) => Ok(BranchScope::ExplicitRequiredDynamic {
1085
- branch_ids,
1086
- param_indexes,
1087
- }),
1088
- (BoundWriteInput::Query { .. }, _) => Err(super::error::unsupported(
1089
- "INSERT ... SELECT by-branch writes are not supported",
1090
- )),
1091
- }
1092
- }
1093
-
1094
- fn lix_state_by_branch_scope(
1095
- input: &BoundWriteInput,
1096
- branch_column: &str,
1097
- branch_selector: BranchSelector,
1098
- global_selector: GlobalSelector,
1099
- ) -> Result<BranchScope, LixError> {
1100
- if matches!(global_selector, GlobalSelector::Empty) || branch_selector.is_empty() {
1101
- return Ok(BranchScope::Empty);
1102
- }
1103
-
1104
- match global_selector {
1105
- GlobalSelector::Static(true) => match branch_selector {
1106
- BranchSelector::Missing => Ok(BranchScope::Global),
1107
- BranchSelector::Static(branch_ids)
1108
- if branch_ids == BTreeSet::from([GLOBAL_BRANCH_ID.to_string()]) =>
1109
- {
1110
- Ok(BranchScope::Global)
1111
- }
1112
- BranchSelector::Static(_) => Err(super::error::unsupported(
1113
- "lix_state_by_branch writes cannot combine global = true with non-global branch_id",
1114
- )),
1115
- BranchSelector::Dynamic { .. } => by_branch_scope(input, branch_column, branch_selector),
1116
- },
1117
- GlobalSelector::Static(false) => match &branch_selector {
1118
- BranchSelector::Static(branch_ids) if branch_ids.contains(GLOBAL_BRANCH_ID) => {
1119
- Err(super::error::unsupported(
1120
- "lix_state_by_branch writes cannot combine global = false with global branch_id",
1121
- ))
1122
- }
1123
- _ => by_branch_scope(input, branch_column, branch_selector),
1124
- },
1125
- GlobalSelector::Dynamic => by_branch_scope(input, branch_column, branch_selector),
1126
- GlobalSelector::Missing => match &branch_selector {
1127
- BranchSelector::Static(branch_ids)
1128
- if branch_ids == &BTreeSet::from([GLOBAL_BRANCH_ID.to_string()]) =>
1129
- {
1130
- Ok(BranchScope::Global)
1131
- }
1132
- BranchSelector::Static(branch_ids) if branch_ids.contains(GLOBAL_BRANCH_ID) => {
1133
- Err(super::error::unsupported(
1134
- "lix_state_by_branch writes cannot mix global and non-global branch scopes",
1135
- ))
1136
- }
1137
- _ => by_branch_scope(input, branch_column, branch_selector),
1138
- },
1139
- GlobalSelector::Mixed => Err(super::error::unsupported(
1140
- "lix_state_by_branch writes cannot mix global and branch-specific rows",
1141
- )),
1142
- GlobalSelector::Empty => Ok(BranchScope::Empty),
1143
- }
1144
- }
1145
-
1146
- fn bind_base_write_branch_scope(
1147
- kind: &PublicSurfaceKind,
1148
- input: &BoundWriteInput,
1149
- predicate: &BoundPredicate,
1150
- active_branch_id: &str,
1151
- ) -> Result<BranchScope, LixError> {
1152
- if predicate == &BoundPredicate::False {
1153
- return Ok(BranchScope::Empty);
1154
- }
1155
- if matches!(kind, PublicSurfaceKind::Branch) {
1156
- return Ok(BranchScope::Global);
1157
- }
1158
- if !matches!(kind, PublicSurfaceKind::LixState) {
1159
- return Ok(active_branch_scope(active_branch_id));
1160
- }
1161
- match input {
1162
- BoundWriteInput::Values(values) => {
1163
- let mut selector = GlobalSelector::Missing;
1164
- let global_column_index = values.column_index("global");
1165
- for row in &values.rows {
1166
- selector = selector.union(insert_row_global_selector(global_column_index, row)?);
1167
- }
1168
- match selector {
1169
- GlobalSelector::Missing | GlobalSelector::Static(false) => {
1170
- Ok(active_branch_scope(active_branch_id))
1171
- }
1172
- GlobalSelector::Static(true) => Ok(BranchScope::Global),
1173
- GlobalSelector::Empty => Ok(BranchScope::Empty),
1174
- GlobalSelector::Dynamic => Err(super::error::unsupported(
1175
- "parameterized lix_state global scope selectors are not supported yet",
1176
- )),
1177
- GlobalSelector::Mixed => Err(super::error::unsupported(
1178
- "lix_state INSERT cannot mix global and active-branch rows",
1179
- )),
1180
- }
1181
- }
1182
- BoundWriteInput::None => match predicate_global_selector(predicate)? {
1183
- GlobalSelector::Static(true) => Ok(BranchScope::Global),
1184
- GlobalSelector::Static(false) | GlobalSelector::Missing => {
1185
- Ok(active_branch_scope(active_branch_id))
1186
- }
1187
- GlobalSelector::Empty => Ok(BranchScope::Empty),
1188
- GlobalSelector::Dynamic => Err(super::error::unsupported(
1189
- "parameterized lix_state global scope selectors are not supported yet",
1190
- )),
1191
- GlobalSelector::Mixed => Err(super::error::unsupported(
1192
- "lix_state global predicates select mixed branch scopes",
1193
- )),
1194
- },
1195
- BoundWriteInput::Query { .. } => Ok(active_branch_scope(active_branch_id)),
1196
- }
1197
- }
1198
-
1199
- #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1200
- enum GlobalSelector {
1201
- Missing,
1202
- Static(bool),
1203
- Dynamic,
1204
- Mixed,
1205
- Empty,
1206
- }
1207
-
1208
- impl GlobalSelector {
1209
- fn union(self, other: Self) -> Self {
1210
- match (self, other) {
1211
- (Self::Mixed, _) | (_, Self::Mixed) => Self::Mixed,
1212
- (Self::Dynamic, _) | (_, Self::Dynamic) => Self::Dynamic,
1213
- (Self::Empty, selector) | (selector, Self::Empty) => selector,
1214
- (Self::Missing, selector) | (selector, Self::Missing) => selector,
1215
- (Self::Static(left), Self::Static(right)) if left == right => Self::Static(left),
1216
- (Self::Static(_), Self::Static(_)) => Self::Mixed,
1217
- }
1218
- }
1219
-
1220
- fn intersect(self, other: Self) -> Self {
1221
- match (self, other) {
1222
- (Self::Empty, _) | (_, Self::Empty) => Self::Empty,
1223
- (Self::Dynamic, Self::Missing) | (Self::Missing, Self::Dynamic) => Self::Dynamic,
1224
- (Self::Dynamic, selector) | (selector, Self::Dynamic) => selector,
1225
- (Self::Mixed, Self::Missing) | (Self::Missing, Self::Mixed) => Self::Mixed,
1226
- (Self::Mixed, selector) | (selector, Self::Mixed) => selector,
1227
- (Self::Missing, selector) | (selector, Self::Missing) => selector,
1228
- (Self::Static(left), Self::Static(right)) if left == right => Self::Static(left),
1229
- (Self::Static(_), Self::Static(_)) => Self::Empty,
1230
- }
1231
- }
1232
- }
1233
-
1234
- fn insert_row_global_selector(
1235
- column_index: Option<usize>,
1236
- row: &[BoundExpr],
1237
- ) -> Result<GlobalSelector, LixError> {
1238
- let Some(column_index) = column_index else {
1239
- return Ok(GlobalSelector::Missing);
1240
- };
1241
- global_selector_value(&row[column_index])
1242
- }
1243
-
1244
- fn predicate_global_selector(predicate: &BoundPredicate) -> Result<GlobalSelector, LixError> {
1245
- match predicate {
1246
- BoundPredicate::True => Ok(GlobalSelector::Missing),
1247
- BoundPredicate::False => Ok(GlobalSelector::Empty),
1248
- BoundPredicate::And(predicates) => {
1249
- let mut result = GlobalSelector::Missing;
1250
- for predicate in predicates {
1251
- result = result.intersect(predicate_global_selector(predicate)?);
1252
- }
1253
- Ok(result)
1254
- }
1255
- BoundPredicate::Or(predicates) => {
1256
- let mut result = GlobalSelector::Empty;
1257
- let mut has_missing_branch = false;
1258
- for predicate in predicates {
1259
- let selector = predicate_global_selector(predicate)?;
1260
- if selector == GlobalSelector::Missing {
1261
- has_missing_branch = true;
1262
- continue;
1263
- }
1264
- result = result.union(selector);
1265
- }
1266
- if has_missing_branch {
1267
- if result == GlobalSelector::Empty {
1268
- Ok(GlobalSelector::Missing)
1269
- } else {
1270
- Ok(GlobalSelector::Mixed)
1271
- }
1272
- } else {
1273
- Ok(result)
1274
- }
1275
- }
1276
- BoundPredicate::Eq(left, right) => global_value_from_binary_exprs(left, right)
1277
- .or_else(|| global_value_from_binary_exprs(right, left))
1278
- .transpose()
1279
- .map(|selector| selector.unwrap_or(GlobalSelector::Missing)),
1280
- BoundPredicate::IsNull(_) | BoundPredicate::IsNotNull(_) => Ok(GlobalSelector::Missing),
1281
- BoundPredicate::In { expr, values } => {
1282
- let BoundExpr::Column(column) = expr else {
1283
- return Ok(GlobalSelector::Missing);
1284
- };
1285
- if column.name != "global" {
1286
- return Ok(GlobalSelector::Missing);
1287
- }
1288
- let mut result = GlobalSelector::Missing;
1289
- for value in values {
1290
- result = result.union(global_selector_value(value)?);
1291
- }
1292
- Ok(result)
1293
- }
1294
- }
1295
- }
1296
-
1297
- fn global_value_from_binary_exprs(
1298
- column_expr: &BoundExpr,
1299
- value_expr: &BoundExpr,
1300
- ) -> Option<Result<GlobalSelector, LixError>> {
1301
- let BoundExpr::Column(column) = column_expr else {
1302
- return None;
1303
- };
1304
- if column.name != "global" {
1305
- return None;
1306
- }
1307
- Some(global_selector_value(value_expr))
1308
- }
1309
-
1310
- fn global_selector_value(expr: &BoundExpr) -> Result<GlobalSelector, LixError> {
1311
- match expr {
1312
- BoundExpr::Literal(BoundLiteral::Bool(value)) => Ok(GlobalSelector::Static(*value)),
1313
- BoundExpr::Param(_) => Ok(GlobalSelector::Dynamic),
1314
- _ => Err(super::error::unsupported(
1315
- "lix_state global predicates require boolean literals",
1316
- )),
1317
- }
1318
- }
1319
-
1320
- fn by_branch_column_name(kind: &PublicSurfaceKind) -> Option<&'static str> {
1321
- match kind {
1322
- PublicSurfaceKind::LixStateByBranch => Some("branch_id"),
1323
- PublicSurfaceKind::EntityByBranch { .. }
1324
- | PublicSurfaceKind::FileByBranch
1325
- | PublicSurfaceKind::DirectoryByBranch => Some("lixcol_branch_id"),
1326
- _ => None,
1327
- }
1328
- }
1329
-
1330
- #[derive(Clone, Debug, Eq, PartialEq)]
1331
- enum BranchSelector {
1332
- Missing,
1333
- Static(BTreeSet<String>),
1334
- Dynamic {
1335
- branch_ids: BTreeSet<String>,
1336
- param_indexes: BTreeSet<usize>,
1337
- },
1338
- }
1339
-
1340
- impl BranchSelector {
1341
- fn is_empty(&self) -> bool {
1342
- matches!(self, Self::Static(branch_ids) if branch_ids.is_empty())
1343
- }
1344
-
1345
- fn intersect(self, other: Self) -> Self {
1346
- match (self, other) {
1347
- (Self::Missing, selector) | (selector, Self::Missing) => selector,
1348
- (Self::Static(branch_ids), Self::Dynamic { param_indexes, .. })
1349
- | (Self::Dynamic { param_indexes, .. }, Self::Static(branch_ids))
1350
- if branch_ids.is_empty() || param_indexes.is_empty() =>
1351
- {
1352
- Self::Static(BTreeSet::new())
1353
- }
1354
- (Self::Static(left), Self::Static(right)) => {
1355
- Self::Static(left.intersection(&right).cloned().collect())
1356
- }
1357
- (
1358
- Self::Dynamic {
1359
- mut branch_ids,
1360
- mut param_indexes,
1361
- },
1362
- Self::Dynamic {
1363
- branch_ids: right_branches,
1364
- param_indexes: right_params,
1365
- },
1366
- ) => {
1367
- branch_ids.extend(right_branches);
1368
- param_indexes.extend(right_params);
1369
- Self::Dynamic {
1370
- branch_ids,
1371
- param_indexes,
1372
- }
1373
- }
1374
- (
1375
- Self::Static(mut branch_ids),
1376
- Self::Dynamic {
1377
- branch_ids: right_branches,
1378
- param_indexes,
1379
- },
1380
- )
1381
- | (
1382
- Self::Dynamic {
1383
- branch_ids: right_branches,
1384
- param_indexes,
1385
- },
1386
- Self::Static(mut branch_ids),
1387
- ) => {
1388
- branch_ids.extend(right_branches);
1389
- Self::Dynamic {
1390
- branch_ids,
1391
- param_indexes,
1392
- }
1393
- }
1394
- }
1395
- }
1396
-
1397
- fn union(self, other: Self) -> Self {
1398
- match (self, other) {
1399
- (Self::Missing, selector) | (selector, Self::Missing) => selector,
1400
- (Self::Static(mut left), Self::Static(right)) => {
1401
- left.extend(right);
1402
- Self::Static(left)
1403
- }
1404
- (
1405
- Self::Dynamic {
1406
- mut branch_ids,
1407
- mut param_indexes,
1408
- },
1409
- Self::Dynamic {
1410
- branch_ids: right_branches,
1411
- param_indexes: right_params,
1412
- },
1413
- ) => {
1414
- branch_ids.extend(right_branches);
1415
- param_indexes.extend(right_params);
1416
- Self::Dynamic {
1417
- branch_ids,
1418
- param_indexes,
1419
- }
1420
- }
1421
- (
1422
- Self::Static(mut branch_ids),
1423
- Self::Dynamic {
1424
- branch_ids: right_branches,
1425
- param_indexes,
1426
- },
1427
- )
1428
- | (
1429
- Self::Dynamic {
1430
- branch_ids: right_branches,
1431
- param_indexes,
1432
- },
1433
- Self::Static(mut branch_ids),
1434
- ) => {
1435
- branch_ids.extend(right_branches);
1436
- Self::Dynamic {
1437
- branch_ids,
1438
- param_indexes,
1439
- }
1440
- }
1441
- }
1442
- }
1443
- }
1444
-
1445
- fn predicate_branch_selector(
1446
- predicate: &BoundPredicate,
1447
- branch_column: &str,
1448
- ) -> Result<BranchSelector, LixError> {
1449
- match predicate {
1450
- BoundPredicate::True => Ok(BranchSelector::Missing),
1451
- BoundPredicate::False => Ok(BranchSelector::Static(BTreeSet::new())),
1452
- BoundPredicate::And(predicates) => {
1453
- let mut result = BranchSelector::Missing;
1454
- for predicate in predicates {
1455
- result = result.intersect(predicate_branch_selector(predicate, branch_column)?);
1456
- }
1457
- Ok(result)
1458
- }
1459
- BoundPredicate::Or(predicates) => {
1460
- let mut result = BranchSelector::Static(BTreeSet::new());
1461
- for predicate in predicates {
1462
- let selector = predicate_branch_selector(predicate, branch_column)?;
1463
- if selector == BranchSelector::Missing {
1464
- return Ok(BranchSelector::Missing);
1465
- }
1466
- result = result.union(selector);
1467
- }
1468
- Ok(result)
1469
- }
1470
- BoundPredicate::Eq(left, right) => {
1471
- branch_selector_from_binary_exprs(left, right, branch_column)
1472
- .or_else(|| branch_selector_from_binary_exprs(right, left, branch_column))
1473
- .transpose()
1474
- .map(|selector| selector.unwrap_or(BranchSelector::Missing))
1475
- }
1476
- BoundPredicate::IsNull(_) | BoundPredicate::IsNotNull(_) => Ok(BranchSelector::Missing),
1477
- BoundPredicate::In { expr, values } => {
1478
- let BoundExpr::Column(column) = expr else {
1479
- return Ok(BranchSelector::Missing);
1480
- };
1481
- if column.name != branch_column {
1482
- return Ok(BranchSelector::Missing);
1483
- }
1484
- let mut selector = BranchSelector::Missing;
1485
- for value in values {
1486
- selector = selector.union(value_branch_selector(value)?);
1487
- }
1488
- Ok(selector)
1489
- }
1490
- }
1491
- }
1492
-
1493
- fn branch_selector_from_binary_exprs(
1494
- column_expr: &BoundExpr,
1495
- value_expr: &BoundExpr,
1496
- branch_column: &str,
1497
- ) -> Option<Result<BranchSelector, LixError>> {
1498
- let BoundExpr::Column(column) = column_expr else {
1499
- return None;
1500
- };
1501
- if column.name != branch_column {
1502
- return None;
1503
- }
1504
- Some(value_branch_selector(value_expr))
1505
- }
1506
-
1507
- fn value_branch_selector(expr: &BoundExpr) -> Result<BranchSelector, LixError> {
1508
- match expr {
1509
- BoundExpr::Literal(BoundLiteral::Text(branch_id)) => {
1510
- Ok(BranchSelector::Static(BTreeSet::from([branch_id.clone()])))
1511
- }
1512
- BoundExpr::Param(param) => Ok(BranchSelector::Dynamic {
1513
- branch_ids: BTreeSet::new(),
1514
- param_indexes: BTreeSet::from([param.index]),
1515
- }),
1516
- _ => Err(super::error::unsupported(
1517
- "by-branch SQL write predicates require string branch ids",
1518
- )),
1519
- }
1520
- }
1521
-
1522
- fn active_branch_scope(active_branch_id: &str) -> BranchScope {
1523
- BranchScope::Active {
1524
- branch_id: active_branch_id.to_string(),
1525
- }
1526
- }
1527
-
1528
- struct ParamBinder {
1529
- next_implicit_index: usize,
1530
- mode: Option<ParamMode>,
1531
- params: BTreeMap<usize, BoundParamRef>,
1532
- }
1533
-
1534
- #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1535
- enum ParamMode {
1536
- Implicit,
1537
- Numbered,
1538
- }
1539
-
1540
- impl Default for ParamBinder {
1541
- fn default() -> Self {
1542
- Self {
1543
- next_implicit_index: 0,
1544
- mode: None,
1545
- params: BTreeMap::new(),
1546
- }
1547
- }
1548
- }
1549
-
1550
- impl ParamBinder {
1551
- fn bind(&mut self, name: &str) -> Result<BoundParamRef, LixError> {
1552
- let index = if name == "?" {
1553
- self.require_mode(ParamMode::Implicit, name)?;
1554
- self.next_implicit_index += 1;
1555
- self.next_implicit_index
1556
- } else {
1557
- self.require_mode(ParamMode::Numbered, name)?;
1558
- name.strip_prefix('$')
1559
- .and_then(|raw| raw.parse::<usize>().ok())
1560
- .filter(|index| *index > 0)
1561
- .ok_or_else(|| {
1562
- LixError::new(
1563
- LixError::CODE_PARSE_ERROR,
1564
- format!("unsupported SQL parameter placeholder '{name}'"),
1565
- )
1566
- .with_hint(
1567
- "Use placeholders like ?, ? or numbered placeholders like $1, $2, ...",
1568
- )
1569
- })?
1570
- };
1571
- let param = BoundParamRef { index };
1572
- self.params.entry(index).or_insert(param);
1573
- Ok(param)
1574
- }
1575
-
1576
- fn require_mode(&mut self, mode: ParamMode, name: &str) -> Result<(), LixError> {
1577
- match self.mode {
1578
- Some(existing) if existing != mode => Err(LixError::new(
1579
- LixError::CODE_PARSE_ERROR,
1580
- format!("cannot mix SQL parameter placeholder styles near '{name}'"),
1581
- )
1582
- .with_hint("Use either positional ? placeholders or numbered $1 placeholders.")),
1583
- Some(_) => Ok(()),
1584
- None => {
1585
- self.mode = Some(mode);
1586
- Ok(())
1587
- }
1588
- }
1589
- }
1590
-
1591
- fn into_map(self) -> BoundParamMap {
1592
- BoundParamMap {
1593
- params: self.params,
1594
- }
1595
- }
1596
- }
1597
-
1598
- #[cfg(test)]
1599
- mod tests {
1600
- use super::*;
1601
- use datafusion::sql::parser::Statement as DataFusionStatement;
1602
-
1603
- #[test]
1604
- fn bind_statement_uses_exact_table_binding_for_write_targets() {
1605
- let statement = parse_statement("INSERT INTO foo.lix_file (id) VALUES ('file1')");
1606
- let error = bind_statement(&statement, &[], "branch1")
1607
- .expect_err("qualified write target should be rejected by the binder");
1608
-
1609
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
1610
- assert!(error.message.contains("qualified SQL table names"));
1611
- }
1612
-
1613
- #[test]
1614
- fn bind_statement_rejects_hidden_insert_columns() {
1615
- let statement = parse_statement(
1616
- "INSERT INTO lix_file (id, path, directory_id, name, data, lixcol_schema_key) VALUES ('file1', '/a', null, 'a', null, 'schema')",
1617
- );
1618
- let error = bind_statement(&statement, &[], "branch1")
1619
- .expect_err("hidden columns should not bind through statement binder");
1620
-
1621
- assert_eq!(error.code, LixError::CODE_COLUMN_NOT_FOUND);
1622
- assert!(error.message.contains("not part of public SQL surface"));
1623
- }
1624
-
1625
- #[test]
1626
- fn bind_statement_rejects_implicit_insert_columns() {
1627
- let statement = parse_statement("INSERT INTO lix_file VALUES ('file1')");
1628
- let error = bind_statement(&statement, &[], "branch1")
1629
- .expect_err("implicit insert column list should fail closed");
1630
-
1631
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
1632
- assert!(error
1633
- .message
1634
- .contains("INSERT requires an explicit public column list"));
1635
- }
1636
-
1637
- #[test]
1638
- fn bind_statement_rejects_entity_insert_select() {
1639
- let statement = parse_statement(
1640
- "INSERT INTO test_state_schema (lixcol_entity_pk, value) SELECT lix_json('[\"a\"]'), 'A'",
1641
- );
1642
- let error = bind_statement(
1643
- &statement,
1644
- &[serde_json::json!({
1645
- "x-lix-key": "test_state_schema",
1646
- "properties": {
1647
- "value": { "type": "string" }
1648
- }
1649
- })],
1650
- "branch1",
1651
- )
1652
- .expect_err("entity INSERT SELECT should fail closed at binding");
1653
-
1654
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
1655
- assert!(error
1656
- .message
1657
- .contains("INSERT ... SELECT is not supported for entity SQL surfaces yet"));
1658
- }
1659
-
1660
- #[test]
1661
- fn bind_statement_rejects_duplicate_insert_columns() {
1662
- let statement = parse_statement("INSERT INTO lix_file (id, id) VALUES ('file1', 'file2')");
1663
- let error = bind_statement(&statement, &[], "branch1")
1664
- .expect_err("duplicate insert columns should be rejected");
1665
-
1666
- assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
1667
- assert!(error.message.contains("duplicate write target column 'id'"));
1668
- }
1669
-
1670
- #[test]
1671
- fn bind_statement_rejects_duplicate_lix_state_by_branch_insert_columns() {
1672
- let statement = parse_statement(
1673
- "INSERT INTO lix_state_by_branch (\
1674
- entity_pk, schema_key, snapshot_content, branch_id, branch_id\
1675
- ) VALUES (\
1676
- '[\"entity1\"]', 'lix_key_value', '{\"key\":\"k\",\"value\":\"v\"}', 'branch1', 'branch2'\
1677
- )",
1678
- );
1679
- let error = bind_statement(&statement, &[], "branch1")
1680
- .expect_err("duplicate lix_state_by_branch insert columns should be rejected");
1681
-
1682
- assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
1683
- assert!(error
1684
- .message
1685
- .contains("duplicate write target column 'branch_id'"));
1686
- }
1687
-
1688
- #[test]
1689
- fn bind_statement_rejects_duplicate_update_columns() {
1690
- let statement = parse_statement("UPDATE lix_file SET name = 'a', name = 'b'");
1691
- let error = bind_statement(&statement, &[], "branch1")
1692
- .expect_err("duplicate update columns should be rejected");
1693
-
1694
- assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
1695
- assert!(error
1696
- .message
1697
- .contains("duplicate write target column 'name'"));
1698
- }
1699
-
1700
- #[test]
1701
- fn bind_statement_rejects_read_only_history_writes() {
1702
- let statement = parse_statement("DELETE FROM lix_file_history");
1703
- let error = bind_statement(&statement, &[], "branch1")
1704
- .expect_err("history surfaces should be read-only");
1705
-
1706
- assert_eq!(error.code, LixError::CODE_READ_ONLY);
1707
- }
1708
-
1709
- #[test]
1710
- fn bind_statement_preserves_update_assignment_and_predicate() {
1711
- let statement = parse_statement(
1712
- "UPDATE test_state_schema_by_branch SET name = 'next' WHERE lixcol_branch_id = 'branch2'",
1713
- );
1714
- let bound = bind_statement(
1715
- &statement,
1716
- &[serde_json::json!({
1717
- "x-lix-key": "test_state_schema",
1718
- "properties": {
1719
- "id": { "type": "string" },
1720
- "name": { "type": "string" }
1721
- }
1722
- })],
1723
- "branch1",
1724
- )
1725
- .expect("write body should bind");
1726
-
1727
- let write = bound;
1728
- assert!(matches!(
1729
- write.target,
1730
- BoundWriteTarget::Entity(EntityWriteSurface::ByBranch { .. })
1731
- ));
1732
- assert_eq!(write.op, BoundWriteOp::Update);
1733
- assert_eq!(write.assignments.len(), 1);
1734
- assert_eq!(write.assignments[0].column.name, "name");
1735
- assert!(matches!(
1736
- write.assignments[0].value,
1737
- BoundExpr::Literal(BoundLiteral::Text(ref value)) if value == "next"
1738
- ));
1739
- assert!(matches!(
1740
- write.predicate,
1741
- BoundPredicate::Eq(
1742
- BoundExpr::Column(ref column),
1743
- BoundExpr::Literal(BoundLiteral::Text(ref value)),
1744
- ) if column.name == "lixcol_branch_id" && value == "branch2"
1745
- ));
1746
- assert!(matches!(
1747
- write.branch_scope,
1748
- BranchScope::ExplicitRequired { ref branch_ids }
1749
- if branch_ids == &BTreeSet::from(["branch2".to_string()])
1750
- ));
1751
- }
1752
-
1753
- #[test]
1754
- fn bind_statement_rejects_hidden_predicate_columns() {
1755
- let statement = parse_statement("DELETE FROM lix_file WHERE lixcol_schema_key = 'schema'");
1756
- let error = bind_statement(&statement, &[], "branch1")
1757
- .expect_err("hidden predicate columns should not bind");
1758
-
1759
- assert_eq!(error.code, LixError::CODE_COLUMN_NOT_FOUND);
1760
- assert!(error.message.contains("not part of public SQL surface"));
1761
- }
1762
-
1763
- #[test]
1764
- fn bind_statement_binds_insert_values_and_params_once() {
1765
- let statement = parse_statement("INSERT INTO lix_file (id, name) VALUES ($1, $2)");
1766
- let bound = bind_statement(&statement, &[], "branch1").expect("insert should bind");
1767
-
1768
- let write = bound;
1769
- assert_eq!(write.op, BoundWriteOp::Insert);
1770
- assert_eq!(
1771
- write.params.params.keys().copied().collect::<Vec<_>>(),
1772
- vec![1, 2]
1773
- );
1774
- let BoundWriteInput::Values(values) = write.input else {
1775
- panic!("expected values input");
1776
- };
1777
- assert_eq!(
1778
- values
1779
- .columns
1780
- .iter()
1781
- .map(|column| column.name.as_str())
1782
- .collect::<Vec<_>>(),
1783
- vec!["id", "name"]
1784
- );
1785
- assert_eq!(values.rows.len(), 1);
1786
- assert_eq!(values.rows[0].len(), 2);
1787
- assert!(values.rows[0]
1788
- .iter()
1789
- .any(|value| matches!(value, BoundExpr::Param(param) if param.index == 1)));
1790
- assert!(values.rows[0]
1791
- .iter()
1792
- .any(|value| matches!(value, BoundExpr::Param(param) if param.index == 2)));
1793
- }
1794
-
1795
- #[test]
1796
- fn bind_statement_rejects_insert_values_column_refs() {
1797
- let statement = parse_statement("INSERT INTO lix_file (id) VALUES (name)");
1798
- let error = bind_statement(&statement, &[], "branch1")
1799
- .expect_err("VALUES rows should not bind target table column refs");
1800
-
1801
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
1802
- assert!(error
1803
- .message
1804
- .contains("unsupported INSERT VALUES expression"));
1805
- }
1806
-
1807
- #[test]
1808
- fn bind_statement_binds_hex_literals_as_blobs() {
1809
- let statement =
1810
- parse_statement("INSERT INTO lix_file (id, data) VALUES ('file1', X'4142')");
1811
- let bound = bind_statement(&statement, &[], "branch1").expect("insert should bind");
1812
-
1813
- let write = bound;
1814
- let BoundWriteInput::Values(values) = write.input else {
1815
- panic!("expected values input");
1816
- };
1817
- assert!(values.rows[0]
1818
- .iter()
1819
- .any(|value| matches!(value, BoundExpr::Literal(BoundLiteral::Blob(bytes)) if bytes == &vec![0x41, 0x42])));
1820
- }
1821
-
1822
- #[test]
1823
- fn bind_statement_predecodes_lix_json_literal_values() {
1824
- let statement = parse_statement(
1825
- "INSERT INTO lix_state (entity_pk, schema_key, snapshot_content) VALUES (lix_json('[\"e1\"]'), 'app.test', lix_json('{\"id\":\"e1\"}'))",
1826
- );
1827
- let bound = bind_statement(&statement, &[], "branch1").expect("insert should bind");
1828
-
1829
- let write = bound;
1830
- let BoundWriteInput::Values(values) = write.input else {
1831
- panic!("expected values input");
1832
- };
1833
- assert_eq!(values.rows[0].len(), 3);
1834
- assert!(
1835
- values.rows[0]
1836
- .iter()
1837
- .filter(|value| matches!(value, BoundExpr::Literal(BoundLiteral::Json(_))))
1838
- .count()
1839
- >= 2
1840
- );
1841
- }
1842
-
1843
- #[test]
1844
- fn bind_statement_binds_public_values_functions() {
1845
- let statement = parse_statement(
1846
- "INSERT INTO lix_file (id, path, data) VALUES (lix_uuid_v7(), lix_timestamp(), lix_text_encode('hello'))",
1847
- );
1848
- let bound = bind_statement(&statement, &[], "branch1").expect("insert should bind");
1849
-
1850
- let write = bound;
1851
- let BoundWriteInput::Values(values) = write.input else {
1852
- panic!("expected values input");
1853
- };
1854
- let function_names = values.rows[0]
1855
- .iter()
1856
- .filter_map(|value| match value {
1857
- BoundExpr::Function { name, .. } => Some(name.as_str()),
1858
- _ => None,
1859
- })
1860
- .collect::<BTreeSet<_>>();
1861
- assert_eq!(
1862
- function_names,
1863
- BTreeSet::from(["lix_text_encode", "lix_timestamp", "lix_uuid_v7"])
1864
- );
1865
- }
1866
-
1867
- #[test]
1868
- fn bind_statement_rejects_unsupported_function_details() {
1869
- for sql in [
1870
- "INSERT INTO lix_file (id, data) VALUES ('f1', lix_text_encode('Ada', 'base64'))",
1871
- "INSERT INTO lix_file (id, data) VALUES ('f1', lix_empty_blob() FILTER (WHERE false))",
1872
- ] {
1873
- let error = bind_statement(&parse_statement(sql), &[], "branch1")
1874
- .expect_err("unsupported function details should fail closed");
1875
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL, "{sql}");
1876
- }
1877
- }
1878
-
1879
- #[test]
1880
- fn bind_statement_binds_by_branch_insert_scope_from_branch_column() {
1881
- let statement = parse_statement(
1882
- "INSERT INTO lix_file_by_branch (id, name, lixcol_branch_id) VALUES ('file1', 'a', 'branch2')",
1883
- );
1884
- let bound = bind_statement(&statement, &[], "branch1").expect("insert should bind");
1885
-
1886
- let write = bound;
1887
- assert!(matches!(
1888
- write.branch_scope,
1889
- BranchScope::Explicit { ref branch_ids }
1890
- if branch_ids == &BTreeSet::from(["branch2".to_string()])
1891
- ));
1892
- }
1893
-
1894
- #[test]
1895
- fn bind_statement_preserves_parameterized_by_branch_scope_selectors() {
1896
- let update = bind_statement(
1897
- &parse_statement(
1898
- "UPDATE lix_file_by_branch SET hidden = true WHERE id = 'file1' AND lixcol_branch_id = $1",
1899
- ),
1900
- &[],
1901
- "branch1",
1902
- )
1903
- .expect("parameterized update branch scope should bind");
1904
- assert_eq!(
1905
- update.branch_scope,
1906
- BranchScope::ExplicitRequiredDynamic {
1907
- branch_ids: BTreeSet::new(),
1908
- param_indexes: BTreeSet::from([1])
1909
- }
1910
- );
1911
-
1912
- let insert = bind_statement(
1913
- &parse_statement(
1914
- "INSERT INTO lix_file_by_branch (id, name, lixcol_branch_id) VALUES ('file1', 'a', $1)",
1915
- ),
1916
- &[],
1917
- "branch1",
1918
- )
1919
- .expect("parameterized insert branch scope should bind");
1920
- assert_eq!(
1921
- insert.branch_scope,
1922
- BranchScope::ExplicitDynamic {
1923
- branch_ids: BTreeSet::new(),
1924
- param_indexes: BTreeSet::from([1])
1925
- }
1926
- );
1927
- }
1928
-
1929
- #[test]
1930
- fn bind_statement_binds_contradictory_by_branch_selectors_as_empty() {
1931
- let statement = parse_statement(
1932
- "DELETE FROM lix_file_by_branch WHERE lixcol_branch_id IN ('v1') AND lixcol_branch_id IN ('v2')",
1933
- );
1934
- let bound = bind_statement(&statement, &[], "branch1").expect("delete should bind");
1935
-
1936
- let write = bound;
1937
- assert_eq!(write.branch_scope, BranchScope::Empty);
1938
- }
1939
-
1940
- #[test]
1941
- fn bind_statement_binds_false_by_branch_predicate_as_empty() {
1942
- let statement = parse_statement("DELETE FROM lix_file_by_branch WHERE false");
1943
- let bound = bind_statement(&statement, &[], "branch1").expect("no-match delete binds");
1944
-
1945
- let write = bound;
1946
- assert_eq!(write.branch_scope, BranchScope::Empty);
1947
- }
1948
-
1949
- #[test]
1950
- fn bind_statement_binds_false_base_predicates_as_empty() {
1951
- for sql in [
1952
- "DELETE FROM lix_file WHERE false",
1953
- "UPDATE lix_state SET metadata = '{}' WHERE false",
1954
- "DELETE FROM lix_branch WHERE false",
1955
- ] {
1956
- let bound = bind_statement(&parse_statement(sql), &[], "branch1")
1957
- .expect("no-match write should bind");
1958
- let write = bound;
1959
- assert_eq!(write.branch_scope, BranchScope::Empty, "{sql}");
1960
- }
1961
- }
1962
-
1963
- #[test]
1964
- fn bind_statement_accepts_is_null_and_is_not_null_predicates() {
1965
- for sql in [
1966
- "DELETE FROM lix_file WHERE data IS NULL",
1967
- "DELETE FROM lix_file WHERE data IS NOT NULL",
1968
- ] {
1969
- bind_statement(&parse_statement(sql), &[], "branch1")
1970
- .unwrap_or_else(|error| panic!("{sql} should bind, got {error:?}"));
1971
- }
1972
- }
1973
-
1974
- #[test]
1975
- fn bind_statement_binds_global_lix_state_insert_scope() {
1976
- let statement = parse_statement(
1977
- "INSERT INTO lix_state (entity_pk, schema_key, snapshot_content, global) VALUES ('[\"e1\"]', 'app.test', '{}', true)",
1978
- );
1979
- let bound = bind_statement(&statement, &[], "branch1").expect("insert should bind");
1980
-
1981
- let write = bound;
1982
- assert_eq!(write.branch_scope, BranchScope::Global);
1983
- }
1984
-
1985
- #[test]
1986
- fn bind_statement_rejects_parameterized_lix_state_global_scope() {
1987
- let statement = parse_statement(
1988
- "INSERT INTO lix_state (entity_pk, schema_key, snapshot_content, global) VALUES ('[\"e1\"]', 'app.test', '{}', $1)",
1989
- );
1990
- let error = bind_statement(&statement, &[], "branch1")
1991
- .expect_err("parameterized global scope should fail closed until scope resolution");
1992
-
1993
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
1994
- assert!(error
1995
- .message
1996
- .contains("parameterized lix_state global scope selectors"));
1997
- }
1998
-
1999
- #[test]
2000
- fn bind_statement_rejects_lix_state_by_branch_global_true_with_branch_id() {
2001
- let statement = parse_statement(
2002
- "INSERT INTO lix_state_by_branch (entity_pk, schema_key, snapshot_content, branch_id, global) VALUES ('[\"e1\"]', 'app.test', '{}', 'v1', true)",
2003
- );
2004
- let error = bind_statement(&statement, &[], "branch1")
2005
- .expect_err("global true and branch_id select contradictory scopes");
2006
-
2007
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2008
- assert!(error
2009
- .message
2010
- .contains("cannot combine global = true with non-global branch_id"));
2011
- }
2012
-
2013
- #[test]
2014
- fn bind_statement_binds_lix_state_by_branch_global_true_with_global_branch() {
2015
- let statement = parse_statement(
2016
- "INSERT INTO lix_state_by_branch (entity_pk, schema_key, snapshot_content, branch_id, global) VALUES ('[\"e1\"]', 'app.test', '{}', 'global', true)",
2017
- );
2018
- let bound = bind_statement(&statement, &[], "branch1").expect("global row should bind");
2019
-
2020
- let write = bound;
2021
- assert_eq!(write.branch_scope, BranchScope::Global);
2022
- }
2023
-
2024
- #[test]
2025
- fn bind_statement_rejects_lix_state_by_branch_global_false_with_global_branch() {
2026
- let statement = parse_statement(
2027
- "INSERT INTO lix_state_by_branch (entity_pk, schema_key, snapshot_content, branch_id, global) VALUES ('[\"e1\"]', 'app.test', '{}', 'global', false)",
2028
- );
2029
- let error = bind_statement(&statement, &[], "branch1")
2030
- .expect_err("global false cannot target the global branch");
2031
-
2032
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2033
- assert!(error
2034
- .message
2035
- .contains("cannot combine global = false with global branch_id"));
2036
- }
2037
-
2038
- #[test]
2039
- fn bind_statement_binds_parameterized_lix_state_by_branch_branch_id() {
2040
- let statement = parse_statement(
2041
- "INSERT INTO lix_state_by_branch (entity_pk, schema_key, snapshot_content, branch_id) VALUES ('[\"e1\"]', 'app.test', '{}', $1)",
2042
- );
2043
- let bound = bind_statement(&statement, &[], "branch1")
2044
- .expect("parameterized lix_state_by_branch branch_id should bind");
2045
-
2046
- assert_eq!(
2047
- bound.branch_scope,
2048
- BranchScope::ExplicitDynamic {
2049
- branch_ids: BTreeSet::new(),
2050
- param_indexes: BTreeSet::from([1])
2051
- }
2052
- );
2053
- }
2054
-
2055
- #[test]
2056
- fn bind_statement_binds_parameterized_lix_state_by_branch_global_false_branch_id() {
2057
- let statement = parse_statement(
2058
- "INSERT INTO lix_state_by_branch (entity_pk, schema_key, snapshot_content, branch_id, global) VALUES ('[\"e1\"]', 'app.test', '{}', $1, false)",
2059
- );
2060
- let bound = bind_statement(&statement, &[], "branch1")
2061
- .expect("parameterized lix_state_by_branch non-global row should bind");
2062
-
2063
- assert_eq!(
2064
- bound.branch_scope,
2065
- BranchScope::ExplicitDynamic {
2066
- branch_ids: BTreeSet::new(),
2067
- param_indexes: BTreeSet::from([1])
2068
- }
2069
- );
2070
- }
2071
-
2072
- #[test]
2073
- fn bind_statement_binds_contradictory_lix_state_global_predicates_as_empty() {
2074
- let statement = parse_statement(
2075
- "UPDATE lix_state SET metadata = '{}' WHERE global = true AND global = false",
2076
- );
2077
- let bound = bind_statement(&statement, &[], "branch1").expect("no-match scope should bind");
2078
-
2079
- let write = bound;
2080
- assert_eq!(write.branch_scope, BranchScope::Empty);
2081
- }
2082
-
2083
- #[test]
2084
- fn bind_statement_rejects_mixed_or_lix_state_global_scope() {
2085
- let statement = parse_statement(
2086
- "UPDATE lix_state SET metadata = '{}' WHERE global = true OR schema_key = 'app.test'",
2087
- );
2088
- let error = bind_statement(&statement, &[], "branch1")
2089
- .expect_err("mixed global OR scope should fail closed");
2090
-
2091
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2092
- assert!(
2093
- error
2094
- .message
2095
- .contains("lix_state global predicates select mixed branch scopes"),
2096
- "{}",
2097
- error.message
2098
- );
2099
- }
2100
-
2101
- #[test]
2102
- fn global_selector_mixed_intersect_missing_stays_mixed() {
2103
- assert_eq!(
2104
- GlobalSelector::Mixed.intersect(GlobalSelector::Missing),
2105
- GlobalSelector::Mixed
2106
- );
2107
- assert_eq!(
2108
- GlobalSelector::Missing.intersect(GlobalSelector::Mixed),
2109
- GlobalSelector::Mixed
2110
- );
2111
- }
2112
-
2113
- #[test]
2114
- fn bind_statement_rejects_dynamic_entity_primary_key_updates() {
2115
- let statement = parse_statement("UPDATE project_message SET id = 'm2' WHERE id = 'm1'");
2116
- let error = bind_statement(
2117
- &statement,
2118
- &[serde_json::json!({
2119
- "x-lix-key": "project_message",
2120
- "x-lix-primary-key": ["/id"],
2121
- "type": "object",
2122
- "properties": {
2123
- "id": { "type": "string" },
2124
- "body": { "type": "string" }
2125
- },
2126
- "required": ["id", "body"],
2127
- "additionalProperties": false
2128
- })],
2129
- "branch1",
2130
- )
2131
- .expect_err("entity primary key columns should be insert-only");
2132
-
2133
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2134
- assert!(error.message.contains("is not writable"));
2135
- }
2136
-
2137
- #[test]
2138
- fn bind_statement_binds_branch_writes_as_global() {
2139
- let statement =
2140
- parse_statement("INSERT INTO lix_branch (id, name) VALUES ('draft', 'Draft')");
2141
- let bound = bind_statement(&statement, &[], "branch1").expect("insert should bind");
2142
-
2143
- let write = bound;
2144
- assert_eq!(write.branch_scope, BranchScope::Global);
2145
- }
2146
-
2147
- #[test]
2148
- fn bind_statement_binds_negative_numeric_literals() {
2149
- let statement = parse_statement(
2150
- "UPDATE lix_state SET snapshot_content = -1 WHERE entity_pk = '[\"e1\"]'",
2151
- );
2152
- let bound = bind_statement(&statement, &[], "branch1").expect("update should bind");
2153
-
2154
- let write = bound;
2155
- assert!(matches!(
2156
- write.assignments[0].value,
2157
- BoundExpr::Literal(BoundLiteral::Integer(-1))
2158
- ));
2159
- }
2160
-
2161
- #[test]
2162
- fn bind_statement_rejects_by_branch_writes_without_branch_selector() {
2163
- let statement =
2164
- parse_statement("UPDATE lix_file_by_branch SET hidden = true WHERE id = 'file1'");
2165
- let error = bind_statement(&statement, &[], "branch1")
2166
- .expect_err("by-branch writes should require explicit branch predicate");
2167
-
2168
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2169
- assert!(error.message.contains("require an explicit"));
2170
- }
2171
-
2172
- #[test]
2173
- fn bind_statement_rejects_mixed_placeholder_styles() {
2174
- let mut params = ParamBinder::default();
2175
- params.bind("?").expect("implicit placeholder should bind");
2176
- let error = params
2177
- .bind("$1")
2178
- .expect_err("mixed placeholder styles should be rejected");
2179
-
2180
- assert_eq!(error.code, LixError::CODE_PARSE_ERROR);
2181
- assert!(error
2182
- .message
2183
- .contains("cannot mix SQL parameter placeholder styles"));
2184
- }
2185
-
2186
- #[test]
2187
- fn bind_statement_rejects_read_only_by_branch_columns_as_write_targets() {
2188
- let statement =
2189
- parse_statement("UPDATE lix_file_by_branch SET lixcol_branch_id = 'branch2'");
2190
- let error = bind_statement(&statement, &[], "branch1")
2191
- .expect_err("by-branch branch columns are filter-only");
2192
-
2193
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2194
- assert!(error.message.contains("is not writable"));
2195
- }
2196
-
2197
- #[test]
2198
- fn bind_statement_rejects_provider_read_only_update_columns() {
2199
- let statement = parse_statement("UPDATE lix_state SET entity_pk = '[\"next\"]'");
2200
- let error = bind_statement(&statement, &[], "branch1")
2201
- .expect_err("lix_state identity columns are insert-only");
2202
-
2203
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2204
- assert!(error.message.contains("is not writable"));
2205
- }
2206
-
2207
- #[test]
2208
- fn bind_statement_rejects_explain_wrappers() {
2209
- let statement =
2210
- parse_statement("EXPLAIN UPDATE lix_file SET name = 'x' WHERE id = 'file1'");
2211
- let error = bind_statement(&statement, &[], "branch1")
2212
- .expect_err("EXPLAIN should not bind as a write");
2213
-
2214
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2215
- assert!(error
2216
- .message
2217
- .contains("EXPLAIN statements are not supported"));
2218
- }
2219
-
2220
- #[test]
2221
- fn bind_statement_rejects_unsupported_write_clauses() {
2222
- let statement =
2223
- parse_statement("UPDATE lix_file AS f SET name = 'next' WHERE f.id = 'file1'");
2224
- let error = bind_statement(&statement, &[], "branch1")
2225
- .expect_err("target aliases should not be ignored");
2226
-
2227
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
2228
- assert!(error
2229
- .message
2230
- .contains("DML target aliases are not supported"));
2231
- }
2232
-
2233
- fn parse_statement(sql: &str) -> DataFusionStatement {
2234
- crate::sql2::parse_statement(sql).expect("parse SQL")
2235
- }
2236
- }