@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.
- package/README.md +76 -4
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +3 -3
- package/dist/native.d.ts +1 -0
- package/dist/native.js +47 -0
- package/dist/open-lix.d.ts +38 -207
- package/dist/open-lix.js +59 -284
- package/dist/result.d.ts +18 -0
- package/dist/result.js +48 -0
- package/dist/types.d.ts +114 -1
- package/dist/value.d.ts +28 -0
- package/dist/value.js +245 -0
- package/package.json +38 -71
- package/SKILL.md +0 -507
- package/dist/builtin-schemas.d.ts +0 -1
- package/dist/builtin-schemas.js +0 -1
- package/dist/engine-wasm/index.d.ts +0 -87
- package/dist/engine-wasm/index.js +0 -339
- package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
- package/dist/engine-wasm/wasm/lix_engine.js +0 -833
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
- package/dist/generated/builtin-schemas.d.ts +0 -427
- package/dist/generated/builtin-schemas.js +0 -643
- package/dist/sqlite/index.d.ts +0 -12
- package/dist/sqlite/index.js +0 -359
- package/dist-engine-src/README.md +0 -18
- package/dist-engine-src/src/backend/capabilities.rs +0 -67
- package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
- package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
- package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
- package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
- package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
- package/dist-engine-src/src/backend/conformance/model.rs +0 -28
- package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
- package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
- package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
- package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
- package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
- package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
- package/dist-engine-src/src/backend/conformance/write.rs +0 -16
- package/dist-engine-src/src/backend/error.rs +0 -94
- package/dist-engine-src/src/backend/in_memory.rs +0 -670
- package/dist-engine-src/src/backend/mod.rs +0 -39
- package/dist-engine-src/src/backend/predicate.rs +0 -80
- package/dist-engine-src/src/backend/traits.rs +0 -260
- package/dist-engine-src/src/backend/types.rs +0 -239
- package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
- package/dist-engine-src/src/binary_cas/codec.rs +0 -346
- package/dist-engine-src/src/binary_cas/context.rs +0 -139
- package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
- package/dist-engine-src/src/binary_cas/mod.rs +0 -11
- package/dist-engine-src/src/binary_cas/types.rs +0 -121
- package/dist-engine-src/src/branch/context.rs +0 -40
- package/dist-engine-src/src/branch/lifecycle.rs +0 -221
- package/dist-engine-src/src/branch/mod.rs +0 -13
- package/dist-engine-src/src/branch/refs.rs +0 -321
- package/dist-engine-src/src/branch/stage_rows.rs +0 -67
- package/dist-engine-src/src/branch/types.rs +0 -21
- package/dist-engine-src/src/catalog/context.rs +0 -412
- package/dist-engine-src/src/catalog/mod.rs +0 -10
- package/dist-engine-src/src/catalog/schema.rs +0 -4
- package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
- package/dist-engine-src/src/cel/context.rs +0 -86
- package/dist-engine-src/src/cel/error.rs +0 -19
- package/dist-engine-src/src/cel/mod.rs +0 -8
- package/dist-engine-src/src/cel/provider.rs +0 -9
- package/dist-engine-src/src/cel/runtime.rs +0 -167
- package/dist-engine-src/src/cel/value.rs +0 -50
- package/dist-engine-src/src/changelog/bench_support.rs +0 -785
- package/dist-engine-src/src/changelog/change.rs +0 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -497
- package/dist-engine-src/src/changelog/commit.rs +0 -1
- package/dist-engine-src/src/changelog/context.rs +0 -1614
- package/dist-engine-src/src/changelog/mod.rs +0 -29
- package/dist-engine-src/src/changelog/store.rs +0 -163
- package/dist-engine-src/src/changelog/test_support.rs +0 -54
- package/dist-engine-src/src/changelog/types.rs +0 -213
- package/dist-engine-src/src/commit_graph/context.rs +0 -944
- package/dist-engine-src/src/commit_graph/mod.rs +0 -9
- package/dist-engine-src/src/commit_graph/types.rs +0 -89
- package/dist-engine-src/src/commit_graph/walker.rs +0 -786
- package/dist-engine-src/src/common/error.rs +0 -347
- package/dist-engine-src/src/common/fingerprint.rs +0 -3
- package/dist-engine-src/src/common/fs_path.rs +0 -1336
- package/dist-engine-src/src/common/identity.rs +0 -145
- package/dist-engine-src/src/common/json_pointer.rs +0 -67
- package/dist-engine-src/src/common/metadata.rs +0 -40
- package/dist-engine-src/src/common/mod.rs +0 -23
- package/dist-engine-src/src/common/types.rs +0 -105
- package/dist-engine-src/src/common/wire.rs +0 -222
- package/dist-engine-src/src/domain.rs +0 -320
- package/dist-engine-src/src/engine.rs +0 -203
- package/dist-engine-src/src/entity_pk.rs +0 -402
- package/dist-engine-src/src/functions/context.rs +0 -296
- package/dist-engine-src/src/functions/deterministic.rs +0 -113
- package/dist-engine-src/src/functions/mod.rs +0 -18
- package/dist-engine-src/src/functions/provider.rs +0 -130
- package/dist-engine-src/src/functions/state.rs +0 -335
- package/dist-engine-src/src/functions/types.rs +0 -37
- package/dist-engine-src/src/init.rs +0 -692
- package/dist-engine-src/src/json_store/compression.rs +0 -77
- package/dist-engine-src/src/json_store/context.rs +0 -172
- package/dist-engine-src/src/json_store/encoded.rs +0 -15
- package/dist-engine-src/src/json_store/mod.rs +0 -38
- package/dist-engine-src/src/json_store/store.rs +0 -494
- package/dist-engine-src/src/json_store/types.rs +0 -212
- package/dist-engine-src/src/lib.rs +0 -92
- package/dist-engine-src/src/live_state/context.rs +0 -1883
- package/dist-engine-src/src/live_state/mod.rs +0 -21
- package/dist-engine-src/src/live_state/overlay.rs +0 -75
- package/dist-engine-src/src/live_state/reader.rs +0 -23
- package/dist-engine-src/src/live_state/types.rs +0 -231
- package/dist-engine-src/src/live_state/visibility.rs +0 -666
- package/dist-engine-src/src/plugin/archive.rs +0 -438
- package/dist-engine-src/src/plugin/component.rs +0 -183
- package/dist-engine-src/src/plugin/install.rs +0 -619
- package/dist-engine-src/src/plugin/manifest.rs +0 -516
- package/dist-engine-src/src/plugin/materializer.rs +0 -202
- package/dist-engine-src/src/plugin/mod.rs +0 -33
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
- package/dist-engine-src/src/plugin/storage.rs +0 -74
- package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
- package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
- package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
- package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
- package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
- package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
- package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
- package/dist-engine-src/src/schema/compatibility.rs +0 -787
- package/dist-engine-src/src/schema/definition.json +0 -187
- package/dist-engine-src/src/schema/definition.rs +0 -742
- package/dist-engine-src/src/schema/key.rs +0 -138
- package/dist-engine-src/src/schema/mod.rs +0 -20
- package/dist-engine-src/src/schema/seed.rs +0 -14
- package/dist-engine-src/src/schema/tests.rs +0 -780
- package/dist-engine-src/src/session/context.rs +0 -1059
- package/dist-engine-src/src/session/create_branch.rs +0 -94
- package/dist-engine-src/src/session/execute.rs +0 -681
- package/dist-engine-src/src/session/merge/analysis.rs +0 -108
- package/dist-engine-src/src/session/merge/branch.rs +0 -417
- package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
- package/dist-engine-src/src/session/merge/mod.rs +0 -10
- package/dist-engine-src/src/session/merge/stats.rs +0 -61
- package/dist-engine-src/src/session/mod.rs +0 -30
- package/dist-engine-src/src/session/switch_branch.rs +0 -113
- package/dist-engine-src/src/session/transaction.rs +0 -557
- package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
- package/dist-engine-src/src/sql2/bind/error.rs +0 -5
- package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
- package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
- package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
- package/dist-engine-src/src/sql2/bind/read.rs +0 -65
- package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
- package/dist-engine-src/src/sql2/bind/table.rs +0 -273
- package/dist-engine-src/src/sql2/bind/write.rs +0 -86
- package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
- package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
- package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
- package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
- package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
- package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
- package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
- package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
- package/dist-engine-src/src/sql2/context.rs +0 -317
- package/dist-engine-src/src/sql2/dml.rs +0 -148
- package/dist-engine-src/src/sql2/error.rs +0 -215
- package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
- package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
- package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
- package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
- package/dist-engine-src/src/sql2/exec/write.rs +0 -661
- package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
- package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
- package/dist-engine-src/src/sql2/history_projection.rs +0 -56
- package/dist-engine-src/src/sql2/history_route.rs +0 -661
- package/dist-engine-src/src/sql2/mod.rs +0 -52
- package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
- package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
- package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
- package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
- package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
- package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
- package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
- package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
- package/dist-engine-src/src/sql2/plan/write.rs +0 -147
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
- package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
- package/dist-engine-src/src/sql2/providers/change.rs +0 -445
- package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
- package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
- package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
- package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
- package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
- package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
- package/dist-engine-src/src/sql2/providers/history.rs +0 -426
- package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
- package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
- package/dist-engine-src/src/sql2/read_only.rs +0 -63
- package/dist-engine-src/src/sql2/record_batch.rs +0 -17
- package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
- package/dist-engine-src/src/sql2/runtime.rs +0 -60
- package/dist-engine-src/src/sql2/session.rs +0 -83
- package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
- package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
- package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
- package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
- package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
- package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
- package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
- package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
- package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
- package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
- package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
- package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
- package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
- package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
- package/dist-engine-src/src/storage/conformance.rs +0 -399
- package/dist-engine-src/src/storage/context.rs +0 -620
- package/dist-engine-src/src/storage/mod.rs +0 -52
- package/dist-engine-src/src/storage/point.rs +0 -440
- package/dist-engine-src/src/storage/read_scope.rs +0 -67
- package/dist-engine-src/src/storage/reader.rs +0 -867
- package/dist-engine-src/src/storage/scan.rs +0 -784
- package/dist-engine-src/src/storage/spaces.rs +0 -236
- package/dist-engine-src/src/storage/stats.rs +0 -80
- package/dist-engine-src/src/storage/write_set.rs +0 -962
- package/dist-engine-src/src/storage_bench.rs +0 -171
- package/dist-engine-src/src/test_support.rs +0 -450
- package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
- package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
- package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
- package/dist-engine-src/src/tracked_state/context.rs +0 -2801
- package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
- package/dist-engine-src/src/tracked_state/merge.rs +0 -478
- package/dist-engine-src/src/tracked_state/mod.rs +0 -35
- package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
- package/dist-engine-src/src/tracked_state/storage.rs +0 -427
- package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
- package/dist-engine-src/src/tracked_state/types.rs +0 -238
- package/dist-engine-src/src/transaction/bench_support.rs +0 -407
- package/dist-engine-src/src/transaction/commit.rs +0 -1592
- package/dist-engine-src/src/transaction/context.rs +0 -1653
- package/dist-engine-src/src/transaction/mod.rs +0 -24
- package/dist-engine-src/src/transaction/normalization.rs +0 -877
- package/dist-engine-src/src/transaction/prep.rs +0 -37
- package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
- package/dist-engine-src/src/transaction/staging.rs +0 -1525
- package/dist-engine-src/src/transaction/types.rs +0 -403
- package/dist-engine-src/src/transaction/validation.rs +0 -5766
- package/dist-engine-src/src/untracked_state/codec.rs +0 -615
- package/dist-engine-src/src/untracked_state/context.rs +0 -98
- package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
- package/dist-engine-src/src/untracked_state/mod.rs +0 -15
- package/dist-engine-src/src/untracked_state/storage.rs +0 -898
- package/dist-engine-src/src/untracked_state/types.rs +0 -146
- 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
|
-
}
|