@quereus/quereus 3.3.0 → 4.0.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 +7 -0
- package/dist/src/common/datatype.d.ts +12 -0
- package/dist/src/common/datatype.d.ts.map +1 -1
- package/dist/src/common/datatype.js.map +1 -1
- package/dist/src/common/types.d.ts +24 -0
- package/dist/src/common/types.d.ts.map +1 -1
- package/dist/src/common/types.js.map +1 -1
- package/dist/src/core/database-assertions.d.ts +37 -9
- package/dist/src/core/database-assertions.d.ts.map +1 -1
- package/dist/src/core/database-assertions.js +62 -110
- package/dist/src/core/database-assertions.js.map +1 -1
- package/dist/src/core/database-events.d.ts +163 -0
- package/dist/src/core/database-events.d.ts.map +1 -1
- package/dist/src/core/database-events.js +235 -21
- package/dist/src/core/database-events.js.map +1 -1
- package/dist/src/core/database-external-changes.d.ts +28 -0
- package/dist/src/core/database-external-changes.d.ts.map +1 -0
- package/dist/src/core/database-external-changes.js +242 -0
- package/dist/src/core/database-external-changes.js.map +1 -0
- package/dist/src/core/database-internal.d.ts +50 -1
- package/dist/src/core/database-internal.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views.d.ts +1253 -0
- package/dist/src/core/database-materialized-views.d.ts.map +1 -0
- package/dist/src/core/database-materialized-views.js +3064 -0
- package/dist/src/core/database-materialized-views.js.map +1 -0
- package/dist/src/core/database-options.d.ts +4 -0
- package/dist/src/core/database-options.d.ts.map +1 -1
- package/dist/src/core/database-options.js +10 -0
- package/dist/src/core/database-options.js.map +1 -1
- package/dist/src/core/database-transaction.d.ts +19 -3
- package/dist/src/core/database-transaction.d.ts.map +1 -1
- package/dist/src/core/database-transaction.js +30 -3
- package/dist/src/core/database-transaction.js.map +1 -1
- package/dist/src/core/database-watchers.d.ts +19 -0
- package/dist/src/core/database-watchers.d.ts.map +1 -1
- package/dist/src/core/database-watchers.js +63 -3
- package/dist/src/core/database-watchers.js.map +1 -1
- package/dist/src/core/database.d.ts +203 -11
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +493 -29
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/derived-row-validator.d.ts +137 -0
- package/dist/src/core/derived-row-validator.d.ts.map +1 -0
- package/dist/src/core/derived-row-validator.js +314 -0
- package/dist/src/core/derived-row-validator.js.map +1 -0
- package/dist/src/core/statement.d.ts.map +1 -1
- package/dist/src/core/statement.js +30 -9
- package/dist/src/core/statement.js.map +1 -1
- package/dist/src/emit/ast-stringify.d.ts +135 -1
- package/dist/src/emit/ast-stringify.d.ts.map +1 -1
- package/dist/src/emit/ast-stringify.js +793 -118
- package/dist/src/emit/ast-stringify.js.map +1 -1
- package/dist/src/func/builtins/aggregate.d.ts.map +1 -1
- package/dist/src/func/builtins/aggregate.js +11 -10
- package/dist/src/func/builtins/aggregate.js.map +1 -1
- package/dist/src/func/builtins/builtin-window-functions.d.ts.map +1 -1
- package/dist/src/func/builtins/builtin-window-functions.js +32 -0
- package/dist/src/func/builtins/builtin-window-functions.js.map +1 -1
- package/dist/src/func/builtins/explain.d.ts +3 -0
- package/dist/src/func/builtins/explain.d.ts.map +1 -1
- package/dist/src/func/builtins/explain.js +229 -0
- package/dist/src/func/builtins/explain.js.map +1 -1
- package/dist/src/func/builtins/index.d.ts.map +1 -1
- package/dist/src/func/builtins/index.js +10 -2
- package/dist/src/func/builtins/index.js.map +1 -1
- package/dist/src/func/builtins/json.d.ts.map +1 -1
- package/dist/src/func/builtins/json.js +3 -2
- package/dist/src/func/builtins/json.js.map +1 -1
- package/dist/src/func/builtins/mutation.d.ts +2 -0
- package/dist/src/func/builtins/mutation.d.ts.map +1 -0
- package/dist/src/func/builtins/mutation.js +53 -0
- package/dist/src/func/builtins/mutation.js.map +1 -0
- package/dist/src/func/builtins/schema.d.ts +2 -0
- package/dist/src/func/builtins/schema.d.ts.map +1 -1
- package/dist/src/func/builtins/schema.js +713 -26
- package/dist/src/func/builtins/schema.js.map +1 -1
- package/dist/src/func/builtins/string.js +1 -1
- package/dist/src/func/builtins/string.js.map +1 -1
- package/dist/src/func/registration.d.ts +9 -0
- package/dist/src/func/registration.d.ts.map +1 -1
- package/dist/src/func/registration.js +4 -0
- package/dist/src/func/registration.js.map +1 -1
- package/dist/src/index.d.ts +25 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +27 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/parser/ast.d.ts +353 -21
- package/dist/src/parser/ast.d.ts.map +1 -1
- package/dist/src/parser/index.d.ts +14 -1
- package/dist/src/parser/index.d.ts.map +1 -1
- package/dist/src/parser/index.js +19 -0
- package/dist/src/parser/index.js.map +1 -1
- package/dist/src/parser/lexer.d.ts +9 -0
- package/dist/src/parser/lexer.d.ts.map +1 -1
- package/dist/src/parser/lexer.js +9 -0
- package/dist/src/parser/lexer.js.map +1 -1
- package/dist/src/parser/parser.d.ts +276 -7
- package/dist/src/parser/parser.d.ts.map +1 -1
- package/dist/src/parser/parser.js +1387 -469
- package/dist/src/parser/parser.js.map +1 -1
- package/dist/src/parser/visitor.d.ts.map +1 -1
- package/dist/src/parser/visitor.js +12 -8
- package/dist/src/parser/visitor.js.map +1 -1
- package/dist/src/planner/analysis/assertion-classifier.d.ts.map +1 -1
- package/dist/src/planner/analysis/assertion-classifier.js +4 -0
- package/dist/src/planner/analysis/assertion-classifier.js.map +1 -1
- package/dist/src/planner/analysis/assertion-hoist-cache.d.ts.map +1 -1
- package/dist/src/planner/analysis/assertion-hoist-cache.js +8 -4
- package/dist/src/planner/analysis/assertion-hoist-cache.js.map +1 -1
- package/dist/src/planner/analysis/authored-inverse.d.ts +22 -0
- package/dist/src/planner/analysis/authored-inverse.d.ts.map +1 -0
- package/dist/src/planner/analysis/authored-inverse.js +267 -0
- package/dist/src/planner/analysis/authored-inverse.js.map +1 -0
- package/dist/src/planner/analysis/change-scope.d.ts +34 -4
- package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
- package/dist/src/planner/analysis/change-scope.js +108 -7
- package/dist/src/planner/analysis/change-scope.js.map +1 -1
- package/dist/src/planner/analysis/check-extraction.d.ts +36 -2
- package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/check-extraction.js +174 -46
- package/dist/src/planner/analysis/check-extraction.js.map +1 -1
- package/dist/src/planner/analysis/coarsened-key.d.ts +109 -0
- package/dist/src/planner/analysis/coarsened-key.d.ts.map +1 -0
- package/dist/src/planner/analysis/coarsened-key.js +228 -0
- package/dist/src/planner/analysis/coarsened-key.js.map +1 -0
- package/dist/src/planner/analysis/comparison-collation.d.ts +216 -0
- package/dist/src/planner/analysis/comparison-collation.d.ts.map +1 -0
- package/dist/src/planner/analysis/comparison-collation.js +341 -0
- package/dist/src/planner/analysis/comparison-collation.js.map +1 -0
- package/dist/src/planner/analysis/constraint-extractor.d.ts +3 -1
- package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.js +192 -9
- package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
- package/dist/src/planner/analysis/coverage-prover.d.ts +321 -0
- package/dist/src/planner/analysis/coverage-prover.d.ts.map +1 -0
- package/dist/src/planner/analysis/coverage-prover.js +1038 -0
- package/dist/src/planner/analysis/coverage-prover.js.map +1 -0
- package/dist/src/planner/analysis/key-filter.d.ts +22 -0
- package/dist/src/planner/analysis/key-filter.d.ts.map +1 -0
- package/dist/src/planner/analysis/key-filter.js +105 -0
- package/dist/src/planner/analysis/key-filter.js.map +1 -0
- package/dist/src/planner/analysis/partial-unique-extraction.d.ts +36 -1
- package/dist/src/planner/analysis/partial-unique-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/partial-unique-extraction.js +148 -22
- package/dist/src/planner/analysis/partial-unique-extraction.js.map +1 -1
- package/dist/src/planner/analysis/predicate-normalizer.d.ts.map +1 -1
- package/dist/src/planner/analysis/predicate-normalizer.js +30 -1
- package/dist/src/planner/analysis/predicate-normalizer.js.map +1 -1
- package/dist/src/planner/analysis/predicate-shape.d.ts +36 -1
- package/dist/src/planner/analysis/predicate-shape.d.ts.map +1 -1
- package/dist/src/planner/analysis/predicate-shape.js +51 -13
- package/dist/src/planner/analysis/predicate-shape.js.map +1 -1
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +314 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.js +1081 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -0
- package/dist/src/planner/analysis/scalar-invertibility.d.ts +92 -0
- package/dist/src/planner/analysis/scalar-invertibility.d.ts.map +1 -0
- package/dist/src/planner/analysis/scalar-invertibility.js +129 -0
- package/dist/src/planner/analysis/scalar-invertibility.js.map +1 -0
- package/dist/src/planner/analysis/update-lineage.d.ts +196 -0
- package/dist/src/planner/analysis/update-lineage.d.ts.map +1 -0
- package/dist/src/planner/analysis/update-lineage.js +322 -0
- package/dist/src/planner/analysis/update-lineage.js.map +1 -0
- package/dist/src/planner/analysis/view-complement.d.ts +42 -0
- package/dist/src/planner/analysis/view-complement.d.ts.map +1 -0
- package/dist/src/planner/analysis/view-complement.js +54 -0
- package/dist/src/planner/analysis/view-complement.js.map +1 -0
- package/dist/src/planner/building/alter-table.d.ts +1 -1
- package/dist/src/planner/building/alter-table.d.ts.map +1 -1
- package/dist/src/planner/building/alter-table.js +211 -2
- package/dist/src/planner/building/alter-table.js.map +1 -1
- package/dist/src/planner/building/block.d.ts.map +1 -1
- package/dist/src/planner/building/block.js +18 -1
- package/dist/src/planner/building/block.js.map +1 -1
- package/dist/src/planner/building/constraint-builder.d.ts +33 -5
- package/dist/src/planner/building/constraint-builder.d.ts.map +1 -1
- package/dist/src/planner/building/constraint-builder.js +63 -28
- package/dist/src/planner/building/constraint-builder.js.map +1 -1
- package/dist/src/planner/building/create-view.d.ts +9 -0
- package/dist/src/planner/building/create-view.d.ts.map +1 -1
- package/dist/src/planner/building/create-view.js +41 -12
- package/dist/src/planner/building/create-view.js.map +1 -1
- package/dist/src/planner/building/ddl.d.ts.map +1 -1
- package/dist/src/planner/building/ddl.js +94 -0
- package/dist/src/planner/building/ddl.js.map +1 -1
- package/dist/src/planner/building/declare-schema.d.ts +1 -0
- package/dist/src/planner/building/declare-schema.d.ts.map +1 -1
- package/dist/src/planner/building/declare-schema.js +4 -1
- package/dist/src/planner/building/declare-schema.js.map +1 -1
- package/dist/src/planner/building/default-scope.d.ts +26 -0
- package/dist/src/planner/building/default-scope.d.ts.map +1 -0
- package/dist/src/planner/building/default-scope.js +41 -0
- package/dist/src/planner/building/default-scope.js.map +1 -0
- package/dist/src/planner/building/delete.d.ts +19 -1
- package/dist/src/planner/building/delete.d.ts.map +1 -1
- package/dist/src/planner/building/delete.js +109 -30
- package/dist/src/planner/building/delete.js.map +1 -1
- package/dist/src/planner/building/dml-target.d.ts +118 -0
- package/dist/src/planner/building/dml-target.d.ts.map +1 -0
- package/dist/src/planner/building/dml-target.js +282 -0
- package/dist/src/planner/building/dml-target.js.map +1 -0
- package/dist/src/planner/building/drop-index.d.ts.map +1 -1
- package/dist/src/planner/building/drop-index.js +4 -1
- package/dist/src/planner/building/drop-index.js.map +1 -1
- package/dist/src/planner/building/drop-view.d.ts.map +1 -1
- package/dist/src/planner/building/drop-view.js +4 -2
- package/dist/src/planner/building/drop-view.js.map +1 -1
- package/dist/src/planner/building/expression.d.ts.map +1 -1
- package/dist/src/planner/building/expression.js +60 -21
- package/dist/src/planner/building/expression.js.map +1 -1
- package/dist/src/planner/building/foreign-key-builder.d.ts +30 -0
- package/dist/src/planner/building/foreign-key-builder.d.ts.map +1 -1
- package/dist/src/planner/building/foreign-key-builder.js +160 -129
- package/dist/src/planner/building/foreign-key-builder.js.map +1 -1
- package/dist/src/planner/building/insert.d.ts +45 -2
- package/dist/src/planner/building/insert.d.ts.map +1 -1
- package/dist/src/planner/building/insert.js +257 -88
- package/dist/src/planner/building/insert.js.map +1 -1
- package/dist/src/planner/building/lens-auxiliary-access.d.ts +22 -0
- package/dist/src/planner/building/lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/planner/building/lens-auxiliary-access.js +132 -0
- package/dist/src/planner/building/lens-auxiliary-access.js.map +1 -0
- package/dist/src/planner/building/materialized-view.d.ts +16 -0
- package/dist/src/planner/building/materialized-view.d.ts.map +1 -0
- package/dist/src/planner/building/materialized-view.js +57 -0
- package/dist/src/planner/building/materialized-view.js.map +1 -0
- package/dist/src/planner/building/returning-star.d.ts +32 -0
- package/dist/src/planner/building/returning-star.d.ts.map +1 -0
- package/dist/src/planner/building/returning-star.js +45 -0
- package/dist/src/planner/building/returning-star.js.map +1 -0
- package/dist/src/planner/building/select-aggregates.d.ts.map +1 -1
- package/dist/src/planner/building/select-aggregates.js +47 -0
- package/dist/src/planner/building/select-aggregates.js.map +1 -1
- package/dist/src/planner/building/select-compound.d.ts.map +1 -1
- package/dist/src/planner/building/select-compound.js +84 -11
- package/dist/src/planner/building/select-compound.js.map +1 -1
- package/dist/src/planner/building/select-context.d.ts +10 -2
- package/dist/src/planner/building/select-context.d.ts.map +1 -1
- package/dist/src/planner/building/select-context.js +7 -1
- package/dist/src/planner/building/select-context.js.map +1 -1
- package/dist/src/planner/building/select-modifiers.js +6 -0
- package/dist/src/planner/building/select-modifiers.js.map +1 -1
- package/dist/src/planner/building/select-ordinal.d.ts +18 -0
- package/dist/src/planner/building/select-ordinal.d.ts.map +1 -1
- package/dist/src/planner/building/select-ordinal.js +30 -0
- package/dist/src/planner/building/select-ordinal.js.map +1 -1
- package/dist/src/planner/building/select-projections.d.ts +8 -2
- package/dist/src/planner/building/select-projections.d.ts.map +1 -1
- package/dist/src/planner/building/select-projections.js +26 -4
- package/dist/src/planner/building/select-projections.js.map +1 -1
- package/dist/src/planner/building/select-window.d.ts.map +1 -1
- package/dist/src/planner/building/select-window.js +8 -5
- package/dist/src/planner/building/select-window.js.map +1 -1
- package/dist/src/planner/building/select.d.ts.map +1 -1
- package/dist/src/planner/building/select.js +164 -59
- package/dist/src/planner/building/select.js.map +1 -1
- package/dist/src/planner/building/set-object-tags.d.ts +7 -0
- package/dist/src/planner/building/set-object-tags.d.ts.map +1 -0
- package/dist/src/planner/building/set-object-tags.js +38 -0
- package/dist/src/planner/building/set-object-tags.js.map +1 -0
- package/dist/src/planner/building/tag-diagnostics.d.ts +27 -0
- package/dist/src/planner/building/tag-diagnostics.d.ts.map +1 -0
- package/dist/src/planner/building/tag-diagnostics.js +37 -0
- package/dist/src/planner/building/tag-diagnostics.js.map +1 -0
- package/dist/src/planner/building/update.d.ts +18 -1
- package/dist/src/planner/building/update.d.ts.map +1 -1
- package/dist/src/planner/building/update.js +134 -58
- package/dist/src/planner/building/update.js.map +1 -1
- package/dist/src/planner/building/view-mutation-builder.d.ts +15 -0
- package/dist/src/planner/building/view-mutation-builder.d.ts.map +1 -0
- package/dist/src/planner/building/view-mutation-builder.js +1158 -0
- package/dist/src/planner/building/view-mutation-builder.js.map +1 -0
- package/dist/src/planner/building/with.d.ts +11 -0
- package/dist/src/planner/building/with.d.ts.map +1 -1
- package/dist/src/planner/building/with.js +48 -10
- package/dist/src/planner/building/with.js.map +1 -1
- package/dist/src/planner/cost/index.d.ts +83 -0
- package/dist/src/planner/cost/index.d.ts.map +1 -1
- package/dist/src/planner/cost/index.js +114 -0
- package/dist/src/planner/cost/index.js.map +1 -1
- package/dist/src/planner/framework/characteristics.d.ts +38 -4
- package/dist/src/planner/framework/characteristics.d.ts.map +1 -1
- package/dist/src/planner/framework/characteristics.js +50 -6
- package/dist/src/planner/framework/characteristics.js.map +1 -1
- package/dist/src/planner/framework/pass.d.ts.map +1 -1
- package/dist/src/planner/framework/pass.js +2 -1
- package/dist/src/planner/framework/pass.js.map +1 -1
- package/dist/src/planner/framework/registry.d.ts +39 -1
- package/dist/src/planner/framework/registry.d.ts.map +1 -1
- package/dist/src/planner/framework/registry.js +18 -2
- package/dist/src/planner/framework/registry.js.map +1 -1
- package/dist/src/planner/mutation/backward-body.d.ts +131 -0
- package/dist/src/planner/mutation/backward-body.d.ts.map +1 -0
- package/dist/src/planner/mutation/backward-body.js +135 -0
- package/dist/src/planner/mutation/backward-body.js.map +1 -0
- package/dist/src/planner/mutation/cte-flatten.d.ts +17 -0
- package/dist/src/planner/mutation/cte-flatten.d.ts.map +1 -0
- package/dist/src/planner/mutation/cte-flatten.js +364 -0
- package/dist/src/planner/mutation/cte-flatten.js.map +1 -0
- package/dist/src/planner/mutation/decomposition.d.ts +273 -0
- package/dist/src/planner/mutation/decomposition.d.ts.map +1 -0
- package/dist/src/planner/mutation/decomposition.js +1719 -0
- package/dist/src/planner/mutation/decomposition.js.map +1 -0
- package/dist/src/planner/mutation/lens-enforcement.d.ts +165 -0
- package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -0
- package/dist/src/planner/mutation/lens-enforcement.js +745 -0
- package/dist/src/planner/mutation/lens-enforcement.js.map +1 -0
- package/dist/src/planner/mutation/multi-source.d.ts +568 -0
- package/dist/src/planner/mutation/multi-source.d.ts.map +1 -0
- package/dist/src/planner/mutation/multi-source.js +2915 -0
- package/dist/src/planner/mutation/multi-source.js.map +1 -0
- package/dist/src/planner/mutation/mutation-diagnostic.d.ts +37 -0
- package/dist/src/planner/mutation/mutation-diagnostic.d.ts.map +1 -0
- package/dist/src/planner/mutation/mutation-diagnostic.js +24 -0
- package/dist/src/planner/mutation/mutation-diagnostic.js.map +1 -0
- package/dist/src/planner/mutation/mutation-tags.d.ts +33 -0
- package/dist/src/planner/mutation/mutation-tags.d.ts.map +1 -0
- package/dist/src/planner/mutation/mutation-tags.js +31 -0
- package/dist/src/planner/mutation/mutation-tags.js.map +1 -0
- package/dist/src/planner/mutation/propagate.d.ts +97 -0
- package/dist/src/planner/mutation/propagate.d.ts.map +1 -0
- package/dist/src/planner/mutation/propagate.js +220 -0
- package/dist/src/planner/mutation/propagate.js.map +1 -0
- package/dist/src/planner/mutation/scope-transform.d.ts +181 -0
- package/dist/src/planner/mutation/scope-transform.d.ts.map +1 -0
- package/dist/src/planner/mutation/scope-transform.js +574 -0
- package/dist/src/planner/mutation/scope-transform.js.map +1 -0
- package/dist/src/planner/mutation/set-op.d.ts +242 -0
- package/dist/src/planner/mutation/set-op.d.ts.map +1 -0
- package/dist/src/planner/mutation/set-op.js +1687 -0
- package/dist/src/planner/mutation/set-op.js.map +1 -0
- package/dist/src/planner/mutation/single-source.d.ts +261 -0
- package/dist/src/planner/mutation/single-source.d.ts.map +1 -0
- package/dist/src/planner/mutation/single-source.js +1096 -0
- package/dist/src/planner/mutation/single-source.js.map +1 -0
- package/dist/src/planner/nodes/aggregate-node.js +3 -3
- package/dist/src/planner/nodes/aggregate-node.js.map +1 -1
- package/dist/src/planner/nodes/alias-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/alias-node.js +5 -1
- package/dist/src/planner/nodes/alias-node.js.map +1 -1
- package/dist/src/planner/nodes/alter-table-node.d.ts +124 -1
- package/dist/src/planner/nodes/alter-table-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/alter-table-node.js +27 -0
- package/dist/src/planner/nodes/alter-table-node.js.map +1 -1
- package/dist/src/planner/nodes/analyze-node.d.ts +2 -1
- package/dist/src/planner/nodes/analyze-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/analyze-node.js +18 -1
- package/dist/src/planner/nodes/analyze-node.js.map +1 -1
- package/dist/src/planner/nodes/asserted-keys-node.d.ts +43 -0
- package/dist/src/planner/nodes/asserted-keys-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/asserted-keys-node.js +99 -0
- package/dist/src/planner/nodes/asserted-keys-node.js.map +1 -0
- package/dist/src/planner/nodes/async-gather-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/async-gather-node.js +33 -8
- package/dist/src/planner/nodes/async-gather-node.js.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.js +2 -1
- package/dist/src/planner/nodes/bloom-join-node.js.map +1 -1
- package/dist/src/planner/nodes/create-view-node.d.ts +7 -2
- package/dist/src/planner/nodes/create-view-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/create-view-node.js +4 -1
- package/dist/src/planner/nodes/create-view-node.js.map +1 -1
- package/dist/src/planner/nodes/declarative-schema.d.ts +13 -1
- package/dist/src/planner/nodes/declarative-schema.d.ts.map +1 -1
- package/dist/src/planner/nodes/declarative-schema.js +32 -0
- package/dist/src/planner/nodes/declarative-schema.js.map +1 -1
- package/dist/src/planner/nodes/distinct-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/distinct-node.js +2 -0
- package/dist/src/planner/nodes/distinct-node.js.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts +29 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.js +27 -3
- package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
- package/dist/src/planner/nodes/eager-prefetch-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/eager-prefetch-node.js +2 -0
- package/dist/src/planner/nodes/eager-prefetch-node.js.map +1 -1
- package/dist/src/planner/nodes/envelope-scan-node.d.ts +42 -0
- package/dist/src/planner/nodes/envelope-scan-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/envelope-scan-node.js +62 -0
- package/dist/src/planner/nodes/envelope-scan-node.js.map +1 -0
- package/dist/src/planner/nodes/fanout-lookup-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.js +11 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.js.map +1 -1
- package/dist/src/planner/nodes/filter.d.ts.map +1 -1
- package/dist/src/planner/nodes/filter.js +63 -13
- package/dist/src/planner/nodes/filter.js.map +1 -1
- package/dist/src/planner/nodes/join-node.d.ts +41 -1
- package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/join-node.js +78 -8
- package/dist/src/planner/nodes/join-node.js.map +1 -1
- package/dist/src/planner/nodes/join-utils.d.ts +33 -6
- package/dist/src/planner/nodes/join-utils.d.ts.map +1 -1
- package/dist/src/planner/nodes/join-utils.js +124 -9
- package/dist/src/planner/nodes/join-utils.js.map +1 -1
- package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts +104 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js +91 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js.map +1 -0
- package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
- package/dist/src/planner/nodes/limit-offset.js +4 -5
- package/dist/src/planner/nodes/limit-offset.js.map +1 -1
- package/dist/src/planner/nodes/materialized-view-nodes.d.ts +69 -0
- package/dist/src/planner/nodes/materialized-view-nodes.d.ts.map +1 -0
- package/dist/src/planner/nodes/materialized-view-nodes.js +111 -0
- package/dist/src/planner/nodes/materialized-view-nodes.js.map +1 -0
- package/dist/src/planner/nodes/merge-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/merge-join-node.js +2 -1
- package/dist/src/planner/nodes/merge-join-node.js.map +1 -1
- package/dist/src/planner/nodes/ordinal-slice-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/ordinal-slice-node.js +2 -0
- package/dist/src/planner/nodes/ordinal-slice-node.js.map +1 -1
- package/dist/src/planner/nodes/plan-node-type.d.ts +9 -0
- package/dist/src/planner/nodes/plan-node-type.d.ts.map +1 -1
- package/dist/src/planner/nodes/plan-node-type.js +9 -0
- package/dist/src/planner/nodes/plan-node-type.js.map +1 -1
- package/dist/src/planner/nodes/plan-node.d.ts +265 -5
- package/dist/src/planner/nodes/plan-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/plan-node.js.map +1 -1
- package/dist/src/planner/nodes/pragma.d.ts +2 -1
- package/dist/src/planner/nodes/pragma.d.ts.map +1 -1
- package/dist/src/planner/nodes/pragma.js +12 -0
- package/dist/src/planner/nodes/pragma.js.map +1 -1
- package/dist/src/planner/nodes/project-node.d.ts +14 -1
- package/dist/src/planner/nodes/project-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/project-node.js +85 -11
- package/dist/src/planner/nodes/project-node.js.map +1 -1
- package/dist/src/planner/nodes/reference.d.ts.map +1 -1
- package/dist/src/planner/nodes/reference.js +62 -27
- package/dist/src/planner/nodes/reference.js.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.js +7 -0
- package/dist/src/planner/nodes/retrieve-node.js.map +1 -1
- package/dist/src/planner/nodes/returning-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/returning-node.js +10 -3
- package/dist/src/planner/nodes/returning-node.js.map +1 -1
- package/dist/src/planner/nodes/scalar.d.ts +20 -0
- package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
- package/dist/src/planner/nodes/scalar.js +71 -14
- package/dist/src/planner/nodes/scalar.js.map +1 -1
- package/dist/src/planner/nodes/set-object-tags-node.d.ts +39 -0
- package/dist/src/planner/nodes/set-object-tags-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/set-object-tags-node.js +41 -0
- package/dist/src/planner/nodes/set-object-tags-node.js.map +1 -0
- package/dist/src/planner/nodes/set-operation-node.d.ts +123 -1
- package/dist/src/planner/nodes/set-operation-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/set-operation-node.js +291 -18
- package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
- package/dist/src/planner/nodes/single-row.d.ts.map +1 -1
- package/dist/src/planner/nodes/single-row.js +3 -0
- package/dist/src/planner/nodes/single-row.js.map +1 -1
- package/dist/src/planner/nodes/sort.d.ts.map +1 -1
- package/dist/src/planner/nodes/sort.js +7 -6
- package/dist/src/planner/nodes/sort.js.map +1 -1
- package/dist/src/planner/nodes/subquery.d.ts +2 -0
- package/dist/src/planner/nodes/subquery.d.ts.map +1 -1
- package/dist/src/planner/nodes/subquery.js +18 -2
- package/dist/src/planner/nodes/subquery.js.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.js +23 -3
- package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
- package/dist/src/planner/nodes/table-function-call.js +6 -0
- package/dist/src/planner/nodes/table-function-call.js.map +1 -1
- package/dist/src/planner/nodes/values-node.d.ts +1 -0
- package/dist/src/planner/nodes/values-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/values-node.js +16 -6
- package/dist/src/planner/nodes/values-node.js.map +1 -1
- package/dist/src/planner/nodes/view-mutation-node.d.ts +259 -0
- package/dist/src/planner/nodes/view-mutation-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/view-mutation-node.js +273 -0
- package/dist/src/planner/nodes/view-mutation-node.js.map +1 -0
- package/dist/src/planner/nodes/window-function.d.ts +17 -1
- package/dist/src/planner/nodes/window-function.d.ts.map +1 -1
- package/dist/src/planner/nodes/window-function.js +15 -1
- package/dist/src/planner/nodes/window-function.js.map +1 -1
- package/dist/src/planner/nodes/window-node.js +2 -2
- package/dist/src/planner/nodes/window-node.js.map +1 -1
- package/dist/src/planner/optimizer.d.ts.map +1 -1
- package/dist/src/planner/optimizer.js +372 -39
- package/dist/src/planner/optimizer.js.map +1 -1
- package/dist/src/planner/planning-context.d.ts +1 -1
- package/dist/src/planner/planning-context.d.ts.map +1 -1
- package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts +70 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts.map +1 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.js +156 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.js.map +1 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts +31 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js +176 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js.map +1 -0
- package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
- package/dist/src/planner/rules/access/rule-select-access-path.js +435 -37
- package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.d.ts.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js +9 -0
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts +39 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts.map +1 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +616 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js.map +1 -0
- package/dist/src/planner/rules/cache/rule-scalar-cse.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-scalar-cse.js +8 -1
- package/dist/src/planner/rules/cache/rule-scalar-cse.js.map +1 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.d.ts +36 -0
- package/dist/src/planner/rules/join/equi-pair-extractor.d.ts.map +1 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.js +38 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.js.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +10 -0
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js +19 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js.map +1 -1
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts +130 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js +206 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js.map +1 -0
- package/dist/src/planner/rules/join/rule-join-elimination.d.ts +67 -14
- package/dist/src/planner/rules/join/rule-join-elimination.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-elimination.js +81 -25
- package/dist/src/planner/rules/join/rule-join-elimination.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts +84 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.js +138 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.js.map +1 -0
- package/dist/src/planner/rules/join/rule-join-greedy-commute.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js +9 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.js +12 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.js.map +1 -1
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.js +4 -0
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.js.map +1 -1
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.js +4 -0
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.js.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js +10 -0
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js.map +1 -1
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts +286 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js +548 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js.map +1 -0
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js +9 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js +7 -0
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js.map +1 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js +10 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js +9 -0
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js +18 -0
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js +7 -0
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js +9 -0
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js +13 -3
- package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.d.ts.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.js +14 -0
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.js.map +1 -1
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.d.ts +1 -1
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js +4 -4
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js +8 -0
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js +7 -0
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js +12 -0
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js.map +1 -1
- package/dist/src/planner/type-utils.d.ts +14 -0
- package/dist/src/planner/type-utils.d.ts.map +1 -1
- package/dist/src/planner/type-utils.js +66 -21
- package/dist/src/planner/type-utils.js.map +1 -1
- package/dist/src/planner/util/fd-utils.d.ts +177 -43
- package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
- package/dist/src/planner/util/fd-utils.js +396 -101
- package/dist/src/planner/util/fd-utils.js.map +1 -1
- package/dist/src/planner/util/ind-utils.d.ts +27 -1
- package/dist/src/planner/util/ind-utils.d.ts.map +1 -1
- package/dist/src/planner/util/ind-utils.js +80 -6
- package/dist/src/planner/util/ind-utils.js.map +1 -1
- package/dist/src/planner/util/key-utils.d.ts.map +1 -1
- package/dist/src/planner/util/key-utils.js +81 -12
- package/dist/src/planner/util/key-utils.js.map +1 -1
- package/dist/src/planner/util/set-op-wrapper.d.ts +37 -0
- package/dist/src/planner/util/set-op-wrapper.d.ts.map +1 -0
- package/dist/src/planner/util/set-op-wrapper.js +82 -0
- package/dist/src/planner/util/set-op-wrapper.js.map +1 -0
- package/dist/src/planner/validation/plan-validator.d.ts.map +1 -1
- package/dist/src/planner/validation/plan-validator.js +1 -0
- package/dist/src/planner/validation/plan-validator.js.map +1 -1
- package/dist/src/runtime/context-helpers.d.ts +13 -1
- package/dist/src/runtime/context-helpers.d.ts.map +1 -1
- package/dist/src/runtime/context-helpers.js +7 -1
- package/dist/src/runtime/context-helpers.js.map +1 -1
- package/dist/src/runtime/delta-executor.d.ts +30 -1
- package/dist/src/runtime/delta-executor.d.ts.map +1 -1
- package/dist/src/runtime/delta-executor.js +29 -4
- package/dist/src/runtime/delta-executor.js.map +1 -1
- package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
- package/dist/src/runtime/emit/add-constraint.js +38 -5
- package/dist/src/runtime/emit/add-constraint.js.map +1 -1
- package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
- package/dist/src/runtime/emit/aggregate.js +10 -8
- package/dist/src/runtime/emit/aggregate.js.map +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/alter-table.js +664 -108
- package/dist/src/runtime/emit/alter-table.js.map +1 -1
- package/dist/src/runtime/emit/analyze.d.ts.map +1 -1
- package/dist/src/runtime/emit/analyze.js +2 -1
- package/dist/src/runtime/emit/analyze.js.map +1 -1
- package/dist/src/runtime/emit/asof-scan.d.ts.map +1 -1
- package/dist/src/runtime/emit/asof-scan.js +18 -5
- package/dist/src/runtime/emit/asof-scan.js.map +1 -1
- package/dist/src/runtime/emit/asserted-keys.d.ts +13 -0
- package/dist/src/runtime/emit/asserted-keys.d.ts.map +1 -0
- package/dist/src/runtime/emit/asserted-keys.js +13 -0
- package/dist/src/runtime/emit/asserted-keys.js.map +1 -0
- package/dist/src/runtime/emit/between.d.ts.map +1 -1
- package/dist/src/runtime/emit/between.js +24 -19
- package/dist/src/runtime/emit/between.js.map +1 -1
- package/dist/src/runtime/emit/binary.d.ts.map +1 -1
- package/dist/src/runtime/emit/binary.js +5 -9
- package/dist/src/runtime/emit/binary.js.map +1 -1
- package/dist/src/runtime/emit/block.d.ts.map +1 -1
- package/dist/src/runtime/emit/block.js +11 -2
- package/dist/src/runtime/emit/block.js.map +1 -1
- package/dist/src/runtime/emit/bloom-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/bloom-join.js +8 -2
- package/dist/src/runtime/emit/bloom-join.js.map +1 -1
- package/dist/src/runtime/emit/constraint-check.js +15 -0
- package/dist/src/runtime/emit/constraint-check.js.map +1 -1
- package/dist/src/runtime/emit/create-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-table.js +8 -0
- package/dist/src/runtime/emit/create-table.js.map +1 -1
- package/dist/src/runtime/emit/create-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-view.js +16 -1
- package/dist/src/runtime/emit/create-view.js.map +1 -1
- package/dist/src/runtime/emit/dml-executor.d.ts +27 -0
- package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
- package/dist/src/runtime/emit/dml-executor.js +413 -193
- package/dist/src/runtime/emit/dml-executor.js.map +1 -1
- package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-table.js +10 -0
- package/dist/src/runtime/emit/drop-table.js.map +1 -1
- package/dist/src/runtime/emit/drop-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-view.js +17 -0
- package/dist/src/runtime/emit/drop-view.js.map +1 -1
- package/dist/src/runtime/emit/envelope-scan.d.ts +13 -0
- package/dist/src/runtime/emit/envelope-scan.d.ts.map +1 -0
- package/dist/src/runtime/emit/envelope-scan.js +22 -0
- package/dist/src/runtime/emit/envelope-scan.js.map +1 -0
- package/dist/src/runtime/emit/join.d.ts +10 -2
- package/dist/src/runtime/emit/join.d.ts.map +1 -1
- package/dist/src/runtime/emit/join.js +128 -38
- package/dist/src/runtime/emit/join.js.map +1 -1
- package/dist/src/runtime/emit/lens-auxiliary-access.d.ts +16 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.js +16 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.js.map +1 -0
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts +640 -0
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -0
- package/dist/src/runtime/emit/materialized-view-helpers.js +2576 -0
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -0
- package/dist/src/runtime/emit/materialized-view.d.ts +31 -0
- package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -0
- package/dist/src/runtime/emit/materialized-view.js +187 -0
- package/dist/src/runtime/emit/materialized-view.js.map +1 -0
- package/dist/src/runtime/emit/merge-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/merge-join.js +15 -3
- package/dist/src/runtime/emit/merge-join.js.map +1 -1
- package/dist/src/runtime/emit/project.d.ts.map +1 -1
- package/dist/src/runtime/emit/project.js +10 -5
- package/dist/src/runtime/emit/project.js.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.d.ts +1 -0
- package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.js +101 -5
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/runtime/emit/set-object-tags.d.ts +16 -0
- package/dist/src/runtime/emit/set-object-tags.d.ts.map +1 -0
- package/dist/src/runtime/emit/set-object-tags.js +57 -0
- package/dist/src/runtime/emit/set-object-tags.js.map +1 -0
- package/dist/src/runtime/emit/set-operation.d.ts.map +1 -1
- package/dist/src/runtime/emit/set-operation.js +140 -24
- package/dist/src/runtime/emit/set-operation.js.map +1 -1
- package/dist/src/runtime/emit/subquery.d.ts.map +1 -1
- package/dist/src/runtime/emit/subquery.js +110 -5
- package/dist/src/runtime/emit/subquery.js.map +1 -1
- package/dist/src/runtime/emit/unary.d.ts.map +1 -1
- package/dist/src/runtime/emit/unary.js +34 -6
- package/dist/src/runtime/emit/unary.js.map +1 -1
- package/dist/src/runtime/emit/view-mutation.d.ts +70 -0
- package/dist/src/runtime/emit/view-mutation.d.ts.map +1 -0
- package/dist/src/runtime/emit/view-mutation.js +299 -0
- package/dist/src/runtime/emit/view-mutation.js.map +1 -0
- package/dist/src/runtime/emit/window.js +29 -5
- package/dist/src/runtime/emit/window.js.map +1 -1
- package/dist/src/runtime/foreign-key-actions.d.ts +66 -3
- package/dist/src/runtime/foreign-key-actions.d.ts.map +1 -1
- package/dist/src/runtime/foreign-key-actions.js +580 -172
- package/dist/src/runtime/foreign-key-actions.js.map +1 -1
- package/dist/src/runtime/parallel-driver.d.ts +4 -1
- package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
- package/dist/src/runtime/parallel-driver.js +5 -1
- package/dist/src/runtime/parallel-driver.js.map +1 -1
- package/dist/src/runtime/register.d.ts.map +1 -1
- package/dist/src/runtime/register.js +17 -1
- package/dist/src/runtime/register.js.map +1 -1
- package/dist/src/runtime/types.d.ts +10 -0
- package/dist/src/runtime/types.d.ts.map +1 -1
- package/dist/src/runtime/types.js.map +1 -1
- package/dist/src/schema/basis-backfill.d.ts +63 -0
- package/dist/src/schema/basis-backfill.d.ts.map +1 -0
- package/dist/src/schema/basis-backfill.js +161 -0
- package/dist/src/schema/basis-backfill.js.map +1 -0
- package/dist/src/schema/catalog.d.ts +115 -1
- package/dist/src/schema/catalog.d.ts.map +1 -1
- package/dist/src/schema/catalog.js +249 -22
- package/dist/src/schema/catalog.js.map +1 -1
- package/dist/src/schema/change-events.d.ts +42 -1
- package/dist/src/schema/change-events.d.ts.map +1 -1
- package/dist/src/schema/change-events.js.map +1 -1
- package/dist/src/schema/column.d.ts +16 -0
- package/dist/src/schema/column.d.ts.map +1 -1
- package/dist/src/schema/column.js.map +1 -1
- package/dist/src/schema/constraint-builder.d.ts +182 -0
- package/dist/src/schema/constraint-builder.d.ts.map +1 -0
- package/dist/src/schema/constraint-builder.js +424 -0
- package/dist/src/schema/constraint-builder.js.map +1 -0
- package/dist/src/schema/ddl-generator.d.ts +86 -1
- package/dist/src/schema/ddl-generator.d.ts.map +1 -1
- package/dist/src/schema/ddl-generator.js +316 -20
- package/dist/src/schema/ddl-generator.js.map +1 -1
- package/dist/src/schema/declared-schema-manager.d.ts +51 -0
- package/dist/src/schema/declared-schema-manager.d.ts.map +1 -1
- package/dist/src/schema/declared-schema-manager.js +61 -0
- package/dist/src/schema/declared-schema-manager.js.map +1 -1
- package/dist/src/schema/derivation.d.ts +106 -0
- package/dist/src/schema/derivation.d.ts.map +1 -0
- package/dist/src/schema/derivation.js +25 -0
- package/dist/src/schema/derivation.js.map +1 -0
- package/dist/src/schema/function.d.ts +13 -0
- package/dist/src/schema/function.d.ts.map +1 -1
- package/dist/src/schema/function.js.map +1 -1
- package/dist/src/schema/lens-ack.d.ts +90 -0
- package/dist/src/schema/lens-ack.d.ts.map +1 -0
- package/dist/src/schema/lens-ack.js +361 -0
- package/dist/src/schema/lens-ack.js.map +1 -0
- package/dist/src/schema/lens-compiler.d.ts +62 -0
- package/dist/src/schema/lens-compiler.d.ts.map +1 -0
- package/dist/src/schema/lens-compiler.js +1594 -0
- package/dist/src/schema/lens-compiler.js.map +1 -0
- package/dist/src/schema/lens-fk-discovery.d.ts +175 -0
- package/dist/src/schema/lens-fk-discovery.d.ts.map +1 -0
- package/dist/src/schema/lens-fk-discovery.js +336 -0
- package/dist/src/schema/lens-fk-discovery.js.map +1 -0
- package/dist/src/schema/lens-prover.d.ts +336 -0
- package/dist/src/schema/lens-prover.d.ts.map +1 -0
- package/dist/src/schema/lens-prover.js +1988 -0
- package/dist/src/schema/lens-prover.js.map +1 -0
- package/dist/src/schema/lens.d.ts +254 -0
- package/dist/src/schema/lens.d.ts.map +1 -0
- package/dist/src/schema/lens.js +21 -0
- package/dist/src/schema/lens.js.map +1 -0
- package/dist/src/schema/manager.d.ts +676 -18
- package/dist/src/schema/manager.d.ts.map +1 -1
- package/dist/src/schema/manager.js +1573 -238
- package/dist/src/schema/manager.js.map +1 -1
- package/dist/src/schema/mapping-advertisement-tags.d.ts +39 -0
- package/dist/src/schema/mapping-advertisement-tags.d.ts.map +1 -0
- package/dist/src/schema/mapping-advertisement-tags.js +216 -0
- package/dist/src/schema/mapping-advertisement-tags.js.map +1 -0
- package/dist/src/schema/rename-rewriter.d.ts +45 -4
- package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
- package/dist/src/schema/rename-rewriter.js +412 -19
- package/dist/src/schema/rename-rewriter.js.map +1 -1
- package/dist/src/schema/reserved-tags-policy.d.ts +32 -0
- package/dist/src/schema/reserved-tags-policy.d.ts.map +1 -0
- package/dist/src/schema/reserved-tags-policy.js +34 -0
- package/dist/src/schema/reserved-tags-policy.js.map +1 -0
- package/dist/src/schema/reserved-tags.d.ts +170 -0
- package/dist/src/schema/reserved-tags.d.ts.map +1 -0
- package/dist/src/schema/reserved-tags.js +507 -0
- package/dist/src/schema/reserved-tags.js.map +1 -0
- package/dist/src/schema/schema-differ.d.ts +158 -2
- package/dist/src/schema/schema-differ.d.ts.map +1 -1
- package/dist/src/schema/schema-differ.js +1460 -78
- package/dist/src/schema/schema-differ.js.map +1 -1
- package/dist/src/schema/schema-hasher.d.ts +8 -3
- package/dist/src/schema/schema-hasher.d.ts.map +1 -1
- package/dist/src/schema/schema-hasher.js +22 -2
- package/dist/src/schema/schema-hasher.js.map +1 -1
- package/dist/src/schema/schema.d.ts +25 -1
- package/dist/src/schema/schema.d.ts.map +1 -1
- package/dist/src/schema/schema.js +36 -2
- package/dist/src/schema/schema.js.map +1 -1
- package/dist/src/schema/table.d.ts +259 -10
- package/dist/src/schema/table.d.ts.map +1 -1
- package/dist/src/schema/table.js +309 -26
- package/dist/src/schema/table.js.map +1 -1
- package/dist/src/schema/unique-enforcement.d.ts +78 -0
- package/dist/src/schema/unique-enforcement.d.ts.map +1 -0
- package/dist/src/schema/unique-enforcement.js +93 -0
- package/dist/src/schema/unique-enforcement.js.map +1 -0
- package/dist/src/schema/view.d.ts +83 -2
- package/dist/src/schema/view.d.ts.map +1 -1
- package/dist/src/schema/view.js +67 -1
- package/dist/src/schema/view.js.map +1 -1
- package/dist/src/schema/window-function.d.ts +9 -1
- package/dist/src/schema/window-function.d.ts.map +1 -1
- package/dist/src/schema/window-function.js.map +1 -1
- package/dist/src/util/comparison.d.ts +24 -0
- package/dist/src/util/comparison.d.ts.map +1 -1
- package/dist/src/util/comparison.js +34 -0
- package/dist/src/util/comparison.js.map +1 -1
- package/dist/src/util/mutation-statement.d.ts.map +1 -1
- package/dist/src/util/mutation-statement.js +4 -1
- package/dist/src/util/mutation-statement.js.map +1 -1
- package/dist/src/util/serialization.d.ts +9 -0
- package/dist/src/util/serialization.d.ts.map +1 -1
- package/dist/src/util/serialization.js +26 -0
- package/dist/src/util/serialization.js.map +1 -1
- package/dist/src/vtab/backing-host.d.ts +286 -0
- package/dist/src/vtab/backing-host.d.ts.map +1 -0
- package/dist/src/vtab/backing-host.js +118 -0
- package/dist/src/vtab/backing-host.js.map +1 -0
- package/dist/src/vtab/best-access-plan.d.ts +21 -0
- package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
- package/dist/src/vtab/best-access-plan.js.map +1 -1
- package/dist/src/vtab/capabilities.d.ts +5 -5
- package/dist/src/vtab/capabilities.d.ts.map +1 -1
- package/dist/src/vtab/mapping-advertisement.d.ts +163 -0
- package/dist/src/vtab/mapping-advertisement.d.ts.map +1 -0
- package/dist/src/vtab/mapping-advertisement.js +2 -0
- package/dist/src/vtab/mapping-advertisement.js.map +1 -0
- package/dist/src/vtab/memory/index.d.ts +64 -4
- package/dist/src/vtab/memory/index.d.ts.map +1 -1
- package/dist/src/vtab/memory/index.js +119 -12
- package/dist/src/vtab/memory/index.js.map +1 -1
- package/dist/src/vtab/memory/layer/base.d.ts +38 -1
- package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/base.js +112 -24
- package/dist/src/vtab/memory/layer/base.js.map +1 -1
- package/dist/src/vtab/memory/layer/manager.d.ts +291 -4
- package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.js +1050 -91
- package/dist/src/vtab/memory/layer/manager.js.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.js +35 -6
- package/dist/src/vtab/memory/layer/plan-filter.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.js +66 -14
- package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.d.ts +14 -0
- package/dist/src/vtab/memory/layer/scan-plan.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.js +27 -4
- package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.js +5 -1
- package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
- package/dist/src/vtab/memory/module.d.ts +17 -0
- package/dist/src/vtab/memory/module.d.ts.map +1 -1
- package/dist/src/vtab/memory/module.js +82 -3
- package/dist/src/vtab/memory/module.js.map +1 -1
- package/dist/src/vtab/memory/table.d.ts.map +1 -1
- package/dist/src/vtab/memory/table.js +15 -5
- package/dist/src/vtab/memory/table.js.map +1 -1
- package/dist/src/vtab/memory/types.d.ts +20 -2
- package/dist/src/vtab/memory/types.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.js +46 -24
- package/dist/src/vtab/memory/utils/predicate.js.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key-encode.d.ts +31 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.d.ts.map +1 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.js +101 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.js.map +1 -0
- package/dist/src/vtab/memory/utils/primary-key.d.ts +8 -0
- package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key.js +12 -5
- package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
- package/dist/src/vtab/module.d.ts +203 -4
- package/dist/src/vtab/module.d.ts.map +1 -1
- package/dist/src/vtab/table.d.ts +9 -0
- package/dist/src/vtab/table.d.ts.map +1 -1
- package/dist/src/vtab/table.js.map +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,2915 @@
|
|
|
1
|
+
import { resolveReferencedColumns } from '../../schema/table.js';
|
|
2
|
+
import { PlanNode } from '../nodes/plan-node.js';
|
|
3
|
+
import { sqlValuesEqual } from '../../util/comparison.js';
|
|
4
|
+
import { TableReferenceNode, ColumnReferenceNode } from '../nodes/reference.js';
|
|
5
|
+
import { InternalRecursiveCTERefNode } from '../nodes/internal-recursive-cte-ref-node.js';
|
|
6
|
+
import { analyzeBodyLineage } from './backward-body.js';
|
|
7
|
+
import { buildExpression } from '../building/expression.js';
|
|
8
|
+
import { columnSchemaToScalarType } from '../type-utils.js';
|
|
9
|
+
import { JoinNode } from '../nodes/join-node.js';
|
|
10
|
+
import { EXISTENCE_FLAG_TYPE } from '../nodes/join-utils.js';
|
|
11
|
+
import { FilterNode } from '../nodes/filter.js';
|
|
12
|
+
import { ProjectNode } from '../nodes/project-node.js';
|
|
13
|
+
import { raiseMutationDiagnostic } from './mutation-diagnostic.js';
|
|
14
|
+
import { combineAnd, flattenAnd, makeViewColumnDescend, assertTopLevelViewColumns, raiseUnknownViewColumn, SELF_ALIAS, assertReturningStarQualifier } from './single-source.js';
|
|
15
|
+
import { transformExpr, cloneExpr, mapQueryExprUniform, substituteNewRefs, transformScopedExpr, transformAliasScopedExpr } from './scope-transform.js';
|
|
16
|
+
import { requireValidatedNewRefIndex } from '../analysis/authored-inverse.js';
|
|
17
|
+
/**
|
|
18
|
+
* Multi-source view-mediated DML decomposition — the **key-preserving join**
|
|
19
|
+
* acceptance case of the view-mutation substrate (docs/view-updateability.md
|
|
20
|
+
* § Per-Operator Semantics — Inner Join, § Outer Joins, § Multi-Base-Table Mutations).
|
|
21
|
+
*
|
|
22
|
+
* Scope: a view body that is an **n-way (≥2) equi-join** of base tables — `inner`,
|
|
23
|
+
* `left`, or `full` — including composite-PK sides and **self-joins** (one base table
|
|
24
|
+
* under two or more distinct aliases) — written through with `update` / `delete` /
|
|
25
|
+
* `insert`. **LEFT** and **RIGHT** outer joins are admitted for the
|
|
26
|
+
* **statically-expressible** cases: preserved-side update passthrough,
|
|
27
|
+
* delete-to-the-preserved-side, and insert routing (both-side / preserved-only /
|
|
28
|
+
* presence-gated non-preserved member). RIGHT is the exact **mirror** of LEFT — the
|
|
29
|
+
* right operand of a `right` join is preserved, the left operand is null-extended — and
|
|
30
|
+
* the runtime now executes a RIGHT join (`outer-join-right-full-runtime`), so a
|
|
31
|
+
* RIGHT-join view is both readable and writable. **FULL** is admitted only to carry its
|
|
32
|
+
* precise conservative diagnostics (no preserved side, so all writes reject and the
|
|
33
|
+
* surfaces report all-`NO`; FULL write-through — a preserved anchor for a body that is
|
|
34
|
+
* null-extended per row — is a separable future concern). The one outer-join case that
|
|
35
|
+
* needs new runtime — an UPDATE of a **non-preserved** column (a per-row matched-update /
|
|
36
|
+
* null-extended-insert branch) — defers with `unsupported-outer-join-update`
|
|
37
|
+
* (`view-write-optional-member-transitions`). The body is
|
|
38
|
+
* **planned once**
|
|
39
|
+
* (`analyzeJoinView`); its `PhysicalProperties.updateLineage` (threaded by
|
|
40
|
+
* `view-mutation-physical-lineage`) routes each output column to its owning base
|
|
41
|
+
* table. Each per-base SET/value is still lowered to an AST `BaseOp` so the ordinary
|
|
42
|
+
* base-table builders are reused verbatim (the documented lower-risk path — the base
|
|
43
|
+
* builders stay untouched), but **row identification no longer round-trips through a
|
|
44
|
+
* re-planned AST body**: every affected view row's base-PK identities are captured
|
|
45
|
+
* ONCE up-front, built as plan nodes directly over the already-planned join body
|
|
46
|
+
* (`Project_{k<side>}(Filter_{idPred}(joinNode))` — the derived backward walk the
|
|
47
|
+
* docs name, § Round-Trip Laws and the Derived Backward Walk), materialized before
|
|
48
|
+
* any base op fires, and each base op reads its identifying values back from that
|
|
49
|
+
* `__vmupd_keys` set:
|
|
50
|
+
*
|
|
51
|
+
* ```sql
|
|
52
|
+
* -- view: select j1.id as id, j1.a as a, j2.c as c
|
|
53
|
+
* -- from tj1 j1 join tj2 j2 on j2.id = j1.t2id
|
|
54
|
+
* update jv set a = 5, c = 9 where id = 3
|
|
55
|
+
* -- capture (plan nodes over the planned join body, materialized once):
|
|
56
|
+
* -- __vmupd_keys = π_{j1.id as k0, j2.id as k1}( σ_{id = 3}( tj1 ⋈ tj2 ) )
|
|
57
|
+
* -> update tj1 set a = 5 where id in (select k0 from __vmupd_keys)
|
|
58
|
+
* update tj2 set c = 9 where id in (select k1 from __vmupd_keys)
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* The capture reconstructs the row-identifying predicate (each owning side's base PK)
|
|
62
|
+
* from the planned join body — exactly the predicate the optimizer already proves a
|
|
63
|
+
* key over — so a side whose own PK is hidden by the projection (`tj2.id` above) is
|
|
64
|
+
* still addressable, and a both-sides write is mutation-order-independent (the
|
|
65
|
+
* FK-parent op cannot rewrite a predicate column out from under the FK-child op).
|
|
66
|
+
* UPDATE RETURNING re-queries the same planned `joinNode` (post-mutation, restricted
|
|
67
|
+
* to the captured identities); DELETE RETURNING projects the planned body `root` (the
|
|
68
|
+
* `pre` OLD image). The body is planned once and reused — no second `buildSelectStmt`
|
|
69
|
+
* / `cloneFromClause` of it for identification or RETURNING.
|
|
70
|
+
*
|
|
71
|
+
* Multi-source **`insert`** is analysed here (`analyzeMultiSourceInsert`) but *built* by
|
|
72
|
+
* `building/view-mutation-builder.ts` (`buildMultiSourceInsert`), because it needs the
|
|
73
|
+
* plan-level shared-surrogate envelope rather than an AST `BaseOp`: the shared join key is
|
|
74
|
+
* not a view column, so it is sourced from the anchor key column's declared `default` once
|
|
75
|
+
* per row at the envelope and threaded into the active base inserts via the equivalence
|
|
76
|
+
* class (§ Mutation Context). For an outer join the **non-preserved** side is an *optional*
|
|
77
|
+
* member of the fan-out — dropped when its columns are absent (the preserved-only insert),
|
|
78
|
+
* presence-gated per row when supplied (the both-side insert).
|
|
79
|
+
*
|
|
80
|
+
* **Deferred, rejected here with a structured diagnostic:**
|
|
81
|
+
* - UPDATE of a non-preserved outer-join column — `unsupported-outer-join-update`.
|
|
82
|
+
* - INSERT of only non-preserved columns with no preserved anchor — `null-extended-create-conflict`.
|
|
83
|
+
* - cross / set-op / aggregate / window bodies — `unsupported-*`.
|
|
84
|
+
* - comma (implicit) joins, `select *` join bodies, and cross-side `set` value
|
|
85
|
+
* references — each a precise diagnostic.
|
|
86
|
+
* - composite **shared-key insert** (the surrogate envelope threads a single-column
|
|
87
|
+
* key) — `unsupported-decomposition-key`. (Composite-PK *identification* on the
|
|
88
|
+
* update/delete capture path IS supported here; only the insert envelope's shared
|
|
89
|
+
* key stays single-column.)
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* The shared identity-capture CTE name for a multi-source (n-way inner-join) UPDATE /
|
|
93
|
+
* multi-side DELETE fan-out. Each affected view row's base-PK identities are
|
|
94
|
+
* materialized ONCE — *before* any base op fires — into `rctx.tableContexts` under a
|
|
95
|
+
* shared descriptor. *Every* per-side base op reads its identifying values back from
|
|
96
|
+
* this set via a correlated EXISTS (`exists (select 1 from __vmupd_keys k where
|
|
97
|
+
* k.k<side>_<j> = <side>.<pk<j>> …)`) instead of a live re-query of the join body, so
|
|
98
|
+
* the first op cannot empty the join — or rewrite a predicate column — out from under
|
|
99
|
+
* a later op's identifying subquery (a mutation-order-independent identity). The same
|
|
100
|
+
* capture backs the UPDATE RETURNING re-query (docs/view-updateability.md § Inner Join,
|
|
101
|
+
* § `returning`).
|
|
102
|
+
*
|
|
103
|
+
* The capture relation carries one column **per side per PK column**, named
|
|
104
|
+
* `k<sideIndex>_<pkColumnOrdinal>` ({@link keyColumnName}) — so a composite-PK side
|
|
105
|
+
* contributes `k<side>_0, k<side>_1, …`. This flattened per-side-per-column shape is
|
|
106
|
+
* what generalizes the substrate past the retired single-column `(k0, k1)` tuple.
|
|
107
|
+
*/
|
|
108
|
+
export const MS_UPDATE_KEYS_CTE = '__vmupd_keys';
|
|
109
|
+
/**
|
|
110
|
+
* The capture column name for side `sideIndex`'s `j`-th PK column. A single-column-PK
|
|
111
|
+
* side yields just `k<side>_0`; a composite-PK side yields `k<side>_0, k<side>_1, …`.
|
|
112
|
+
*/
|
|
113
|
+
export function keyColumnName(sideIndex, j) {
|
|
114
|
+
return `k${sideIndex}_${j}`;
|
|
115
|
+
}
|
|
116
|
+
// --- entry ----------------------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* True when the view body is a join (and so routes to this multi-source path
|
|
119
|
+
* rather than the single-source spine). Cheap AST peek — no plan built.
|
|
120
|
+
*
|
|
121
|
+
* A **compound (set-op) body** returns `false` even when its left-most leg's FROM is a
|
|
122
|
+
* join (`select … from a join b … union …`): a set-op body routes to the set-op write path
|
|
123
|
+
* (`set-op.ts`), never this multi-source join spine, whose capture/lineage walk reads only
|
|
124
|
+
* the leg's `JoinNode` and silently ignores the surrounding compound — mishandling it
|
|
125
|
+
* (`set-op-write-multisource-leg-reject`). Excluding it here lets such a body fall through to
|
|
126
|
+
* the single-source spine's clean `classifyViewBody` reject (the intent
|
|
127
|
+
* `propagate.ts` already documents). The set-op path itself detects a multi-source LEG by
|
|
128
|
+
* calling this same predicate on the **leg** SELECT (which carries no `compound`).
|
|
129
|
+
*/
|
|
130
|
+
export function isJoinBody(selectAst) {
|
|
131
|
+
if (selectAst.type !== 'select' || !selectAst.from)
|
|
132
|
+
return false;
|
|
133
|
+
if (selectAst.compound)
|
|
134
|
+
return false;
|
|
135
|
+
return selectAst.from.length > 1 || selectAst.from.some(f => f.type === 'join');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Non-throwing AST shape check — the boolean shadow of {@link collectJoinSources}'s
|
|
139
|
+
* acceptance: `true` iff the body is a single explicit **n-way (≥2) equi-join** — `inner`,
|
|
140
|
+
* `left`, `right`, or `full` (RIGHT is the exact mirror of LEFT; FULL has no preserved
|
|
141
|
+
* side, so it self-conservatizes downstream; see {@link collectJoinSources}) with an ON
|
|
142
|
+
* (or USING) predicate over plain base tables (the
|
|
143
|
+
* exact multi-source shape `propagate()` decomposes), including **composite-PK sides** and
|
|
144
|
+
* **self-joins** (one base table under two or more distinct aliases). Every other
|
|
145
|
+
* multi-table body — cross / comma (implicit) / subquery- or function-source — returns
|
|
146
|
+
* `false`.
|
|
147
|
+
*
|
|
148
|
+
* Shared with the static updateability surfaces (`deriveViewInfo` /
|
|
149
|
+
* `deriveColumnInfo` in `func/builtins/schema.ts`): they gate on this so they
|
|
150
|
+
* agree with what a real mutation through the view accepts. An outer join is now
|
|
151
|
+
* **partially** writable (preserved-side update, delete-to-preserved, insert), so it is
|
|
152
|
+
* decomposable here; those surfaces read per-column `null-extended` lineage to report a
|
|
153
|
+
* non-preserved column non-updatable (the matching deferral). The throwing
|
|
154
|
+
* {@link collectJoinSources} stays the substrate's source of truth; this mirrors only its
|
|
155
|
+
* AST-level shape gate (it does not re-check DISTINCT/LIMIT/`select *`, which are deeper
|
|
156
|
+
* semantic rejects handled downstream — PK shape is no longer a reject now that composite
|
|
157
|
+
* keys are admitted).
|
|
158
|
+
*/
|
|
159
|
+
export function isDecomposableJoinBody(selectAst) {
|
|
160
|
+
if (selectAst.type !== 'select' || !selectAst.from)
|
|
161
|
+
return false;
|
|
162
|
+
const from = selectAst.from;
|
|
163
|
+
if (from.length !== 1 || from[0].type !== 'join')
|
|
164
|
+
return false;
|
|
165
|
+
let tableCount = 0;
|
|
166
|
+
const visit = (fc) => {
|
|
167
|
+
switch (fc.type) {
|
|
168
|
+
case 'table':
|
|
169
|
+
tableCount += 1;
|
|
170
|
+
return true;
|
|
171
|
+
case 'join': {
|
|
172
|
+
// INNER / LEFT / RIGHT / FULL join with an explicit ON predicate or a USING column
|
|
173
|
+
// list. RIGHT is now **admitted**: the runtime reads a RIGHT join and write-through
|
|
174
|
+
// recognition mirrors LEFT (the right of a `right` is preserved, the left
|
|
175
|
+
// null-extended; `view-write-right-join-readmit`). FULL is admitted but has no
|
|
176
|
+
// preserved side, so it self-conservatizes downstream (no false positive); FULL
|
|
177
|
+
// write-through is a separable future concern.
|
|
178
|
+
const accepted = fc.joinType === 'inner' || fc.joinType === 'left' || fc.joinType === 'right' || fc.joinType === 'full';
|
|
179
|
+
if (!accepted || (!fc.condition && !(fc.columns && fc.columns.length > 0)))
|
|
180
|
+
return false;
|
|
181
|
+
return visit(fc.left) && visit(fc.right);
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
return false; // subquery / function source — not a plain base table
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
if (!visit(from[0]))
|
|
188
|
+
return false;
|
|
189
|
+
// ≥2 plain base tables. A self-join (the same base table under distinct aliases) is
|
|
190
|
+
// now accepted; routing is alias-keyed downstream, so the table names need not be
|
|
191
|
+
// distinct here.
|
|
192
|
+
return tableCount >= 2;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* True iff `selectAst` is a decomposable join body ({@link isDecomposableJoinBody}) whose
|
|
196
|
+
* joins are **all INNER**. The set-op join-leg compose
|
|
197
|
+
* (`set-op-write-multisource-leg-compose`) ships INNER join legs — the gate is purely
|
|
198
|
+
* `joinType`-based, so a non-equi (theta) INNER join is admitted identically to an equi-join.
|
|
199
|
+
* An OUTER (left/right/full) join leg's set-op write composition is deferred. The set-op
|
|
200
|
+
* recognizers gate on this so a body with an outer-join leg reports the conservative
|
|
201
|
+
* all-`NO` static surface AND rejects the dynamic write cleanly (never an internal error),
|
|
202
|
+
* matching exactly. A cross / no-ON join (not decomposable) likewise fails this and is
|
|
203
|
+
* deferred — agreeing with `analyzeJoinView`'s downstream reject.
|
|
204
|
+
*/
|
|
205
|
+
export function isInnerJoinBody(selectAst) {
|
|
206
|
+
if (!isDecomposableJoinBody(selectAst))
|
|
207
|
+
return false;
|
|
208
|
+
const from = selectAst.from;
|
|
209
|
+
const allInner = (fc) => fc.type !== 'join' || (fc.joinType === 'inner' && allInner(fc.left) && allInner(fc.right));
|
|
210
|
+
return allInner(from[0]);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Decompose a multi-source (n-way `inner`/`left`/`full` join) view mutation into
|
|
214
|
+
* an ordered `BaseOp[]`. Throws a structured diagnostic for any unsupported shape.
|
|
215
|
+
*/
|
|
216
|
+
export function propagateMultiSource(ctx, view, req) {
|
|
217
|
+
// Validate the join shape first (this rejects cross/comma joins, non-table sources,
|
|
218
|
+
// etc. with a `cannot write through view` diagnostic), so every unsupported join —
|
|
219
|
+
// including an `insert` through one — surfaces the precise shape reason before the
|
|
220
|
+
// op-specific handling. Outer joins are admitted: an UPDATE of a non-preserved column
|
|
221
|
+
// defers (`unsupported-outer-join-update`), DELETE routes to the preserved side(s).
|
|
222
|
+
const analysis = analyzeJoinView(ctx, view);
|
|
223
|
+
switch (req.op) {
|
|
224
|
+
case 'update': return decomposeUpdate(ctx, view, analysis, req.stmt);
|
|
225
|
+
case 'delete': return decomposeDelete(ctx, view, analysis, req.stmt);
|
|
226
|
+
case 'insert':
|
|
227
|
+
// Insert needs the plan-level shared-surrogate envelope, so it is built
|
|
228
|
+
// directly by `building/view-mutation-builder.ts` (`buildMultiSourceInsert`,
|
|
229
|
+
// off `analyzeMultiSourceInsert` below), not lowered to AST BaseOps here.
|
|
230
|
+
// `buildViewMutation` routes a join insert there before `propagate` runs,
|
|
231
|
+
// so this case is unreachable on the supported path.
|
|
232
|
+
raiseMutationDiagnostic({
|
|
233
|
+
reason: 'unsupported-multisource-insert',
|
|
234
|
+
table: view.name,
|
|
235
|
+
message: `internal: multi-source insert must be built via buildMultiSourceInsert, not propagate`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Decompose an n-way key-preserving INSERT into the per-side base inserts plus the
|
|
241
|
+
* shared-surrogate envelope they fan out from. Throws a structured diagnostic for any
|
|
242
|
+
* unsupported shape (computed target column, a not-null base column with no value, a
|
|
243
|
+
* non-equi-join key, …). The shared key remains **single-column**: a side contributing a
|
|
244
|
+
* composite shared key to the join's equivalence class is rejected with
|
|
245
|
+
* `unsupported-decomposition-key` (the envelope threads one key value per row).
|
|
246
|
+
*
|
|
247
|
+
* **Outer joins** (§ Outer Joins — Inserts): a **non-preserved** side is an *optional*
|
|
248
|
+
* member of the fan-out. A side whose columns are all absent emits no insert (it is
|
|
249
|
+
* dropped); a non-preserved side that IS supplied is presence-gated per row (it inserts
|
|
250
|
+
* only for rows supplying ≥1 of its columns). The shared key is minted/threaded only when
|
|
251
|
+
* ≥2 sides are active (the preserved-only case is a single preserved insert — the row
|
|
252
|
+
* reads back null-extended); an insert supplying *only* non-preserved columns, with no
|
|
253
|
+
* preserved anchor row to attach to, is rejected `null-extended-create-conflict` (v1).
|
|
254
|
+
*/
|
|
255
|
+
export function analyzeMultiSourceInsert(ctx, view, stmt) {
|
|
256
|
+
rejectReturning(view, stmt.returning);
|
|
257
|
+
const analysis = analyzeJoinView(ctx, view);
|
|
258
|
+
const { sides, outColumns } = analysis;
|
|
259
|
+
const keyColumns = extractJoinKeyColumns(view, analysis.sel, sides);
|
|
260
|
+
// Constant-FD insert-defaults from the join body's σ (where-clause): each `column =
|
|
261
|
+
// literal` conjunct, resolved to its owning join side, is lifted as a per-side
|
|
262
|
+
// insert-default (the side-aware analog of single-source `extractFilterConstants`). An
|
|
263
|
+
// omitted σ-constrained column is then supplied its σ constant so the inserted row
|
|
264
|
+
// satisfies the view predicate and is visible through the view; an explicit value that
|
|
265
|
+
// contradicts the constant rejects at plan time below (§ Inner Join — Inserts).
|
|
266
|
+
const filterConstants = extractJoinFilterConstants(analysis.sel.where, sides);
|
|
267
|
+
// Supplied view columns: the explicit list, or every base-routed (identity, rename,
|
|
268
|
+
// or outer-join null-extended) view output column. An `inverse`-profile column
|
|
269
|
+
// (writable on the UPDATE path) is NOT insertable — the shared-surrogate envelope
|
|
270
|
+
// writes supplied values to base columns verbatim, with no hook to apply the column's
|
|
271
|
+
// inverse, so an inserted `cv1` would land raw in `cv`. Excluding it from the implicit
|
|
272
|
+
// set lets it fall to its base default / not-null check; an explicit supply is
|
|
273
|
+
// rejected below. A non-preserved (null-extended) base column IS insertable here (the
|
|
274
|
+
// both-sides envelope supplies it), so it is included even though it is read-only on
|
|
275
|
+
// the UPDATE path.
|
|
276
|
+
const suppliedNames = stmt.columns && stmt.columns.length > 0
|
|
277
|
+
? stmt.columns
|
|
278
|
+
: outColumns.filter(c => c.sideIndex !== undefined && c.baseColumn !== undefined && !c.inverse).map(c => c.name);
|
|
279
|
+
const supplied = suppliedNames.map((rawName, columnIndex) => {
|
|
280
|
+
const name = rawName.toLowerCase();
|
|
281
|
+
const out = outColumns.find(c => c.name === name);
|
|
282
|
+
// An existence flag is consumed as a routing directive, not stored. Pull its uniform
|
|
283
|
+
// boolean literal out of the VALUES source (`true` ⇒ insert the non-preserved side;
|
|
284
|
+
// `false` ⇒ omit it / preserved-only). It stays an envelope column for arity but is
|
|
285
|
+
// never a base target (no `sideIndex`/`baseColumn`).
|
|
286
|
+
if (out?.existenceComponent) {
|
|
287
|
+
if (out.existenceSide === undefined) {
|
|
288
|
+
raiseMutationDiagnostic({
|
|
289
|
+
reason: 'unsupported-outer-join-update',
|
|
290
|
+
column: rawName,
|
|
291
|
+
table: view.name,
|
|
292
|
+
message: `cannot insert through view '${view.name}': the existence column '${rawName}' does not resolve to a single non-preserved side (an ambiguous / full-outer existence shape is deferred)`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const existenceFlag = existenceInsertFlag(view, stmt, columnIndex, rawName);
|
|
296
|
+
return { name, type: EXISTENCE_FLAG_TYPE, isKey: false, existenceSide: out.existenceSide, existenceFlag };
|
|
297
|
+
}
|
|
298
|
+
// Evaluating an authored (`with inverse`) column's puts through the multi-source
|
|
299
|
+
// shared-surrogate envelope is deferred (the envelope projects supplied columns
|
|
300
|
+
// verbatim per side; per-row put evaluation over it is a follow-up — recorded in
|
|
301
|
+
// docs/view-updateability.md § Authored inverses). Name the deferral precisely
|
|
302
|
+
// rather than letting it fall into the generic non-insertable reject below.
|
|
303
|
+
if (out?.authored) {
|
|
304
|
+
raiseMutationDiagnostic({
|
|
305
|
+
reason: 'no-inverse',
|
|
306
|
+
column: rawName,
|
|
307
|
+
table: view.name,
|
|
308
|
+
message: `cannot insert through view '${view.name}': column '${rawName}' carries an authored inverse (WITH INVERSE); evaluating authored puts through a join view's insert envelope is deferred — insert into the base tables directly`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// A base-routed column (identity/rename or outer-join null-extended) carries
|
|
312
|
+
// `sideIndex` + `baseColumn`; a computed column does not, and an `inverse` column
|
|
313
|
+
// cannot store a raw value. Either of the latter is non-insertable.
|
|
314
|
+
if (!out || out.inverse || out.sideIndex === undefined || !out.baseColumn) {
|
|
315
|
+
raiseMutationDiagnostic({
|
|
316
|
+
reason: 'no-inverse',
|
|
317
|
+
column: rawName,
|
|
318
|
+
table: view.name,
|
|
319
|
+
message: `cannot insert through view '${view.name}': column '${rawName}' is computed (non-invertible), a transformed (invertible) column, or not a base column, so it cannot receive an inserted value`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const sideIndex = out.sideIndex;
|
|
323
|
+
const baseCol = columnByName(sides[sideIndex].schema, out.baseColumn);
|
|
324
|
+
const isKey = out.baseColumn.toLowerCase() === keyColumns[sideIndex].toLowerCase();
|
|
325
|
+
return { name, sideIndex, baseColumn: out.baseColumn, type: columnSchemaToScalarType(baseCol), isKey };
|
|
326
|
+
});
|
|
327
|
+
// A FULL outer join has no preserved anchor side to mint/thread the shared key from
|
|
328
|
+
// (every side is null-extended per row), so a statically-routed insert is not
|
|
329
|
+
// expressible — defer it (the static `view_info` surface short-circuits the same body
|
|
330
|
+
// to all-`NO`).
|
|
331
|
+
const hasPreservedSide = sides.some(s => s.preserved);
|
|
332
|
+
if (!hasPreservedSide) {
|
|
333
|
+
raiseMutationDiagnostic({
|
|
334
|
+
reason: 'unsupported-join',
|
|
335
|
+
table: view.name,
|
|
336
|
+
message: `cannot insert through view '${view.name}': a FULL outer join has no preserved anchor side to mint/thread the shared key from; inserting through a full-outer view is deferred`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Existence directives (§ Existence columns — Inserts): an `exists … as` flag forces its
|
|
340
|
+
// non-preserved side active (`true`) or inactive (`false`), overriding the columns-
|
|
341
|
+
// supplied inference. A `false` directive on a side whose columns ARE supplied, or a
|
|
342
|
+
// `true`+`false` collision, contradicts — reject rather than silently pick one.
|
|
343
|
+
const forcedActive = new Set();
|
|
344
|
+
const forcedInactive = new Set();
|
|
345
|
+
for (const s of supplied) {
|
|
346
|
+
if (s.existenceSide === undefined)
|
|
347
|
+
continue;
|
|
348
|
+
(s.existenceFlag ? forcedActive : forcedInactive).add(s.existenceSide);
|
|
349
|
+
}
|
|
350
|
+
const baseSupplied = supplied.filter((s) => s.sideIndex !== undefined);
|
|
351
|
+
const suppliedSides = new Set(baseSupplied.map(s => s.sideIndex));
|
|
352
|
+
for (const i of forcedInactive) {
|
|
353
|
+
if (forcedActive.has(i) || suppliedSides.has(i)) {
|
|
354
|
+
raiseMutationDiagnostic({
|
|
355
|
+
reason: 'conflicting-assignment',
|
|
356
|
+
table: view.name,
|
|
357
|
+
message: `cannot insert through view '${view.name}': an existence flag is false (omit base table '${sides[i].schema.name}') but the same insert ${forcedActive.has(i) ? 'also sets that flag true' : 'supplies one of its columns'} — the two contradict`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Active sides: a preserved (or inner) side is always inserted (the anchor row); a
|
|
362
|
+
// non-preserved (outer) side is active when ≥1 of its columns is supplied OR an
|
|
363
|
+
// existence flag forces it (`true`). A `false` directive forces it inactive even if a
|
|
364
|
+
// stray column slipped through (already rejected above). An absent non-preserved side
|
|
365
|
+
// emits no insert (the per-row null-extension semantics).
|
|
366
|
+
const isActive = (i) => forcedInactive.has(i) ? false : (forcedActive.has(i) || sides[i].preserved || suppliedSides.has(i));
|
|
367
|
+
const activeIndices = sides.map((_, i) => i).filter(isActive);
|
|
368
|
+
// Non-preserved-only reject (§ Outer Joins — Inserts): activating only a non-preserved
|
|
369
|
+
// side (columns supplied or `hasB = true`), with no preserved anchor row to mint/thread
|
|
370
|
+
// the shared key from, is not yet expressible (the envelope sources the key from the
|
|
371
|
+
// preserved anchor).
|
|
372
|
+
const anyNonPreservedActive = sides.some((s, i) => !s.preserved && isActive(i));
|
|
373
|
+
const anyPreservedSupplied = baseSupplied.some(s => sides[s.sideIndex].preserved);
|
|
374
|
+
if (anyNonPreservedActive && !anyPreservedSupplied) {
|
|
375
|
+
raiseMutationDiagnostic({
|
|
376
|
+
reason: 'null-extended-create-conflict',
|
|
377
|
+
table: view.name,
|
|
378
|
+
message: `cannot insert through view '${view.name}': only non-preserved-side columns were supplied through the outer join, with no preserved-side row to attach to; supply the preserved side's columns too (the shared key is minted/threaded from the preserved anchor)`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
// The shared key relates two or more active sides; with only one active side (the
|
|
382
|
+
// preserved-only insert) no key is needed — the single side inserts and the row reads
|
|
383
|
+
// back null-extended.
|
|
384
|
+
const needsSharedKey = activeIndices.length >= 2;
|
|
385
|
+
// The shared key is either directly supplied (a supplied view column maps to a
|
|
386
|
+
// join-key base column) or sourced from the anchor key column's declared `default`,
|
|
387
|
+
// evaluated once per row at the envelope and EC-threaded into the active sides. The
|
|
388
|
+
// engine mints nothing of its own — the basis author declares the policy
|
|
389
|
+
// (docs/view-updateability.md § Mutation Context).
|
|
390
|
+
const suppliedKeys = supplied.filter(s => s.isKey);
|
|
391
|
+
if (suppliedKeys.length > 1) {
|
|
392
|
+
raiseMutationDiagnostic({
|
|
393
|
+
reason: 'unsupported-join',
|
|
394
|
+
table: view.name,
|
|
395
|
+
message: `cannot insert through view '${view.name}': the shared join key is exposed by more than one view column (${suppliedKeys.map(s => `'${s.name}'`).join(', ')}); supply it through a single view column`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const suppliedKeyIndex = supplied.findIndex(s => s.isKey);
|
|
399
|
+
let keyDefault;
|
|
400
|
+
let keyEnvelopeIndex = -1;
|
|
401
|
+
if (needsSharedKey) {
|
|
402
|
+
if (suppliedKeyIndex >= 0) {
|
|
403
|
+
keyEnvelopeIndex = suppliedKeyIndex;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
keyEnvelopeIndex = supplied.length; // the default-sourced column is appended last
|
|
407
|
+
// The anchor is the FK-root among the **active** sides (so a dropped optional
|
|
408
|
+
// member never seeds the surrogate); its key column's declared default sources
|
|
409
|
+
// the minted value.
|
|
410
|
+
const anchorIndex = orderSides(sides).find(isActive);
|
|
411
|
+
const anchorKeyCol = columnByName(sides[anchorIndex].schema, keyColumns[anchorIndex]);
|
|
412
|
+
keyDefault = requireKeyDefault(view, sides[anchorIndex].schema, anchorKeyCol);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Per active side: the shared key (when needed) plus the supplied view columns it
|
|
416
|
+
// owns. A non-preserved active side carries a presence gate over its supplied columns.
|
|
417
|
+
//
|
|
418
|
+
// When a non-preserved side IS active but a *given row's* supplied values are all null
|
|
419
|
+
// (its presence gate fails for that row), that row's non-preserved insert is dropped.
|
|
420
|
+
// An FK-child side that threads the minted key into its join column unconditionally
|
|
421
|
+
// would then point that FK column at a key with no partner row (a dangling reference —
|
|
422
|
+
// an FK violation under enforcement, a latent spooky-join otherwise). The per-row
|
|
423
|
+
// conditional key thread below (`keyGate`) closes that: the FK-child's key column is
|
|
424
|
+
// nulled for exactly the rows whose presence-gated partner is absent, so the
|
|
425
|
+
// preserved row reads back cleanly null-extended with no dangling FK. The *statically*
|
|
426
|
+
// absent case (a non-preserved side with NO supplied columns) needs no gate — it is
|
|
427
|
+
// inactive ⇒ no key is threaded at all.
|
|
428
|
+
// σ-constant contradiction reject (the supplied-and-contradicting case): when an
|
|
429
|
+
// explicit insert value lands on a σ-constrained base column AND contradicts its
|
|
430
|
+
// constant, reject `predicate-contradiction` at plan time — parity with single-source
|
|
431
|
+
// `checkContradiction`. Match a supplied entry by its resolved (sideIndex, baseColumn)
|
|
432
|
+
// so a *renamed* view column is covered (the supplied entry carries the base column,
|
|
433
|
+
// not the view spelling). Only literal VALUES cells are provable; a non-literal cell, a
|
|
434
|
+
// parameter, or a SELECT source is unprovable ⇒ skipped (proceed), exactly as single-
|
|
435
|
+
// source. The user supplies the value, so no σ-default is appended for it.
|
|
436
|
+
for (const fc of filterConstants) {
|
|
437
|
+
const idx = supplied.findIndex(s => s.sideIndex === fc.sideIndex && s.baseColumn !== undefined && s.baseColumn.toLowerCase() === fc.baseColumn.toLowerCase());
|
|
438
|
+
if (idx >= 0)
|
|
439
|
+
checkJoinFilterContradiction(stmt.source, idx, fc, view);
|
|
440
|
+
}
|
|
441
|
+
const specByIndex = new Map();
|
|
442
|
+
for (const sideIndex of activeIndices) {
|
|
443
|
+
const side = sides[sideIndex];
|
|
444
|
+
const targetColumns = [];
|
|
445
|
+
const envelopeIndices = [];
|
|
446
|
+
if (needsSharedKey) {
|
|
447
|
+
targetColumns.push(keyColumns[sideIndex]);
|
|
448
|
+
envelopeIndices.push(keyEnvelopeIndex);
|
|
449
|
+
}
|
|
450
|
+
const presenceGateIndices = [];
|
|
451
|
+
supplied.forEach((s, idx) => {
|
|
452
|
+
// An existence directive (no `baseColumn`/`sideIndex`) is never stored — it is an
|
|
453
|
+
// unused envelope column. Base columns route to their owning side as before.
|
|
454
|
+
if (s.sideIndex !== sideIndex || s.baseColumn === undefined)
|
|
455
|
+
return;
|
|
456
|
+
if (needsSharedKey && s.isKey)
|
|
457
|
+
return; // the key is threaded above
|
|
458
|
+
targetColumns.push(s.baseColumn);
|
|
459
|
+
envelopeIndices.push(idx);
|
|
460
|
+
if (!side.preserved)
|
|
461
|
+
presenceGateIndices.push(idx);
|
|
462
|
+
});
|
|
463
|
+
// σ-default routing (the not-supplied, owning-side-active case): a σ-constrained base
|
|
464
|
+
// column this side owns that the insert did NOT supply is defaulted to its σ constant
|
|
465
|
+
// — appended as a constant projection on the side (the builder compiles `valueExpr`),
|
|
466
|
+
// NOT an envelope column. The shared join key is skipped (`keyColumns[sideIndex]` — the
|
|
467
|
+
// EC/key thread owns that value; a σ on a join key is degenerate); a supplied column is
|
|
468
|
+
// skipped (the user's value wins, contradiction-checked above). The σ default is a
|
|
469
|
+
// per-row constant, never added to `presenceGateIndices`, so it never makes an
|
|
470
|
+
// otherwise-absent optional side "present". An inactive side reaches neither this loop
|
|
471
|
+
// nor a default (no base row to default into — the documented structural residual).
|
|
472
|
+
const sigmaDefaults = [];
|
|
473
|
+
for (const fc of filterConstants) {
|
|
474
|
+
if (fc.sideIndex !== sideIndex)
|
|
475
|
+
continue;
|
|
476
|
+
if (fc.baseColumn.toLowerCase() === keyColumns[sideIndex].toLowerCase())
|
|
477
|
+
continue;
|
|
478
|
+
if (supplied.some(s => s.sideIndex === sideIndex && s.baseColumn !== undefined && s.baseColumn.toLowerCase() === fc.baseColumn.toLowerCase()))
|
|
479
|
+
continue;
|
|
480
|
+
if (sigmaDefaults.some(d => d.baseColumn.toLowerCase() === fc.baseColumn.toLowerCase()))
|
|
481
|
+
continue;
|
|
482
|
+
sigmaDefaults.push({ baseColumn: fc.baseColumn, valueExpr: fc.valueExpr });
|
|
483
|
+
}
|
|
484
|
+
// A σ default legitimately covers a NOT-NULL-without-default base column (e.g. `where
|
|
485
|
+
// color='red'` covering a NOT NULL `color`), so fold the σ-default columns into the
|
|
486
|
+
// covered set BEFORE the NOT-NULL assertion.
|
|
487
|
+
assertNoMissingNotNull(view, side.schema, [...targetColumns, ...sigmaDefaults.map(d => d.baseColumn)]);
|
|
488
|
+
specByIndex.set(sideIndex, { table: side.table, schema: side.schema, targetColumns, envelopeIndices, presenceGateIndices, ...(sigmaDefaults.length > 0 ? { sigmaDefaults } : {}) });
|
|
489
|
+
}
|
|
490
|
+
// Per-row conditional key thread (the FK-dangling-key fix). With the key MINTED and
|
|
491
|
+
// threaded, any active side `S` that declares a foreign key onto a presence-gated active
|
|
492
|
+
// partner `P` must NOT point its key (FK) column at the shared key for a row where `P` is
|
|
493
|
+
// per-row absent (its presence gate fails, dropping its insert) — otherwise `S`'s row
|
|
494
|
+
// references a freshly minted key with no partner row. Gate `S`'s key column on the AND,
|
|
495
|
+
// over each such partner, of that partner's presence predicate (the OR of its supplied
|
|
496
|
+
// columns being non-null — its own `presenceGateIndices`), nulling the key when all such
|
|
497
|
+
// partners are absent. A parent/anchor side (whose key is its own referenced PK)
|
|
498
|
+
// declares no FK onto the partner ⇒ no gate ⇒ its key threads unconditionally (nulling
|
|
499
|
+
// a NOT NULL PK would be wrong); a key shared only among always-active sides likewise
|
|
500
|
+
// stays unconditional. The key sits at target index 0 (pushed first under
|
|
501
|
+
// `needsSharedKey`).
|
|
502
|
+
//
|
|
503
|
+
// A *supplied* shared key (a view column carries it, `suppliedKeyIndex >= 0`) is NEVER
|
|
504
|
+
// gated: the value is the user's explicit reference, which may point at a PRE-EXISTING
|
|
505
|
+
// parent the insert does not touch (`pv` left null because the parent already exists), so
|
|
506
|
+
// nulling it would silently discard the user's key and orphan the child. The both-side-
|
|
507
|
+
// create "no partner ⇒ no key" reasoning holds only for the engine-minted key, whose
|
|
508
|
+
// referent exists iff this insert creates it; for a supplied key, FK enforcement is the
|
|
509
|
+
// correct validator of a dangling reference (an honest error beats a silent null).
|
|
510
|
+
if (needsSharedKey && suppliedKeyIndex < 0) {
|
|
511
|
+
for (const sideIndex of activeIndices) {
|
|
512
|
+
const groups = [];
|
|
513
|
+
for (const partnerIndex of activeIndices) {
|
|
514
|
+
if (partnerIndex === sideIndex)
|
|
515
|
+
continue;
|
|
516
|
+
const partner = specByIndex.get(partnerIndex);
|
|
517
|
+
if (partner.presenceGateIndices.length === 0)
|
|
518
|
+
continue; // an always-active partner
|
|
519
|
+
if (!sideDeclaresFkOnto(sides[sideIndex], sides[partnerIndex]))
|
|
520
|
+
continue;
|
|
521
|
+
groups.push([...partner.presenceGateIndices]);
|
|
522
|
+
}
|
|
523
|
+
// `groups.length >= 2` is the under-determined multi-parent shape: this FK-child
|
|
524
|
+
// threads its SINGLE shared key column into ≥2 presence-gated (outer-joined)
|
|
525
|
+
// parents (`cc.pr references p1(pp) references p2(qq)`, both LEFT-joined and
|
|
526
|
+
// supplied). One key value `K` must satisfy two FK constraints at once, so a
|
|
527
|
+
// both-create row needs BOTH parents present; a partial-supply row (one parent's
|
|
528
|
+
// value null) nulls `pr` entirely via the AND-gate, yet the present parent still
|
|
529
|
+
// materializes through its own presence filter — silently losing the supplied
|
|
530
|
+
// value and orphaning that parent. We cannot statically prove every row supplies
|
|
531
|
+
// all parents, so the shape is rejected rather than threaded as a broken AND-gated
|
|
532
|
+
// key (§ Outer Joins — Inserts; the per-parent-key-columns generalization is future
|
|
533
|
+
// work). The single-parent (`groups.length === 1`) gate below is the shipped,
|
|
534
|
+
// tested `ojv2` behavior and is unaffected.
|
|
535
|
+
if (groups.length >= 2) {
|
|
536
|
+
raiseMutationDiagnostic({
|
|
537
|
+
reason: 'unsupported-decomposition-key',
|
|
538
|
+
table: view.name,
|
|
539
|
+
message: `cannot insert through view '${view.name}': the FK-child side '${sides[sideIndex].schema.name}' threads a single shared key into ${groups.length} optional (outer-joined) parents; one key column cannot reference some-but-not-all of them per row (a multi-parent shared-key insert is not yet supported — supply all parents, or split into per-parent key columns)`,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
if (groups.length > 0) {
|
|
543
|
+
const spec = specByIndex.get(sideIndex);
|
|
544
|
+
specByIndex.set(sideIndex, { ...spec, keyGate: { keyTargetIndex: 0, groups } });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const order = orderSides(sides).filter(i => specByIndex.has(i));
|
|
549
|
+
return {
|
|
550
|
+
suppliedColumns: supplied.map(s => ({ name: s.name, type: s.type })),
|
|
551
|
+
orderedSides: order.map(i => specByIndex.get(i)),
|
|
552
|
+
keyDefault,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The uniform boolean directive an `exists … as` existence column supplies on a
|
|
557
|
+
* multi-source INSERT — `true` ⇒ insert the non-preserved side, `false` ⇒ omit it
|
|
558
|
+
* (preserved-only). The flag is a *routing directive*, decided at plan time, so it must
|
|
559
|
+
* be a boolean literal that is the **same** across every inserted VALUES row (a per-row
|
|
560
|
+
* branch on the written value, or a SELECT/DML source whose value is not statically
|
|
561
|
+
* known, is deferred — `unsupported-outer-join-update` / `unsupported-source`).
|
|
562
|
+
* `columnIndex` is the flag's position in the explicit column list, hence its position in
|
|
563
|
+
* each VALUES tuple.
|
|
564
|
+
*/
|
|
565
|
+
function existenceInsertFlag(view, stmt, columnIndex, columnName) {
|
|
566
|
+
if (stmt.source.type !== 'values') {
|
|
567
|
+
raiseMutationDiagnostic({
|
|
568
|
+
reason: 'unsupported-source',
|
|
569
|
+
column: columnName,
|
|
570
|
+
table: view.name,
|
|
571
|
+
message: `cannot insert through view '${view.name}': the existence column '${columnName}' is a routing directive that must be a literal in a VALUES source (a SELECT/DML source's per-row value is deferred)`,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
let flag;
|
|
575
|
+
for (const row of stmt.source.values) {
|
|
576
|
+
const cell = row[columnIndex];
|
|
577
|
+
const b = cell ? asBooleanLiteral(cell) : undefined;
|
|
578
|
+
if (b === undefined) {
|
|
579
|
+
raiseMutationDiagnostic({
|
|
580
|
+
reason: 'unsupported-outer-join-update',
|
|
581
|
+
column: columnName,
|
|
582
|
+
table: view.name,
|
|
583
|
+
message: `cannot insert through view '${view.name}': the existence column '${columnName}' must be a boolean literal (true/false); a non-literal per-row directive is deferred`,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (flag === undefined)
|
|
587
|
+
flag = b;
|
|
588
|
+
else if (flag !== b) {
|
|
589
|
+
raiseMutationDiagnostic({
|
|
590
|
+
reason: 'unsupported-outer-join-update',
|
|
591
|
+
column: columnName,
|
|
592
|
+
table: view.name,
|
|
593
|
+
message: `cannot insert through view '${view.name}': the existence column '${columnName}' must be uniform across the inserted rows (a per-row mix of true/false is deferred)`,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// An empty VALUES list cannot reach here (the parser requires ≥1 row); default false.
|
|
598
|
+
return flag ?? false;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Walk every nested `JoinClause`'s ON predicate (flattened on AND) and USING column
|
|
602
|
+
* list across the n-way join tree, collecting cross-side `column = column` equalities
|
|
603
|
+
* (each operand resolving to a *different* side via {@link resolveColumnSide}). The
|
|
604
|
+
* shared backward read the insert envelope's shared-key extraction relies on — it must
|
|
605
|
+
* see ALL conjunctions (not just the outermost join's ON), since for `a join b on …
|
|
606
|
+
* join c on …` only the last ON is on `from[0]`.
|
|
607
|
+
*/
|
|
608
|
+
function collectCrossSideEqualities(from, sides) {
|
|
609
|
+
const out = [];
|
|
610
|
+
const sidesUnder = (fc) => {
|
|
611
|
+
switch (fc.type) {
|
|
612
|
+
case 'table': {
|
|
613
|
+
const alias = (fc.alias ?? fc.table.name).toLowerCase();
|
|
614
|
+
const idx = sides.findIndex(s => s.alias === alias);
|
|
615
|
+
return idx >= 0 ? [idx] : [];
|
|
616
|
+
}
|
|
617
|
+
case 'join':
|
|
618
|
+
return [...sidesUnder(fc.left), ...sidesUnder(fc.right)];
|
|
619
|
+
default:
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const visit = (fc) => {
|
|
624
|
+
if (fc.type !== 'join')
|
|
625
|
+
return;
|
|
626
|
+
visit(fc.left);
|
|
627
|
+
visit(fc.right);
|
|
628
|
+
if (fc.condition) {
|
|
629
|
+
for (const conj of flattenAnd(fc.condition)) {
|
|
630
|
+
if (conj.type !== 'binary' || conj.operator !== '=')
|
|
631
|
+
continue;
|
|
632
|
+
if (conj.left.type !== 'column' || conj.right.type !== 'column')
|
|
633
|
+
continue;
|
|
634
|
+
const sa = resolveColumnSide(conj.left, sides);
|
|
635
|
+
const sb = resolveColumnSide(conj.right, sides);
|
|
636
|
+
if (sa === undefined || sb === undefined || sa === sb)
|
|
637
|
+
continue;
|
|
638
|
+
out.push({ sideA: sa, colA: conj.left.name, sideB: sb, colB: conj.right.name });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// USING (c, …): each named column equates the same-named column on the left and
|
|
642
|
+
// right operands. The operands may be nested joins, so locate the unique owning
|
|
643
|
+
// side under each (a column present on exactly one side of each operand subtree).
|
|
644
|
+
if (fc.columns) {
|
|
645
|
+
const ownerUnder = (operand, col) => {
|
|
646
|
+
const owners = sidesUnder(operand).filter(i => sides[i].schema.columns.some(c => c.name.toLowerCase() === col.toLowerCase()));
|
|
647
|
+
return owners.length === 1 ? owners[0] : undefined;
|
|
648
|
+
};
|
|
649
|
+
for (const colName of fc.columns) {
|
|
650
|
+
const sa = ownerUnder(fc.left, colName);
|
|
651
|
+
const sb = ownerUnder(fc.right, colName);
|
|
652
|
+
if (sa === undefined || sb === undefined || sa === sb)
|
|
653
|
+
continue;
|
|
654
|
+
out.push({ sideA: sa, colA: colName, sideB: sb, colB: colName });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
visit(from[0]);
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The per-side shared-key base columns of an n-way inner equi-join, aligned to `sides`
|
|
663
|
+
* by index. Walks every ON conjunction / USING column ({@link collectCrossSideEqualities})
|
|
664
|
+
* and requires they connect all sides into a **single** shared-key equivalence class
|
|
665
|
+
* with exactly one key column per side (the surrogate the decomposition threads through
|
|
666
|
+
* the envelope's equivalence class). A side contributing more than one column to the EC
|
|
667
|
+
* is the deferred multi-column-surrogate shape — rejected `unsupported-decomposition-key`;
|
|
668
|
+
* a join that does not relate every side through one shared value (a chained / multi-key
|
|
669
|
+
* join) is rejected `unsupported-join`.
|
|
670
|
+
*/
|
|
671
|
+
function extractJoinKeyColumns(view, sel, sides) {
|
|
672
|
+
const equalities = collectCrossSideEqualities(sel.from, sides);
|
|
673
|
+
if (equalities.length === 0) {
|
|
674
|
+
raiseMutationDiagnostic({
|
|
675
|
+
reason: 'unsupported-join',
|
|
676
|
+
table: view.name,
|
|
677
|
+
message: `cannot insert through view '${view.name}': the join must carry an explicit equi-join ON/USING predicate naming the shared key`,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
// Per side: the distinct columns it contributes to a cross-side equality.
|
|
681
|
+
const perSideCols = sides.map(() => new Set());
|
|
682
|
+
// Union-find over `<side>:<col>` keys — proves a single shared-key equivalence class.
|
|
683
|
+
const parent = new Map();
|
|
684
|
+
const ensure = (k) => { if (!parent.has(k))
|
|
685
|
+
parent.set(k, k); };
|
|
686
|
+
const find = (k) => { ensure(k); let r = k; while (parent.get(r) !== r)
|
|
687
|
+
r = parent.get(r); parent.set(k, r); return r; };
|
|
688
|
+
const union = (a, b) => { parent.set(find(a), find(b)); };
|
|
689
|
+
const nodeKey = (side, col) => `${side}:${col.toLowerCase()}`;
|
|
690
|
+
for (const eq of equalities) {
|
|
691
|
+
perSideCols[eq.sideA].add(eq.colA);
|
|
692
|
+
perSideCols[eq.sideB].add(eq.colB);
|
|
693
|
+
union(nodeKey(eq.sideA, eq.colA), nodeKey(eq.sideB, eq.colB));
|
|
694
|
+
}
|
|
695
|
+
const keyCols = sides.map((side, i) => {
|
|
696
|
+
const cols = [...perSideCols[i]];
|
|
697
|
+
if (cols.length === 0) {
|
|
698
|
+
raiseMutationDiagnostic({
|
|
699
|
+
reason: 'unsupported-join',
|
|
700
|
+
table: view.name,
|
|
701
|
+
message: `cannot insert through view '${view.name}': base table '${side.schema.name}' is not related to the shared join key by any equi-join predicate`,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
if (cols.length > 1) {
|
|
705
|
+
raiseMutationDiagnostic({
|
|
706
|
+
reason: 'unsupported-decomposition-key',
|
|
707
|
+
table: view.name,
|
|
708
|
+
message: `cannot insert through view '${view.name}': base table '${side.schema.name}' contributes a composite shared key (${cols.join(', ')}); a multi-column shared-key insert envelope is not yet supported`,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return cols[0];
|
|
712
|
+
});
|
|
713
|
+
// All sides' key columns must belong to ONE equivalence class — a single shared value
|
|
714
|
+
// threaded into every side via the EC. A chain (`a.x=b.y join … b.z=c.w`) yields
|
|
715
|
+
// disjoint key classes that no single surrogate can thread.
|
|
716
|
+
const root0 = find(nodeKey(0, keyCols[0]));
|
|
717
|
+
for (let i = 1; i < sides.length; i++) {
|
|
718
|
+
if (find(nodeKey(i, keyCols[i])) !== root0) {
|
|
719
|
+
raiseMutationDiagnostic({
|
|
720
|
+
reason: 'unsupported-join',
|
|
721
|
+
table: view.name,
|
|
722
|
+
message: `cannot insert through view '${view.name}': the join does not relate all base tables through a single shared key (a chained / multi-key join insert is not yet supported)`,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return keyCols;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Lift the join body's σ (`where`) `column = literal` equality conjuncts as per-side
|
|
730
|
+
* constant-FD insert-defaults — the side-aware analog of single-source
|
|
731
|
+
* `extractFilterConstants`. Each conjunct's column operand is resolved to its owning join
|
|
732
|
+
* side via {@link resolveColumnSide} (which handles alias-qualified `sv1.color` AND
|
|
733
|
+
* unqualified columns, returning `undefined` for ambiguous/unresolved — skipped
|
|
734
|
+
* conservatively, parity with {@link joinCorrelatesMutualFk}); the side's canonical base-
|
|
735
|
+
* column name is resolved via {@link columnByName}. Only a **literal** RHS is lifted (parity
|
|
736
|
+
* with single-source — a `where color = :p` parameter, or a `col = col` / non-equality
|
|
737
|
+
* conjunct, is not a constant-FD producer and is skipped). The σ-constrained column is
|
|
738
|
+
* frequently projected away (`color` is not a view output column), so re-scanning the AST
|
|
739
|
+
* — rather than reading the planned body's output attributes — is the established write-path
|
|
740
|
+
* pattern.
|
|
741
|
+
*/
|
|
742
|
+
function extractJoinFilterConstants(where, sides) {
|
|
743
|
+
const out = [];
|
|
744
|
+
if (!where)
|
|
745
|
+
return out;
|
|
746
|
+
for (const conj of flattenAnd(where)) {
|
|
747
|
+
if (conj.type !== 'binary' || conj.operator !== '=')
|
|
748
|
+
continue;
|
|
749
|
+
const colSide = conj.left.type === 'column' ? conj.left : conj.right.type === 'column' ? conj.right : undefined;
|
|
750
|
+
const litSide = conj.left.type === 'literal' ? conj.left : conj.right.type === 'literal' ? conj.right : undefined;
|
|
751
|
+
if (!colSide || !litSide)
|
|
752
|
+
continue;
|
|
753
|
+
const sideIndex = resolveColumnSide(colSide, sides);
|
|
754
|
+
if (sideIndex === undefined)
|
|
755
|
+
continue; // ambiguous / unresolved — skip conservatively
|
|
756
|
+
const baseCol = columnByName(sides[sideIndex].schema, colSide.name);
|
|
757
|
+
const value = litSide.value instanceof Promise ? undefined : litSide.value;
|
|
758
|
+
out.push({ sideIndex, baseColumn: baseCol.name, valueExpr: litSide, value });
|
|
759
|
+
}
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Reject an insert literal cell that contradicts a join-σ constant — the multi-source
|
|
764
|
+
* analog of single-source `checkContradiction`. Walks every VALUES row's `columnIndex`
|
|
765
|
+
* cell (the supplied entry's position in the envelope/VALUES tuple) and rejects
|
|
766
|
+
* `predicate-contradiction` when a literal cell ≠ the σ constant. A non-literal cell, a
|
|
767
|
+
* parameter, an unprovable constant (`fc.value === undefined`), or a non-VALUES (SELECT)
|
|
768
|
+
* source is skipped (unprovable ⇒ proceed).
|
|
769
|
+
*/
|
|
770
|
+
function checkJoinFilterContradiction(source, columnIndex, fc, view) {
|
|
771
|
+
if (source.type !== 'values' || fc.value === undefined)
|
|
772
|
+
return;
|
|
773
|
+
for (const row of source.values) {
|
|
774
|
+
const cell = row[columnIndex];
|
|
775
|
+
if (!cell || cell.type !== 'literal' || cell.value instanceof Promise)
|
|
776
|
+
continue;
|
|
777
|
+
if (!sqlValuesEqual(cell.value, fc.value)) {
|
|
778
|
+
raiseMutationDiagnostic({
|
|
779
|
+
reason: 'predicate-contradiction',
|
|
780
|
+
column: fc.baseColumn,
|
|
781
|
+
table: view.name,
|
|
782
|
+
message: `insert into view '${view.name}' contradicts its selection predicate on column '${fc.baseColumn}'`,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/** Reject a not-null base column with no declared default that no envelope value covers. */
|
|
788
|
+
function assertNoMissingNotNull(view, schema, targetColumns) {
|
|
789
|
+
const covered = new Set(targetColumns.map(c => c.toLowerCase()));
|
|
790
|
+
for (const col of schema.columns) {
|
|
791
|
+
if (col.generated || !col.notNull || col.defaultValue !== null)
|
|
792
|
+
continue;
|
|
793
|
+
if (covered.has(col.name.toLowerCase()))
|
|
794
|
+
continue;
|
|
795
|
+
raiseMutationDiagnostic({
|
|
796
|
+
reason: 'no-default',
|
|
797
|
+
column: col.name,
|
|
798
|
+
table: view.name,
|
|
799
|
+
message: `cannot insert through view '${view.name}': base table '${schema.name}' column '${col.name}' is NOT NULL with no default and no value supplied through the view`,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* The anchor key column's declared `default` — the surrogate's per-row source —
|
|
805
|
+
* evaluated once per produced row at the envelope (with `mutation_ordinal()` in
|
|
806
|
+
* scope) and threaded into both sides via the equivalence class. The engine no
|
|
807
|
+
* longer invents a surrogate: a key that is neither supplied nor defaulted raises
|
|
808
|
+
* `no-default` with the migration recipe.
|
|
809
|
+
*/
|
|
810
|
+
function requireKeyDefault(view, schema, keyCol) {
|
|
811
|
+
if (keyCol.defaultValue === null) {
|
|
812
|
+
raiseMutationDiagnostic({
|
|
813
|
+
reason: 'no-default',
|
|
814
|
+
column: keyCol.name,
|
|
815
|
+
table: view.name,
|
|
816
|
+
message: `cannot insert through view '${view.name}': the shared key '${schema.name}.${keyCol.name}' is neither supplied nor declares a DEFAULT; declare a default (e.g. \`default (coalesce((select max(${keyCol.name}) from ${schema.name}), 0) + mutation_ordinal())\`) or supply the key through a view column`,
|
|
817
|
+
suggestion: `declare a DEFAULT on '${schema.name}.${keyCol.name}', or expose the key as a supplied view column`,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
return keyCol.defaultValue;
|
|
821
|
+
}
|
|
822
|
+
function columnByName(schema, name) {
|
|
823
|
+
const col = schema.columns.find(c => c.name.toLowerCase() === name.toLowerCase());
|
|
824
|
+
if (!col) {
|
|
825
|
+
raiseMutationDiagnostic({ reason: 'no-base-lineage', table: schema.name, column: name, message: `column '${name}' not found on base table '${schema.name}'` });
|
|
826
|
+
}
|
|
827
|
+
return col;
|
|
828
|
+
}
|
|
829
|
+
// --- analysis -------------------------------------------------------------
|
|
830
|
+
export function analyzeJoinView(ctx, view) {
|
|
831
|
+
if (view.selectAst.type !== 'select') {
|
|
832
|
+
raiseMutationDiagnostic({
|
|
833
|
+
reason: 'no-base-lineage',
|
|
834
|
+
table: view.name,
|
|
835
|
+
message: `view '${view.name}' has a ${view.selectAst.type.toUpperCase()} body, which has no recoverable base operation`,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const sel = view.selectAst;
|
|
839
|
+
// LIMIT / OFFSET / DISTINCT escape the predicate-conjoin rewrite — reject (as
|
|
840
|
+
// the single-source spine does) rather than silently widen the write.
|
|
841
|
+
if (sel.limit || sel.offset) {
|
|
842
|
+
raiseMutationDiagnostic({
|
|
843
|
+
reason: 'unsupported-limit',
|
|
844
|
+
table: view.name,
|
|
845
|
+
message: `cannot write through view '${view.name}': a LIMIT/OFFSET join body is not decomposable (a mutation would escape the limited window)`,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (sel.distinct) {
|
|
849
|
+
raiseMutationDiagnostic({
|
|
850
|
+
reason: 'unsupported-distinct',
|
|
851
|
+
table: view.name,
|
|
852
|
+
message: `cannot write through view '${view.name}': a DISTINCT join body has no 1:1 base-row lineage`,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
const sources = collectJoinSources(view, sel.from);
|
|
856
|
+
// Explicit projections only: a `select *` over a join is rejected here (before
|
|
857
|
+
// the shared backward read) so it surfaces the join-specific diagnostic rather
|
|
858
|
+
// than the generic projection/attribute arity mismatch (column→base routing
|
|
859
|
+
// relies on a 1:1 projection list).
|
|
860
|
+
if (sel.columns.some(c => c.type === 'all')) {
|
|
861
|
+
raiseMutationDiagnostic({
|
|
862
|
+
reason: 'unsupported-join',
|
|
863
|
+
table: view.name,
|
|
864
|
+
message: `cannot write through view '${view.name}': list the join's output columns explicitly (a 'select *' join body is not yet decomposable)`,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
// Plan the body ONCE and read its threaded `updateLineage` through the shared
|
|
868
|
+
// backward-walk consumer (`analyzeBodyLineage`) — the same n-way reader the
|
|
869
|
+
// decomposition fan-out consumes (§ Round-Trip Laws and the Derived Backward
|
|
870
|
+
// Walk). The raw JoinNode + its column scope and the per-side routing layer on
|
|
871
|
+
// top.
|
|
872
|
+
const { root, tableRefsById, viewColToBaseRef, columns } = analyzeBodyLineage(ctx, view);
|
|
873
|
+
// The raw JoinNode + its combined column scope, captured from the SINGLE plan of
|
|
874
|
+
// the body above. The identity capture and RETURNING re-query build their
|
|
875
|
+
// identifying / projection plan nodes directly on top of these (no AST re-plan of
|
|
876
|
+
// the join body for row identification — § Round-Trip Laws and the Derived
|
|
877
|
+
// Backward Walk). `joinScope` is the exact scope `buildSelectStmt` resolved the
|
|
878
|
+
// body's own predicate/projections against (set into `ctx.outputScopes` during
|
|
879
|
+
// `buildJoin`), so reusing it makes base-term resolution byte-identical to the
|
|
880
|
+
// retired re-plan.
|
|
881
|
+
const joinNode = findJoinNode(view, root);
|
|
882
|
+
const joinScope = ctx.outputScopes.get(joinNode);
|
|
883
|
+
if (!joinScope) {
|
|
884
|
+
raiseMutationDiagnostic({
|
|
885
|
+
reason: 'no-base-lineage',
|
|
886
|
+
table: view.name,
|
|
887
|
+
message: `cannot write through view '${view.name}': the planned join body did not expose a resolvable column scope`,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
// Map each AST source's **alias** to its planned `TableReferenceNode` by resolving
|
|
891
|
+
// the alias-qualified PK column through the join's combined scope (the same scope the
|
|
892
|
+
// body's own projections resolved against) to the producing attribute → its owning
|
|
893
|
+
// `TableReferenceNode`. A by-table-NAME match is ambiguous for a self-join (two
|
|
894
|
+
// sources share one table name); the alias is the discriminator, and each alias is a
|
|
895
|
+
// distinct scan node post-plan, so resolving through the scope pins the right ref.
|
|
896
|
+
// `attrToTableRef` inverts every base ref's attribute ids (which the inner join
|
|
897
|
+
// preserves up to its output) so a resolved column reference identifies its source.
|
|
898
|
+
const attrToTableRef = new Map();
|
|
899
|
+
for (const ref of tableRefsById.values()) {
|
|
900
|
+
for (const attr of ref.getAttributes())
|
|
901
|
+
attrToTableRef.set(attr.id, ref);
|
|
902
|
+
}
|
|
903
|
+
const schemaByTableName = new Map();
|
|
904
|
+
for (const ref of tableRefsById.values())
|
|
905
|
+
schemaByTableName.set(ref.tableSchema.name.toLowerCase(), ref.tableSchema);
|
|
906
|
+
const sides = sources.map((src) => {
|
|
907
|
+
const alias = (src.source.alias ?? src.source.table.name).toLowerCase();
|
|
908
|
+
const schema = schemaByTableName.get(src.source.table.name.toLowerCase());
|
|
909
|
+
if (!schema) {
|
|
910
|
+
raiseMutationDiagnostic({
|
|
911
|
+
reason: 'no-base-lineage',
|
|
912
|
+
table: view.name,
|
|
913
|
+
message: `cannot write through view '${view.name}': base table '${src.source.table.name}' did not resolve in the planned body`,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
const ref = resolveSourceTableRef(ctx, joinScope, schema, alias, attrToTableRef, view);
|
|
917
|
+
return { table: ref, schema: ref.tableSchema, alias, preserved: src.preserved, ...(src.guard ? { guard: src.guard } : {}) };
|
|
918
|
+
});
|
|
919
|
+
const sideByTableId = new Map();
|
|
920
|
+
sides.forEach((s, idx) => sideByTableId.set(Number(s.table.id), idx));
|
|
921
|
+
// The existence flag's `RelationalComponentRef` carries the JoinNode CHILD's
|
|
922
|
+
// plan-node id (best-effort — an *aliased* source wraps the scan in an `AliasNode`,
|
|
923
|
+
// so the child id is the wrapper's, not the scan's). Map every body node id to the
|
|
924
|
+
// SOLE `TableReferenceNode` beneath it so a flag's component id resolves through the
|
|
925
|
+
// wrapper to its scan node, then to its side index (§ Existence columns).
|
|
926
|
+
const nodeToSoleTableRef = buildNodeToSoleTableRef(root);
|
|
927
|
+
// Route each shared backward column onto its owning join side. An inner-join body
|
|
928
|
+
// never null-extends (`nullExtended` always false); an outer-join body marks the
|
|
929
|
+
// non-preserved side's columns `nullExtended: true` (the join-predicate-guarded
|
|
930
|
+
// site `deriveJoinUpdateLineage` wraps) — still base-routed, but read-only on
|
|
931
|
+
// update (the deferred materialization) and insertable as an optional member.
|
|
932
|
+
const outColumns = columns.map((bc) => {
|
|
933
|
+
if (bc.baseTableId !== undefined && bc.baseColumn !== undefined) {
|
|
934
|
+
const sideIndex = sideByTableId.get(bc.baseTableId);
|
|
935
|
+
return {
|
|
936
|
+
name: bc.name,
|
|
937
|
+
displayName: bc.displayName,
|
|
938
|
+
sideIndex,
|
|
939
|
+
baseColumn: bc.baseColumn,
|
|
940
|
+
writable: sideIndex !== undefined && !bc.nullExtended,
|
|
941
|
+
nullExtended: bc.nullExtended,
|
|
942
|
+
...(bc.inverse ? { inverse: bc.inverse } : {}),
|
|
943
|
+
...(bc.domain ? { domain: bc.domain } : {}),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
if (bc.existenceComponent) {
|
|
947
|
+
const existenceSide = resolveExistenceSide(bc.existenceComponent, nodeToSoleTableRef, sideByTableId, sides);
|
|
948
|
+
return {
|
|
949
|
+
name: bc.name,
|
|
950
|
+
displayName: bc.displayName,
|
|
951
|
+
// Not a base-column write — its write is an insert/delete effect on the
|
|
952
|
+
// component side, routed by `decomposeUpdate` off `existenceComponent`.
|
|
953
|
+
writable: false,
|
|
954
|
+
nullExtended: false,
|
|
955
|
+
existenceComponent: bc.existenceComponent,
|
|
956
|
+
...(existenceSide !== undefined ? { existenceSide } : {}),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
// An authored (`with inverse`) column: resolve each put's owning base relation
|
|
960
|
+
// to its join side — the same ownership routing every other put rides. A put
|
|
961
|
+
// whose relation is not a join side (defensive; the lineage routed it through a
|
|
962
|
+
// body TableReferenceNode) degrades the column to read-only.
|
|
963
|
+
if (bc.authored) {
|
|
964
|
+
const puts = [];
|
|
965
|
+
let routable = true;
|
|
966
|
+
for (const p of bc.authored.puts) {
|
|
967
|
+
const sideIndex = sideByTableId.get(p.table);
|
|
968
|
+
if (sideIndex === undefined) {
|
|
969
|
+
routable = false;
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
puts.push({ sideIndex, baseColumn: p.baseColumn, expr: p.expr });
|
|
973
|
+
}
|
|
974
|
+
if (routable) {
|
|
975
|
+
return {
|
|
976
|
+
name: bc.name,
|
|
977
|
+
displayName: bc.displayName,
|
|
978
|
+
writable: true,
|
|
979
|
+
nullExtended: false,
|
|
980
|
+
authored: { puts, newRefIndex: bc.authored.newRefIndex },
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return { name: bc.name, displayName: bc.displayName, writable: false, nullExtended: false };
|
|
985
|
+
});
|
|
986
|
+
return { sel, sides, viewColToBaseRef, outColumns, root, joinNode, joinScope };
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Resolve one AST join source (by its `alias`) to its planned `TableReferenceNode`.
|
|
990
|
+
* Probes with the source's first PK column (or first column if keyless) qualified by
|
|
991
|
+
* the alias, resolved through the join's combined `joinScope` — the inner join
|
|
992
|
+
* preserves the producing base scan's attribute id up to its output, so the resolved
|
|
993
|
+
* `ColumnReferenceNode`'s attribute id pins the alias's owning `TableReferenceNode`
|
|
994
|
+
* via `attrToTableRef`. This is what disambiguates a **self-join** (two sources sharing
|
|
995
|
+
* one table name but distinct aliases → distinct scan nodes).
|
|
996
|
+
*/
|
|
997
|
+
function resolveSourceTableRef(ctx, joinScope, schema, alias, attrToTableRef, view) {
|
|
998
|
+
const pk = schema.primaryKeyDefinition;
|
|
999
|
+
const probeColName = (pk.length > 0 ? schema.columns[pk[0].index] : schema.columns[0])?.name;
|
|
1000
|
+
if (!probeColName) {
|
|
1001
|
+
raiseMutationDiagnostic({
|
|
1002
|
+
reason: 'no-base-lineage',
|
|
1003
|
+
table: view.name,
|
|
1004
|
+
message: `cannot write through view '${view.name}': base table '${schema.name}' (alias '${alias}') has no columns to resolve its join side`,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
const probe = buildExpression({ ...ctx, scope: joinScope }, { type: 'column', name: probeColName, table: alias });
|
|
1008
|
+
if (!(probe instanceof ColumnReferenceNode)) {
|
|
1009
|
+
raiseMutationDiagnostic({
|
|
1010
|
+
reason: 'no-base-lineage',
|
|
1011
|
+
table: view.name,
|
|
1012
|
+
message: `cannot write through view '${view.name}': join source alias '${alias}' did not resolve to a base column reference in the planned body`,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
const ref = attrToTableRef.get(probe.attributeId);
|
|
1016
|
+
if (!ref) {
|
|
1017
|
+
raiseMutationDiagnostic({
|
|
1018
|
+
reason: 'no-base-lineage',
|
|
1019
|
+
table: view.name,
|
|
1020
|
+
message: `cannot write through view '${view.name}': join source alias '${alias}' did not resolve to a base table in the planned body`,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return ref;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* The single `JoinNode` inside a planned n-way inner-join body — the outermost
|
|
1027
|
+
* `JoinNode` reached from the root (the body's plan is `Project(Filter?(Join…))`; for
|
|
1028
|
+
* an n-way join the outermost JoinNode transitively contains the nested ones). Reused
|
|
1029
|
+
* (not re-planned) as the source the identifying-capture / RETURNING relations build
|
|
1030
|
+
* on; the nested joins ride inside it via its own `getRelations()`.
|
|
1031
|
+
*/
|
|
1032
|
+
function findJoinNode(view, root) {
|
|
1033
|
+
let found;
|
|
1034
|
+
const visit = (n) => {
|
|
1035
|
+
if (found)
|
|
1036
|
+
return;
|
|
1037
|
+
if (n instanceof JoinNode) {
|
|
1038
|
+
found = n;
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
for (const child of n.getRelations())
|
|
1042
|
+
visit(child);
|
|
1043
|
+
};
|
|
1044
|
+
visit(root);
|
|
1045
|
+
if (!found) {
|
|
1046
|
+
raiseMutationDiagnostic({
|
|
1047
|
+
reason: 'no-base-lineage',
|
|
1048
|
+
table: view.name,
|
|
1049
|
+
message: `cannot write through view '${view.name}': the planned body did not contain a join node`,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return found;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Map every node in a planned body to the SOLE `TableReferenceNode` reachable beneath
|
|
1056
|
+
* it. Resolves an existence flag's `RelationalComponentRef` — which names the JoinNode
|
|
1057
|
+
* child's plan-node id, an `AliasNode` wrapper for an *aliased* source — back to its
|
|
1058
|
+
* scan node. A node spanning two or more base tables is left unmapped (size ≠ 1).
|
|
1059
|
+
*/
|
|
1060
|
+
function buildNodeToSoleTableRef(root) {
|
|
1061
|
+
const out = new Map();
|
|
1062
|
+
const visit = (n) => {
|
|
1063
|
+
if (n instanceof TableReferenceNode) {
|
|
1064
|
+
out.set(Number(n.id), n);
|
|
1065
|
+
return [n];
|
|
1066
|
+
}
|
|
1067
|
+
const refs = [];
|
|
1068
|
+
for (const child of n.getRelations())
|
|
1069
|
+
refs.push(...visit(child));
|
|
1070
|
+
const uniqueIds = new Set(refs.map(r => Number(r.id)));
|
|
1071
|
+
if (uniqueIds.size === 1)
|
|
1072
|
+
out.set(Number(n.id), refs[0]);
|
|
1073
|
+
return refs;
|
|
1074
|
+
};
|
|
1075
|
+
visit(root);
|
|
1076
|
+
return out;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Resolve an existence flag's component to the non-preserved join side it drives. The
|
|
1080
|
+
* component names the JoinNode child's plan-node id; resolve it through any wrapper to
|
|
1081
|
+
* its scan node ({@link buildNodeToSoleTableRef}) → side index. Falls back to the unique
|
|
1082
|
+
* non-preserved side when the id does not resolve (v1: a single LEFT join has exactly
|
|
1083
|
+
* one such side). Returns `undefined` only when the side is genuinely ambiguous (≠1
|
|
1084
|
+
* non-preserved side — e.g. a parser-rejected FULL); the write router then defers.
|
|
1085
|
+
*/
|
|
1086
|
+
function resolveExistenceSide(component, nodeToSoleTableRef, sideByTableId, sides) {
|
|
1087
|
+
if (component.kind === 'join-side') {
|
|
1088
|
+
const ref = nodeToSoleTableRef.get(component.table);
|
|
1089
|
+
const direct = ref ? sideByTableId.get(Number(ref.id)) : undefined;
|
|
1090
|
+
if (direct !== undefined)
|
|
1091
|
+
return direct;
|
|
1092
|
+
}
|
|
1093
|
+
const nonPreserved = sides.flatMap((s, i) => s.preserved ? [] : [i]);
|
|
1094
|
+
return nonPreserved.length === 1 ? nonPreserved[0] : undefined;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Collect the join's base-table sources (in AST declaration order), validating the body
|
|
1098
|
+
* is an **n-way (>=2) equi-join** over plain base tables — `inner`, `left`, `right`, or
|
|
1099
|
+
* `full` (RIGHT is the mirror of LEFT; FULL has no preserved side, so it self-
|
|
1100
|
+
* conservatizes downstream; no comma/implicit cross join, no subquery or function
|
|
1101
|
+
* sources). A **self-join**
|
|
1102
|
+
* — the same base table under distinct aliases — is accepted (routing is alias-keyed
|
|
1103
|
+
* downstream); USING joins are accepted alongside ON joins. The declaration order is the
|
|
1104
|
+
* alias-declaration order the substrate serializes per-side ops in (§ Cycles, Self-Joins).
|
|
1105
|
+
*
|
|
1106
|
+
* Each source is tagged **preserved** / **non-preserved** by walking the join tree and
|
|
1107
|
+
* tracking which branch each table sits on: the right of a `left`, the left of a `right`,
|
|
1108
|
+
* and both sides of a `full` are non-preserved (potentially null-extended), carrying the
|
|
1109
|
+
* enclosing outer join's ON predicate as their guard (§ Outer Joins). An inner join
|
|
1110
|
+
* propagates its parents' classification unchanged. This is the AST-shape dual of the
|
|
1111
|
+
* planned body's `null-extended` lineage (which `analyzeJoinView` cross-checks per column).
|
|
1112
|
+
*/
|
|
1113
|
+
function collectJoinSources(view, from) {
|
|
1114
|
+
if (from.length !== 1 || from[0].type !== 'join') {
|
|
1115
|
+
raiseMutationDiagnostic({
|
|
1116
|
+
reason: 'unsupported-join',
|
|
1117
|
+
table: view.name,
|
|
1118
|
+
message: `cannot write through view '${view.name}': only an explicit 'JOIN ... ON/USING' body is decomposable (a comma/implicit cross join is not)`,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
const out = [];
|
|
1122
|
+
// `nonPreserved` is true when an enclosing outer join can null-extend the subtree;
|
|
1123
|
+
// `guards` are those outer joins' ON predicates (conjoined onto each leaf's guard).
|
|
1124
|
+
const visit = (fc, nonPreserved, guards) => {
|
|
1125
|
+
switch (fc.type) {
|
|
1126
|
+
case 'table':
|
|
1127
|
+
out.push({
|
|
1128
|
+
source: fc,
|
|
1129
|
+
preserved: !nonPreserved,
|
|
1130
|
+
guard: guards.reduce((acc, g) => combineAnd(acc, g), undefined),
|
|
1131
|
+
});
|
|
1132
|
+
return;
|
|
1133
|
+
case 'join': {
|
|
1134
|
+
const hasPredicate = !!fc.condition || (!!fc.columns && fc.columns.length > 0);
|
|
1135
|
+
// RIGHT is **admitted** (`view-write-right-join-readmit`): the runtime reads a
|
|
1136
|
+
// RIGHT join and its preserved/non-preserved classification is the exact mirror of
|
|
1137
|
+
// LEFT (the right operand of a `right` is preserved, the left null-extended — see
|
|
1138
|
+
// the per-side recursion below), so the substrate routes it symmetrically (it keys
|
|
1139
|
+
// off `JoinSide.preserved`, not source order). FULL is accepted only to carry
|
|
1140
|
+
// through to its precise conservative diagnostics (it has no preserved side, so it
|
|
1141
|
+
// never falsely advertises); FULL write-through is a separable future concern.
|
|
1142
|
+
const acceptedType = fc.joinType === 'inner' || fc.joinType === 'left' || fc.joinType === 'right' || fc.joinType === 'full';
|
|
1143
|
+
if (!acceptedType || !hasPredicate) {
|
|
1144
|
+
raiseMutationDiagnostic({
|
|
1145
|
+
reason: 'unsupported-join',
|
|
1146
|
+
table: view.name,
|
|
1147
|
+
message: `cannot write through view '${view.name}': only INNER/LEFT/RIGHT/FULL joins with an ON/USING predicate are decomposable (got '${fc.joinType}'${hasPredicate ? '' : ' without ON/USING'})`,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
// USING joins carry no AST `Expression` guard — only an explicit ON predicate
|
|
1151
|
+
// is surfaced as the null-extension guard (v1 routing does not consume it).
|
|
1152
|
+
const guardsWith = fc.condition ? [...guards, fc.condition] : guards;
|
|
1153
|
+
switch (fc.joinType) {
|
|
1154
|
+
case 'inner':
|
|
1155
|
+
visit(fc.left, nonPreserved, guards);
|
|
1156
|
+
visit(fc.right, nonPreserved, guards);
|
|
1157
|
+
break;
|
|
1158
|
+
case 'left':
|
|
1159
|
+
visit(fc.left, nonPreserved, guards);
|
|
1160
|
+
visit(fc.right, true, guardsWith);
|
|
1161
|
+
break;
|
|
1162
|
+
case 'right':
|
|
1163
|
+
// Mirror of `left`: the left operand of a RIGHT join is null-extended
|
|
1164
|
+
// (non-preserved), the right operand is preserved.
|
|
1165
|
+
visit(fc.left, true, guardsWith);
|
|
1166
|
+
visit(fc.right, nonPreserved, guards);
|
|
1167
|
+
break;
|
|
1168
|
+
case 'full':
|
|
1169
|
+
visit(fc.left, true, guardsWith);
|
|
1170
|
+
visit(fc.right, true, guardsWith);
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
default:
|
|
1176
|
+
raiseMutationDiagnostic({
|
|
1177
|
+
reason: 'nested-view',
|
|
1178
|
+
table: view.name,
|
|
1179
|
+
message: `cannot write through view '${view.name}': join sources must be plain base tables (a subquery / function source in the join is not yet supported)`,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
visit(from[0], false, []);
|
|
1184
|
+
if (out.length < 2) {
|
|
1185
|
+
raiseMutationDiagnostic({
|
|
1186
|
+
reason: 'unsupported-join',
|
|
1187
|
+
table: view.name,
|
|
1188
|
+
message: `cannot write through view '${view.name}': a decomposable join needs at least two base tables (found ${out.length})`,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Scope guard for the multi-source path — parity with the single-source spine
|
|
1195
|
+
* (`single-source.ts` § `assertTopLevelViewColumns`). A top-level `where` / `set`
|
|
1196
|
+
* reference that is not a join-view output column would otherwise pass through
|
|
1197
|
+
* `substituteViewColumns` unmapped and re-bind against a base table in the
|
|
1198
|
+
* identifying subquery's join body (the same encapsulation leak). `outColumns`
|
|
1199
|
+
* already enumerates every projected view column (computed ones included, marked
|
|
1200
|
+
* non-writable), so it is the view's exposed column set.
|
|
1201
|
+
*/
|
|
1202
|
+
function guardTopLevelScope(expr, analysis, view) {
|
|
1203
|
+
assertTopLevelViewColumns(expr, new Set(analysis.outColumns.map(c => c.name)), analysis.outColumns.map(c => c.displayName), view);
|
|
1204
|
+
}
|
|
1205
|
+
export function decomposeUpdate(ctx, view, analysis, stmt, sourceValues, captureRelationName = MS_UPDATE_KEYS_CTE) {
|
|
1206
|
+
// RETURNING through a multi-source update is supported, but the rows are not
|
|
1207
|
+
// recoverable from the per-side base ops (the view row spans both tables), so
|
|
1208
|
+
// the builder (`view-mutation-builder.ts`) supplies them via a re-query of the
|
|
1209
|
+
// planned join body; the base ops themselves carry no RETURNING.
|
|
1210
|
+
// Each assignment routes to its owning base side by lineage, unconditionally —
|
|
1211
|
+
// there is no statement-level base-set override (the routing tags were removed;
|
|
1212
|
+
// a per-row presence/membership column expresses any non-default routing).
|
|
1213
|
+
// Scope guard: top-level `where` references must name view columns (parity with
|
|
1214
|
+
// the single-source spine — a base-only name must not leak through the join body).
|
|
1215
|
+
if (stmt.where)
|
|
1216
|
+
guardTopLevelScope(stmt.where, analysis, view);
|
|
1217
|
+
// Cross-source SET values (`set a.x = b.y`) ride the same `__vmupd_keys` capture:
|
|
1218
|
+
// each partner-side base column the SET reads is projected into the capture under a
|
|
1219
|
+
// stable `srcN` alias, and the reference is rewritten to a correlated scalar read of
|
|
1220
|
+
// it (keyed by the owning side's PK). The carrier is the `sourceValues` out-param the
|
|
1221
|
+
// builder threads into `buildMultiSourceKeyCapture`; absent it (the legacy
|
|
1222
|
+
// `propagateMultiSource` path, unreachable from build) cross-source values stay
|
|
1223
|
+
// rejected by `stripSideQualifier`'s throw.
|
|
1224
|
+
const srcDedup = new Map();
|
|
1225
|
+
// Project an arbitrary base-term expression into the up-front `__vmupd_keys` capture
|
|
1226
|
+
// under a stable `srcN` alias (deduped by `key`), returning that alias. The carrier is
|
|
1227
|
+
// the `sourceValues` out-param the builder threads into the capture, so each projection
|
|
1228
|
+
// is materialized pre-mutation over the join body. Backs both the cross-source SET reads
|
|
1229
|
+
// and the outer-join non-preserved materialization (the captured assigned value + the EC
|
|
1230
|
+
// join key). Absent ⇒ the legacy non-build path, which keeps deferring those shapes.
|
|
1231
|
+
const registerCapturedExpr = sourceValues
|
|
1232
|
+
? (key, expr) => {
|
|
1233
|
+
const existing = srcDedup.get(key);
|
|
1234
|
+
if (existing)
|
|
1235
|
+
return existing;
|
|
1236
|
+
const alias = `src${sourceValues.length}`;
|
|
1237
|
+
srcDedup.set(key, alias);
|
|
1238
|
+
sourceValues.push({ alias, expr });
|
|
1239
|
+
return alias;
|
|
1240
|
+
}
|
|
1241
|
+
: undefined;
|
|
1242
|
+
const registerCrossSource = sourceValues
|
|
1243
|
+
? (col) => {
|
|
1244
|
+
const key = `${(col.table ?? '').toLowerCase()}.${col.name.toLowerCase()}`;
|
|
1245
|
+
const existing = srcDedup.get(key);
|
|
1246
|
+
if (existing)
|
|
1247
|
+
return existing;
|
|
1248
|
+
const alias = `src${sourceValues.length}`;
|
|
1249
|
+
srcDedup.set(key, alias);
|
|
1250
|
+
sourceValues.push({ alias, expr: { type: 'column', name: col.name, table: col.table } });
|
|
1251
|
+
return alias;
|
|
1252
|
+
}
|
|
1253
|
+
: undefined;
|
|
1254
|
+
// Route each assignment to its owning base side (one entry per side, index 0..n-1).
|
|
1255
|
+
const perSide = analysis.sides.map(() => []);
|
|
1256
|
+
// Non-preserved (outer-join null-extended) assignments, keyed by their owning side: each
|
|
1257
|
+
// rides a matched-UPDATE (pushed into `perSide`, its captured PK non-null) plus a single
|
|
1258
|
+
// null-extended-INSERT op (built after the per-side loop, one per non-preserved side,
|
|
1259
|
+
// carrying every column assigned on that side). § Outer Joins — Updates.
|
|
1260
|
+
const nullExtendedBySide = new Map();
|
|
1261
|
+
// Existence-flag writes (§ Existence columns): writing an `exists … as` flag drives the
|
|
1262
|
+
// non-preserved side's existence. `set hasB = true` materializes the side for the
|
|
1263
|
+
// null-extended partition (the matched-update path's null-extended INSERT with no
|
|
1264
|
+
// assigned columns — the EC join key + base defaults); `set hasB = false` deletes the
|
|
1265
|
+
// matched partition. Tracked by the non-preserved side they drive.
|
|
1266
|
+
const existenceInsertSides = new Set();
|
|
1267
|
+
const existenceDeleteSides = new Set();
|
|
1268
|
+
// `new.<x>` in an authored put binds the WRITTEN view row: when `x` is also
|
|
1269
|
+
// assigned in this statement, that assignment's value (every embedded RHS reads
|
|
1270
|
+
// the pre-update join row, so cross-references are order-independent); otherwise
|
|
1271
|
+
// the column's forward read image. First occurrence wins on a duplicate target —
|
|
1272
|
+
// the base builder's duplicate-assignment backstop rejects the statement anyway.
|
|
1273
|
+
// Keyed by out-column index (the `newRefIndex` domain).
|
|
1274
|
+
const assignedValueByIdx = new Map();
|
|
1275
|
+
stmt.assignments.forEach(a => {
|
|
1276
|
+
const i = analysis.outColumns.findIndex(c => c.name === a.column.toLowerCase());
|
|
1277
|
+
if (i >= 0 && !assignedValueByIdx.has(i))
|
|
1278
|
+
assignedValueByIdx.set(i, a.value);
|
|
1279
|
+
});
|
|
1280
|
+
for (const asg of stmt.assignments) {
|
|
1281
|
+
const out = analysis.outColumns.find(c => c.name === asg.column.toLowerCase());
|
|
1282
|
+
if (!out) {
|
|
1283
|
+
// Not a view column at all — the same encapsulation-leak guard as the
|
|
1284
|
+
// top-level `where` scan (distinct from a computed view column below).
|
|
1285
|
+
raiseUnknownViewColumn(asg.column, view, analysis.outColumns.map(c => c.displayName));
|
|
1286
|
+
}
|
|
1287
|
+
// An `exists … as` existence flag (no base column): writing it is the explicit
|
|
1288
|
+
// insert/delete-of-the-component effect. `true` ⇒ insert the non-preserved side for
|
|
1289
|
+
// the null-extended partition; `false` ⇒ delete the matched partition. Reuses the
|
|
1290
|
+
// non-preserved-column update substrate (capture + null-extended INSERT / captured-key
|
|
1291
|
+
// DELETE), so the runtime is reused, not extended (§ Existence columns).
|
|
1292
|
+
if (out.existenceComponent) {
|
|
1293
|
+
if (!registerCapturedExpr) {
|
|
1294
|
+
raiseMutationDiagnostic({
|
|
1295
|
+
reason: 'unsupported-outer-join-update',
|
|
1296
|
+
column: asg.column,
|
|
1297
|
+
table: view.name,
|
|
1298
|
+
message: `cannot write through view '${view.name}': the existence column '${asg.column}' drives a per-row insert/delete of the non-preserved side, which needs the capture carrier`,
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
if (out.existenceSide === undefined) {
|
|
1302
|
+
raiseMutationDiagnostic({
|
|
1303
|
+
reason: 'unsupported-outer-join-update',
|
|
1304
|
+
column: asg.column,
|
|
1305
|
+
table: view.name,
|
|
1306
|
+
message: `cannot write through view '${view.name}': the existence column '${asg.column}' does not resolve to a single non-preserved side (an ambiguous / full-outer existence shape is deferred)`,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
const npSideIndex = out.existenceSide;
|
|
1310
|
+
// RETURNING is not recoverable through an existence-flip (the post-mutation
|
|
1311
|
+
// re-query identifies by the captured non-preserved PK — null for a freshly
|
|
1312
|
+
// materialized row, deleted for a removed one), so reject it (parity with the
|
|
1313
|
+
// non-preserved-column update).
|
|
1314
|
+
if (stmt.returning && stmt.returning.length > 0) {
|
|
1315
|
+
raiseMutationDiagnostic({
|
|
1316
|
+
reason: 'returning-through-view',
|
|
1317
|
+
column: asg.column,
|
|
1318
|
+
table: view.name,
|
|
1319
|
+
message: `cannot write through view '${view.name}': RETURNING is not supported on an existence-flag write '${asg.column}' — the materialized/deleted non-preserved row is not recoverable by the captured-identity re-query`,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
const flag = asBooleanLiteral(asg.value);
|
|
1323
|
+
if (flag === undefined) {
|
|
1324
|
+
raiseMutationDiagnostic({
|
|
1325
|
+
reason: 'unsupported-outer-join-update',
|
|
1326
|
+
column: asg.column,
|
|
1327
|
+
table: view.name,
|
|
1328
|
+
message: `cannot write through view '${view.name}': the existence column '${asg.column}' must be assigned a boolean literal (true/false); a per-row branch on a non-literal value is deferred`,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
if (flag) {
|
|
1332
|
+
// `true`: materialize the non-preserved side for the null-extended partition.
|
|
1333
|
+
// Ensure a (possibly empty) `nullExtendedBySide` entry so the post-loop emits
|
|
1334
|
+
// the materialization INSERT; a same-side `set` folds its columns into it.
|
|
1335
|
+
existenceInsertSides.add(npSideIndex);
|
|
1336
|
+
if (!nullExtendedBySide.has(npSideIndex))
|
|
1337
|
+
nullExtendedBySide.set(npSideIndex, []);
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
// `false`: delete the matched partition (captured non-preserved PK non-null).
|
|
1341
|
+
existenceDeleteSides.add(npSideIndex);
|
|
1342
|
+
}
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
// A non-preserved (outer-join null-extended) base column splits per row (§ Outer
|
|
1346
|
+
// Joins — Updates on a non-preserved-side column): where the non-preserved side
|
|
1347
|
+
// matched it is an ordinary base update; where the row is null-extended (no match) it
|
|
1348
|
+
// is rewritten as an insert on that side. Both ride the up-front `__vmupd_keys`
|
|
1349
|
+
// capture, materialized pre-mutation over the join body: the matched op reads its
|
|
1350
|
+
// captured PK (non-null for a matched row); the null-extended op fires for the rows
|
|
1351
|
+
// whose captured PK is null. The assigned value is captured ONCE (so both branches
|
|
1352
|
+
// read the identical pre-mutation value), and the matched op reads it back keyed on
|
|
1353
|
+
// the non-preserved PK. Needs the capture carrier; the legacy `propagateMultiSource`
|
|
1354
|
+
// path (no carrier) keeps deferring with `unsupported-outer-join-update`.
|
|
1355
|
+
if (out.nullExtended && out.sideIndex !== undefined && out.baseColumn) {
|
|
1356
|
+
if (!registerCapturedExpr) {
|
|
1357
|
+
raiseMutationDiagnostic({
|
|
1358
|
+
reason: 'unsupported-outer-join-update',
|
|
1359
|
+
column: asg.column,
|
|
1360
|
+
table: view.name,
|
|
1361
|
+
message: `cannot write through view '${view.name}': column '${asg.column}' is backed by the non-preserved side of an outer join (base table '${analysis.sides[out.sideIndex].schema.name}'); the per-row matched-update / null-extended-insert materialization needs the capture carrier`,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
// RETURNING through a non-preserved-side update IS supported: the post-mutation
|
|
1365
|
+
// re-query (`buildMultiSourceUpdateReturning`) re-keys its identity EXISTS off the
|
|
1366
|
+
// stable preserved-side PK (a per-non-preserved-side matched-OR-null disjunction),
|
|
1367
|
+
// so a freshly-materialized null-extended row — whose non-preserved PK was captured
|
|
1368
|
+
// NULL — surfaces via its preserved-side equalities instead of being dropped by a
|
|
1369
|
+
// `NULL = <minted pk>` match (`view-write-outer-join-nonpreserved-returning`). The
|
|
1370
|
+
// existence-flag RETURNING reject above stays — `set hasB = false` deletes the
|
|
1371
|
+
// matched partition, which neither disjunction branch recovers.
|
|
1372
|
+
// The assigned value's top-level references must name view columns (parity with
|
|
1373
|
+
// the preserved path); the value is then lowered to base terms over the join body
|
|
1374
|
+
// and captured pre-mutation, so a same- or cross-side read resolves uniformly.
|
|
1375
|
+
guardTopLevelScope(asg.value, analysis, view);
|
|
1376
|
+
const npSide = analysis.sides[out.sideIndex];
|
|
1377
|
+
const baseValue = substituteViewColumns(ctx, asg.value, analysis.viewColToBaseRef, view, analysis.sides);
|
|
1378
|
+
const valAlias = registerCapturedExpr(`neval:${out.sideIndex}:${out.baseColumn.toLowerCase()}`, baseValue);
|
|
1379
|
+
// Matched rows: a per-side UPDATE reading the captured value back, correlated by
|
|
1380
|
+
// the non-preserved side's PK (`buildCapturedKeyPredicate` already filters to
|
|
1381
|
+
// matched rows — a null captured PK never equals a real one). The read-back is
|
|
1382
|
+
// `min`-de-duped per non-preserved partner: when N preserved rows share one
|
|
1383
|
+
// existing partner, that partner's PK matches all N capture rows, so a bare scalar
|
|
1384
|
+
// read would error `Scalar subquery returned more than one row` — `min` collapses
|
|
1385
|
+
// the shared-partner group to one value (a no-op for a constant / np-only SET).
|
|
1386
|
+
perSide[out.sideIndex].push({ column: out.baseColumn, value: capturedValueSubquery(valAlias, out.sideIndex, requireKeyColumns(view, npSide), 'min', SELF_ALIAS, captureRelationName) });
|
|
1387
|
+
// Null-extended rows: accumulate the (column, captured value) for this side's
|
|
1388
|
+
// single materialization insert, built after the loop.
|
|
1389
|
+
let list = nullExtendedBySide.get(out.sideIndex);
|
|
1390
|
+
if (!list) {
|
|
1391
|
+
list = [];
|
|
1392
|
+
nullExtendedBySide.set(out.sideIndex, list);
|
|
1393
|
+
}
|
|
1394
|
+
list.push({ baseColumn: out.baseColumn, valAlias });
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
// Lower a view-term value expression onto one owning side: gate cross-source
|
|
1398
|
+
// reads + 1:many cardinality, substitute view columns to base terms, then strip
|
|
1399
|
+
// the owning side's qualifier (a partner-side read becomes a correlated read of
|
|
1400
|
+
// its captured pre-mutation value). Shared by the plain per-column route and the
|
|
1401
|
+
// authored put fan-out below.
|
|
1402
|
+
const lowerValueOntoSide = (valueViewTerms, owningSideIndex, assignedCol) => {
|
|
1403
|
+
// Gate cross-source reads: a value that reads a partner-side view column is
|
|
1404
|
+
// admitted only when that column has `base` lineage (its value is recoverable
|
|
1405
|
+
// from a captured base column). A computed (non-base) partner column stays
|
|
1406
|
+
// rejected (`no-inverse`); a same-side read keeps the qualifier-strip path. Run
|
|
1407
|
+
// only when a capture carrier is threaded — the legacy path rejects wholesale.
|
|
1408
|
+
if (registerCrossSource)
|
|
1409
|
+
gateCrossSourceReads(valueViewTerms, owningSideIndex, analysis, view);
|
|
1410
|
+
const side = analysis.sides[owningSideIndex];
|
|
1411
|
+
// Cross-source cardinality gate (§ Inner Join, cross-source `set`): a cross-source
|
|
1412
|
+
// value `set owner.x = partner.y` is well-defined only when the owning side joins AT
|
|
1413
|
+
// MOST ONE partner row — else the capture's correlated read-back is multi-valued and
|
|
1414
|
+
// the runtime would error `Scalar subquery returned more than one row`. Reject the
|
|
1415
|
+
// 1:many direction at plan time, naming the cross-source ambiguity. Bound to this
|
|
1416
|
+
// assignment's owning side; memoized per partner side so the join equalities are
|
|
1417
|
+
// collected once. Threaded only on the capture-carrier path (symmetric with
|
|
1418
|
+
// `registerCrossSource`); the legacy path rejects cross-source wholesale before this.
|
|
1419
|
+
const cardinalityProven = new Map();
|
|
1420
|
+
const gateCrossSourceCardinality = registerCrossSource
|
|
1421
|
+
? (partnerCol) => {
|
|
1422
|
+
const partnerIdx = resolveColumnSide(partnerCol, analysis.sides);
|
|
1423
|
+
if (partnerIdx === undefined || partnerIdx === owningSideIndex)
|
|
1424
|
+
return;
|
|
1425
|
+
let proven = cardinalityProven.get(partnerIdx);
|
|
1426
|
+
if (proven === undefined) {
|
|
1427
|
+
proven = ownerJoinsAtMostOnePartner(owningSideIndex, partnerIdx, analysis.sel, analysis.sides);
|
|
1428
|
+
cardinalityProven.set(partnerIdx, proven);
|
|
1429
|
+
}
|
|
1430
|
+
if (!proven) {
|
|
1431
|
+
const partnerTable = analysis.sides[partnerIdx].schema.name;
|
|
1432
|
+
raiseMutationDiagnostic({
|
|
1433
|
+
reason: 'cross-source-ambiguous-cardinality',
|
|
1434
|
+
column: assignedCol,
|
|
1435
|
+
table: view.name,
|
|
1436
|
+
message: `cannot write through view '${view.name}': the cross-source assignment of column '${assignedCol}' reads column '${partnerCol.name}' on base table '${partnerTable}', but the assigned side joins more than one '${partnerTable}' row (the join does not constrain '${partnerTable}' to a unique key), so the partner value is ambiguous — a cross-source \`set\` value is well-defined only when the assigned side joins at most one partner row`,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
: undefined;
|
|
1441
|
+
// Rewrite the assigned value into base terms, then strip the owning side's
|
|
1442
|
+
// qualifier (the base UPDATE targets that table directly). A reference to a
|
|
1443
|
+
// partner side is rewritten to a correlated read of its captured pre-mutation
|
|
1444
|
+
// value (`registerCrossSource`); absent the carrier it is rejected.
|
|
1445
|
+
return stripSideQualifier(substituteViewColumns(ctx, valueViewTerms, analysis.viewColToBaseRef, view, analysis.sides), view, side, owningSideIndex, analysis.sides, registerCrossSource, gateCrossSourceCardinality);
|
|
1446
|
+
};
|
|
1447
|
+
// An authored (`with inverse`) column lowers to one base assignment per put,
|
|
1448
|
+
// each routed to its owning join side — a two-sided target set yields two child
|
|
1449
|
+
// ops, atomic, FK-parent-first ordered by the shared `orderSides` below. Inside
|
|
1450
|
+
// each put, `new.<x>` binds the WRITTEN view row: the assigned value when `x`
|
|
1451
|
+
// is assigned in this statement (including this column itself), that view
|
|
1452
|
+
// column's name otherwise — still in VIEW terms — then the standard lowering
|
|
1453
|
+
// maps everything onto the put's side (the forward read image for non-assigned
|
|
1454
|
+
// columns; a cross-side read rides the same captured-read machinery as a
|
|
1455
|
+
// cross-source SET value). docs/view-updateability.md § Authored inverses.
|
|
1456
|
+
if (out.authored) {
|
|
1457
|
+
const authored = out.authored;
|
|
1458
|
+
// The assigned VALUE's top-level references must name view columns (parity
|
|
1459
|
+
// with the plain route below).
|
|
1460
|
+
guardTopLevelScope(asg.value, analysis, view);
|
|
1461
|
+
for (const put of authored.puts) {
|
|
1462
|
+
const viewTermExpr = substituteNewRefs(put.expr, name => {
|
|
1463
|
+
const idx = requireValidatedNewRefIndex(authored.newRefIndex, name, asg.column);
|
|
1464
|
+
return assignedValueByIdx.get(idx)
|
|
1465
|
+
?? { type: 'column', name: analysis.outColumns[idx].displayName };
|
|
1466
|
+
});
|
|
1467
|
+
perSide[put.sideIndex].push({
|
|
1468
|
+
column: put.baseColumn,
|
|
1469
|
+
value: lowerValueOntoSide(viewTermExpr, put.sideIndex, out.displayName),
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (!out.writable || out.sideIndex === undefined || !out.baseColumn) {
|
|
1475
|
+
raiseMutationDiagnostic({
|
|
1476
|
+
reason: 'no-inverse',
|
|
1477
|
+
column: asg.column,
|
|
1478
|
+
table: view.name,
|
|
1479
|
+
message: `cannot write through view '${view.name}': column '${asg.column}' is a computed (non-invertible) expression and is read-only`,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
// The assigned VALUE's top-level references must name view columns too (parity
|
|
1483
|
+
// with the single-source spine). On a single-table side a base-only name would
|
|
1484
|
+
// otherwise re-bind in that table; across sides it would fail to resolve with a
|
|
1485
|
+
// generic error — the structured guard makes the diagnostic uniform either way.
|
|
1486
|
+
guardTopLevelScope(asg.value, analysis, view);
|
|
1487
|
+
const baseValue = lowerValueOntoSide(asg.value, out.sideIndex, out.displayName);
|
|
1488
|
+
// For an `inverse`-profile column the assigned value is in the VIEW domain;
|
|
1489
|
+
// apply the site's inverse to recover the BASE value (`cv1 = cv + 1` ⇒ the
|
|
1490
|
+
// write `cv1 = w` stores `cv = w - 1`). The base-term substitution + side-
|
|
1491
|
+
// qualifier strip above already produced the written value in base terms; the
|
|
1492
|
+
// inverse wraps that last (it expects a value already in base terms).
|
|
1493
|
+
const written = out.inverse ? out.inverse(baseValue) : baseValue;
|
|
1494
|
+
perSide[out.sideIndex].push({ column: out.baseColumn, value: written });
|
|
1495
|
+
// NB: a present `out.domain` (an `inverse`-profile restriction) would conjoin
|
|
1496
|
+
// into the identifying predicate. No shipped invertibility profile produces a
|
|
1497
|
+
// domain (`x ± k` is unrestricted over integers), and the capture path that now
|
|
1498
|
+
// backs EVERY side's identification (`__vmupd_keys`) does not yet thread
|
|
1499
|
+
// per-assignment domains — deferred uniformly until a domain-bearing profile
|
|
1500
|
+
// lands (§ Scalar Invertibility).
|
|
1501
|
+
}
|
|
1502
|
+
// Existence-flip contradiction (§ Existence columns): `set <npCol> = …, hasB = false`
|
|
1503
|
+
// cannot both delete the non-preserved side and write one of its columns; an np-column
|
|
1504
|
+
// write always emits a matched per-side UPDATE, so a non-empty `perSide[side]` on a
|
|
1505
|
+
// delete side is the contradiction. `hasB = true, hasB = false` (insert+delete the same
|
|
1506
|
+
// side) is the same conflict. Reject rather than silently picking one effect.
|
|
1507
|
+
for (const side of existenceDeleteSides) {
|
|
1508
|
+
if (perSide[side].length > 0 || existenceInsertSides.has(side)) {
|
|
1509
|
+
raiseMutationDiagnostic({
|
|
1510
|
+
reason: 'conflicting-assignment',
|
|
1511
|
+
table: view.name,
|
|
1512
|
+
message: `cannot write through view '${view.name}': an existence-flag write deletes base table '${analysis.sides[side].schema.name}' (the non-preserved side) while the same statement also writes one of its columns — the two effects contradict`,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
// Every affected view row's base-PK identities are captured ONCE up-front (before
|
|
1517
|
+
// any base op fires) and each per-side op reads its identifying values back from
|
|
1518
|
+
// that captured set via a correlated EXISTS over `__vmupd_keys` (matching all of the
|
|
1519
|
+
// side's PK columns — composite keys included), a mutation-order-independent identity
|
|
1520
|
+
// built from the ALREADY-planned join body (the builder materializes the capture; see
|
|
1521
|
+
// `buildMultiSourceKeyCapture`). This unifies the single-side and both-sides paths
|
|
1522
|
+
// onto the same identity (no live re-query of a re-planned AST body): a both-sides
|
|
1523
|
+
// update's FK-parent op can no longer rewrite a predicate column out from under the
|
|
1524
|
+
// FK-child op, and a single-side op — having no ordering hazard — reads the same
|
|
1525
|
+
// pre-mutation set it would have re-queried live.
|
|
1526
|
+
// Order FK-parent before FK-child by the n-way FK topological sort (matches insert
|
|
1527
|
+
// ordering intent and avoids surprising mid-statement FK states); source order within
|
|
1528
|
+
// an FK-equivalence class (and for self-joins, whose mutual edges fall back to
|
|
1529
|
+
// alias-declaration order — § Cycles, Self-Joins).
|
|
1530
|
+
const order = orderSides(analysis.sides);
|
|
1531
|
+
const ops = [];
|
|
1532
|
+
for (const sideIndex of order) {
|
|
1533
|
+
const assignments = perSide[sideIndex];
|
|
1534
|
+
if (assignments.length === 0)
|
|
1535
|
+
continue;
|
|
1536
|
+
const side = analysis.sides[sideIndex];
|
|
1537
|
+
const where = buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName);
|
|
1538
|
+
const statement = {
|
|
1539
|
+
type: 'update',
|
|
1540
|
+
table: tableIdentifier(side.schema),
|
|
1541
|
+
// Synthesised collision-proof correlation name on the lowered per-side target
|
|
1542
|
+
// (mirrors the single-source spine): the base builder registers it as the
|
|
1543
|
+
// target's AliasedScope alias, so a `__vm_self.col` operand emitted by the
|
|
1544
|
+
// capture read-back / owning-strip qualifications above binds the outer target
|
|
1545
|
+
// row regardless of a user value subquery's own FROM.
|
|
1546
|
+
alias: SELF_ALIAS,
|
|
1547
|
+
assignments,
|
|
1548
|
+
where,
|
|
1549
|
+
contextValues: stmt.contextValues,
|
|
1550
|
+
schemaPath: stmt.schemaPath,
|
|
1551
|
+
loc: stmt.loc,
|
|
1552
|
+
};
|
|
1553
|
+
ops.push({ table: side.table, op: 'update', statement });
|
|
1554
|
+
}
|
|
1555
|
+
// Materialize the null-extended rows: one insert per non-preserved side over the
|
|
1556
|
+
// captured partition (the affected rows whose non-preserved PK was captured null). The
|
|
1557
|
+
// matched UPDATE for the same side was already emitted by the per-side loop above (its
|
|
1558
|
+
// tag-allowance enforced there), so this only adds the create branch. § Outer Joins.
|
|
1559
|
+
for (const [sideIndex, cols] of nullExtendedBySide) {
|
|
1560
|
+
ops.push(buildNullExtendedInsert(ctx, view, analysis, sideIndex, cols, registerCapturedExpr, stmt, captureRelationName));
|
|
1561
|
+
}
|
|
1562
|
+
// Existence-flip deletes (§ Existence columns): `set hasB = false` removes the matched
|
|
1563
|
+
// non-preserved rows (their captured PK is non-null; a null-extended row's captured PK
|
|
1564
|
+
// is null, so the same captured-key EXISTS naturally excludes it). The preserved side is
|
|
1565
|
+
// untouched, so a deleted row reads back null-extended (`hasB` now false).
|
|
1566
|
+
for (const sideIndex of existenceDeleteSides) {
|
|
1567
|
+
const side = analysis.sides[sideIndex];
|
|
1568
|
+
const where = buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName);
|
|
1569
|
+
const statement = {
|
|
1570
|
+
type: 'delete',
|
|
1571
|
+
table: tableIdentifier(side.schema),
|
|
1572
|
+
where,
|
|
1573
|
+
contextValues: stmt.contextValues,
|
|
1574
|
+
schemaPath: stmt.schemaPath,
|
|
1575
|
+
loc: stmt.loc,
|
|
1576
|
+
};
|
|
1577
|
+
ops.push({ table: side.table, op: 'delete', statement });
|
|
1578
|
+
}
|
|
1579
|
+
if (ops.length === 0) {
|
|
1580
|
+
// No assignment routed (e.g. only computed columns) — caught above as
|
|
1581
|
+
// no-inverse, so this is unreachable; guard for safety.
|
|
1582
|
+
raiseMutationDiagnostic({
|
|
1583
|
+
reason: 'no-inverse',
|
|
1584
|
+
table: view.name,
|
|
1585
|
+
message: `cannot write through view '${view.name}': no writable base column targeted by the update`,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
return ops;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Build the null-extended materialization INSERT for a non-preserved outer-join side:
|
|
1592
|
+
* `insert into <np> (<joinKey>, <set cols…>) select k.<jk>, min(k.<val…>) from __vmupd_keys k
|
|
1593
|
+
* where <every np PK k col> is null and k.<jk> is not null group by k.<jk>` (§ Outer Joins —
|
|
1594
|
+
* Updates). The `group by k.<jk>` de-dups per dangling join key so a shared missing partner
|
|
1595
|
+
* materializes exactly once (a fan-out of N preserved rows would otherwise double-insert the
|
|
1596
|
+
* partner PK); the value projections are `min` so each is single-valued per group. It
|
|
1597
|
+
* fires only for the affected rows the join null-extended (the non-preserved PK captured
|
|
1598
|
+
* null) whose preserved-side join key is non-null (a null key cannot seed a joinable row).
|
|
1599
|
+
* The new row carries the EC join key (so the preserved row joins it), the assigned
|
|
1600
|
+
* value(s) read from the same pre-mutation `__vmupd_keys` capture the matched UPDATE reads,
|
|
1601
|
+
* and base defaults for everything else; a NOT NULL base column without a default that no
|
|
1602
|
+
* value covers raises `null-extended-create-conflict`.
|
|
1603
|
+
*
|
|
1604
|
+
* Built as a pure AST `BaseOp` (an insert-from-select over `__vmupd_keys`, resolved by the
|
|
1605
|
+
* builder's `cteNodes` injection) — no new plan-node substrate: the existing
|
|
1606
|
+
* capture-materialize-then-drain machinery already supplies the pre-mutation partition.
|
|
1607
|
+
*/
|
|
1608
|
+
function buildNullExtendedInsert(_ctx, view, analysis, npSideIndex, cols, registerCapturedExpr, stmt, captureRelationName = MS_UPDATE_KEYS_CTE) {
|
|
1609
|
+
const npSide = analysis.sides[npSideIndex];
|
|
1610
|
+
const { npJoinColumn, preservedExpr } = outerJoinInsertKey(view, analysis, npSideIndex);
|
|
1611
|
+
const jkAlias = registerCapturedExpr(`nejk:${npSideIndex}`, preservedExpr);
|
|
1612
|
+
// Insert columns: the non-preserved join column (= the captured preserved-side join
|
|
1613
|
+
// value, so the preserved row joins the freshly materialized row) followed by each
|
|
1614
|
+
// assigned base column (= its captured value). The join column is threaded once.
|
|
1615
|
+
//
|
|
1616
|
+
// De-dup per dangling join key: a `group by k.<jkAlias>` collapses the N preserved rows
|
|
1617
|
+
// that share one missing partner to a single materialized row (else N rows projecting the
|
|
1618
|
+
// same join key would each insert the partner PK → `UNIQUE constraint failed`). The join
|
|
1619
|
+
// column projection IS the GROUP BY key (bare); each value column is wrapped in `min` so
|
|
1620
|
+
// it is single-valued per group — a no-op for a constant / np-only SET, a deterministic
|
|
1621
|
+
// pick for a value that differs per preserved row (mirrors the matched read-back's `min`).
|
|
1622
|
+
const targetColumns = [npJoinColumn];
|
|
1623
|
+
const projections = [
|
|
1624
|
+
{ type: 'column', expr: { type: 'column', name: jkAlias, table: 'k' }, alias: npJoinColumn },
|
|
1625
|
+
];
|
|
1626
|
+
const joinColLower = npJoinColumn.toLowerCase();
|
|
1627
|
+
for (const c of cols) {
|
|
1628
|
+
if (c.baseColumn.toLowerCase() === joinColLower)
|
|
1629
|
+
continue; // join column already threaded
|
|
1630
|
+
targetColumns.push(c.baseColumn);
|
|
1631
|
+
projections.push({ type: 'column', expr: { type: 'function', name: 'min', args: [{ type: 'column', name: c.valAlias, table: 'k' }] }, alias: c.baseColumn });
|
|
1632
|
+
}
|
|
1633
|
+
assertNullExtendedInsertCovered(view, npSide.schema, targetColumns);
|
|
1634
|
+
// Restrict to the null-extended partition: every captured PK column of the non-preserved
|
|
1635
|
+
// side is null (no join match), and the preserved join key is non-null (a null key has
|
|
1636
|
+
// no joinable row to create).
|
|
1637
|
+
const conds = requireKeyColumns(view, npSide).map((_pk, j) => ({ type: 'unary', operator: 'IS NULL', expr: { type: 'column', name: keyColumnName(npSideIndex, j), table: 'k' } }));
|
|
1638
|
+
conds.push({ type: 'unary', operator: 'IS NOT NULL', expr: { type: 'column', name: jkAlias, table: 'k' } });
|
|
1639
|
+
const where = conds.reduce((acc, c) => combineAnd(acc, c));
|
|
1640
|
+
const select = {
|
|
1641
|
+
type: 'select',
|
|
1642
|
+
columns: projections,
|
|
1643
|
+
from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
|
|
1644
|
+
where,
|
|
1645
|
+
groupBy: [{ type: 'column', name: jkAlias, table: 'k' }],
|
|
1646
|
+
};
|
|
1647
|
+
const statement = {
|
|
1648
|
+
type: 'insert',
|
|
1649
|
+
table: tableIdentifier(npSide.schema),
|
|
1650
|
+
columns: targetColumns,
|
|
1651
|
+
source: select,
|
|
1652
|
+
contextValues: stmt.contextValues,
|
|
1653
|
+
schemaPath: stmt.schemaPath,
|
|
1654
|
+
loc: stmt.loc,
|
|
1655
|
+
};
|
|
1656
|
+
return { table: npSide.table, op: 'insert', statement };
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* The non-preserved side's join column + the preserved partner's join value for the
|
|
1660
|
+
* null-extended materialization insert. Walks the join's cross-side equalities
|
|
1661
|
+
* ({@link collectCrossSideEqualities}) for one connecting the non-preserved side to a
|
|
1662
|
+
* PRESERVED side: the non-preserved column is set to the preserved value, so the
|
|
1663
|
+
* materialized row joins back to the preserved row. A non-preserved side related to no
|
|
1664
|
+
* preserved side by an equi-join key cannot be materialized with a joinable key — rejected
|
|
1665
|
+
* `unsupported-outer-join-update`.
|
|
1666
|
+
*
|
|
1667
|
+
* Only a **single-column** join key is materializable: the insert threads exactly one
|
|
1668
|
+
* non-preserved join column, so a composite key (the non-preserved side equated on more
|
|
1669
|
+
* than one distinct column) would leave the extra predicate(s) unsatisfied — the freshly
|
|
1670
|
+
* inserted row would NOT join back to the preserved row (a silent non-join leaving a stray
|
|
1671
|
+
* unreachable row), so it is rejected `unsupported-outer-join-update`. Mirrors the
|
|
1672
|
+
* inner-join insert envelope's single-column shared-key restriction
|
|
1673
|
+
* ({@link extractJoinKeyColumns}); the matched-update branch (keyed on the full np PK) is
|
|
1674
|
+
* unaffected, but the whole non-preserved update rejects at plan time since the create
|
|
1675
|
+
* branch cannot be expressed (the conservative, data-independent precedent of
|
|
1676
|
+
* {@link assertNullExtendedInsertCovered}).
|
|
1677
|
+
*/
|
|
1678
|
+
function outerJoinInsertKey(view, analysis, npSideIndex) {
|
|
1679
|
+
const eqs = collectCrossSideEqualities(analysis.sel.from, analysis.sides);
|
|
1680
|
+
// Every cross-side equality the non-preserved side participates in (its own column +
|
|
1681
|
+
// the partner side/column it is equated to).
|
|
1682
|
+
const npEqs = eqs.flatMap(eq => {
|
|
1683
|
+
if (eq.sideA === npSideIndex)
|
|
1684
|
+
return [{ npCol: eq.colA, partnerSide: eq.sideB, partnerCol: eq.colB }];
|
|
1685
|
+
if (eq.sideB === npSideIndex)
|
|
1686
|
+
return [{ npCol: eq.colB, partnerSide: eq.sideA, partnerCol: eq.colA }];
|
|
1687
|
+
return [];
|
|
1688
|
+
});
|
|
1689
|
+
// Reject a composite join key (the np side equated on >1 distinct column): the
|
|
1690
|
+
// single-column materialization insert cannot satisfy the extra predicate(s), so the
|
|
1691
|
+
// new row would not join back (a silent non-join). Distinct by np column name — the
|
|
1692
|
+
// same np column equated to several partners (a 3-way shared key) still threads once.
|
|
1693
|
+
const distinctNpCols = new Set(npEqs.map(e => e.npCol.toLowerCase()));
|
|
1694
|
+
if (distinctNpCols.size > 1) {
|
|
1695
|
+
raiseMutationDiagnostic({
|
|
1696
|
+
reason: 'unsupported-outer-join-update',
|
|
1697
|
+
table: view.name,
|
|
1698
|
+
message: `cannot write through view '${view.name}': the non-preserved side (base table '${analysis.sides[npSideIndex].schema.name}') is related to the join by a composite key (${[...distinctNpCols].join(', ')}); a null-extended row can only be materialized through a single-column join key`,
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
const match = npEqs.find(e => analysis.sides[e.partnerSide].preserved);
|
|
1702
|
+
if (match) {
|
|
1703
|
+
return {
|
|
1704
|
+
npJoinColumn: match.npCol,
|
|
1705
|
+
preservedExpr: { type: 'column', name: match.partnerCol, table: analysis.sides[match.partnerSide].alias },
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
return raiseMutationDiagnostic({
|
|
1709
|
+
reason: 'unsupported-outer-join-update',
|
|
1710
|
+
table: view.name,
|
|
1711
|
+
message: `cannot write through view '${view.name}': the non-preserved side (base table '${analysis.sides[npSideIndex].schema.name}') is not related to a preserved side by an equi-join key, so a null-extended row cannot be materialized with a joinable key`,
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Reject a NOT NULL base column on the non-preserved side that the null-extended
|
|
1716
|
+
* materialization insert leaves unset (no default, no covering value) — the row cannot be
|
|
1717
|
+
* created. Mirrors {@link assertNoMissingNotNull} but raises `null-extended-create-conflict`
|
|
1718
|
+
* (the outer-join create-side diagnostic), distinguishing a missing materialization value
|
|
1719
|
+
* from an ordinary insert's missing column.
|
|
1720
|
+
*/
|
|
1721
|
+
function assertNullExtendedInsertCovered(view, schema, covered) {
|
|
1722
|
+
const set = new Set(covered.map(c => c.toLowerCase()));
|
|
1723
|
+
for (const col of schema.columns) {
|
|
1724
|
+
if (col.generated || !col.notNull || col.defaultValue !== null)
|
|
1725
|
+
continue;
|
|
1726
|
+
if (set.has(col.name.toLowerCase()))
|
|
1727
|
+
continue;
|
|
1728
|
+
raiseMutationDiagnostic({
|
|
1729
|
+
reason: 'null-extended-create-conflict',
|
|
1730
|
+
column: col.name,
|
|
1731
|
+
table: view.name,
|
|
1732
|
+
message: `cannot update through view '${view.name}': materializing a null-extended row on base table '${schema.name}' would leave NOT NULL column '${col.name}' (no default) unset, so the non-preserved-side row cannot be created`,
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* The **preserved** side indices — the DELETE candidate set (§ Outer Joins —
|
|
1738
|
+
* Deletes: "deleting from the preserved side is the only way for the joined row to
|
|
1739
|
+
* disappear from the view"). For an inner join every side is preserved, so this is the
|
|
1740
|
+
* full set `0..n-1`. For a LEFT/RIGHT outer join it is the single
|
|
1741
|
+
* preserved side. A `full` outer join has *no* preserved side; the caller falls back to
|
|
1742
|
+
* the full candidate set there (every side is both preserved and non-preserved).
|
|
1743
|
+
*/
|
|
1744
|
+
function preservedSideIndices(sides) {
|
|
1745
|
+
return sides.flatMap((s, i) => s.preserved ? [i] : []);
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* The correlated EXISTS identifying predicate a per-side base op routes on:
|
|
1749
|
+
* `exists (select 1 from __vmupd_keys k where k.k<side>_0 = <pk0> [and k.k<side>_1 =
|
|
1750
|
+
* <pk1> …])` — matching ALL of the side's PK columns (composite keys included) against
|
|
1751
|
+
* the up-front materialized key set (the both-sides-assigned UPDATE path, every
|
|
1752
|
+
* single-side update/delete, and the multi-side DELETE fan-out; see
|
|
1753
|
+
* {@link MS_UPDATE_KEYS_CTE}). The right-hand `<pk_j>` are unqualified, so they bind to
|
|
1754
|
+
* the base op's own target table; `k.k<side>_<j>` reads the captured column. The builder
|
|
1755
|
+
* injects `__vmupd_keys` into the base op's planning `cteNodes` (resolving to the
|
|
1756
|
+
* context-backed key relation), so this is read by descriptor rather than re-querying
|
|
1757
|
+
* the join body. (EXISTS — not a row-value `IN` — to reuse the UPDATE RETURNING
|
|
1758
|
+
* re-query's correlation shape; one pattern.)
|
|
1759
|
+
*/
|
|
1760
|
+
function buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName = MS_UPDATE_KEYS_CTE) {
|
|
1761
|
+
const keyCols = requireKeyColumns(view, side);
|
|
1762
|
+
const conds = keyCols.map((pkCol, j) => ({
|
|
1763
|
+
type: 'binary',
|
|
1764
|
+
operator: '=',
|
|
1765
|
+
left: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' },
|
|
1766
|
+
right: { type: 'column', name: pkCol },
|
|
1767
|
+
}));
|
|
1768
|
+
return {
|
|
1769
|
+
type: 'exists',
|
|
1770
|
+
subquery: {
|
|
1771
|
+
type: 'select',
|
|
1772
|
+
columns: [{ type: 'column', expr: { type: 'literal', value: 1 } }],
|
|
1773
|
+
from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
|
|
1774
|
+
where: conds.reduce((acc, c) => combineAnd(acc, c)),
|
|
1775
|
+
},
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Mint a context-backed key relation (`InternalRecursiveCTERefNode`) over a
|
|
1780
|
+
* capture's descriptor — what a multi-side base op's identifying `in`-subquery or
|
|
1781
|
+
* the RETURNING re-query's EXISTS scans `__vmupd_keys` through. Fresh attribute ids
|
|
1782
|
+
* per call (each ref lives in its own subtree); the **descriptor** identity is what
|
|
1783
|
+
* ties it to the rows the emitter materializes.
|
|
1784
|
+
*
|
|
1785
|
+
* `captureRelationName` is used for BOTH the node's display CTE name and every
|
|
1786
|
+
* attribute's `sourceRelation`, so a half-updated node can never keep the constant as
|
|
1787
|
+
* `sourceRelation` while the caller injects under a fresh name. It defaults to the
|
|
1788
|
+
* capture's own {@link MultiSourceKeyCapture.relationName} (falling back to
|
|
1789
|
+
* {@link MS_UPDATE_KEYS_CTE}), so a ref minted from a fresh-named capture is
|
|
1790
|
+
* self-consistent without the caller re-passing the name.
|
|
1791
|
+
*/
|
|
1792
|
+
export function makeMultiSourceKeyRef(scope, capture, captureRelationName = capture.relationName ?? MS_UPDATE_KEYS_CTE) {
|
|
1793
|
+
const keyAttrs = capture.keyColumns.map(c => ({
|
|
1794
|
+
id: PlanNode.nextAttrId(),
|
|
1795
|
+
name: c.name,
|
|
1796
|
+
type: c.type,
|
|
1797
|
+
sourceRelation: captureRelationName,
|
|
1798
|
+
}));
|
|
1799
|
+
const keyRelType = {
|
|
1800
|
+
typeClass: 'relation',
|
|
1801
|
+
isReadOnly: true,
|
|
1802
|
+
isSet: false,
|
|
1803
|
+
columns: capture.keyColumns.map(c => ({ name: c.name, type: c.type })),
|
|
1804
|
+
keys: [],
|
|
1805
|
+
rowConstraints: [],
|
|
1806
|
+
};
|
|
1807
|
+
return new InternalRecursiveCTERefNode(scope, captureRelationName, keyAttrs, keyRelType, capture.descriptor);
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* A planning context with `capture` injected into `cteNodes` under its relation name (the
|
|
1811
|
+
* capture's {@link MultiSourceKeyCapture.relationName}, defaulting to {@link MS_UPDATE_KEYS_CTE}),
|
|
1812
|
+
* so a base op's / inner capture's `from <relationName> k` reference resolves to the capture's
|
|
1813
|
+
* context-backed key relation (over the shared capture descriptor). The injected ref's own
|
|
1814
|
+
* name is pinned to the SAME `relationName` so the map key and the ref agree — a nested capture
|
|
1815
|
+
* injects under its fresh `__vmupd_keys$N` name while shadowing nothing under the default. A
|
|
1816
|
+
* fresh ref per call keeps each consumer's subtree from sharing a node instance.
|
|
1817
|
+
*
|
|
1818
|
+
* Shared by the multi-source UPDATE/DELETE builder (`view-mutation-builder.ts`) and the set-op
|
|
1819
|
+
* join-leg compose (`set-op.ts`, which injects the OUTER set-op capture so an inner per-branch
|
|
1820
|
+
* capture's `memberExists` filter resolves against it).
|
|
1821
|
+
*/
|
|
1822
|
+
export function withKeyCapture(ctx, capture) {
|
|
1823
|
+
const cteNodes = new Map(ctx.cteNodes ?? []);
|
|
1824
|
+
const relationName = capture.relationName ?? MS_UPDATE_KEYS_CTE;
|
|
1825
|
+
cteNodes.set(relationName, makeMultiSourceKeyRef(ctx.scope, capture, relationName));
|
|
1826
|
+
return { ...ctx, cteNodes };
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* The distinct join-side indices the emitted base ops target (each base op carries the
|
|
1830
|
+
* planned side's `TableReferenceNode`), sorted ascending — the sides whose PKs the capture
|
|
1831
|
+
* must project so each op's `select k<side> from <relationName>` resolves. Shared by the
|
|
1832
|
+
* standalone multi-source UPDATE/DELETE builder (`view-mutation-builder.ts`) and the set-op
|
|
1833
|
+
* join-leg compose (`set-op.ts`), which each pass it straight to
|
|
1834
|
+
* {@link buildMultiSourceKeyCapture}.
|
|
1835
|
+
*/
|
|
1836
|
+
export function capturedSideIndices(baseOps, analysis) {
|
|
1837
|
+
const set = new Set();
|
|
1838
|
+
for (const op of baseOps) {
|
|
1839
|
+
const i = analysis.sides.findIndex(s => s.table.id === op.table.id);
|
|
1840
|
+
if (i >= 0)
|
|
1841
|
+
set.add(i);
|
|
1842
|
+
}
|
|
1843
|
+
return [...set].sort((a, b) => a - b);
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Build the up-front identity capture: each affected view row's base-PK identities,
|
|
1847
|
+
* by the same identifying predicate the base ops route on (user WHERE → base ∧ body
|
|
1848
|
+
* WHERE). Built as **plan nodes directly over the ALREADY-planned join body**
|
|
1849
|
+
* (`analysis.joinNode` + `analysis.joinScope`) — `Project_{k<side>_<j>}(Filter_{idPred}
|
|
1850
|
+
* (joinNode))` — instead of re-planning a cloned AST FROM, so the body is planned
|
|
1851
|
+
* ONCE (§ Round-Trip Laws and the Derived Backward Walk). `preserveInputColumns=false`
|
|
1852
|
+
* ⇒ the materialized rows are exactly the requested key columns, positionally aligned
|
|
1853
|
+
* to the `k<side>_<j>` columns every reader scans back (`keyColumns` and the projection
|
|
1854
|
+
* derive from the same `sideIndices` order; a composite-PK side contributes one column
|
|
1855
|
+
* per PK column).
|
|
1856
|
+
*
|
|
1857
|
+
* `sideIndices` selects which sides' PKs to capture (each requires ≥1 PK column via
|
|
1858
|
+
* {@link requireKeyColumns}; a keyless side is rejected with `unsupported-join`). The
|
|
1859
|
+
* builder passes exactly the sides whose base ops read the capture (plus all sides, for
|
|
1860
|
+
* an UPDATE with RETURNING whose EXISTS correlates the full joined row), so a single-
|
|
1861
|
+
* side write never forces a PK on an untouched side.
|
|
1862
|
+
*
|
|
1863
|
+
* Op-agnostic: takes the user `where` directly (an UPDATE's or a DELETE's) — the
|
|
1864
|
+
* identifying predicate is the same either way.
|
|
1865
|
+
*
|
|
1866
|
+
* `captureRelationName` is the AST relation name the resulting capture is injected
|
|
1867
|
+
* under (and that the base-op predicates `decomposeUpdate`/`decomposeDelete` emit read
|
|
1868
|
+
* back); it is stamped onto the returned {@link MultiSourceKeyCapture.relationName} so
|
|
1869
|
+
* downstream injection/RETURNING read it from the capture object rather than
|
|
1870
|
+
* re-deriving the literal. Defaults to {@link MS_UPDATE_KEYS_CTE} — the standalone
|
|
1871
|
+
* multi-source path is byte-identical; a nested capture passes a fresh name.
|
|
1872
|
+
*/
|
|
1873
|
+
export function buildMultiSourceKeyCapture(ctx, view, where, analysis, sideIndices, sourceValues, captureRelationName = MS_UPDATE_KEYS_CTE) {
|
|
1874
|
+
// The identifying predicate (user WHERE → base terms ∧ the body's own WHERE), built
|
|
1875
|
+
// as a ScalarPlanNode over the planned join body's own scope — the exact scope
|
|
1876
|
+
// `buildSelectStmt` resolved the body against. The body WHERE is conjoined
|
|
1877
|
+
// explicitly (the source is the raw `joinNode`, before the body's σ), so the
|
|
1878
|
+
// captured set is byte-identical to the retired re-plan over the cloned FROM.
|
|
1879
|
+
const idPredicateAst = buildIdentifyingPredicate(ctx, analysis, where, view);
|
|
1880
|
+
const predicate = idPredicateAst
|
|
1881
|
+
? buildExpression({ ...ctx, scope: analysis.joinScope }, idPredicateAst)
|
|
1882
|
+
: undefined;
|
|
1883
|
+
const filtered = predicate
|
|
1884
|
+
? new FilterNode(analysis.joinScope, analysis.joinNode, predicate)
|
|
1885
|
+
: analysis.joinNode;
|
|
1886
|
+
// One capture column per requested side per PK column: `k<side>_<j>`. A composite-PK
|
|
1887
|
+
// side projects all its PK columns; the readers' EXISTS correlate on the same set.
|
|
1888
|
+
const keyColumns = [];
|
|
1889
|
+
const projections = [];
|
|
1890
|
+
for (const i of sideIndices) {
|
|
1891
|
+
const side = analysis.sides[i];
|
|
1892
|
+
const pkCols = requireKeyColumns(view, side);
|
|
1893
|
+
pkCols.forEach((pk, j) => {
|
|
1894
|
+
const name = keyColumnName(i, j);
|
|
1895
|
+
keyColumns.push({ name, type: columnSchemaToScalarType(columnByName(side.schema, pk)) });
|
|
1896
|
+
projections.push({
|
|
1897
|
+
node: buildExpression({ ...ctx, scope: analysis.joinScope }, { type: 'column', name: pk, table: side.alias }),
|
|
1898
|
+
alias: name,
|
|
1899
|
+
});
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
// Cross-source SET read values: project each partner base column the SET reads under
|
|
1903
|
+
// its stable `srcN` alias (over the same join-body scope), so every per-side base op's
|
|
1904
|
+
// correlated `select srcN from __vmupd_keys k where …` reads the captured pre-mutation
|
|
1905
|
+
// value. Appended AFTER the per-side PK columns, positionally aligned with the readers'
|
|
1906
|
+
// `keyColumns` (which are pushed in the same order).
|
|
1907
|
+
for (const sv of sourceValues ?? []) {
|
|
1908
|
+
const node = buildExpression({ ...ctx, scope: analysis.joinScope }, sv.expr);
|
|
1909
|
+
keyColumns.push({ name: sv.alias, type: node.getType() });
|
|
1910
|
+
projections.push({ node, alias: sv.alias });
|
|
1911
|
+
}
|
|
1912
|
+
const source = new ProjectNode(analysis.joinScope, filtered, projections, undefined, undefined, false);
|
|
1913
|
+
return { source, descriptor: {}, keyColumns, relationName: captureRelationName };
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Build the post-mutation RETURNING re-query for a multi-source UPDATE
|
|
1917
|
+
* (docs/view-updateability.md § `returning`). A re-query that matched by the *user
|
|
1918
|
+
* predicate* cannot recapture a row whose predicate column the update itself
|
|
1919
|
+
* rewrote (the changed row no longer matches), so this matches by the captured
|
|
1920
|
+
* **identity** instead: project the view-spelled, base-term RETURNING columns over
|
|
1921
|
+
* the post-mutation join body, restricted to the captured identities by a correlated
|
|
1922
|
+
* `exists (select 1 from __vmupd_keys k where <per-side identity>)` — so a row the
|
|
1923
|
+
* update pushed *out* of the view's filter (or whose predicate column it rewrote) is
|
|
1924
|
+
* still returned (single-source NEW semantics). It keeps only the structural join
|
|
1925
|
+
* ON-condition; the body/user WHERE is intentionally NOT re-applied.
|
|
1926
|
+
*
|
|
1927
|
+
* The per-side identity is **preserved-keyed**: a preserved side matches by exact
|
|
1928
|
+
* per-PK-column equality (`k.k<p>_<j> = s<p>.pk<j>`), while a non-preserved (outer-join
|
|
1929
|
+
* null-extended) side uses a matched-OR-null disjunction `(AND_j k.k<np>_<j> =
|
|
1930
|
+
* s<np>.pk<j>) OR (AND_j k.k<np>_<j> is null)`. This re-keys the re-query off the
|
|
1931
|
+
* **stable preserved-side identity** so a freshly-materialized null-extended row (whose
|
|
1932
|
+
* non-preserved PK was captured NULL) surfaces via its preserved-side equalities alone,
|
|
1933
|
+
* rather than being silently dropped by a `NULL = <minted pk>` match. For an all-
|
|
1934
|
+
* preserved (inner) join every side is exact equality — byte-identical to the prior
|
|
1935
|
+
* behavior, so inner-join RETURNING is unchanged.
|
|
1936
|
+
*
|
|
1937
|
+
* Reads the shared {@link MultiSourceKeyCapture} the builder materializes
|
|
1938
|
+
* before the base ops fire (via its own freshly-minted key ref over the same
|
|
1939
|
+
* descriptor). The capture covers ALL sides for an UPDATE with RETURNING, so this
|
|
1940
|
+
* correlates the full joined row's identity.
|
|
1941
|
+
*/
|
|
1942
|
+
export function buildMultiSourceUpdateReturning(ctx, view, stmt, capture, analysis) {
|
|
1943
|
+
const returningCols = stmt.returning;
|
|
1944
|
+
// Restrict the POST-mutation join body to the captured identities, built as plan
|
|
1945
|
+
// nodes over the ALREADY-planned `joinNode` (its structural ON-condition only —
|
|
1946
|
+
// the body/user WHERE is intentionally NOT re-applied) — no AST re-plan of the
|
|
1947
|
+
// body. The EXISTS subquery resolves `__vmupd_keys` via `cteNodes` to a fresh key
|
|
1948
|
+
// ref over the shared capture descriptor; `s<side>.pk<j>` correlate to the outer
|
|
1949
|
+
// join row through `joinScope`.
|
|
1950
|
+
//
|
|
1951
|
+
// The per-side identity predicate is AND'd over all sides:
|
|
1952
|
+
// - a **preserved** side keys by exact per-PK-column equality (`AND_j k.k<side>_<j>
|
|
1953
|
+
// = s<side>.pk<j>`) — its PK is stable across the mutation and uniquely identifies
|
|
1954
|
+
// the view row (the premise that makes a non-preserved column updatable at all);
|
|
1955
|
+
// - a **non-preserved** (outer-join null-extended) side keys by a matched-OR-null
|
|
1956
|
+
// disjunction `(AND_j k.k<np>_<j> = s<np>.pk<j>) OR (AND_j k.k<np>_<j> is null)`.
|
|
1957
|
+
//
|
|
1958
|
+
// A *matched* capture row (np PK non-null) takes the matched branch and finds the
|
|
1959
|
+
// stable np row; the null branch is false (the np PK is non-null). A *materialized
|
|
1960
|
+
// null-extended* capture row (np PK captured NULL — it had no pre-mutation partner)
|
|
1961
|
+
// fails the matched branch (`null = …` is not-true) and takes the null branch, so it
|
|
1962
|
+
// is identified by the preserved-side equalities ALONE — surfacing the freshly-minted
|
|
1963
|
+
// partner row (and a preserved-side update touching a still-null-extended row, the
|
|
1964
|
+
// latent partial-set bug #2). SQL three-valued comparison keeps the two branches
|
|
1965
|
+
// disjoint, so no explicit `is not null` guard is needed.
|
|
1966
|
+
const sideConds = analysis.sides.map((side, sideIndex) => {
|
|
1967
|
+
const pkCols = requireKeyColumns(view, side);
|
|
1968
|
+
const exact = pkCols.map((pk, j) => ({
|
|
1969
|
+
type: 'binary',
|
|
1970
|
+
operator: '=',
|
|
1971
|
+
left: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' },
|
|
1972
|
+
right: { type: 'column', name: pk, table: side.alias },
|
|
1973
|
+
})).reduce((acc, c) => combineAnd(acc, c));
|
|
1974
|
+
if (side.preserved)
|
|
1975
|
+
return exact;
|
|
1976
|
+
// Null-extended branch: every captured PK column of this non-preserved side is null
|
|
1977
|
+
// (no pre-mutation join partner), so the row is identified by the preserved sides'
|
|
1978
|
+
// exact equalities alone.
|
|
1979
|
+
const allNull = pkCols.map((_pk, j) => ({ type: 'unary', operator: 'IS NULL', expr: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' } }))
|
|
1980
|
+
.reduce((acc, c) => combineAnd(acc, c));
|
|
1981
|
+
return { type: 'binary', operator: 'OR', left: exact, right: allNull };
|
|
1982
|
+
});
|
|
1983
|
+
// Read the capture's own relation name so a fresh-named capture's RETURNING re-query
|
|
1984
|
+
// reads back from (and injects under) the same relation its base ops do — the constant
|
|
1985
|
+
// is only the default the standalone path stamps.
|
|
1986
|
+
const captureRelationName = capture.relationName ?? MS_UPDATE_KEYS_CTE;
|
|
1987
|
+
const keyRef = makeMultiSourceKeyRef(ctx.scope, capture, captureRelationName);
|
|
1988
|
+
const existsPredicateAst = {
|
|
1989
|
+
type: 'exists',
|
|
1990
|
+
subquery: {
|
|
1991
|
+
type: 'select',
|
|
1992
|
+
columns: [{ type: 'column', expr: { type: 'literal', value: 1 } }],
|
|
1993
|
+
from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
|
|
1994
|
+
where: sideConds.reduce((acc, c) => combineAnd(acc, c)),
|
|
1995
|
+
},
|
|
1996
|
+
};
|
|
1997
|
+
const cteNodes = new Map(ctx.cteNodes ?? []);
|
|
1998
|
+
cteNodes.set(captureRelationName, keyRef);
|
|
1999
|
+
const existsNode = buildExpression({ ...ctx, scope: analysis.joinScope, cteNodes }, existsPredicateAst);
|
|
2000
|
+
const filtered = new FilterNode(analysis.joinScope, analysis.joinNode, existsNode);
|
|
2001
|
+
// Project the view-spelled, base-term RETURNING columns over the filtered join.
|
|
2002
|
+
return buildMultiSourceReturningProjection(ctx, view, analysis, filtered, returningCols);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Build the multi-source RETURNING projection over a `filtered` join relation: lower
|
|
2006
|
+
* each RETURNING result column to its view-spelled, base-term form
|
|
2007
|
+
* ({@link buildReturningProjection}) and project it as a `ScalarPlanNode` over the
|
|
2008
|
+
* input through `analysis.joinScope`. `preserveInputColumns=false` ⇒ the output is
|
|
2009
|
+
* exactly the RETURNING columns. Shared by the UPDATE re-query (filter = the
|
|
2010
|
+
* post-mutation EXISTS-over-capture join) and the DELETE re-query (filter = the
|
|
2011
|
+
* pre-mutation identifying predicate over the raw join), which differ only in the
|
|
2012
|
+
* `filtered` input relation they pass.
|
|
2013
|
+
*/
|
|
2014
|
+
function buildMultiSourceReturningProjection(ctx, view, analysis, filtered, returningCols) {
|
|
2015
|
+
const projections = buildReturningProjection(ctx, view, analysis, returningCols).map((rc) => {
|
|
2016
|
+
const col = rc;
|
|
2017
|
+
return {
|
|
2018
|
+
node: buildExpression({ ...ctx, scope: analysis.joinScope }, col.expr),
|
|
2019
|
+
alias: col.alias,
|
|
2020
|
+
};
|
|
2021
|
+
});
|
|
2022
|
+
return new ProjectNode(analysis.joinScope, filtered, projections, undefined, undefined, false);
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Lower a multi-source UPDATE's RETURNING result columns to base terms over the
|
|
2026
|
+
* join body, preserving the **view spelling** of each output column. A bare view-
|
|
2027
|
+
* column ref substitutes to its base term aliased to the column's written spelling
|
|
2028
|
+
* (so a renamed view col `eid`→base `id` still surfaces as `eid`); a computed
|
|
2029
|
+
* RETURNING expression has its nested view-column refs substituted; `returning *`
|
|
2030
|
+
* expands to every view output column's base term aliased to its display name.
|
|
2031
|
+
*/
|
|
2032
|
+
function buildReturningProjection(ctx, view, analysis, returningCols) {
|
|
2033
|
+
const out = [];
|
|
2034
|
+
for (const rc of returningCols) {
|
|
2035
|
+
if (rc.type === 'all') {
|
|
2036
|
+
assertReturningStarQualifier(rc.table, view.name);
|
|
2037
|
+
for (const col of analysis.outColumns) {
|
|
2038
|
+
const baseExpr = analysis.viewColToBaseRef.get(col.name);
|
|
2039
|
+
if (!baseExpr) {
|
|
2040
|
+
raiseMutationDiagnostic({
|
|
2041
|
+
reason: 'returning-through-view',
|
|
2042
|
+
table: view.name,
|
|
2043
|
+
message: `cannot expand 'returning *' through view '${view.name}': no base term for column '${col.displayName}'`,
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
out.push({ type: 'column', expr: cloneExpr(baseExpr), alias: col.displayName });
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
else {
|
|
2050
|
+
// Scope guard: a top-level RETURNING reference must name a view column —
|
|
2051
|
+
// otherwise it would pass through `substituteViewColumns` unmapped and
|
|
2052
|
+
// re-bind against a base table in the re-query's join body (the same leak
|
|
2053
|
+
// the where/set guard closes). Parity with the single-source RETURNING guard.
|
|
2054
|
+
guardTopLevelScope(rc.expr, analysis, view);
|
|
2055
|
+
const substituted = substituteViewColumns(ctx, rc.expr, analysis.viewColToBaseRef, view, analysis.sides);
|
|
2056
|
+
// Preserve the user's view spelling as the output name: an explicit alias
|
|
2057
|
+
// wins; a bare column ref keeps its own name; otherwise leave it unnamed.
|
|
2058
|
+
const alias = rc.alias ?? (rc.expr.type === 'column' ? rc.expr.name : undefined);
|
|
2059
|
+
out.push({ type: 'column', expr: substituted, alias });
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return out;
|
|
2063
|
+
}
|
|
2064
|
+
// --- DELETE ---------------------------------------------------------------
|
|
2065
|
+
/**
|
|
2066
|
+
* Build the pre-mutation RETURNING re-query for a multi-source DELETE
|
|
2067
|
+
* (docs/view-updateability.md § `returning`). The OLD view image of the rows about
|
|
2068
|
+
* to vanish: project the view-spelled, base-term RETURNING columns over the raw
|
|
2069
|
+
* `analysis.joinNode` restricted to the identifying predicate (user WHERE → base ∧
|
|
2070
|
+
* body WHERE — the same predicate the key capture and base ops route on). Captured
|
|
2071
|
+
* `pre` (before the base ops fire) so it reads the live base tables through the join.
|
|
2072
|
+
*
|
|
2073
|
+
* Building the projection in **base terms** (rather than referencing the planned
|
|
2074
|
+
* body `root`'s output attribute ids) is what fixes a computed view column: a
|
|
2075
|
+
* computed column has no surviving intermediate attribute id after project-merge, so
|
|
2076
|
+
* a by-id reference dangles — recomputing from base columns has nothing fragile to
|
|
2077
|
+
* reference. Mirrors {@link buildMultiSourceUpdateReturning}; they differ only in the
|
|
2078
|
+
* filter + timing.
|
|
2079
|
+
*/
|
|
2080
|
+
export function buildMultiSourceDeleteReturning(ctx, view, stmt, analysis) {
|
|
2081
|
+
const idPredicateAst = buildIdentifyingPredicate(ctx, analysis, stmt.where, view);
|
|
2082
|
+
const predicate = idPredicateAst
|
|
2083
|
+
? buildExpression({ ...ctx, scope: analysis.joinScope }, idPredicateAst)
|
|
2084
|
+
: undefined;
|
|
2085
|
+
const filtered = predicate
|
|
2086
|
+
? new FilterNode(analysis.joinScope, analysis.joinNode, predicate)
|
|
2087
|
+
: analysis.joinNode;
|
|
2088
|
+
return buildMultiSourceReturningProjection(ctx, view, analysis, filtered, stmt.returning);
|
|
2089
|
+
}
|
|
2090
|
+
export function decomposeDelete(_ctx, view, analysis, stmt, captureRelationName = MS_UPDATE_KEYS_CTE) {
|
|
2091
|
+
// RETURNING through a multi-source delete is supported via a re-query of the
|
|
2092
|
+
// planned view body captured *before* the base delete fires (the builder); the
|
|
2093
|
+
// base op itself carries no RETURNING.
|
|
2094
|
+
// Scope guard: top-level `where` references must name view columns (parity with
|
|
2095
|
+
// the single-source spine).
|
|
2096
|
+
if (stmt.where)
|
|
2097
|
+
guardTopLevelScope(stmt.where, analysis, view);
|
|
2098
|
+
const sides = chooseDeleteSides(view, analysis);
|
|
2099
|
+
// Every base delete (single-side and multi-side fan-out alike) reads its
|
|
2100
|
+
// identifying values from the up-front identity capture the builder materializes
|
|
2101
|
+
// ONCE before any base op fires (a correlated EXISTS over `__vmupd_keys` matching
|
|
2102
|
+
// the side's PK columns), a mutation-order-independent set built from the
|
|
2103
|
+
// ALREADY-planned join body. So the first side's delete cannot empty the join out
|
|
2104
|
+
// from under a later side's identifying set (the predicate-honest multi-side
|
|
2105
|
+
// fan-out), and a single-side delete — having no ordering hazard — reads the same
|
|
2106
|
+
// pre-mutation set it would have re-queried live. (No live re-query of a re-planned
|
|
2107
|
+
// AST body.)
|
|
2108
|
+
// Order the base deletes. The **two-side fan-out over a 2-table join** orders by ON
|
|
2109
|
+
// DELETE action so the side whose removal clears the other's reference runs first
|
|
2110
|
+
// (`orderDeleteFanout`); a mutual FK whose actions no side order can satisfy under
|
|
2111
|
+
// immediate enforcement raises `mutual-fk-restrict-delete` at plan time — but ONLY
|
|
2112
|
+
// when the join provably correlates a mutual FK edge (the joined rows necessarily
|
|
2113
|
+
// cross-reference, so a RESTRICT necessarily fires). When the join correlates neither
|
|
2114
|
+
// edge (e.g. a join on non-FK columns), the schema-only reject is a data-independent
|
|
2115
|
+
// over-rejection: fall back to the fixed `[0, 1]` fan-out and defer to the runtime
|
|
2116
|
+
// RESTRICT pre-check (`runtime/foreign-key-actions.ts`) on the actual data.
|
|
2117
|
+
//
|
|
2118
|
+
// This plan-time mutual-FK analysis is **deliberately NOT generalized past two
|
|
2119
|
+
// sides** (§ Out of scope): an n-way (>2) delete uses the **reverse** FK-topological
|
|
2120
|
+
// order (FK-child before FK-parent) over the chosen sides — the FK-safe delete
|
|
2121
|
+
// direction — and defers any mutual-FK cycle wholesale to the runtime RESTRICT
|
|
2122
|
+
// pre-check. A single-side delete has no ordering hazard, so it keeps its trivial
|
|
2123
|
+
// order.
|
|
2124
|
+
let order;
|
|
2125
|
+
if (analysis.sides.length === 2 && sides.length === 2) {
|
|
2126
|
+
const fanoutOrder = orderDeleteFanout(analysis.sides);
|
|
2127
|
+
if (fanoutOrder === undefined) {
|
|
2128
|
+
if (joinCorrelatesMutualFk(analysis)) {
|
|
2129
|
+
const [a, b] = analysis.sides;
|
|
2130
|
+
raiseMutationDiagnostic({
|
|
2131
|
+
reason: 'mutual-fk-restrict-delete',
|
|
2132
|
+
table: view.name,
|
|
2133
|
+
message: `cannot delete through view '${view.name}': the joined row spans a mutual foreign key ('${a.schema.name}'↔'${b.schema.name}') whose ON DELETE actions cannot be satisfied in either order under immediate FK enforcement (deleting either side trips the other's RESTRICT, directly or transitively through a cascade); break the cycle outside the view — null out the referencing column(s) first, or restructure the offending ON DELETE action — before deleting through the view (a 'deferrable initially deferred' declaration does not help: RESTRICT is enforced immediately regardless)`,
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
// No mutual FK edge is correlated by the join — defer to the runtime
|
|
2137
|
+
// RESTRICT pre-check on the real data via the fixed fan-out order.
|
|
2138
|
+
order = [0, 1];
|
|
2139
|
+
}
|
|
2140
|
+
else {
|
|
2141
|
+
order = fanoutOrder;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
else {
|
|
2145
|
+
// Single side, or an n-way (>2) fan-out. Delete in **reverse** FK-topological
|
|
2146
|
+
// order — FK-CHILD before FK-parent — so a child's referencing row is gone before
|
|
2147
|
+
// its parent row is deleted (the canonical columnar-split shape: each member's PK
|
|
2148
|
+
// references the anchor's). The forward (parent-first) order trips the parent's
|
|
2149
|
+
// inbound RESTRICT/NO-ACTION under immediate FK enforcement and aborts the whole
|
|
2150
|
+
// statement. Child-first is unconditionally FK-safe (deleting a referencing row
|
|
2151
|
+
// never trips a constraint on the referenced row, for any ON DELETE action), so it
|
|
2152
|
+
// is the right default for both RESTRICT and CASCADE; a mutual-FK cycle still
|
|
2153
|
+
// defers wholesale to the runtime RESTRICT pre-check. A single-side delete reverses
|
|
2154
|
+
// a one-element order (a no-op). The eager up-front key capture makes the order
|
|
2155
|
+
// purely an FK-enforcement concern — identity is fixed before any op fires.
|
|
2156
|
+
order = orderSides(analysis.sides).filter(i => sides.includes(i)).reverse();
|
|
2157
|
+
}
|
|
2158
|
+
const ops = [];
|
|
2159
|
+
for (const sideIndex of order) {
|
|
2160
|
+
const side = analysis.sides[sideIndex];
|
|
2161
|
+
const where = buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName);
|
|
2162
|
+
const statement = {
|
|
2163
|
+
type: 'delete',
|
|
2164
|
+
table: tableIdentifier(side.schema),
|
|
2165
|
+
where,
|
|
2166
|
+
contextValues: stmt.contextValues,
|
|
2167
|
+
schemaPath: stmt.schemaPath,
|
|
2168
|
+
loc: stmt.loc,
|
|
2169
|
+
};
|
|
2170
|
+
ops.push({ table: side.table, op: 'delete', statement });
|
|
2171
|
+
}
|
|
2172
|
+
return ops;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Pick the base side(s) a join delete routes to (§ Inner Join — Deletes). Deleting
|
|
2176
|
+
* one side of an inner equi-join already removes the joined row from the view, so the
|
|
2177
|
+
* common case resolves to a single side; the maximal-lenient case fans out to every
|
|
2178
|
+
* candidate side ("make this joined row not exist"). Returns 1 or more sides.
|
|
2179
|
+
*
|
|
2180
|
+
* The routing is **predicate/FK truth only** — there is no tag override (the routing
|
|
2181
|
+
* tags were removed; a per-row presence/membership column, e.g. the outer-join
|
|
2182
|
+
* existence column, expresses any non-default side explicitly):
|
|
2183
|
+
* 1. The candidate set is the **preserved** side(s) — deleting a preserved side is the
|
|
2184
|
+
* only way the joined row leaves the view (§ Outer Joins — Deletes). An inner join
|
|
2185
|
+
* is all-preserved; a `full` outer join has no preserved side, so its delete defers.
|
|
2186
|
+
* 2. If a foreign key proves the FK-many (child) side, that single side (deleting the
|
|
2187
|
+
* child leaves the parent — the documented FK-style default; the FK resolves the
|
|
2188
|
+
* ambiguity, so it is NOT a fan-out).
|
|
2189
|
+
* 3. Otherwise the deletion side is ambiguous and the (hardwired) lenient default
|
|
2190
|
+
* **fans out to every candidate side** — the predicate-honest multi-side delete
|
|
2191
|
+
* (see {@link decomposeDelete}'s eager key capture).
|
|
2192
|
+
*/
|
|
2193
|
+
function chooseDeleteSides(view, analysis) {
|
|
2194
|
+
// The candidate set is the **preserved** side(s) — deleting the preserved side is
|
|
2195
|
+
// the only way the joined row leaves the view (§ Outer Joins — Deletes). Inner
|
|
2196
|
+
// joins are all-preserved (⇒ the full set). A `full` outer join has no preserved
|
|
2197
|
+
// side (each side is both preserved and non-preserved per row), so a
|
|
2198
|
+
// statically-routed delete is not expressible — defer it.
|
|
2199
|
+
const candidates = preservedSideIndices(analysis.sides);
|
|
2200
|
+
if (candidates.length === 0) {
|
|
2201
|
+
raiseMutationDiagnostic({
|
|
2202
|
+
reason: 'unsupported-join',
|
|
2203
|
+
table: view.name,
|
|
2204
|
+
message: `cannot delete through view '${view.name}': a FULL outer join has no preserved side to route the delete to (each side is both preserved and non-preserved per row); deleting through a full-outer view is deferred`,
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
if (candidates.length === 1)
|
|
2208
|
+
return candidates;
|
|
2209
|
+
// A provable FK-child side resolves the ambiguity to that single side (deleting the
|
|
2210
|
+
// child leaves the parent). `fkChildIndex` is binary, so for an n-way (>2) join it
|
|
2211
|
+
// is undefined and the delete fans out.
|
|
2212
|
+
const childIndex = fkChildIndex(analysis.sides);
|
|
2213
|
+
if (childIndex !== undefined && candidates.includes(childIndex))
|
|
2214
|
+
return [childIndex];
|
|
2215
|
+
// ≥2 residual candidates + no provable single-direction FK-child default: fan out to
|
|
2216
|
+
// every candidate side (the hardwired lenient, predicate-honest multi-side delete).
|
|
2217
|
+
return candidates;
|
|
2218
|
+
}
|
|
2219
|
+
// --- predicate / subquery construction ------------------------------------
|
|
2220
|
+
/**
|
|
2221
|
+
* The combined base-term identifying predicate: the user's WHERE (rewritten from
|
|
2222
|
+
* view columns to base terms) conjoined with the view body's own WHERE (already
|
|
2223
|
+
* in base terms). Either may be absent.
|
|
2224
|
+
*/
|
|
2225
|
+
function buildIdentifyingPredicate(ctx, analysis, userWhere, view) {
|
|
2226
|
+
const userBase = userWhere ? substituteViewColumns(ctx, userWhere, analysis.viewColToBaseRef, view, analysis.sides) : undefined;
|
|
2227
|
+
const bodyWhere = analysis.sel.where ? cloneExpr(analysis.sel.where) : undefined;
|
|
2228
|
+
return combineAnd(userBase, bodyWhere);
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Substitute references to view columns (unqualified, or qualified by the view's
|
|
2232
|
+
* own name) with their base-term replacement expression. References already
|
|
2233
|
+
* qualified by a base alias are left untouched. A view-column reference nested
|
|
2234
|
+
* inside a `subquery` / `exists` / `in`-subquery operand is rewritten too, via
|
|
2235
|
+
* the scope-aware {@link makeViewColumnDescend} descent.
|
|
2236
|
+
*
|
|
2237
|
+
* Every injected replacement is **side-alias-qualified** ({@link
|
|
2238
|
+
* makeSideQualifyScope}, threaded both into the top-level substitution and as the
|
|
2239
|
+
* descent's `baseQualify`). A body may legally project a partner column BARE
|
|
2240
|
+
* (`select c.cid as cid, cval, pv from c join p …` — `pv` unambiguous across the
|
|
2241
|
+
* sides), so its lineage leaf arrives unqualified, and an unqualified leaf emitted
|
|
2242
|
+
* inside a subquery operand would re-bind, by innermost-scope SQL rules, to a
|
|
2243
|
+
* same-named column of that subquery's own FROM instead of the join body — the
|
|
2244
|
+
* multi-source analog of the single-source correlation-qualification of
|
|
2245
|
+
* substituted terms (`makeBaseQualifier`). Qualifying at injection keeps every
|
|
2246
|
+
* scope decision in this walk: downstream, the qualifier strip
|
|
2247
|
+
* ({@link stripSideQualifier}) is qualifier-driven, and a bare leaf reaching it is
|
|
2248
|
+
* only ever a user-authored local/unknown name. The strip is **alias-scope-aware**
|
|
2249
|
+
* for the converse case this pass does not cover — a *user-authored*
|
|
2250
|
+
* alias-qualified ref whose qualifier collides with a side alias/table name but is
|
|
2251
|
+
* shadowed by an inner value-subquery's own FROM alias is left subquery-local there
|
|
2252
|
+
* (these injected lineage leaves carry side aliases a user subquery would not reuse,
|
|
2253
|
+
* so they are never the shadowed ones).
|
|
2254
|
+
*/
|
|
2255
|
+
function substituteViewColumns(ctx, expr, viewColToBaseRef, view, sides) {
|
|
2256
|
+
const viewName = view.name.toLowerCase();
|
|
2257
|
+
const sideQualifyScope = makeSideQualifyScope(sides, view);
|
|
2258
|
+
const sideQualify = (repl) => transformScopedExpr(ctx, sideQualifyScope, repl);
|
|
2259
|
+
const descend = makeViewColumnDescend(ctx, viewColToBaseRef, view.name, view, sideQualify);
|
|
2260
|
+
return transformExpr(expr, (col) => {
|
|
2261
|
+
if (col.table && col.table.toLowerCase() !== viewName)
|
|
2262
|
+
return undefined;
|
|
2263
|
+
const repl = viewColToBaseRef.get(col.name.toLowerCase());
|
|
2264
|
+
return repl ? sideQualify(repl) : undefined;
|
|
2265
|
+
}, descend);
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* The {@link ScopeContext} that side-alias-qualifies a substituted base-term
|
|
2269
|
+
* lineage expression at injection time — the multi-source analog of the
|
|
2270
|
+
* single-source `makeBaseQualifyScope` (docs/view-updateability.md § Inner Join,
|
|
2271
|
+
* cross-source `set`). A bare, non-shadowed leaf is resolved by **unique column
|
|
2272
|
+
* ownership** across the join sides ({@link resolveColumnSide}, the exact rule
|
|
2273
|
+
* join-condition operands use) and qualified with the owning side's **alias** —
|
|
2274
|
+
* never the table name, so a self-join's distinct aliases stay distinct. A name
|
|
2275
|
+
* on NO side is a lineage-internal correlated/local name and stays bare; a name
|
|
2276
|
+
* on 2+ sides resolves to `undefined`, but such a bare body projection
|
|
2277
|
+
* (`select av …` with `av` on both sides) is already rejected as ambiguous at
|
|
2278
|
+
* body planning (analyzeBodyLineage → buildSelectStmt), so for genuine lineage
|
|
2279
|
+
* leaves that branch is unreachable — the bare pass-through serves only the
|
|
2280
|
+
* no-side case. Shadowing within the lineage's own nested subqueries is handled
|
|
2281
|
+
* by the shared scoped descent (a lineage term `(select x from oth where fk =
|
|
2282
|
+
* cid)` qualifies only its correlation ref `cid`; `x`/`fk`, shadowed by `oth`,
|
|
2283
|
+
* stay local).
|
|
2284
|
+
*
|
|
2285
|
+
* An unresolvable nested scope is **rejected** rather than tainted (matching
|
|
2286
|
+
* `makeBaseQualifyScope`): shadowing cannot be proven, so the term could over- or
|
|
2287
|
+
* under-qualify into a silent wrong write.
|
|
2288
|
+
*/
|
|
2289
|
+
function makeSideQualifyScope(sides, view) {
|
|
2290
|
+
return {
|
|
2291
|
+
makeSubstitute: (shadowed) => (col) => {
|
|
2292
|
+
if (col.table)
|
|
2293
|
+
return undefined;
|
|
2294
|
+
if (shadowed.has(col.name.toLowerCase()))
|
|
2295
|
+
return undefined;
|
|
2296
|
+
const side = resolveColumnSide(col, sides);
|
|
2297
|
+
if (side === undefined)
|
|
2298
|
+
return undefined;
|
|
2299
|
+
return { ...col, table: sides[side].alias };
|
|
2300
|
+
},
|
|
2301
|
+
unresolvableScope: 'reject',
|
|
2302
|
+
rejectUnresolvableScope: () => raiseMutationDiagnostic({
|
|
2303
|
+
reason: 'unsupported-subquery-correlation',
|
|
2304
|
+
table: view.name,
|
|
2305
|
+
message: `cannot write through view '${view.name}': a view column's base-term lineage contains a correlated subquery whose source columns are not statically resolvable (a 'select *' / table-valued function / unresolved source), so its correlation cannot be proven; restructure the view body`,
|
|
2306
|
+
}),
|
|
2307
|
+
rejectDmlSubquery: () => raiseMutationDiagnostic({
|
|
2308
|
+
reason: 'unsupported-subquery-correlation',
|
|
2309
|
+
table: view.name,
|
|
2310
|
+
message: `cannot write through view '${view.name}': a data-modifying subquery (INSERT/UPDATE/DELETE) within a view column's base-term lineage cannot be analysed`,
|
|
2311
|
+
}),
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Rewrite the owning side's alias qualifier on a base-term assignment value to the lowered
|
|
2316
|
+
* UPDATE's `__vm_self` correlation alias ({@link SELF_ALIAS}), so it binds the single-table
|
|
2317
|
+
* UPDATE's target row directly even when nested in a user value subquery whose own FROM
|
|
2318
|
+
* carries a same-named column. A reference to **any other side** cannot
|
|
2319
|
+
* be expressed as a single-table SET, so it is either captured-and-rewritten (when a
|
|
2320
|
+
* `registerCrossSource` carrier is supplied — § Inner Join, cross-source `set`) or
|
|
2321
|
+
* rejected (`cross-source-assignment`, the legacy path). The strip is qualifier-driven
|
|
2322
|
+
* but **alias-scope-aware**: the route/strip decision reads a column's own table
|
|
2323
|
+
* qualifier, yet a qualifier shadowed by an inner value-subquery's FROM **alias** binds
|
|
2324
|
+
* to that inner source (innermost-scope SQL rules), so it is left local. The alias-shadow
|
|
2325
|
+
* set accumulates per nesting depth through the {@link transformAliasScopedExpr} descent
|
|
2326
|
+
* (the alias-only analog of the view-column descent's column-name shadowing); a nested
|
|
2327
|
+
* *owning*-side reference whose qualifier is NOT shadowed is still correlated to the
|
|
2328
|
+
* target row of the lowered UPDATE just like a top-level one.
|
|
2329
|
+
*
|
|
2330
|
+
* The **alias-shadow check fires first** (before the owning/other qualifier sets), so a
|
|
2331
|
+
* user-authored alias-qualified ref colliding with a side alias OR a side's table name
|
|
2332
|
+
* (`from things c` shadowing owning alias `c`; `from aux parent` shadowing a side's table
|
|
2333
|
+
* name `parent`) is left subquery-local — never stripped to bare, never mis-routed
|
|
2334
|
+
* through the capture. Injected base-term lineage leaves carry side aliases a user
|
|
2335
|
+
* subquery would not intentionally reuse, so they are never shadowed; the narrowing
|
|
2336
|
+
* affects only genuine user collisions.
|
|
2337
|
+
*
|
|
2338
|
+
* The owning-side qualifier set is checked **before** the other-side set, so a self-join
|
|
2339
|
+
* (where an `other` side shares the owning side's table name) still strips an owning-alias
|
|
2340
|
+
* reference; only a reference qualified by a *different alias* is the cross-source case.
|
|
2341
|
+
*
|
|
2342
|
+
* A **bare** leaf is left untouched — binding locally (a nested subquery's own FROM, or
|
|
2343
|
+
* the lowered single-table UPDATE's target) or failing loudly at build. The strip never
|
|
2344
|
+
* resolves a bare name against the view sides: every base-term lineage leaf was
|
|
2345
|
+
* side-alias-qualified when `substituteViewColumns` injected it
|
|
2346
|
+
* ({@link makeSideQualifyScope} — including a partner column the body projected bare, so
|
|
2347
|
+
* that read rides the qualified routing below at ANY non-shadowed nesting depth), and
|
|
2348
|
+
* resolving a bare name here would mis-route an inner-scope column whose name merely collides with a
|
|
2349
|
+
* partner base column (e.g. `(select psecret from t)` where the partner side also has a
|
|
2350
|
+
* `psecret`) to the partner's captured value.
|
|
2351
|
+
*
|
|
2352
|
+
* A cross-source read is rewritten to `(select <srcN> from __vmupd_keys k where
|
|
2353
|
+
* k.k<owningSide>_0 = __vm_self.<pk0> [and …])`: `registerCrossSource` projects the partner
|
|
2354
|
+
* column into the capture under `srcN` and returns the alias; the `<pk_j>` (qualified with
|
|
2355
|
+
* the lowered UPDATE's `__vm_self` correlation alias — {@link SELF_ALIAS}) bind to its own
|
|
2356
|
+
* target row, so each row reads the captured pre-mutation
|
|
2357
|
+
* partner value of its joined row. The cross-source gate (`gateCrossSourceReads`) has
|
|
2358
|
+
* already proved every reached partner column has `base` lineage.
|
|
2359
|
+
*
|
|
2360
|
+
* Before the rewrite, `gateCrossSourceCardinality` (when supplied) rejects the **1:many**
|
|
2361
|
+
* direction at plan time: the capture carries one `srcN` row per joined owner/partner pair,
|
|
2362
|
+
* so the correlated read-back is well-defined only when the owning side joins **at most one**
|
|
2363
|
+
* partner row ({@link ownerJoinsAtMostOnePartner}). Placed here — at the rewrite site — so it
|
|
2364
|
+
* covers a partner ref nested in a value subquery as well as a top-level one (both lower to
|
|
2365
|
+
* {@link capturedValueSubquery}).
|
|
2366
|
+
*/
|
|
2367
|
+
function stripSideQualifier(expr, view, owning, owningSideIndex, allSides, registerCrossSource, gateCrossSourceCardinality) {
|
|
2368
|
+
const owningQuals = new Set([owning.alias, owning.schema.name.toLowerCase()]);
|
|
2369
|
+
const otherQuals = new Set();
|
|
2370
|
+
allSides.forEach((s, i) => {
|
|
2371
|
+
if (i === owningSideIndex)
|
|
2372
|
+
return;
|
|
2373
|
+
otherQuals.add(s.alias);
|
|
2374
|
+
otherQuals.add(s.schema.name.toLowerCase());
|
|
2375
|
+
});
|
|
2376
|
+
// The owning side's PK — the correlation a captured cross-source read binds on.
|
|
2377
|
+
// Resolved lazily (only a cross-source rewrite needs it).
|
|
2378
|
+
let owningPk;
|
|
2379
|
+
// Route a partner-side base-column read through the up-front capture: project it into
|
|
2380
|
+
// `__vmupd_keys` under a stable `srcN` alias and rewrite the reference to a correlated
|
|
2381
|
+
// scalar read of it, keyed by the owning side's PK. Shared by the qualified-other branch
|
|
2382
|
+
// and the unqualified-partner branch (both lower identically; the `srcN` dedup key is
|
|
2383
|
+
// `<table>.<col>`, so a body mixing `a.av` and a partner-resolved bare `av` — qualified
|
|
2384
|
+
// here with the same alias — mints ONE capture column). Absent a capture carrier (the
|
|
2385
|
+
// legacy non-build path) reject `cross-source-assignment`.
|
|
2386
|
+
const routePartnerRead = (col) => {
|
|
2387
|
+
if (!registerCrossSource) {
|
|
2388
|
+
raiseMutationDiagnostic({
|
|
2389
|
+
reason: 'cross-source-assignment',
|
|
2390
|
+
column: col.name,
|
|
2391
|
+
table: view.name,
|
|
2392
|
+
message: `cannot write through view '${view.name}': an update value references column '${col.name}' on a different base table than the column it assigns; cross-source assignment is not supported`,
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
// Reject the 1:many direction at plan time before lowering to a (multi-valued)
|
|
2396
|
+
// correlated read of the capture (§ Inner Join, cross-source `set`).
|
|
2397
|
+
gateCrossSourceCardinality?.(col);
|
|
2398
|
+
const srcAlias = registerCrossSource(col);
|
|
2399
|
+
owningPk ??= requireKeyColumns(view, owning);
|
|
2400
|
+
// Qualify the owning-PK operands with the per-side UPDATE's collision-proof alias so
|
|
2401
|
+
// the read-back correlates to the target row even when this subquery nests inside a
|
|
2402
|
+
// user value subquery whose FROM has a same-named column (the bug-1 site).
|
|
2403
|
+
return capturedValueSubquery(srcAlias, owningSideIndex, owningPk, undefined, SELF_ALIAS);
|
|
2404
|
+
};
|
|
2405
|
+
// QUALIFIED-only substitution: an owning-alias ref is re-qualified to the lowered
|
|
2406
|
+
// target's `__vm_self` correlation alias; a partner-alias ref
|
|
2407
|
+
// routes through the capture; a BARE ref is left untouched (only ever a user-authored
|
|
2408
|
+
// local/unknown name — every lineage leaf arrives side-alias-qualified; see the
|
|
2409
|
+
// docstring). The route/strip decision is qualifier-driven but ALIAS-SCOPE-AWARE: a
|
|
2410
|
+
// qualifier shadowed by an inner value-subquery FROM alias (`aliasShadow`) binds to that
|
|
2411
|
+
// inner source by innermost-scope SQL rules, so it is left local — checked BEFORE the
|
|
2412
|
+
// side-qualifier sets, so an owning-/partner-/table-name collision with an inner alias
|
|
2413
|
+
// never strips or routes. Injected lineage leaves carry side aliases a user subquery
|
|
2414
|
+
// would not reuse, so they are never shadowed; only a user-authored alias-qualified ref
|
|
2415
|
+
// colliding with a side alias/table name is affected. The alias set accumulates per
|
|
2416
|
+
// nesting depth via `transformAliasScopedExpr` (mirrors the view-column descent's
|
|
2417
|
+
// column-name shadowing); at the top level it is empty, so behaviour is byte-identical
|
|
2418
|
+
// for every non-colliding statement.
|
|
2419
|
+
const substitute = (col, aliasShadow) => {
|
|
2420
|
+
if (!col.table)
|
|
2421
|
+
return undefined;
|
|
2422
|
+
const t = col.table.toLowerCase();
|
|
2423
|
+
if (aliasShadow.has(t))
|
|
2424
|
+
return undefined;
|
|
2425
|
+
// Qualify the stripped owning ref with the per-side UPDATE's collision-proof alias
|
|
2426
|
+
// rather than emitting a bare column: a bare base-name ref nested in a user value
|
|
2427
|
+
// subquery whose FROM carries that base name would re-bind locally (the bug-2 site).
|
|
2428
|
+
if (owningQuals.has(t))
|
|
2429
|
+
return { type: 'column', name: col.name, table: SELF_ALIAS };
|
|
2430
|
+
if (otherQuals.has(t))
|
|
2431
|
+
return routePartnerRead(col);
|
|
2432
|
+
return undefined;
|
|
2433
|
+
};
|
|
2434
|
+
return transformAliasScopedExpr(expr, substitute);
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* The correlated scalar read a cross-source SET value lowers to:
|
|
2438
|
+
* `(select <srcAlias> from __vmupd_keys k where k.k<owningSide>_0 = <pk0> [and …])`
|
|
2439
|
+
* — `<srcAlias>` is the capture projection of the partner base column; the unqualified
|
|
2440
|
+
* `<pk_j>` bind to the lowered UPDATE's own target row (the owning side), matching the
|
|
2441
|
+
* per-side identifying EXISTS so each target row reads the captured pre-mutation partner
|
|
2442
|
+
* value of its joined row. Composite owning keys conjoin one equality per PK column.
|
|
2443
|
+
*
|
|
2444
|
+
* `dedupAggregate` wraps the projection in that aggregate (`min(k.<srcAlias>)`) so the
|
|
2445
|
+
* correlated read is single-valued even when the owning PK matches MORE THAN ONE capture
|
|
2446
|
+
* row — the non-preserved-side fan-out case, where N preserved rows share one non-preserved
|
|
2447
|
+
* partner so its PK matches all N captures (§ Outer Joins). For a constant / np-only SET the
|
|
2448
|
+
* captured value is identical across the group so `min` is an exact no-op de-dup; for a
|
|
2449
|
+
* value that genuinely differs per preserved row it resolves the ambiguity deterministically
|
|
2450
|
+
* rather than erroring at runtime. The cross-source `set` callers leave it off (their gate
|
|
2451
|
+
* already proves at-most-one partner), keeping the bare-column form byte-identical.
|
|
2452
|
+
*
|
|
2453
|
+
* `correlationAlias` qualifies each owning-PK right operand (`<pk_j>` → `<alias>.<pk_j>`).
|
|
2454
|
+
* When this read-back nests inside a user value subquery whose own FROM introduces a
|
|
2455
|
+
* column named like the owning PK, a **bare** `<pk_j>` would re-bind to that inner column
|
|
2456
|
+
* by innermost-scope SQL rules (keying the read-back on the wrong value); qualifying it with
|
|
2457
|
+
* the lowered per-side UPDATE's collision-proof alias ({@link SELF_ALIAS}) binds the outer
|
|
2458
|
+
* target row instead. The multi-source per-side callers pass `SELF_ALIAS`; `decomposition.ts`
|
|
2459
|
+
* and any caller that omits it keep the bare form (byte-identical — composite keys still
|
|
2460
|
+
* qualify every conjunct only when supplied).
|
|
2461
|
+
*
|
|
2462
|
+
* `captureRelationName` is the relation the correlated read scans (`from <name> k`); it
|
|
2463
|
+
* defaults to {@link MS_UPDATE_KEYS_CTE} so the standalone multi-source and decomposition
|
|
2464
|
+
* callers are byte-identical, while a nested multi-source capture threads its fresh name.
|
|
2465
|
+
*/
|
|
2466
|
+
export function capturedValueSubquery(srcAlias, owningSideIndex, owningPk, dedupAggregate, correlationAlias, captureRelationName = MS_UPDATE_KEYS_CTE) {
|
|
2467
|
+
const conds = owningPk.map((pk, j) => ({
|
|
2468
|
+
type: 'binary',
|
|
2469
|
+
operator: '=',
|
|
2470
|
+
left: { type: 'column', name: keyColumnName(owningSideIndex, j), table: 'k' },
|
|
2471
|
+
right: correlationAlias ? { type: 'column', name: pk, table: correlationAlias } : { type: 'column', name: pk },
|
|
2472
|
+
}));
|
|
2473
|
+
const colRef = { type: 'column', name: srcAlias, table: 'k' };
|
|
2474
|
+
const projection = dedupAggregate
|
|
2475
|
+
? { type: 'function', name: dedupAggregate, args: [colRef] }
|
|
2476
|
+
: colRef;
|
|
2477
|
+
return {
|
|
2478
|
+
type: 'subquery',
|
|
2479
|
+
query: {
|
|
2480
|
+
type: 'select',
|
|
2481
|
+
columns: [{ type: 'column', expr: projection }],
|
|
2482
|
+
from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
|
|
2483
|
+
where: conds.reduce((acc, c) => combineAnd(acc, c)),
|
|
2484
|
+
},
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Reject a cross-source value read whose partner-side view column is **not** `base`
|
|
2489
|
+
* (computed / non-invertible) — its value is not recoverable from a captured base
|
|
2490
|
+
* column, so the cross-source rewrite cannot carry it (`no-inverse`; an outer-join
|
|
2491
|
+
* `null-extended` partner is already rejected wholesale upstream). A same-side read (the
|
|
2492
|
+
* column reads only the assigned side) is left to the qualifier strip; a `base` partner
|
|
2493
|
+
* column is admitted and captured. Walks only the value's top-level column references
|
|
2494
|
+
* (the scope `guardTopLevelScope` already proved are view columns); a reference nested in
|
|
2495
|
+
* a value subquery is left to the qualifier strip's per-leaf handling.
|
|
2496
|
+
*
|
|
2497
|
+
* Known asymmetry (deliberate, conservative): because this walks top level only, a
|
|
2498
|
+
* computed partner read nested in a value subquery is *admitted* via the per-leaf
|
|
2499
|
+
* capture — which is value-correct (leaves captured pre-mutation, scalar applied on
|
|
2500
|
+
* read) — while the same read at the top level is rejected here. `no-inverse` is only
|
|
2501
|
+
* a hard requirement for a computed column as an assignment *target*; admitting the
|
|
2502
|
+
* top-level read through the same capture is the intended unification, pending an
|
|
2503
|
+
* audit of mixed owning/partner leaves under an owning-site inverse. See
|
|
2504
|
+
* docs/view-updateability.md § cross-source `set` values.
|
|
2505
|
+
*/
|
|
2506
|
+
function gateCrossSourceReads(value, owningSideIndex, analysis, view) {
|
|
2507
|
+
forEachTopLevelColumnRef(value, (col) => {
|
|
2508
|
+
const vco = analysis.outColumns.find(c => c.name === col.name.toLowerCase());
|
|
2509
|
+
if (!vco)
|
|
2510
|
+
return; // guardTopLevelScope already proved top-level refs are view columns
|
|
2511
|
+
const readSides = viewColumnReadSides(vco, analysis);
|
|
2512
|
+
const crossSource = [...readSides].some(s => s !== owningSideIndex);
|
|
2513
|
+
if (crossSource && !vco.writable) {
|
|
2514
|
+
raiseMutationDiagnostic({
|
|
2515
|
+
reason: 'no-inverse',
|
|
2516
|
+
column: vco.displayName,
|
|
2517
|
+
table: view.name,
|
|
2518
|
+
message: `cannot write through view '${view.name}': the update value reads computed column '${vco.displayName}' on a different base table than the column it assigns; a cross-source read requires the partner column to have base lineage`,
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* The set of join-side indices a view column's value reads. A `base` site reads only
|
|
2525
|
+
* its owning side; a computed site reads every side its base-term expression's column
|
|
2526
|
+
* leaves resolve to (so a same-side computed read stays admissible while a cross-source
|
|
2527
|
+
* computed read is rejected).
|
|
2528
|
+
*/
|
|
2529
|
+
function viewColumnReadSides(vco, analysis) {
|
|
2530
|
+
if (vco.writable && vco.sideIndex !== undefined)
|
|
2531
|
+
return new Set([vco.sideIndex]);
|
|
2532
|
+
const sides = new Set();
|
|
2533
|
+
const expr = analysis.viewColToBaseRef.get(vco.name);
|
|
2534
|
+
if (expr) {
|
|
2535
|
+
forEachColumnRefDeep(expr, (col) => {
|
|
2536
|
+
const s = resolveColumnSide(col, analysis.sides);
|
|
2537
|
+
if (s !== undefined)
|
|
2538
|
+
sides.add(s);
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
return sides;
|
|
2542
|
+
}
|
|
2543
|
+
/** Observe every TOP-LEVEL column reference in an expression (no subquery descent). */
|
|
2544
|
+
function forEachTopLevelColumnRef(expr, fn) {
|
|
2545
|
+
transformExpr(expr, (col) => { fn(col); return undefined; });
|
|
2546
|
+
}
|
|
2547
|
+
/** Observe every column reference in an expression, descending into subqueries. */
|
|
2548
|
+
function forEachColumnRefDeep(expr, fn) {
|
|
2549
|
+
const observe = (col) => { fn(col); return undefined; };
|
|
2550
|
+
transformExpr(expr, observe, (q) => mapQueryExprUniform(q, observe));
|
|
2551
|
+
}
|
|
2552
|
+
// --- helpers --------------------------------------------------------------
|
|
2553
|
+
function tableIdentifier(table) {
|
|
2554
|
+
return { type: 'identifier', name: table.name, schema: table.schemaName };
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* The boolean value of a literal existence-flag assignment (`set hasB = true|false`), or
|
|
2558
|
+
* `undefined` for any non-literal / non-boolean value — a per-row branch on the *written*
|
|
2559
|
+
* value is deferred (§ Existence columns, v1). Accepts the boolean literals `true`/`false`
|
|
2560
|
+
* and the numeric `1`/`0` spellings (integers lower to `bigint` here).
|
|
2561
|
+
*/
|
|
2562
|
+
function asBooleanLiteral(expr) {
|
|
2563
|
+
if (expr.type !== 'literal')
|
|
2564
|
+
return undefined;
|
|
2565
|
+
const v = expr.value;
|
|
2566
|
+
if (v === true || v === false)
|
|
2567
|
+
return v;
|
|
2568
|
+
if (v === 1 || v === 1n)
|
|
2569
|
+
return true;
|
|
2570
|
+
if (v === 0 || v === 0n)
|
|
2571
|
+
return false;
|
|
2572
|
+
return undefined;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* The side's primary-key column names (≥1), in declaration order — the per-side
|
|
2576
|
+
* identifying key the capture projects and the base ops' EXISTS correlates on.
|
|
2577
|
+
* Composite keys are admitted (each PK column contributes a `k<side>_<j>` capture
|
|
2578
|
+
* column); a keyless table is the only reject (`unsupported-join`).
|
|
2579
|
+
*/
|
|
2580
|
+
function requireKeyColumns(view, side) {
|
|
2581
|
+
const pk = side.schema.primaryKeyDefinition;
|
|
2582
|
+
if (pk.length === 0) {
|
|
2583
|
+
raiseMutationDiagnostic({
|
|
2584
|
+
reason: 'unsupported-join',
|
|
2585
|
+
table: view.name,
|
|
2586
|
+
message: `cannot write through view '${view.name}': base table '${side.schema.name}' has no primary key; multi-source identifying predicates need a key`,
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
return pk.map(def => side.schema.columns[def.index].name);
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* True when `fk` (declared on `child`) targets `parent` — the shared FK-match
|
|
2593
|
+
* predicate: case-insensitive `referencedTable` against the parent's name, with an
|
|
2594
|
+
* absent `referencedSchema` defaulting to the child's own schema. The single source
|
|
2595
|
+
* of truth for "does this declared FK reference that side", reused by
|
|
2596
|
+
* {@link fkChildIndex}, {@link inboundDeleteAction}, and {@link edgeCorrelated}.
|
|
2597
|
+
*/
|
|
2598
|
+
function fkTargetsSide(fk, child, parent) {
|
|
2599
|
+
return fk.referencedTable.toLowerCase() === parent.schema.name.toLowerCase()
|
|
2600
|
+
&& (fk.referencedSchema ?? child.schema.schemaName).toLowerCase() === parent.schema.schemaName.toLowerCase();
|
|
2601
|
+
}
|
|
2602
|
+
/** True when `child` declares any foreign key onto `parent`. */
|
|
2603
|
+
function sideDeclaresFkOnto(child, parent) {
|
|
2604
|
+
return (child.schema.foreignKeys ?? []).some(fk => fkTargetsSide(fk, child, parent));
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Index of the FK-child (many) side of a **two-side** join: the side declaring a
|
|
2608
|
+
* foreign key onto the other. `undefined` when no FK is provable, both sides reference
|
|
2609
|
+
* each other (mutual), or the join is not two-sided (the binary FK-child concept does
|
|
2610
|
+
* not generalize past two sides — the n-way delete fan-out / `orderSides` topo sort
|
|
2611
|
+
* handle >2). Used by the two-side delete routing ({@link chooseDeleteSides}).
|
|
2612
|
+
*/
|
|
2613
|
+
function fkChildIndex(sides) {
|
|
2614
|
+
if (sides.length !== 2)
|
|
2615
|
+
return undefined;
|
|
2616
|
+
const zeroRefsOne = sideDeclaresFkOnto(sides[0], sides[1]);
|
|
2617
|
+
const oneRefsZero = sideDeclaresFkOnto(sides[1], sides[0]);
|
|
2618
|
+
if (zeroRefsOne && !oneRefsZero)
|
|
2619
|
+
return 0;
|
|
2620
|
+
if (oneRefsZero && !zeroRefsOne)
|
|
2621
|
+
return 1;
|
|
2622
|
+
return undefined;
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Side execution order: an FK **topological sort** over the n sides — every FK-parent
|
|
2626
|
+
* precedes its FK-child — stable by source order within an FK-equivalence class. A
|
|
2627
|
+
* mutual FK (each side referencing the other, e.g. a self-join's two aliases of one
|
|
2628
|
+
* self-referencing table) forms a cycle with no zero-in-degree head; it is broken by
|
|
2629
|
+
* lowest source index, i.e. it falls back to **alias-declaration order** (§ Cycles,
|
|
2630
|
+
* Self-Joins). The two-side binary order (`[parent, child]` / `[0, 1]`) is the n=2
|
|
2631
|
+
* specialization of this.
|
|
2632
|
+
*/
|
|
2633
|
+
function orderSides(sides) {
|
|
2634
|
+
const n = sides.length;
|
|
2635
|
+
// parents[child] = set of side indices the child must follow (its declared FK parents).
|
|
2636
|
+
const parents = sides.map(() => new Set());
|
|
2637
|
+
for (let child = 0; child < n; child++) {
|
|
2638
|
+
for (let parent = 0; parent < n; parent++) {
|
|
2639
|
+
if (child !== parent && sideDeclaresFkOnto(sides[child], sides[parent]))
|
|
2640
|
+
parents[child].add(parent);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const placed = new Set();
|
|
2644
|
+
const order = [];
|
|
2645
|
+
while (order.length < n) {
|
|
2646
|
+
// Lowest-index unplaced side all of whose (non-self, unplaced-cycle-aside) parents
|
|
2647
|
+
// are already placed.
|
|
2648
|
+
let pick = -1;
|
|
2649
|
+
for (let i = 0; i < n; i++) {
|
|
2650
|
+
if (placed.has(i))
|
|
2651
|
+
continue;
|
|
2652
|
+
if ([...parents[i]].every(p => placed.has(p))) {
|
|
2653
|
+
pick = i;
|
|
2654
|
+
break;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
// A cycle (mutual FK) leaves no ready node — break it by lowest unplaced index
|
|
2658
|
+
// (source / alias-declaration order).
|
|
2659
|
+
if (pick === -1) {
|
|
2660
|
+
for (let i = 0; i < n; i++)
|
|
2661
|
+
if (!placed.has(i)) {
|
|
2662
|
+
pick = i;
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
order.push(pick);
|
|
2667
|
+
placed.add(pick);
|
|
2668
|
+
}
|
|
2669
|
+
return order;
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* The governing ON DELETE action of FK(s) declared on `child` that reference
|
|
2673
|
+
* `parent` — i.e. the action that fires when a `parent` row is deleted. When more
|
|
2674
|
+
* than one such FK runs between the same ordered pair, the **most-blocking** action
|
|
2675
|
+
* governs (immediate enforcement fires every referencing FK): `restrict` over
|
|
2676
|
+
* `cascade` over `setNull`/`setDefault` over absent (`undefined` — no FK on `child`
|
|
2677
|
+
* references `parent`). Mirrors the FK-match predicate in {@link fkChildIndex} (same
|
|
2678
|
+
* `referencedTable` / `referencedSchema` comparison).
|
|
2679
|
+
*/
|
|
2680
|
+
function inboundDeleteAction(child, parent) {
|
|
2681
|
+
let governing;
|
|
2682
|
+
for (const fk of child.schema.foreignKeys ?? []) {
|
|
2683
|
+
if (!fkTargetsSide(fk, child, parent))
|
|
2684
|
+
continue;
|
|
2685
|
+
if (fk.onDelete === 'restrict')
|
|
2686
|
+
return 'restrict'; // most-blocking — governs outright
|
|
2687
|
+
if (fk.onDelete === 'cascade')
|
|
2688
|
+
governing = 'cascade';
|
|
2689
|
+
else if (governing !== 'cascade')
|
|
2690
|
+
governing = fk.onDelete; // setNull / setDefault, unless a cascade already won
|
|
2691
|
+
}
|
|
2692
|
+
return governing;
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* True when deleting side `X` first (then the other side `Y`) does **not** abort
|
|
2696
|
+
* under immediate FK enforcement + the transitive RESTRICT pre-walk
|
|
2697
|
+
* (`runtime/foreign-key-actions.ts` `assertTransitiveRestrictsForParentMutation`).
|
|
2698
|
+
* `inboundX` governs deleting X (the action of the FK referencing X); `inboundY`
|
|
2699
|
+
* governs deleting Y. Deleting X first is feasible iff X carries no inbound
|
|
2700
|
+
* reference, or its inbound action *clears* Y's reference without tripping a RESTRICT:
|
|
2701
|
+
* - `inboundX` absent — nothing references X, so its delete is unconstrained;
|
|
2702
|
+
* - `inboundX ∈ {setNull, setDefault}` — Y's reference is cleared (no cascade, no RESTRICT);
|
|
2703
|
+
* - `inboundX === cascade` **and** `inboundY !== restrict` — the cascade into Y does
|
|
2704
|
+
* not recurse into a RESTRICT (Y's only inbound child here is X, the root, via `inboundY`).
|
|
2705
|
+
* `inboundX === restrict` ⇒ Y still references X ⇒ NOT deletable-first.
|
|
2706
|
+
*/
|
|
2707
|
+
function deletableFirst(inboundX, inboundY) {
|
|
2708
|
+
if (inboundX === undefined)
|
|
2709
|
+
return true;
|
|
2710
|
+
if (inboundX === 'setNull' || inboundX === 'setDefault')
|
|
2711
|
+
return true;
|
|
2712
|
+
return inboundX === 'cascade' && inboundY !== 'restrict';
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* The feasible base-delete order for the two-side DELETE fan-out, or `undefined`
|
|
2716
|
+
* when a mutual FK's ON DELETE actions cannot be satisfied in *any* order under
|
|
2717
|
+
* immediate enforcement (the caller raises `mutual-fk-restrict-delete`). Reached
|
|
2718
|
+
* only at `fkChildIndex(sides) === undefined` (no FK either way, or a mutual FK): a
|
|
2719
|
+
* no-FK pair has both inbound actions absent ⇒ side0 deletable-first ⇒ `[0, 1]`
|
|
2720
|
+
* (unchanged); a both-cascade mutual FK likewise keeps `[0, 1]`. Prefers `[0, 1]`
|
|
2721
|
+
* when side0 is deletable-first so the no-FK / symmetric paths stay order-stable.
|
|
2722
|
+
*/
|
|
2723
|
+
function orderDeleteFanout(sides) {
|
|
2724
|
+
const inbound0 = inboundDeleteAction(sides[1], sides[0]); // governs deleting side0
|
|
2725
|
+
const inbound1 = inboundDeleteAction(sides[0], sides[1]); // governs deleting side1
|
|
2726
|
+
if (deletableFirst(inbound0, inbound1))
|
|
2727
|
+
return [0, 1];
|
|
2728
|
+
if (deletableFirst(inbound1, inbound0))
|
|
2729
|
+
return [1, 0];
|
|
2730
|
+
return undefined;
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Canonical (order-independent) key for a cross-side column equality, so a join
|
|
2734
|
+
* conjunct written either way (`b.aref = a.aid` or `a.aid = b.aref`) hashes the
|
|
2735
|
+
* same and an edge lookup need not know which operand the join named first.
|
|
2736
|
+
*/
|
|
2737
|
+
function crossEqualityKey(sideA, colA, sideB, colB) {
|
|
2738
|
+
const a = `${sideA}:${colA.toLowerCase()}`;
|
|
2739
|
+
const b = `${sideB}:${colB.toLowerCase()}`;
|
|
2740
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Resolve a join-condition column operand to its owning side index (`0..n-1`), or
|
|
2744
|
+
* `undefined` when the reference cannot be pinned to exactly one side. An explicit
|
|
2745
|
+
* `.table` qualifier matches a side's `alias` (already lowercased) or `schema.name`
|
|
2746
|
+
* (alias preferred, so a self-join's distinct aliases resolve unambiguously even
|
|
2747
|
+
* though the table names collide); an unqualified ref resolves by **unique** ownership
|
|
2748
|
+
* of `col.name` across the sides' columns. An ambiguous / unresolved ref returns
|
|
2749
|
+
* `undefined` (conservative — a term that cannot be placed cannot prove correlation).
|
|
2750
|
+
*/
|
|
2751
|
+
function resolveColumnSide(col, sides) {
|
|
2752
|
+
const qualifier = col.table?.toLowerCase();
|
|
2753
|
+
if (qualifier !== undefined) {
|
|
2754
|
+
const idx = sides.findIndex(s => s.alias === qualifier || s.schema.name.toLowerCase() === qualifier);
|
|
2755
|
+
return idx < 0 ? undefined : idx;
|
|
2756
|
+
}
|
|
2757
|
+
const colName = col.name.toLowerCase();
|
|
2758
|
+
const owners = sides.flatMap((s, i) => s.schema.columns.some(c => c.name.toLowerCase() === colName) ? [i] : []);
|
|
2759
|
+
return owners.length === 1 ? owners[0] : undefined;
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* True when the **owning** side (the side a cross-source `set` assigns) provably joins
|
|
2763
|
+
* **at most one** row of the **partner** side (the side the SET value reads), across the
|
|
2764
|
+
* view's join — the cardinality proof that makes a cross-source `set` value well-defined
|
|
2765
|
+
* (§ Inner Join, cross-source `set`). The up-front `__vmupd_keys` capture carries one
|
|
2766
|
+
* `srcN` row per joined owner/partner pair, so the per-row correlated read-back
|
|
2767
|
+
* ({@link capturedValueSubquery}) is single-valued only when the owning side joins at most
|
|
2768
|
+
* one partner. The **reverse** (1:many) direction returns multiple `srcN` rows for a fixed
|
|
2769
|
+
* owner PK and would fail at runtime with the generic `Scalar subquery returned more than
|
|
2770
|
+
* one row`; the caller rejects it at plan time instead with a diagnostic that names the
|
|
2771
|
+
* cross-source ambiguity.
|
|
2772
|
+
*
|
|
2773
|
+
* The proof: collect the join's **direct** owner↔partner `column = column` equalities
|
|
2774
|
+
* ({@link collectCrossSideEqualities} already walks every nested ON predicate and USING
|
|
2775
|
+
* list across the n-way tree), gather the **partner-side** columns they pin, and check
|
|
2776
|
+
* whether some **unique key** of the partner table is a subset of that pinned set — fixing
|
|
2777
|
+
* each column of a unique key to a per-owner-row value admits ≤1 partner row. Partner
|
|
2778
|
+
* unique keys considered: the PRIMARY KEY; every **non-partial** UNIQUE constraint; every
|
|
2779
|
+
* **non-partial** UNIQUE index. A **partial** unique key (one carrying a `predicate`) does
|
|
2780
|
+
* not bound the rows outside its predicate scope, so it does not prove global at-most-one
|
|
2781
|
+
* and is NOT counted. NULL semantics need no special handling — a `=` join only matches
|
|
2782
|
+
* non-null equal values and a unique key bounds each non-null value to ≤1 row (PK columns
|
|
2783
|
+
* are NOT NULL regardless).
|
|
2784
|
+
*
|
|
2785
|
+
* This is the inverse of the FK-correlation reasoning {@link edgeCorrelated} the delete
|
|
2786
|
+
* path uses, but **FK is not required** — the proof is purely partner-side uniqueness (the
|
|
2787
|
+
* canonical FK-child-reads-parent case is subsumed: the FK references the parent's PK and
|
|
2788
|
+
* the join equates the child's FK column to it, so the parent's PK ⊆ the pinned set).
|
|
2789
|
+
* **Multi-hop / transitive** cross-source (owner and partner not directly joined) pins no
|
|
2790
|
+
* partner column ⇒ NOT proven ⇒ the caller rejects (conservative: this only over-rejects,
|
|
2791
|
+
* never falsely accepts; a transitive value-determinacy proof is a possible follow-up).
|
|
2792
|
+
*/
|
|
2793
|
+
function ownerJoinsAtMostOnePartner(ownerIdx, partnerIdx, sel, sides) {
|
|
2794
|
+
const partner = sides[partnerIdx];
|
|
2795
|
+
// The partner-side columns the join pins equal to an owner-side value (lowercased).
|
|
2796
|
+
const partnerEquatedCols = new Set();
|
|
2797
|
+
for (const eq of collectCrossSideEqualities(sel.from, sides)) {
|
|
2798
|
+
if (eq.sideA === ownerIdx && eq.sideB === partnerIdx)
|
|
2799
|
+
partnerEquatedCols.add(eq.colB.toLowerCase());
|
|
2800
|
+
else if (eq.sideB === ownerIdx && eq.sideA === partnerIdx)
|
|
2801
|
+
partnerEquatedCols.add(eq.colA.toLowerCase());
|
|
2802
|
+
}
|
|
2803
|
+
if (partnerEquatedCols.size === 0)
|
|
2804
|
+
return false; // no direct owner↔partner equality — not proven (e.g. multi-hop)
|
|
2805
|
+
// A non-empty unique-key column set all of whose columns the join pins ⇒ ≤1 partner row.
|
|
2806
|
+
const provesAtMostOne = (cols) => cols.length > 0 && cols.every(c => partnerEquatedCols.has(c.toLowerCase()));
|
|
2807
|
+
// The partner's PRIMARY KEY.
|
|
2808
|
+
const pkNames = partner.schema.primaryKeyDefinition.map(def => partner.schema.columns[def.index].name);
|
|
2809
|
+
if (provesAtMostOne(pkNames))
|
|
2810
|
+
return true;
|
|
2811
|
+
// Non-partial UNIQUE constraints (a partial UNIQUE bounds uniqueness only within its predicate scope).
|
|
2812
|
+
for (const uc of partner.schema.uniqueConstraints ?? []) {
|
|
2813
|
+
if (uc.predicate)
|
|
2814
|
+
continue;
|
|
2815
|
+
if (provesAtMostOne(uc.columns.map(idx => partner.schema.columns[idx].name)))
|
|
2816
|
+
return true;
|
|
2817
|
+
}
|
|
2818
|
+
// Non-partial UNIQUE indexes (e.g. a CREATE UNIQUE INDEX not mirrored as a constraint).
|
|
2819
|
+
for (const idx of partner.schema.indexes ?? []) {
|
|
2820
|
+
if (!idx.unique || idx.predicate)
|
|
2821
|
+
continue;
|
|
2822
|
+
if (provesAtMostOne(idx.columns.map(c => partner.schema.columns[c.index].name)))
|
|
2823
|
+
return true;
|
|
2824
|
+
}
|
|
2825
|
+
return false;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* True when the FK on side `childIdx` referencing side `parentIdx` is **correlated**
|
|
2829
|
+
* by the join — i.e. the join's cross-side equalities force the child's FK column(s)
|
|
2830
|
+
* equal to the parent's referenced column(s) for *every* `(childCol, refCol)` pair,
|
|
2831
|
+
* so a joined partner necessarily references the deleted row (a RESTRICT necessarily
|
|
2832
|
+
* fires). Matches the same `referencedTable` / `referencedSchema` predicate as
|
|
2833
|
+
* {@link fkChildIndex}; any one matching FK whose whole column pairing is equated
|
|
2834
|
+
* makes the edge correlated.
|
|
2835
|
+
*/
|
|
2836
|
+
function edgeCorrelated(childIdx, parentIdx, crossEqualities, sides) {
|
|
2837
|
+
const child = sides[childIdx];
|
|
2838
|
+
const parent = sides[parentIdx];
|
|
2839
|
+
return (child.schema.foreignKeys ?? []).some(fk => {
|
|
2840
|
+
if (!fkTargetsSide(fk, child, parent))
|
|
2841
|
+
return false;
|
|
2842
|
+
const refIndices = resolveReferencedColumns(fk, parent.schema);
|
|
2843
|
+
if (refIndices.length !== fk.columns.length)
|
|
2844
|
+
return false;
|
|
2845
|
+
return fk.columns.every((childColIdx, i) => {
|
|
2846
|
+
const childCol = child.schema.columns[childColIdx].name;
|
|
2847
|
+
const refCol = parent.schema.columns[refIndices[i]].name;
|
|
2848
|
+
return crossEqualities.has(crossEqualityKey(childIdx, childCol, parentIdx, refCol));
|
|
2849
|
+
});
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Whether the view's join **provably correlates at least one mutual FK edge** — the
|
|
2854
|
+
* gate on the plan-time `mutual-fk-restrict-delete` reject (§ Inner Join — Deletes).
|
|
2855
|
+
* Reached only when {@link orderDeleteFanout} found no feasible order (a mutual FK
|
|
2856
|
+
* whose actions no order can satisfy). The two mutual edges mirror the
|
|
2857
|
+
* {@link fkChildIndex} match: edgeA = the FK on side0 referencing side1, edgeB = the
|
|
2858
|
+
* FK on side1 referencing side0. An edge is *correlated* when the join's cross-side
|
|
2859
|
+
* column equalities force that FK's child column(s) equal to the parent's referenced
|
|
2860
|
+
* column(s) — so the joined partner necessarily references the deleted row and a
|
|
2861
|
+
* RESTRICT necessarily fires.
|
|
2862
|
+
*
|
|
2863
|
+
* Cross-side equalities are collected from the join ON condition (`sel.from[0]` is the
|
|
2864
|
+
* single `join`) **and** the body WHERE, flattened on `AND`, keeping each conjunct
|
|
2865
|
+
* that is `column = column` with both operands resolving to *different* sides
|
|
2866
|
+
* ({@link resolveColumnSide}; an unresolved/ambiguous/same-side term is skipped —
|
|
2867
|
+
* conservatively, it cannot prove correlation).
|
|
2868
|
+
*
|
|
2869
|
+
* Returns `true` iff **at least one** edge is correlated. A non-FK join (or a join on
|
|
2870
|
+
* non-FK columns) correlates neither edge ⇒ `false`, and the caller falls back to the
|
|
2871
|
+
* fixed-order fan-out, deferring to the runtime RESTRICT pre-check on the real data.
|
|
2872
|
+
* This is a strict *reduction* of over-rejection, not perfect precision: a join that
|
|
2873
|
+
* correlates one edge whose *other* edge's FK columns happen to be NULL at delete time
|
|
2874
|
+
* is still rejected (indistinguishable at plan time from the (fo-h) data-referencing
|
|
2875
|
+
* shape — accepted residual conservatism).
|
|
2876
|
+
*/
|
|
2877
|
+
function joinCorrelatesMutualFk(analysis) {
|
|
2878
|
+
const conjuncts = [];
|
|
2879
|
+
const join = analysis.sel.from?.[0];
|
|
2880
|
+
if (join && join.type === 'join' && join.condition)
|
|
2881
|
+
conjuncts.push(...flattenAnd(join.condition));
|
|
2882
|
+
if (analysis.sel.where)
|
|
2883
|
+
conjuncts.push(...flattenAnd(analysis.sel.where));
|
|
2884
|
+
const crossEqualities = new Set();
|
|
2885
|
+
for (const conj of conjuncts) {
|
|
2886
|
+
if (conj.type !== 'binary' || conj.operator !== '=')
|
|
2887
|
+
continue;
|
|
2888
|
+
if (conj.left.type !== 'column' || conj.right.type !== 'column')
|
|
2889
|
+
continue;
|
|
2890
|
+
const leftSide = resolveColumnSide(conj.left, analysis.sides);
|
|
2891
|
+
const rightSide = resolveColumnSide(conj.right, analysis.sides);
|
|
2892
|
+
if (leftSide === undefined || rightSide === undefined || leftSide === rightSide)
|
|
2893
|
+
continue;
|
|
2894
|
+
crossEqualities.add(crossEqualityKey(leftSide, conj.left.name, rightSide, conj.right.name));
|
|
2895
|
+
}
|
|
2896
|
+
return edgeCorrelated(0, 1, crossEqualities, analysis.sides)
|
|
2897
|
+
|| edgeCorrelated(1, 0, crossEqualities, analysis.sides);
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* RETURNING through a multi-source **insert** is not yet supported: it would need
|
|
2901
|
+
* the per-row minted shared surrogate threaded into the projected rows, which the
|
|
2902
|
+
* envelope materialization does not yet expose to a RETURNING projection. Reject
|
|
2903
|
+
* with a structured diagnostic (single- and multi-source update/delete RETURNING
|
|
2904
|
+
* are supported; see the builder).
|
|
2905
|
+
*/
|
|
2906
|
+
function rejectReturning(view, returning) {
|
|
2907
|
+
if (returning && returning.length > 0) {
|
|
2908
|
+
raiseMutationDiagnostic({
|
|
2909
|
+
reason: 'returning-through-view',
|
|
2910
|
+
table: view.name,
|
|
2911
|
+
message: `RETURNING through a multi-source (join) insert into view '${view.name}' is not yet supported`,
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
//# sourceMappingURL=multi-source.js.map
|