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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +65 -64
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
  5. package/dist/engine-wasm/wasm/lix_engine.js +130 -118
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
  8. package/dist/generated/builtin-schemas.d.ts +69 -69
  9. package/dist/generated/builtin-schemas.js +94 -94
  10. package/dist/open-lix.d.ts +33 -26
  11. package/dist/open-lix.js +10 -10
  12. package/dist/sqlite/index.js +86 -30
  13. package/dist-engine-src/README.md +3 -3
  14. package/dist-engine-src/src/backend/capabilities.rs +67 -0
  15. package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
  16. package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
  17. package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
  18. package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
  19. package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
  20. package/dist-engine-src/src/backend/conformance/model.rs +28 -0
  21. package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
  22. package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
  23. package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
  24. package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
  25. package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
  26. package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
  27. package/dist-engine-src/src/backend/conformance/write.rs +16 -0
  28. package/dist-engine-src/src/backend/error.rs +94 -0
  29. package/dist-engine-src/src/backend/in_memory.rs +670 -0
  30. package/dist-engine-src/src/backend/mod.rs +36 -9
  31. package/dist-engine-src/src/backend/predicate.rs +80 -0
  32. package/dist-engine-src/src/backend/traits.rs +260 -0
  33. package/dist-engine-src/src/backend/types.rs +224 -81
  34. package/dist-engine-src/src/binary_cas/context.rs +8 -8
  35. package/dist-engine-src/src/binary_cas/kv.rs +234 -259
  36. package/dist-engine-src/src/{version → branch}/context.rs +12 -12
  37. package/dist-engine-src/src/branch/lifecycle.rs +221 -0
  38. package/dist-engine-src/src/branch/mod.rs +13 -0
  39. package/dist-engine-src/src/branch/refs.rs +321 -0
  40. package/dist-engine-src/src/branch/stage_rows.rs +67 -0
  41. package/dist-engine-src/src/branch/types.rs +21 -0
  42. package/dist-engine-src/src/catalog/context.rs +18 -18
  43. package/dist-engine-src/src/catalog/snapshot.rs +8 -8
  44. package/dist-engine-src/src/changelog/bench_support.rs +785 -0
  45. package/dist-engine-src/src/changelog/change.rs +1 -0
  46. package/dist-engine-src/src/changelog/codec.rs +497 -0
  47. package/dist-engine-src/src/changelog/commit.rs +1 -0
  48. package/dist-engine-src/src/changelog/context.rs +1614 -0
  49. package/dist-engine-src/src/changelog/mod.rs +29 -0
  50. package/dist-engine-src/src/changelog/store.rs +163 -0
  51. package/dist-engine-src/src/changelog/test_support.rs +54 -0
  52. package/dist-engine-src/src/changelog/types.rs +213 -0
  53. package/dist-engine-src/src/commit_graph/context.rs +317 -274
  54. package/dist-engine-src/src/commit_graph/mod.rs +2 -4
  55. package/dist-engine-src/src/commit_graph/types.rs +22 -42
  56. package/dist-engine-src/src/commit_graph/walker.rs +133 -103
  57. package/dist-engine-src/src/common/error.rs +52 -18
  58. package/dist-engine-src/src/common/identity.rs +2 -2
  59. package/dist-engine-src/src/common/mod.rs +1 -1
  60. package/dist-engine-src/src/domain.rs +42 -46
  61. package/dist-engine-src/src/engine.rs +74 -96
  62. package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
  63. package/dist-engine-src/src/functions/context.rs +56 -52
  64. package/dist-engine-src/src/functions/state.rs +51 -52
  65. package/dist-engine-src/src/init.rs +288 -154
  66. package/dist-engine-src/src/json_store/context.rs +15 -266
  67. package/dist-engine-src/src/json_store/mod.rs +26 -0
  68. package/dist-engine-src/src/json_store/store.rs +103 -718
  69. package/dist-engine-src/src/json_store/types.rs +4 -9
  70. package/dist-engine-src/src/lib.rs +49 -19
  71. package/dist-engine-src/src/live_state/context.rs +654 -790
  72. package/dist-engine-src/src/live_state/mod.rs +9 -3
  73. package/dist-engine-src/src/live_state/overlay.rs +4 -4
  74. package/dist-engine-src/src/live_state/types.rs +30 -21
  75. package/dist-engine-src/src/live_state/visibility.rs +514 -71
  76. package/dist-engine-src/src/plugin/install.rs +48 -48
  77. package/dist-engine-src/src/plugin/manifest.rs +7 -7
  78. package/dist-engine-src/src/plugin/materializer.rs +0 -275
  79. package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
  80. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
  81. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
  82. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
  85. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
  86. package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
  87. package/dist-engine-src/src/schema/compatibility.rs +11 -11
  88. package/dist-engine-src/src/schema/definition.json +2 -2
  89. package/dist-engine-src/src/schema/definition.rs +5 -5
  90. package/dist-engine-src/src/schema/key.rs +3 -3
  91. package/dist-engine-src/src/schema/mod.rs +1 -1
  92. package/dist-engine-src/src/schema/tests.rs +18 -18
  93. package/dist-engine-src/src/session/context.rs +803 -148
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +223 -83
  96. package/dist-engine-src/src/session/merge/analysis.rs +9 -3
  97. package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
  98. package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
  99. package/dist-engine-src/src/session/merge/mod.rs +5 -6
  100. package/dist-engine-src/src/session/merge/stats.rs +7 -11
  101. package/dist-engine-src/src/session/mod.rs +15 -12
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +495 -14
  104. package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
  105. package/dist-engine-src/src/sql2/bind/error.rs +5 -0
  106. package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
  107. package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
  108. package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
  109. package/dist-engine-src/src/sql2/bind/read.rs +65 -0
  110. package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
  111. package/dist-engine-src/src/sql2/bind/table.rs +273 -0
  112. package/dist-engine-src/src/sql2/bind/write.rs +86 -0
  113. package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
  114. package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
  115. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
  116. package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
  117. package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
  118. package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
  119. package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
  120. package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
  121. package/dist-engine-src/src/sql2/context.rs +36 -30
  122. package/dist-engine-src/src/sql2/error.rs +1 -1
  123. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
  124. package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
  125. package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
  126. package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
  127. package/dist-engine-src/src/sql2/exec/write.rs +661 -0
  128. package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
  129. package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
  130. package/dist-engine-src/src/sql2/history_projection.rs +8 -8
  131. package/dist-engine-src/src/sql2/history_route.rs +35 -31
  132. package/dist-engine-src/src/sql2/mod.rs +28 -23
  133. package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
  134. package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
  135. package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
  136. package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
  137. package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
  138. package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
  139. package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
  140. package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
  141. package/dist-engine-src/src/sql2/plan/write.rs +147 -0
  142. package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
  143. package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
  144. package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
  145. package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
  146. package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
  147. package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
  148. package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
  149. package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
  150. package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
  151. package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
  152. package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
  153. package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
  154. package/dist-engine-src/src/sql2/read_only.rs +2 -2
  155. package/dist-engine-src/src/sql2/session.rs +47 -96
  156. package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
  157. package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
  158. package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
  159. package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
  160. package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
  161. package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
  162. package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
  163. package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
  164. package/dist-engine-src/src/storage/conformance.rs +399 -0
  165. package/dist-engine-src/src/storage/context.rs +552 -288
  166. package/dist-engine-src/src/storage/mod.rs +48 -10
  167. package/dist-engine-src/src/storage/point.rs +440 -0
  168. package/dist-engine-src/src/storage/read_scope.rs +43 -64
  169. package/dist-engine-src/src/storage/reader.rs +867 -0
  170. package/dist-engine-src/src/storage/scan.rs +784 -0
  171. package/dist-engine-src/src/storage/spaces.rs +236 -0
  172. package/dist-engine-src/src/storage/stats.rs +80 -0
  173. package/dist-engine-src/src/storage/write_set.rs +962 -0
  174. package/dist-engine-src/src/storage_bench.rs +136 -4828
  175. package/dist-engine-src/src/test_support.rs +360 -138
  176. package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
  177. package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
  178. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
  179. package/dist-engine-src/src/tracked_state/context.rs +1927 -993
  180. package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
  181. package/dist-engine-src/src/tracked_state/merge.rs +74 -88
  182. package/dist-engine-src/src/tracked_state/mod.rs +19 -16
  183. package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
  184. package/dist-engine-src/src/tracked_state/storage.rs +243 -191
  185. package/dist-engine-src/src/tracked_state/tree.rs +247 -371
  186. package/dist-engine-src/src/tracked_state/types.rs +49 -42
  187. package/dist-engine-src/src/transaction/bench_support.rs +407 -0
  188. package/dist-engine-src/src/transaction/commit.rs +821 -713
  189. package/dist-engine-src/src/transaction/context.rs +705 -600
  190. package/dist-engine-src/src/transaction/mod.rs +13 -2
  191. package/dist-engine-src/src/transaction/normalization.rs +63 -76
  192. package/dist-engine-src/src/transaction/prep.rs +13 -13
  193. package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
  194. package/dist-engine-src/src/transaction/staging.rs +228 -434
  195. package/dist-engine-src/src/transaction/types.rs +41 -98
  196. package/dist-engine-src/src/transaction/validation.rs +382 -446
  197. package/dist-engine-src/src/untracked_state/codec.rs +337 -29
  198. package/dist-engine-src/src/untracked_state/context.rs +7 -7
  199. package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
  200. package/dist-engine-src/src/untracked_state/mod.rs +1 -1
  201. package/dist-engine-src/src/untracked_state/storage.rs +659 -157
  202. package/dist-engine-src/src/untracked_state/types.rs +21 -21
  203. package/package.json +71 -68
  204. package/dist-engine-src/src/backend/kv.rs +0 -358
  205. package/dist-engine-src/src/backend/testing.rs +0 -658
  206. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  207. package/dist-engine-src/src/commit_store/context.rs +0 -944
  208. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  209. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  210. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  211. package/dist-engine-src/src/commit_store/types.rs +0 -215
  212. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  213. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  214. package/dist-engine-src/src/session/create_version.rs +0 -88
  215. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  216. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  217. package/dist-engine-src/src/session/switch_version.rs +0 -110
  218. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  219. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  220. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  221. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  222. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  223. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  224. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  225. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  226. package/dist-engine-src/src/storage/types.rs +0 -501
  227. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  228. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  229. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  230. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  231. package/dist-engine-src/src/version/mod.rs +0 -13
  232. package/dist-engine-src/src/version/refs.rs +0 -330
  233. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  234. package/dist-engine-src/src/version/types.rs +0 -21
@@ -0,0 +1,661 @@
1
+ //! Write execution for bound sql2 plans.
2
+
3
+ use std::collections::BTreeSet;
4
+
5
+ use serde_json::json;
6
+
7
+ use datafusion::sql::parser::Statement as DataFusionStatement;
8
+
9
+ use super::SqlLogicalPlan;
10
+ use crate::sql2::bind::expr::{BoundExpr, BoundLiteral};
11
+ use crate::sql2::bind::write::{BoundWriteInput, BoundWriteTarget};
12
+ use crate::sql2::parse::parse_statement;
13
+ use crate::sql2::plan::branch_scope::BranchScope;
14
+ use crate::sql2::plan::predicate::BoundPredicate;
15
+ use crate::sql2::plan::LogicalWritePlan;
16
+ use crate::sql2::SqlWriteExecutionContext;
17
+ use crate::{LixError, Value, GLOBAL_BRANCH_ID};
18
+
19
+ #[cfg(test)]
20
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
21
+ pub(crate) enum WriteExecutorMode {
22
+ Auto,
23
+ ForceDataFusion,
24
+ ForceFast,
25
+ }
26
+
27
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
28
+ pub(crate) enum WriteExecutorPath {
29
+ Fast,
30
+ DataFusion,
31
+ }
32
+
33
+ pub(crate) struct WriteLogicalPlan {
34
+ pub(super) plan: LogicalWritePlan,
35
+ }
36
+
37
+ #[allow(dead_code)]
38
+ pub(crate) async fn create_write_logical_plan(
39
+ ctx: &mut dyn SqlWriteExecutionContext,
40
+ sql: &str,
41
+ ) -> Result<SqlLogicalPlan, LixError> {
42
+ let statement = parse_statement(sql)?;
43
+ create_write_logical_plan_from_parsed(ctx, statement).await
44
+ }
45
+
46
+ pub(crate) async fn create_write_logical_plan_from_parsed(
47
+ ctx: &mut dyn SqlWriteExecutionContext,
48
+ statement: DataFusionStatement,
49
+ ) -> Result<SqlLogicalPlan, LixError> {
50
+ let visible_schemas = ctx.list_visible_schemas()?;
51
+ let bound_write =
52
+ crate::sql2::bind_statement(&statement, &visible_schemas, ctx.active_branch_id())?;
53
+ let logical_write = crate::sql2::plan_write(bound_write)?;
54
+ Ok(SqlLogicalPlan::Write(WriteLogicalPlan {
55
+ plan: logical_write,
56
+ }))
57
+ }
58
+
59
+ pub(crate) async fn execute_write_logical_plan(
60
+ ctx: &mut dyn SqlWriteExecutionContext,
61
+ plan: SqlLogicalPlan,
62
+ params: &[Value],
63
+ ) -> Result<u64, LixError> {
64
+ execute_write_logical_plan_with_mode_inner(ctx, plan, params, WriteExecutorModeInner::Auto)
65
+ .await
66
+ .map(|(rows_affected, _path)| rows_affected)
67
+ }
68
+
69
+ #[cfg(test)]
70
+ pub(crate) async fn execute_write_logical_plan_with_mode(
71
+ ctx: &mut dyn SqlWriteExecutionContext,
72
+ plan: SqlLogicalPlan,
73
+ params: &[Value],
74
+ mode: WriteExecutorMode,
75
+ ) -> Result<u64, LixError> {
76
+ execute_write_logical_plan_with_mode_and_trace(ctx, plan, params, mode)
77
+ .await
78
+ .map(|(rows_affected, _path)| rows_affected)
79
+ }
80
+
81
+ #[cfg(test)]
82
+ pub(crate) async fn execute_write_logical_plan_with_mode_and_trace(
83
+ ctx: &mut dyn SqlWriteExecutionContext,
84
+ plan: SqlLogicalPlan,
85
+ params: &[Value],
86
+ mode: WriteExecutorMode,
87
+ ) -> Result<(u64, WriteExecutorPath), LixError> {
88
+ let mode = match mode {
89
+ WriteExecutorMode::Auto => WriteExecutorModeInner::Auto,
90
+ WriteExecutorMode::ForceDataFusion => WriteExecutorModeInner::ForceDataFusion,
91
+ WriteExecutorMode::ForceFast => WriteExecutorModeInner::ForceFast,
92
+ };
93
+ execute_write_logical_plan_with_mode_inner(ctx, plan, params, mode).await
94
+ }
95
+
96
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
97
+ enum WriteExecutorModeInner {
98
+ Auto,
99
+ ForceDataFusion,
100
+ ForceFast,
101
+ }
102
+
103
+ async fn execute_write_logical_plan_with_mode_inner(
104
+ ctx: &mut dyn SqlWriteExecutionContext,
105
+ plan: SqlLogicalPlan,
106
+ params: &[Value],
107
+ mode: WriteExecutorModeInner,
108
+ ) -> Result<(u64, WriteExecutorPath), LixError> {
109
+ let SqlLogicalPlan::Write(write_plan) = plan else {
110
+ return Err(LixError::new(
111
+ LixError::CODE_UNSUPPORTED_SQL,
112
+ "expected SQL write logical plan",
113
+ ));
114
+ };
115
+ let write_plan = resolve_parameterized_branch_scope(write_plan.plan, params)?;
116
+ validate_write_parameter_count(&write_plan, params.len())?;
117
+
118
+ if mode != WriteExecutorModeInner::ForceDataFusion
119
+ && super::bound_public_write::supports_bound_public_write(&write_plan)
120
+ {
121
+ let rows_affected =
122
+ super::bound_public_write::execute_bound_public_write(ctx, &write_plan, params)
123
+ .await
124
+ .map_err(normalize_bound_public_write_error)?;
125
+ return Ok((rows_affected, WriteExecutorPath::Fast));
126
+ }
127
+
128
+ if mode != WriteExecutorModeInner::ForceDataFusion {
129
+ super::datafusion::validate_datafusion_write_logical_plan(ctx, &write_plan, params).await?;
130
+ if let Some(fast_plan) =
131
+ crate::sql2::optimize::simple_write::try_make_fast_write_plan(&write_plan)?
132
+ {
133
+ let rows_affected =
134
+ crate::sql2::exec::fast_write::try_execute_simple_write(ctx, fast_plan, params)
135
+ .await?;
136
+ return Ok((rows_affected, WriteExecutorPath::Fast));
137
+ }
138
+ if mode == WriteExecutorModeInner::ForceFast {
139
+ return Err(LixError::new(
140
+ LixError::CODE_UNSUPPORTED_SQL,
141
+ "SQL write plan is not eligible for fast execution",
142
+ ));
143
+ }
144
+ }
145
+
146
+ let rows_affected =
147
+ super::datafusion::execute_datafusion_write_logical_plan(ctx, &write_plan, params).await?;
148
+ Ok((rows_affected, WriteExecutorPath::DataFusion))
149
+ }
150
+
151
+ fn resolve_parameterized_branch_scope(
152
+ mut plan: LogicalWritePlan,
153
+ params: &[Value],
154
+ ) -> Result<LogicalWritePlan, LixError> {
155
+ plan.bound.branch_scope = match plan.bound.branch_scope {
156
+ BranchScope::ExplicitDynamic {
157
+ mut branch_ids,
158
+ param_indexes,
159
+ } => {
160
+ insert_branch_param_values(&mut branch_ids, &param_indexes, params)?;
161
+ if branch_ids.is_empty() {
162
+ BranchScope::Empty
163
+ } else {
164
+ BranchScope::Explicit { branch_ids }
165
+ }
166
+ }
167
+ BranchScope::ExplicitRequiredDynamic {
168
+ mut branch_ids,
169
+ param_indexes,
170
+ } => match branch_column_for_target(&plan.bound.target) {
171
+ Some(branch_column) => {
172
+ match resolved_predicate_branch_selector(
173
+ &plan.bound.predicate,
174
+ branch_column,
175
+ params,
176
+ )? {
177
+ ResolvedBranchSelector::Static(branch_ids) if branch_ids.is_empty() => {
178
+ BranchScope::Empty
179
+ }
180
+ ResolvedBranchSelector::Static(branch_ids) => {
181
+ BranchScope::ExplicitRequired { branch_ids }
182
+ }
183
+ ResolvedBranchSelector::Missing => {
184
+ insert_branch_param_values(&mut branch_ids, &param_indexes, params)?;
185
+ if branch_ids.is_empty() {
186
+ BranchScope::Empty
187
+ } else {
188
+ BranchScope::ExplicitRequired { branch_ids }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ None => {
194
+ insert_branch_param_values(&mut branch_ids, &param_indexes, params)?;
195
+ if branch_ids.is_empty() {
196
+ BranchScope::Empty
197
+ } else {
198
+ BranchScope::ExplicitRequired { branch_ids }
199
+ }
200
+ }
201
+ },
202
+ scope => scope,
203
+ };
204
+ normalize_lix_state_by_branch_scope(&mut plan, params)?;
205
+ Ok(plan)
206
+ }
207
+
208
+ fn branch_column_for_target(target: &BoundWriteTarget) -> Option<&'static str> {
209
+ match target {
210
+ BoundWriteTarget::LixStateByBranch => Some("branch_id"),
211
+ BoundWriteTarget::Entity(crate::sql2::bind::write::EntityWriteSurface::ByBranch {
212
+ ..
213
+ })
214
+ | BoundWriteTarget::File(crate::sql2::bind::write::FileWriteSurface::ByBranch)
215
+ | BoundWriteTarget::Directory(crate::sql2::bind::write::DirectoryWriteSurface::ByBranch) => {
216
+ Some("lixcol_branch_id")
217
+ }
218
+ _ => None,
219
+ }
220
+ }
221
+
222
+ fn normalize_lix_state_by_branch_scope(
223
+ plan: &mut LogicalWritePlan,
224
+ params: &[Value],
225
+ ) -> Result<(), LixError> {
226
+ if !matches!(plan.bound.target, BoundWriteTarget::LixStateByBranch) {
227
+ return Ok(());
228
+ }
229
+ let branch_ids = match &plan.bound.branch_scope {
230
+ BranchScope::Explicit { branch_ids } | BranchScope::ExplicitRequired { branch_ids } => {
231
+ branch_ids
232
+ }
233
+ _ => return Ok(()),
234
+ };
235
+ let explicit_global = explicit_lix_state_global_value(&plan.bound.input, params)?.or(
236
+ predicate_lix_state_global_value(&plan.bound.predicate, params)?,
237
+ );
238
+ if branch_ids.len() > 1 {
239
+ if explicit_global == Some(true) || branch_ids.contains(GLOBAL_BRANCH_ID) {
240
+ return Err(LixError::new(
241
+ LixError::CODE_UNSUPPORTED_SQL,
242
+ "lix_state_by_branch writes cannot mix global and branch-specific rows",
243
+ ));
244
+ }
245
+ return Ok(());
246
+ }
247
+ let is_global_branch = branch_ids.contains(GLOBAL_BRANCH_ID);
248
+ if explicit_global == Some(true) && !is_global_branch {
249
+ return Err(LixError::new(
250
+ LixError::CODE_UNSUPPORTED_SQL,
251
+ "lix_state_by_branch writes cannot combine global = true with non-global branch_id",
252
+ ));
253
+ }
254
+ if !is_global_branch {
255
+ return Ok(());
256
+ }
257
+ match explicit_global {
258
+ Some(false) => Err(LixError::new(
259
+ LixError::CODE_UNSUPPORTED_SQL,
260
+ "lix_state_by_branch writes cannot combine global = false with global branch_id",
261
+ )),
262
+ Some(true) | None => {
263
+ plan.bound.branch_scope = BranchScope::Global;
264
+ Ok(())
265
+ }
266
+ }
267
+ }
268
+
269
+ fn explicit_lix_state_global_value(
270
+ input: &BoundWriteInput,
271
+ params: &[Value],
272
+ ) -> Result<Option<bool>, LixError> {
273
+ let BoundWriteInput::Values(values) = input else {
274
+ return Ok(None);
275
+ };
276
+ let Some(global_index) = values.column_index("global") else {
277
+ return Ok(None);
278
+ };
279
+ let mut explicit = None;
280
+ for row in &values.rows {
281
+ let value = match &row[global_index] {
282
+ BoundExpr::Literal(BoundLiteral::Bool(value)) => *value,
283
+ BoundExpr::Literal(BoundLiteral::Null) => continue,
284
+ BoundExpr::Param(param) => match params.get(param.index.saturating_sub(1)) {
285
+ Some(Value::Boolean(value)) => *value,
286
+ Some(_) => {
287
+ return Err(LixError::new(
288
+ LixError::CODE_TYPE_MISMATCH,
289
+ "lix_state_by_branch global selectors must be boolean parameters",
290
+ ));
291
+ }
292
+ None => {
293
+ return Err(LixError::new(
294
+ LixError::CODE_INVALID_PARAM,
295
+ format!("missing SQL parameter ${}", param.index),
296
+ ));
297
+ }
298
+ },
299
+ _ => {
300
+ return Err(LixError::new(
301
+ LixError::CODE_UNSUPPORTED_SQL,
302
+ "lix_state_by_branch global selectors must be static booleans",
303
+ ));
304
+ }
305
+ };
306
+ if explicit.is_some_and(|prior| prior != value) {
307
+ return Err(LixError::new(
308
+ LixError::CODE_UNSUPPORTED_SQL,
309
+ "lix_state_by_branch writes cannot mix global and branch-specific rows",
310
+ ));
311
+ }
312
+ explicit = Some(value);
313
+ }
314
+ Ok(explicit)
315
+ }
316
+
317
+ #[derive(Clone, Debug, Eq, PartialEq)]
318
+ enum ResolvedBranchSelector {
319
+ Missing,
320
+ Static(BTreeSet<String>),
321
+ }
322
+
323
+ impl ResolvedBranchSelector {
324
+ fn union(self, other: Self) -> Self {
325
+ match (self, other) {
326
+ (Self::Missing, _) | (_, Self::Missing) => Self::Missing,
327
+ (Self::Static(mut left), Self::Static(right)) => {
328
+ left.extend(right);
329
+ Self::Static(left)
330
+ }
331
+ }
332
+ }
333
+
334
+ fn intersect(self, other: Self) -> Self {
335
+ match (self, other) {
336
+ (Self::Missing, selector) | (selector, Self::Missing) => selector,
337
+ (Self::Static(left), Self::Static(right)) => {
338
+ Self::Static(left.intersection(&right).cloned().collect())
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ fn resolved_predicate_branch_selector(
345
+ predicate: &BoundPredicate,
346
+ branch_column: &str,
347
+ params: &[Value],
348
+ ) -> Result<ResolvedBranchSelector, LixError> {
349
+ match predicate {
350
+ BoundPredicate::True => Ok(ResolvedBranchSelector::Missing),
351
+ BoundPredicate::False => Ok(ResolvedBranchSelector::Static(BTreeSet::new())),
352
+ BoundPredicate::And(predicates) => {
353
+ let mut result = ResolvedBranchSelector::Missing;
354
+ for predicate in predicates {
355
+ result = result.intersect(resolved_predicate_branch_selector(
356
+ predicate,
357
+ branch_column,
358
+ params,
359
+ )?);
360
+ }
361
+ Ok(result)
362
+ }
363
+ BoundPredicate::Or(predicates) => {
364
+ let mut result = ResolvedBranchSelector::Static(BTreeSet::new());
365
+ for predicate in predicates {
366
+ result = result.union(resolved_predicate_branch_selector(
367
+ predicate,
368
+ branch_column,
369
+ params,
370
+ )?);
371
+ }
372
+ Ok(result)
373
+ }
374
+ BoundPredicate::Eq(left, right) => {
375
+ resolved_branch_selector_from_binary_exprs(left, right, branch_column, params)
376
+ .or_else(|| {
377
+ resolved_branch_selector_from_binary_exprs(right, left, branch_column, params)
378
+ })
379
+ .transpose()
380
+ .map(|selector| selector.unwrap_or(ResolvedBranchSelector::Missing))
381
+ }
382
+ BoundPredicate::IsNull(_) | BoundPredicate::IsNotNull(_) => {
383
+ Ok(ResolvedBranchSelector::Missing)
384
+ }
385
+ BoundPredicate::In { expr, values } => {
386
+ let BoundExpr::Column(column) = expr else {
387
+ return Ok(ResolvedBranchSelector::Missing);
388
+ };
389
+ if column.name != branch_column {
390
+ return Ok(ResolvedBranchSelector::Missing);
391
+ }
392
+ let mut result = ResolvedBranchSelector::Static(BTreeSet::new());
393
+ for value in values {
394
+ result = result.union(resolved_value_branch_selector(value, params)?);
395
+ }
396
+ Ok(result)
397
+ }
398
+ }
399
+ }
400
+
401
+ fn resolved_branch_selector_from_binary_exprs(
402
+ column_expr: &BoundExpr,
403
+ value_expr: &BoundExpr,
404
+ branch_column: &str,
405
+ params: &[Value],
406
+ ) -> Option<Result<ResolvedBranchSelector, LixError>> {
407
+ let BoundExpr::Column(column) = column_expr else {
408
+ return None;
409
+ };
410
+ if column.name != branch_column {
411
+ return None;
412
+ }
413
+ Some(resolved_value_branch_selector(value_expr, params))
414
+ }
415
+
416
+ fn resolved_value_branch_selector(
417
+ expr: &BoundExpr,
418
+ params: &[Value],
419
+ ) -> Result<ResolvedBranchSelector, LixError> {
420
+ match expr {
421
+ BoundExpr::Literal(BoundLiteral::Text(branch_id)) => {
422
+ Ok(ResolvedBranchSelector::Static(BTreeSet::from([
423
+ branch_id.clone()
424
+ ])))
425
+ }
426
+ BoundExpr::Literal(BoundLiteral::Null) => {
427
+ Ok(ResolvedBranchSelector::Static(BTreeSet::new()))
428
+ }
429
+ BoundExpr::Param(param) => match params.get(param.index.saturating_sub(1)) {
430
+ Some(Value::Text(branch_id)) => Ok(ResolvedBranchSelector::Static(BTreeSet::from([
431
+ branch_id.clone(),
432
+ ]))),
433
+ Some(Value::Null) => Ok(ResolvedBranchSelector::Static(BTreeSet::new())),
434
+ Some(_) => Err(LixError::new(
435
+ LixError::CODE_TYPE_MISMATCH,
436
+ "by-branch SQL write selectors require text branch-id parameters",
437
+ )),
438
+ None => Err(LixError::new(
439
+ LixError::CODE_INVALID_PARAM,
440
+ format!(
441
+ "SQL branch selector parameter ${} was not provided",
442
+ param.index
443
+ ),
444
+ )),
445
+ },
446
+ _ => Err(LixError::new(
447
+ LixError::CODE_UNSUPPORTED_SQL,
448
+ "by-branch SQL write predicates require string branch ids",
449
+ )),
450
+ }
451
+ }
452
+
453
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
454
+ enum ResolvedGlobalSelector {
455
+ Missing,
456
+ Empty,
457
+ Static(bool),
458
+ Mixed,
459
+ }
460
+
461
+ impl ResolvedGlobalSelector {
462
+ fn union(self, other: Self) -> Self {
463
+ match (self, other) {
464
+ (Self::Mixed, _) | (_, Self::Mixed) => Self::Mixed,
465
+ (Self::Missing, selector) | (selector, Self::Missing) => selector,
466
+ (Self::Empty, selector) | (selector, Self::Empty) => selector,
467
+ (Self::Static(left), Self::Static(right)) if left == right => Self::Static(left),
468
+ (Self::Static(_), Self::Static(_)) => Self::Mixed,
469
+ }
470
+ }
471
+
472
+ fn intersect(self, other: Self) -> Self {
473
+ match (self, other) {
474
+ (Self::Empty, _) | (_, Self::Empty) => Self::Empty,
475
+ (Self::Missing, selector) | (selector, Self::Missing) => selector,
476
+ (Self::Mixed, selector) | (selector, Self::Mixed) => selector,
477
+ (Self::Static(left), Self::Static(right)) if left == right => Self::Static(left),
478
+ (Self::Static(_), Self::Static(_)) => Self::Empty,
479
+ }
480
+ }
481
+ }
482
+
483
+ fn predicate_lix_state_global_value(
484
+ predicate: &BoundPredicate,
485
+ params: &[Value],
486
+ ) -> Result<Option<bool>, LixError> {
487
+ match resolved_predicate_global_selector(predicate, params)? {
488
+ ResolvedGlobalSelector::Static(value) => Ok(Some(value)),
489
+ ResolvedGlobalSelector::Mixed => Err(LixError::new(
490
+ LixError::CODE_UNSUPPORTED_SQL,
491
+ "lix_state_by_branch writes cannot mix global and branch-specific rows",
492
+ )),
493
+ ResolvedGlobalSelector::Missing | ResolvedGlobalSelector::Empty => Ok(None),
494
+ }
495
+ }
496
+
497
+ fn resolved_predicate_global_selector(
498
+ predicate: &BoundPredicate,
499
+ params: &[Value],
500
+ ) -> Result<ResolvedGlobalSelector, LixError> {
501
+ match predicate {
502
+ BoundPredicate::True => Ok(ResolvedGlobalSelector::Missing),
503
+ BoundPredicate::False => Ok(ResolvedGlobalSelector::Empty),
504
+ BoundPredicate::And(predicates) => {
505
+ let mut result = ResolvedGlobalSelector::Missing;
506
+ for predicate in predicates {
507
+ result = result.intersect(resolved_predicate_global_selector(predicate, params)?);
508
+ }
509
+ Ok(result)
510
+ }
511
+ BoundPredicate::Or(predicates) => {
512
+ let mut result = ResolvedGlobalSelector::Empty;
513
+ let mut has_missing_branch = false;
514
+ for predicate in predicates {
515
+ let selector = resolved_predicate_global_selector(predicate, params)?;
516
+ if selector == ResolvedGlobalSelector::Missing {
517
+ has_missing_branch = true;
518
+ continue;
519
+ }
520
+ result = result.union(selector);
521
+ }
522
+ if has_missing_branch {
523
+ if result == ResolvedGlobalSelector::Empty {
524
+ Ok(ResolvedGlobalSelector::Missing)
525
+ } else {
526
+ Ok(ResolvedGlobalSelector::Mixed)
527
+ }
528
+ } else {
529
+ Ok(result)
530
+ }
531
+ }
532
+ BoundPredicate::Eq(left, right) => global_value_from_binary_exprs(left, right)
533
+ .or_else(|| global_value_from_binary_exprs(right, left))
534
+ .map(|expr| global_selector_value(expr, params))
535
+ .transpose()
536
+ .map(|selector| selector.unwrap_or(ResolvedGlobalSelector::Missing)),
537
+ BoundPredicate::IsNull(_) | BoundPredicate::IsNotNull(_) => {
538
+ Ok(ResolvedGlobalSelector::Missing)
539
+ }
540
+ BoundPredicate::In { expr, values } => {
541
+ let BoundExpr::Column(column) = expr else {
542
+ return Ok(ResolvedGlobalSelector::Missing);
543
+ };
544
+ if column.name != "global" {
545
+ return Ok(ResolvedGlobalSelector::Missing);
546
+ }
547
+ let mut result = ResolvedGlobalSelector::Missing;
548
+ for value in values {
549
+ result = result.union(global_selector_value(value, params)?);
550
+ }
551
+ Ok(result)
552
+ }
553
+ }
554
+ }
555
+
556
+ fn global_value_from_binary_exprs<'a>(
557
+ column_expr: &BoundExpr,
558
+ value_expr: &'a BoundExpr,
559
+ ) -> Option<&'a BoundExpr> {
560
+ let BoundExpr::Column(column) = column_expr else {
561
+ return None;
562
+ };
563
+ if column.name != "global" {
564
+ return None;
565
+ }
566
+ Some(value_expr)
567
+ }
568
+
569
+ fn global_selector_value(
570
+ expr: &BoundExpr,
571
+ params: &[Value],
572
+ ) -> Result<ResolvedGlobalSelector, LixError> {
573
+ match expr {
574
+ BoundExpr::Literal(BoundLiteral::Bool(value)) => Ok(ResolvedGlobalSelector::Static(*value)),
575
+ BoundExpr::Param(param) => match params.get(param.index.saturating_sub(1)) {
576
+ Some(Value::Boolean(value)) => Ok(ResolvedGlobalSelector::Static(*value)),
577
+ Some(Value::Null) => Ok(ResolvedGlobalSelector::Missing),
578
+ Some(_) => Err(LixError::new(
579
+ LixError::CODE_TYPE_MISMATCH,
580
+ "lix_state global predicates require boolean parameters",
581
+ )),
582
+ None => Err(LixError::new(
583
+ LixError::CODE_INVALID_PARAM,
584
+ format!("missing SQL parameter ${}", param.index),
585
+ )),
586
+ },
587
+ _ => Err(LixError::new(
588
+ LixError::CODE_UNSUPPORTED_SQL,
589
+ "lix_state global predicates require boolean literals",
590
+ )),
591
+ }
592
+ }
593
+
594
+ fn insert_branch_param_values(
595
+ branch_ids: &mut std::collections::BTreeSet<String>,
596
+ param_indexes: &std::collections::BTreeSet<usize>,
597
+ params: &[Value],
598
+ ) -> Result<(), LixError> {
599
+ for index in param_indexes {
600
+ match params.get(index.saturating_sub(1)) {
601
+ Some(Value::Text(branch_id)) => {
602
+ branch_ids.insert(branch_id.clone());
603
+ }
604
+ Some(Value::Null) => {}
605
+ Some(_) => {
606
+ return Err(LixError::new(
607
+ LixError::CODE_TYPE_MISMATCH,
608
+ "by-branch SQL write selectors require text branch-id parameters",
609
+ ));
610
+ }
611
+ None => {
612
+ return Err(LixError::new(
613
+ LixError::CODE_INVALID_PARAM,
614
+ format!("SQL branch selector parameter ${index} was not provided"),
615
+ ));
616
+ }
617
+ }
618
+ }
619
+ Ok(())
620
+ }
621
+
622
+ fn normalize_bound_public_write_error(error: LixError) -> LixError {
623
+ if error.code == LixError::CODE_SCHEMA_DEFINITION
624
+ && error.message.to_ascii_lowercase().contains("system schema")
625
+ {
626
+ return LixError {
627
+ code: LixError::CODE_INVALID_PARAM.to_string(),
628
+ ..error
629
+ };
630
+ }
631
+ error
632
+ }
633
+
634
+ fn validate_write_parameter_count(
635
+ plan: &LogicalWritePlan,
636
+ param_count: usize,
637
+ ) -> Result<(), LixError> {
638
+ let expected_count = plan.bound.params.params.keys().copied().max().unwrap_or(0);
639
+ if param_count == expected_count {
640
+ return Ok(());
641
+ }
642
+
643
+ Err(LixError::new(
644
+ LixError::CODE_INVALID_PARAM,
645
+ format!(
646
+ "SQL expected {expected_count} parameter(s), but {param_count} parameter(s) were provided"
647
+ ),
648
+ )
649
+ .with_details(json!({
650
+ "operation": "execute",
651
+ "expected_param_count": expected_count,
652
+ "provided_param_count": param_count,
653
+ "placeholders": plan
654
+ .bound
655
+ .params
656
+ .params
657
+ .keys()
658
+ .map(|index| format!("${index}"))
659
+ .collect::<Vec<_>>(),
660
+ })))
661
+ }