@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,3064 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialized-view maintenance: schema-change staleness tracking plus row-time
|
|
3
|
+
* write-through maintenance.
|
|
4
|
+
*
|
|
5
|
+
* Two responsibilities:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Staleness** — a *schema* change to a source table (drop / alter) can break
|
|
8
|
+
* an MV's body. This manager subscribes to schema-change events and marks any
|
|
9
|
+
* MV whose body reads a modified/removed source `stale`. The next reference
|
|
10
|
+
* re-validates the body (erroring with the staleness diagnostic on an
|
|
11
|
+
* incompatible change); the next successful refresh clears the flag. One
|
|
12
|
+
* carve-out: a **body-irrelevant** `table_modified` (constraint/stats/tags-only —
|
|
13
|
+
* columns and physical PK identical, see `isBodyIrrelevantTableChange`) instead
|
|
14
|
+
* RECOMPILES each live dependent's row-time plan in place
|
|
15
|
+
* (`tryRecompileMaterializedViewLive`, gated by shape re-derivation), falling
|
|
16
|
+
* back to mark-stale on any failure — so DROP/ADD/RENAME CONSTRAINT and ANALYZE
|
|
17
|
+
* no longer de-liven dependents whose backing shape is unaffected. The SAME
|
|
18
|
+
* subscription also rebuilds a maintained table's compiled **derived-row
|
|
19
|
+
* constraint validator** when a *constraint-only* dependency — an FK parent or a
|
|
20
|
+
* subquery-CHECK target, neither a derivation source — is renamed/dropped/re-created
|
|
21
|
+
* (see {@link MaterializedViewManager.rebuildConstraintValidatorsFor}); without
|
|
22
|
+
* this the validator, compiled once at registration, would keep resolving against
|
|
23
|
+
* the dead/renamed incarnation and fail maintenance writes with an internal
|
|
24
|
+
* module-connect error.
|
|
25
|
+
*
|
|
26
|
+
* 2. **Row-time write-through** (`maintainRowTime`) — the backing table is kept
|
|
27
|
+
* consistent *synchronously* with each source row-write, driven from the
|
|
28
|
+
* runtime DML boundary (not at COMMIT). Each MV's maintenance is **cost-gated with a
|
|
29
|
+
* floor**: the builder matches the body to a bounded-delta arm (the covering-index
|
|
30
|
+
* inverse projection, an aggregate / lateral-TVF / 1:1-join residual) when one fits —
|
|
31
|
+
* each source row then maps to a bounded backing delta, no full scan — and otherwise
|
|
32
|
+
* falls through to the always-correct **full-rebuild floor** (re-evaluate the whole
|
|
33
|
+
* body, replace the backing). **No body is rejected for its shape;** the only
|
|
34
|
+
* create-time rejections are non-shape (non-determinism, bag/no-key, no relational
|
|
35
|
+
* output, and a full-rebuild-only body over a source past the size threshold). The
|
|
36
|
+
* write targets the backing table's *pending* transaction layer through the same
|
|
37
|
+
* connection a `select` from the MV uses, so the change is visible mid-transaction
|
|
38
|
+
* (reads-own-writes) and is committed/rolled-back in lockstep with the source write by
|
|
39
|
+
* the coordinated commit (see {@link MaterializedViewManager.buildMaintenancePlan}).
|
|
40
|
+
*/
|
|
41
|
+
import { createLogger } from '../common/logger.js';
|
|
42
|
+
import { QuereusError } from '../common/errors.js';
|
|
43
|
+
import { StatusCode } from '../common/types.js';
|
|
44
|
+
import { isRelationalNode, isScalarNode } from '../planner/nodes/plan-node.js';
|
|
45
|
+
import { ColumnReferenceNode, TableReferenceNode } from '../planner/nodes/reference.js';
|
|
46
|
+
import { FilterNode } from '../planner/nodes/filter.js';
|
|
47
|
+
import { checkDeterministic } from '../planner/validation/determinism-validator.js';
|
|
48
|
+
import { emitPlanNode } from '../runtime/emitters.js';
|
|
49
|
+
import { EmissionContext } from '../runtime/emission-context.js';
|
|
50
|
+
import { Scheduler } from '../runtime/scheduler.js';
|
|
51
|
+
import { RowContextMap } from '../runtime/context-helpers.js';
|
|
52
|
+
import { createStrictRowContextMap, wrapTableContextsStrict } from '../runtime/strict-fork.js';
|
|
53
|
+
import { isAsyncIterable } from '../runtime/utils.js';
|
|
54
|
+
import { AggregateNode } from '../planner/nodes/aggregate-node.js';
|
|
55
|
+
import { TableFunctionCallNode } from '../planner/nodes/table-function-call.js';
|
|
56
|
+
import { PlanNodeType } from '../planner/nodes/plan-node-type.js';
|
|
57
|
+
import { buildSourceUnionScope } from '../planner/analysis/change-scope.js';
|
|
58
|
+
import { injectKeyFilter } from '../planner/analysis/key-filter.js';
|
|
59
|
+
import { keysOf } from '../planner/util/fd-utils.js';
|
|
60
|
+
import { deriveCoarsenedBackingKey, resolveValuePreservingSourceCol } from '../planner/analysis/coarsened-key.js';
|
|
61
|
+
import { proveOneToOneJoin } from '../planner/analysis/coverage-prover.js';
|
|
62
|
+
import { CapabilityDetectors } from '../planner/framework/characteristics.js';
|
|
63
|
+
import { selectMaintenanceStrategy, isFullRebuildPathological, seqScanCost, filterCost, projectCost, } from '../planner/cost/index.js';
|
|
64
|
+
import { resolveBackingHost, tryResolveBackingHost, isBodyIrrelevantTableChange, tryRecompileMaterializedViewLive } from '../runtime/emit/materialized-view-helpers.js';
|
|
65
|
+
import { assertTransitiveRestrictsForParentMutation, executeForeignKeyActionsAndLens } from '../runtime/foreign-key-actions.js';
|
|
66
|
+
import { buildDerivedRowValidator, makePoisonedDerivedRowValidator, validateDerivedRowImage } from './derived-row-validator.js';
|
|
67
|
+
import { buildPrimaryKeyFromValues } from '../vtab/memory/utils/primary-key.js';
|
|
68
|
+
import { compilePredicate } from '../vtab/memory/utils/predicate.js';
|
|
69
|
+
import { compareSqlValues, rowsValueIdentical, normalizeCollationName } from '../util/comparison.js';
|
|
70
|
+
import { coveringMvHonorsIndexCollation, uniqueEnforcementCollations } from '../schema/unique-enforcement.js';
|
|
71
|
+
const log = createLogger('core:materialized-views');
|
|
72
|
+
/** Fallback source row estimate when the StatsProvider has no count (mirrors the
|
|
73
|
+
* optimizer's naive default). Only feeds the create-time maintenance cost gate. */
|
|
74
|
+
const DEFAULT_SOURCE_ROWS = 1000;
|
|
75
|
+
export class MaterializedViewManager {
|
|
76
|
+
ctx;
|
|
77
|
+
unsubscribeSchemaChanges = null;
|
|
78
|
+
/** Compiled maintenance plans keyed by MV `schema.name` (lowercase). */
|
|
79
|
+
rowTime = new Map();
|
|
80
|
+
/** Source base (lowercased `schema.table`) → set of MV keys with a row-time plan
|
|
81
|
+
* reading it. The per-row DML maintenance hook looks plans up by source base. */
|
|
82
|
+
rowTimeBySource = new Map();
|
|
83
|
+
constructor(ctx) {
|
|
84
|
+
this.ctx = ctx;
|
|
85
|
+
this.subscribeToSchemaChanges();
|
|
86
|
+
}
|
|
87
|
+
subscribeToSchemaChanges() {
|
|
88
|
+
const notifier = this.ctx.schemaManager.getChangeNotifier();
|
|
89
|
+
this.unsubscribeSchemaChanges = notifier.addListener((event) => {
|
|
90
|
+
if (event.type === 'table_removed' || event.type === 'table_modified') {
|
|
91
|
+
const changed = `${event.schemaName}.${event.objectName}`.toLowerCase();
|
|
92
|
+
// A **genuine** source `table_modified` (distinct old/new objects). Live
|
|
93
|
+
// dependents are routed through an in-place RECOMPILE that keeps them live
|
|
94
|
+
// when provably unaffected, instead of marked stale — covering BOTH a
|
|
95
|
+
// body-irrelevant change (constraint/stats/tags/default-only — columns + PK
|
|
96
|
+
// identical: DROP/RENAME/ADD CONSTRAINT, declarative FK retargets, ANALYZE,
|
|
97
|
+
// rename propagation's constraint-AST rewrites) AND a structural ALTER
|
|
98
|
+
// (ADD/DROP/ALTER COLUMN) the body provably never reads. The recompile is
|
|
99
|
+
// shape-gated, and for a structural value-semantics ALTER (type/collation)
|
|
100
|
+
// additionally content-stability-gated (see tryRecompileMaterializedViewLive).
|
|
101
|
+
// The synthetic backing-invalidation event emitBackingInvalidation fires with
|
|
102
|
+
// the SAME object as old/new is deliberately NOT genuine (the
|
|
103
|
+
// `oldObject !== newObject` guard) — it must cascade staleness down MV-over-MV
|
|
104
|
+
// chains, never trigger a keep-live recompile.
|
|
105
|
+
const modified = event.type === 'table_modified' && event.oldObject !== event.newObject
|
|
106
|
+
? event
|
|
107
|
+
: undefined;
|
|
108
|
+
// Body-irrelevant is retained ONLY to decide the already-stale skip below,
|
|
109
|
+
// whose semantics differ between the constraint-only and structural cases.
|
|
110
|
+
const bodyIrrelevant = modified !== undefined
|
|
111
|
+
&& isBodyIrrelevantTableChange(modified.oldObject, modified.newObject);
|
|
112
|
+
for (const mv of this.ctx.schemaManager.getAllMaintainedTables()) {
|
|
113
|
+
if (!mv.derivation.sourceTables.includes(changed))
|
|
114
|
+
continue;
|
|
115
|
+
// CONSTRAINT-ONLY change on an already-stale dependent: skip entirely.
|
|
116
|
+
// There is no live plan to recompile, only REFRESH may clear a pre-existing
|
|
117
|
+
// flag (the backing may be behind), and re-releasing the (absent) plan /
|
|
118
|
+
// re-emitting invalidation would be pointless churn. A STRUCTURAL change on
|
|
119
|
+
// an already-stale dependent instead FALLS THROUGH to re-emit below — the
|
|
120
|
+
// backing shape may now differ, so cached plans must recompile.
|
|
121
|
+
if (bodyIrrelevant && mv.derivation.stale)
|
|
122
|
+
continue;
|
|
123
|
+
// Genuine source change on a LIVE dependent: try to keep it live. On success
|
|
124
|
+
// `stale` is untouched, the plan is rebuilt against the new catalog, and NO
|
|
125
|
+
// emitBackingInvalidation fires — the backing stays maintained, so cached plans
|
|
126
|
+
// reading it remain correct (a plan reading the *source* invalidates via its own
|
|
127
|
+
// direct statement dependency on the source table). Any failure (shape mismatch,
|
|
128
|
+
// content not provably stable, ineligible re-plan) falls through to the stale
|
|
129
|
+
// path below, verbatim.
|
|
130
|
+
if (modified !== undefined && !mv.derivation.stale
|
|
131
|
+
&& tryRecompileMaterializedViewLive(this.ctx, mv, modified.oldObject, modified.newObject))
|
|
132
|
+
continue;
|
|
133
|
+
if (!mv.derivation.stale) {
|
|
134
|
+
mv.derivation.stale = true;
|
|
135
|
+
log('Marked materialized view %s.%s stale due to %s on %s', mv.schemaName, mv.name, event.type, changed);
|
|
136
|
+
}
|
|
137
|
+
// A source schema change invalidates the compiled row-time plan;
|
|
138
|
+
// detach it. The MV reads "stale" until refreshed or recreated,
|
|
139
|
+
// which re-registers it.
|
|
140
|
+
this.releaseRowTime(mvKey(mv.schemaName, mv.name));
|
|
141
|
+
// Invalidate any cached prepared-statement plan reading this MV's
|
|
142
|
+
// backing table so it recompiles and re-hits the build-time `stale`
|
|
143
|
+
// guard (see emitBackingInvalidation). This is load-bearing for a plan
|
|
144
|
+
// compiled while the MV was NOT stale: its only schema dependency is the
|
|
145
|
+
// backing table, which the source event never names. (A plan compiled
|
|
146
|
+
// while already stale instead carries a direct dependency on the source —
|
|
147
|
+
// the while-stale build-time re-validation resolves and records it — so
|
|
148
|
+
// the emit is defensive redundancy there, not a correctness requirement.)
|
|
149
|
+
// Emitting per qualifying event (rather than only on the false→true
|
|
150
|
+
// transition) also re-propagates the cascade down an MV-over-MV chain.
|
|
151
|
+
this.emitBackingInvalidation(mv);
|
|
152
|
+
}
|
|
153
|
+
// Rebuild any derived-row validator that depends on the changed table as a
|
|
154
|
+
// CONSTRAINT-ONLY dependency (FK parent / subquery-CHECK target — never a
|
|
155
|
+
// derivation source, handled above). Runs AFTER the source loop so a plan
|
|
156
|
+
// the source path just released is naturally skipped (it is gone from
|
|
157
|
+
// `rowTime`). `matchOwnName` covers the rename: an FK-parent / CHECK-target
|
|
158
|
+
// rename rewrites THIS maintained table's own FK/CHECK in place and fires
|
|
159
|
+
// `table_modified` on the maintained table itself (the original dependency
|
|
160
|
+
// name is gone from the catalog), so the dependency-set match alone misses it.
|
|
161
|
+
// Runs for body-irrelevant events too — this IS the constraint-only-
|
|
162
|
+
// dependency rebuild path; a just-recompiled dependent's validator was
|
|
163
|
+
// already rebuilt fresh inside registerMaterializedView, so the second
|
|
164
|
+
// rebuild here is idempotent.
|
|
165
|
+
this.rebuildConstraintValidatorsFor(changed, /*matchOwnName*/ true);
|
|
166
|
+
}
|
|
167
|
+
else if (event.type === 'table_added') {
|
|
168
|
+
// A re-created dependency (previously dropped → poisoned or absent-parent
|
|
169
|
+
// fallback validator) self-heals: rebuild any validator that named it. No
|
|
170
|
+
// own-name match — a maintained table's own creation registers its validator
|
|
171
|
+
// directly. The table is already in the catalog when this fires.
|
|
172
|
+
const changed = `${event.schemaName}.${event.objectName}`.toLowerCase();
|
|
173
|
+
this.rebuildConstraintValidatorsFor(changed, /*matchOwnName*/ false);
|
|
174
|
+
}
|
|
175
|
+
else if (event.type === 'materialized_view_removed') {
|
|
176
|
+
this.releaseRowTime(mvKey(event.schemaName, event.objectName));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Rebuild the derived-row constraint validator of every registered plan whose
|
|
182
|
+
* validator depends on `changed` (lowercased `schema.table`): it names `changed`
|
|
183
|
+
* in {@link DerivedRowConstraintValidator.dependencyTables} (FK parent /
|
|
184
|
+
* subquery-CHECK target), or — when `matchOwnName` — `changed` IS the maintained
|
|
185
|
+
* table itself (the rename signal; see {@link subscribeToSchemaChanges}).
|
|
186
|
+
*
|
|
187
|
+
* The derivation is unaffected by a constraint-only dependency's DDL, so this
|
|
188
|
+
* rebuilds the validator ONLY — no {@link releaseRowTime}, no staleness, no
|
|
189
|
+
* maintenance interruption. The rebuild reads the CURRENT catalog record
|
|
190
|
+
* (`getMaintainedTable`) so a rename re-resolves against the new name, and
|
|
191
|
+
* replacing the validator also refreshes its `dependencyTables` (a rename re-keys
|
|
192
|
+
* `{main.parent}` → `{main.parent2}`, so a later drop of `parent2` is caught too).
|
|
193
|
+
*
|
|
194
|
+
* Rebuild-failure handling: a rebuild THROWS when the subquery-CHECK target was
|
|
195
|
+
* dropped (`buildConstraintChecks` → optimize raises a sited "table not found").
|
|
196
|
+
* The throw is caught and a {@link makePoisonedDerivedRowValidator} installed, so
|
|
197
|
+
* (a) this listener never propagates an exception — a schema-change event must not
|
|
198
|
+
* fail the unrelated DDL that triggered it — and (b) the next derivation write
|
|
199
|
+
* surfaces the clear sited planning error instead of the stale validator's internal
|
|
200
|
+
* module-connect failure. The FK-parent-dropped case does NOT throw: the
|
|
201
|
+
* absent-parent null-guards-only fallback (`buildChildSideFKChecks`) builds cleanly,
|
|
202
|
+
* so the rebuilt validator is healthy (a non-NULL ref fails with the maintained-table
|
|
203
|
+
* FK attribution; a NULL ref is admitted under MATCH SIMPLE).
|
|
204
|
+
*/
|
|
205
|
+
rebuildConstraintValidatorsFor(changed, matchOwnName) {
|
|
206
|
+
for (const plan of this.rowTime.values()) {
|
|
207
|
+
const validator = plan.derivedRowValidator;
|
|
208
|
+
if (!validator)
|
|
209
|
+
continue;
|
|
210
|
+
const ownName = `${validator.schemaName}.${validator.tableName}`.toLowerCase();
|
|
211
|
+
if (!validator.dependencyTables.has(changed) && !(matchOwnName && changed === ownName))
|
|
212
|
+
continue;
|
|
213
|
+
const currentMv = this.ctx.schemaManager.getMaintainedTable(validator.schemaName, validator.tableName);
|
|
214
|
+
// MV gone (dropped) — `materialized_view_removed` releases the plan separately.
|
|
215
|
+
if (!currentMv)
|
|
216
|
+
continue;
|
|
217
|
+
try {
|
|
218
|
+
plan.derivedRowValidator = buildDerivedRowValidator(this.ctx, currentMv);
|
|
219
|
+
log('Rebuilt derived-row validator for %s after schema change on %s', ownName, changed);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const error = err instanceof QuereusError
|
|
223
|
+
? err
|
|
224
|
+
: new QuereusError(`rebuilding derived-row validator for '${ownName}' failed: ${err.message}`, StatusCode.ERROR);
|
|
225
|
+
log('Derived-row validator rebuild for %s failed after schema change on %s (%s); installing poisoned validator', ownName, changed, error.message);
|
|
226
|
+
plan.derivedRowValidator = makePoisonedDerivedRowValidator(validator, error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Emit a synthetic `table_modified` event for `mv`'s backing table so any cached
|
|
232
|
+
* prepared-statement plan that reads the backing table directly invalidates →
|
|
233
|
+
* recompiles → re-hits the build-time `stale` guard in `building/select.ts`.
|
|
234
|
+
*
|
|
235
|
+
* A `select … from mv` compiled while the MV was NOT stale resolves to a
|
|
236
|
+
* `TableReference` against the maintained table itself, so its only schema
|
|
237
|
+
* dependency is that table. The *source* change event that marks the MV stale never
|
|
238
|
+
* names the maintained table, so without this emit the cached plan would re-run the
|
|
239
|
+
* scan and serve stale rows against a structurally-changed source — bypassing the
|
|
240
|
+
* guard a fresh prepare would hit. (A plan compiled while the MV is *already* stale
|
|
241
|
+
* is separately safe: the while-stale build-time re-validation resolves the body's
|
|
242
|
+
* source tables and records them as direct statement dependencies, so a later source
|
|
243
|
+
* change invalidates it without this emit — verified by the regression suite, which
|
|
244
|
+
* stays green even with the emit removed for that case.) The `Statement` listener
|
|
245
|
+
* maps `table_*` → `'table'` and matches on type + objectName (+ optional schemaName)
|
|
246
|
+
* only, ignoring the payload, so the maintained `TableSchema` is passed as both old/new.
|
|
247
|
+
*
|
|
248
|
+
* **Same-object payload contract (load-bearing coupling).** Passing the SAME object
|
|
249
|
+
* as `oldObject` and `newObject` is what keeps this synthetic event body-RELEVANT to
|
|
250
|
+
* `isBodyIrrelevantTableChange` (its reference-equality guard) — so it cascades
|
|
251
|
+
* staleness down an MV-over-MV chain instead of triggering the consumers'
|
|
252
|
+
* recompile-in-place path. Every genuine `table_modified` emitter passes distinct
|
|
253
|
+
* old/new objects. If this payload ever changes, change the classifier's guard with
|
|
254
|
+
* it (see the matching comment in runtime/emit/materialized-view-helpers.ts).
|
|
255
|
+
*
|
|
256
|
+
* Safety: the event names the maintained table itself, which is never in its OWN
|
|
257
|
+
* `sourceTables` (self-reference is rejected at create), so this manager's listener
|
|
258
|
+
* treats it as a no-op for a plain MV; for an MV-over-MV chain it conservatively
|
|
259
|
+
* cascades staleness down the producer→consumer DAG (acyclic — a consumer requires
|
|
260
|
+
* its producer to pre-exist), so the nested notification terminates. If the table
|
|
261
|
+
* lookup unexpectedly fails the MV is already in a broken state — skip the emit
|
|
262
|
+
* rather than fabricate a partial event.
|
|
263
|
+
*/
|
|
264
|
+
emitBackingInvalidation(mv) {
|
|
265
|
+
const backing = this.ctx.schemaManager.getTable(mv.schemaName, mv.name);
|
|
266
|
+
if (!backing) {
|
|
267
|
+
log('Skipping backing invalidation for %s.%s: backing table %s not found (MV already broken)', mv.schemaName, mv.name, mv.name);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.ctx.schemaManager.getChangeNotifier().notifyChange({
|
|
271
|
+
type: 'table_modified',
|
|
272
|
+
schemaName: mv.schemaName,
|
|
273
|
+
objectName: mv.name,
|
|
274
|
+
oldObject: backing,
|
|
275
|
+
newObject: backing,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Compile + register an MV for row-time write-through maintenance. Always
|
|
280
|
+
* builds the maintenance plan via {@link buildMaintenancePlan}, which throws on a
|
|
281
|
+
* body that is not row-time maintainable — the create emitter rolls the MV back on
|
|
282
|
+
* throw, so an ineligible body errors cleanly at create time.
|
|
283
|
+
*/
|
|
284
|
+
registerMaterializedView(mv) {
|
|
285
|
+
const key = mvKey(mv.schemaName, mv.name);
|
|
286
|
+
// Cache the source-union change-scope so a `select` from this MV projects to
|
|
287
|
+
// its sources in `analyzeChangeScope`: the backing table is maintained off the
|
|
288
|
+
// user change log (synchronously at the DML boundary), so a `Database.watch`
|
|
289
|
+
// on this MV must project to its sources rather than the never-change-logged
|
|
290
|
+
// backing table. v1 is the conservative union of a `full` watch per source.
|
|
291
|
+
mv.derivation.sourceScope = buildSourceUnionScope(mv.derivation.sourceTables);
|
|
292
|
+
this.releaseRowTime(key);
|
|
293
|
+
const plan = this.buildMaintenancePlan(mv); // throws on ineligible shape
|
|
294
|
+
// Compile the declared-CHECK/FK derived-row validator (undefined when the
|
|
295
|
+
// table declares nothing — the zero-overhead gate). Built here, inside the
|
|
296
|
+
// registration the create/attach paths roll back on throw, so a constraint
|
|
297
|
+
// that cannot compile (e.g. a non-deterministic CHECK without the pragma)
|
|
298
|
+
// errors cleanly at create time.
|
|
299
|
+
plan.derivedRowValidator = buildDerivedRowValidator(this.ctx, mv);
|
|
300
|
+
// Precompute the weakened-K′-column watch for row-time collision telemetry.
|
|
301
|
+
// `undefined` unless this MV carries a coarsened backing key — the zero-overhead
|
|
302
|
+
// gate that keeps a non-coarsened MV's maintenance path untouched (see
|
|
303
|
+
// {@link detectAndReportCoarseningCollisions}).
|
|
304
|
+
plan.coarseningWatch = this.buildCoarseningWatch(mv);
|
|
305
|
+
this.rowTime.set(key, plan);
|
|
306
|
+
// Index the plan under every source base it reads. Single-source arms index
|
|
307
|
+
// under `sourceBase` only; the 1:1-join arm also indexes under the lookup base
|
|
308
|
+
// so a write to `P` fires maintenance too (handled by the reverse residual).
|
|
309
|
+
for (const base of planSourceBases(plan)) {
|
|
310
|
+
let set = this.rowTimeBySource.get(base);
|
|
311
|
+
if (!set) {
|
|
312
|
+
set = new Set();
|
|
313
|
+
this.rowTimeBySource.set(base, set);
|
|
314
|
+
}
|
|
315
|
+
set.add(key);
|
|
316
|
+
}
|
|
317
|
+
log('Registered row-time materialized view %s.%s', mv.schemaName, mv.name);
|
|
318
|
+
}
|
|
319
|
+
/** Detach an MV's row-time plan + its source-base index entry (DROP path). */
|
|
320
|
+
unregisterMaterializedView(schemaName, name) {
|
|
321
|
+
this.releaseRowTime(mvKey(schemaName, name));
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Force-mark an MV stale: set the flag, detach its row-time plan, and invalidate
|
|
325
|
+
* cached prepared-statement plans reading its backing so the next reference
|
|
326
|
+
* re-hits the build-time stale guard. Mirrors the schema-change listener's stale
|
|
327
|
+
* transition exactly; exposed for the ALTER … RENAME propagation failure path
|
|
328
|
+
* (a dependent MV whose in-place body rewrite / backing rename / re-registration
|
|
329
|
+
* failed mid-way must not keep serving its backing as if live).
|
|
330
|
+
*/
|
|
331
|
+
markMaterializedViewStale(mv) {
|
|
332
|
+
if (!mv.derivation.stale) {
|
|
333
|
+
mv.derivation.stale = true;
|
|
334
|
+
log('Marked materialized view %s.%s stale (forced)', mv.schemaName, mv.name);
|
|
335
|
+
}
|
|
336
|
+
this.releaseRowTime(mvKey(mv.schemaName, mv.name));
|
|
337
|
+
this.emitBackingInvalidation(mv);
|
|
338
|
+
}
|
|
339
|
+
dispose() {
|
|
340
|
+
if (this.unsubscribeSchemaChanges) {
|
|
341
|
+
this.unsubscribeSchemaChanges();
|
|
342
|
+
this.unsubscribeSchemaChanges = null;
|
|
343
|
+
}
|
|
344
|
+
for (const key of [...this.rowTime.keys()]) {
|
|
345
|
+
this.releaseRowTime(key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Drop a row-time plan and its source-base index entry (DROP / schema-change / re-register). */
|
|
349
|
+
releaseRowTime(key) {
|
|
350
|
+
const plan = this.rowTime.get(key);
|
|
351
|
+
if (!plan)
|
|
352
|
+
return;
|
|
353
|
+
this.rowTime.delete(key);
|
|
354
|
+
for (const base of planSourceBases(plan)) {
|
|
355
|
+
const set = this.rowTimeBySource.get(base);
|
|
356
|
+
if (set) {
|
|
357
|
+
set.delete(key);
|
|
358
|
+
if (set.size === 0)
|
|
359
|
+
this.rowTimeBySource.delete(base);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/* ──────────────────── convergence ordering ──────────────────── */
|
|
364
|
+
/**
|
|
365
|
+
* The source bases (lowercased `schema.table`) an MV's body reads — the
|
|
366
|
+
* dependency edges {@link Database.refreshAllMaterializedViews} orders the
|
|
367
|
+
* convergence sweep on. A registered (live) MV reports its compiled plan's
|
|
368
|
+
* bases ({@link planSourceBases} — the same set `rowTimeBySource` indexes it
|
|
369
|
+
* under). A **stale** MV has no live plan (a body-relevant source change
|
|
370
|
+
* released it), so its bases come from the recorded
|
|
371
|
+
* {@link import('../schema/derivation.js').TableDerivation.sourceTables} — the
|
|
372
|
+
* body's source-table set captured at (re)registration and kept current
|
|
373
|
+
* through every reshape. That recorded set is identical to what re-planning
|
|
374
|
+
* the body would derive (the create/refresh path fills it from the same
|
|
375
|
+
* analysis), but never re-plans a stale body that may no longer plan — so the
|
|
376
|
+
* ordering pass cannot throw a planning error before the per-MV refresh
|
|
377
|
+
* surfaces the real staleness diagnostic.
|
|
378
|
+
*/
|
|
379
|
+
sourceBasesFor(mv) {
|
|
380
|
+
const plan = this.rowTime.get(mvKey(mv.schemaName, mv.name));
|
|
381
|
+
return plan ? planSourceBases(plan) : mv.derivation.sourceTables;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* All maintained tables in **source-dependency order**: a base MV precedes
|
|
385
|
+
* every MV whose body reads it (MV-over-MV — in the unified model a base MV's
|
|
386
|
+
* backing is a table under its own name, so a dependent's
|
|
387
|
+
* {@link sourceBasesFor} contains that qualified name). A sequential refresh
|
|
388
|
+
* sweep over this order is correct because refresh is commit-first per MV: a
|
|
389
|
+
* base MV's backing commits before a dependent's body re-reads it
|
|
390
|
+
* ({@link Database.refreshAllMaterializedViews}).
|
|
391
|
+
*
|
|
392
|
+
* Edges are `sourceBasesFor(mv)` intersected with the MV-key set (a non-MV
|
|
393
|
+
* source is no ordering constraint); Kahn's algorithm produces the order.
|
|
394
|
+
* Throws {@link StatusCode.INTERNAL} on a cycle — the create-time gates
|
|
395
|
+
* (`assertNoSelfReference` / `assertNoDerivationCycle`) reject recursive MVs,
|
|
396
|
+
* so a cycle here is an impossible-state backstop, never a silently dropped MV.
|
|
397
|
+
*/
|
|
398
|
+
materializedViewRefreshOrder() {
|
|
399
|
+
const mvs = this.ctx.schemaManager.getAllMaintainedTables();
|
|
400
|
+
const byKey = new Map();
|
|
401
|
+
for (const mv of mvs)
|
|
402
|
+
byKey.set(mvKey(mv.schemaName, mv.name), mv);
|
|
403
|
+
// Prerequisite count (in-degree) + reverse adjacency (base → consumers).
|
|
404
|
+
const indegree = new Map();
|
|
405
|
+
const consumers = new Map();
|
|
406
|
+
for (const key of byKey.keys()) {
|
|
407
|
+
indegree.set(key, 0);
|
|
408
|
+
consumers.set(key, []);
|
|
409
|
+
}
|
|
410
|
+
for (const mv of mvs) {
|
|
411
|
+
const key = mvKey(mv.schemaName, mv.name);
|
|
412
|
+
const prereqs = new Set();
|
|
413
|
+
for (const base of this.sourceBasesFor(mv)) {
|
|
414
|
+
const baseKey = base.toLowerCase();
|
|
415
|
+
// A non-MV source is no ordering constraint; a self-edge is impossible
|
|
416
|
+
// (create-time gate) — skip both, and dedup so a body reading a base
|
|
417
|
+
// twice adds one edge.
|
|
418
|
+
if (baseKey === key || !byKey.has(baseKey) || prereqs.has(baseKey))
|
|
419
|
+
continue;
|
|
420
|
+
prereqs.add(baseKey);
|
|
421
|
+
consumers.get(baseKey).push(key);
|
|
422
|
+
indegree.set(key, indegree.get(key) + 1);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Kahn: drain zero-in-degree keys in catalog-enumeration order (stable).
|
|
426
|
+
const order = [];
|
|
427
|
+
const ready = [];
|
|
428
|
+
for (const key of byKey.keys())
|
|
429
|
+
if (indegree.get(key) === 0)
|
|
430
|
+
ready.push(key);
|
|
431
|
+
while (ready.length > 0) {
|
|
432
|
+
const key = ready.shift();
|
|
433
|
+
order.push(byKey.get(key));
|
|
434
|
+
for (const dep of consumers.get(key)) {
|
|
435
|
+
const next = indegree.get(dep) - 1;
|
|
436
|
+
indegree.set(dep, next);
|
|
437
|
+
if (next === 0)
|
|
438
|
+
ready.push(dep);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (order.length !== mvs.length) {
|
|
442
|
+
throw new QuereusError(`materialized-view convergence ordering found a dependency cycle among maintained tables `
|
|
443
|
+
+ `(ordered ${order.length} of ${mvs.length}) — recursive materialized views are rejected at create time`, StatusCode.INTERNAL);
|
|
444
|
+
}
|
|
445
|
+
return order;
|
|
446
|
+
}
|
|
447
|
+
/* ──────────────────── coarsening collision telemetry ──────────────────── */
|
|
448
|
+
/**
|
|
449
|
+
* Precompute the weakened-K′-column watch list for row-time collision telemetry —
|
|
450
|
+
* one entry per coarsening column of the MV's coarsened backing key. Returns
|
|
451
|
+
* `undefined` (the zero-overhead gate) unless `mv.derivation.coarsenedKey` is
|
|
452
|
+
* stamped with ≥1 weakened column: a provable-key or refining-lineage-key MV builds
|
|
453
|
+
* no watch, so {@link detectAndReportCoarseningCollisions} short-circuits and the
|
|
454
|
+
* maintenance path is untouched. Each weakened column name resolves to its backing
|
|
455
|
+
* column index via `mv.columnIndexMap` (the maintained table IS the backing table),
|
|
456
|
+
* carrying the source → output collations the divergence test needs.
|
|
457
|
+
*/
|
|
458
|
+
buildCoarseningWatch(mv) {
|
|
459
|
+
const coarsened = mv.derivation.coarsenedKey;
|
|
460
|
+
if (!coarsened || coarsened.weakened.length === 0)
|
|
461
|
+
return undefined;
|
|
462
|
+
const watch = [];
|
|
463
|
+
for (const w of coarsened.weakened) {
|
|
464
|
+
const index = mv.columnIndexMap.get(w.column.toLowerCase());
|
|
465
|
+
// Defensive: a weakened name that does not resolve to a backing column would
|
|
466
|
+
// be a derivation/stamp inconsistency — skip it rather than mis-key the read.
|
|
467
|
+
if (index === undefined) {
|
|
468
|
+
log("Coarsening watch: weakened column '%s' not found on backing %s.%s; skipping", w.column, mv.schemaName, mv.name);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
watch.push({
|
|
472
|
+
index,
|
|
473
|
+
sourceCollation: w.sourceCollation,
|
|
474
|
+
outputCollation: w.outputCollation,
|
|
475
|
+
column: w.column,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return watch.length > 0 ? watch : undefined;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Observe-only row-time collision telemetry: scan the **realized**
|
|
482
|
+
* {@link BackingRowChange}s a maintenance apply produced and queue a
|
|
483
|
+
* {@link MaintenanceCollisionEvent} for each one that is a key-coarsening collision —
|
|
484
|
+
* an `update` whose replaced backing row came from a **distinct source identity**
|
|
485
|
+
* than the incoming row's, merged under the coarsened backing key K′ (last-writer-win).
|
|
486
|
+
*
|
|
487
|
+
* **Zero-overhead gate.** Returns immediately unless `plan.coarseningWatch` is present
|
|
488
|
+
* (only a coarsened-key MV builds one). A non-coarsened MV never scans `backingChanges`.
|
|
489
|
+
*
|
|
490
|
+
* **Criterion.** For each `'update'` change, a weakened K′ column is *diverged* when its
|
|
491
|
+
* old/new backing values differ under the **source** (pre-coarsening, stricter) collation.
|
|
492
|
+
* An `update` here means the incoming row landed on an existing backing row sharing K′
|
|
493
|
+
* under the **output** collation (that is what made the upsert replacing, not inserting);
|
|
494
|
+
* if those rows are equal under the source collation it is the same source row's value
|
|
495
|
+
* being updated (e.g. an `email` change — not reported), and if they differ under the
|
|
496
|
+
* source collation two distinct source identities (`'Bob'`/`'bob'`) collapsed onto one
|
|
497
|
+
* backing key (reported). `insert`/`delete` changes are never collisions (new key / removal).
|
|
498
|
+
*
|
|
499
|
+
* Runs **independently** of the cascade — it neither consumes nor reorders the
|
|
500
|
+
* `backingChanges` routed onward (observe-only), so an MV-over-MV chain is unperturbed.
|
|
501
|
+
* The queued event rides the emitter's transaction batching, so a collision inside a
|
|
502
|
+
* rolled-back transaction reports nothing and does not increment the counter.
|
|
503
|
+
*/
|
|
504
|
+
detectAndReportCoarseningCollisions(plan, backingChanges) {
|
|
505
|
+
const watch = plan.coarseningWatch;
|
|
506
|
+
if (!watch)
|
|
507
|
+
return;
|
|
508
|
+
const coarsened = plan.mv.derivation.coarsenedKey;
|
|
509
|
+
if (!coarsened)
|
|
510
|
+
return; // defensive — a watch implies a stamped coarsenedKey
|
|
511
|
+
// K′ key column indices (ALL key columns, in key order) for the event payload's `key`.
|
|
512
|
+
// Resolved once for the whole batch; collisions are rare so this is off the hot path.
|
|
513
|
+
const keyIndices = coarsened.columns.map(name => plan.mv.columnIndexMap.get(name.toLowerCase()) ?? -1);
|
|
514
|
+
const emitter = this.ctx.getEventEmitter();
|
|
515
|
+
for (const change of backingChanges) {
|
|
516
|
+
if (change.op !== 'update')
|
|
517
|
+
continue;
|
|
518
|
+
const weakenedColumns = [];
|
|
519
|
+
for (const w of watch) {
|
|
520
|
+
if (compareSqlValues(change.oldRow[w.index], change.newRow[w.index], w.sourceCollation) !== 0) {
|
|
521
|
+
weakenedColumns.push(w.column);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (weakenedColumns.length === 0)
|
|
525
|
+
continue;
|
|
526
|
+
const event = {
|
|
527
|
+
schemaName: plan.backingSchema,
|
|
528
|
+
tableName: plan.backingTableName,
|
|
529
|
+
key: keyIndices.map(i => change.newRow[i]),
|
|
530
|
+
weakenedColumns,
|
|
531
|
+
oldRow: change.oldRow,
|
|
532
|
+
newRow: change.newRow,
|
|
533
|
+
};
|
|
534
|
+
emitter.queueCollision(event);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/* ──────────────────── row-time write-through ──────────────────── */
|
|
538
|
+
/**
|
|
539
|
+
* True iff a row-time covering structure reads `sourceBase` (lowercased
|
|
540
|
+
* `schema.table`). The DML write boundary consults this synchronously so the
|
|
541
|
+
* per-row maintenance hook is a zero-allocation no-op when nothing depends on
|
|
542
|
+
* the written table.
|
|
543
|
+
*/
|
|
544
|
+
hasRowTimePlanFor(sourceBase) {
|
|
545
|
+
const set = this.rowTimeBySource.get(sourceBase.toLowerCase());
|
|
546
|
+
return set !== undefined && set.size > 0;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Synchronously maintain every row-time covering structure on `sourceBase` for
|
|
550
|
+
* one source row-write. Each plan computes the per-row backing delta (a pure
|
|
551
|
+
* projection of the changed row) and applies it to the backing table's pending
|
|
552
|
+
* transaction layer through the connection a `select` from the MV would use —
|
|
553
|
+
* so the write is visible mid-transaction and rides the coordinated commit.
|
|
554
|
+
*
|
|
555
|
+
* **MV-over-MV cascade.** A backing write is itself a row-write that every MV
|
|
556
|
+
* reading *that backing table* must see. When a plan's backing base has its own
|
|
557
|
+
* dependents (`rowTimeBySource[backingBase]` non-empty), each effective
|
|
558
|
+
* {@link BackingRowChange} the write produced is routed back through this method,
|
|
559
|
+
* recursively. The dependency graph is acyclic (a consumer MV requires its
|
|
560
|
+
* producer MV to already exist at create time), so this synchronous depth-first
|
|
561
|
+
* recursion is DAG-ordered — a producer's backing is fully written before its
|
|
562
|
+
* consumers run — and the whole chain commits/rolls-back atomically on the live
|
|
563
|
+
* transaction. The leaf fast path (`!rowTimeBySource.has(backingBase)`) keeps a
|
|
564
|
+
* non-chained MV at exactly today's cost (one map lookup, no recursion). `depth`
|
|
565
|
+
* feeds the structural-cycle backstop in {@link assertCascadeDepth}.
|
|
566
|
+
*
|
|
567
|
+
* `cache` is the optional per-statement {@link BackingConnectionCache}: when the
|
|
568
|
+
* DML boundary supplies one, every backing (this plan's and each cascade level's)
|
|
569
|
+
* resolves its connection at most once for the whole statement. The cascade threads
|
|
570
|
+
* the same cache through, so a multi-level chain amortizes each level's resolution
|
|
571
|
+
* too. Omitted by the cold enforcement/eviction callers, which re-resolve the same
|
|
572
|
+
* connection deterministically.
|
|
573
|
+
*
|
|
574
|
+
* `deferred` is the optional per-statement deferred-rebuild set (MV keys). A
|
|
575
|
+
* `'full-rebuild'` plan re-evaluates the WHOLE body, so applying it per source row is
|
|
576
|
+
* O(rows × body) — pathological. When the DML boundary supplies a `deferred` set, a
|
|
577
|
+
* full-rebuild plan is instead marked dirty here (no per-row apply) and rebuilt exactly
|
|
578
|
+
* once at the end-of-statement {@link flushDeferredRebuilds} boundary. The bounded-delta
|
|
579
|
+
* arms stay per-row-immediate (cheap, and the covering-UNIQUE enforcement scan depends on
|
|
580
|
+
* their per-row backing visibility; a full-rebuild MV is never a covering structure, so
|
|
581
|
+
* deferring it cannot starve that scan). A cold caller without a `deferred` set falls
|
|
582
|
+
* through to an inline rebuild — a safe, unamortized fallback that the
|
|
583
|
+
* enforcement/eviction callers never actually reach (they never name a full-rebuild MV).
|
|
584
|
+
*/
|
|
585
|
+
async maintainRowTime(sourceBase, change, cache, deferred, depth = 0) {
|
|
586
|
+
const changedBase = sourceBase.toLowerCase();
|
|
587
|
+
const keys = this.rowTimeBySource.get(changedBase);
|
|
588
|
+
if (!keys || keys.size === 0)
|
|
589
|
+
return;
|
|
590
|
+
for (const key of keys) {
|
|
591
|
+
const plan = this.rowTime.get(key);
|
|
592
|
+
if (!plan)
|
|
593
|
+
continue;
|
|
594
|
+
// Full-rebuild is the one deferred arm — mark dirty and drain at flush.
|
|
595
|
+
if (plan.kind === 'full-rebuild' && deferred) {
|
|
596
|
+
deferred.add(key);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const backingChanges = await this.applyMaintenancePlan(plan, change, changedBase, cache);
|
|
600
|
+
if (backingChanges.length === 0)
|
|
601
|
+
continue;
|
|
602
|
+
// Row-time coarsening collision telemetry: observe-only over the realized
|
|
603
|
+
// delta (gated on `coarseningWatch` — a no-op for a non-coarsened MV). Runs
|
|
604
|
+
// independently of the cascade below; it neither consumes nor reorders the
|
|
605
|
+
// backing changes routed onward.
|
|
606
|
+
this.detectAndReportCoarseningCollisions(plan, backingChanges);
|
|
607
|
+
// Declared CHECK / child-side FK over the rows this delta wrote — BEFORE
|
|
608
|
+
// cascading, so a consumer never consumes an invalid producer row. Every
|
|
609
|
+
// row already in the backing was validated when it entered (the bulk
|
|
610
|
+
// validation at create/attach seeds the induction), so only the delta is
|
|
611
|
+
// validated. No-op (`undefined`) for a constraint-less table.
|
|
612
|
+
if (plan.derivedRowValidator) {
|
|
613
|
+
await this.validateDerivedChanges(plan, plan.derivedRowValidator, backingChanges, cache);
|
|
614
|
+
}
|
|
615
|
+
// Parent-side referential enforcement: this maintenance delete/key-update of an
|
|
616
|
+
// `M` row may orphan rows in an ordinary table `C` whose FK references `M`. Fire
|
|
617
|
+
// the shared engine over the backing delta — RESTRICT-walk then declared actions —
|
|
618
|
+
// after `M`'s own image is validated, before the MV-over-MV cascade. Runs whether
|
|
619
|
+
// or not `M` has MV consumers (placed before the leaf fast-path).
|
|
620
|
+
await this.enforceParentSideReferentialActions(plan, backingChanges);
|
|
621
|
+
const backingBase = `${plan.backingSchema}.${plan.backingTableName}`.toLowerCase();
|
|
622
|
+
if (!this.rowTimeBySource.has(backingBase))
|
|
623
|
+
continue; // leaf — no dependents
|
|
624
|
+
this.assertCascadeDepth(depth + 1, backingBase);
|
|
625
|
+
for (const bc of backingChanges) {
|
|
626
|
+
await this.maintainRowTime(backingBase, bc, cache, deferred, depth + 1);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Flush the per-statement deferred full-rebuild set at the end-of-statement boundary:
|
|
632
|
+
* rebuild every dirtied full-rebuild MV exactly once (not once per source row) and
|
|
633
|
+
* cascade each rebuild's effective {@link BackingRowChange}(s) onward so MV-over-MV
|
|
634
|
+
* consumers converge.
|
|
635
|
+
*
|
|
636
|
+
* Drained as a worklist over the producer→consumer DAG. Each rebuild calls
|
|
637
|
+
* {@link applyFullRebuild} (re-run the whole body against live mid-transaction source
|
|
638
|
+
* state → a `'replace-all'` diff) and routes the realized delta back through
|
|
639
|
+
* {@link maintainRowTime} with the SAME `deferred` set: an incremental consumer applies
|
|
640
|
+
* inline; a full-rebuild consumer re-dirties into the drain (rebuilt in a later round,
|
|
641
|
+
* after its producer's delta has landed). The drain proceeds in **rounds** — each round
|
|
642
|
+
* snapshots the current dirty set, clears it, and rebuilds each member, collecting the
|
|
643
|
+
* next round's re-dirties — so a consumer is never permanently stale (a producer rebuilt
|
|
644
|
+
* in the same round re-dirties it for the next), and convergence takes at most one round
|
|
645
|
+
* per level of the full-rebuild sub-DAG.
|
|
646
|
+
*
|
|
647
|
+
* Termination: the dependency DAG is acyclic (a consumer MV requires its producer to
|
|
648
|
+
* pre-exist), so the longest full-rebuild chain — hence the round count — is bounded by
|
|
649
|
+
* the registered-row-time-MV count. Exceeding it signals a structurally-impossible cycle
|
|
650
|
+
* and fails loud ({@link assertFlushRounds}) — the worklist analogue of
|
|
651
|
+
* {@link assertCascadeDepth}. This should never fire.
|
|
652
|
+
*
|
|
653
|
+
* The DML executor calls this INSIDE the statement-atomicity savepoint (after the row
|
|
654
|
+
* loop, before the savepoint release), so a failed rebuild rolls the whole statement
|
|
655
|
+
* back. An empty set is a no-op (no overhead on statements touching no full-rebuild MV).
|
|
656
|
+
*/
|
|
657
|
+
async flushDeferredRebuilds(deferred, cache) {
|
|
658
|
+
let round = 0;
|
|
659
|
+
while (deferred.size > 0) {
|
|
660
|
+
this.assertFlushRounds(++round);
|
|
661
|
+
const batch = [...deferred];
|
|
662
|
+
deferred.clear();
|
|
663
|
+
for (const key of batch) {
|
|
664
|
+
const plan = this.rowTime.get(key);
|
|
665
|
+
// Only full-rebuild plans are ever deferred; a non-full-rebuild key (or a
|
|
666
|
+
// plan released mid-flush) is a no-op. Defensive — `maintainRowTime` only
|
|
667
|
+
// ever adds `'full-rebuild'` keys.
|
|
668
|
+
if (!plan || plan.kind !== 'full-rebuild')
|
|
669
|
+
continue;
|
|
670
|
+
const backingChanges = await this.applyFullRebuild(plan, cache);
|
|
671
|
+
if (backingChanges.length === 0)
|
|
672
|
+
continue;
|
|
673
|
+
// Coarsening collision telemetry over the rebuild diff — the full-rebuild
|
|
674
|
+
// floor's collation-keyed `replace-all` realizes the same LWW merge as the
|
|
675
|
+
// bounded-delta arms (observe-only; gated on `coarseningWatch`).
|
|
676
|
+
this.detectAndReportCoarseningCollisions(plan, backingChanges);
|
|
677
|
+
// Validate the rebuild diff's written images at the flush boundary —
|
|
678
|
+
// the full-rebuild analogue of the per-row validation in
|
|
679
|
+
// {@link maintainRowTime} (deferred-rebuild semantics preserved: a bulk
|
|
680
|
+
// source write fails once at end-of-statement, not per source row).
|
|
681
|
+
if (plan.derivedRowValidator) {
|
|
682
|
+
await this.validateDerivedChanges(plan, plan.derivedRowValidator, backingChanges, cache);
|
|
683
|
+
}
|
|
684
|
+
// Parent-side referential enforcement for the rebuild diff's deletes/key-updates,
|
|
685
|
+
// fired inside the statement-atomicity savepoint (the flush runs before its
|
|
686
|
+
// release) so a RESTRICT failure or cascade error unwinds the whole statement.
|
|
687
|
+
await this.enforceParentSideReferentialActions(plan, backingChanges);
|
|
688
|
+
const backingBase = `${plan.backingSchema}.${plan.backingTableName}`.toLowerCase();
|
|
689
|
+
if (!this.rowTimeBySource.has(backingBase))
|
|
690
|
+
continue; // leaf — no dependents
|
|
691
|
+
for (const bc of backingChanges) {
|
|
692
|
+
// Cascade at depth 0: an incremental consumer applies inline (its own
|
|
693
|
+
// `assertCascadeDepth` backstops that recursion); a full-rebuild consumer
|
|
694
|
+
// re-dirties `deferred` for the next round.
|
|
695
|
+
await this.maintainRowTime(backingBase, bc, cache, deferred);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Round backstop for {@link flushDeferredRebuilds}. The full-rebuild sub-DAG is acyclic,
|
|
702
|
+
* so the drain converges in at most one round per chain level — bounded by the row-time
|
|
703
|
+
* MV count. A round count beyond that (`+1` slack for an initial dirty set already
|
|
704
|
+
* spanning multiple levels) signals a structural impossibility (a cycle) — fail loud
|
|
705
|
+
* rather than spin. This should never fire.
|
|
706
|
+
*/
|
|
707
|
+
assertFlushRounds(round) {
|
|
708
|
+
if (round > this.rowTime.size + 1) {
|
|
709
|
+
throw new QuereusError(`materialized-view deferred-rebuild flush exceeded maximum rounds (${this.rowTime.size + 1}) — `
|
|
710
|
+
+ `a row-time dependency cycle should be structurally impossible`, StatusCode.INTERNAL);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Defense-in-depth backstop for the cascade. Cycles are structurally impossible
|
|
715
|
+
* (a consumer MV can only be created once its producer exists, and an MV's source
|
|
716
|
+
* set is fixed at create), so a valid chain descends at most once per registered
|
|
717
|
+
* row-time MV. A depth beyond that count signals a structural impossibility (a
|
|
718
|
+
* cycle) — fail loud with `INTERNAL` naming the backing base rather than overflow
|
|
719
|
+
* the stack. This should never fire.
|
|
720
|
+
*/
|
|
721
|
+
assertCascadeDepth(depth, backingBase) {
|
|
722
|
+
if (depth > this.rowTime.size) {
|
|
723
|
+
throw new QuereusError(`materialized-view cascade exceeded maximum depth (${this.rowTime.size}) at backing `
|
|
724
|
+
+ `'${backingBase}' — a row-time dependency cycle should be structurally impossible`, StatusCode.INTERNAL);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Dispatch a maintenance plan on its `kind`, compute the per-row backing delta,
|
|
729
|
+
* apply it, and return the **effective** {@link BackingRowChange}(s) the backing
|
|
730
|
+
* layer realized (so the cascade can drive this plan's own dependents). The builder
|
|
731
|
+
* yields `'inverse-projection'` (covering-index shape), `'residual-recompute'`
|
|
732
|
+
* (single-source aggregate), `'prefix-delete'` (single-source lateral-TVF fan-out), and
|
|
733
|
+
* `'full-rebuild'` (the floor — re-evaluate the whole body and replace the backing). The
|
|
734
|
+
* floor ignores the specific `change` (it rebuilds wholesale); the others derive a
|
|
735
|
+
* bounded per-row delta from it.
|
|
736
|
+
*/
|
|
737
|
+
async applyMaintenancePlan(plan, change, changedBase, cache) {
|
|
738
|
+
switch (plan.kind) {
|
|
739
|
+
case 'inverse-projection':
|
|
740
|
+
return this.applyInverseProjection(plan, change, cache);
|
|
741
|
+
case 'residual-recompute':
|
|
742
|
+
return this.applyForwardResidual(plan, change, cache);
|
|
743
|
+
case 'prefix-delete':
|
|
744
|
+
return this.applyPrefixDelete(plan, change, cache);
|
|
745
|
+
case 'join-residual':
|
|
746
|
+
return this.applyJoinResidual(plan, change, changedBase, cache);
|
|
747
|
+
case 'full-rebuild':
|
|
748
|
+
return this.applyFullRebuild(plan, cache);
|
|
749
|
+
default: {
|
|
750
|
+
// A new arm added to MaintenancePlan must extend this dispatch; the
|
|
751
|
+
// never-assignment makes that a compile error rather than a silent
|
|
752
|
+
// fall-through (noImplicitReturns is off in this package).
|
|
753
|
+
const exhaustiveCheck = plan;
|
|
754
|
+
throw new QuereusError(`unknown maintenance plan kind: ${exhaustiveCheck.kind}`, StatusCode.INTERNAL);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Compute an `'inverse-projection'` plan's per-row backing delta, apply it, and
|
|
760
|
+
* return the **effective** {@link BackingRowChange}(s) the backing layer realized.
|
|
761
|
+
* An out-of-scope row (or a delete of an absent backing key) yields no change. This
|
|
762
|
+
* body is the shipped covering-index maintenance, lifted verbatim from the former
|
|
763
|
+
* `applyRowTimeChange`, plus the equal-image short-circuit: an UPDATE whose old and
|
|
764
|
+
* new projected images are value-identical (both in scope) projects to NO backing
|
|
765
|
+
* delta — the dominant no-op echo (a source update touching only unprojected columns,
|
|
766
|
+
* or rewriting a projected column to its existing value) is suppressed before any
|
|
767
|
+
* backing-connection work. Accurate by the maintenance invariant (the backing row IS
|
|
768
|
+
* the old image's projection), so nothing would have changed; the host's
|
|
769
|
+
* value-identical upsert skip (vtab/backing-host.ts) remains the effective-state
|
|
770
|
+
* backstop for the paths that do emit ops.
|
|
771
|
+
*/
|
|
772
|
+
async applyInverseProjection(plan, change, cache) {
|
|
773
|
+
const inScope = (row) => plan.predicate === undefined || plan.predicate.evaluate(row) === true;
|
|
774
|
+
const project = (row) => plan.projectors.map(p => p.kind === 'passthrough' ? row[p.sourceCol] : p.eval(row));
|
|
775
|
+
const keyOf = (backingRow) => buildPrimaryKeyFromValues(plan.backingPkDefinition.map(d => backingRow[d.index]), plan.backingPkDefinition);
|
|
776
|
+
const ops = [];
|
|
777
|
+
if (change.op === 'insert') {
|
|
778
|
+
if (inScope(change.newRow))
|
|
779
|
+
ops.push({ kind: 'upsert', row: project(change.newRow) });
|
|
780
|
+
}
|
|
781
|
+
else if (change.op === 'delete') {
|
|
782
|
+
if (inScope(change.oldRow))
|
|
783
|
+
ops.push({ kind: 'delete-key', key: keyOf(project(change.oldRow)) });
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
// UPDATE: a both-in-scope, same-backing-key change is one upsert (the host
|
|
787
|
+
// reports a single `update`); otherwise delete the old image if it was in
|
|
788
|
+
// scope and upsert the new image if it is — predicate-scope transitions and
|
|
789
|
+
// key-changing updates are genuinely two-sided. The scope check reads the
|
|
790
|
+
// SOURCE row (the predicate may reference unprojected columns), so both
|
|
791
|
+
// images must be in scope for the equal-image short-circuit.
|
|
792
|
+
const oldIn = inScope(change.oldRow);
|
|
793
|
+
const newIn = inScope(change.newRow);
|
|
794
|
+
if (oldIn && newIn) {
|
|
795
|
+
const oldImage = project(change.oldRow);
|
|
796
|
+
const newImage = project(change.newRow);
|
|
797
|
+
// Byte-faithful identity (rowsValueIdentical): subsumes key equality, and a
|
|
798
|
+
// collation-equal / byte-different image is NOT suppressed (it must re-key
|
|
799
|
+
// the stored bytes) — the same discipline as the host-level upsert skip.
|
|
800
|
+
if (rowsValueIdentical(oldImage, newImage))
|
|
801
|
+
return [];
|
|
802
|
+
if (this.backingPkEqual(plan.backingPkDefinition, oldImage, newImage)) {
|
|
803
|
+
// Same backing key (collation-aware — a collation-equal / byte-different
|
|
804
|
+
// key is the SAME btree identity, and the upsert re-keys the stored
|
|
805
|
+
// bytes): one upsert replaces the row wholesale, so the host reports
|
|
806
|
+
// a single `update` — matching the residual arms' post-suppression
|
|
807
|
+
// shape (one cascade dispatch, one change-log entry, no secondary-index
|
|
808
|
+
// churn from a delete+insert at an unchanged key).
|
|
809
|
+
ops.push({ kind: 'upsert', row: newImage });
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
ops.push({ kind: 'delete-key', key: keyOf(oldImage) });
|
|
813
|
+
ops.push({ kind: 'upsert', row: newImage });
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
if (oldIn)
|
|
818
|
+
ops.push({ kind: 'delete-key', key: keyOf(project(change.oldRow)) });
|
|
819
|
+
if (newIn)
|
|
820
|
+
ops.push({ kind: 'upsert', row: project(change.newRow) });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (ops.length === 0)
|
|
824
|
+
return [];
|
|
825
|
+
const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
826
|
+
if (!backing) {
|
|
827
|
+
throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
|
|
828
|
+
}
|
|
829
|
+
const host = this.backingHost(backing);
|
|
830
|
+
const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
|
|
831
|
+
return host.applyMaintenance(connection, ops);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Validate the row images a maintenance apply WROTE (insert/update
|
|
835
|
+
* {@link BackingRowChange}s — a delete writes no image) against the plan's
|
|
836
|
+
* compiled {@link DerivedRowConstraintValidator}. Inline checks abort the
|
|
837
|
+
* writing statement with the maintained-table-attributed CONSTRAINT error;
|
|
838
|
+
* auto-deferred checks (subquery CHECK, every child-side FK) queue to the
|
|
839
|
+
* deferred-constraint queue and validate at commit. Deferred entries are
|
|
840
|
+
* pinned to the backing connection the maintenance write used (resolved from
|
|
841
|
+
* the per-statement cache, or re-resolved deterministically — the same
|
|
842
|
+
* connection either way) so commit-time evaluation reads the same pending
|
|
843
|
+
* state, mirroring the DML pipeline's active-connection capture.
|
|
844
|
+
*/
|
|
845
|
+
async validateDerivedChanges(plan, validator, changes, cache) {
|
|
846
|
+
let connectionId;
|
|
847
|
+
if (validator.checks.some(c => c.needsDeferred)) {
|
|
848
|
+
const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
849
|
+
if (backing) {
|
|
850
|
+
const host = this.backingHost(backing);
|
|
851
|
+
const conn = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
|
|
852
|
+
connectionId = conn.connectionId;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
for (const change of changes) {
|
|
856
|
+
if (change.op === 'delete')
|
|
857
|
+
continue;
|
|
858
|
+
await validateDerivedRowImage(this.ctx, validator, change.newRow, connectionId);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Fire **parent-side** referential enforcement over the backing rows a maintenance
|
|
863
|
+
* apply REMOVED or re-keyed (delete / key-update {@link BackingRowChange}s — an insert
|
|
864
|
+
* has no parent-side action). When the maintained table `M` is the PARENT (FK target)
|
|
865
|
+
* of an FK declared on an ordinary table `C` (`create table C (… references M(col) …)`),
|
|
866
|
+
* a maintenance-driven delete/key-update of the referenced `M` row would silently orphan
|
|
867
|
+
* `C`'s rows, bypassing the declared RESTRICT / referential action. This is the
|
|
868
|
+
* **dual** of {@link validateDerivedChanges} (constraints declared *on* `M`); the FK here
|
|
869
|
+
* lives on `C` and references `M`, so it is invisible to `M`'s own plan/validator.
|
|
870
|
+
*
|
|
871
|
+
* It reuses the SAME shared referential-action engine the DML executor and the
|
|
872
|
+
* external-change ingestion seam use — no third copy — applying its two functions over
|
|
873
|
+
* each backing change exactly as `database-external-changes.ts` does:
|
|
874
|
+
* - {@link assertTransitiveRestrictsForParentMutation} — pre-walk the transitive cascade
|
|
875
|
+
* closure and throw a CONSTRAINT error naming `M` on any surviving RESTRICT child;
|
|
876
|
+
* - {@link executeForeignKeyActionsAndLens} — run declared CASCADE / SET NULL / SET DEFAULT,
|
|
877
|
+
* re-entering the DML executor (the already-holding-the-mutex variant) for each cascaded
|
|
878
|
+
* child write, so `C`'s own constraints, watches, nested cascades, and (if `C` is itself
|
|
879
|
+
* an MV source) its own maintenance all fire.
|
|
880
|
+
*
|
|
881
|
+
* Ordering: called AFTER the backing delta has landed in the pending layer (the RESTRICT
|
|
882
|
+
* walk runs POST-application — the child rows it keys off still exist because the cascade
|
|
883
|
+
* has not run yet) and AFTER `M`'s own image is validated, matching the DML executor's
|
|
884
|
+
* per-change order (capture → MV maintenance → FK actions) and the external-changes seam.
|
|
885
|
+
* `lensRouted = false`: a maintenance backing write is a physical basis write (maintained
|
|
886
|
+
* tables are not lens basis spines). A surviving RESTRICT throws up through
|
|
887
|
+
* {@link maintainRowTime} → the DML executor → the statement, rolling back the source write
|
|
888
|
+
* attributed to `M`.
|
|
889
|
+
*
|
|
890
|
+
* Gate: a cheap `foreign_keys`-pragma early-return keeps the pragma-off path free (the
|
|
891
|
+
* engine also early-returns, but skipping the `getTable` + loop avoids all per-change work).
|
|
892
|
+
* NOT gated on `plan.derivedRowValidator` — that gate is child-side (constraints *on* `M`);
|
|
893
|
+
* an inbound FK lives on `C` and leaves `M`'s plan untouched. Beyond the gate it fires
|
|
894
|
+
* unconditionally per delete/update change, but the engine no longer pays an `O(catalog)`
|
|
895
|
+
* scan: both calls route through `SchemaManager.getReferencingForeignKeys`, the precomputed
|
|
896
|
+
* reverse-FK index, so an `M` that nothing references resolves to the shared empty bucket and
|
|
897
|
+
* each call early-returns in O(1) — a maintained table with no inbound FK (the common case)
|
|
898
|
+
* pays only the pragma check plus one map lookup per delete/key-update change.
|
|
899
|
+
*/
|
|
900
|
+
async enforceParentSideReferentialActions(plan, changes) {
|
|
901
|
+
const db = this.ctx;
|
|
902
|
+
if (!db.options.getBooleanOption('foreign_keys'))
|
|
903
|
+
return; // cheap gate; engine early-returns too
|
|
904
|
+
// The backing `TableSchema` — same object validateDerivedChanges resolves; its `.name`
|
|
905
|
+
// equals `M`'s, so an FK on `C` (`references M`) matches the engine's referencing scan.
|
|
906
|
+
const parent = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
907
|
+
if (!parent)
|
|
908
|
+
return; // backing gone ⇒ MV already broken
|
|
909
|
+
for (const change of changes) {
|
|
910
|
+
if (change.op === 'insert')
|
|
911
|
+
continue; // inserts have no parent-side actions
|
|
912
|
+
await assertTransitiveRestrictsForParentMutation(db, parent, change.op, change.oldRow, change.newRow);
|
|
913
|
+
await executeForeignKeyActionsAndLens(db, parent, change.op, change.oldRow, change.newRow);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Resolve the {@link BackingHost} capability surface for a backing table —
|
|
918
|
+
* see `vtab/backing-host.ts` for the contract. The host is resolved fresh per
|
|
919
|
+
* use (a map lookup on the owning module), so a drop+recreate of the backing
|
|
920
|
+
* always yields the new incarnation's host.
|
|
921
|
+
*/
|
|
922
|
+
backingHost(backing) {
|
|
923
|
+
// The ctx IS the Database (same construction as buildMaintenancePlan's cast).
|
|
924
|
+
return resolveBackingHost(this.ctx, backing);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Obtain (lazily create + register) the backing table's
|
|
928
|
+
* {@link VirtualTableConnection} for the current transaction. Reuses the same
|
|
929
|
+
* connection a `select` from the MV resolves to (so reads-own-writes holds) —
|
|
930
|
+
* matched among the Database's registered connections by
|
|
931
|
+
* {@link BackingHost.ownsConnection}, which is pinned to the live backing
|
|
932
|
+
* incarnation; a freshly created connection is registered with the Database so
|
|
933
|
+
* the coordinated commit/rollback covers its pending state in lockstep with the
|
|
934
|
+
* source write.
|
|
935
|
+
*
|
|
936
|
+
* When an optional per-statement {@link BackingConnectionCache} is supplied, the
|
|
937
|
+
* scan over the Database's active connections (the dominant per-row cost on a bulk
|
|
938
|
+
* write) is paid once per (statement, backing): a hit returns the cached connection
|
|
939
|
+
* directly, and a miss caches whichever connection the scan resolves — or the one it
|
|
940
|
+
* lazily creates + registers. Caching the resolved/created connection is sound
|
|
941
|
+
* because the scan is deterministic within a statement (nothing interleaves between
|
|
942
|
+
* a statement's rows to change which connection a `select` from the MV picks), so the
|
|
943
|
+
* cache holds exactly what an uncached re-resolution would return.
|
|
944
|
+
*/
|
|
945
|
+
async getBackingConnection(host, qualifiedName, cache) {
|
|
946
|
+
const cacheKey = qualifiedName.toLowerCase();
|
|
947
|
+
const cached = cache?.get(cacheKey);
|
|
948
|
+
if (cached)
|
|
949
|
+
return cached;
|
|
950
|
+
for (const c of this.ctx.getConnectionsForTable(qualifiedName)) {
|
|
951
|
+
if (host.ownsConnection(c)) {
|
|
952
|
+
cache?.set(cacheKey, c);
|
|
953
|
+
return c;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const conn = host.connect();
|
|
957
|
+
await this.ctx.registerConnection(conn);
|
|
958
|
+
cache?.set(cacheKey, conn);
|
|
959
|
+
return conn;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Build the row-time maintenance plan for an MV — **cost-gated, with a floor, never a
|
|
963
|
+
* shape allowlist**. The builder tries to match a bounded-delta arm by shape
|
|
964
|
+
* ({@link tryBuildBoundedDeltaArm}); a body that matches **none** falls through to the
|
|
965
|
+
* always-correct {@link buildFullRebuildPlan} floor (re-evaluate the whole body, replace
|
|
966
|
+
* the backing transactionally). **No body is rejected for its shape.** Only four
|
|
967
|
+
* create-time rejections remain, all non-shape:
|
|
968
|
+
* - a **non-deterministic** body without `pragma nondeterministic_schema` — a hard reject
|
|
969
|
+
* in the matched arm (so the arm-specific determinism diagnostic survives) or, for a
|
|
970
|
+
* body matching no arm, in the floor's whole-body determinism check;
|
|
971
|
+
* - a **bag** (no provable unique key) — the floor's `keysOf` reject (a duplicate-producing
|
|
972
|
+
* body usually fails the set contract earlier, at create-fill);
|
|
973
|
+
* - a body with **no relational output**;
|
|
974
|
+
* - a **full-rebuild-only body over a source past the size threshold**
|
|
975
|
+
* ({@link isFullRebuildPathological}, the `materialized_view_rebuild_row_threshold` option).
|
|
976
|
+
*
|
|
977
|
+
* The single source may itself be another MV's backing table (an MV-over-MV body):
|
|
978
|
+
* `building/select.ts` rewrites a reference to `mv1` into a `TableReference` against
|
|
979
|
+
* `mv1`'s backing table, so the source base is `mv1`'s backing base and the same checks
|
|
980
|
+
* evaluate against the (keyed `memory`) backing schema unchanged. A write to `mv1` then
|
|
981
|
+
* drives `mv2` via the cascade in {@link maintainRowTime}.
|
|
982
|
+
*
|
|
983
|
+
* Eligibility is a *cost choice* among the body's structurally-sound strategies
|
|
984
|
+
* ({@link selectMaintenanceStrategy}): the bounded-delta arms are preferred by the argmin
|
|
985
|
+
* cost gate, and full-rebuild is selected exactly when no bounded-delta arm is sound (an
|
|
986
|
+
* empty sound set resolves to the floor) — so an existing eligible shape is unaffected.
|
|
987
|
+
*/
|
|
988
|
+
buildMaintenancePlan(mv) {
|
|
989
|
+
const db = this.ctx;
|
|
990
|
+
// Analyze the MV's own body to compile maintenance; suppress the read-side
|
|
991
|
+
// rewrite so the body stays over its SOURCE table, not re-pointed at this
|
|
992
|
+
// MV's backing (which the maintenance plan is what keeps consistent).
|
|
993
|
+
const analyzed = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
|
|
994
|
+
const { plan } = this.ctx._buildPlan([mv.derivation.selectAst]);
|
|
995
|
+
return this.ctx.optimizer.optimizeForAnalysis(plan, db);
|
|
996
|
+
});
|
|
997
|
+
// Replicable-determinism gate — host-conditional, inert by default. A backing host
|
|
998
|
+
// whose backing replicates across peers (the sync-store) sets
|
|
999
|
+
// `requiresReplicableDerivations` so a platform-dependent UDF or collation cannot
|
|
1000
|
+
// diverge peers. Checked here — after `analyzed`, before arm selection — so it applies
|
|
1001
|
+
// regardless of which maintenance arm wins, over the SAME analyzed plan the determinism
|
|
1002
|
+
// gate walks (nested calls, WHERE / GROUP BY / aggregate-arg / TVF-arg positions all
|
|
1003
|
+
// reached). It sits NEXT TO, not in place of, the determinism gate: the two are
|
|
1004
|
+
// orthogonal, so this is NOT lifted by `pragma nondeterministic_schema`. A memory/store
|
|
1005
|
+
// host leaves the flag undefined ⇒ this block is skipped ⇒ zero behavior change.
|
|
1006
|
+
// Idempotent (same body ⇒ same verdict), so it is also desirable on re-register /
|
|
1007
|
+
// catalog import: a tampered catalog cannot smuggle a non-replicable body past a
|
|
1008
|
+
// demanding host. Two gates of the same shape: a non-replicable FUNCTION (which the
|
|
1009
|
+
// body walk's function-bearing nodes carry) and a non-replicable COLLATION (which rides
|
|
1010
|
+
// each scalar node's resolved type plus the backing key's declared collations).
|
|
1011
|
+
// Resolve the host LENIENTLY: at the create-time gate registration of an
|
|
1012
|
+
// `alter table … set maintained` attach, a module that materializes its durable
|
|
1013
|
+
// backing late (lamina's `ensureBackingForAttach`, after this gate) has no host
|
|
1014
|
+
// yet. The host is used here ONLY for the host-conditional, default-inert
|
|
1015
|
+
// `requiresReplicableDerivations` gate — a host that sets it (the synced-store
|
|
1016
|
+
// flavor) always exists by plan-build time, so skipping the gate when the host
|
|
1017
|
+
// is absent never lets a non-replicable body slip past. The reconcile resolves
|
|
1018
|
+
// the host for real, and the maintenance arms re-resolve it per use.
|
|
1019
|
+
const host = tryResolveBackingHost(db, mv);
|
|
1020
|
+
if (host?.requiresReplicableDerivations) {
|
|
1021
|
+
const offendingFn = findNonReplicableFunction(analyzed);
|
|
1022
|
+
if (offendingFn)
|
|
1023
|
+
throw nonReplicableDerivationError(mv.name, offendingFn);
|
|
1024
|
+
const offendingCollation = findNonReplicableCollation(analyzed, mv, db);
|
|
1025
|
+
if (offendingCollation)
|
|
1026
|
+
throw nonReplicableCollationDerivationError(mv.name, offendingCollation);
|
|
1027
|
+
}
|
|
1028
|
+
// Try a bounded-delta arm; a shape that fits none falls through to the floor.
|
|
1029
|
+
const boundedDelta = this.tryBuildBoundedDeltaArm(mv, analyzed);
|
|
1030
|
+
return boundedDelta ?? this.buildFullRebuildPlan(mv, analyzed);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Route the analyzed body to the matching bounded-delta arm, or return `null` when its
|
|
1034
|
+
* shape fits **no** bounded-delta arm (the caller then builds the full-rebuild floor).
|
|
1035
|
+
* Each arm builder likewise returns `null` on a sub-shape mismatch and falls through
|
|
1036
|
+
* here. The arms keep only **determinism** as a hard reject (so their arm-specific
|
|
1037
|
+
* determinism diagnostic survives — see the individual builders); every other mismatch
|
|
1038
|
+
* is a `null` fall-through. Bag / no-output / size rejects live in the floor.
|
|
1039
|
+
*/
|
|
1040
|
+
tryBuildBoundedDeltaArm(mv, analyzed) {
|
|
1041
|
+
// A body that reads no source table has no bounded-delta arm → floor (which rejects
|
|
1042
|
+
// a sourceless body). (A self-join / TVF fan-out surfaces ≥2 refs or a TVF node.)
|
|
1043
|
+
const tableRefs = [...collectTableRefs(analyzed).values()];
|
|
1044
|
+
if (tableRefs.length === 0)
|
|
1045
|
+
return null;
|
|
1046
|
+
// Shapes no bounded-delta arm models — a window function reads across the partition,
|
|
1047
|
+
// set ops / recursive CTEs / DISTINCT / row caps are out of the bounded-delta model.
|
|
1048
|
+
// They are NOT rejected: a deterministic, keyed such body is maintained by the floor.
|
|
1049
|
+
if (containsNodeType(analyzed, PlanNodeType.Window))
|
|
1050
|
+
return null;
|
|
1051
|
+
if (containsNodeType(analyzed, PlanNodeType.Distinct))
|
|
1052
|
+
return null;
|
|
1053
|
+
if (containsNodeType(analyzed, PlanNodeType.SetOperation))
|
|
1054
|
+
return null;
|
|
1055
|
+
if (containsNodeType(analyzed, PlanNodeType.RecursiveCTE))
|
|
1056
|
+
return null;
|
|
1057
|
+
if (mv.derivation.selectAst.type === 'select' && (mv.derivation.selectAst.limit !== undefined || mv.derivation.selectAst.offset !== undefined)) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
const tableRef = tableRefs[0];
|
|
1061
|
+
const sourceSchema = tableRef.tableSchema;
|
|
1062
|
+
const sourceBase = `${sourceSchema.schemaName}.${sourceSchema.name}`.toLowerCase();
|
|
1063
|
+
// Single base source `T` joined to ONE lateral table-valued function — a fan-out
|
|
1064
|
+
// body (each base row produces N backing rows) → the prefix-delete arm. Routed
|
|
1065
|
+
// *before* the generic join branch below, because a lateral fan-out surfaces BOTH a
|
|
1066
|
+
// Join and a TableFunctionCall. A multi-base TVF body falls to the floor.
|
|
1067
|
+
if (containsNodeType(analyzed, PlanNodeType.TableFunctionCall)) {
|
|
1068
|
+
if (tableRefs.length !== 1)
|
|
1069
|
+
return null;
|
|
1070
|
+
return this.buildLateralTvfPrefixDeletePlan(mv, analyzed, tableRef, sourceBase);
|
|
1071
|
+
}
|
|
1072
|
+
// Any join → the provably-1:1 join-residual arm. A fanning (non-1:1) join, an outer
|
|
1073
|
+
// join, a >2-source join, an aggregate over a join, or a partial WHERE returns `null`
|
|
1074
|
+
// from the builder → floor. (The lateral-TVF fan-out above is matched first because
|
|
1075
|
+
// it also surfaces a join node.)
|
|
1076
|
+
if (containsAnyJoin(analyzed)) {
|
|
1077
|
+
return this.buildJoinResidualPlan(mv, analyzed, tableRefs);
|
|
1078
|
+
}
|
|
1079
|
+
// A non-join multi-source body (e.g. a WHERE-subquery over a second table) has no
|
|
1080
|
+
// bounded-delta arm → floor.
|
|
1081
|
+
if (tableRefs.length > 1)
|
|
1082
|
+
return null;
|
|
1083
|
+
// Single-source aggregate (`group by` over bare columns) → residual-recompute arm.
|
|
1084
|
+
// Each changed source row belongs to exactly one group; maintenance recomputes that
|
|
1085
|
+
// group's backing row from live state. A scalar aggregate (no GROUP BY) falls to the
|
|
1086
|
+
// floor.
|
|
1087
|
+
const aggregate = findAggregate(analyzed);
|
|
1088
|
+
if (aggregate) {
|
|
1089
|
+
return this.buildAggregateResidualPlan(mv, analyzed, tableRef, sourceBase, aggregate);
|
|
1090
|
+
}
|
|
1091
|
+
// The covering-index shape → inverse-projection arm (the default single-source arm).
|
|
1092
|
+
return this.buildInverseProjectionPlan(mv, analyzed, tableRef, sourceBase);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Build an `'inverse-projection'` plan for the covering-index shape: a single
|
|
1096
|
+
* row-preserving source `T` with a primary key, a linear
|
|
1097
|
+
* `TableReference → optional Filter → Project → optional Sort` body, a projection that
|
|
1098
|
+
* resolves every source PK column (and every backing-key column) to a **passthrough**
|
|
1099
|
+
* source column — non-key columns may instead be a **deterministic scalar expression**
|
|
1100
|
+
* over the source row — and a partial WHERE evaluable on a single source row. Returns
|
|
1101
|
+
* `null` on any **shape** mismatch (the caller falls through to the full-rebuild floor);
|
|
1102
|
+
* a **non-deterministic** computed column is the one hard reject (its arm-specific
|
|
1103
|
+
* determinism diagnostic must survive rather than fall through to the floor's generic one).
|
|
1104
|
+
*/
|
|
1105
|
+
buildInverseProjectionPlan(mv, analyzed, tableRef, sourceBase) {
|
|
1106
|
+
const db = this.ctx;
|
|
1107
|
+
const sourceSchema = tableRef.tableSchema;
|
|
1108
|
+
const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
|
|
1109
|
+
if (sourcePkCols.length === 0)
|
|
1110
|
+
return null; // source has no PK → floor
|
|
1111
|
+
const backing = this.ctx._findTable(mv.name, mv.schemaName);
|
|
1112
|
+
if (!backing) {
|
|
1113
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
1114
|
+
}
|
|
1115
|
+
// Projection classification: each backing output column is either a passthrough
|
|
1116
|
+
// source column (a pure permutation entry) or a deterministic scalar expression
|
|
1117
|
+
// over the single source row. A passthrough makes maintenance a column copy; an
|
|
1118
|
+
// expression column evaluates `project(sourceRow)` via the runtime (still a pure
|
|
1119
|
+
// per-row function — O(log n), no body re-execution). PK / backing-key columns
|
|
1120
|
+
// must stay passthrough (the backing key and the inverse-projection conflict map
|
|
1121
|
+
// depend on it); non-key columns may be computed.
|
|
1122
|
+
//
|
|
1123
|
+
// "Passthrough" is value-preserving lineage (`resolveValuePreservingSourceCol`):
|
|
1124
|
+
// a bare column reference, OR one wrapped in `collate` / a no-op `cast` — those
|
|
1125
|
+
// wrappers copy the source VALUE verbatim, so the column-copy maintenance is
|
|
1126
|
+
// exact. This is what lets the collation-weakening migration shape (`select b
|
|
1127
|
+
// collate nocase as b from t`) register here with its coarsened backing key:
|
|
1128
|
+
// the per-row upsert is keyed under the backing PK's (output) collation, so a
|
|
1129
|
+
// colliding source row last-write-wins into the shared backing row, and a
|
|
1130
|
+
// delete of one colliding sibling removes the shared row (the documented
|
|
1131
|
+
// anomaly — docs/materialized-views.md § Coarsened backing keys).
|
|
1132
|
+
const sourceAttrToCol = new Map();
|
|
1133
|
+
const sourceDescriptor = [];
|
|
1134
|
+
tableRef.getAttributes().forEach((a, i) => {
|
|
1135
|
+
sourceAttrToCol.set(a.id, i);
|
|
1136
|
+
sourceDescriptor[a.id] = i;
|
|
1137
|
+
});
|
|
1138
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
1139
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
1140
|
+
if (!rootAttrs)
|
|
1141
|
+
return null; // no relational output → floor (which hard-rejects it)
|
|
1142
|
+
const projectors = [];
|
|
1143
|
+
for (let outCol = 0; outCol < rootAttrs.length; outCol++) {
|
|
1144
|
+
const attr = rootAttrs[outCol];
|
|
1145
|
+
const sourceCol = attr ? resolveValuePreservingSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
|
|
1146
|
+
if (sourceCol !== undefined) {
|
|
1147
|
+
projectors.push({ kind: 'passthrough', sourceCol });
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
// Computed column: a deterministic scalar over the source row. A
|
|
1151
|
+
// non-deterministic producer is a HARD reject (the arm-specific determinism
|
|
1152
|
+
// diagnostic must survive — so `random()` fails on *determinism*, not by silently
|
|
1153
|
+
// falling to a still-rejected floor); a deterministic-but-unsupported *shape*
|
|
1154
|
+
// (no resolvable producer, a subquery / cross-row reference, an async form)
|
|
1155
|
+
// returns `null` → the floor.
|
|
1156
|
+
const colName = attr?.name ?? `#${outCol}`;
|
|
1157
|
+
const producing = attr ? producingByAttrId.get(attr.id) : undefined;
|
|
1158
|
+
if (!producing)
|
|
1159
|
+
return null;
|
|
1160
|
+
const det = checkDeterministic(producing);
|
|
1161
|
+
if (!det.valid) {
|
|
1162
|
+
throw cannotMaterialize(mv.name, `it projects a non-deterministic expression column '${colName}' (${det.expression}); `
|
|
1163
|
+
+ `a row-time backing value must be reproducible from the source row`);
|
|
1164
|
+
}
|
|
1165
|
+
if (!isSingleRowEvaluable(producing, sourceDescriptor))
|
|
1166
|
+
return null;
|
|
1167
|
+
let evalFn;
|
|
1168
|
+
try {
|
|
1169
|
+
evalFn = compileSourceRowEvaluator(db, producing, sourceDescriptor);
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
return null; // not row-time maintainable as a single-row scalar → floor
|
|
1173
|
+
}
|
|
1174
|
+
projectors.push({ kind: 'expr', eval: evalFn });
|
|
1175
|
+
}
|
|
1176
|
+
// Every source PK column must be projected as a passthrough column so the backing
|
|
1177
|
+
// key is a deterministic identity of the source row that `lookupCoveringConflicts`
|
|
1178
|
+
// can invert. A PK column produced only via an expression (or not at all) breaks
|
|
1179
|
+
// that recovery.
|
|
1180
|
+
const passthroughSourceCols = new Set(projectors.flatMap(p => p.kind === 'passthrough' ? [p.sourceCol] : []));
|
|
1181
|
+
for (const pk of sourcePkCols) {
|
|
1182
|
+
if (!passthroughSourceCols.has(pk))
|
|
1183
|
+
return null; // PK not passthrough-projected → floor
|
|
1184
|
+
}
|
|
1185
|
+
const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
|
|
1186
|
+
// A computed column may never land in the backing primary key: the btree keys on
|
|
1187
|
+
// it and `lookupCoveringConflicts` recovers the source PK from it, both of which
|
|
1188
|
+
// require a passthrough source-column identity.
|
|
1189
|
+
for (const d of backingPkDefinition) {
|
|
1190
|
+
if (projectors[d.index]?.kind !== 'passthrough')
|
|
1191
|
+
return null; // computed backing-key col → floor
|
|
1192
|
+
}
|
|
1193
|
+
// Partial WHERE must be evaluable on a single source row (no subqueries /
|
|
1194
|
+
// cross-row references). `compilePredicate` throws on unsupported forms; an
|
|
1195
|
+
// unsupported WHERE shape falls to the floor.
|
|
1196
|
+
let predicate;
|
|
1197
|
+
const bodyWhere = mv.derivation.selectAst.type === 'select' ? mv.derivation.selectAst.where : undefined;
|
|
1198
|
+
if (bodyWhere) {
|
|
1199
|
+
try {
|
|
1200
|
+
predicate = compilePredicate(bodyWhere, sourceSchema.columns);
|
|
1201
|
+
}
|
|
1202
|
+
catch {
|
|
1203
|
+
return null; // WHERE not evaluable on a single source row → floor
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
// ── Cost gate (incremental-maintenance-cost-gate) ──
|
|
1207
|
+
// The covering-index shape's only structurally-sound maintenance strategy is
|
|
1208
|
+
// 'inverse-projection' (O(1) per changed row); 'full-rebuild' is the floor for bodies
|
|
1209
|
+
// this arm did NOT match (reached via the `null` fall-through above), so it is not a
|
|
1210
|
+
// competitor here. Eligibility is thus a cost choice among the sound strategies (argmin
|
|
1211
|
+
// maintenanceCost); for this shape it resolves to inverse-projection while recording the
|
|
1212
|
+
// choice + the cost inputs the runtime reuses.
|
|
1213
|
+
const soundStrategies = ['inverse-projection'];
|
|
1214
|
+
const sourceStats = this.estimateMaintenanceStats(sourceSchema, projectors.length, predicate !== undefined);
|
|
1215
|
+
// Create-time change-cardinality estimate: ~1% of the source per statement (typical OLTP).
|
|
1216
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
1217
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
1218
|
+
// Defensive: this arm's sound set is exactly ['inverse-projection']. A different choice
|
|
1219
|
+
// would mean the set grew without the corresponding apply-arm — fail loud rather than
|
|
1220
|
+
// register an unexecutable plan.
|
|
1221
|
+
if (chosenStrategy !== 'inverse-projection') {
|
|
1222
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
kind: 'inverse-projection',
|
|
1226
|
+
mv,
|
|
1227
|
+
sourceBase,
|
|
1228
|
+
backingSchema: mv.schemaName,
|
|
1229
|
+
backingTableName: mv.name,
|
|
1230
|
+
chosenStrategy,
|
|
1231
|
+
sourceStats,
|
|
1232
|
+
backingPkDefinition,
|
|
1233
|
+
projectors,
|
|
1234
|
+
predicate,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Build a `'residual-recompute'` plan for a single-source aggregate body
|
|
1239
|
+
* (`select g1,…, agg(…) from T [where P] group by g1,…` over **bare** group columns),
|
|
1240
|
+
* or return `null` on a shape mismatch (the caller falls through to the full-rebuild
|
|
1241
|
+
* floor). Each changed source row belongs to exactly one group `(g1,…)`; maintaining the
|
|
1242
|
+
* MV means recomputing that group's backing row from live state — delete the old slice,
|
|
1243
|
+
* run the group-keyed residual, upsert the recomputed slice (zero rows when the group
|
|
1244
|
+
* emptied). See {@link ResidualRecomputePlan} and `docs/incremental-maintenance.md`
|
|
1245
|
+
* § residual-recompute.
|
|
1246
|
+
*
|
|
1247
|
+
* A **non-deterministic** group/aggregate expression is the one hard reject (the
|
|
1248
|
+
* arm-specific determinism diagnostic must survive); every other mismatch — a scalar
|
|
1249
|
+
* aggregate, a computed group key, a backing key that is not the group key — returns
|
|
1250
|
+
* `null` → the floor.
|
|
1251
|
+
*
|
|
1252
|
+
* NOTE: the group binding is derived **directly** from the aggregate node's bare GROUP
|
|
1253
|
+
* BY columns, not via `extractBindings`. `analyzeRowSpecific`'s `'group'` classification
|
|
1254
|
+
* additionally requires the group key to cover a *source* unique key (so it reports
|
|
1255
|
+
* `'global'` for the common `group by <non-key>` body), which is the wrong test here —
|
|
1256
|
+
* the backing is keyed by the group key regardless of whether it is a source key.
|
|
1257
|
+
*/
|
|
1258
|
+
buildAggregateResidualPlan(mv, analyzed, tableRef, sourceBase, aggregate) {
|
|
1259
|
+
// A scalar aggregate (no GROUP BY) is one global row keyed by the empty key — no
|
|
1260
|
+
// bounded-delta group binding, so it falls to the floor.
|
|
1261
|
+
if (aggregate.groupBy.length === 0)
|
|
1262
|
+
return null;
|
|
1263
|
+
// Map T's output attributes to source column indices. T is a bare
|
|
1264
|
+
// `TableReferenceNode`, so output-column index == source-column index.
|
|
1265
|
+
const sourceAttrToCol = new Map();
|
|
1266
|
+
tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
|
|
1267
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
1268
|
+
// Transitive provenance: chase output-attr → producing ColumnReference chains
|
|
1269
|
+
// (Project-over-Aggregate adds a hop the single-hop `resolveSourceCol` cannot
|
|
1270
|
+
// follow) until landing on a T source column, or `undefined`.
|
|
1271
|
+
const resolveToSourceCol = (attrId) => resolveTransitiveSourceCol(attrId, sourceAttrToCol, producingByAttrId);
|
|
1272
|
+
// Each GROUP BY expression must be a bare source column (a computed group key has
|
|
1273
|
+
// no source-column index to bind / key the backing on) → otherwise the floor.
|
|
1274
|
+
const groupColumns = [];
|
|
1275
|
+
for (const expr of aggregate.groupBy) {
|
|
1276
|
+
if (!(expr instanceof ColumnReferenceNode))
|
|
1277
|
+
return null;
|
|
1278
|
+
const sourceCol = sourceAttrToCol.get(expr.attributeId);
|
|
1279
|
+
if (sourceCol === undefined)
|
|
1280
|
+
return null;
|
|
1281
|
+
groupColumns.push(sourceCol);
|
|
1282
|
+
}
|
|
1283
|
+
// Determinism: a residual must reproduce exactly what `select <body>` returns, so a
|
|
1284
|
+
// volatile group/aggregate expression (random()/now()/volatile UDF) is a HARD reject.
|
|
1285
|
+
for (const expr of aggregate.groupBy) {
|
|
1286
|
+
const det = checkDeterministic(expr);
|
|
1287
|
+
if (!det.valid)
|
|
1288
|
+
throw cannotMaterialize(mv.name, `it groups by a non-deterministic expression (${det.expression})`);
|
|
1289
|
+
}
|
|
1290
|
+
for (const agg of aggregate.aggregates) {
|
|
1291
|
+
const det = checkDeterministic(agg.expression);
|
|
1292
|
+
if (!det.valid)
|
|
1293
|
+
throw cannotMaterialize(mv.name, `it aggregates a non-deterministic expression (${det.expression})`);
|
|
1294
|
+
}
|
|
1295
|
+
// Backing table + its physical PK. The aggregate's group-key FD
|
|
1296
|
+
// (`propagateAggregateFds`) makes the group key the backing key (via `keysOf`).
|
|
1297
|
+
const backing = this.ctx._findTable(mv.name, mv.schemaName);
|
|
1298
|
+
if (!backing) {
|
|
1299
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
1300
|
+
}
|
|
1301
|
+
const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
|
|
1302
|
+
// Map each backing-PK column back to the source group column it projects, so a
|
|
1303
|
+
// changed row's old backing-slice delete key can be built. Every backing-PK column
|
|
1304
|
+
// MUST resolve to a GROUP BY source column — else the backing key is not the group
|
|
1305
|
+
// key and point-keyed delete+upsert would be unsound → fall to the floor.
|
|
1306
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
1307
|
+
if (!rootAttrs)
|
|
1308
|
+
return null;
|
|
1309
|
+
const groupColumnSet = new Set(groupColumns);
|
|
1310
|
+
const backingPkSourceCols = [];
|
|
1311
|
+
for (const d of backingPkDefinition) {
|
|
1312
|
+
const attr = rootAttrs[d.index];
|
|
1313
|
+
const sourceCol = attr ? resolveToSourceCol(attr.id) : undefined;
|
|
1314
|
+
if (sourceCol === undefined || !groupColumnSet.has(sourceCol))
|
|
1315
|
+
return null;
|
|
1316
|
+
backingPkSourceCols.push(sourceCol);
|
|
1317
|
+
}
|
|
1318
|
+
// Compile + cache the group-keyed residual once (the body with `g1 = :gk0 AND …`
|
|
1319
|
+
// injected on T). Re-run per affected group key against the live transaction.
|
|
1320
|
+
const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
|
|
1321
|
+
const residualScheduler = this.compileResidual(analyzed, relKey, groupColumns, 'gk');
|
|
1322
|
+
if (!residualScheduler)
|
|
1323
|
+
return null; // could not parameterize the residual → floor
|
|
1324
|
+
// ── Cost gate ──
|
|
1325
|
+
// The residual is the structurally-sound incremental arm for an aggregate body;
|
|
1326
|
+
// 'full-rebuild' is the always-correct floor for shapes where the residual is NOT
|
|
1327
|
+
// sound, so (as with inverse-projection) it is not a competitor here. We still
|
|
1328
|
+
// record the chosen strategy + cost inputs for parity with the substrate.
|
|
1329
|
+
const soundStrategies = ['residual-recompute'];
|
|
1330
|
+
const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
|
|
1331
|
+
const sourceStats = this.estimateMaintenanceStats(tableRef.tableSchema, backing.columns.length, hasPredicate);
|
|
1332
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
1333
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
1334
|
+
if (chosenStrategy !== 'residual-recompute') {
|
|
1335
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
kind: 'residual-recompute',
|
|
1339
|
+
mv,
|
|
1340
|
+
sourceBase,
|
|
1341
|
+
backingSchema: mv.schemaName,
|
|
1342
|
+
backingTableName: mv.name,
|
|
1343
|
+
chosenStrategy,
|
|
1344
|
+
sourceStats,
|
|
1345
|
+
binding: { kind: 'group', groupColumns: [...groupColumns] },
|
|
1346
|
+
degradeToRebuild: false,
|
|
1347
|
+
residualScheduler,
|
|
1348
|
+
bindParamPrefix: 'gk',
|
|
1349
|
+
bindColumns: groupColumns,
|
|
1350
|
+
backingPkDefinition,
|
|
1351
|
+
backingPkSourceCols,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Build a `'join-residual'` plan for a provably-1:1 row-preserving **inner/cross join**
|
|
1356
|
+
* body (`select … from T join P on T.fk = P.id`), or return `null` on a shape mismatch
|
|
1357
|
+
* (the caller falls through to the full-rebuild floor). The driving table `T` is the one
|
|
1358
|
+
* whose PK the optimizer surfaced as the backing key (the 1:1 join collapses the composite
|
|
1359
|
+
* product key to `T`'s PK); the other base ref is the lookup `P`. See {@link JoinResidualPlan}
|
|
1360
|
+
* and `docs/incremental-maintenance.md` § join-residual.
|
|
1361
|
+
*
|
|
1362
|
+
* Soundness gates (a mismatch on any returns `null` → floor): exactly two base tables; no
|
|
1363
|
+
* aggregate over the join; the backing PK is exactly `T`'s PK projected as passthrough
|
|
1364
|
+
* columns (so each changed `T` row maps to one backing row and the reverse residual's rows
|
|
1365
|
+
* carry the backing key); the join is provably 1:1 on `T` ({@link proveOneToOneJoin} — no
|
|
1366
|
+
* row loss via NOT-NULL FK→PK RI, no fan-out via the join-frame `isUnique(T.pk)`); and the
|
|
1367
|
+
* join is **inner/cross** (an outer join would make the lookup-side reverse residual unsound
|
|
1368
|
+
* — filtering `P` drops the null-extended rows). A **non-deterministic** projection is the
|
|
1369
|
+
* one hard reject (its arm-specific determinism diagnostic must survive).
|
|
1370
|
+
*
|
|
1371
|
+
* **A body WHERE is now accepted** (it is no longer a blanket reject): the predicate is
|
|
1372
|
+
* classified by which base table(s) its columns reference (reusing the per-base-ref
|
|
1373
|
+
* attribute→source-column maps below). A `T`-only predicate needs nothing extra — the
|
|
1374
|
+
* forward residual already carries it and the membership set `{ T : T.fk = P.pk }` cannot
|
|
1375
|
+
* move on a `P` write, so the lookup side stays upsert-only. A predicate referencing `P` (or
|
|
1376
|
+
* both sides) switches the lookup side to a **delete-capable** reverse residual by building
|
|
1377
|
+
* `lookupMembershipResidualScheduler` (the body with the WHERE stripped, keyed on `P`). See
|
|
1378
|
+
* {@link JoinResidualPlan}'s "WHERE handling" note and {@link applyLookupResidual}.
|
|
1379
|
+
*/
|
|
1380
|
+
buildJoinResidualPlan(mv, analyzed, tableRefs) {
|
|
1381
|
+
// A >2-source join or an aggregate over the join has no join-residual binding → floor.
|
|
1382
|
+
// A body WHERE is no longer rejected here — it is classified (T-only vs P-referencing)
|
|
1383
|
+
// below, after `T`/`P` are identified, and routed to the matching lookup-side strategy.
|
|
1384
|
+
if (tableRefs.length !== 2)
|
|
1385
|
+
return null;
|
|
1386
|
+
if (findAggregate(analyzed))
|
|
1387
|
+
return null;
|
|
1388
|
+
const backing = this.ctx._findTable(mv.name, mv.schemaName);
|
|
1389
|
+
if (!backing) {
|
|
1390
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
1391
|
+
}
|
|
1392
|
+
const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
|
|
1393
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
1394
|
+
if (!rootAttrs)
|
|
1395
|
+
return null;
|
|
1396
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
1397
|
+
// Per-base-ref attribute → source-column maps. `T` and `P` are bare
|
|
1398
|
+
// TableReferenceNodes (output-col index == source-col index).
|
|
1399
|
+
const refInfos = tableRefs.map(ref => {
|
|
1400
|
+
const attrToCol = new Map();
|
|
1401
|
+
ref.getAttributes().forEach((a, i) => attrToCol.set(a.id, i));
|
|
1402
|
+
return { ref, attrToCol };
|
|
1403
|
+
});
|
|
1404
|
+
// Identify the driving table `T` as the one every backing-PK column resolves to, and
|
|
1405
|
+
// map each backing-PK column to its `T` source column (the delete key the forward arm
|
|
1406
|
+
// builds). A backing-PK column resolving to neither ref — or columns spanning both —
|
|
1407
|
+
// means the backing is not keyed on a single source's PK (the join is not provably
|
|
1408
|
+
// 1:1, or `keysOf` fell back to all-columns): reject.
|
|
1409
|
+
let tIndex;
|
|
1410
|
+
const backingPkSourceCols = [];
|
|
1411
|
+
for (const d of backingPkDefinition) {
|
|
1412
|
+
const attr = rootAttrs[d.index];
|
|
1413
|
+
let resolvedRef;
|
|
1414
|
+
let resolvedCol;
|
|
1415
|
+
for (let i = 0; i < refInfos.length; i++) {
|
|
1416
|
+
const sc = attr ? resolveTransitiveSourceCol(attr.id, refInfos[i].attrToCol, producingByAttrId) : undefined;
|
|
1417
|
+
if (sc !== undefined) {
|
|
1418
|
+
resolvedRef = i;
|
|
1419
|
+
resolvedCol = sc;
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
// A backing-PK column resolving to neither ref, or columns spanning both, means the
|
|
1424
|
+
// backing is not keyed on a single source's PK (not provably 1:1) → fall to the floor.
|
|
1425
|
+
if (resolvedRef === undefined)
|
|
1426
|
+
return null;
|
|
1427
|
+
if (tIndex === undefined)
|
|
1428
|
+
tIndex = resolvedRef;
|
|
1429
|
+
else if (tIndex !== resolvedRef)
|
|
1430
|
+
return null;
|
|
1431
|
+
backingPkSourceCols.push(resolvedCol);
|
|
1432
|
+
}
|
|
1433
|
+
if (tIndex === undefined)
|
|
1434
|
+
return null;
|
|
1435
|
+
const tRef = refInfos[tIndex].ref;
|
|
1436
|
+
const pRef = refInfos[tIndex === 0 ? 1 : 0].ref;
|
|
1437
|
+
const tSchema = tRef.tableSchema;
|
|
1438
|
+
const pSchema = pRef.tableSchema;
|
|
1439
|
+
const sourceBase = `${tSchema.schemaName}.${tSchema.name}`.toLowerCase();
|
|
1440
|
+
const lookupBase = `${pSchema.schemaName}.${pSchema.name}`.toLowerCase();
|
|
1441
|
+
if (sourceBase === lookupBase)
|
|
1442
|
+
return null; // self-join → floor
|
|
1443
|
+
// The backing key must be EXACTLY the driving source's PK (each `T` row → one backing
|
|
1444
|
+
// row). `keysOf` surfaced `T.pk` for the 1:1 join; verify it resolved to a real PK key
|
|
1445
|
+
// (not the all-columns fallback) by set-equality with `T`'s declared PK.
|
|
1446
|
+
const tPkCols = tSchema.primaryKeyDefinition.map(d => d.index);
|
|
1447
|
+
if (tPkCols.length === 0)
|
|
1448
|
+
return null;
|
|
1449
|
+
const backingPkSet = new Set(backingPkSourceCols);
|
|
1450
|
+
if (backingPkSet.size !== tPkCols.length || !tPkCols.every(c => backingPkSet.has(c)))
|
|
1451
|
+
return null;
|
|
1452
|
+
// Prove the join is 1:1 on `T` (no row loss + no fan-out), reusing the coverage
|
|
1453
|
+
// prover's shared join predicates over the analyzed body. A fanning / non-1:1 join
|
|
1454
|
+
// falls to the floor.
|
|
1455
|
+
const root = rootRelationalNode(analyzed);
|
|
1456
|
+
if (!root)
|
|
1457
|
+
return null;
|
|
1458
|
+
const proof = proveOneToOneJoin(root, tSchema);
|
|
1459
|
+
if (!proof.ok)
|
|
1460
|
+
return null;
|
|
1461
|
+
// Restrict to inner/cross: the lookup-side reverse residual filters `P`, which would
|
|
1462
|
+
// drop an outer join's null-extended rows (unsound). An outer 1:1 join falls to the floor.
|
|
1463
|
+
const topJoin = proof.topJoin;
|
|
1464
|
+
const joinType = topJoin && CapabilityDetectors.isJoin(topJoin) ? topJoin.getJoinType() : undefined;
|
|
1465
|
+
if (joinType !== 'inner' && joinType !== 'cross')
|
|
1466
|
+
return null;
|
|
1467
|
+
// Determinism: the residual must reproduce exactly what `select <body>` returns, so a
|
|
1468
|
+
// volatile projection (random()/now()/volatile UDF) is a HARD reject.
|
|
1469
|
+
for (const attr of rootAttrs) {
|
|
1470
|
+
const producing = attr ? producingByAttrId.get(attr.id) : undefined;
|
|
1471
|
+
if (!producing)
|
|
1472
|
+
continue; // a bare passthrough column has no producing expr to check
|
|
1473
|
+
const det = checkDeterministic(producing);
|
|
1474
|
+
if (!det.valid) {
|
|
1475
|
+
throw cannotMaterialize(mv.name, `it projects a non-deterministic expression (${det.expression}); a row-time backing value must be reproducible from the source rows`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
// Forward (`T`) residual: the body with `T.pk = :pk0 AND …` injected on `T`. Recomputes
|
|
1479
|
+
// the one joined row for a changed `T` row (delegated to `applyForwardResidual`).
|
|
1480
|
+
const tRelKey = `${sourceBase}#${tRef.id ?? 'unknown'}`;
|
|
1481
|
+
const forwardResidual = this.compileResidual(analyzed, tRelKey, tPkCols, 'pk');
|
|
1482
|
+
if (!forwardResidual)
|
|
1483
|
+
return null;
|
|
1484
|
+
// Reverse (`P`) **in-scope** residual: the body — WHERE retained — with `P.pk = :pk0 AND …`
|
|
1485
|
+
// injected on `P`. Drives lookup-side maintenance — finds every currently in-scope joined
|
|
1486
|
+
// row referencing a changed `P` row.
|
|
1487
|
+
const pPkCols = pSchema.primaryKeyDefinition.map(d => d.index);
|
|
1488
|
+
if (pPkCols.length === 0)
|
|
1489
|
+
return null;
|
|
1490
|
+
const pRelKey = `${lookupBase}#${pRef.id ?? 'unknown'}`;
|
|
1491
|
+
const reverseResidual = this.compileResidual(analyzed, pRelKey, pPkCols, 'pk');
|
|
1492
|
+
if (!reverseResidual)
|
|
1493
|
+
return null;
|
|
1494
|
+
// Classify the body WHERE by which base table(s) its columns reference: a predicate that
|
|
1495
|
+
// references `P` (or both sides) can flip a row's WHERE membership on a `P` write, so the
|
|
1496
|
+
// lookup side must become delete-capable; a `T`-only predicate cannot move membership, so
|
|
1497
|
+
// the upsert-only reverse residual above stays sound. The membership residual is the body
|
|
1498
|
+
// with the WHERE **stripped** and the key filter on `P` — it returns every currently
|
|
1499
|
+
// referencing `T` row (regardless of scope) so its backing key can be deleted before the
|
|
1500
|
+
// in-scope survivors are re-upserted. Absent for a no-WHERE / `T`-only-WHERE body.
|
|
1501
|
+
const hasWhere = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
|
|
1502
|
+
// A volatile WHERE would make every residual (which embeds it) irreproducible → fall to
|
|
1503
|
+
// the floor's pragma-gated whole-body determinism reject, not an unsound bounded-delta arm.
|
|
1504
|
+
if (hasWhere && bodyWhereIsNonDeterministic(analyzed))
|
|
1505
|
+
return null;
|
|
1506
|
+
const whereReferencesLookup = hasWhere
|
|
1507
|
+
&& bodyWhereReferencesLookup(analyzed, refInfos[tIndex].attrToCol, producingByAttrId);
|
|
1508
|
+
let lookupMembershipResidual;
|
|
1509
|
+
if (whereReferencesLookup) {
|
|
1510
|
+
const membership = this.compileLookupMembershipResidual(mv, lookupBase, pPkCols);
|
|
1511
|
+
if (!membership)
|
|
1512
|
+
return null; // could not strip + re-key the membership residual → floor
|
|
1513
|
+
lookupMembershipResidual = membership;
|
|
1514
|
+
}
|
|
1515
|
+
// ── Cost gate (parity with the other residual arms) ──
|
|
1516
|
+
const soundStrategies = ['residual-recompute'];
|
|
1517
|
+
const sourceStats = this.estimateMaintenanceStats(tSchema, backing.columns.length, hasWhere);
|
|
1518
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
1519
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
1520
|
+
if (chosenStrategy !== 'residual-recompute') {
|
|
1521
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
kind: 'join-residual',
|
|
1525
|
+
mv,
|
|
1526
|
+
sourceBase,
|
|
1527
|
+
backingSchema: mv.schemaName,
|
|
1528
|
+
backingTableName: mv.name,
|
|
1529
|
+
chosenStrategy,
|
|
1530
|
+
sourceStats,
|
|
1531
|
+
binding: { kind: 'row', keyColumns: [...tPkCols] },
|
|
1532
|
+
degradeToRebuild: false,
|
|
1533
|
+
residualScheduler: forwardResidual,
|
|
1534
|
+
bindParamPrefix: 'pk',
|
|
1535
|
+
bindColumns: tPkCols,
|
|
1536
|
+
backingPkDefinition,
|
|
1537
|
+
backingPkSourceCols,
|
|
1538
|
+
lookupBase,
|
|
1539
|
+
lookupResidualScheduler: reverseResidual,
|
|
1540
|
+
lookupMembershipResidualScheduler: lookupMembershipResidual,
|
|
1541
|
+
lookupBindColumns: pPkCols,
|
|
1542
|
+
lookupBindParamPrefix: 'pk',
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Compile the **lookup membership** residual for the join-residual arm's delete-capable
|
|
1547
|
+
* lookup side: the MV body with its top-level WHERE **stripped** (membership only) and a
|
|
1548
|
+
* key-equality filter injected on the lookup `P`, keyed `pk0…`. The WHERE is stripped at the
|
|
1549
|
+
* AST level (a shallow clone dropping `where`) and the body re-built + re-analyzed, so only
|
|
1550
|
+
* the WHERE is removed — the join, its `ON` condition, and any projection sub-expressions are
|
|
1551
|
+
* preserved. Re-analysis assigns fresh node ids, so `P`'s reference is re-located by base name
|
|
1552
|
+
* to compute the injection target. Returns `null` if the lookup ref or the key-filter
|
|
1553
|
+
* injection could not be resolved (the caller then falls to the full-rebuild floor).
|
|
1554
|
+
*
|
|
1555
|
+
* Run per affected `P` key, this residual returns **every** `T` row currently joined to `P`
|
|
1556
|
+
* via the join's `ON` condition — irrespective of the WHERE — so {@link applyLookupResidual}
|
|
1557
|
+
* can delete each one's `T.pk` backing key before the in-scope residual re-upserts the
|
|
1558
|
+
* survivors (the membership set the WHERE-bearing reverse residual would otherwise never
|
|
1559
|
+
* shrink).
|
|
1560
|
+
*/
|
|
1561
|
+
compileLookupMembershipResidual(mv, lookupBase, pPkCols) {
|
|
1562
|
+
const db = this.ctx;
|
|
1563
|
+
const strippedAst = { ...mv.derivation.selectAst, where: undefined };
|
|
1564
|
+
const stripped = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
|
|
1565
|
+
const { plan } = this.ctx._buildPlan([strippedAst]);
|
|
1566
|
+
return this.ctx.optimizer.optimizeForAnalysis(plan, db);
|
|
1567
|
+
});
|
|
1568
|
+
// Re-locate `P` in the WHERE-stripped plan by base name (fresh node ids) to build the
|
|
1569
|
+
// injection target key the way `compileResidual`'s callers do.
|
|
1570
|
+
let pRelKey;
|
|
1571
|
+
for (const [relKey, ref] of collectTableRefs(stripped)) {
|
|
1572
|
+
if (`${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase() === lookupBase) {
|
|
1573
|
+
pRelKey = relKey;
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (pRelKey === undefined)
|
|
1578
|
+
return null;
|
|
1579
|
+
return this.compileResidual(stripped, pRelKey, pPkCols, 'pk');
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Build a `'full-rebuild'` plan — the always-correct floor — for an MV whose body matches
|
|
1583
|
+
* no bounded-delta arm, or throw with a non-shape diagnostic. This is the fall-through
|
|
1584
|
+
* builder {@link tryBuildBoundedDeltaArm} routes to on a `null` (no bounded-delta arm fits).
|
|
1585
|
+
* See {@link FullRebuildPlan} and `docs/materialized-views.md` § Full-rebuild floor /
|
|
1586
|
+
* § Primary key inference.
|
|
1587
|
+
*
|
|
1588
|
+
* Create-time rejections (none shape-based — the floor accepts general bodies):
|
|
1589
|
+
* - **bag** body with no provable unique key (`keysOf` over the optimized body root is
|
|
1590
|
+
* empty) — there is no row identity to materialize on. `keysOf` already gates its
|
|
1591
|
+
* all-columns fallback on `isSet`, so a non-empty result is a real key (a true column
|
|
1592
|
+
* key OR the all-columns key of a provable set) and an empty result is exactly a bag.
|
|
1593
|
+
* (A duplicate-producing body usually fails the set contract earlier, at create-fill.)
|
|
1594
|
+
* - **non-deterministic** body (any `random()`/`now()`/volatile UDF anywhere in the plan)
|
|
1595
|
+
* without `pragma nondeterministic_schema` — no maintenance could keep it equal to its
|
|
1596
|
+
* plain view (mirrors the per-arm determinism rejects and the DDL determinism gate);
|
|
1597
|
+
* - body with **no relational output** (degenerate);
|
|
1598
|
+
* - **size**: full-rebuild is the only sound strategy *and* the **largest** participating
|
|
1599
|
+
* source exceeds the `materialized_view_rebuild_row_threshold` option
|
|
1600
|
+
* ({@link isFullRebuildPathological}) — every DML write would re-scan that source.
|
|
1601
|
+
* `0` disables the size reject (accept any size).
|
|
1602
|
+
*/
|
|
1603
|
+
buildFullRebuildPlan(mv, analyzed) {
|
|
1604
|
+
const db = this.ctx;
|
|
1605
|
+
// Optimize the whole body ONCE — read-side MV rewrite suppressed so it reads its
|
|
1606
|
+
// sources, not the backing it populates — then derive the body's key + determinism
|
|
1607
|
+
// from, and compile its scheduler from, the SAME optimized plan.
|
|
1608
|
+
const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => this.ctx.optimizer.optimize(analyzed, db));
|
|
1609
|
+
const root = rootRelationalNode(optimized);
|
|
1610
|
+
if (!root)
|
|
1611
|
+
throw cannotMaterialize(mv.name, 'its body produced no relational output');
|
|
1612
|
+
// Backing key = the body's provable unique key. A bag (no provable key — a key-dropping
|
|
1613
|
+
// projection, a `union all` of overlapping inputs, …) has no row identity to key a
|
|
1614
|
+
// materialization on, so it must be a set. An all-columns pseudo-key is admitted only
|
|
1615
|
+
// when the body is provably a set (`keysOf` gates it on `isSet`); a bag with an
|
|
1616
|
+
// all-columns "key" still resolves to empty here and rejects, else duplicates would
|
|
1617
|
+
// collide on the backing key.
|
|
1618
|
+
//
|
|
1619
|
+
// One carve-out: a keyless body whose source key survives through value-preserving
|
|
1620
|
+
// passthrough lineage (the parallel-migration collation-weakening shape) is keyed on
|
|
1621
|
+
// the COARSENED lineage key instead of rejected — the same `deriveCoarsenedBackingKey`
|
|
1622
|
+
// derivation `deriveBackingShape` keyed the backing with, over the same fully-optimized
|
|
1623
|
+
// body, so the two agree by construction. Colliding rows then last-write-win under the
|
|
1624
|
+
// floor's collation-keyed `replace-all` diff (docs/materialized-views.md § Coarsened
|
|
1625
|
+
// backing keys); the create emitter owns the key-coarsening warning.
|
|
1626
|
+
if (keysOf(root).length === 0 && deriveCoarsenedBackingKey(root) === undefined) {
|
|
1627
|
+
throw cannotMaterialize(mv.name, 'its body has no provable unique key — it is a bag (e.g. a key-dropping '
|
|
1628
|
+
+ 'projection or a `union all` of overlapping inputs), so it must be a set');
|
|
1629
|
+
}
|
|
1630
|
+
// Whole-body determinism: a non-deterministic body can never be kept equal to its
|
|
1631
|
+
// plain view by any maintenance, so it is a hard reject unless the schema-determinism
|
|
1632
|
+
// gate is lifted. Mirrors the per-arm determinism rejects (and the DDL gate).
|
|
1633
|
+
if (!db.options.getBooleanOption('nondeterministic_schema')) {
|
|
1634
|
+
const nonDet = findNonDeterministic(analyzed);
|
|
1635
|
+
if (nonDet) {
|
|
1636
|
+
throw cannotMaterialize(mv.name, `its body is non-deterministic (${nonDet}); a materialized view body must be `
|
|
1637
|
+
+ 'reproducible to stay equal to its plain view (set `pragma nondeterministic_schema` to override)');
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
// Every source the body reads (set-op legs, every join source, …) so a write to any of
|
|
1641
|
+
// them triggers a rebuild. Collected from the (pre-physical) analyzed plan, where every
|
|
1642
|
+
// source is a bare `TableReferenceNode` — the optimized plan may have wrapped them in
|
|
1643
|
+
// physical access nodes.
|
|
1644
|
+
const tableRefs = [...collectTableRefs(analyzed).values()];
|
|
1645
|
+
const sourceBases = [...new Set(tableRefs.map(ref => `${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase()))];
|
|
1646
|
+
if (sourceBases.length === 0)
|
|
1647
|
+
throw cannotMaterialize(mv.name, 'its body reads no source table');
|
|
1648
|
+
const backing = this.ctx._findTable(mv.name, mv.schemaName);
|
|
1649
|
+
if (!backing) {
|
|
1650
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
1651
|
+
}
|
|
1652
|
+
// ── Cost gate + size reject ──
|
|
1653
|
+
// Full-rebuild is the floor: an EMPTY sound set resolves to it (`selectMaintenanceStrategy`).
|
|
1654
|
+
// Cost the rebuild against the LARGEST participating source — every write re-evaluates the
|
|
1655
|
+
// whole body, so the largest source it scans governs whether the per-write rebuild is
|
|
1656
|
+
// pathological (e.g. a tiny driving table joined to a huge lookup gates on the lookup).
|
|
1657
|
+
// Re-resolve each source's CURRENT schema (the analyzed plan node may carry a pre-`analyze`
|
|
1658
|
+
// snapshot whose `statistics` predates the latest counts) so the size gate reflects the
|
|
1659
|
+
// live source size at create time.
|
|
1660
|
+
const statsProvider = this.ctx.optimizer.getStats();
|
|
1661
|
+
let largestSchema = tableRefs[0].tableSchema;
|
|
1662
|
+
let largestRows = -1;
|
|
1663
|
+
for (const ref of tableRefs) {
|
|
1664
|
+
const live = this.liveSourceSchema(ref);
|
|
1665
|
+
const rows = statsProvider.tableRows(live) ?? DEFAULT_SOURCE_ROWS;
|
|
1666
|
+
if (rows > largestRows) {
|
|
1667
|
+
largestRows = rows;
|
|
1668
|
+
largestSchema = live;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const sourceStats = this.estimateMaintenanceStats(largestSchema, backing.columns.length, /*hasPredicate*/ false);
|
|
1672
|
+
// Size reject: full-rebuild is the only sound strategy here, so a source past the
|
|
1673
|
+
// configurable threshold makes every write pathological. `0` disables the reject.
|
|
1674
|
+
const rebuildThreshold = db.options.getNumberOption('materialized_view_rebuild_row_threshold');
|
|
1675
|
+
if (isFullRebuildPathological(sourceStats, rebuildThreshold)) {
|
|
1676
|
+
const largestBase = `${largestSchema.schemaName}.${largestSchema.name}`.toLowerCase();
|
|
1677
|
+
throw cannotMaterialize(mv.name, `its only sound maintenance strategy is a full body rebuild, but its largest source '${largestBase}' has `
|
|
1678
|
+
+ `~${sourceStats.tableRows} rows, over the materialized_view_rebuild_row_threshold (${rebuildThreshold}) — `
|
|
1679
|
+
+ `every write would re-scan it. Raise or disable the threshold `
|
|
1680
|
+
+ `(\`pragma materialized_view_rebuild_row_threshold = 0\`)`);
|
|
1681
|
+
}
|
|
1682
|
+
// Compile the whole optimized body once into a reusable scheduler (no key filter).
|
|
1683
|
+
const bodyScheduler = new Scheduler(emitPlanNode(optimized, new EmissionContext(db)));
|
|
1684
|
+
const chosenStrategy = selectMaintenanceStrategy([], Math.max(1, sourceStats.tableRows * 0.01), sourceStats);
|
|
1685
|
+
if (chosenStrategy !== 'full-rebuild') {
|
|
1686
|
+
throw new QuereusError(`Internal error: cost gate selected '${chosenStrategy}' for the full-rebuild floor of materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1687
|
+
}
|
|
1688
|
+
return {
|
|
1689
|
+
kind: 'full-rebuild',
|
|
1690
|
+
mv,
|
|
1691
|
+
sourceBase: sourceBases[0],
|
|
1692
|
+
backingSchema: mv.schemaName,
|
|
1693
|
+
backingTableName: mv.name,
|
|
1694
|
+
chosenStrategy,
|
|
1695
|
+
sourceStats,
|
|
1696
|
+
bodyScheduler,
|
|
1697
|
+
sourceBases,
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Compile the key-filtered residual for a binding into a reusable {@link Scheduler}:
|
|
1702
|
+
* the analyzed body with a key-equality filter injected on `T`'s `TableReferenceNode`
|
|
1703
|
+
* (parameterized `${paramPrefix}0…`), then optimized + emitted. Mirrors the assertion
|
|
1704
|
+
* evaluator's residual compilation (`database-assertions.ts`) so the two cannot drift.
|
|
1705
|
+
* Returns `null` if `injectKeyFilter` could not target `T` (the arm builder then falls
|
|
1706
|
+
* through to the full-rebuild floor).
|
|
1707
|
+
*/
|
|
1708
|
+
compileResidual(analyzed, relKey, bindColumns, paramPrefix) {
|
|
1709
|
+
const db = this.ctx;
|
|
1710
|
+
const rewritten = injectKeyFilter(analyzed, relKey, bindColumns, paramPrefix);
|
|
1711
|
+
if (rewritten === analyzed)
|
|
1712
|
+
return null; // could not parameterize the residual → floor
|
|
1713
|
+
// Suppress the read-side rewrite: the residual is the MV's own body (+ a key
|
|
1714
|
+
// filter) compiled to maintain its backing, so it must stay over the source.
|
|
1715
|
+
const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => this.ctx.optimizer.optimize(rewritten, db));
|
|
1716
|
+
const instruction = emitPlanNode(optimized, new EmissionContext(db));
|
|
1717
|
+
return new Scheduler(instruction);
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Execute a cached key-filtered residual for one affected key tuple, returning its
|
|
1721
|
+
* result rows (0 or 1 for the aggregate shape; 0..N for the lateral-TVF fan-out shape).
|
|
1722
|
+
* Bound through a fresh {@link RuntimeContext} on the live `db` so the residual's source
|
|
1723
|
+
* scan reuses `T`'s transaction connection and reads this statement's pending writes
|
|
1724
|
+
* (reads-own-writes) — the synchronous analogue of
|
|
1725
|
+
* `database-assertions.ts:executeResidualPerTuple`. Shared by the residual-recompute
|
|
1726
|
+
* (`'gk'`) and prefix-delete (`'pk'`) arms.
|
|
1727
|
+
*/
|
|
1728
|
+
async runResidual(residualScheduler, bindParamPrefix, keyTuple) {
|
|
1729
|
+
const params = {};
|
|
1730
|
+
for (let i = 0; i < keyTuple.length; i++) {
|
|
1731
|
+
params[`${bindParamPrefix}${i}`] = keyTuple[i];
|
|
1732
|
+
}
|
|
1733
|
+
return this.runScheduler(residualScheduler, params);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Run a cached maintenance scheduler to completion against **live mid-transaction source
|
|
1737
|
+
* state** and collect its result rows. Bound through a fresh strict {@link RuntimeContext}
|
|
1738
|
+
* on the live `db` so the scan reuses the source's transaction connection and reads this
|
|
1739
|
+
* statement's pending writes (reads-own-writes). The no-`stmt`, fresh-context shape is the
|
|
1740
|
+
* synchronous analogue of `database-assertions.ts:executeResidualPerTuple`. Shared by the
|
|
1741
|
+
* key-filtered residual arms ({@link runResidual}, parameterized) and the whole-body
|
|
1742
|
+
* full-rebuild arm ({@link applyFullRebuild}, no params).
|
|
1743
|
+
*/
|
|
1744
|
+
async runScheduler(scheduler, params) {
|
|
1745
|
+
const rctx = {
|
|
1746
|
+
db: this.ctx,
|
|
1747
|
+
stmt: undefined,
|
|
1748
|
+
params,
|
|
1749
|
+
context: createStrictRowContextMap(),
|
|
1750
|
+
tableContexts: wrapTableContextsStrict(new Map()),
|
|
1751
|
+
enableMetrics: false,
|
|
1752
|
+
};
|
|
1753
|
+
const result = await scheduler.run(rctx);
|
|
1754
|
+
const rows = [];
|
|
1755
|
+
if (isAsyncIterable(result)) {
|
|
1756
|
+
for await (const r of result)
|
|
1757
|
+
rows.push(r);
|
|
1758
|
+
}
|
|
1759
|
+
return rows;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Maintain a `'full-rebuild'` MV: re-evaluate the **whole** body against live
|
|
1763
|
+
* mid-transaction source state and replace the backing transactionally. Run the cached
|
|
1764
|
+
* {@link FullRebuildPlan.bodyScheduler} to completion (no params — reads-own-writes via
|
|
1765
|
+
* the same fresh-context path the residual arms use), collect every recomputed row, and
|
|
1766
|
+
* apply a single `'replace-all'` {@link MaintenanceOp}: a keyed diff (by backing PK) of
|
|
1767
|
+
* the recomputed rows against the backing's current pending-layer contents (insert/
|
|
1768
|
+
* update/delete, identical rows skipped). The diff rides the backing's **pending**
|
|
1769
|
+
* `TransactionLayer`, so it commits/rolls-back in lockstep with the source write, and the
|
|
1770
|
+
* returned effective {@link BackingRowChange}(s) drive the MV-over-MV cascade unchanged.
|
|
1771
|
+
*
|
|
1772
|
+
* Unlike the bounded-delta arms this ignores the specific changed row — the floor
|
|
1773
|
+
* rebuilds wholesale. It is therefore deferred to a single end-of-statement flush
|
|
1774
|
+
* ({@link flushDeferredRebuilds}) rather than run per source row, so a bulk statement
|
|
1775
|
+
* rebuilds exactly once; this is that one rebuild. An empty body (zero rows) yields a
|
|
1776
|
+
* `'replace-all' []`, which empties the backing.
|
|
1777
|
+
*/
|
|
1778
|
+
async applyFullRebuild(plan, cache) {
|
|
1779
|
+
const rows = await this.runScheduler(plan.bodyScheduler, {});
|
|
1780
|
+
const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
1781
|
+
if (!backing) {
|
|
1782
|
+
throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
|
|
1783
|
+
}
|
|
1784
|
+
const host = this.backingHost(backing);
|
|
1785
|
+
const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
|
|
1786
|
+
return host.applyMaintenance(connection, [{ kind: 'replace-all', rows }]);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Compute a **forward** key-filtered residual plan's per-row backing delta and apply it:
|
|
1790
|
+
* derive the affected binding key(s) from the changed row (OLD ∪ NEW, deduped), re-run
|
|
1791
|
+
* the key-filtered residual against live source state for each, and apply the **keyed
|
|
1792
|
+
* diff**: a non-empty recomputed slice is upserted (the backing key IS the affected key,
|
|
1793
|
+
* so the upsert replaces the old row wholesale — no delete-first — and the host's
|
|
1794
|
+
* value-identical upsert skip turns a no-op recompute into ZERO effective changes
|
|
1795
|
+
* instead of delete+insert churn); an emptied slice (residual returns nothing) emits the
|
|
1796
|
+
* point delete, removing the stale backing row (nothing reported if it was already
|
|
1797
|
+
* absent). Returns the effective {@link BackingRowChange}(s) the backing layer realized,
|
|
1798
|
+
* for the MV-over-MV cascade — a real same-key change now reports one `update`.
|
|
1799
|
+
*
|
|
1800
|
+
* Shared by the single-source aggregate (`'residual-recompute'`, group key, ≤1 row per
|
|
1801
|
+
* key) and the 1:1-join (`'join-residual'`, the driving table `T`'s PK, exactly the one
|
|
1802
|
+
* joined row per key) arms — both bind on the forward driving source via
|
|
1803
|
+
* {@link ForwardResidualPlan}; the only difference is the binding (group vs PK).
|
|
1804
|
+
*
|
|
1805
|
+
* Per-row recompute is correct without per-statement batching: every change to a key
|
|
1806
|
+
* triggers a full recompute of that key's slice from live (reads-own-writes) state, so
|
|
1807
|
+
* the last change to touch a key writes the authoritative backing row. Batching/dedup
|
|
1808
|
+
* across a whole statement is an affordability optimization deferred with the
|
|
1809
|
+
* statement-flush boundary (see the ticket handoff).
|
|
1810
|
+
*/
|
|
1811
|
+
async applyForwardResidual(plan, change, cache) {
|
|
1812
|
+
// Distinct affected keys (OLD ∪ NEW), deduped on the backing-key values: a
|
|
1813
|
+
// non-key-changing update recomputes the group once; a key-changing update
|
|
1814
|
+
// recomputes both the old and the new group.
|
|
1815
|
+
const affected = new Map();
|
|
1816
|
+
const addFrom = (row) => {
|
|
1817
|
+
const keyVals = plan.backingPkSourceCols.map(sc => row[sc]);
|
|
1818
|
+
const dedupKey = canonKeyValues(keyVals);
|
|
1819
|
+
if (affected.has(dedupKey))
|
|
1820
|
+
return;
|
|
1821
|
+
affected.set(dedupKey, {
|
|
1822
|
+
keyTuple: plan.bindColumns.map(c => row[c]),
|
|
1823
|
+
keyVals,
|
|
1824
|
+
deleteKey: buildPrimaryKeyFromValues(keyVals, plan.backingPkDefinition),
|
|
1825
|
+
});
|
|
1826
|
+
};
|
|
1827
|
+
if (change.op === 'insert')
|
|
1828
|
+
addFrom(change.newRow);
|
|
1829
|
+
else if (change.op === 'delete')
|
|
1830
|
+
addFrom(change.oldRow);
|
|
1831
|
+
else {
|
|
1832
|
+
addFrom(change.oldRow);
|
|
1833
|
+
addFrom(change.newRow);
|
|
1834
|
+
}
|
|
1835
|
+
const ops = [];
|
|
1836
|
+
for (const { keyTuple, keyVals, deleteKey } of affected.values()) {
|
|
1837
|
+
const recomputed = await this.runResidual(plan.residualScheduler, plan.bindParamPrefix, keyTuple);
|
|
1838
|
+
// Keep only the recomputed rows whose backing key equals the affected key.
|
|
1839
|
+
// The residual for key K must only contribute K's slice; any other row is
|
|
1840
|
+
// spurious and is dropped. This is the soundness net for an emptied group: when
|
|
1841
|
+
// no source row matches the key, a *correct* grouped residual returns zero rows,
|
|
1842
|
+
// but a constant-pinned multi-column grouped aggregate is mis-collapsed by the
|
|
1843
|
+
// optimizer into a *scalar* aggregate that emits one all-NULL `count=0` row over
|
|
1844
|
+
// the empty input (a pre-existing optimizer bug, filed separately as
|
|
1845
|
+
// `fix/optimizer-constant-group-aggregate-empty-input-spurious-row`). That row's
|
|
1846
|
+
// key ≠ K, so it is filtered here and the delete-without-upsert correctly removes
|
|
1847
|
+
// the emptied group's backing row.
|
|
1848
|
+
const slice = recomputed.filter(row => this.residualRowMatchesKey(plan, row, keyVals));
|
|
1849
|
+
if (slice.length === 0) {
|
|
1850
|
+
// Emptied slice: delete-without-upsert removes the stale backing row (the
|
|
1851
|
+
// host reports nothing if the key was already absent).
|
|
1852
|
+
ops.push({ kind: 'delete-key', key: deleteKey });
|
|
1853
|
+
}
|
|
1854
|
+
else {
|
|
1855
|
+
// The slice shares the affected backing key, so each upsert REPLACES the old
|
|
1856
|
+
// backing row — no delete-first — and the host's value-identical skip
|
|
1857
|
+
// (vtab/backing-host.ts) suppresses a recompute that changed nothing.
|
|
1858
|
+
for (const row of slice)
|
|
1859
|
+
ops.push({ kind: 'upsert', row });
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (ops.length === 0)
|
|
1863
|
+
return [];
|
|
1864
|
+
const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
1865
|
+
if (!backing) {
|
|
1866
|
+
throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
|
|
1867
|
+
}
|
|
1868
|
+
const host = this.backingHost(backing);
|
|
1869
|
+
const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
|
|
1870
|
+
return host.applyMaintenance(connection, ops);
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* True iff `row`'s backing primary-key columns equal `keyVals` (the affected binding
|
|
1874
|
+
* key, in `backingPkDefinition` order), under each column's collation. Used to keep
|
|
1875
|
+
* only the residual row(s) belonging to the recomputed key — see
|
|
1876
|
+
* {@link applyForwardResidual}.
|
|
1877
|
+
*/
|
|
1878
|
+
residualRowMatchesKey(plan, row, keyVals) {
|
|
1879
|
+
for (let i = 0; i < plan.backingPkDefinition.length; i++) {
|
|
1880
|
+
const d = plan.backingPkDefinition[i];
|
|
1881
|
+
if (compareSqlValues(row[d.index], keyVals[i], d.collation) !== 0)
|
|
1882
|
+
return false;
|
|
1883
|
+
}
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Dispatch a `'join-residual'` plan on **which source changed**. A write to the driving
|
|
1888
|
+
* table `T` (`changedBase === plan.sourceBase`) is the forward case — recompute the one
|
|
1889
|
+
* joined row keyed on `T`'s PK, identical to a size-1 `'row'`-binding residual — so it
|
|
1890
|
+
* delegates straight to {@link applyForwardResidual} (delete old backing slice → run the
|
|
1891
|
+
* `T`-keyed residual → upsert). A write to the lookup table `P` is the reverse case,
|
|
1892
|
+
* handled by {@link applyLookupResidual}.
|
|
1893
|
+
*/
|
|
1894
|
+
async applyJoinResidual(plan, change, changedBase, cache) {
|
|
1895
|
+
if (changedBase === plan.sourceBase) {
|
|
1896
|
+
return this.applyForwardResidual(plan, change, cache);
|
|
1897
|
+
}
|
|
1898
|
+
return this.applyLookupResidual(plan, change, cache);
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Maintain a `'join-residual'` MV for a **lookup-side (`P`)** change: refresh the joined
|
|
1902
|
+
* rows referencing each affected `P` key. Derive the affected `P` key(s) from the changed
|
|
1903
|
+
* row (OLD ∪ NEW, deduped on `P`'s PK), and for each run the in-scope lookup-keyed residual
|
|
1904
|
+
* (`… where P.pk = :pk0`, the body's WHERE retained) against live source state — returning
|
|
1905
|
+
* every currently in-scope joined row, each carrying its `T.pk` backing key — and **upsert**
|
|
1906
|
+
* each.
|
|
1907
|
+
*
|
|
1908
|
+
* **Upsert-only is sound for a no-WHERE / `T`-only-WHERE body.** For an inner/cross join with
|
|
1909
|
+
* enforced RI and a predicate that cannot reference `P`, the *set* of backing rows referencing
|
|
1910
|
+
* a given `P` row is `{ T : T.fk = P.pk }`, determined entirely by `T.fk` (a `T` column the
|
|
1911
|
+
* `P` write cannot change), and the WHERE — over `T` only — cannot flip on a `P` write. So a
|
|
1912
|
+
* `P` change can only re-derive the lookup-projected columns of those existing backing rows
|
|
1913
|
+
* (an upsert at the unchanged `T.pk` key), never add or remove one: a `P` insert with no
|
|
1914
|
+
* referencing `T` rows yields an empty residual (no-op); a `P` delete is only admissible (RI)
|
|
1915
|
+
* when no `T` references it (empty residual); a `P` payload update upserts the affected rows
|
|
1916
|
+
* with the new value.
|
|
1917
|
+
*
|
|
1918
|
+
* **A `P`-referencing WHERE needs the delete-capable pass.** When the body WHERE references
|
|
1919
|
+
* `P`, a `P` write can flip a joined row's WHERE truth and so add or remove its backing row —
|
|
1920
|
+
* which the in-scope upsert above (it returns *only* in-scope rows) could never delete. The
|
|
1921
|
+
* builder then supplies `lookupMembershipResidualScheduler` (the body with the WHERE stripped,
|
|
1922
|
+
* keyed on `P`). Per affected `P` key this runs both residuals against the same live state and
|
|
1923
|
+
* applies the **keyed diff**: it **deletes** only the membership keys the in-scope recompute no
|
|
1924
|
+
* longer produces (rows that left scope — the delete keys come from live `T` via the join, so
|
|
1925
|
+
* they match existing backing keys and touch nothing belonging to another `P`; membership and
|
|
1926
|
+
* in-scope rows read the same live state, so their key bytes match exactly), and **upserts**
|
|
1927
|
+
* every in-scope row. A row leaving scope is deleted (removed); a row entering scope is
|
|
1928
|
+
* upserted (added); an unchanged in-scope row's upsert is suppressed by the host's
|
|
1929
|
+
* value-identical skip (vtab/backing-host.ts) — ZERO effective changes instead of the former
|
|
1930
|
+
* delete+insert refresh churn; a changed in-scope row reports one `update`. The membership
|
|
1931
|
+
* residual MUST ignore the WHERE — else a row leaving scope would never be deleted.
|
|
1932
|
+
*
|
|
1933
|
+
* A `T`-side membership change (insert/delete/FK-move) is the *forward* path's job and fires
|
|
1934
|
+
* its own maintenance. Returns the effective {@link BackingRowChange}(s) for the MV-over-MV
|
|
1935
|
+
* cascade. Per-row recompute is correct without batching for the same
|
|
1936
|
+
* last-write-wins-against-live-state reason as {@link applyForwardResidual}.
|
|
1937
|
+
*/
|
|
1938
|
+
async applyLookupResidual(plan, change, cache) {
|
|
1939
|
+
// Distinct affected lookup keys (OLD ∪ NEW), deduped on `P`'s PK values.
|
|
1940
|
+
const affected = new Map();
|
|
1941
|
+
const addFrom = (row) => {
|
|
1942
|
+
const keyTuple = plan.lookupBindColumns.map(c => row[c]);
|
|
1943
|
+
const dedupKey = canonKeyValues(keyTuple);
|
|
1944
|
+
if (!affected.has(dedupKey))
|
|
1945
|
+
affected.set(dedupKey, keyTuple);
|
|
1946
|
+
};
|
|
1947
|
+
if (change.op === 'insert')
|
|
1948
|
+
addFrom(change.newRow);
|
|
1949
|
+
else if (change.op === 'delete')
|
|
1950
|
+
addFrom(change.oldRow);
|
|
1951
|
+
else {
|
|
1952
|
+
addFrom(change.oldRow);
|
|
1953
|
+
addFrom(change.newRow);
|
|
1954
|
+
}
|
|
1955
|
+
const ops = [];
|
|
1956
|
+
for (const keyTuple of affected.values()) {
|
|
1957
|
+
const recomputed = await this.runResidual(plan.lookupResidualScheduler, plan.lookupBindParamPrefix, keyTuple);
|
|
1958
|
+
// Delete-capable (P-referencing WHERE): keyed diff of the membership residual
|
|
1959
|
+
// (WHERE stripped) against the in-scope recompute — delete ONLY the membership
|
|
1960
|
+
// keys the recompute no longer produces (rows that left the WHERE scope), not
|
|
1961
|
+
// every member. Both residuals read the same live state, so a surviving row's
|
|
1962
|
+
// key bytes match exactly (the byte-canonical set lookup is exact). Deletes
|
|
1963
|
+
// precede upserts, preserving the prior arm's ordering discipline.
|
|
1964
|
+
if (plan.lookupMembershipResidualScheduler) {
|
|
1965
|
+
const produced = new Set(recomputed.map(row => canonKeyValues(plan.backingPkDefinition.map(d => row[d.index]))));
|
|
1966
|
+
const members = await this.runResidual(plan.lookupMembershipResidualScheduler, plan.lookupBindParamPrefix, keyTuple);
|
|
1967
|
+
for (const row of members) {
|
|
1968
|
+
const keyVals = plan.backingPkDefinition.map(d => row[d.index]);
|
|
1969
|
+
if (produced.has(canonKeyValues(keyVals)))
|
|
1970
|
+
continue; // still in scope — upserted below
|
|
1971
|
+
ops.push({ kind: 'delete-key', key: buildPrimaryKeyFromValues(keyVals, plan.backingPkDefinition) });
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
// Upsert every in-scope row; the host's value-identical skip suppresses the
|
|
1975
|
+
// unchanged ones (an in-scope refresh that changed nothing reports nothing).
|
|
1976
|
+
for (const row of recomputed)
|
|
1977
|
+
ops.push({ kind: 'upsert', row });
|
|
1978
|
+
}
|
|
1979
|
+
if (ops.length === 0)
|
|
1980
|
+
return [];
|
|
1981
|
+
const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
1982
|
+
if (!backing) {
|
|
1983
|
+
throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
|
|
1984
|
+
}
|
|
1985
|
+
const host = this.backingHost(backing);
|
|
1986
|
+
const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
|
|
1987
|
+
return host.applyMaintenance(connection, ops);
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Build a `'prefix-delete'` plan for a single-source lateral-TVF fan-out body
|
|
1991
|
+
* (`select T.pk…, …, f.* from T cross join lateral tvf(<args over T>) f`), or return
|
|
1992
|
+
* `null` on a shape mismatch (the caller falls through to the full-rebuild floor). The
|
|
1993
|
+
* backing PK is the composite product key `(T.pk ∪ tvf-key)` that `keysOf` advertises
|
|
1994
|
+
* through the lateral join; the base PK is its leading prefix. See {@link PrefixDeletePlan}
|
|
1995
|
+
* and `docs/incremental-maintenance.md` § prefix-delete.
|
|
1996
|
+
*
|
|
1997
|
+
* Soundness gates (a mismatch on any returns `null` → floor): exactly one lateral TVF and
|
|
1998
|
+
* one join (no nested/multi TVF, no aggregate over the fan-out); the TVF advertises a
|
|
1999
|
+
* per-call key; the base PK projected and forming the **leading prefix** of the backing PK
|
|
2000
|
+
* with a non-empty TVF-key tail (so each base row's fan-out rows are individually
|
|
2001
|
+
* addressable and a by-prefix delete selects exactly one base row's slice). An `order by`
|
|
2002
|
+
* over the fan-out that reorders the composite key so the base PK no longer leads is a
|
|
2003
|
+
* `null` fall-through (the floor maintains it wholesale). The body's WHERE, if any, is part
|
|
2004
|
+
* of the residual (so an out-of-scope base row fans out to zero rows), exactly as in the
|
|
2005
|
+
* aggregate arm. A **non-deterministic** TVF / argument is the one hard reject (its
|
|
2006
|
+
* arm-specific determinism diagnostic must survive).
|
|
2007
|
+
*/
|
|
2008
|
+
buildLateralTvfPrefixDeletePlan(mv, analyzed, tableRef, sourceBase) {
|
|
2009
|
+
// Exactly one lateral TVF and one join. A second base table is already excluded by
|
|
2010
|
+
// the single-source check upstream; this falls to the floor for a second TVF / chained
|
|
2011
|
+
// lateral join (`t join lateral tvf1 join lateral tvf2`).
|
|
2012
|
+
if (countNodeType(analyzed, PlanNodeType.TableFunctionCall) !== 1)
|
|
2013
|
+
return null;
|
|
2014
|
+
if (countJoins(analyzed) !== 1)
|
|
2015
|
+
return null;
|
|
2016
|
+
// An aggregate over the fan-out is a different shape → floor (the TVF route is taken
|
|
2017
|
+
// before the aggregate route, so an `... group by` over a lateral TVF lands here).
|
|
2018
|
+
if (findAggregate(analyzed))
|
|
2019
|
+
return null;
|
|
2020
|
+
// Determinism: a residual must reproduce exactly what `select <body>` returns, so a
|
|
2021
|
+
// volatile TVF (or a volatile argument expression) is a HARD reject.
|
|
2022
|
+
const tvf = findTableFunctionCall(analyzed);
|
|
2023
|
+
if (!tvf) {
|
|
2024
|
+
// Unreachable — countNodeType(...) === 1 above guarantees one exists.
|
|
2025
|
+
throw new QuereusError(`Internal error: lateral TVF node not found for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
2026
|
+
}
|
|
2027
|
+
if (tvf.physical.deterministic === false) {
|
|
2028
|
+
throw cannotMaterialize(mv.name, `it fans out through a non-deterministic table-valued function '${tvf.functionName}' (a row-time fan-out must be reproducible from the base row)`);
|
|
2029
|
+
}
|
|
2030
|
+
for (const operand of tvf.operands) {
|
|
2031
|
+
const det = checkDeterministic(operand);
|
|
2032
|
+
if (!det.valid)
|
|
2033
|
+
throw cannotMaterialize(mv.name, `it passes a non-deterministic argument (${det.expression}) to the lateral table-valued function`);
|
|
2034
|
+
}
|
|
2035
|
+
// The lateral TVF must advertise a per-call key, so the composite product key is a
|
|
2036
|
+
// real column key `(base PK ∪ TVF key)` rather than the all-columns / `isSet`
|
|
2037
|
+
// fallback. Without one the fan-out rows are not individually addressable by a proper
|
|
2038
|
+
// key — the by-prefix delete + keyed upsert would be unsound — so fall to the floor.
|
|
2039
|
+
// `getType().keys` carries the validated advertisement (an out-of-range key
|
|
2040
|
+
// advertisement is dropped, leaving this empty), so it is the authoritative
|
|
2041
|
+
// "did the TVF advertise a usable key" signal.
|
|
2042
|
+
if (tvf.getType().keys.length === 0)
|
|
2043
|
+
return null;
|
|
2044
|
+
// Base T's PK source columns.
|
|
2045
|
+
const sourceSchema = tableRef.tableSchema;
|
|
2046
|
+
const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
|
|
2047
|
+
if (sourcePkCols.length === 0)
|
|
2048
|
+
return null;
|
|
2049
|
+
// Backing table + its physical PK (the composite product key).
|
|
2050
|
+
const backing = this.ctx._findTable(mv.name, mv.schemaName);
|
|
2051
|
+
if (!backing) {
|
|
2052
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
2053
|
+
}
|
|
2054
|
+
const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
|
|
2055
|
+
// Map each output attribute to a base-T source column (or `undefined` for a TVF
|
|
2056
|
+
// output column). T's attributes pass through the join unchanged, so a base-PK
|
|
2057
|
+
// output column resolves to a T column while a TVF-key output column does not.
|
|
2058
|
+
const sourceAttrToCol = new Map();
|
|
2059
|
+
tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
|
|
2060
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
2061
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
2062
|
+
if (!rootAttrs)
|
|
2063
|
+
return null;
|
|
2064
|
+
// Prefix soundness: the LEADING `basePrefixLen` backing-PK columns must each
|
|
2065
|
+
// project (transitively) a distinct base-T PK column, their set must equal the base
|
|
2066
|
+
// PK, and there must be a non-empty TVF-key tail. So the base PK is the leading
|
|
2067
|
+
// prefix of the composite product key and the by-prefix delete selects exactly one
|
|
2068
|
+
// base row's fan-out. A composite key that did not form this way falls to the floor.
|
|
2069
|
+
const basePrefixLen = sourcePkCols.length;
|
|
2070
|
+
if (backingPkDefinition.length <= basePrefixLen)
|
|
2071
|
+
return null;
|
|
2072
|
+
const basePkSet = new Set(sourcePkCols);
|
|
2073
|
+
const leadingSourceCols = new Set();
|
|
2074
|
+
const backingPrefixSourceCols = [];
|
|
2075
|
+
for (let i = 0; i < basePrefixLen; i++) {
|
|
2076
|
+
const d = backingPkDefinition[i];
|
|
2077
|
+
const attr = rootAttrs[d.index];
|
|
2078
|
+
const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
|
|
2079
|
+
if (sc === undefined || !basePkSet.has(sc))
|
|
2080
|
+
return null; // base PK not the leading prefix → floor
|
|
2081
|
+
// Soundness precondition for the binary prefix scan (the property
|
|
2082
|
+
// `prefix-delete-noncase-collation-regression-test` locks in): the backing base-PK
|
|
2083
|
+
// column MUST inherit the source PK column's collation. The btree orders this prefix
|
|
2084
|
+
// by `d.collation`, but the keyed diff's existing-slice read (`scanEffective` with
|
|
2085
|
+
// the base prefix, in `applyPrefixDelete`) early-terminates the prefix scan on a
|
|
2086
|
+
// BINARY compare (scan-layer.ts) — sound ONLY because source-PK uniqueness under that
|
|
2087
|
+
// collation collapses each collation class to a single binary value, so a base row's
|
|
2088
|
+
// fan-out rows are binary-homogeneous and contiguous. A backing collation MORE
|
|
2089
|
+
// permissive than the source's would let collation-equal/binary-different base rows
|
|
2090
|
+
// interleave and break that. The backing column derives its collation from the body
|
|
2091
|
+
// relation's type (deriveBackingShape), so a mismatch is an internal derivation bug —
|
|
2092
|
+
// fail loud rather than register an unsound plan.
|
|
2093
|
+
const backingColl = normalizeCollation(d.collation);
|
|
2094
|
+
const sourceColl = normalizeCollation(sourceSchema.columns[sc]?.collation);
|
|
2095
|
+
if (backingColl !== sourceColl) {
|
|
2096
|
+
throw new QuereusError(`Internal error: materialized view '${mv.name}' backing base-PK column `
|
|
2097
|
+
+ `'${backing.columns[d.index]?.name ?? d.index}' has collation '${backingColl}' but its source `
|
|
2098
|
+
+ `primary-key column '${sourceSchema.columns[sc]?.name ?? sc}' has collation '${sourceColl}'; `
|
|
2099
|
+
+ `the prefix-delete arm's binary prefix scan requires the backing base-PK column to inherit the `
|
|
2100
|
+
+ `source PK collation (see scan-layer.ts early-termination)`, StatusCode.INTERNAL);
|
|
2101
|
+
}
|
|
2102
|
+
leadingSourceCols.add(sc);
|
|
2103
|
+
backingPrefixSourceCols.push(sc);
|
|
2104
|
+
}
|
|
2105
|
+
if (leadingSourceCols.size !== basePkSet.size)
|
|
2106
|
+
return null; // prefix does not cover the base PK → floor
|
|
2107
|
+
// The TVF-key tail must NOT re-use a base-PK column — else the fan-out rows would
|
|
2108
|
+
// not be distinguished and the "key" would be base-only (defensive: the product key
|
|
2109
|
+
// places the TVF key, a distinct relation's columns, in the tail). Otherwise → floor.
|
|
2110
|
+
for (let i = basePrefixLen; i < backingPkDefinition.length; i++) {
|
|
2111
|
+
const d = backingPkDefinition[i];
|
|
2112
|
+
const attr = rootAttrs[d.index];
|
|
2113
|
+
const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
|
|
2114
|
+
if (sc !== undefined && basePkSet.has(sc))
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
// Compile + cache the base-PK-keyed residual once (the body with `T.pk = :pk0 AND …`
|
|
2118
|
+
// injected on T). Re-run per affected base key against the live transaction; it
|
|
2119
|
+
// re-runs the lateral join + TVF for that single base row, fanning out to N rows.
|
|
2120
|
+
const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
|
|
2121
|
+
const residualScheduler = this.compileResidual(analyzed, relKey, sourcePkCols, 'pk');
|
|
2122
|
+
if (!residualScheduler)
|
|
2123
|
+
return null; // could not parameterize the residual → floor
|
|
2124
|
+
// ── Cost gate ──
|
|
2125
|
+
// The fan-out residual shares the residual-recompute cost shape (a key-filtered
|
|
2126
|
+
// re-execution of the body); the fan-out factor (rows per base key) is not known at
|
|
2127
|
+
// create, so we cost it as a residual and record the choice for substrate parity.
|
|
2128
|
+
// The synchronous reject-at-create / degrade-to-rebuild machinery stays dormant, as
|
|
2129
|
+
// it does for the other arms (a TVF whose fan-out is pathological is not detectable
|
|
2130
|
+
// without fan-out stats — deferred with the fanning-keyed-join follow-up).
|
|
2131
|
+
const soundStrategies = ['residual-recompute'];
|
|
2132
|
+
const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
|
|
2133
|
+
const sourceStats = this.estimateMaintenanceStats(sourceSchema, backing.columns.length, hasPredicate);
|
|
2134
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
2135
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
2136
|
+
if (chosenStrategy !== 'residual-recompute') {
|
|
2137
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
2138
|
+
}
|
|
2139
|
+
return {
|
|
2140
|
+
kind: 'prefix-delete',
|
|
2141
|
+
mv,
|
|
2142
|
+
sourceBase,
|
|
2143
|
+
backingSchema: mv.schemaName,
|
|
2144
|
+
backingTableName: mv.name,
|
|
2145
|
+
chosenStrategy,
|
|
2146
|
+
sourceStats,
|
|
2147
|
+
binding: { kind: 'row', keyColumns: [...sourcePkCols] },
|
|
2148
|
+
degradeToRebuild: false,
|
|
2149
|
+
residualScheduler,
|
|
2150
|
+
bindParamPrefix: 'pk',
|
|
2151
|
+
bindColumns: sourcePkCols,
|
|
2152
|
+
backingPkDefinition,
|
|
2153
|
+
basePrefixLength: basePrefixLen,
|
|
2154
|
+
backingPrefixSourceCols,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Compute a `'prefix-delete'` plan's per-row backing delta and apply it: derive the
|
|
2159
|
+
* affected base key(s) from the changed row (OLD ∪ NEW, deduped on the base key), and
|
|
2160
|
+
* for each — re-run the base-PK-keyed residual against live source state and apply the
|
|
2161
|
+
* **keyed diff against the existing effective fan-out slice** (read via the host's
|
|
2162
|
+
* `scanEffective` with the base prefix, pending over committed — the same contiguous
|
|
2163
|
+
* range the former wholesale `'delete-by-prefix'` removed): delete ONLY the existing
|
|
2164
|
+
* keys the recompute no longer produces, upsert every recomputed row (the host's
|
|
2165
|
+
* value-identical skip suppresses the unchanged ones). A base-PK-changing UPDATE
|
|
2166
|
+
* recomputes both the OLD base key (slice diffs to all-deletes; the residual returns
|
|
2167
|
+
* nothing for the now-absent old PK) and the NEW base key (new fan-out upserted); a
|
|
2168
|
+
* DELETE diffs the old slice to all-deletes; an INSERT diffs against an empty slice
|
|
2169
|
+
* (all upserts). An emptied/shrunk fan-out keeps the delete-without-upsert exactly —
|
|
2170
|
+
* a disappearance is never "skipped". Returns the effective
|
|
2171
|
+
* {@link BackingRowChange}(s) the backing layer realized, for the MV-over-MV cascade.
|
|
2172
|
+
*
|
|
2173
|
+
* Prefix-scan soundness is unchanged from the wholesale arm: the diff's slice read
|
|
2174
|
+
* uses the same binary `equalityPrefix` scan `'delete-by-prefix'` used, sound under
|
|
2175
|
+
* the build-time collation gate (the backing base-PK prefix inherits the source PK
|
|
2176
|
+
* collation, and source-PK uniqueness collapses each collation class to one binary
|
|
2177
|
+
* value). The stored slice's prefix bytes always equal the OLD image's (the slice was
|
|
2178
|
+
* projected from that very source row), and OLD ∪ NEW both iterate, so a case-only
|
|
2179
|
+
* base-PK rewrite still converges: the OLD-prefix pass pairs the slice with the
|
|
2180
|
+
* recomputed rows (key pairing is collation-aware — the btree's identity — so a
|
|
2181
|
+
* collation-equal key is REPLACED by its upsert, never also deleted) and the byte
|
|
2182
|
+
* change surfaces as `update`s that re-key the stored bytes.
|
|
2183
|
+
*
|
|
2184
|
+
* Structurally the same as {@link applyForwardResidual}, differing only in the
|
|
2185
|
+
* **prefix-slice** diff (one base row owns N backing rows sharing the prefix) and the
|
|
2186
|
+
* **N-row** residual. Per-row recompute is correct without per-statement batching: the
|
|
2187
|
+
* residual reads live (reads-own-writes) state, so the last write to a base key produces
|
|
2188
|
+
* the authoritative slice. (Statement-level dedup of distinct base keys is the same
|
|
2189
|
+
* affordability optimization deferred for the aggregate arm.)
|
|
2190
|
+
*/
|
|
2191
|
+
async applyPrefixDelete(plan, change, cache) {
|
|
2192
|
+
// Distinct affected base keys (OLD ∪ NEW), deduped on the base-PK values. `keyTuple`
|
|
2193
|
+
// binds the residual (`pk{i}`); `prefix` is the slice's leading-PK equality key (the
|
|
2194
|
+
// base-PK values in backing-PK order — identical here since the base PK leads the
|
|
2195
|
+
// backing PK, but kept distinct for clarity).
|
|
2196
|
+
const affected = new Map();
|
|
2197
|
+
const addFrom = (row) => {
|
|
2198
|
+
const keyTuple = plan.bindColumns.map(c => row[c]);
|
|
2199
|
+
const dedupKey = canonKeyValues(keyTuple);
|
|
2200
|
+
if (affected.has(dedupKey))
|
|
2201
|
+
return;
|
|
2202
|
+
affected.set(dedupKey, { keyTuple, prefix: plan.backingPrefixSourceCols.map(sc => row[sc]) });
|
|
2203
|
+
};
|
|
2204
|
+
if (change.op === 'insert')
|
|
2205
|
+
addFrom(change.newRow);
|
|
2206
|
+
else if (change.op === 'delete')
|
|
2207
|
+
addFrom(change.oldRow);
|
|
2208
|
+
else {
|
|
2209
|
+
addFrom(change.oldRow);
|
|
2210
|
+
addFrom(change.newRow);
|
|
2211
|
+
}
|
|
2212
|
+
// Resolved up front (unlike the point-op arms): the keyed diff reads the existing
|
|
2213
|
+
// effective slice before any op exists. The former wholesale arm always emitted ops,
|
|
2214
|
+
// so this resolves no more connections than it did.
|
|
2215
|
+
const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
2216
|
+
if (!backing) {
|
|
2217
|
+
throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
|
|
2218
|
+
}
|
|
2219
|
+
const host = this.backingHost(backing);
|
|
2220
|
+
const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
|
|
2221
|
+
const ops = [];
|
|
2222
|
+
for (const { keyTuple, prefix } of affected.values()) {
|
|
2223
|
+
const recomputed = await this.runResidual(plan.residualScheduler, plan.bindParamPrefix, keyTuple);
|
|
2224
|
+
// The residual for base key K filters T to K, so every row it returns shares K's
|
|
2225
|
+
// base-PK prefix; the prefix-match guard is a defensive soundness net (mirrors
|
|
2226
|
+
// the aggregate arm's `residualRowMatchesKey`).
|
|
2227
|
+
const slice = recomputed.filter(row => this.residualRowMatchesBasePrefix(plan, row, prefix));
|
|
2228
|
+
// Existing effective fan-out rows for this base prefix (pending over committed).
|
|
2229
|
+
const existing = [];
|
|
2230
|
+
for await (const row of host.scanEffective(connection, { equalityPrefix: prefix })) {
|
|
2231
|
+
existing.push(row);
|
|
2232
|
+
}
|
|
2233
|
+
// Keyed diff. Key pairing is collation-aware over the full backing PK (the btree's
|
|
2234
|
+
// identity): a recomputed row whose key is collation-equal to an existing row
|
|
2235
|
+
// REPLACES it via the upsert below, so it must not also be deleted. Deletes precede
|
|
2236
|
+
// upserts (the wholesale arm's ordering discipline). The delete keys are built from
|
|
2237
|
+
// the EXISTING rows' stored values, so the host's collation-aware point lookup
|
|
2238
|
+
// always finds them.
|
|
2239
|
+
for (const ex of existing) {
|
|
2240
|
+
if (slice.some(row => this.backingPkEqual(plan.backingPkDefinition, row, ex)))
|
|
2241
|
+
continue;
|
|
2242
|
+
ops.push({
|
|
2243
|
+
kind: 'delete-key',
|
|
2244
|
+
key: buildPrimaryKeyFromValues(plan.backingPkDefinition.map(d => ex[d.index]), plan.backingPkDefinition),
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
for (const row of slice)
|
|
2248
|
+
ops.push({ kind: 'upsert', row });
|
|
2249
|
+
}
|
|
2250
|
+
if (ops.length === 0)
|
|
2251
|
+
return [];
|
|
2252
|
+
return host.applyMaintenance(connection, ops);
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* True iff two backing rows agree on every backing-PK column under that column's
|
|
2256
|
+
* collation — the btree's key identity. Pairs an existing slice row with the
|
|
2257
|
+
* recomputed row that replaces it in {@link applyPrefixDelete}'s keyed diff.
|
|
2258
|
+
*/
|
|
2259
|
+
backingPkEqual(pkDef, a, b) {
|
|
2260
|
+
for (const d of pkDef) {
|
|
2261
|
+
if (compareSqlValues(a[d.index], b[d.index], d.collation) !== 0)
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
return true;
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* True iff `row`'s **leading** (base-prefix) backing-PK columns equal `prefixVals` (the
|
|
2268
|
+
* affected base key, in backing-PK order), under each column's collation. Keeps only the
|
|
2269
|
+
* residual fan-out row(s) belonging to the recomputed base key — see
|
|
2270
|
+
* {@link applyPrefixDelete}.
|
|
2271
|
+
*/
|
|
2272
|
+
residualRowMatchesBasePrefix(plan, row, prefixVals) {
|
|
2273
|
+
for (let i = 0; i < plan.basePrefixLength; i++) {
|
|
2274
|
+
const d = plan.backingPkDefinition[i];
|
|
2275
|
+
if (compareSqlValues(row[d.index], prefixVals[i], d.collation) !== 0)
|
|
2276
|
+
return false;
|
|
2277
|
+
}
|
|
2278
|
+
return true;
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Assemble {@link MaintenanceSourceStats} for the cost gate from the optimizer's
|
|
2282
|
+
* StatsProvider and tuning. `tableRows` / `distinctGroupsEstimate` come from the
|
|
2283
|
+
* provider (heuristic defaults when absent); `forwardBodyCost` is estimated from the
|
|
2284
|
+
* forward cost helpers (a scan + optional filter + projection of the source — the
|
|
2285
|
+
* covering-index body shape); `fallbackRatio` carries the detection kernel's
|
|
2286
|
+
* `deltaPerRowFallbackRatio` for the no-stats residual path.
|
|
2287
|
+
*/
|
|
2288
|
+
/**
|
|
2289
|
+
* The CURRENT `TableSchema` of a source `TableReferenceNode`, re-resolved through the
|
|
2290
|
+
* schema manager. A plan node captures the schema as of plan-build; a later `analyze`
|
|
2291
|
+
* replaces the catalog entry with one carrying fresh `statistics`, so the stale captured
|
|
2292
|
+
* schema would report pre-`analyze` row counts. Re-resolving keeps the floor's size gate
|
|
2293
|
+
* on the live source size. Falls back to the node's captured schema if the name no longer
|
|
2294
|
+
* resolves (it always should — the body planned).
|
|
2295
|
+
*/
|
|
2296
|
+
liveSourceSchema(ref) {
|
|
2297
|
+
const captured = ref.tableSchema;
|
|
2298
|
+
return this.ctx._findTable(captured.name, captured.schemaName) ?? captured;
|
|
2299
|
+
}
|
|
2300
|
+
estimateMaintenanceStats(sourceSchema, projectionCount, hasPredicate) {
|
|
2301
|
+
const optimizer = this.ctx.optimizer;
|
|
2302
|
+
const statsProvider = optimizer.getStats();
|
|
2303
|
+
const tableRows = statsProvider.tableRows(sourceSchema) ?? DEFAULT_SOURCE_ROWS;
|
|
2304
|
+
const forwardBodyCost = seqScanCost(tableRows)
|
|
2305
|
+
+ (hasPredicate ? filterCost(tableRows) : 0)
|
|
2306
|
+
+ projectCost(tableRows, projectionCount);
|
|
2307
|
+
const stats = {
|
|
2308
|
+
tableRows,
|
|
2309
|
+
forwardBodyCost,
|
|
2310
|
+
fallbackRatio: optimizer.tuning.deltaPerRowFallbackRatio,
|
|
2311
|
+
};
|
|
2312
|
+
// `distinctValues` is an optional, per-column StatsProvider method. For the
|
|
2313
|
+
// covering-index shape the source PK is the grouping key; a single-column PK
|
|
2314
|
+
// yields a usable distinct-groups estimate (which only feeds the never-chosen-here
|
|
2315
|
+
// residual cost). Multi-column PKs leave it unset → residual takes the no-stats path.
|
|
2316
|
+
const pkDef = sourceSchema.primaryKeyDefinition;
|
|
2317
|
+
if (pkDef.length === 1 && statsProvider.distinctValues) {
|
|
2318
|
+
const pkColName = sourceSchema.columns[pkDef[0].index]?.name;
|
|
2319
|
+
if (pkColName !== undefined) {
|
|
2320
|
+
const distinct = statsProvider.distinctValues(sourceSchema, pkColName);
|
|
2321
|
+
if (distinct !== undefined)
|
|
2322
|
+
stats.distinctGroupsEstimate = distinct;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return stats;
|
|
2326
|
+
}
|
|
2327
|
+
/* ──────────────── row-time covering enforcement ──────────────── */
|
|
2328
|
+
/**
|
|
2329
|
+
* Resolve the linked, enforcement-ready covering MV for a UNIQUE constraint on
|
|
2330
|
+
* `schema.table`, or `undefined`. The constraint's `coveringStructureName`
|
|
2331
|
+
* forward pointer (set by the eager prove-and-link) is the source of truth;
|
|
2332
|
+
* this confirms a live row-time plan exists for the source, the MV is not
|
|
2333
|
+
* `stale` (structural breakage), and the plan is **per-row maintained** — only
|
|
2334
|
+
* then is its backing table row-time consistent enough to answer conflict
|
|
2335
|
+
* resolution. A `'full-rebuild'` plan is deferred to the end-of-statement flush
|
|
2336
|
+
* (its backing lags the source mid-statement), so it can never serve as a
|
|
2337
|
+
* covering structure for a synchronous per-row UNIQUE probe — it is skipped here
|
|
2338
|
+
* regardless of any (informational) `coveringStructureName` link, which keeps the
|
|
2339
|
+
* eligibility flip from opening a stale-read enforcement path. O(1) negative fast
|
|
2340
|
+
* path off {@link rowTimeBySource} so a source table with no row-time covering MV
|
|
2341
|
+
* pays a single map lookup and stays on the synchronous index/scan path.
|
|
2342
|
+
*
|
|
2343
|
+
* **Collation eligibility gate.** A covering MV generates its conflict candidates
|
|
2344
|
+
* by re-comparing each backing row under the SOURCE column's DECLARED collation
|
|
2345
|
+
* ({@link lookupCoveringConflicts} / {@link tryBuildCoveringPrefix}), while the
|
|
2346
|
+
* re-validators (store `findUniqueConflictViaCoveringMv`, memory
|
|
2347
|
+
* `checkUniqueViaMaterializedView`) filter under the index per-column collation. The
|
|
2348
|
+
* candidate set is a sound *superset* of the index-collation matches — safe to filter
|
|
2349
|
+
* down — only when the index collation is coarser-or-equal to the declared collation
|
|
2350
|
+
* per constrained column (see {@link coveringMvHonorsIndexCollation}). For a
|
|
2351
|
+
* finer/incomparable index-derived UNIQUE (e.g. a coarser NOCASE index over a BINARY
|
|
2352
|
+
* column) the candidate set may be a *subset* that silently misses conflicts, so the
|
|
2353
|
+
* MV is declined here and enforcement falls back to the per-scan / auto-index path
|
|
2354
|
+
* (already correct under the index collation). All three callers (store, memory,
|
|
2355
|
+
* lens-prover) consult this resolver, so they decline the same MV in lockstep and
|
|
2356
|
+
* candidate generation never runs for a declined MV. This gate is load-bearing, not
|
|
2357
|
+
* mere defense-in-depth: the covering-link prover's own collation gate compares the
|
|
2358
|
+
* OUTPUT column collation against the DECLARED base-column collation (not the index
|
|
2359
|
+
* collation), so it DOES link a coarser-index covering MV — confirmed by the
|
|
2360
|
+
* premise-check test in `covering-structure.spec.ts`.
|
|
2361
|
+
*/
|
|
2362
|
+
findRowTimeCoveringStructure(schemaName, tableName, uc) {
|
|
2363
|
+
const sourceBase = `${schemaName}.${tableName}`.toLowerCase();
|
|
2364
|
+
const keys = this.rowTimeBySource.get(sourceBase);
|
|
2365
|
+
if (!keys || keys.size === 0)
|
|
2366
|
+
return undefined; // O(1) negative fast path
|
|
2367
|
+
const mvName = this.resolveCoveringStructureName(schemaName, tableName, uc);
|
|
2368
|
+
if (!mvName)
|
|
2369
|
+
return undefined;
|
|
2370
|
+
for (const key of keys) {
|
|
2371
|
+
const plan = this.rowTime.get(key);
|
|
2372
|
+
if (!plan)
|
|
2373
|
+
continue;
|
|
2374
|
+
const mv = plan.mv;
|
|
2375
|
+
if (mv.name !== mvName)
|
|
2376
|
+
continue; // must be THE linked covering MV
|
|
2377
|
+
// A deferred full-rebuild MV is not per-row consistent (reconciled only at
|
|
2378
|
+
// the end-of-statement flush), so it cannot answer a synchronous probe.
|
|
2379
|
+
if (plan.chosenStrategy === 'full-rebuild')
|
|
2380
|
+
return undefined;
|
|
2381
|
+
if (mv.derivation.stale)
|
|
2382
|
+
return undefined; // not row-time consistent
|
|
2383
|
+
// Decline the MV when its declared-collation candidate set is not a sound
|
|
2384
|
+
// superset of the index-collation matches (finer/incomparable index-derived
|
|
2385
|
+
// UNIQUE). Resolve the source schema for the declared/index collations; if it
|
|
2386
|
+
// cannot be resolved, fall through to the existing behavior rather than throw
|
|
2387
|
+
// (mirrors the `if (!index) …` tolerance elsewhere).
|
|
2388
|
+
const sourceSchema = this.ctx._findTable(tableName, schemaName);
|
|
2389
|
+
if (sourceSchema && !coveringMvHonorsIndexCollation(sourceSchema, uc))
|
|
2390
|
+
return undefined;
|
|
2391
|
+
return mv;
|
|
2392
|
+
}
|
|
2393
|
+
return undefined;
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Resolve a constraint's `coveringStructureName` forward pointer. Prefers the
|
|
2397
|
+
* pointer already on the passed `uc` (the memory source shares the
|
|
2398
|
+
* schema-manager's frozen constraint, so the eager link's mutation is visible).
|
|
2399
|
+
* A store source holds a *copied* schema whose constraint never received the
|
|
2400
|
+
* mutation, so fall back to the authoritative schema-manager constraint matched
|
|
2401
|
+
* by column set — keeping the covering-structure lookup module-agnostic.
|
|
2402
|
+
*/
|
|
2403
|
+
resolveCoveringStructureName(schemaName, tableName, uc) {
|
|
2404
|
+
if (uc.coveringStructureName)
|
|
2405
|
+
return uc.coveringStructureName;
|
|
2406
|
+
const table = this.ctx._findTable(tableName, schemaName);
|
|
2407
|
+
const live = table?.uniqueConstraints?.find(c => c.columns.length === uc.columns.length
|
|
2408
|
+
&& c.columns.every((col, i) => col === uc.columns[i]));
|
|
2409
|
+
return live?.coveringStructureName;
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Point-look up the covering MV's backing table for rows whose backing columns
|
|
2413
|
+
* equal `newRow`'s UNIQUE-constraint values, recover each conflicting **source**
|
|
2414
|
+
* PK from the projected PK columns, and exclude the row being written
|
|
2415
|
+
* (`newSourcePk`). Returns the conflicting source PK(s) — the caller resolves
|
|
2416
|
+
* IGNORE/ABORT/REPLACE against its own source storage (recovering the live
|
|
2417
|
+
* source row and validating the candidate against it, since the backing entry
|
|
2418
|
+
* for an internally-deleted/updated source row can lag within a statement).
|
|
2419
|
+
*
|
|
2420
|
+
* Reads-own-writes: the scan resolves to the backing table's coordinated
|
|
2421
|
+
* connection (the same one {@link maintainRowTime} writes), so the backing
|
|
2422
|
+
* reflects all prior rows of the statement. The backing is hosted by whatever
|
|
2423
|
+
* backing-host-capable module the MV declared (`memory` by default, the store
|
|
2424
|
+
* module under `using store`), independent of the source module — the host's
|
|
2425
|
+
* `scanEffective` abstracts the storage.
|
|
2426
|
+
*
|
|
2427
|
+
* The conflict check is a **backing-PK prefix scan** keyed on `newRow`'s UC
|
|
2428
|
+
* values — O(log n + matches) rather than the former O(n) full backing scan.
|
|
2429
|
+
* Soundness rests on the covering-index shape: the body's `order by` columns are
|
|
2430
|
+
* a permutation of the UC columns ({@link buildMaintenancePlan} eligibility +
|
|
2431
|
+
* the coverage prover), and they seed the leading backing-PK columns
|
|
2432
|
+
* (`computeBackingPrimaryKey`), so the leading `k = uc.columns.length` backing-PK
|
|
2433
|
+
* columns are exactly the UC columns. {@link tryBuildCoveringPrefix} builds the
|
|
2434
|
+
* equality prefix in backing-PK column order; the scan seeks to it and
|
|
2435
|
+
* early-terminates when the leading columns stop matching. It falls back to a
|
|
2436
|
+
* full scan whenever the fast-path gate fails (non-BINARY collation, or a
|
|
2437
|
+
* leading-prefix shape that does not lead with exactly the UC columns) — the
|
|
2438
|
+
* full scan re-compares with the source collation, so the fallback is
|
|
2439
|
+
* collation-correct. Either way the result is only a *candidate* set: the caller
|
|
2440
|
+
* validates each against the live source row.
|
|
2441
|
+
*/
|
|
2442
|
+
async lookupCoveringConflicts(mv, uc, newRow, newSourcePk) {
|
|
2443
|
+
const plan = this.rowTime.get(mvKey(mv.schemaName, mv.name));
|
|
2444
|
+
if (!plan)
|
|
2445
|
+
return [];
|
|
2446
|
+
// Covering-conflict resolution reads the inverse projection (source↔backing
|
|
2447
|
+
// column map). Only the `'inverse-projection'` arm carries it; the other arms do
|
|
2448
|
+
// not cover a source UNIQUE constraint in the covering sense, so a covering
|
|
2449
|
+
// structure is never linked to one — defensively skip if reached.
|
|
2450
|
+
if (plan.kind !== 'inverse-projection')
|
|
2451
|
+
return [];
|
|
2452
|
+
const [srcSchemaName, srcTableName] = plan.sourceBase.split('.');
|
|
2453
|
+
const sourceSchema = this.ctx._findTable(srcTableName, srcSchemaName);
|
|
2454
|
+
if (!sourceSchema)
|
|
2455
|
+
return [];
|
|
2456
|
+
// Inverse projection: source column index → backing column index (first
|
|
2457
|
+
// occurrence). Only the passthrough projectors carry a source-column identity
|
|
2458
|
+
// (a computed `'expr'` column has no inverse), and the eligibility gate forces
|
|
2459
|
+
// every PK / UNIQUE-covered column to be passthrough, so conflict resolution is
|
|
2460
|
+
// unaffected by any extra computed columns the body also projects.
|
|
2461
|
+
const sourceColToBacking = new Map();
|
|
2462
|
+
plan.projectors.forEach((p, backingCol) => {
|
|
2463
|
+
if (p.kind === 'passthrough' && !sourceColToBacking.has(p.sourceCol)) {
|
|
2464
|
+
sourceColToBacking.set(p.sourceCol, backingCol);
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
const ucBackingCols = [];
|
|
2468
|
+
for (const c of uc.columns) {
|
|
2469
|
+
const b = sourceColToBacking.get(c);
|
|
2470
|
+
if (b === undefined)
|
|
2471
|
+
return []; // the prover guarantees this; defensive
|
|
2472
|
+
ucBackingCols.push(b);
|
|
2473
|
+
}
|
|
2474
|
+
const pkDef = sourceSchema.primaryKeyDefinition;
|
|
2475
|
+
const pkBackingCols = [];
|
|
2476
|
+
for (const d of pkDef) {
|
|
2477
|
+
const b = sourceColToBacking.get(d.index);
|
|
2478
|
+
if (b === undefined)
|
|
2479
|
+
return [];
|
|
2480
|
+
pkBackingCols.push(b);
|
|
2481
|
+
}
|
|
2482
|
+
const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
|
|
2483
|
+
if (!backing)
|
|
2484
|
+
return [];
|
|
2485
|
+
const host = this.backingHost(backing);
|
|
2486
|
+
const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`);
|
|
2487
|
+
const conflicts = [];
|
|
2488
|
+
// Fast path: a backing-PK prefix scan keyed on `newRow`'s UC values. The
|
|
2489
|
+
// covering-index shape guarantees the leading backing-PK columns are the UC
|
|
2490
|
+
// columns, so this seeks to the matching block and early-terminates instead of
|
|
2491
|
+
// scanning the whole backing. `undefined` ⇒ the gate failed (non-binary
|
|
2492
|
+
// collation / unexpected shape) and we fall back to the full effective scan,
|
|
2493
|
+
// which re-compares with the source collation and is therefore
|
|
2494
|
+
// collation-correct. The host executes the scan over the connection's
|
|
2495
|
+
// effective (reads-own-writes) state; the binary-collation soundness gate
|
|
2496
|
+
// stays engine-side in {@link tryBuildCoveringPrefix}.
|
|
2497
|
+
const equalityPrefix = this.tryBuildCoveringPrefix(plan, uc, sourceSchema, newRow);
|
|
2498
|
+
for await (const backingRow of host.scanEffective(connection, { equalityPrefix })) {
|
|
2499
|
+
let match = true;
|
|
2500
|
+
for (let k = 0; k < uc.columns.length; k++) {
|
|
2501
|
+
const coll = sourceSchema.columns[uc.columns[k]]?.collation;
|
|
2502
|
+
if (compareSqlValues(newRow[uc.columns[k]], backingRow[ucBackingCols[k]], coll) !== 0) {
|
|
2503
|
+
match = false;
|
|
2504
|
+
break;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
if (!match)
|
|
2508
|
+
continue;
|
|
2509
|
+
const sourcePk = pkBackingCols.map(b => backingRow[b]);
|
|
2510
|
+
// Exclude the row currently being written (its own source PK).
|
|
2511
|
+
let isSelf = sourcePk.length === newSourcePk.length;
|
|
2512
|
+
for (let i = 0; isSelf && i < sourcePk.length; i++) {
|
|
2513
|
+
if (compareSqlValues(sourcePk[i], newSourcePk[i], pkDef[i]?.collation) !== 0)
|
|
2514
|
+
isSelf = false;
|
|
2515
|
+
}
|
|
2516
|
+
if (isSelf)
|
|
2517
|
+
continue;
|
|
2518
|
+
conflicts.push({ pk: sourcePk });
|
|
2519
|
+
}
|
|
2520
|
+
return conflicts;
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Build the backing-PK equality prefix for a covering-conflict scan, or
|
|
2524
|
+
* `undefined` to fall back to the full backing scan.
|
|
2525
|
+
*
|
|
2526
|
+
* The covering-index shape guarantees the body's `order by` columns are a
|
|
2527
|
+
* permutation of the UC columns and that they seed the leading backing-PK columns
|
|
2528
|
+
* (`computeBackingPrimaryKey`). So the leading `k = uc.columns.length` backing-PK
|
|
2529
|
+
* columns are exactly the UC columns (as a set, possibly reordered by `order by`).
|
|
2530
|
+
* The returned prefix is keyed in **backing-PK column order** (not `uc.columns`
|
|
2531
|
+
* order), so a permuting `order by` still seeks to the right block:
|
|
2532
|
+
* `prefix[i] = newRow[ sourceCol(backingPkDefinition[i]) ]`.
|
|
2533
|
+
*
|
|
2534
|
+
* Returns `undefined` (full-scan fallback) when any holds:
|
|
2535
|
+
* - fewer than `k` backing-PK columns, or a leading column is not a passthrough
|
|
2536
|
+
* of a source column (defensive — the covering shape guarantees passthrough);
|
|
2537
|
+
* - the leading `k` backing-PK columns do not map to **exactly** the UC
|
|
2538
|
+
* source-column set (defensive guard against a non-UC-leading structure);
|
|
2539
|
+
* - any leading backing-PK column, or its source UC column, has a **non-BINARY**
|
|
2540
|
+
* collation. This is a *soundness* gate, not a perf choice: the prefix seek's
|
|
2541
|
+
* early-termination compares with plain `compareSqlValues` (binary), while the
|
|
2542
|
+
* backing btree orders the PK by its declared collation and the UNIQUE
|
|
2543
|
+
* constraint conflicts by the source collation. Under a non-binary collation
|
|
2544
|
+
* the binary early-termination could `break` before a collated-equal /
|
|
2545
|
+
* binary-different conflict, missing it. The full-scan fallback re-compares
|
|
2546
|
+
* with the source collation, so it stays collation-correct.
|
|
2547
|
+
*
|
|
2548
|
+
* DESC-leading prefixes are admitted: equality on a column makes its order
|
|
2549
|
+
* direction irrelevant to *grouping* (the binary-equal rows stay contiguous), and
|
|
2550
|
+
* `scanLayer`'s `equalityPrefix` seek + ascending walk lands at the group start
|
|
2551
|
+
* for either direction (verified by the `order by … desc` enforcement test).
|
|
2552
|
+
*/
|
|
2553
|
+
tryBuildCoveringPrefix(plan, uc, sourceSchema, newRow) {
|
|
2554
|
+
const k = uc.columns.length;
|
|
2555
|
+
const backingPk = plan.backingPkDefinition;
|
|
2556
|
+
if (backingPk.length < k)
|
|
2557
|
+
return undefined;
|
|
2558
|
+
const ucSourceCols = new Set(uc.columns);
|
|
2559
|
+
const leadingSourceCols = new Set();
|
|
2560
|
+
const prefix = [];
|
|
2561
|
+
for (let i = 0; i < k; i++) {
|
|
2562
|
+
const d = backingPk[i];
|
|
2563
|
+
const projector = plan.projectors[d.index];
|
|
2564
|
+
if (!projector || projector.kind !== 'passthrough')
|
|
2565
|
+
return undefined;
|
|
2566
|
+
// Soundness: both the backing-PK column (btree ordering / early-termination)
|
|
2567
|
+
// and its source UC column (UNIQUE semantics) must be BINARY for the binary
|
|
2568
|
+
// prefix-equality scan to neither over- nor under-match.
|
|
2569
|
+
if (!isBinaryCollation(d.collation))
|
|
2570
|
+
return undefined;
|
|
2571
|
+
const sourceCol = projector.sourceCol;
|
|
2572
|
+
if (!isBinaryCollation(sourceSchema.columns[sourceCol]?.collation))
|
|
2573
|
+
return undefined;
|
|
2574
|
+
leadingSourceCols.add(sourceCol);
|
|
2575
|
+
prefix.push(newRow[sourceCol]);
|
|
2576
|
+
}
|
|
2577
|
+
// The leading `k` backing-PK columns must be exactly the UC source columns.
|
|
2578
|
+
if (leadingSourceCols.size !== ucSourceCols.size)
|
|
2579
|
+
return undefined;
|
|
2580
|
+
for (const c of ucSourceCols) {
|
|
2581
|
+
if (!leadingSourceCols.has(c))
|
|
2582
|
+
return undefined;
|
|
2583
|
+
}
|
|
2584
|
+
return prefix;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
/* ─────────────────────────── helpers ─────────────────────────── */
|
|
2588
|
+
/** True for the default (binary) collation: an absent name or a case-insensitive
|
|
2589
|
+
* `BINARY`. Non-binary collations gate off the prefix-scan fast path (see
|
|
2590
|
+
* {@link MaterializedViewManager.tryBuildCoveringPrefix}). */
|
|
2591
|
+
function isBinaryCollation(collation) {
|
|
2592
|
+
return collation === undefined || collation.toUpperCase() === 'BINARY';
|
|
2593
|
+
}
|
|
2594
|
+
/** Canonical upper-case collation name (absent ⇒ `BINARY`). Used to compare a backing-PK
|
|
2595
|
+
* column's collation against its source PK column's at plan-build (see
|
|
2596
|
+
* {@link MaterializedViewManager.buildLateralTvfPrefixDeletePlan}). */
|
|
2597
|
+
function normalizeCollation(collation) {
|
|
2598
|
+
return (collation ?? 'BINARY').toUpperCase();
|
|
2599
|
+
}
|
|
2600
|
+
function mvKey(schemaName, name) {
|
|
2601
|
+
return `${schemaName}.${name}`.toLowerCase();
|
|
2602
|
+
}
|
|
2603
|
+
/** Every source base (lowercased `schema.table`) a plan must be indexed under in
|
|
2604
|
+
* `rowTimeBySource`. Single-source arms read one base; the 1:1-join arm also reads
|
|
2605
|
+
* the lookup base, so a write to `P` fires maintenance too; the full-rebuild floor reads
|
|
2606
|
+
* every source its body touches (set-op legs, all join sources). */
|
|
2607
|
+
function planSourceBases(plan) {
|
|
2608
|
+
if (plan.kind === 'full-rebuild') {
|
|
2609
|
+
return plan.sourceBases;
|
|
2610
|
+
}
|
|
2611
|
+
if (plan.kind === 'join-residual' && plan.lookupBase !== plan.sourceBase) {
|
|
2612
|
+
return [plan.sourceBase, plan.lookupBase];
|
|
2613
|
+
}
|
|
2614
|
+
return [plan.sourceBase];
|
|
2615
|
+
}
|
|
2616
|
+
/** Walk the whole plan; return the string form of the first non-deterministic scalar
|
|
2617
|
+
* expression (a `random()`/`now()`/volatile UDF, anywhere in the body), or `undefined`
|
|
2618
|
+
* when the body is fully deterministic. The full-rebuild floor's whole-body determinism
|
|
2619
|
+
* gate uses this — a non-deterministic body can never be kept equal to its plain view.
|
|
2620
|
+
* `physical.deterministic` is computed lazily and propagates from leaves, so checking each
|
|
2621
|
+
* scalar node is sound on either the pre-physical or optimized plan. */
|
|
2622
|
+
function findNonDeterministic(node) {
|
|
2623
|
+
if (isScalarNode(node)) {
|
|
2624
|
+
const det = checkDeterministic(node);
|
|
2625
|
+
if (!det.valid)
|
|
2626
|
+
return det.expression ?? node.toString();
|
|
2627
|
+
}
|
|
2628
|
+
for (const child of node.getChildren()) {
|
|
2629
|
+
const found = findNonDeterministic(child);
|
|
2630
|
+
if (found)
|
|
2631
|
+
return found;
|
|
2632
|
+
}
|
|
2633
|
+
return undefined;
|
|
2634
|
+
}
|
|
2635
|
+
/** Walk the whole plan; return the NAME of the first function whose schema is not declared
|
|
2636
|
+
* REPLICABLE (bit-identical across peers/platforms/app-versions — built-ins auto-qualify),
|
|
2637
|
+
* or `undefined` when every function in the body qualifies. Mirrors {@link findNonDeterministic}'s
|
|
2638
|
+
* `getChildren()` recursion so nested calls (a UDF inside a builtin inside a UDF) and the
|
|
2639
|
+
* WHERE / GROUP BY / aggregate-arg / TVF-arg positions are all reached. The structural
|
|
2640
|
+
* `'functionSchema' in node` test covers all four function-bearing node kinds uniformly —
|
|
2641
|
+
* scalar (`function.ts`), aggregate (`aggregate-function.ts`), TVF call
|
|
2642
|
+
* (`table-function-call.ts`), and TVF reference (`reference.ts`) — without per-type imports.
|
|
2643
|
+
* Window functions live in a separate builtin-only registry with no UDF registration path and
|
|
2644
|
+
* carry no scalar/aggregate/TVF `functionSchema` on these nodes, so they are inherently
|
|
2645
|
+
* replicable and are never flagged. Consumed only when the backing host declares
|
|
2646
|
+
* `requiresReplicableDerivations` (see {@link MaterializedViewManager.buildMaintenancePlan}). */
|
|
2647
|
+
function findNonReplicableFunction(node) {
|
|
2648
|
+
if ('functionSchema' in node) {
|
|
2649
|
+
const schema = node.functionSchema;
|
|
2650
|
+
if (schema.replicable !== true)
|
|
2651
|
+
return schema.name;
|
|
2652
|
+
}
|
|
2653
|
+
for (const child of node.getChildren()) {
|
|
2654
|
+
const found = findNonReplicableFunction(child);
|
|
2655
|
+
if (found)
|
|
2656
|
+
return found;
|
|
2657
|
+
}
|
|
2658
|
+
return undefined;
|
|
2659
|
+
}
|
|
2660
|
+
/** The built-in collation names. These are pure JS string operations (`<`/`>`,
|
|
2661
|
+
* locale-independent `toLowerCase()`, ASCII-space trim), so they are bit-identical
|
|
2662
|
+
* across peers' JS engines and auto-qualify as REPLICABLE — exactly parallel to why
|
|
2663
|
+
* built-in functions do. A custom collation must opt in with `replicable: true` at
|
|
2664
|
+
* registration. Short-circuiting on name (regardless of `collationSource`) keeps the
|
|
2665
|
+
* walk free of rank reasoning: a `default` BINARY and an `explicit` NOCASE both pass;
|
|
2666
|
+
* only a custom name is ever subjected to `_isCollationReplicable`. */
|
|
2667
|
+
const BUILTIN_COLLATION_NAMES = new Set(['BINARY', 'NOCASE', 'RTRIM']);
|
|
2668
|
+
/** True when `collation` is a non-builtin name the database does not assert REPLICABLE.
|
|
2669
|
+
* `undefined`/builtin/replicable ⇒ not offending. */
|
|
2670
|
+
function collationIsOffending(collation, db) {
|
|
2671
|
+
if (collation === undefined)
|
|
2672
|
+
return false;
|
|
2673
|
+
const norm = normalizeCollationName(collation);
|
|
2674
|
+
if (BUILTIN_COLLATION_NAMES.has(norm))
|
|
2675
|
+
return false;
|
|
2676
|
+
return !db._isCollationReplicable(norm);
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* The collation analogue of {@link findNonReplicableFunction}: return the NAME of the
|
|
2680
|
+
* first collation that governs derived bytes and is neither built-in nor declared
|
|
2681
|
+
* REPLICABLE, or `undefined` when every collation qualifies. Two sources, soundness-first
|
|
2682
|
+
* (any non-builtin non-replicable collation anywhere rejects — see the soundness note in
|
|
2683
|
+
* `replicable-collation-class`):
|
|
2684
|
+
*
|
|
2685
|
+
* 1. **Body scalars** — every fold/order/key site (explicit `COLLATE`, a declared/default
|
|
2686
|
+
* column collation, a comparison's effective collation, ORDER BY / GROUP BY / DISTINCT
|
|
2687
|
+
* keys) resolves through some scalar node whose `getType().collationName` carries the
|
|
2688
|
+
* name. One `getChildren()` walk reading that field uniformly reaches them all, including
|
|
2689
|
+
* nested COLLATE, subquery/CTE/set-op legs, and MV-over-MV bodies (whose source columns
|
|
2690
|
+
* carry the producing backing's published collation).
|
|
2691
|
+
* 2. **Backing key** — a custom collation can govern the backing key MERGE without appearing
|
|
2692
|
+
* on any body scalar type (a maintained table declared with an explicit
|
|
2693
|
+
* `UNIQUE (… COLLATE custom)` or PK collation the SELECT body never names). The body walk
|
|
2694
|
+
* alone would miss it, so the maintained table's own PK column collations + declared
|
|
2695
|
+
* secondary UNIQUE per-column enforcement collations (resolving an index-derived override
|
|
2696
|
+
* via {@link uniqueEnforcementCollations}) are checked directly — the robust closure.
|
|
2697
|
+
*
|
|
2698
|
+
* Consumed only when the backing host declares `requiresReplicableDerivations`.
|
|
2699
|
+
*/
|
|
2700
|
+
function findNonReplicableCollation(node, mv, db) {
|
|
2701
|
+
const bodyOffender = findNonReplicableBodyCollation(node, db);
|
|
2702
|
+
if (bodyOffender !== undefined)
|
|
2703
|
+
return bodyOffender;
|
|
2704
|
+
return findNonReplicableKeyCollation(mv, db);
|
|
2705
|
+
}
|
|
2706
|
+
/** Source 1: walk the plan; first scalar node whose resolved `collationName` is a
|
|
2707
|
+
* non-builtin non-replicable collation. Mirrors {@link findNonReplicableFunction}'s
|
|
2708
|
+
* recursion so every body position is reached. */
|
|
2709
|
+
function findNonReplicableBodyCollation(node, db) {
|
|
2710
|
+
if (isScalarNode(node)) {
|
|
2711
|
+
const collation = node.getType().collationName;
|
|
2712
|
+
if (collationIsOffending(collation, db))
|
|
2713
|
+
return normalizeCollationName(collation);
|
|
2714
|
+
}
|
|
2715
|
+
for (const child of node.getChildren()) {
|
|
2716
|
+
const found = findNonReplicableBodyCollation(child, db);
|
|
2717
|
+
if (found !== undefined)
|
|
2718
|
+
return found;
|
|
2719
|
+
}
|
|
2720
|
+
return undefined;
|
|
2721
|
+
}
|
|
2722
|
+
/** Source 2: the maintained table's backing-key collations — PK column collations and
|
|
2723
|
+
* declared secondary UNIQUE per-column enforcement collations. First non-builtin
|
|
2724
|
+
* non-replicable name returns. */
|
|
2725
|
+
function findNonReplicableKeyCollation(mv, db) {
|
|
2726
|
+
for (const pk of mv.primaryKeyDefinition) {
|
|
2727
|
+
if (collationIsOffending(pk.collation, db))
|
|
2728
|
+
return normalizeCollationName(pk.collation);
|
|
2729
|
+
}
|
|
2730
|
+
for (const uc of mv.uniqueConstraints ?? []) {
|
|
2731
|
+
for (const collation of uniqueEnforcementCollations(mv, uc)) {
|
|
2732
|
+
if (collationIsOffending(collation, db))
|
|
2733
|
+
return normalizeCollationName(collation);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return undefined;
|
|
2737
|
+
}
|
|
2738
|
+
/** Canonical, order-stable, bigint-safe string for a key tuple — used to dedup the
|
|
2739
|
+
* distinct affected backing keys of a single change in the residual-recompute arm. */
|
|
2740
|
+
function canonKeyValues(values) {
|
|
2741
|
+
return JSON.stringify(values, (_k, v) => (typeof v === 'bigint' ? `${v}n` : v));
|
|
2742
|
+
}
|
|
2743
|
+
/** Aggregate node types (logical + physical) — the analyzed plan may carry any. */
|
|
2744
|
+
const AGGREGATE_NODE_TYPES = new Set([
|
|
2745
|
+
PlanNodeType.Aggregate,
|
|
2746
|
+
PlanNodeType.StreamAggregate,
|
|
2747
|
+
PlanNodeType.HashAggregate,
|
|
2748
|
+
]);
|
|
2749
|
+
/** Find the first aggregate node anywhere in the plan. */
|
|
2750
|
+
function findAggregate(node) {
|
|
2751
|
+
if (AGGREGATE_NODE_TYPES.has(node.nodeType))
|
|
2752
|
+
return node;
|
|
2753
|
+
for (const child of node.getChildren()) {
|
|
2754
|
+
const found = findAggregate(child);
|
|
2755
|
+
if (found)
|
|
2756
|
+
return found;
|
|
2757
|
+
}
|
|
2758
|
+
return undefined;
|
|
2759
|
+
}
|
|
2760
|
+
/**
|
|
2761
|
+
* Join-bearing PlanNodeTypes (logical + physical). `optimizeForAnalysis` stops
|
|
2762
|
+
* before physical join selection, so the analyzed plan carries the logical
|
|
2763
|
+
* {@link PlanNodeType.Join}; the physical variants are included so the
|
|
2764
|
+
* eligibility gate stays correct if analysis ever surfaces them.
|
|
2765
|
+
*/
|
|
2766
|
+
const JOIN_NODE_TYPES = new Set([
|
|
2767
|
+
PlanNodeType.Join,
|
|
2768
|
+
PlanNodeType.NestedLoopJoin,
|
|
2769
|
+
PlanNodeType.HashJoin,
|
|
2770
|
+
PlanNodeType.MergeJoin,
|
|
2771
|
+
PlanNodeType.FanOutLookupJoin,
|
|
2772
|
+
PlanNodeType.AsofScan,
|
|
2773
|
+
]);
|
|
2774
|
+
/** True if any node in the plan has the given type (recursive `getChildren` walk). */
|
|
2775
|
+
function containsNodeType(node, type) {
|
|
2776
|
+
if (node.nodeType === type)
|
|
2777
|
+
return true;
|
|
2778
|
+
for (const child of node.getChildren()) {
|
|
2779
|
+
if (containsNodeType(child, type))
|
|
2780
|
+
return true;
|
|
2781
|
+
}
|
|
2782
|
+
return false;
|
|
2783
|
+
}
|
|
2784
|
+
/** True if the plan carries any join node (logical or physical). Used by the
|
|
2785
|
+
* row-time gate, which is single-source — any join is ineligible. */
|
|
2786
|
+
function containsAnyJoin(node) {
|
|
2787
|
+
for (const t of JOIN_NODE_TYPES) {
|
|
2788
|
+
if (containsNodeType(node, t))
|
|
2789
|
+
return true;
|
|
2790
|
+
}
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
/** Count nodes of the given type (recursive `getChildren` walk). Used by the
|
|
2794
|
+
* lateral-TVF gate to reject nested/multiple TVFs. */
|
|
2795
|
+
function countNodeType(node, type) {
|
|
2796
|
+
let n = node.nodeType === type ? 1 : 0;
|
|
2797
|
+
for (const child of node.getChildren())
|
|
2798
|
+
n += countNodeType(child, type);
|
|
2799
|
+
return n;
|
|
2800
|
+
}
|
|
2801
|
+
/** Count join nodes (logical + physical) in the plan — used to reject a chained
|
|
2802
|
+
* lateral join (the admitted lateral-TVF shape carries exactly one). */
|
|
2803
|
+
function countJoins(node) {
|
|
2804
|
+
let n = 0;
|
|
2805
|
+
for (const t of JOIN_NODE_TYPES)
|
|
2806
|
+
n += countNodeType(node, t);
|
|
2807
|
+
return n;
|
|
2808
|
+
}
|
|
2809
|
+
/** Find the first {@link TableFunctionCallNode} anywhere in the plan, or `undefined`. */
|
|
2810
|
+
function findTableFunctionCall(node) {
|
|
2811
|
+
if (node instanceof TableFunctionCallNode)
|
|
2812
|
+
return node;
|
|
2813
|
+
for (const child of node.getChildren()) {
|
|
2814
|
+
const found = findTableFunctionCall(child);
|
|
2815
|
+
if (found)
|
|
2816
|
+
return found;
|
|
2817
|
+
}
|
|
2818
|
+
return undefined;
|
|
2819
|
+
}
|
|
2820
|
+
/** Collect `relationKey → TableReferenceNode` over a plan. */
|
|
2821
|
+
function collectTableRefs(node, out = new Map()) {
|
|
2822
|
+
if (node instanceof TableReferenceNode) {
|
|
2823
|
+
const base = `${node.tableSchema.schemaName}.${node.tableSchema.name}`.toLowerCase();
|
|
2824
|
+
out.set(`${base}#${node.id ?? 'unknown'}`, node);
|
|
2825
|
+
}
|
|
2826
|
+
for (const child of node.getChildren())
|
|
2827
|
+
collectTableRefs(child, out);
|
|
2828
|
+
return out;
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Merge attribute provenance (output attr id → producing scalar expr) from every
|
|
2832
|
+
* node that exposes it. Physical aggregates expose `getProducingExprs()`; the
|
|
2833
|
+
* logical {@link AggregateNode} present in the pre-physical analyzed plan does
|
|
2834
|
+
* not, so its group-by → output-attr mapping is reconstructed directly here.
|
|
2835
|
+
*/
|
|
2836
|
+
function collectProducingExprs(node, out = new Map()) {
|
|
2837
|
+
const fn = node.getProducingExprs;
|
|
2838
|
+
if (typeof fn === 'function') {
|
|
2839
|
+
for (const [attrId, expr] of fn.call(node)) {
|
|
2840
|
+
if (!out.has(attrId))
|
|
2841
|
+
out.set(attrId, expr);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
else if (node instanceof AggregateNode) {
|
|
2845
|
+
const attrs = node.getAttributes();
|
|
2846
|
+
node.groupBy.forEach((expr, i) => {
|
|
2847
|
+
const attr = attrs[i];
|
|
2848
|
+
if (attr && !out.has(attr.id))
|
|
2849
|
+
out.set(attr.id, expr);
|
|
2850
|
+
});
|
|
2851
|
+
node.aggregates.forEach((agg, i) => {
|
|
2852
|
+
const attr = attrs[node.groupBy.length + i];
|
|
2853
|
+
if (attr && !out.has(attr.id))
|
|
2854
|
+
out.set(attr.id, agg.expression);
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
for (const child of node.getChildren())
|
|
2858
|
+
collectProducingExprs(child, out);
|
|
2859
|
+
return out;
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Transitive provenance: chase an output-attr → producing `ColumnReference` chain (a
|
|
2863
|
+
* Project-over-Aggregate or a passthrough-through-Join adds a hop the single-hop
|
|
2864
|
+
* {@link resolveSourceCol} cannot follow) until landing on a base-source column, or
|
|
2865
|
+
* `undefined` (e.g. a TVF-output column with no base-source identity). Shared by the
|
|
2866
|
+
* aggregate-residual and lateral-TVF arms.
|
|
2867
|
+
*/
|
|
2868
|
+
function resolveTransitiveSourceCol(attrId, sourceAttrToCol, producingByAttrId) {
|
|
2869
|
+
const seen = new Set();
|
|
2870
|
+
let cur = attrId;
|
|
2871
|
+
while (cur !== undefined && !seen.has(cur)) {
|
|
2872
|
+
seen.add(cur);
|
|
2873
|
+
const direct = sourceAttrToCol.get(cur);
|
|
2874
|
+
if (direct !== undefined)
|
|
2875
|
+
return direct;
|
|
2876
|
+
const expr = producingByAttrId.get(cur);
|
|
2877
|
+
if (expr instanceof ColumnReferenceNode) {
|
|
2878
|
+
cur = expr.attributeId;
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
return undefined;
|
|
2882
|
+
}
|
|
2883
|
+
return undefined;
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* True iff the analyzed join body's WHERE references the lookup table `P` (or any base other
|
|
2887
|
+
* than the driving `T`) — the classification the join-residual arm uses to decide whether the
|
|
2888
|
+
* lookup side must be delete-capable (see {@link MaterializedViewManager.buildJoinResidualPlan}).
|
|
2889
|
+
* The body WHERE — possibly split by predicate-pushdown — surfaces as one or more
|
|
2890
|
+
* {@link FilterNode}s above/around the join; the join's own `ON` condition lives inside the
|
|
2891
|
+
* JoinNode (not a Filter) and so is excluded. Each column a filter predicate references is
|
|
2892
|
+
* resolved against `T`'s attribute→source-column map (transitively); a reference that does NOT
|
|
2893
|
+
* resolve to a `T` column is a `P` (the arm requires exactly two base refs, `T` and `P`) — or
|
|
2894
|
+
* otherwise non-`T` — reference. Conservative by construction: an unresolved reference counts as
|
|
2895
|
+
* lookup-referencing, so the cheaper `T`-only upsert-only path is taken only when **every**
|
|
2896
|
+
* filter column provably belongs to `T`.
|
|
2897
|
+
*/
|
|
2898
|
+
function bodyWhereReferencesLookup(analyzed, tAttrToCol, producingByAttrId) {
|
|
2899
|
+
const filterAttrs = new Set();
|
|
2900
|
+
collectFilterPredicateAttrs(analyzed, filterAttrs);
|
|
2901
|
+
for (const attrId of filterAttrs) {
|
|
2902
|
+
if (resolveTransitiveSourceCol(attrId, tAttrToCol, producingByAttrId) === undefined)
|
|
2903
|
+
return true;
|
|
2904
|
+
}
|
|
2905
|
+
return false;
|
|
2906
|
+
}
|
|
2907
|
+
/** Collect every attribute id referenced by a ColumnReferenceNode inside any {@link FilterNode}
|
|
2908
|
+
* predicate in the plan (the body WHERE; the join `ON` condition is not a Filter). */
|
|
2909
|
+
function collectFilterPredicateAttrs(node, out) {
|
|
2910
|
+
if (node instanceof FilterNode)
|
|
2911
|
+
collectColumnRefAttrs(node.predicate, out);
|
|
2912
|
+
for (const child of node.getChildren())
|
|
2913
|
+
collectFilterPredicateAttrs(child, out);
|
|
2914
|
+
}
|
|
2915
|
+
/** Collect every {@link ColumnReferenceNode} attribute id in a scalar subtree. */
|
|
2916
|
+
function collectColumnRefAttrs(node, out) {
|
|
2917
|
+
if (node instanceof ColumnReferenceNode)
|
|
2918
|
+
out.add(node.attributeId);
|
|
2919
|
+
for (const child of node.getChildren())
|
|
2920
|
+
collectColumnRefAttrs(child, out);
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* True iff any {@link FilterNode} predicate in the body (the body WHERE) is non-deterministic.
|
|
2924
|
+
* The join-residual arm embeds the body WHERE in every residual (forward, in-scope reverse, and
|
|
2925
|
+
* — when delete-capable — membership), so a volatile predicate (`random()`/`now()`/a volatile
|
|
2926
|
+
* UDF) would make them irreproducible and diverge from the plain view. The arm therefore declines
|
|
2927
|
+
* such a body (returns `null` → the full-rebuild floor, which applies the **pragma-gated**
|
|
2928
|
+
* whole-body determinism reject — rejected without `pragma nondeterministic_schema`, accepted as a
|
|
2929
|
+
* wholesale rebuild with it), preserving the pre-WHERE-widening behavior rather than building an
|
|
2930
|
+
* unsound bounded-delta residual.
|
|
2931
|
+
*/
|
|
2932
|
+
function bodyWhereIsNonDeterministic(analyzed) {
|
|
2933
|
+
const visit = (node) => {
|
|
2934
|
+
if (node instanceof FilterNode && !checkDeterministic(node.predicate).valid)
|
|
2935
|
+
return true;
|
|
2936
|
+
for (const child of node.getChildren()) {
|
|
2937
|
+
if (visit(child))
|
|
2938
|
+
return true;
|
|
2939
|
+
}
|
|
2940
|
+
return false;
|
|
2941
|
+
};
|
|
2942
|
+
return visit(analyzed);
|
|
2943
|
+
}
|
|
2944
|
+
/** Read the output attributes of a block's final relational statement. */
|
|
2945
|
+
function relationalAttributes(block) {
|
|
2946
|
+
const children = block.getChildren();
|
|
2947
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
2948
|
+
const child = children[i];
|
|
2949
|
+
if (typeof child.getAttributes === 'function')
|
|
2950
|
+
return child.getAttributes();
|
|
2951
|
+
}
|
|
2952
|
+
return undefined;
|
|
2953
|
+
}
|
|
2954
|
+
/** The root relational node of a block's final relational statement — the node whose
|
|
2955
|
+
* attributes {@link relationalAttributes} reads — or `undefined`. Feeds the shared
|
|
2956
|
+
* coverage-prover join predicates ({@link proveOneToOneJoin}) for the join-residual arm. */
|
|
2957
|
+
function rootRelationalNode(block) {
|
|
2958
|
+
const children = block.getChildren();
|
|
2959
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
2960
|
+
const child = children[i];
|
|
2961
|
+
if (isRelationalNode(child))
|
|
2962
|
+
return child;
|
|
2963
|
+
}
|
|
2964
|
+
return undefined;
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* The diagnostic for a create-time **hard** reject — one of the four non-shape rejections
|
|
2968
|
+
* the cost-gated-with-floor model keeps (non-determinism, bag/no-key, no relational output,
|
|
2969
|
+
* size). Names the MV and steers to a plain `view` (live re-evaluation) or
|
|
2970
|
+
* `create table ... as <body>` (a one-off snapshot) — never a refresh policy, never an
|
|
2971
|
+
* internal implementation detail. Used by the arm builders (for their arm-specific
|
|
2972
|
+
* determinism diagnostic) and by {@link MaterializedViewManager.buildFullRebuildPlan}.
|
|
2973
|
+
*/
|
|
2974
|
+
function cannotMaterialize(mvName, detail) {
|
|
2975
|
+
return new QuereusError(`materialized view '${mvName}' cannot be materialized: ${detail}. For this body, use a `
|
|
2976
|
+
+ `plain 'create view' (live re-evaluation) or 'create table ... as <body>' (a one-off snapshot)`, StatusCode.UNSUPPORTED);
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* The diagnostic for the create-time **replicable-determinism** reject — distinct from
|
|
2980
|
+
* {@link cannotMaterialize} because the fix here is not "use a plain view": the body is
|
|
2981
|
+
* fine, it just calls a function the backing host requires be REPLICABLE. So this names the
|
|
2982
|
+
* function and steers to declaring it `replicable: true` at registration (built-ins qualify
|
|
2983
|
+
* automatically). Fires only when the resolved backing host declares
|
|
2984
|
+
* `requiresReplicableDerivations`. `StatusCode.UNSUPPORTED`.
|
|
2985
|
+
*/
|
|
2986
|
+
function nonReplicableDerivationError(mvName, fnName) {
|
|
2987
|
+
return new QuereusError(`materialized view '${mvName}' cannot be materialized on this backing host: it calls non-replicable `
|
|
2988
|
+
+ `function '${fnName}'. This host requires every function in the body to be bit-identical across `
|
|
2989
|
+
+ `peers/platforms; declare the function \`replicable: true\` at registration (built-in functions `
|
|
2990
|
+
+ `qualify automatically)`, StatusCode.UNSUPPORTED);
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* The diagnostic for the create-time **replicable-collation** reject — the collation
|
|
2994
|
+
* analogue of {@link nonReplicableDerivationError}. The body is fine; it just folds or
|
|
2995
|
+
* orders (comparison / ORDER BY / GROUP BY / DISTINCT / backing key) under a collation the
|
|
2996
|
+
* backing host requires be bit-identical across peers — so this does NOT steer to a plain
|
|
2997
|
+
* view. It names the collation and steers to declaring it `replicable: true` at registration
|
|
2998
|
+
* (built-in collations qualify automatically). Fires only when the resolved backing host
|
|
2999
|
+
* declares `requiresReplicableDerivations`. `StatusCode.UNSUPPORTED`.
|
|
3000
|
+
*/
|
|
3001
|
+
function nonReplicableCollationDerivationError(mvName, collationName) {
|
|
3002
|
+
return new QuereusError(`materialized view '${mvName}' cannot be materialized on this backing host: it folds or orders under `
|
|
3003
|
+
+ `non-replicable collation '${collationName}'. This host requires every collation in the body to be `
|
|
3004
|
+
+ `bit-identical across peers/platforms; declare the collation \`replicable: true\` at registration `
|
|
3005
|
+
+ `(built-in collations qualify automatically)`, StatusCode.UNSUPPORTED);
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* True iff a computed projection expression can be evaluated as a pure function of the
|
|
3009
|
+
* changed source row — i.e. it contains no subquery / relational subtree (cross-row) and
|
|
3010
|
+
* every column reference resolves to a source column (no correlated / outer reference).
|
|
3011
|
+
* This is the "shape" gate distinct from the determinism gate (a determinism failure is
|
|
3012
|
+
* caught earlier by `checkDeterministic`); a `false` here is a `null` fall-through to the
|
|
3013
|
+
* full-rebuild floor, not a hard reject.
|
|
3014
|
+
*/
|
|
3015
|
+
function isSingleRowEvaluable(expr, sourceDescriptor) {
|
|
3016
|
+
const visit = (node) => {
|
|
3017
|
+
if (node !== expr && isRelationalNode(node))
|
|
3018
|
+
return false; // a subquery / relational subtree
|
|
3019
|
+
if (node instanceof ColumnReferenceNode && sourceDescriptor[node.attributeId] === undefined) {
|
|
3020
|
+
return false; // references a value outside the source row
|
|
3021
|
+
}
|
|
3022
|
+
for (const child of node.getChildren()) {
|
|
3023
|
+
if (!visit(child))
|
|
3024
|
+
return false;
|
|
3025
|
+
}
|
|
3026
|
+
return true;
|
|
3027
|
+
};
|
|
3028
|
+
return visit(expr);
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Compile a deterministic scalar plan node into a per-source-row evaluator by reusing
|
|
3032
|
+
* the runtime: emit the node once, then run it against a row context that maps each
|
|
3033
|
+
* source attribute id to its column index in the changed row. Reusing the runtime
|
|
3034
|
+
* (rather than a hand-rolled scalar interpreter) guarantees a computed backing value is
|
|
3035
|
+
* byte-for-byte what `select <body>` would produce — the materialized-view ≡ view
|
|
3036
|
+
* contract. The gated forms (deterministic scalars over a single row, no subqueries —
|
|
3037
|
+
* see {@link assertSingleRowEvaluable}) resolve synchronously; a Promise result would
|
|
3038
|
+
* signal an unsupported async form and is surfaced loudly rather than silently awaited.
|
|
3039
|
+
*/
|
|
3040
|
+
function compileSourceRowEvaluator(db, expr, sourceDescriptor) {
|
|
3041
|
+
const instruction = emitPlanNode(expr, new EmissionContext(db));
|
|
3042
|
+
const scheduler = new Scheduler(instruction);
|
|
3043
|
+
const context = new RowContextMap();
|
|
3044
|
+
let currentRow = [];
|
|
3045
|
+
// Installed once; the getter reads the closed-over `currentRow`, refreshed per call.
|
|
3046
|
+
context.set(sourceDescriptor, () => currentRow);
|
|
3047
|
+
const rctx = {
|
|
3048
|
+
db,
|
|
3049
|
+
stmt: undefined,
|
|
3050
|
+
params: {},
|
|
3051
|
+
context,
|
|
3052
|
+
tableContexts: new Map(),
|
|
3053
|
+
enableMetrics: false,
|
|
3054
|
+
};
|
|
3055
|
+
return (row) => {
|
|
3056
|
+
currentRow = row;
|
|
3057
|
+
const result = scheduler.run(rctx);
|
|
3058
|
+
if (result instanceof Promise) {
|
|
3059
|
+
throw new QuereusError('a row-time projection expression evaluated asynchronously (unexpected for a gated single-row scalar)', StatusCode.INTERNAL);
|
|
3060
|
+
}
|
|
3061
|
+
return result;
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
//# sourceMappingURL=database-materialized-views.js.map
|