@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,1719 @@
|
|
|
1
|
+
import { FilterNode } from '../nodes/filter.js';
|
|
2
|
+
import { ProjectNode } from '../nodes/project-node.js';
|
|
3
|
+
import { buildTableReference } from '../building/table.js';
|
|
4
|
+
import { combineAnd } from './single-source.js';
|
|
5
|
+
import { transformExpr, cloneExpr } from './scope-transform.js';
|
|
6
|
+
import { buildExpression } from '../building/expression.js';
|
|
7
|
+
import { columnSchemaToScalarType } from '../type-utils.js';
|
|
8
|
+
import { createRuntimeExpressionEvaluator } from '../analysis/const-evaluator.js';
|
|
9
|
+
import { containsNonDeterministicCall } from '../analysis/check-extraction.js';
|
|
10
|
+
import { FunctionFlags } from '../../common/constants.js';
|
|
11
|
+
import { analyzeBodyLineage } from './backward-body.js';
|
|
12
|
+
import { keyColumnName, capturedValueSubquery } from './multi-source.js';
|
|
13
|
+
import { raiseMutationDiagnostic } from './mutation-diagnostic.js';
|
|
14
|
+
/**
|
|
15
|
+
* Plan the synthesized get body **once** and read its threaded `updateLineage`
|
|
16
|
+
* into a {@link DecompShape}: the per-column backward lineage (column → owning base
|
|
17
|
+
* relation) plus a `TableReferenceNode`-id → member map, so the routing / anchor
|
|
18
|
+
* gate decide off the plan-node backward walk shared with the multi-source path
|
|
19
|
+
* (not a parallel projection-AST scan). EAV pivot members are correlated subqueries,
|
|
20
|
+
* not join sources, so they carry no planned `TableReferenceNode` and are absent
|
|
21
|
+
* from {@link DecompShape.memberByTableId} (resolved off the advertisement instead).
|
|
22
|
+
*/
|
|
23
|
+
export function analyzeDecomposition(ctx, view, storage) {
|
|
24
|
+
const anchor = storage.members.find(m => m.relationId === storage.anchorRelationId);
|
|
25
|
+
if (!anchor) {
|
|
26
|
+
// Validated at advertisement resolution; defensive.
|
|
27
|
+
raiseMutationDiagnostic({
|
|
28
|
+
reason: 'no-base-lineage',
|
|
29
|
+
table: view.name,
|
|
30
|
+
message: `cannot write through logical table '${view.name}': decomposition anchor '${storage.anchorRelationId}' is not among its members`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const lineage = analyzeBodyLineage(ctx, view);
|
|
34
|
+
const memberByTableId = new Map();
|
|
35
|
+
for (const [id, ref] of lineage.tableRefsById) {
|
|
36
|
+
const matches = storage.members.filter(m => m.relation.table.toLowerCase() === ref.tableSchema.name.toLowerCase()
|
|
37
|
+
&& m.relation.schema.toLowerCase() === ref.tableSchema.schemaName.toLowerCase());
|
|
38
|
+
if (matches.length > 1) {
|
|
39
|
+
// Two members over the **same** physical base relation (a self-decomposition)
|
|
40
|
+
// both claim this body `TableReferenceNode`, so routing any column off
|
|
41
|
+
// `memberByTableId` would be ambiguous (a last-writer would silently win).
|
|
42
|
+
// The multi-source path rejects self-joins upstream, but that guard sits
|
|
43
|
+
// outside this code; enforce the single-member-per-base-ref assumption
|
|
44
|
+
// locally rather than relying on it.
|
|
45
|
+
raiseMutationDiagnostic({
|
|
46
|
+
reason: 'unsupported-decomposition-member',
|
|
47
|
+
table: view.name,
|
|
48
|
+
message: `cannot write through logical table '${view.name}': decomposition members ${matches.map(m => `'${m.relationId}'`).join(' and ')} both resolve to the same base relation '${ref.tableSchema.schemaName}.${ref.tableSchema.name}' (a self-decomposition); the put fan-out cannot disambiguate which member backs a column`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (matches.length === 1)
|
|
52
|
+
memberByTableId.set(id, matches[0]);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
storage, anchor, viewColToBaseRef: lineage.viewColToBaseRef, columns: lineage.columns, memberByTableId,
|
|
56
|
+
bodySource: lineage.bodySource, bodyScope: lineage.bodyScope,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The decomposition put fan-out for an authored (`with inverse`) column is
|
|
61
|
+
* deferred (docs/view-updateability.md § Authored inverses — the documented
|
|
62
|
+
* decomposition deferral): reject a write that targets one, naming the member(s)
|
|
63
|
+
* its puts route to, rather than letting it fall through {@link classifyColumn}'s
|
|
64
|
+
* EAV / computed-mapping fallbacks into a silent mis-route. Read consumers (the
|
|
65
|
+
* WHERE anchor-resolvability gate) deliberately do NOT run this — an authored
|
|
66
|
+
* column reads through its forward expression like any computed column.
|
|
67
|
+
*/
|
|
68
|
+
function rejectAuthoredDecompositionWrite(view, shape, name, displayName) {
|
|
69
|
+
const col = shape.columns.find(c => c.name === name);
|
|
70
|
+
if (!col?.authored)
|
|
71
|
+
return;
|
|
72
|
+
const members = [...new Set(col.authored.puts.map(p => shape.memberByTableId.get(p.table)?.relationId ?? `relation #${p.table}`))];
|
|
73
|
+
raiseMutationDiagnostic({
|
|
74
|
+
reason: 'unsupported-decomposition-member',
|
|
75
|
+
column: displayName,
|
|
76
|
+
table: view.name,
|
|
77
|
+
message: `cannot write through logical table '${view.name}': column '${displayName}' carries an authored inverse (WITH INVERSE) targeting member${members.length > 1 ? 's' : ''} ${members.map(m => `'${m}'`).join(', ')}; the decomposition put fan-out for authored inverses is deferred`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Classify one logical column against the decomposition. The **primary routing**
|
|
82
|
+
* (which member backs a writable/insertable column, and its base column) is read
|
|
83
|
+
* from the threaded `updateLineage` (`shape.columns` + `shape.memberByTableId`);
|
|
84
|
+
* the advertisement only disambiguates the deferred shapes (a non-identity mapping,
|
|
85
|
+
* an EAV pivot column), preserving the exact deferral diagnostics. Precedence
|
|
86
|
+
* mirrors the retired advertisement scan: identity base column → member-mapping →
|
|
87
|
+
* EAV pivot → unbacked.
|
|
88
|
+
*/
|
|
89
|
+
function classifyColumn(view, shape, name) {
|
|
90
|
+
const col = shape.columns.find(c => c.name === name);
|
|
91
|
+
// An identity base column on a join member, routed by the plan-node lineage.
|
|
92
|
+
if (col?.baseColumn !== undefined && col.baseTableId !== undefined && col.inverse === undefined) {
|
|
93
|
+
const member = shape.memberByTableId.get(col.baseTableId);
|
|
94
|
+
if (member)
|
|
95
|
+
return { kind: 'member', member, baseColumn: col.baseColumn, nullExtended: col.nullExtended };
|
|
96
|
+
// The lineage resolved an **identity** base column (a base site, no inverse),
|
|
97
|
+
// but no decomposition member owns its base `TableReferenceNode`. In a
|
|
98
|
+
// faithfully-synthesized body every base table-ref IS a member, so this is a
|
|
99
|
+
// lineage-resolution miss (e.g. a `memberByTableId` schema/name mismatch), NOT a
|
|
100
|
+
// non-identity mapping. Reject defensively — falling through to the name-only
|
|
101
|
+
// `member.columns` match below would silently degrade a *writable* column to
|
|
102
|
+
// `computed-mapping` (read-only), masking the lineage bug as a benign read-only
|
|
103
|
+
// column.
|
|
104
|
+
raiseMutationDiagnostic({
|
|
105
|
+
reason: 'no-base-lineage',
|
|
106
|
+
column: col.displayName,
|
|
107
|
+
table: view.name,
|
|
108
|
+
message: `cannot write through logical table '${view.name}': column '${col.displayName}' resolves to identity base column '${col.baseColumn}', but no decomposition member backs its base relation (lineage-resolution miss); a writable column must not silently degrade to read-only`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// A `member.columns` mapping the lineage did not resolve to an identity base column
|
|
112
|
+
// is a non-identity (computed / non-invertible) mapping — read-only.
|
|
113
|
+
for (const member of shape.storage.members) {
|
|
114
|
+
if (member.columns.some(c => c.logicalColumn.toLowerCase() === name)) {
|
|
115
|
+
return { kind: 'computed-mapping', member };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// An EAV pivot backs a logical column the get body projects as a (non-column)
|
|
119
|
+
// correlated subquery — never a `member.columns` entry, so the loops above miss it.
|
|
120
|
+
const projected = shape.viewColToBaseRef.get(name);
|
|
121
|
+
if (projected && projected.type !== 'column') {
|
|
122
|
+
const eav = shape.storage.members.find(m => m.attributePivot);
|
|
123
|
+
if (eav)
|
|
124
|
+
return { kind: 'eav', member: eav };
|
|
125
|
+
}
|
|
126
|
+
return { kind: 'unbacked' };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Decompose a mutation through a decomposition-backed logical table into an
|
|
130
|
+
* ordered `BaseOp[]`. Throws a structured diagnostic for any deferred shape.
|
|
131
|
+
*/
|
|
132
|
+
export function propagateDecomposition(ctx, view, storage, req) {
|
|
133
|
+
const shape = analyzeDecomposition(ctx, view, storage);
|
|
134
|
+
switch (req.op) {
|
|
135
|
+
case 'delete': return decomposeDelete(ctx, view, shape, req.stmt);
|
|
136
|
+
case 'update': return decomposeUpdate(ctx, view, shape, req.stmt);
|
|
137
|
+
case 'insert':
|
|
138
|
+
// INSERT needs the plan-level shared-surrogate envelope (materialized
|
|
139
|
+
// source + per-row mint), which the AST `BaseOp[]` model cannot express,
|
|
140
|
+
// so it is built directly by `building/view-mutation-builder.ts`
|
|
141
|
+
// (`buildDecompositionInsert`, off `analyzeDecompositionInsert` below).
|
|
142
|
+
// `buildViewMutation` routes a decomposition insert there before
|
|
143
|
+
// `propagate` runs, so this case is unreachable on the supported path.
|
|
144
|
+
raiseMutationDiagnostic({
|
|
145
|
+
reason: 'unsupported-decomposition-insert',
|
|
146
|
+
table: view.name,
|
|
147
|
+
message: `internal: decomposition insert must be built via buildDecompositionInsert, not propagate`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Decompose an INSERT through a decomposition-backed logical table into the
|
|
153
|
+
* per-member base inserts plus the shared-surrogate envelope they fan out from
|
|
154
|
+
* (anchor first — the FK-order root). Throws a structured diagnostic for any
|
|
155
|
+
* deferred shape (composite/absent key, non-integer surrogate, an uncoverable
|
|
156
|
+
* not-null member column, a computed/unbacked logical column).
|
|
157
|
+
*/
|
|
158
|
+
export function analyzeDecompositionInsert(ctx, view, storage, stmt) {
|
|
159
|
+
rejectReturning(view, stmt.returning);
|
|
160
|
+
const shape = analyzeDecomposition(ctx, view, storage);
|
|
161
|
+
const anchor = shape.anchor;
|
|
162
|
+
// Resolve every member's table once (reused for schema lookups + the seed table).
|
|
163
|
+
const memberRefs = new Map();
|
|
164
|
+
for (const m of storage.members)
|
|
165
|
+
memberRefs.set(m.relationId, resolveMemberTable(ctx, m));
|
|
166
|
+
// Supplied logical columns: the explicit list, or every projected logical column
|
|
167
|
+
// (in projection order — the order the shared backward consumer enumerates them).
|
|
168
|
+
const suppliedNames = stmt.columns && stmt.columns.length > 0
|
|
169
|
+
? stmt.columns
|
|
170
|
+
: shape.columns.map(c => c.name);
|
|
171
|
+
// Declared-case logical column names (the get body's projection aliases) — the
|
|
172
|
+
// exact attribute literals an EAV write must store (the read does not case-fold).
|
|
173
|
+
const declaredNames = declaredColumnNames(view);
|
|
174
|
+
const routed = suppliedNames.map((raw, idx) => routeInsertColumn(view, shape, memberRefs, declaredNames, raw, idx));
|
|
175
|
+
// Shared key: a surrogate's value comes from the anchor key column's declared
|
|
176
|
+
// `default`, evaluated once per row at the envelope; a logical-tuple threads the
|
|
177
|
+
// supplied logical PK. `keyEnvelopeIndex` is the envelope column every member's key
|
|
178
|
+
// column reads (the default-sourced column for a surrogate, the supplied PK column
|
|
179
|
+
// for a logical-tuple, or undefined for the singleton empty key).
|
|
180
|
+
const { keyEnvelopeIndex, keyDefault } = resolveInsertSharedKey(view, shape, memberRefs, routed);
|
|
181
|
+
// Member ops, anchor first (FK-order root: members may FK-reference the anchor).
|
|
182
|
+
const ops = [];
|
|
183
|
+
emitMemberInsert(view, shape, memberRefs, routed, keyEnvelopeIndex, anchor, ops);
|
|
184
|
+
for (const member of storage.members) {
|
|
185
|
+
if (member.relationId === anchor.relationId)
|
|
186
|
+
continue;
|
|
187
|
+
emitMemberInsert(view, shape, memberRefs, routed, keyEnvelopeIndex, member, ops);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
suppliedColumns: routed.map(r => ({ name: r.name, type: r.type })),
|
|
191
|
+
ops,
|
|
192
|
+
keyDefault,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Route one supplied logical column to a columnar member mapping or an EAV pivot,
|
|
197
|
+
* off the threaded backward lineage ({@link classifyColumn}). A columnar route binds
|
|
198
|
+
* the value to its member's identity base column; an EAV route writes an attribute
|
|
199
|
+
* triple gated on the value. Optional members are insertable here (the per-row
|
|
200
|
+
* presence gate in {@link emitMemberInsert} drops absent components).
|
|
201
|
+
*/
|
|
202
|
+
function routeInsertColumn(view, shape, memberRefs, declaredNames, rawName, idx) {
|
|
203
|
+
const name = rawName.toLowerCase();
|
|
204
|
+
rejectAuthoredDecompositionWrite(view, shape, name, rawName);
|
|
205
|
+
const route = classifyColumn(view, shape, name);
|
|
206
|
+
switch (route.kind) {
|
|
207
|
+
case 'member': {
|
|
208
|
+
const ref = memberRefs.get(route.member.relationId);
|
|
209
|
+
const col = columnByName(view, ref.tableSchema, route.baseColumn);
|
|
210
|
+
return { name, envelopeIndex: idx, type: columnSchemaToScalarType(col), columnar: { relationId: route.member.relationId, basisColumn: route.baseColumn } };
|
|
211
|
+
}
|
|
212
|
+
case 'eav': {
|
|
213
|
+
const ref = memberRefs.get(route.member.relationId);
|
|
214
|
+
const valCol = columnByName(view, ref.tableSchema, route.member.attributePivot.valueColumn);
|
|
215
|
+
return { name, envelopeIndex: idx, type: columnSchemaToScalarType(valCol), eav: route.member, eavAttribute: declaredNames.get(name) ?? rawName };
|
|
216
|
+
}
|
|
217
|
+
case 'computed-mapping':
|
|
218
|
+
return raiseMutationDiagnostic({
|
|
219
|
+
reason: 'no-inverse',
|
|
220
|
+
column: rawName,
|
|
221
|
+
table: view.name,
|
|
222
|
+
message: `cannot insert into logical table '${view.name}': column '${rawName}' is a computed (non-invertible) decomposition mapping and cannot receive an inserted value`,
|
|
223
|
+
});
|
|
224
|
+
case 'unbacked':
|
|
225
|
+
return raiseMutationDiagnostic({
|
|
226
|
+
reason: 'no-inverse',
|
|
227
|
+
column: rawName,
|
|
228
|
+
table: view.name,
|
|
229
|
+
message: `cannot insert into logical table '${view.name}': column '${rawName}' is not backed by any decomposition member`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Resolve the shared key envelope index + optional anchor-default source for an insert. */
|
|
234
|
+
function resolveInsertSharedKey(view, shape, memberRefs, routed) {
|
|
235
|
+
const sharedKey = shape.storage.sharedKey;
|
|
236
|
+
const anchor = shape.anchor;
|
|
237
|
+
if (sharedKey.kind === 'surrogate') {
|
|
238
|
+
const anchorKeys = memberKeyColumns(view, shape, anchor);
|
|
239
|
+
if (anchorKeys.length !== 1) {
|
|
240
|
+
raiseMutationDiagnostic({
|
|
241
|
+
reason: 'unsupported-decomposition-key',
|
|
242
|
+
table: view.name,
|
|
243
|
+
message: `cannot insert into logical table '${view.name}': a surrogate decomposition needs a single-column key on the anchor '${anchor.relationId}' (v1 threads a single-column surrogate)`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const anchorRef = memberRefs.get(anchor.relationId);
|
|
247
|
+
// The surrogate's value comes from the anchor key column's declared `default`
|
|
248
|
+
// (the engine no longer auto-generates one): evaluated once per row at the
|
|
249
|
+
// envelope and EC-threaded into every member's key column.
|
|
250
|
+
const keyDefault = requireKeyDefault(view, anchorRef.tableSchema, columnByName(view, anchorRef.tableSchema, anchorKeys[0]));
|
|
251
|
+
return {
|
|
252
|
+
keyEnvelopeIndex: routed.length, // the default-sourced column is appended last
|
|
253
|
+
keyDefault,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// logical-tuple: the supplied logical PK threads to every member's key column.
|
|
257
|
+
const anchorKeys = memberKeyColumns(view, shape, anchor);
|
|
258
|
+
if (anchorKeys.length === 0) {
|
|
259
|
+
return { keyEnvelopeIndex: undefined, keyDefault: undefined }; // singleton — no key to thread
|
|
260
|
+
}
|
|
261
|
+
const anchorKeyCol = anchorKeys[0].toLowerCase();
|
|
262
|
+
const keyRouted = routed.find(r => r.columnar
|
|
263
|
+
&& r.columnar.relationId === anchor.relationId
|
|
264
|
+
&& r.columnar.basisColumn.toLowerCase() === anchorKeyCol);
|
|
265
|
+
if (!keyRouted) {
|
|
266
|
+
raiseMutationDiagnostic({
|
|
267
|
+
reason: 'no-default',
|
|
268
|
+
table: view.name,
|
|
269
|
+
message: `cannot insert into logical table '${view.name}': the logical-tuple shared key (anchor '${anchor.relationId}' column '${anchorKeys[0]}') is not supplied through the logical table; a logical-tuple key threads the supplied value, so it must be provided`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return { keyEnvelopeIndex: keyRouted.envelopeIndex, keyDefault: undefined };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The anchor key column's declared `default` — the surrogate's per-row source —
|
|
276
|
+
* evaluated once per produced row at the envelope (with `mutation_ordinal()` in
|
|
277
|
+
* scope) and EC-threaded into every member's key column. The engine no longer
|
|
278
|
+
* invents a surrogate: a surrogate key whose anchor column declares no default
|
|
279
|
+
* raises `no-default` with the migration recipe.
|
|
280
|
+
*/
|
|
281
|
+
function requireKeyDefault(view, schema, keyCol) {
|
|
282
|
+
if (keyCol.defaultValue === null) {
|
|
283
|
+
raiseMutationDiagnostic({
|
|
284
|
+
reason: 'no-default',
|
|
285
|
+
column: keyCol.name,
|
|
286
|
+
table: view.name,
|
|
287
|
+
message: `cannot insert into logical table '${view.name}': the surrogate shared key '${schema.name}.${keyCol.name}' declares no DEFAULT; a surrogate's value comes from the anchor key column's default (e.g. \`default (coalesce((select max(${keyCol.name}) from ${schema.name}), 0) + mutation_ordinal())\`) — the engine no longer auto-generates one`,
|
|
288
|
+
suggestion: `declare a DEFAULT on '${schema.name}.${keyCol.name}', or expose the key as a supplied logical column`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return keyCol.defaultValue;
|
|
292
|
+
}
|
|
293
|
+
/** Emit the base insert op(s) for one member (one columnar op, or one triple per supplied EAV attribute). */
|
|
294
|
+
function emitMemberInsert(view, shape, memberRefs, routed, keyEnvelopeIndex, member, ops) {
|
|
295
|
+
const ref = memberRefs.get(member.relationId);
|
|
296
|
+
const schema = ref.tableSchema;
|
|
297
|
+
if (member.attributePivot) {
|
|
298
|
+
// EAV pivot: one triple insert per supplied attribute, gated on a non-null value.
|
|
299
|
+
const pivot = member.attributePivot;
|
|
300
|
+
for (const r of routed) {
|
|
301
|
+
if (r.eav?.relationId !== member.relationId)
|
|
302
|
+
continue;
|
|
303
|
+
const columns = [
|
|
304
|
+
{ baseColumn: pivot.entityColumn, envelopeIndex: requireKeyIndex(view, member, keyEnvelopeIndex) },
|
|
305
|
+
{ baseColumn: pivot.attributeColumn, literal: r.eavAttribute ?? r.name },
|
|
306
|
+
{ baseColumn: pivot.valueColumn, envelopeIndex: r.envelopeIndex },
|
|
307
|
+
];
|
|
308
|
+
assertNoMissingNotNull(view, schema, columns);
|
|
309
|
+
ops.push({ table: ref, schema, columns, presenceGateIndices: [r.envelopeIndex] });
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const ownedSupplied = routed.filter(r => r.columnar?.relationId === member.relationId);
|
|
314
|
+
// An optional member with no supplied columns materializes no row (the read's
|
|
315
|
+
// outer join already yields null for an absent component).
|
|
316
|
+
if (member.presence === 'optional' && ownedSupplied.length === 0)
|
|
317
|
+
return;
|
|
318
|
+
const memberKeys = memberKeyColumns(view, shape, member);
|
|
319
|
+
const columns = [];
|
|
320
|
+
if (memberKeys.length === 1) {
|
|
321
|
+
columns.push({ baseColumn: memberKeys[0], envelopeIndex: requireKeyIndex(view, member, keyEnvelopeIndex) });
|
|
322
|
+
}
|
|
323
|
+
const keyColLower = memberKeys[0]?.toLowerCase();
|
|
324
|
+
for (const r of ownedSupplied) {
|
|
325
|
+
// The anchor's own key column is already threaded above; don't double-insert it.
|
|
326
|
+
if (keyColLower && r.columnar.basisColumn.toLowerCase() === keyColLower)
|
|
327
|
+
continue;
|
|
328
|
+
columns.push({ baseColumn: r.columnar.basisColumn, envelopeIndex: r.envelopeIndex });
|
|
329
|
+
}
|
|
330
|
+
assertNoMissingNotNull(view, schema, columns);
|
|
331
|
+
// Optional members are gated per-row on supplying ≥1 of their (non-key) values.
|
|
332
|
+
const gate = member.presence === 'optional'
|
|
333
|
+
? ownedSupplied.filter(r => r.columnar.basisColumn.toLowerCase() !== keyColLower).map(r => r.envelopeIndex)
|
|
334
|
+
: [];
|
|
335
|
+
ops.push({ table: ref, schema, columns, presenceGateIndices: gate });
|
|
336
|
+
}
|
|
337
|
+
/** The shared-key columns for a member (0 for a singleton; >1 is a deferred composite key). */
|
|
338
|
+
function memberKeyColumns(view, shape, member) {
|
|
339
|
+
const keys = shape.storage.sharedKey.keyColumnsByRelation.get(member.relationId) ?? [];
|
|
340
|
+
if (keys.length > 1) {
|
|
341
|
+
raiseMutationDiagnostic({
|
|
342
|
+
reason: 'unsupported-decomposition-key',
|
|
343
|
+
table: view.name,
|
|
344
|
+
message: `cannot write through a decomposition with a composite shared key on member '${member.relationId}': v1 fan-out threads a single-column key`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return [...keys];
|
|
348
|
+
}
|
|
349
|
+
/** A member needs a shared key value to thread, but the decomposition's key is empty (singleton). */
|
|
350
|
+
function requireKeyIndex(view, member, keyEnvelopeIndex) {
|
|
351
|
+
if (keyEnvelopeIndex === undefined) {
|
|
352
|
+
raiseMutationDiagnostic({
|
|
353
|
+
reason: 'unsupported-decomposition-key',
|
|
354
|
+
table: view.name,
|
|
355
|
+
message: `cannot insert into logical table '${view.name}': member '${member.relationId}' needs a shared key value to thread, but the decomposition has an empty (singleton) key`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return keyEnvelopeIndex;
|
|
359
|
+
}
|
|
360
|
+
/** Reject a not-null base column with no declared default that no envelope value covers. */
|
|
361
|
+
function assertNoMissingNotNull(view, schema, columns) {
|
|
362
|
+
const covered = new Set(columns.map(c => c.baseColumn.toLowerCase()));
|
|
363
|
+
for (const col of schema.columns) {
|
|
364
|
+
if (col.generated || !col.notNull || col.defaultValue !== null)
|
|
365
|
+
continue;
|
|
366
|
+
if (covered.has(col.name.toLowerCase()))
|
|
367
|
+
continue;
|
|
368
|
+
raiseMutationDiagnostic({
|
|
369
|
+
reason: 'no-default',
|
|
370
|
+
column: col.name,
|
|
371
|
+
table: view.name,
|
|
372
|
+
message: `cannot insert into logical table '${view.name}': basis relation '${schema.name}' column '${col.name}' is NOT NULL with no default and no value reaches it through the decomposition`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function columnByName(view, schema, name) {
|
|
377
|
+
const col = schema.columns.find(c => c.name.toLowerCase() === name.toLowerCase());
|
|
378
|
+
if (!col) {
|
|
379
|
+
raiseMutationDiagnostic({
|
|
380
|
+
reason: 'no-base-lineage',
|
|
381
|
+
table: view.name,
|
|
382
|
+
column: name,
|
|
383
|
+
message: `cannot write through logical table '${view.name}': column '${name}' not found on basis relation '${schema.name}'`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return col;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Map each logical column's lowercased name → its **declared** name, read off the
|
|
390
|
+
* get body's projection aliases (`<expr> as <col.name>`). The declared name is the
|
|
391
|
+
* exact attribute literal `compileDecompositionBody` matches an EAV column against,
|
|
392
|
+
* so an EAV write must store that spelling (the read does not case-fold).
|
|
393
|
+
*/
|
|
394
|
+
function declaredColumnNames(view) {
|
|
395
|
+
const map = new Map();
|
|
396
|
+
const sel = view.selectAst;
|
|
397
|
+
if (sel.type !== 'select')
|
|
398
|
+
return map;
|
|
399
|
+
for (const rc of sel.columns) {
|
|
400
|
+
if (rc.type !== 'column')
|
|
401
|
+
continue;
|
|
402
|
+
const name = rc.alias ?? (rc.expr.type === 'column' ? rc.expr.name : undefined);
|
|
403
|
+
if (name)
|
|
404
|
+
map.set(name.toLowerCase(), name);
|
|
405
|
+
}
|
|
406
|
+
return map;
|
|
407
|
+
}
|
|
408
|
+
// --- DELETE ---------------------------------------------------------------
|
|
409
|
+
/**
|
|
410
|
+
* Fan a logical delete out to every member. Order anchor-last; each non-anchor
|
|
411
|
+
* member's identifying set is `select <anchorKey> from <anchor> where <pred>`, so
|
|
412
|
+
* deleting other members never changes it. The anchor's own delete then applies
|
|
413
|
+
* the predicate directly against the anchor (its IN subquery would self-reference
|
|
414
|
+
* the rows it removes, so the bare-predicate form is both simpler and clearer).
|
|
415
|
+
*/
|
|
416
|
+
function decomposeDelete(ctx, view, shape, stmt) {
|
|
417
|
+
rejectReturning(view, stmt.returning);
|
|
418
|
+
const pred = anchorPredicate(view, shape, stmt.where);
|
|
419
|
+
const ops = [];
|
|
420
|
+
// Non-anchor members first (each reads the still-intact anchor), anchor last.
|
|
421
|
+
for (const member of shape.storage.members) {
|
|
422
|
+
if (member.relationId === shape.anchor.relationId)
|
|
423
|
+
continue;
|
|
424
|
+
ops.push(memberDeleteOp(ctx, view, shape, member, pred, stmt));
|
|
425
|
+
}
|
|
426
|
+
ops.push(anchorDeleteOp(ctx, view, shape, pred, stmt));
|
|
427
|
+
return ops;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* One member's delete. No predicate ⇒ an unconditional `delete from <member>`
|
|
431
|
+
* (truncate the component — also the sound singleton path, which has no key to
|
|
432
|
+
* thread). With an anchor predicate ⇒ `delete from <member> where
|
|
433
|
+
* <memberKeyOrEntity> in (select <anchorKey> from <anchor> where <pred>)`.
|
|
434
|
+
*/
|
|
435
|
+
function memberDeleteOp(ctx, view, shape, member, pred, stmt) {
|
|
436
|
+
let where;
|
|
437
|
+
if (pred) {
|
|
438
|
+
const memberCol = member.attributePivot
|
|
439
|
+
? member.attributePivot.entityColumn // EAV: delete every triple for the matched entities
|
|
440
|
+
: singleKeyColumn(view, shape, member);
|
|
441
|
+
where = { type: 'in', expr: { type: 'column', name: memberCol }, subquery: anchorKeySubquery(shape, pred) };
|
|
442
|
+
}
|
|
443
|
+
const statement = {
|
|
444
|
+
type: 'delete',
|
|
445
|
+
table: memberIdentifier(member),
|
|
446
|
+
where,
|
|
447
|
+
contextValues: stmt.contextValues,
|
|
448
|
+
schemaPath: stmt.schemaPath,
|
|
449
|
+
loc: stmt.loc,
|
|
450
|
+
};
|
|
451
|
+
return { table: resolveMemberTable(ctx, member), op: 'delete', statement };
|
|
452
|
+
}
|
|
453
|
+
/** `delete from <anchor> [where <pred bare>]`. */
|
|
454
|
+
function anchorDeleteOp(ctx, view, shape, pred, stmt) {
|
|
455
|
+
const statement = {
|
|
456
|
+
type: 'delete',
|
|
457
|
+
table: memberIdentifier(shape.anchor),
|
|
458
|
+
where: pred ? stripAnchorQualifier(pred, shape) : undefined,
|
|
459
|
+
contextValues: stmt.contextValues,
|
|
460
|
+
schemaPath: stmt.schemaPath,
|
|
461
|
+
loc: stmt.loc,
|
|
462
|
+
};
|
|
463
|
+
return { table: resolveMemberTable(ctx, shape.anchor), op: 'delete', statement };
|
|
464
|
+
}
|
|
465
|
+
// --- UPDATE ---------------------------------------------------------------
|
|
466
|
+
/**
|
|
467
|
+
* Route each assignment to the member that backs it and emit the per-member base
|
|
468
|
+
* ops, anchor-last (so a member whose column the predicate reads is not mutated
|
|
469
|
+
* before a sibling's identifying set is computed, and so every materialization
|
|
470
|
+
* branch reads the still-intact anchor). A **mandatory, non-EAV** member takes one
|
|
471
|
+
* base UPDATE (the legacy path). An **optional columnar** member's write is a per-row
|
|
472
|
+
* materialization transition — matched → base UPDATE, absent → null-extended INSERT,
|
|
473
|
+
* all value columns set null → base DELETE — and an **EAV pivot** member's write is
|
|
474
|
+
* the per-attribute triple analogue (non-null → upsert, null → delete). The branches
|
|
475
|
+
* are ordinary AST base ops keyed off the anchor subquery, not a new plan-node
|
|
476
|
+
* substrate (the same realization consumer 1 used for the outer-join dual). Shared-key
|
|
477
|
+
* (identity) / computed targets stay rejected.
|
|
478
|
+
*
|
|
479
|
+
* `capturedValues` is the optional **capture carrier** the builder threads (parallel to
|
|
480
|
+
* `decomposeUpdate`'s `sourceValues` in multi-source.ts): when present, an **arbitrary**
|
|
481
|
+
* value on an *optional columnar* member OR an **EAV** member (a subquery, cross-member, or
|
|
482
|
+
* mixed anchor+self — and, for EAV, any self-reference, since an EAV value column lowers to a
|
|
483
|
+
* subquery — otherwise rejected) is lowered to base terms and accumulated as a `srcN`
|
|
484
|
+
* projection, and its cell rides the per-row capture two-op path
|
|
485
|
+
* ({@link emitCapturedMemberUpdate} for columnar, {@link emitEavCapturedAttr} for EAV); the
|
|
486
|
+
* builder then materializes the values once over the planned body
|
|
487
|
+
* ({@link buildDecompositionKeyCapture}) and every base op reads them back through the
|
|
488
|
+
* context-injected `__vmupd_keys`. Absent the carrier (the legacy `propagateDecomposition`
|
|
489
|
+
* path, unreachable from build), an arbitrary value stays rejected — the defensive legacy
|
|
490
|
+
* behaviour, exactly as `propagateMultiSource`'s update path does.
|
|
491
|
+
*/
|
|
492
|
+
export function decomposeUpdate(ctx, view, shape, stmt, capturedValues) {
|
|
493
|
+
rejectReturning(view, stmt.returning);
|
|
494
|
+
const pred = anchorPredicate(view, shape, stmt.where);
|
|
495
|
+
const declaredNames = declaredColumnNames(view);
|
|
496
|
+
// Capture carrier (mirrors multi-source.ts `decomposeUpdate`'s `registerCapturedExpr`):
|
|
497
|
+
// project an already-lowered base-term value into the up-front `__vmupd_keys` capture under
|
|
498
|
+
// a stable `srcN` alias (deduped by `key`), returning that alias. `canCapture` gates whether
|
|
499
|
+
// an arbitrary value — on an optional-columnar OR an EAV member — is admitted (carrier present)
|
|
500
|
+
// or rejected (the legacy carrier-absent path).
|
|
501
|
+
const canCapture = capturedValues !== undefined;
|
|
502
|
+
const srcDedup = new Map();
|
|
503
|
+
const registerCapturedExpr = capturedValues
|
|
504
|
+
? (key, expr) => {
|
|
505
|
+
const existing = srcDedup.get(key);
|
|
506
|
+
if (existing)
|
|
507
|
+
return existing;
|
|
508
|
+
const alias = `src${capturedValues.length}`;
|
|
509
|
+
srcDedup.set(key, alias);
|
|
510
|
+
capturedValues.push({ alias, expr });
|
|
511
|
+
return alias;
|
|
512
|
+
}
|
|
513
|
+
: undefined;
|
|
514
|
+
// Per-member accumulation. A member is exactly one kind (mandatory-columnar /
|
|
515
|
+
// optional-columnar / EAV), so at most one of the three maps holds a given member.
|
|
516
|
+
const mandatory = new Map();
|
|
517
|
+
const optional = new Map();
|
|
518
|
+
const eav = new Map();
|
|
519
|
+
// member relationId → (target-key lower → first view-column spelling). Two distinct
|
|
520
|
+
// logical columns can route to the same basis column / EAV attribute on one member
|
|
521
|
+
// (e.g. a duplicate rename `b, b as b2`); the per-member UPDATE / triple would then
|
|
522
|
+
// assign it twice. Reject view-aware so the message names both logical columns.
|
|
523
|
+
const seenPerMember = new Map();
|
|
524
|
+
const noteTarget = (relationId, targetKey, viewCol) => {
|
|
525
|
+
let seen = seenPerMember.get(relationId);
|
|
526
|
+
if (!seen) {
|
|
527
|
+
seen = new Map();
|
|
528
|
+
seenPerMember.set(relationId, seen);
|
|
529
|
+
}
|
|
530
|
+
const prior = seen.get(targetKey);
|
|
531
|
+
if (prior !== undefined) {
|
|
532
|
+
raiseMutationDiagnostic({
|
|
533
|
+
reason: 'conflicting-assignment',
|
|
534
|
+
column: viewCol,
|
|
535
|
+
table: view.name,
|
|
536
|
+
message: `cannot update logical table '${view.name}': columns '${prior}' and '${viewCol}' both target '${targetKey}' on member '${relationId}'; an UPDATE cannot assign one column twice`,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
seen.set(targetKey, viewCol);
|
|
540
|
+
};
|
|
541
|
+
for (const asg of stmt.assignments) {
|
|
542
|
+
const routed = routeAssignment(view, shape, declaredNames, asg, canCapture);
|
|
543
|
+
switch (routed.kind) {
|
|
544
|
+
case 'mandatory': {
|
|
545
|
+
noteTarget(routed.member.relationId, routed.basisColumn.toLowerCase(), asg.column);
|
|
546
|
+
let list = mandatory.get(routed.member.relationId);
|
|
547
|
+
if (!list) {
|
|
548
|
+
list = [];
|
|
549
|
+
mandatory.set(routed.member.relationId, list);
|
|
550
|
+
}
|
|
551
|
+
list.push({ column: routed.basisColumn, value: routed.value });
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'optional': {
|
|
555
|
+
noteTarget(routed.member.relationId, routed.basisColumn.toLowerCase(), asg.column);
|
|
556
|
+
let g = optional.get(routed.member.relationId);
|
|
557
|
+
if (!g) {
|
|
558
|
+
g = { member: routed.member, cells: [] };
|
|
559
|
+
optional.set(routed.member.relationId, g);
|
|
560
|
+
}
|
|
561
|
+
g.cells.push({ basisColumn: routed.basisColumn, value: routed.value, isNull: routed.isNull, kind: routed.valueKind });
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case 'eav': {
|
|
565
|
+
noteTarget(routed.member.relationId, `attr:${routed.attribute.toLowerCase()}`, asg.column);
|
|
566
|
+
let g = eav.get(routed.member.relationId);
|
|
567
|
+
if (!g) {
|
|
568
|
+
g = { member: routed.member, cells: [] };
|
|
569
|
+
eav.set(routed.member.relationId, g);
|
|
570
|
+
}
|
|
571
|
+
g.cells.push({ attribute: routed.attribute, value: routed.value, isNull: routed.isNull, kind: routed.valueKind });
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const ops = [];
|
|
577
|
+
const emit = (member) => {
|
|
578
|
+
const m = mandatory.get(member.relationId);
|
|
579
|
+
if (m && m.length > 0)
|
|
580
|
+
ops.push(memberUpdateOp(ctx, view, shape, member, m, pred, stmt));
|
|
581
|
+
const o = optional.get(member.relationId);
|
|
582
|
+
if (o)
|
|
583
|
+
emitOptionalMemberUpdate(ctx, view, shape, member, o.cells, pred, stmt, ops, registerCapturedExpr);
|
|
584
|
+
const e = eav.get(member.relationId);
|
|
585
|
+
if (e)
|
|
586
|
+
emitEavMemberUpdate(ctx, view, shape, member, e.cells, pred, stmt, ops, registerCapturedExpr);
|
|
587
|
+
};
|
|
588
|
+
for (const member of shape.storage.members) {
|
|
589
|
+
if (member.relationId === shape.anchor.relationId)
|
|
590
|
+
continue;
|
|
591
|
+
emit(member);
|
|
592
|
+
}
|
|
593
|
+
emit(shape.anchor); // anchor last
|
|
594
|
+
return ops;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Resolve one `set <col> = <value>` to its backing member off the threaded backward
|
|
598
|
+
* lineage ({@link classifyColumn}). A mandatory, non-EAV identity member is the legacy
|
|
599
|
+
* value-write; an **optional columnar** member and an **EAV pivot** member are now
|
|
600
|
+
* routed to a per-row materialization transition (matched update / absent insert /
|
|
601
|
+
* emptied delete) instead of rejected. A shared-key (identity) / computed / unbacked
|
|
602
|
+
* target stays rejected with its precise diagnostic.
|
|
603
|
+
*/
|
|
604
|
+
function routeAssignment(view, shape, declaredNames, asg, canCapture) {
|
|
605
|
+
const logical = asg.column.toLowerCase();
|
|
606
|
+
if (isSharedKeyColumn(shape, logical)) {
|
|
607
|
+
raiseMutationDiagnostic({
|
|
608
|
+
reason: 'unsupported-decomposition-update',
|
|
609
|
+
column: asg.column,
|
|
610
|
+
table: view.name,
|
|
611
|
+
message: `cannot update logical table '${view.name}': column '${asg.column}' is part of the decomposition shared key; an identity change is not a value write`,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
rejectAuthoredDecompositionWrite(view, shape, logical, asg.column);
|
|
615
|
+
const route = classifyColumn(view, shape, logical);
|
|
616
|
+
switch (route.kind) {
|
|
617
|
+
case 'member': {
|
|
618
|
+
// An optional member is outer-joined (null-extended lineage): writing it is a
|
|
619
|
+
// per-row materialization transition (matched → update, absent → insert, all
|
|
620
|
+
// value columns null → delete), realized as anchor-keyed AST base ops below.
|
|
621
|
+
if (route.member.presence !== 'mandatory' || route.nullExtended) {
|
|
622
|
+
const { kind, value, isNull } = lowerMaterializedValue(view, shape, route.member, asg, canCapture);
|
|
623
|
+
return { kind: 'optional', member: route.member, basisColumn: route.baseColumn, value, isNull, valueKind: kind };
|
|
624
|
+
}
|
|
625
|
+
return { kind: 'mandatory', member: route.member, basisColumn: route.baseColumn, value: rewriteAssignedValue(view, shape, route.member, asg.value) };
|
|
626
|
+
}
|
|
627
|
+
case 'eav': {
|
|
628
|
+
// An EAV pivot backs its logical columns as attribute triples: a null deletes the
|
|
629
|
+
// triple, a constant/anchor value upserts it, and an arbitrary value (a self-reference,
|
|
630
|
+
// cross-member, or subquery — all of which lower to a subquery for EAV) rides the
|
|
631
|
+
// single-identity capture when the carrier is present (`canCapture`), else is rejected.
|
|
632
|
+
const { kind, value, isNull } = lowerMaterializedValue(view, shape, route.member, asg, canCapture);
|
|
633
|
+
return { kind: 'eav', member: route.member, attribute: declaredNames.get(logical) ?? asg.column, value, isNull, valueKind: kind };
|
|
634
|
+
}
|
|
635
|
+
case 'computed-mapping':
|
|
636
|
+
return raiseMutationDiagnostic({
|
|
637
|
+
reason: 'no-inverse',
|
|
638
|
+
column: asg.column,
|
|
639
|
+
table: view.name,
|
|
640
|
+
message: `cannot update logical table '${view.name}': column '${asg.column}' is a computed (non-invertible) decomposition mapping and is read-only`,
|
|
641
|
+
});
|
|
642
|
+
case 'unbacked':
|
|
643
|
+
return raiseMutationDiagnostic({
|
|
644
|
+
reason: 'no-inverse',
|
|
645
|
+
column: asg.column,
|
|
646
|
+
table: view.name,
|
|
647
|
+
message: `cannot update logical table '${view.name}': column '${asg.column}' is not backed by any decomposition member`,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Lower an optional/EAV-member assigned value to base terms and classify how it is
|
|
653
|
+
* scoped ({@link LoweredValue}). The matched write evaluates the value in the **member's**
|
|
654
|
+
* row scope while the materialize INSERT evaluates it over the **anchor** scan — so a
|
|
655
|
+
* non-constant value is only expressible when both branches can agree on it with **no new
|
|
656
|
+
* runtime substrate**. Three self-contained shapes qualify:
|
|
657
|
+
*
|
|
658
|
+
* - **constant** (no column ref, or a null literal) — survives both scopes trivially.
|
|
659
|
+
* - **anchor** — every leaf resolves to an anchor base column (`set c = a + 1`). The value
|
|
660
|
+
* is computed once over the anchor scan and the two branches are unified by an upsert
|
|
661
|
+
* ({@link buildOptionalMemberInsertSelect} `do update`), so the matched side reads the
|
|
662
|
+
* identical anchor-computed value via `excluded.<col>`.
|
|
663
|
+
* - **self** (columnar only) — every leaf is the owning member's own column (`set c = c + 1`,
|
|
664
|
+
* `set c = coalesce(c, 0) + 1`). Present rows take the matched UPDATE (their real prior value);
|
|
665
|
+
* absent rows take a materialize INSERT computing the self-expression with the owner's columns
|
|
666
|
+
* substituted to NULL, gated by a runtime non-empty filter. The matched and materialize branches
|
|
667
|
+
* read different scans (member vs null-substituted anchor), so they stay two distinct ops — not
|
|
668
|
+
* an upsert (see {@link emitOptionalMemberUpdate} / {@link buildSelfMaterializeInsertSelect}).
|
|
669
|
+
*
|
|
670
|
+
* Anything else — a subquery, a cross-member column, an unqualified ref, or a single value
|
|
671
|
+
* mixing anchor and self leaves — is **arbitrary**. With a capture carrier present (`canCapture`)
|
|
672
|
+
* it is admitted as `captured` on **both** an optional columnar member and an **EAV** member:
|
|
673
|
+
* lowered to base terms and (at emit) projected into the up-front `__vmupd_keys` set, read back by
|
|
674
|
+
* the stitch key in both branches — by the member key for columnar ({@link emitCapturedMemberUpdate}),
|
|
675
|
+
* by the entity column / anchor key for the EAV per-attribute triple pair ({@link emitEavMemberUpdate}).
|
|
676
|
+
* An EAV value column substitutes to a correlated subquery, so an EAV self-reference (`set p = p + 1`)
|
|
677
|
+
* lowers to a subquery and reaches here as `captured` (only the carrier-absent legacy
|
|
678
|
+
* `propagateDecomposition` path still rejects it). Classification reads the lowered value's
|
|
679
|
+
* column-ref **relationId qualifiers** (the synthesized body aliases each member by its
|
|
680
|
+
* relationId — the same qualifier {@link rewriteAssignedValue} keys cross-member rejection on);
|
|
681
|
+
* the owner is never the anchor (an optional/EAV member is mandatory-distinct), so `anchor` and
|
|
682
|
+
* `self` never collide.
|
|
683
|
+
*/
|
|
684
|
+
function lowerMaterializedValue(view, shape, owner, asg, canCapture) {
|
|
685
|
+
const lowered = substituteViewColumns(asg.value, shape, view);
|
|
686
|
+
const isNull = isNullLiteral(lowered);
|
|
687
|
+
const { qualifiers, hasUnqualifiedColumn, hasSubquery } = collectValueScopes(lowered);
|
|
688
|
+
const ownerIsColumnar = owner.attributePivot === undefined;
|
|
689
|
+
if (!hasSubquery && !hasUnqualifiedColumn && qualifiers.size === 0) {
|
|
690
|
+
return { kind: 'constant', value: lowered, isNull };
|
|
691
|
+
}
|
|
692
|
+
if (!hasSubquery && !hasUnqualifiedColumn) {
|
|
693
|
+
const anchorId = shape.anchor.relationId;
|
|
694
|
+
if ([...qualifiers].every(q => q === anchorId))
|
|
695
|
+
return { kind: 'anchor', value: lowered, isNull };
|
|
696
|
+
if (ownerIsColumnar && [...qualifiers].every(q => q === owner.relationId))
|
|
697
|
+
return { kind: 'self', value: lowered, isNull };
|
|
698
|
+
}
|
|
699
|
+
// Arbitrary value. With a capture carrier present it is admitted as `captured` — for an optional
|
|
700
|
+
// **columnar** member (read back by the member key in both branches) and for an **EAV** member
|
|
701
|
+
// alike (read back by the entity column / anchor key into the per-attribute triple pair; an EAV
|
|
702
|
+
// value column substitutes to a correlated subquery, so any EAV self-reference reaches here). The
|
|
703
|
+
// cell just classifies `captured`; the value is registered + read back at emit. Only the
|
|
704
|
+
// carrier-absent legacy `propagateDecomposition` path stays rejected.
|
|
705
|
+
if (canCapture) {
|
|
706
|
+
return { kind: 'captured', value: lowered, isNull };
|
|
707
|
+
}
|
|
708
|
+
raiseMutationDiagnostic({
|
|
709
|
+
reason: 'unsupported-decomposition-update',
|
|
710
|
+
column: asg.column,
|
|
711
|
+
table: view.name,
|
|
712
|
+
message: `cannot update logical table '${view.name}': writing ${ownerIsColumnar ? 'optional-member' : 'EAV-member'} column '${asg.column}' with this value needs the per-row capture carrier to thread it across the matched-update and materialize branches, which the legacy non-build path does not supply`,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Walk a lowered (base-term) assigned value, collecting the distinct member-relationId
|
|
717
|
+
* qualifiers its column refs carry, whether any column ref is **unqualified**, and whether
|
|
718
|
+
* it embeds a subquery. {@link lowerMaterializedValue} classifies the value by whether every
|
|
719
|
+
* qualifier is the anchor's (`anchor`) or the owning member's (`self`); an unqualified ref or
|
|
720
|
+
* a subquery forces `arbitrary` (the user-term analogue of the predicate gate's
|
|
721
|
+
* {@link collectViewColumnRefs}, but keyed on the base qualifier rather than the logical name).
|
|
722
|
+
*/
|
|
723
|
+
function collectValueScopes(expr) {
|
|
724
|
+
const qualifiers = new Set();
|
|
725
|
+
let hasUnqualifiedColumn = false;
|
|
726
|
+
let hasSubquery = false;
|
|
727
|
+
const walk = (node) => {
|
|
728
|
+
if (Array.isArray(node)) {
|
|
729
|
+
node.forEach(walk);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (!node || typeof node !== 'object' || !('type' in node))
|
|
733
|
+
return;
|
|
734
|
+
const n = node;
|
|
735
|
+
if (n.type === 'column') {
|
|
736
|
+
if (typeof n.table === 'string')
|
|
737
|
+
qualifiers.add(n.table);
|
|
738
|
+
else
|
|
739
|
+
hasUnqualifiedColumn = true;
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (n.type === 'subquery' || n.type === 'select' || n.type === 'exists') {
|
|
743
|
+
hasSubquery = true;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
for (const v of Object.values(n))
|
|
747
|
+
walk(v);
|
|
748
|
+
};
|
|
749
|
+
walk(expr);
|
|
750
|
+
return { qualifiers, hasUnqualifiedColumn, hasSubquery };
|
|
751
|
+
}
|
|
752
|
+
/** `update <member> set <cols> where <memberKey> in (select <anchorKey> from <anchor> where <pred>)`. */
|
|
753
|
+
function memberUpdateOp(ctx, view, shape, member, assignments, pred, stmt) {
|
|
754
|
+
const memberKey = singleKeyColumn(view, shape, member);
|
|
755
|
+
const where = {
|
|
756
|
+
type: 'in',
|
|
757
|
+
expr: { type: 'column', name: memberKey },
|
|
758
|
+
subquery: anchorKeySubquery(shape, pred),
|
|
759
|
+
};
|
|
760
|
+
const statement = {
|
|
761
|
+
type: 'update',
|
|
762
|
+
table: memberIdentifier(member),
|
|
763
|
+
assignments: assignments.map(a => ({ column: a.column, value: a.value })),
|
|
764
|
+
where,
|
|
765
|
+
contextValues: stmt.contextValues,
|
|
766
|
+
schemaPath: stmt.schemaPath,
|
|
767
|
+
loc: stmt.loc,
|
|
768
|
+
};
|
|
769
|
+
return { table: resolveMemberTable(ctx, member), op: 'update', statement };
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Rewrite an assigned value from logical terms into the owning member's base
|
|
773
|
+
* terms, then strip the member's own alias qualifier (the per-member UPDATE
|
|
774
|
+
* targets that table directly). A reference to a *different* member is a
|
|
775
|
+
* cross-source assignment a single-table SET cannot express — rejected.
|
|
776
|
+
*/
|
|
777
|
+
function rewriteAssignedValue(view, shape, owner, value) {
|
|
778
|
+
const base = substituteViewColumns(value, shape, view);
|
|
779
|
+
return transformExpr(base, (col) => {
|
|
780
|
+
if (!col.table)
|
|
781
|
+
return undefined;
|
|
782
|
+
if (col.table === owner.relationId)
|
|
783
|
+
return { type: 'column', name: col.name };
|
|
784
|
+
raiseMutationDiagnostic({
|
|
785
|
+
reason: 'cross-source-assignment',
|
|
786
|
+
column: col.name,
|
|
787
|
+
table: view.name,
|
|
788
|
+
message: `cannot update logical table '${view.name}': an update value references column '${col.name}' on decomposition member '${col.table}', a different member than the column it assigns; cross-member assignment is not supported`,
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
// --- UPDATE materialization (optional columnar / EAV) ---------------------
|
|
793
|
+
/**
|
|
794
|
+
* Emit the per-row base ops for an optional (outer-joined) columnar member's update
|
|
795
|
+
* group, routed by the group's {@link ValueKind} composition (see the table in the file
|
|
796
|
+
* header):
|
|
797
|
+
*
|
|
798
|
+
* - **has a `captured` cell, OR mixes `anchor` and `self`** → the whole group rides the
|
|
799
|
+
* single-identity per-row capture ({@link emitCapturedMemberUpdate}): EVERY cell's value
|
|
800
|
+
* (including any anchor/self/constant sibling) is materialized once over the planned body
|
|
801
|
+
* under a `srcN`, the matched UPDATE reads each back by the member key, and a filtered
|
|
802
|
+
* materialize INSERT reads each back by the anchor key (gated on a runtime non-null). This
|
|
803
|
+
* subsumes the retired same-statement anchor+self reject — a mixed value, like any arbitrary
|
|
804
|
+
* one, is now a captured value. Requires the capture carrier (the build path); absent it the
|
|
805
|
+
* defensive legacy reject fires.
|
|
806
|
+
* - **has an `anchor` cell** (no `self`/`captured`) → a single **upsert** unifies the matched
|
|
807
|
+
* UPDATE and the absent materialize INSERT: the value is computed once over the anchor scan and
|
|
808
|
+
* both branches read it (insert directly, matched via `excluded.<col>`). Constant cells
|
|
809
|
+
* fold in as literal projections / `do update set col = excluded.col`.
|
|
810
|
+
* - **has a `self` cell** (no `anchor`) → present rows take the matched UPDATE (their real
|
|
811
|
+
* prior member value, owner qualifier stripped); absent rows take a materialize INSERT
|
|
812
|
+
* ({@link buildSelfMaterializeInsertSelect}) that evaluates the self-expression with the
|
|
813
|
+
* owner's own columns substituted to NULL — an absent row's prior value is null. A runtime
|
|
814
|
+
* non-empty filter gates it: a **null-propagating** self-expression (`c + 1` → null) is
|
|
815
|
+
* constant-false and materializes no phantom row, while one that maps null → non-null
|
|
816
|
+
* (`coalesce(c, 0) + 1`) is constant-true and materializes the new value. The two ops stay
|
|
817
|
+
* distinct (they **cannot** collapse into an upsert: the matched value is computed over the
|
|
818
|
+
* member scan, the materialize value over the null-substituted anchor scan — they disagree
|
|
819
|
+
* row-for-row by construction), and `on conflict (<memberKey>) do nothing` cedes matched
|
|
820
|
+
* rows to the UPDATE. Constant cells ride along in both branches (the matched UPDATE applies
|
|
821
|
+
* them; the materialize INSERT projects them, and a non-null constant makes the filter true).
|
|
822
|
+
* - **all `constant`** → the legacy fast lane: all-value-columns-null → base DELETE; else the
|
|
823
|
+
* matched UPDATE plus, when ≥1 value is non-null, the absent materialize INSERT.
|
|
824
|
+
*/
|
|
825
|
+
function emitOptionalMemberUpdate(ctx, view, shape, member, cells, pred, stmt, ops, registerCapturedExpr) {
|
|
826
|
+
const hasCaptured = cells.some(c => c.kind === 'captured');
|
|
827
|
+
const hasAnchor = cells.some(c => c.kind === 'anchor');
|
|
828
|
+
const hasSelf = cells.some(c => c.kind === 'self');
|
|
829
|
+
// A group with ≥1 arbitrary (`captured`) cell — or a mixed anchor+self group, which no
|
|
830
|
+
// single-branch fast lane expresses — rides the single-identity per-row capture: every cell's
|
|
831
|
+
// value is materialized once over the planned body and read back by the stitch key in both
|
|
832
|
+
// branches. A `captured` cell can only exist when the carrier is present (the classifier gated
|
|
833
|
+
// it), so the guard only ever fires for the legacy carrier-absent anchor+self case.
|
|
834
|
+
if (hasCaptured || (hasAnchor && hasSelf)) {
|
|
835
|
+
if (!registerCapturedExpr) {
|
|
836
|
+
raiseMutationDiagnostic({
|
|
837
|
+
reason: 'unsupported-decomposition-update',
|
|
838
|
+
table: view.name,
|
|
839
|
+
message: `cannot update logical table '${view.name}': the update of optional member '${member.relationId}' mixes an anchor-resolvable value and a member self-reference in one statement; threading both across the matched-update and materialize branches needs the per-row capture carrier, which the legacy non-build path does not supply`,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
emitCapturedMemberUpdate(ctx, view, shape, member, cells, pred, stmt, ops, registerCapturedExpr);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (hasAnchor) {
|
|
846
|
+
// Anchor-resolvable group → one upsert replaces both the matched UPDATE and the
|
|
847
|
+
// materialize INSERT (the value agrees row-for-row across both branches by construction).
|
|
848
|
+
ops.push(buildOptionalMemberInsertSelect(ctx, view, shape, member, cells, pred, stmt, 'update'));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (hasSelf) {
|
|
852
|
+
// Self-reference group → the matched UPDATE for present rows over the owner-qualifier-stripped
|
|
853
|
+
// values (their real prior member value), plus — only when the materialize is statically
|
|
854
|
+
// **live** — a materialize INSERT for absent rows that evaluates the self-expression with the
|
|
855
|
+
// owner's own columns substituted to NULL (an absent row's prior value is null), gated by a
|
|
856
|
+
// runtime non-empty filter: a null-propagating self-expression (`c + 1` → null) materializes
|
|
857
|
+
// nothing, while a null→non-null one (`coalesce(c, 0) + 1`) does.
|
|
858
|
+
//
|
|
859
|
+
// When that null-substituted non-empty filter folds **constant-false** at plan time (every
|
|
860
|
+
// self cell null-propagates and no non-null constant sibling keeps it alive), no absent row
|
|
861
|
+
// can ever materialize — so we emit ONLY the matched UPDATE (present-rows-only) and skip the
|
|
862
|
+
// INSERT. Because both soundness gates live inside {@link buildSelfMaterializeInsertSelect},
|
|
863
|
+
// skipping the call skips them with it: sound, since a gate is only a plan-time proxy for "a
|
|
864
|
+
// materialized row would violate", and no row materializes. A non-foldable / volatile /
|
|
865
|
+
// parameterized value cannot be proven dead, so it stays live (emit + gate) — conservative,
|
|
866
|
+
// matching the always-emit behavior. The UPDATE must precede the INSERT so the matched rows
|
|
867
|
+
// are settled before the `do nothing` materialize cedes them (see the doc above for why these
|
|
868
|
+
// cannot collapse into a single upsert).
|
|
869
|
+
ops.push(memberUpdateOp(ctx, view, shape, member, cells.map(c => ({ column: c.basisColumn, value: stripMemberQualifier(c.value, member) })), pred, stmt));
|
|
870
|
+
if (!foldsConstantFalse(ctx, selfMaterializeNonEmptyFilter(cells, member))) {
|
|
871
|
+
ops.push(buildSelfMaterializeInsertSelect(ctx, view, shape, member, cells, pred, stmt));
|
|
872
|
+
}
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
// Pure-constant group: the legacy fast lane.
|
|
876
|
+
const valueBasisCols = optionalValueColumns(view, shape, member);
|
|
877
|
+
const assignedBasis = new Set(cells.map(c => c.basisColumn.toLowerCase()));
|
|
878
|
+
const allValueColsAssigned = valueBasisCols.every(bc => assignedBasis.has(bc.toLowerCase()));
|
|
879
|
+
const allAssignedNull = cells.every(c => c.isNull);
|
|
880
|
+
if (allAssignedNull && allValueColsAssigned) {
|
|
881
|
+
// The component row is emptied across every value column → delete it. Reuse the
|
|
882
|
+
// member-delete builder via a metadata-only synthetic DeleteStmt (it rebuilds the
|
|
883
|
+
// `<memberKey> in (<anchor subquery>)` predicate, and a no-WHERE update truncates
|
|
884
|
+
// the member — correct, since every logical row's value columns become null).
|
|
885
|
+
const asDelete = {
|
|
886
|
+
type: 'delete',
|
|
887
|
+
table: memberIdentifier(member),
|
|
888
|
+
contextValues: stmt.contextValues,
|
|
889
|
+
schemaPath: stmt.schemaPath,
|
|
890
|
+
loc: stmt.loc,
|
|
891
|
+
};
|
|
892
|
+
ops.push(memberDeleteOp(ctx, view, shape, member, pred, asDelete));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
// Matched rows: the legacy member UPDATE over the assigned (constant) values.
|
|
896
|
+
ops.push(memberUpdateOp(ctx, view, shape, member, cells.map(c => ({ column: c.basisColumn, value: cloneExpr(c.value) })), pred, stmt));
|
|
897
|
+
// Absent rows: materialize only when at least one assigned value is non-null (an
|
|
898
|
+
// all-null partial assignment leaves the absent row absent — nothing to create).
|
|
899
|
+
if (cells.some(c => !c.isNull)) {
|
|
900
|
+
ops.push(buildOptionalMemberInsertSelect(ctx, view, shape, member, cells, pred, stmt, 'nothing'));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Emit the captured two-op path for an optional columnar member group that contains an
|
|
905
|
+
* arbitrary (`captured`) cell — or that mixes an anchor and a self cell (which no single-branch
|
|
906
|
+
* fast lane expresses). EVERY cell's already-lowered base-term value is projected into the
|
|
907
|
+
* up-front `__vmupd_keys` capture under a stable `srcN` alias (a `captured` cell's arbitrary
|
|
908
|
+
* value, but also any anchor/self/constant sibling — uniform, harmless: the capture over the
|
|
909
|
+
* planned body null-extends an absent optional member, so a self/cross-member value over an
|
|
910
|
+
* absent row already captures null). Then:
|
|
911
|
+
*
|
|
912
|
+
* - the **matched UPDATE** (unfiltered over the present rows) sets each value from the capture,
|
|
913
|
+
* read back by the **member key** (`(select srcN from __vmupd_keys k where k.k0_0 = <memberKey>)`),
|
|
914
|
+
* so a present row gets its real pre-mutation value; a captured-null on a matched row writes the
|
|
915
|
+
* column null (a benign physical divergence — it reads identically to absent, never a widen).
|
|
916
|
+
* - the **materialize INSERT** ({@link buildCapturedMaterializeInsert}) over the absent rows reads
|
|
917
|
+
* each value back by the **anchor key**, gated on ≥1 captured read-back being non-null at runtime
|
|
918
|
+
* (so a self/cross-member value that captured null springs no phantom row) with
|
|
919
|
+
* `on conflict (<memberKey>) do nothing` to cede matched rows to the UPDATE.
|
|
920
|
+
*
|
|
921
|
+
* The two ops cannot collapse into an upsert: the filter must suppress the absent branch without
|
|
922
|
+
* suppressing the matched write. The UPDATE precedes the INSERT so the matched rows are settled
|
|
923
|
+
* before the `do nothing` materialize cedes them. The capture is materialized pre-mutation
|
|
924
|
+
* (regardless of base-op order), so a both-sides write (`set c = b + 1, b = b + 100`) reads `c`
|
|
925
|
+
* from the pre-mutation `b`. Reuses {@link assertNoUnassignedValueColumnWiden} (raised here, before
|
|
926
|
+
* either op, so a widen reject stays atomic) + {@link assertNoMissingNotNull} (in the builder).
|
|
927
|
+
*/
|
|
928
|
+
function emitCapturedMemberUpdate(ctx, view, shape, member, cells, pred, stmt, ops, registerCapturedExpr) {
|
|
929
|
+
const ref = resolveMemberTable(ctx, member);
|
|
930
|
+
const schema = ref.tableSchema;
|
|
931
|
+
const memberKey = singleKeyColumn(view, shape, member);
|
|
932
|
+
// View-soundness gate (atomic, before either op): an unassigned value column that would not
|
|
933
|
+
// land null cannot be materialized. Shared with the constant / self materialize builders.
|
|
934
|
+
assertNoUnassignedValueColumnWiden(view, shape, member, schema, cells);
|
|
935
|
+
// Register every cell's lowered value into the capture (one `srcN` per cell), keyed by its
|
|
936
|
+
// basis column so the two read-back sites (member key, anchor key) bind the same alias.
|
|
937
|
+
const srcByCell = cells.map(c => registerCapturedExpr(`cap:${member.relationId.toLowerCase()}:${c.basisColumn.toLowerCase()}`, cloneExpr(c.value)));
|
|
938
|
+
// Matched UPDATE (unfiltered over the present rows): each value read back by the member key.
|
|
939
|
+
ops.push(memberUpdateOp(ctx, view, shape, member, cells.map((c, i) => ({ column: c.basisColumn, value: capturedValueSubquery(srcByCell[i], 0, [memberKey]) })), pred, stmt));
|
|
940
|
+
// Materialize INSERT (filtered over the absent rows): each value read back by the anchor key.
|
|
941
|
+
ops.push(buildCapturedMaterializeInsert(ctx, view, shape, member, cells, srcByCell, pred, stmt));
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Build the absent-row materialize INSERT for a captured optional-member group, modeled on
|
|
945
|
+
* {@link buildSelfMaterializeInsertSelect} (`'nothing'` flavour) but sourcing each value from the
|
|
946
|
+
* up-front `__vmupd_keys` capture rather than a null-substituted self-expression. The select is
|
|
947
|
+
* over the anchor; each projected value is `capturedValueSubquery(srcN, 0, [anchorKey])` — the
|
|
948
|
+
* captured read-back correlated by the anchor key (which holds the shared stitch-key value). The
|
|
949
|
+
* WHERE conjoins the user predicate with an OR-chain `<each captured read-back> is not null`, so a
|
|
950
|
+
* row whose every captured value is null (a self/cross-member value over an absent member, or a
|
|
951
|
+
* subquery returning null) materializes nothing — the **runtime** analogue of the self path's
|
|
952
|
+
* plan-time `foldsConstantFalse`, since a captured value is data-dependent and cannot be folded.
|
|
953
|
+
* `on conflict (<memberKey>) do nothing` cedes the matched rows to the UPDATE (emitted first).
|
|
954
|
+
* Always emitted (the filter decides per row). Runs {@link assertNoMissingNotNull} on the target
|
|
955
|
+
* columns (the widen gate already fired in the caller).
|
|
956
|
+
*/
|
|
957
|
+
function buildCapturedMaterializeInsert(ctx, view, shape, member, cells, srcByCell, pred, stmt) {
|
|
958
|
+
const ref = resolveMemberTable(ctx, member);
|
|
959
|
+
const schema = ref.tableSchema;
|
|
960
|
+
const anchorKey = singleKeyColumn(view, shape, shape.anchor);
|
|
961
|
+
const memberKey = singleKeyColumn(view, shape, member);
|
|
962
|
+
const targetColumns = [memberKey];
|
|
963
|
+
const projections = [
|
|
964
|
+
{ type: 'column', expr: { type: 'column', name: anchorKey, table: shape.anchor.relationId } },
|
|
965
|
+
];
|
|
966
|
+
cells.forEach((c, i) => {
|
|
967
|
+
targetColumns.push(c.basisColumn);
|
|
968
|
+
projections.push({ type: 'column', expr: capturedValueSubquery(srcByCell[i], 0, [anchorKey]) });
|
|
969
|
+
});
|
|
970
|
+
assertNoMissingNotNull(view, schema, targetColumns.map((baseColumn) => ({ baseColumn })));
|
|
971
|
+
// Non-empty image filter: OR over each captured value (read back by the anchor key) being
|
|
972
|
+
// non-null. A fresh `capturedValueSubquery` per disjunct (distinct AST nodes from the
|
|
973
|
+
// projections') keeps the filter self-contained.
|
|
974
|
+
const nonEmpty = cells
|
|
975
|
+
.map((_, i) => ({ type: 'unary', operator: 'IS NOT NULL', expr: capturedValueSubquery(srcByCell[i], 0, [anchorKey]) }))
|
|
976
|
+
.reduce((acc, e) => acc ? { type: 'binary', operator: 'OR', left: acc, right: e } : e);
|
|
977
|
+
const where = combineAnd(pred ? cloneExpr(pred) : undefined, nonEmpty);
|
|
978
|
+
const select = {
|
|
979
|
+
type: 'select',
|
|
980
|
+
columns: projections,
|
|
981
|
+
from: [{ ...memberIdentifierSource(shape.anchor), alias: shape.anchor.relationId }],
|
|
982
|
+
where,
|
|
983
|
+
};
|
|
984
|
+
const statement = {
|
|
985
|
+
type: 'insert',
|
|
986
|
+
table: memberIdentifier(member),
|
|
987
|
+
columns: targetColumns,
|
|
988
|
+
source: select,
|
|
989
|
+
upsertClauses: [{ type: 'upsert', conflictTarget: [memberKey], action: 'nothing' }],
|
|
990
|
+
contextValues: stmt.contextValues,
|
|
991
|
+
schemaPath: stmt.schemaPath,
|
|
992
|
+
loc: stmt.loc,
|
|
993
|
+
};
|
|
994
|
+
return { table: ref, op: 'insert', statement };
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Build the single-identity (anchor-key) per-row value capture for a decomposition columnar
|
|
998
|
+
* UPDATE — the decomposition dual of `buildMultiSourceKeyCapture` (multi-source.ts), reusing the
|
|
999
|
+
* shared `__vmupd_keys` substrate ({@link MultiSourceKeyCapture} / `makeMultiSourceKeyRef` /
|
|
1000
|
+
* `keyColumnName`). Built as plan nodes directly over the **already-planned get body**
|
|
1001
|
+
* (`shape.bodySource` + `shape.bodyScope`) — `Project_{k0_0, srcN…}(Filter_{anchorPred}(body))` —
|
|
1002
|
+
* so the body is planned once and the captured value already encodes per-row presence (the body
|
|
1003
|
+
* null-extends an absent optional member).
|
|
1004
|
+
*
|
|
1005
|
+
* - `k0_0` (the only identity column) := the anchor key, read back by every base op (by the
|
|
1006
|
+
* member key in the matched UPDATE, the anchor key in the materialize INSERT — both correlate
|
|
1007
|
+
* `k.k0_0`, which holds the shared stitch-key value, since each member is keyed 1:1 to the
|
|
1008
|
+
* anchor).
|
|
1009
|
+
* - one `srcN` per {@link CapturedDecompValue}, in registration (= read-back) order, so the
|
|
1010
|
+
* flattened `keyColumns` align positionally with the projections every reader scans back.
|
|
1011
|
+
*
|
|
1012
|
+
* The identity predicate is the same anchor-resolvable lowering the base ops route on
|
|
1013
|
+
* ({@link anchorPredicate}, re-validated here — idempotent); a non-anchor WHERE has already
|
|
1014
|
+
* thrown in `decomposeUpdate` before any capture is requested (atomic).
|
|
1015
|
+
*/
|
|
1016
|
+
export function buildDecompositionKeyCapture(ctx, view, shape, where, capturedValues) {
|
|
1017
|
+
const scope = shape.bodyScope;
|
|
1018
|
+
if (!scope) {
|
|
1019
|
+
raiseMutationDiagnostic({
|
|
1020
|
+
reason: 'no-base-lineage',
|
|
1021
|
+
table: view.name,
|
|
1022
|
+
message: `cannot write through logical table '${view.name}': the planned decomposition body did not expose a resolvable column scope for the value capture`,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const anchorKey = singleKeyColumn(view, shape, shape.anchor);
|
|
1026
|
+
// The identifying predicate (anchor-resolvable user WHERE → base terms), built as a
|
|
1027
|
+
// ScalarPlanNode over the body scope, then a Filter over the planned body source.
|
|
1028
|
+
const predAst = anchorPredicate(view, shape, where);
|
|
1029
|
+
const predicate = predAst ? buildExpression({ ...ctx, scope }, predAst) : undefined;
|
|
1030
|
+
const filtered = predicate
|
|
1031
|
+
? new FilterNode(scope, shape.bodySource, predicate)
|
|
1032
|
+
: shape.bodySource;
|
|
1033
|
+
// k0_0 := the anchor key, then one srcN per captured value (lowered over the body scope).
|
|
1034
|
+
const keyColumns = [];
|
|
1035
|
+
const projections = [];
|
|
1036
|
+
const anchorKeyNode = buildExpression({ ...ctx, scope }, { type: 'column', name: anchorKey, table: shape.anchor.relationId });
|
|
1037
|
+
keyColumns.push({ name: keyColumnName(0, 0), type: anchorKeyNode.getType() });
|
|
1038
|
+
projections.push({ node: anchorKeyNode, alias: keyColumnName(0, 0) });
|
|
1039
|
+
for (const cv of capturedValues) {
|
|
1040
|
+
const node = buildExpression({ ...ctx, scope }, cv.expr);
|
|
1041
|
+
keyColumns.push({ name: cv.alias, type: node.getType() });
|
|
1042
|
+
projections.push({ node, alias: cv.alias });
|
|
1043
|
+
}
|
|
1044
|
+
const source = new ProjectNode(scope, filtered, projections, undefined, undefined, false);
|
|
1045
|
+
return { source, descriptor: {}, keyColumns };
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Build the anchor-keyed insert-select that materializes / unifies an optional columnar
|
|
1049
|
+
* member's update, in one of two `action` flavours sharing the identical select + soundness
|
|
1050
|
+
* gates:
|
|
1051
|
+
*
|
|
1052
|
+
* - **`'nothing'`** (constant group, absent branch) — `insert into <member> (<memberKey>,
|
|
1053
|
+
* <cols…>) select <anchorKey>, <values…> from <anchor> where <pred> on conflict (<memberKey>)
|
|
1054
|
+
* do nothing`. The matched anchors (whose member row exists) conflict and are ceded to the
|
|
1055
|
+
* separate matched UPDATE; only the absent rows materialize. The insert source never scans
|
|
1056
|
+
* its own target (which the planner cannot assign an access path to).
|
|
1057
|
+
* - **`'update'`** (anchor-resolvable group) — the same select, but `on conflict (<memberKey>)
|
|
1058
|
+
* do update set <col> = excluded.<col>, …`. This **replaces both** the matched UPDATE and the
|
|
1059
|
+
* materialize INSERT: the value is computed once over the anchor scan, absent rows insert it,
|
|
1060
|
+
* and matched rows read the identical proposed-insert value via `excluded.<col>` — so the two
|
|
1061
|
+
* branches agree row-for-row (the round-trip oracle holds by construction).
|
|
1062
|
+
*
|
|
1063
|
+
* The conflict target is the member's stitch key, which `validatePrimaryAdvertisement`
|
|
1064
|
+
* (lens-compiler.ts) guarantees at deploy time to be a declared PRIMARY KEY / non-partial
|
|
1065
|
+
* UNIQUE on the member basis. That deploy guard is what makes the partition sound: the runtime
|
|
1066
|
+
* `on conflict` fires only on a declared PK/UNIQUE violation, so a non-unique stitch key would
|
|
1067
|
+
* double-insert the matched rows instead of ceding/updating them — and could not deploy.
|
|
1068
|
+
*
|
|
1069
|
+
* Two soundness gates (data-independent, plan-time), enforced on **both** flavours:
|
|
1070
|
+
* - a member **value** column the statement does not assign must materialize as null
|
|
1071
|
+
* (nullable + no declared default), else its base default would silently widen the
|
|
1072
|
+
* absent row's logical image — rejected `unsupported-decomposition-update`;
|
|
1073
|
+
* - a NOT NULL base column with no default that no value covers cannot be created —
|
|
1074
|
+
* rejected via {@link assertNoMissingNotNull} (the decomposition create-conflict).
|
|
1075
|
+
*/
|
|
1076
|
+
function buildOptionalMemberInsertSelect(ctx, view, shape, member, cells, pred, stmt, action) {
|
|
1077
|
+
const ref = resolveMemberTable(ctx, member);
|
|
1078
|
+
const schema = ref.tableSchema;
|
|
1079
|
+
const anchorKey = singleKeyColumn(view, shape, shape.anchor);
|
|
1080
|
+
const memberKey = singleKeyColumn(view, shape, member);
|
|
1081
|
+
assertNoUnassignedValueColumnWiden(view, shape, member, schema, cells);
|
|
1082
|
+
const targetColumns = [memberKey];
|
|
1083
|
+
const projections = [
|
|
1084
|
+
{ type: 'column', expr: { type: 'column', name: anchorKey, table: shape.anchor.relationId } },
|
|
1085
|
+
];
|
|
1086
|
+
for (const c of cells) {
|
|
1087
|
+
targetColumns.push(c.basisColumn);
|
|
1088
|
+
projections.push({ type: 'column', expr: cloneExpr(c.value) });
|
|
1089
|
+
}
|
|
1090
|
+
assertNoMissingNotNull(view, schema, targetColumns.map((baseColumn) => ({ baseColumn })));
|
|
1091
|
+
const select = {
|
|
1092
|
+
type: 'select',
|
|
1093
|
+
columns: projections,
|
|
1094
|
+
from: [{ ...memberIdentifierSource(shape.anchor), alias: shape.anchor.relationId }],
|
|
1095
|
+
where: pred ? cloneExpr(pred) : undefined,
|
|
1096
|
+
};
|
|
1097
|
+
const upsert = action === 'nothing'
|
|
1098
|
+
? { type: 'upsert', conflictTarget: [memberKey], action: 'nothing' }
|
|
1099
|
+
: { type: 'upsert', conflictTarget: [memberKey], action: 'update', assignments: cells.map(c => ({ column: c.basisColumn, value: excludedColumn(c.basisColumn) })) };
|
|
1100
|
+
const statement = {
|
|
1101
|
+
type: 'insert',
|
|
1102
|
+
table: memberIdentifier(member),
|
|
1103
|
+
columns: targetColumns,
|
|
1104
|
+
source: select,
|
|
1105
|
+
upsertClauses: [upsert],
|
|
1106
|
+
contextValues: stmt.contextValues,
|
|
1107
|
+
schemaPath: stmt.schemaPath,
|
|
1108
|
+
loc: stmt.loc,
|
|
1109
|
+
};
|
|
1110
|
+
return { table: ref, op: 'insert', statement };
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Build the absent-row materialize INSERT for a **member self-reference** update group
|
|
1114
|
+
* (`set c = c + 1`, `set c = coalesce(c, 0) + 1`). Modeled on {@link buildOptionalMemberInsertSelect}
|
|
1115
|
+
* (`'nothing'` flavour) and sharing its two soundness gates, but the projected value is the
|
|
1116
|
+
* self-expression with the owner's own column refs **substituted to NULL** — an absent row's
|
|
1117
|
+
* prior member value is null ({@link substituteOwnerColumnsWithNull}). Every leaf of a `self`
|
|
1118
|
+
* cell is the owner's own column (the classifier proved it), so after substitution the value is a
|
|
1119
|
+
* constant expression evaluable over the anchor scan; a constant sibling cell passes through.
|
|
1120
|
+
*
|
|
1121
|
+
* The select is additionally **filtered to a non-empty materialized image**:
|
|
1122
|
+
* `… where <pred> and (<v1> is not null or <v2> is not null or …)` over the null-substituted
|
|
1123
|
+
* values. This is what makes the always-emit sound: a **null-propagating** self-expression
|
|
1124
|
+
* (`c + 1` → `null + 1` → null) yields a constant-false filter and materializes **no** phantom
|
|
1125
|
+
* row, while one that maps null → non-null (`coalesce(c, 0) + 1` → `1`) is constant-true and
|
|
1126
|
+
* materializes. `on conflict (<memberKey>) do nothing` cedes present rows to the matched UPDATE
|
|
1127
|
+
* (which runs first), so only genuinely-absent rows are created — never an upsert (the matched and
|
|
1128
|
+
* materialize values are computed over different scans and disagree row-for-row by construction).
|
|
1129
|
+
*
|
|
1130
|
+
* The caller emits this builder (and therefore runs the two soundness gates) **only when the
|
|
1131
|
+
* materialize is statically live** — when the null-substituted non-empty filter cannot be proven
|
|
1132
|
+
* constant-false at plan time ({@link foldsConstantFalse}). A provably-dead materialize is skipped
|
|
1133
|
+
* entirely, taking both gates with it (sound: no row materializes, so neither gate can be violated).
|
|
1134
|
+
*/
|
|
1135
|
+
function buildSelfMaterializeInsertSelect(ctx, view, shape, member, cells, pred, stmt) {
|
|
1136
|
+
const ref = resolveMemberTable(ctx, member);
|
|
1137
|
+
const schema = ref.tableSchema;
|
|
1138
|
+
const anchorKey = singleKeyColumn(view, shape, shape.anchor);
|
|
1139
|
+
const memberKey = singleKeyColumn(view, shape, member);
|
|
1140
|
+
assertNoUnassignedValueColumnWiden(view, shape, member, schema, cells);
|
|
1141
|
+
// An absent row's prior member value is null, so the materialized image is the self-expression
|
|
1142
|
+
// with the owner's own columns nulled out (a self cell collapses to a constant; a constant cell
|
|
1143
|
+
// is unchanged).
|
|
1144
|
+
const nulled = cells.map(c => ({ basisColumn: c.basisColumn, value: substituteOwnerColumnsWithNull(c.value, member) }));
|
|
1145
|
+
const targetColumns = [memberKey];
|
|
1146
|
+
const projections = [
|
|
1147
|
+
{ type: 'column', expr: { type: 'column', name: anchorKey, table: shape.anchor.relationId } },
|
|
1148
|
+
];
|
|
1149
|
+
for (const n of nulled) {
|
|
1150
|
+
targetColumns.push(n.basisColumn);
|
|
1151
|
+
projections.push({ type: 'column', expr: cloneExpr(n.value) });
|
|
1152
|
+
}
|
|
1153
|
+
assertNoMissingNotNull(view, schema, targetColumns.map((baseColumn) => ({ baseColumn })));
|
|
1154
|
+
// Non-empty image filter (the shared {@link selfMaterializeNonEmptyFilter} OR-chain
|
|
1155
|
+
// `<nulledValue> is not null`) so a null-propagating self-expression creates no phantom row
|
|
1156
|
+
// (constant-false), conjoined with the user predicate. The identical filter, folded WITHOUT the
|
|
1157
|
+
// user predicate, is the caller's static dead-materialize check ({@link foldsConstantFalse}); the
|
|
1158
|
+
// shared helper keeps the two from drifting.
|
|
1159
|
+
const where = combineAnd(pred ? cloneExpr(pred) : undefined, selfMaterializeNonEmptyFilter(cells, member));
|
|
1160
|
+
const select = {
|
|
1161
|
+
type: 'select',
|
|
1162
|
+
columns: projections,
|
|
1163
|
+
from: [{ ...memberIdentifierSource(shape.anchor), alias: shape.anchor.relationId }],
|
|
1164
|
+
where,
|
|
1165
|
+
};
|
|
1166
|
+
const statement = {
|
|
1167
|
+
type: 'insert',
|
|
1168
|
+
table: memberIdentifier(member),
|
|
1169
|
+
columns: targetColumns,
|
|
1170
|
+
source: select,
|
|
1171
|
+
upsertClauses: [{ type: 'upsert', conflictTarget: [memberKey], action: 'nothing' }],
|
|
1172
|
+
contextValues: stmt.contextValues,
|
|
1173
|
+
schemaPath: stmt.schemaPath,
|
|
1174
|
+
loc: stmt.loc,
|
|
1175
|
+
};
|
|
1176
|
+
return { table: ref, op: 'insert', statement };
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* The self-materialize non-empty image filter: the OR-chain `<nulledValue> is not null` over each
|
|
1180
|
+
* cell's value with the owner's own columns substituted to NULL ({@link substituteOwnerColumnsWithNull}).
|
|
1181
|
+
* Shared by {@link buildSelfMaterializeInsertSelect} (where it is conjoined with the user predicate as
|
|
1182
|
+
* the emitted INSERT's WHERE) and the `hasSelf` branch's static dead-materialize check (folded on its
|
|
1183
|
+
* own, {@link foldsConstantFalse}) so the two can never drift. `is not null` is total, so for a
|
|
1184
|
+
* null-propagating self group with no non-null constant sibling every disjunct is `null is not null`
|
|
1185
|
+
* → the whole chain folds constant-false.
|
|
1186
|
+
*/
|
|
1187
|
+
function selfMaterializeNonEmptyFilter(cells, member) {
|
|
1188
|
+
return cells
|
|
1189
|
+
.map((c) => ({ type: 'unary', operator: 'IS NOT NULL', expr: substituteOwnerColumnsWithNull(c.value, member) }))
|
|
1190
|
+
.reduce((acc, e) => acc ? { type: 'binary', operator: 'OR', left: acc, right: e } : e);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* True when `expr` provably folds **constant-false** at plan time — used by the `hasSelf` branch to
|
|
1194
|
+
* decide whether the self-materialize is dead (no absent row can materialize). We reuse the engine's
|
|
1195
|
+
* own constant folding rather than hand-rolling an evaluator: build the expression to a scalar plan
|
|
1196
|
+
* node ({@link buildExpression}) and run it through {@link createRuntimeExpressionEvaluator}. The
|
|
1197
|
+
* input is the {@link selfMaterializeNonEmptyFilter} OR-chain (column-ref-free after null
|
|
1198
|
+
* substitution — every self leaf is the owner's own column, a constant sibling carries none), so the
|
|
1199
|
+
* fold is a deterministic plan-time constant. `is not null` never yields NULL, so a dead materialize
|
|
1200
|
+
* folds to boolean `false` (defensively also `0` / `0n`). Anything else — a non-null fold, a Promise
|
|
1201
|
+
* (async / non-constant), or a throw (unbound parameter) — is NOT provably dead, so we stay
|
|
1202
|
+
* conservative and return `false` (emit the materialize + its gates).
|
|
1203
|
+
*
|
|
1204
|
+
* A **non-deterministic** leaf (a function the registry flags non-`DETERMINISTIC` — `random()` or a
|
|
1205
|
+
* volatile UDF — or a subquery, though a self cell carries none) short-circuits to `false` *before*
|
|
1206
|
+
* the fold: a single plan-time evaluation is an unsound proxy for the per-row runtime filter when the
|
|
1207
|
+
* value can vary, and a nullable volatile inside `coalesce(c, …)` could fold NULL (dead) at plan time
|
|
1208
|
+
* yet yield non-null per row at runtime — dropping an absent row the always-emit path would have
|
|
1209
|
+
* materialized. Keeping volatiles on the emit path matches the ticket's `set c = c + random()`
|
|
1210
|
+
* edge-case and the `hasSelf` branch's own contract (only a *foldable* dead materialize is skipped).
|
|
1211
|
+
*/
|
|
1212
|
+
function foldsConstantFalse(ctx, expr) {
|
|
1213
|
+
const isDeterministic = (name, argc) => {
|
|
1214
|
+
const fn = ctx.schemaManager.findFunction(name, argc) ?? ctx.schemaManager.findFunction(name, -1);
|
|
1215
|
+
return fn ? (fn.flags & FunctionFlags.DETERMINISTIC) !== 0 : true;
|
|
1216
|
+
};
|
|
1217
|
+
if (containsNonDeterministicCall(expr, isDeterministic))
|
|
1218
|
+
return false;
|
|
1219
|
+
try {
|
|
1220
|
+
const node = buildExpression(ctx, expr);
|
|
1221
|
+
const value = createRuntimeExpressionEvaluator(ctx.db)(node);
|
|
1222
|
+
return value === false || value === 0 || value === 0n;
|
|
1223
|
+
}
|
|
1224
|
+
catch {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* View-soundness gate shared by every optional-member materialize insert-select
|
|
1230
|
+
* ({@link buildOptionalMemberInsertSelect} and {@link buildSelfMaterializeInsertSelect}): an
|
|
1231
|
+
* unassigned member **value** column that would not land null (it is NOT NULL, or carries a
|
|
1232
|
+
* declared default) cannot be materialized without changing the absent row's logical image.
|
|
1233
|
+
* Reject conservatively rather than silently widen the view. (The companion gate —
|
|
1234
|
+
* {@link assertNoMissingNotNull} — is called at each builder's target-column site.)
|
|
1235
|
+
*/
|
|
1236
|
+
function assertNoUnassignedValueColumnWiden(view, shape, member, schema, cells) {
|
|
1237
|
+
const assignedBasis = new Set(cells.map(c => c.basisColumn.toLowerCase()));
|
|
1238
|
+
for (const bc of optionalValueColumns(view, shape, member)) {
|
|
1239
|
+
if (assignedBasis.has(bc.toLowerCase()))
|
|
1240
|
+
continue;
|
|
1241
|
+
const col = columnByName(view, schema, bc);
|
|
1242
|
+
if (col.notNull || col.defaultValue !== null) {
|
|
1243
|
+
raiseMutationDiagnostic({
|
|
1244
|
+
reason: 'unsupported-decomposition-update',
|
|
1245
|
+
column: bc,
|
|
1246
|
+
table: view.name,
|
|
1247
|
+
message: `cannot update logical table '${view.name}': materializing an absent row of optional member '${member.relationId}' would leave value column '${bc}' to a base default (it is NOT NULL or declares a default), silently widening the row; assign every value column of the member in this statement, or restrict the update to present rows`,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Emit the per-attribute base ops for an EAV pivot member's update group. Each attribute is
|
|
1254
|
+
* an independent triple, routed by the cell's null-ness and {@link ValueKind}:
|
|
1255
|
+
*
|
|
1256
|
+
* - **null** value → delete its triple for the matched entities.
|
|
1257
|
+
* - **`anchor`** value (`set p = id * 2`) → one **upsert** (`do update`) unifies the matched
|
|
1258
|
+
* UPDATE and the absent materialize INSERT, keyed on `(entity, attribute)` — the value is
|
|
1259
|
+
* computed once over the anchor scan and matched entities read it via `excluded.<valCol>`.
|
|
1260
|
+
* - **`constant`** value → matched UPDATE of the value column + `on conflict (entity, attr)
|
|
1261
|
+
* do nothing` materialize INSERT for entities lacking the triple.
|
|
1262
|
+
* - **`captured`** value (an EAV self-reference `set p = p + 1`, a cross-member read, or an
|
|
1263
|
+
* embedded subquery — all of which an EAV value column lowers to a subquery for) → the
|
|
1264
|
+
* single-identity capture analogue ({@link emitEavCapturedAttr}): the value is materialized once
|
|
1265
|
+
* over the planned body under a `srcN`, the matched UPDATE reads it back by the **entity** column,
|
|
1266
|
+
* and a filtered materialize INSERT reads it back by the **anchor key** (non-null gated). Requires
|
|
1267
|
+
* the capture carrier (the build path).
|
|
1268
|
+
*
|
|
1269
|
+
* A `self` value cannot occur here: an EAV value column substitutes to a correlated subquery, so a
|
|
1270
|
+
* self-reference lowers to a subquery and classifies `captured`, not `self`.
|
|
1271
|
+
*/
|
|
1272
|
+
function emitEavMemberUpdate(ctx, view, shape, member, cells, pred, stmt, ops, registerCapturedExpr) {
|
|
1273
|
+
const pivot = member.attributePivot;
|
|
1274
|
+
for (const cell of cells) {
|
|
1275
|
+
if (cell.isNull) {
|
|
1276
|
+
ops.push(buildEavAttrOp(ctx, view, shape, member, pivot, cell, pred, stmt, 'delete'));
|
|
1277
|
+
}
|
|
1278
|
+
else if (cell.kind === 'anchor') {
|
|
1279
|
+
ops.push(buildEavInsertSelect(ctx, view, shape, member, pivot, cell, pred, stmt, 'update'));
|
|
1280
|
+
}
|
|
1281
|
+
else if (cell.kind === 'captured') {
|
|
1282
|
+
emitEavCapturedAttr(ctx, view, shape, member, pivot, cell, pred, stmt, ops, registerCapturedExpr);
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
ops.push(buildEavAttrOp(ctx, view, shape, member, pivot, cell, pred, stmt, 'update'));
|
|
1286
|
+
ops.push(buildEavInsertSelect(ctx, view, shape, member, pivot, cell, pred, stmt, 'nothing'));
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Emit the captured triple pair for one arbitrary EAV attribute cell — the per-attribute analogue
|
|
1292
|
+
* of {@link emitCapturedMemberUpdate} (columnar). The cell's already-lowered base-term value (a
|
|
1293
|
+
* self-reference / cross-member / subquery value over the planned body) is projected into the
|
|
1294
|
+
* up-front `__vmupd_keys` capture under a stable `srcN` alias, then:
|
|
1295
|
+
*
|
|
1296
|
+
* - the **matched UPDATE** ({@link buildEavAttrOp} `'update'`, value overridden by the read-back)
|
|
1297
|
+
* sets the value column of every existing `<attr>` triple for the matched entities to
|
|
1298
|
+
* `(select srcN from __vmupd_keys k where k.k0_0 = <entity>)` — the captured value correlated by
|
|
1299
|
+
* the entity column (which holds the anchor key, the shared stitch value). Unfiltered: a captured
|
|
1300
|
+
* null on a matched triple writes `val = null`, which reads identically to an absent triple
|
|
1301
|
+
* through the get-body subquery (a benign physical divergence from the explicit `set p = null`
|
|
1302
|
+
* delete — never a widen).
|
|
1303
|
+
* - the **materialize INSERT** ({@link buildEavCapturedInsert}) creates one `<attr>` triple per
|
|
1304
|
+
* matched entity that lacks it, the value read back by the **anchor key**, gated on a runtime
|
|
1305
|
+
* non-null (so an entity whose captured value is null — e.g. incrementing an attribute it does
|
|
1306
|
+
* not have — springs no phantom triple) with `on conflict (entity, attr) do nothing` to cede the
|
|
1307
|
+
* entities that already have the triple to the matched UPDATE (emitted first).
|
|
1308
|
+
*
|
|
1309
|
+
* Requires the capture carrier (the build path); absent it the defensive legacy reject fires (an
|
|
1310
|
+
* arbitrary EAV value cannot be threaded without the `__vmupd_keys` substrate).
|
|
1311
|
+
*/
|
|
1312
|
+
function emitEavCapturedAttr(ctx, view, shape, member, pivot, cell, pred, stmt, ops, registerCapturedExpr) {
|
|
1313
|
+
if (!registerCapturedExpr) {
|
|
1314
|
+
raiseMutationDiagnostic({
|
|
1315
|
+
reason: 'unsupported-decomposition-update',
|
|
1316
|
+
column: cell.attribute,
|
|
1317
|
+
table: view.name,
|
|
1318
|
+
message: `cannot update logical table '${view.name}': writing EAV attribute '${cell.attribute}' with this value needs the per-row capture carrier to thread it across the matched-update and materialize-insert triple pair, which the legacy non-build path does not supply`,
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
// Register the lowered value into the capture (one `srcN` per attribute), keyed by the attribute
|
|
1322
|
+
// so both read-back sites (entity column, anchor key) bind the same alias.
|
|
1323
|
+
const srcAlias = registerCapturedExpr(`cap:${member.relationId.toLowerCase()}:attr:${cell.attribute.toLowerCase()}`, cloneExpr(cell.value));
|
|
1324
|
+
// Matched UPDATE: existing triples read the captured value back by the entity column.
|
|
1325
|
+
ops.push(buildEavAttrOp(ctx, view, shape, member, pivot, cell, pred, stmt, 'update', capturedValueSubquery(srcAlias, 0, [pivot.entityColumn])));
|
|
1326
|
+
// Materialize INSERT: entities lacking the triple, value read back by the anchor key, non-null filtered.
|
|
1327
|
+
ops.push(buildEavCapturedInsert(ctx, view, shape, member, pivot, cell, srcAlias, pred, stmt));
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* One matched EAV op for an attribute: `update <pivot> set <valCol> = <value>` (upsert
|
|
1331
|
+
* value branch) or `delete from <pivot>` (null branch), each scoped
|
|
1332
|
+
* `where <attrCol> = '<attribute>' and <entityCol> in (<anchor subquery>)`. `valueOverride`
|
|
1333
|
+
* supplies the matched-UPDATE value for a `captured` cell (the `__vmupd_keys` read-back correlated
|
|
1334
|
+
* by the entity column); absent, the cell's own lowered value is used (the `constant` branch).
|
|
1335
|
+
*/
|
|
1336
|
+
function buildEavAttrOp(ctx, view, shape, member, pivot, cell, pred, stmt, op, valueOverride) {
|
|
1337
|
+
const where = combineAnd(eavAttrEquals(pivot, cell.attribute), {
|
|
1338
|
+
type: 'in',
|
|
1339
|
+
expr: { type: 'column', name: pivot.entityColumn },
|
|
1340
|
+
subquery: anchorKeySubquery(shape, pred),
|
|
1341
|
+
});
|
|
1342
|
+
const table = memberIdentifier(member);
|
|
1343
|
+
const resolved = resolveMemberTable(ctx, member);
|
|
1344
|
+
if (op === 'delete') {
|
|
1345
|
+
const statement = {
|
|
1346
|
+
type: 'delete', table, where,
|
|
1347
|
+
contextValues: stmt.contextValues, schemaPath: stmt.schemaPath, loc: stmt.loc,
|
|
1348
|
+
};
|
|
1349
|
+
return { table: resolved, op: 'delete', statement };
|
|
1350
|
+
}
|
|
1351
|
+
const statement = {
|
|
1352
|
+
type: 'update', table,
|
|
1353
|
+
assignments: [{ column: pivot.valueColumn, value: valueOverride ?? cloneExpr(cell.value) }],
|
|
1354
|
+
where,
|
|
1355
|
+
contextValues: stmt.contextValues, schemaPath: stmt.schemaPath, loc: stmt.loc,
|
|
1356
|
+
};
|
|
1357
|
+
return { table: resolved, op: 'update', statement };
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Build the anchor-keyed EAV triple insert-select for one attribute, in one of two `action`
|
|
1361
|
+
* flavours sharing the identical select: `insert into <pivot> (<entity>, <attr>, <val>) select
|
|
1362
|
+
* <anchorKey>, '<attribute>', <value> from <anchor> where <pred>` followed by
|
|
1363
|
+
*
|
|
1364
|
+
* - **`'nothing'`** (constant cell, absent branch) — `on conflict (<entity>, <attr>) do nothing`:
|
|
1365
|
+
* one new triple per matched entity that lacks this attribute (entities whose triple exists
|
|
1366
|
+
* conflict and are ceded to the separate matched UPDATE).
|
|
1367
|
+
* - **`'update'`** (anchor-resolvable cell) — `on conflict (<entity>, <attr>) do update set
|
|
1368
|
+
* <valCol> = excluded.<valCol>`: **replaces both** the matched UPDATE and the materialize
|
|
1369
|
+
* INSERT, the value computed once over the anchor scan and read by matched entities via
|
|
1370
|
+
* `excluded.<valCol>`.
|
|
1371
|
+
*
|
|
1372
|
+
* The conflict target `(entity, attribute)` — NOT the stitch key (`entity` alone, which is
|
|
1373
|
+
* intentionally one-to-many) — is guaranteed a declared PRIMARY KEY / non-partial UNIQUE on
|
|
1374
|
+
* the pivot basis by `validatePrimaryAdvertisement` (lens-compiler.ts) at deploy time. That
|
|
1375
|
+
* is what keeps both the matched/materialize partition here and the get-side correlated
|
|
1376
|
+
* subquery single-valued; a non-unique `(entity, attr)` could not deploy.
|
|
1377
|
+
*/
|
|
1378
|
+
function buildEavInsertSelect(ctx, view, shape, member, pivot, cell, pred, stmt, action) {
|
|
1379
|
+
const ref = resolveMemberTable(ctx, member);
|
|
1380
|
+
const anchorKey = singleKeyColumn(view, shape, shape.anchor);
|
|
1381
|
+
const projections = [
|
|
1382
|
+
{ type: 'column', expr: { type: 'column', name: anchorKey, table: shape.anchor.relationId } },
|
|
1383
|
+
{ type: 'column', expr: { type: 'literal', value: cell.attribute } },
|
|
1384
|
+
{ type: 'column', expr: cloneExpr(cell.value) },
|
|
1385
|
+
];
|
|
1386
|
+
const select = {
|
|
1387
|
+
type: 'select',
|
|
1388
|
+
columns: projections,
|
|
1389
|
+
from: [{ ...memberIdentifierSource(shape.anchor), alias: shape.anchor.relationId }],
|
|
1390
|
+
where: pred ? cloneExpr(pred) : undefined,
|
|
1391
|
+
};
|
|
1392
|
+
const upsert = action === 'nothing'
|
|
1393
|
+
? { type: 'upsert', conflictTarget: [pivot.entityColumn, pivot.attributeColumn], action: 'nothing' }
|
|
1394
|
+
: { type: 'upsert', conflictTarget: [pivot.entityColumn, pivot.attributeColumn], action: 'update', assignments: [{ column: pivot.valueColumn, value: excludedColumn(pivot.valueColumn) }] };
|
|
1395
|
+
const statement = {
|
|
1396
|
+
type: 'insert',
|
|
1397
|
+
table: memberIdentifier(member),
|
|
1398
|
+
columns: [pivot.entityColumn, pivot.attributeColumn, pivot.valueColumn],
|
|
1399
|
+
source: select,
|
|
1400
|
+
upsertClauses: [upsert],
|
|
1401
|
+
contextValues: stmt.contextValues,
|
|
1402
|
+
schemaPath: stmt.schemaPath,
|
|
1403
|
+
loc: stmt.loc,
|
|
1404
|
+
};
|
|
1405
|
+
return { table: ref, op: 'insert', statement };
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Build the absent-entity materialize INSERT for a `captured` EAV attribute cell — the triple
|
|
1409
|
+
* analogue of {@link buildCapturedMaterializeInsert} (columnar). `insert into <pivot> (<entity>,
|
|
1410
|
+
* <attr>, <val>) select <anchorKey>, '<attribute>', (select srcN from __vmupd_keys k where k.k0_0
|
|
1411
|
+
* = <anchorKey>) from <anchor> where <pred> and (select srcN …) is not null on conflict (<entity>,
|
|
1412
|
+
* <attr>) do nothing`. The value and the non-null filter both read the captured `srcN` back by the
|
|
1413
|
+
* anchor key. That non-null filter is the **runtime** analogue of the columnar self path's
|
|
1414
|
+
* plan-time fold: a captured EAV value is data-dependent (a self-reference over an entity that
|
|
1415
|
+
* lacks the attribute captures `null + …` = null), so a per-row filter — not a plan-time decision
|
|
1416
|
+
* — gates whether the triple materializes, leaving no phantom null triple. `on conflict (entity,
|
|
1417
|
+
* attr) do nothing` cedes entities that already have the triple to the matched UPDATE (emitted
|
|
1418
|
+
* first); the `(entity, attr)` conflict target is the deploy-guaranteed pivot PK / non-partial
|
|
1419
|
+
* UNIQUE (as in {@link buildEavInsertSelect}). Always emitted (the filter decides per row).
|
|
1420
|
+
*/
|
|
1421
|
+
function buildEavCapturedInsert(ctx, view, shape, member, pivot, cell, srcAlias, pred, stmt) {
|
|
1422
|
+
const ref = resolveMemberTable(ctx, member);
|
|
1423
|
+
const anchorKey = singleKeyColumn(view, shape, shape.anchor);
|
|
1424
|
+
// A fresh read-back subquery per use (projection + filter) — distinct AST nodes.
|
|
1425
|
+
const capturedVal = () => capturedValueSubquery(srcAlias, 0, [anchorKey]);
|
|
1426
|
+
const projections = [
|
|
1427
|
+
{ type: 'column', expr: { type: 'column', name: anchorKey, table: shape.anchor.relationId } },
|
|
1428
|
+
{ type: 'column', expr: { type: 'literal', value: cell.attribute } },
|
|
1429
|
+
{ type: 'column', expr: capturedVal() },
|
|
1430
|
+
];
|
|
1431
|
+
const nonNull = { type: 'unary', operator: 'IS NOT NULL', expr: capturedVal() };
|
|
1432
|
+
const where = combineAnd(pred ? cloneExpr(pred) : undefined, nonNull);
|
|
1433
|
+
const select = {
|
|
1434
|
+
type: 'select',
|
|
1435
|
+
columns: projections,
|
|
1436
|
+
from: [{ ...memberIdentifierSource(shape.anchor), alias: shape.anchor.relationId }],
|
|
1437
|
+
where,
|
|
1438
|
+
};
|
|
1439
|
+
const statement = {
|
|
1440
|
+
type: 'insert',
|
|
1441
|
+
table: memberIdentifier(member),
|
|
1442
|
+
columns: [pivot.entityColumn, pivot.attributeColumn, pivot.valueColumn],
|
|
1443
|
+
source: select,
|
|
1444
|
+
upsertClauses: [{ type: 'upsert', conflictTarget: [pivot.entityColumn, pivot.attributeColumn], action: 'nothing' }],
|
|
1445
|
+
contextValues: stmt.contextValues,
|
|
1446
|
+
schemaPath: stmt.schemaPath,
|
|
1447
|
+
loc: stmt.loc,
|
|
1448
|
+
};
|
|
1449
|
+
return { table: ref, op: 'insert', statement };
|
|
1450
|
+
}
|
|
1451
|
+
/** `<attrCol> = '<attribute>'` — the pivot attribute selector (matched by value). */
|
|
1452
|
+
function eavAttrEquals(pivot, attribute) {
|
|
1453
|
+
return { type: 'binary', operator: '=', left: { type: 'column', name: pivot.attributeColumn }, right: { type: 'literal', value: attribute } };
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* The optional member's **value** basis columns (its non-key logical mappings) — the
|
|
1457
|
+
* set the all-null delete gate ranges over. The shared key is threaded separately and
|
|
1458
|
+
* is never a value column.
|
|
1459
|
+
*/
|
|
1460
|
+
function optionalValueColumns(view, shape, member) {
|
|
1461
|
+
const keyLower = singleKeyColumn(view, shape, member).toLowerCase();
|
|
1462
|
+
const out = [];
|
|
1463
|
+
for (const m of member.columns) {
|
|
1464
|
+
const expr = m.basisExpr;
|
|
1465
|
+
if (expr.type !== 'column')
|
|
1466
|
+
continue; // a computed mapping is read-only, not a value column
|
|
1467
|
+
if (expr.name.toLowerCase() === keyLower)
|
|
1468
|
+
continue;
|
|
1469
|
+
out.push(expr.name);
|
|
1470
|
+
}
|
|
1471
|
+
return out;
|
|
1472
|
+
}
|
|
1473
|
+
/** True for a syntactic null literal (the delete / absent-no-op trigger). */
|
|
1474
|
+
function isNullLiteral(expr) {
|
|
1475
|
+
return expr.type === 'literal' && expr.value === null;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* `excluded.<col>` — the proposed-insert value of an upsert's `do update set` assignment
|
|
1479
|
+
* (registered in the upsert scope by `building/insert.ts`; `NEW.<col>` is the equivalent
|
|
1480
|
+
* alias). An anchor-resolvable upsert assigns each matched row `<col> = excluded.<col>` so
|
|
1481
|
+
* it reads the identical anchor-computed value the absent rows insert.
|
|
1482
|
+
*/
|
|
1483
|
+
function excludedColumn(col) {
|
|
1484
|
+
return { type: 'column', name: col, table: 'excluded' };
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Strip the owning member's relationId qualifier from a lowered self-reference value, so a
|
|
1488
|
+
* per-member UPDATE over it (`<member>.c + 1`) targets that table directly (`c + 1`). The
|
|
1489
|
+
* classifier proved every column ref is the owner's, so a plain strip suffices (a constant
|
|
1490
|
+
* sibling carries no ref — a no-op). Mirrors {@link rewriteAssignedValue}'s strip without
|
|
1491
|
+
* its cross-member reject (the classifier already excluded a foreign qualifier).
|
|
1492
|
+
*/
|
|
1493
|
+
function stripMemberQualifier(value, owner) {
|
|
1494
|
+
return transformExpr(value, (col) => (col.table === owner.relationId ? { type: 'column', name: col.name } : undefined));
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Substitute the owning member's own column refs in a lowered self-reference value with a NULL
|
|
1498
|
+
* literal — an absent row has no member row, so its prior value is null. So `c + 1` lowers to
|
|
1499
|
+
* `null + 1` (→ null, filtered out as a phantom row) and `coalesce(c, 0) + 1` to `coalesce(null, 0)
|
|
1500
|
+
* + 1` (→ 1, materializes). The classifier proved every column ref of a `self` cell is the owner's,
|
|
1501
|
+
* so substituting only the owner qualifier suffices; a constant sibling carries no owner ref and is
|
|
1502
|
+
* left unchanged. Mirrors {@link stripMemberQualifier} but maps the owner's columns to NULL rather
|
|
1503
|
+
* than to a bare member-scoped reference (the materialize evaluates over the anchor, not the member).
|
|
1504
|
+
*/
|
|
1505
|
+
function substituteOwnerColumnsWithNull(value, owner) {
|
|
1506
|
+
return transformExpr(value, (col) => (col.table === owner.relationId ? { type: 'literal', value: null } : undefined));
|
|
1507
|
+
}
|
|
1508
|
+
// --- predicate / subquery construction ------------------------------------
|
|
1509
|
+
/**
|
|
1510
|
+
* The user WHERE rewritten from logical columns into the get body's base terms,
|
|
1511
|
+
* after the anchor-resolvable gate ({@link assertAnchorScoped}) — so each member's
|
|
1512
|
+
* identifying set can be read from the anchor alone (see the file header). The gate
|
|
1513
|
+
* admits an anchor identity column **and** a computed mapping whose basis is the
|
|
1514
|
+
* anchor (`bumped = 11` → `a + 1 = 11`): both substitute into a predicate over the
|
|
1515
|
+
* anchor's own base columns. A predicate touching a non-anchor member (or an EAV
|
|
1516
|
+
* pivot / a subquery) is deferred onto the snapshot-consistent substrate. The
|
|
1517
|
+
* substitution into base terms is the AST construction the anchor subquery rides; the
|
|
1518
|
+
* **gate decision** is read off the threaded backward lineage, not a base-qualifier
|
|
1519
|
+
* scan of the substituted expression.
|
|
1520
|
+
*/
|
|
1521
|
+
function anchorPredicate(view, shape, where) {
|
|
1522
|
+
if (!where)
|
|
1523
|
+
return undefined;
|
|
1524
|
+
assertAnchorScoped(view, shape, where);
|
|
1525
|
+
return substituteViewColumns(where, shape, view);
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Gate a user WHERE to **anchor-resolvable** references via the threaded backward
|
|
1529
|
+
* lineage: every logical column the predicate names must resolve entirely to the
|
|
1530
|
+
* anchor member's own base terms — an identity base column on the anchor
|
|
1531
|
+
* ({@link classifyColumn} → `member` whose relation is the anchor), *or* a computed
|
|
1532
|
+
* mapping whose basis lives on the anchor ({@link classifyColumn} → `computed-mapping`
|
|
1533
|
+
* whose member is the anchor, e.g. `bumped = a + 1` → `a + 1 = 11`). Either substitutes
|
|
1534
|
+
* into a predicate over the anchor's base columns, which the `anchorKeySubquery`
|
|
1535
|
+
* already evaluates, so no new substrate is needed.
|
|
1536
|
+
*
|
|
1537
|
+
* A column backed by a genuine **non-anchor member**, an **EAV pivot**, or an embedded
|
|
1538
|
+
* **subquery** defers onto the snapshot-consistent multi-member substrate — each with
|
|
1539
|
+
* its own accurate message ({@link nonAnchorPredicateDiagnostic}), since an EAV /
|
|
1540
|
+
* unbacked / subquery predicate is not a "non-anchor member" and must not be
|
|
1541
|
+
* misattributed as one. A name that is not a logical column of the table at all is an
|
|
1542
|
+
* encapsulation leak, rejected as `unknown-view-column` (consistent with the
|
|
1543
|
+
* single-source / multi-source `assertTopLevelViewColumns` guard — a typo'd /
|
|
1544
|
+
* projected-away name is a user error, not a deferred multi-member shape). (Replaces
|
|
1545
|
+
* the retired `collectColumnQualifiers` base-qualifier scan — the anchor decision now
|
|
1546
|
+
* reads `updateLineage`, the same backward walk the multi-source path consumes.)
|
|
1547
|
+
*/
|
|
1548
|
+
function assertAnchorScoped(view, shape, where) {
|
|
1549
|
+
const refs = collectViewColumnRefs(where);
|
|
1550
|
+
// Encapsulation-leak guard first: a name the logical table does not expose is an
|
|
1551
|
+
// unknown view column (it would otherwise be mislabeled a "non-anchor member" below).
|
|
1552
|
+
for (const name of refs.names) {
|
|
1553
|
+
if (!shape.columns.some(c => c.name === name)) {
|
|
1554
|
+
raiseMutationDiagnostic({
|
|
1555
|
+
reason: 'unknown-view-column',
|
|
1556
|
+
column: name,
|
|
1557
|
+
table: view.name,
|
|
1558
|
+
message: `cannot write through logical table '${view.name}': '${name}' is not a column of the logical table`,
|
|
1559
|
+
suggestion: `logical table '${view.name}' exposes: ${shape.columns.map(c => c.displayName).join(', ')}.`,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
// A subquery defers regardless of which columns it names (it may name none) — its
|
|
1564
|
+
// multi-member fan-out needs the snapshot-consistent substrate. Checked first so the
|
|
1565
|
+
// message is subquery-specific rather than a misattributed "non-anchor member".
|
|
1566
|
+
if (refs.hasSubquery) {
|
|
1567
|
+
raiseMutationDiagnostic({
|
|
1568
|
+
reason: 'unsupported-decomposition-predicate',
|
|
1569
|
+
table: view.name,
|
|
1570
|
+
message: `cannot write through logical table '${view.name}': the WHERE embeds a subquery; a predicate-honest multi-member fan-out needs snapshot-consistent base-op execution (deferred — filter only on anchor base columns)`,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
const anchorId = shape.anchor.relationId;
|
|
1574
|
+
for (const name of refs.names) {
|
|
1575
|
+
const route = classifyColumn(view, shape, name);
|
|
1576
|
+
// Anchor-resolvable — an identity base column on the anchor, OR a computed mapping
|
|
1577
|
+
// whose basis is the anchor. Both `member` and `computed-mapping` carry `member`,
|
|
1578
|
+
// so the union narrows correctly.
|
|
1579
|
+
if ((route.kind === 'member' || route.kind === 'computed-mapping') && route.member.relationId === anchorId) {
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
raiseMutationDiagnostic(nonAnchorPredicateDiagnostic(view, name, route));
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* The deferral diagnostic for a WHERE column that is **not** anchor-resolvable — a
|
|
1587
|
+
* non-anchor member, an EAV pivot, or a name backed by no member. Each keeps the
|
|
1588
|
+
* `unsupported-decomposition-predicate` reason (the structured contract is unchanged)
|
|
1589
|
+
* and differs only in the human message, so the misattribution the support fix removes
|
|
1590
|
+
* does not recur: an EAV / unbacked column is not a "non-anchor member". The
|
|
1591
|
+
* genuine-non-anchor case preserves the `non-anchor decomposition member` substring the
|
|
1592
|
+
* deferral test pins.
|
|
1593
|
+
*/
|
|
1594
|
+
function nonAnchorPredicateDiagnostic(view, name, route) {
|
|
1595
|
+
const head = `cannot write through logical table '${view.name}': the WHERE references column '${name}',`;
|
|
1596
|
+
const need = `a predicate-honest multi-member fan-out needs snapshot-consistent base-op execution`;
|
|
1597
|
+
switch (route.kind) {
|
|
1598
|
+
case 'eav':
|
|
1599
|
+
return {
|
|
1600
|
+
reason: 'unsupported-decomposition-predicate', column: name, table: view.name,
|
|
1601
|
+
message: `${head} backed by an EAV pivot member; ${need} (deferred — filter only on anchor base columns)`,
|
|
1602
|
+
};
|
|
1603
|
+
case 'unbacked':
|
|
1604
|
+
return {
|
|
1605
|
+
reason: 'unsupported-decomposition-predicate', column: name, table: view.name,
|
|
1606
|
+
message: `${head} which is not backed by any decomposition member; ${need} (deferred — filter only on anchor base columns)`,
|
|
1607
|
+
};
|
|
1608
|
+
default: // 'member' / 'computed-mapping' on a non-anchor member
|
|
1609
|
+
return {
|
|
1610
|
+
reason: 'unsupported-decomposition-predicate', column: name, table: view.name,
|
|
1611
|
+
message: `${head} backed by a non-anchor decomposition member; ${need} (deferred — filter only on the anchor / shared key, or pin the rows via the anchor)`,
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/** `select <anchorKey> from <anchorTable> <anchorAlias> [where <pred>]` — the shared identifying set. */
|
|
1616
|
+
function anchorKeySubquery(shape, pred) {
|
|
1617
|
+
const anchorKey = singleKeyColumn(undefined, shape, shape.anchor);
|
|
1618
|
+
return {
|
|
1619
|
+
type: 'select',
|
|
1620
|
+
columns: [{ type: 'column', expr: { type: 'column', name: anchorKey, table: shape.anchor.relationId } }],
|
|
1621
|
+
from: [{ ...memberIdentifierSource(shape.anchor), alias: shape.anchor.relationId }],
|
|
1622
|
+
where: pred ? cloneExpr(pred) : undefined,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Substitute references to logical columns (unqualified, or qualified by the
|
|
1627
|
+
* logical table's own name) with their backing get-body expression. Base-member-
|
|
1628
|
+
* qualified references are left untouched.
|
|
1629
|
+
*/
|
|
1630
|
+
function substituteViewColumns(expr, shape, view) {
|
|
1631
|
+
const viewName = view.name.toLowerCase();
|
|
1632
|
+
return transformExpr(expr, (col) => {
|
|
1633
|
+
if (col.table && col.table.toLowerCase() !== viewName)
|
|
1634
|
+
return undefined;
|
|
1635
|
+
const repl = shape.viewColToBaseRef.get(col.name.toLowerCase());
|
|
1636
|
+
return repl ? cloneExpr(repl) : undefined;
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
/** Strip the anchor's alias qualifier so a predicate targets the bare anchor UPDATE/DELETE. */
|
|
1640
|
+
function stripAnchorQualifier(expr, shape) {
|
|
1641
|
+
return transformExpr(expr, (col) => (col.table === shape.anchor.relationId ? { type: 'column', name: col.name } : undefined));
|
|
1642
|
+
}
|
|
1643
|
+
// --- shape helpers --------------------------------------------------------
|
|
1644
|
+
/** True when `logical` (lowercased) is one of the anchor's shared-key columns. */
|
|
1645
|
+
function isSharedKeyColumn(shape, logical) {
|
|
1646
|
+
const keys = shape.storage.sharedKey.keyColumnsByRelation.get(shape.anchor.relationId) ?? [];
|
|
1647
|
+
return keys.some(k => k.toLowerCase() === logical);
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* The single shared-key column for a member. v1 threads a single-column key
|
|
1651
|
+
* (mirrors `multi-source.ts`' single-column-PK boundary); a composite/absent key
|
|
1652
|
+
* is deferred. `view` is optional purely so the deferral message can name the
|
|
1653
|
+
* logical table (the anchor-subquery call site has none in scope).
|
|
1654
|
+
*/
|
|
1655
|
+
function singleKeyColumn(view, shape, member) {
|
|
1656
|
+
const keys = shape.storage.sharedKey.keyColumnsByRelation.get(member.relationId) ?? [];
|
|
1657
|
+
if (keys.length !== 1) {
|
|
1658
|
+
raiseMutationDiagnostic({
|
|
1659
|
+
reason: 'unsupported-decomposition-key',
|
|
1660
|
+
table: view?.name,
|
|
1661
|
+
message: `cannot write through a decomposition with a ${keys.length === 0 ? 'missing' : 'composite'} shared key on member '${member.relationId}': v1 fan-out threads a single-column key`,
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
return keys[0];
|
|
1665
|
+
}
|
|
1666
|
+
function memberIdentifier(member) {
|
|
1667
|
+
return { type: 'identifier', name: member.relation.table, schema: member.relation.schema };
|
|
1668
|
+
}
|
|
1669
|
+
function memberIdentifierSource(member) {
|
|
1670
|
+
return { type: 'table', table: memberIdentifier(member) };
|
|
1671
|
+
}
|
|
1672
|
+
function resolveMemberTable(ctx, member) {
|
|
1673
|
+
return buildTableReference(memberIdentifierSource(member), ctx).tableRef;
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Collect the **logical column names** a user predicate references (lowercased,
|
|
1677
|
+
* ignoring any view-name qualifier) and whether it embeds a subquery. The anchor
|
|
1678
|
+
* gate ({@link assertAnchorScoped}) maps each collected name to its owning member
|
|
1679
|
+
* via the threaded backward lineage — so the gate decision is lineage-driven; this
|
|
1680
|
+
* walk only enumerates which columns to check (the user-term analogue of the retired
|
|
1681
|
+
* `collectColumnQualifiers`, which scanned base-table qualifiers on the substituted
|
|
1682
|
+
* expression).
|
|
1683
|
+
*/
|
|
1684
|
+
function collectViewColumnRefs(expr) {
|
|
1685
|
+
const names = new Set();
|
|
1686
|
+
let hasSubquery = false;
|
|
1687
|
+
const walk = (node) => {
|
|
1688
|
+
if (Array.isArray(node)) {
|
|
1689
|
+
node.forEach(walk);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (!node || typeof node !== 'object' || !('type' in node))
|
|
1693
|
+
return;
|
|
1694
|
+
const n = node;
|
|
1695
|
+
if (n.type === 'column') {
|
|
1696
|
+
if (typeof n.name === 'string')
|
|
1697
|
+
names.add(n.name.toLowerCase());
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (n.type === 'subquery' || n.type === 'select' || n.type === 'exists') {
|
|
1701
|
+
hasSubquery = true;
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
for (const v of Object.values(n))
|
|
1705
|
+
walk(v);
|
|
1706
|
+
};
|
|
1707
|
+
walk(expr);
|
|
1708
|
+
return { names, hasSubquery };
|
|
1709
|
+
}
|
|
1710
|
+
function rejectReturning(view, returning) {
|
|
1711
|
+
if (returning && returning.length > 0) {
|
|
1712
|
+
raiseMutationDiagnostic({
|
|
1713
|
+
reason: 'returning-through-view',
|
|
1714
|
+
table: view.name,
|
|
1715
|
+
message: `RETURNING through logical table '${view.name}' is not yet supported`,
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
//# sourceMappingURL=decomposition.js.map
|