@quereus/quereus 4.3.1 → 4.4.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 +25 -7
- package/dist/src/common/types.d.ts +9 -59
- 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 +2 -2
- package/dist/src/core/database-assertions.d.ts.map +1 -1
- package/dist/src/core/database-assertions.js +4 -3
- package/dist/src/core/database-assertions.js.map +1 -1
- package/dist/src/core/database-external-changes.d.ts +3 -3
- package/dist/src/core/database-external-changes.d.ts.map +1 -1
- package/dist/src/core/database-external-changes.js +13 -10
- package/dist/src/core/database-external-changes.js.map +1 -1
- package/dist/src/core/database-internal.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views-analysis.d.ts +199 -0
- package/dist/src/core/database-materialized-views-analysis.d.ts.map +1 -0
- package/dist/src/core/database-materialized-views-analysis.js +502 -0
- package/dist/src/core/database-materialized-views-analysis.js.map +1 -0
- package/dist/src/core/database-materialized-views-apply.d.ts +422 -0
- package/dist/src/core/database-materialized-views-apply.d.ts.map +1 -0
- package/dist/src/core/database-materialized-views-apply.js +929 -0
- package/dist/src/core/database-materialized-views-apply.js.map +1 -0
- package/dist/src/core/database-materialized-views-plan-builders.d.ts +267 -0
- package/dist/src/core/database-materialized-views-plan-builders.d.ts.map +1 -0
- package/dist/src/core/database-materialized-views-plan-builders.js +1423 -0
- package/dist/src/core/database-materialized-views-plan-builders.js.map +1 -0
- package/dist/src/core/database-materialized-views-plans.d.ts +641 -0
- package/dist/src/core/database-materialized-views-plans.d.ts.map +1 -0
- package/dist/src/core/database-materialized-views-plans.js +13 -0
- package/dist/src/core/database-materialized-views-plans.js.map +1 -0
- package/dist/src/core/database-materialized-views.d.ts +108 -860
- package/dist/src/core/database-materialized-views.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views.js +212 -2165
- package/dist/src/core/database-materialized-views.js.map +1 -1
- package/dist/src/core/database-transaction.d.ts +10 -1
- package/dist/src/core/database-transaction.d.ts.map +1 -1
- package/dist/src/core/database-transaction.js +16 -4
- package/dist/src/core/database-transaction.js.map +1 -1
- package/dist/src/core/database-watchers.d.ts.map +1 -1
- package/dist/src/core/database-watchers.js +2 -1
- package/dist/src/core/database-watchers.js.map +1 -1
- package/dist/src/core/database.d.ts +99 -13
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +270 -50
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/derived-row-validator.d.ts +1 -1
- package/dist/src/core/derived-row-validator.d.ts.map +1 -1
- package/dist/src/core/derived-row-validator.js +5 -3
- package/dist/src/core/derived-row-validator.js.map +1 -1
- package/dist/src/core/statement.d.ts +8 -0
- package/dist/src/core/statement.d.ts.map +1 -1
- package/dist/src/core/statement.js +98 -18
- package/dist/src/core/statement.js.map +1 -1
- package/dist/src/emit/ast-stringify.d.ts.map +1 -1
- package/dist/src/emit/ast-stringify.js +24 -13
- 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 +127 -21
- package/dist/src/func/builtins/aggregate.js.map +1 -1
- package/dist/src/func/builtins/explain.d.ts.map +1 -1
- package/dist/src/func/builtins/explain.js +2 -1
- package/dist/src/func/builtins/explain.js.map +1 -1
- package/dist/src/func/builtins/json.d.ts.map +1 -1
- package/dist/src/func/builtins/json.js +2 -1
- package/dist/src/func/builtins/json.js.map +1 -1
- package/dist/src/func/builtins/mutation.d.ts.map +1 -1
- package/dist/src/func/builtins/mutation.js +3 -2
- package/dist/src/func/builtins/mutation.js.map +1 -1
- package/dist/src/func/registration.d.ts +6 -1
- package/dist/src/func/registration.d.ts.map +1 -1
- package/dist/src/func/registration.js +16 -0
- package/dist/src/func/registration.js.map +1 -1
- package/dist/src/index.d.ts +16 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +33 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/parser/ast.d.ts +2 -2
- package/dist/src/parser/lexer.d.ts +11 -1
- package/dist/src/parser/lexer.d.ts.map +1 -1
- package/dist/src/parser/lexer.js +40 -34
- package/dist/src/parser/lexer.js.map +1 -1
- package/dist/src/parser/parser.d.ts.map +1 -1
- package/dist/src/parser/parser.js +16 -9
- package/dist/src/parser/parser.js.map +1 -1
- package/dist/src/planner/analysis/authored-inverse.js +1 -1
- package/dist/src/planner/analysis/comparison-collation.d.ts +52 -0
- package/dist/src/planner/analysis/comparison-collation.d.ts.map +1 -1
- package/dist/src/planner/analysis/comparison-collation.js +84 -7
- package/dist/src/planner/analysis/comparison-collation.js.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.js +64 -17
- package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
- package/dist/src/planner/analysis/coverage-prover.d.ts +1 -1
- package/dist/src/planner/analysis/coverage-prover.js +1 -1
- package/dist/src/planner/analysis/equi-correlation.d.ts +26 -0
- package/dist/src/planner/analysis/equi-correlation.d.ts.map +1 -0
- package/dist/src/planner/analysis/equi-correlation.js +62 -0
- package/dist/src/planner/analysis/equi-correlation.js.map +1 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +54 -18
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -1
- package/dist/src/planner/analysis/query-rewrite-matcher.js +86 -42
- package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -1
- package/dist/src/planner/analysis/sat-checker.d.ts +8 -1
- package/dist/src/planner/analysis/sat-checker.d.ts.map +1 -1
- package/dist/src/planner/analysis/sat-checker.js +117 -22
- package/dist/src/planner/analysis/sat-checker.js.map +1 -1
- package/dist/src/planner/analysis/scalar-invertibility.d.ts +13 -4
- package/dist/src/planner/analysis/scalar-invertibility.d.ts.map +1 -1
- package/dist/src/planner/analysis/scalar-invertibility.js +12 -3
- package/dist/src/planner/analysis/scalar-invertibility.js.map +1 -1
- package/dist/src/planner/analysis/scalar-subqueries.d.ts +28 -0
- package/dist/src/planner/analysis/scalar-subqueries.d.ts.map +1 -0
- package/dist/src/planner/analysis/scalar-subqueries.js +60 -0
- package/dist/src/planner/analysis/scalar-subqueries.js.map +1 -0
- package/dist/src/planner/analysis/update-lineage.d.ts +1 -1
- package/dist/src/planner/analysis/update-lineage.js +1 -1
- package/dist/src/planner/analysis/update-lineage.js.map +1 -1
- package/dist/src/planner/analysis/view-complement.d.ts +2 -2
- package/dist/src/planner/building/delete.js +2 -2
- package/dist/src/planner/building/delete.js.map +1 -1
- package/dist/src/planner/building/dml-target.d.ts +2 -2
- package/dist/src/planner/building/dml-target.js +2 -2
- package/dist/src/planner/building/insert.d.ts.map +1 -1
- package/dist/src/planner/building/insert.js +53 -2
- package/dist/src/planner/building/insert.js.map +1 -1
- package/dist/src/planner/building/schema-resolution.d.ts +3 -0
- package/dist/src/planner/building/schema-resolution.d.ts.map +1 -1
- package/dist/src/planner/building/schema-resolution.js +4 -5
- package/dist/src/planner/building/schema-resolution.js.map +1 -1
- package/dist/src/planner/building/select.d.ts.map +1 -1
- package/dist/src/planner/building/select.js +18 -4
- package/dist/src/planner/building/select.js.map +1 -1
- package/dist/src/planner/building/update.js +2 -2
- package/dist/src/planner/building/update.js.map +1 -1
- package/dist/src/planner/building/view-mutation-builder.js +7 -7
- package/dist/src/planner/building/view-mutation-builder.js.map +1 -1
- package/dist/src/planner/building/with.d.ts.map +1 -1
- package/dist/src/planner/building/with.js +5 -8
- package/dist/src/planner/building/with.js.map +1 -1
- package/dist/src/planner/cache/materialization-advisory.d.ts +34 -2
- package/dist/src/planner/cache/materialization-advisory.d.ts.map +1 -1
- package/dist/src/planner/cache/materialization-advisory.js +185 -42
- package/dist/src/planner/cache/materialization-advisory.js.map +1 -1
- package/dist/src/planner/cache/reference-graph.d.ts +14 -5
- package/dist/src/planner/cache/reference-graph.d.ts.map +1 -1
- package/dist/src/planner/cache/reference-graph.js +31 -39
- package/dist/src/planner/cache/reference-graph.js.map +1 -1
- package/dist/src/planner/cost/index.d.ts +30 -5
- package/dist/src/planner/cost/index.d.ts.map +1 -1
- package/dist/src/planner/cost/index.js +26 -0
- package/dist/src/planner/cost/index.js.map +1 -1
- package/dist/src/planner/debug.d.ts +3 -2
- package/dist/src/planner/debug.d.ts.map +1 -1
- package/dist/src/planner/debug.js +9 -3
- package/dist/src/planner/debug.js.map +1 -1
- package/dist/src/planner/framework/characteristics.d.ts +46 -19
- package/dist/src/planner/framework/characteristics.d.ts.map +1 -1
- package/dist/src/planner/framework/characteristics.js +50 -134
- package/dist/src/planner/framework/characteristics.js.map +1 -1
- package/dist/src/planner/framework/context.d.ts +8 -44
- package/dist/src/planner/framework/context.d.ts.map +1 -1
- package/dist/src/planner/framework/context.js +0 -91
- package/dist/src/planner/framework/context.js.map +1 -1
- package/dist/src/planner/framework/pass.d.ts +7 -0
- package/dist/src/planner/framework/pass.d.ts.map +1 -1
- package/dist/src/planner/framework/pass.js +84 -0
- package/dist/src/planner/framework/pass.js.map +1 -1
- package/dist/src/planner/framework/registry.d.ts +13 -42
- package/dist/src/planner/framework/registry.d.ts.map +1 -1
- package/dist/src/planner/framework/registry.js +14 -182
- package/dist/src/planner/framework/registry.js.map +1 -1
- package/dist/src/planner/mutation/backward-body.d.ts +2 -2
- package/dist/src/planner/mutation/cte-flatten.js +1 -1
- package/dist/src/planner/mutation/decomposition.d.ts +2 -2
- package/dist/src/planner/mutation/decomposition.js +1 -1
- package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -1
- package/dist/src/planner/mutation/lens-enforcement.js +2 -1
- package/dist/src/planner/mutation/lens-enforcement.js.map +1 -1
- package/dist/src/planner/mutation/multi-source.d.ts +8 -8
- package/dist/src/planner/mutation/multi-source.js +11 -11
- package/dist/src/planner/mutation/multi-source.js.map +1 -1
- package/dist/src/planner/mutation/propagate.d.ts +1 -1
- package/dist/src/planner/mutation/propagate.js +1 -1
- package/dist/src/planner/mutation/scope-transform.d.ts +2 -2
- package/dist/src/planner/mutation/scope-transform.js +3 -3
- package/dist/src/planner/mutation/scope-transform.js.map +1 -1
- package/dist/src/planner/mutation/set-op.d.ts +1 -1
- package/dist/src/planner/mutation/set-op.d.ts.map +1 -1
- package/dist/src/planner/mutation/set-op.js +3 -2
- package/dist/src/planner/mutation/set-op.js.map +1 -1
- package/dist/src/planner/mutation/single-source.d.ts +3 -3
- package/dist/src/planner/mutation/single-source.js +9 -9
- package/dist/src/planner/mutation/single-source.js.map +1 -1
- package/dist/src/planner/nodes/aggregate-function.d.ts +3 -1
- package/dist/src/planner/nodes/aggregate-function.d.ts.map +1 -1
- package/dist/src/planner/nodes/aggregate-function.js +1 -0
- package/dist/src/planner/nodes/aggregate-function.js.map +1 -1
- package/dist/src/planner/nodes/aggregate-node.d.ts +1 -0
- package/dist/src/planner/nodes/aggregate-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/aggregate-node.js +8 -1
- 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 +3 -1
- package/dist/src/planner/nodes/alias-node.js.map +1 -1
- package/dist/src/planner/nodes/asof-scan-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/asof-scan-node.js +3 -2
- package/dist/src/planner/nodes/asof-scan-node.js.map +1 -1
- package/dist/src/planner/nodes/asserted-keys-node.d.ts +1 -1
- package/dist/src/planner/nodes/asserted-keys-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/asserted-keys-node.js +5 -2
- package/dist/src/planner/nodes/asserted-keys-node.js.map +1 -1
- package/dist/src/planner/nodes/async-gather-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/async-gather-node.js +3 -1
- package/dist/src/planner/nodes/async-gather-node.js.map +1 -1
- package/dist/src/planner/nodes/block.js +3 -3
- package/dist/src/planner/nodes/block.js.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.d.ts +2 -0
- package/dist/src/planner/nodes/bloom-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.js +5 -1
- package/dist/src/planner/nodes/bloom-join-node.js.map +1 -1
- package/dist/src/planner/nodes/cache-node.d.ts +19 -2
- package/dist/src/planner/nodes/cache-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/cache-node.js +43 -3
- package/dist/src/planner/nodes/cache-node.js.map +1 -1
- package/dist/src/planner/nodes/cte-node.d.ts +16 -1
- package/dist/src/planner/nodes/cte-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/cte-node.js +16 -3
- package/dist/src/planner/nodes/cte-node.js.map +1 -1
- package/dist/src/planner/nodes/distinct-node.js +3 -3
- package/dist/src/planner/nodes/distinct-node.js.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts +21 -0
- package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
- package/dist/src/planner/nodes/envelope-scan-node.d.ts +1 -1
- package/dist/src/planner/nodes/envelope-scan-node.js +1 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.js +4 -2
- package/dist/src/planner/nodes/fanout-lookup-join-node.js.map +1 -1
- package/dist/src/planner/nodes/filter.d.ts +10 -2
- package/dist/src/planner/nodes/filter.d.ts.map +1 -1
- package/dist/src/planner/nodes/filter.js +81 -14
- package/dist/src/planner/nodes/filter.js.map +1 -1
- package/dist/src/planner/nodes/hash-aggregate.d.ts +12 -1
- package/dist/src/planner/nodes/hash-aggregate.d.ts.map +1 -1
- package/dist/src/planner/nodes/hash-aggregate.js +28 -1
- package/dist/src/planner/nodes/hash-aggregate.js.map +1 -1
- package/dist/src/planner/nodes/internal-recursive-cte-ref-node.d.ts +3 -1
- package/dist/src/planner/nodes/internal-recursive-cte-ref-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/internal-recursive-cte-ref-node.js +1 -0
- package/dist/src/planner/nodes/internal-recursive-cte-ref-node.js.map +1 -1
- package/dist/src/planner/nodes/join-node.d.ts +3 -1
- package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/join-node.js +5 -5
- package/dist/src/planner/nodes/join-node.js.map +1 -1
- package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js +4 -1
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js.map +1 -1
- package/dist/src/planner/nodes/limit-offset.d.ts +1 -0
- package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
- package/dist/src/planner/nodes/limit-offset.js +4 -3
- package/dist/src/planner/nodes/limit-offset.js.map +1 -1
- package/dist/src/planner/nodes/merge-join-node.d.ts +2 -0
- package/dist/src/planner/nodes/merge-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/merge-join-node.js +6 -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 +3 -1
- package/dist/src/planner/nodes/ordinal-slice-node.js.map +1 -1
- package/dist/src/planner/nodes/plan-node.d.ts +68 -5
- package/dist/src/planner/nodes/plan-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/plan-node.js +137 -24
- package/dist/src/planner/nodes/plan-node.js.map +1 -1
- package/dist/src/planner/nodes/project-node.d.ts +3 -2
- package/dist/src/planner/nodes/project-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/project-node.js +1 -0
- package/dist/src/planner/nodes/project-node.js.map +1 -1
- package/dist/src/planner/nodes/recursive-cte-node.d.ts +30 -1
- package/dist/src/planner/nodes/recursive-cte-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/recursive-cte-node.js +38 -5
- package/dist/src/planner/nodes/recursive-cte-node.js.map +1 -1
- package/dist/src/planner/nodes/reference.d.ts +5 -1
- package/dist/src/planner/nodes/reference.d.ts.map +1 -1
- package/dist/src/planner/nodes/reference.js +3 -0
- package/dist/src/planner/nodes/reference.js.map +1 -1
- package/dist/src/planner/nodes/remote-query-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/remote-query-node.js +3 -1
- package/dist/src/planner/nodes/remote-query-node.js.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.js +3 -1
- package/dist/src/planner/nodes/retrieve-node.js.map +1 -1
- package/dist/src/planner/nodes/set-operation-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/set-operation-node.js +3 -1
- package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
- package/dist/src/planner/nodes/sink-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/sink-node.js +3 -1
- package/dist/src/planner/nodes/sink-node.js.map +1 -1
- package/dist/src/planner/nodes/sort.d.ts +2 -1
- package/dist/src/planner/nodes/sort.d.ts.map +1 -1
- package/dist/src/planner/nodes/sort.js +9 -5
- package/dist/src/planner/nodes/sort.js.map +1 -1
- package/dist/src/planner/nodes/stream-aggregate.d.ts +12 -1
- package/dist/src/planner/nodes/stream-aggregate.d.ts.map +1 -1
- package/dist/src/planner/nodes/stream-aggregate.js +32 -3
- package/dist/src/planner/nodes/stream-aggregate.js.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.d.ts +2 -1
- package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.js +2 -0
- package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
- package/dist/src/planner/nodes/view-mutation-node.d.ts +2 -2
- package/dist/src/planner/nodes/view-mutation-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/view-mutation-node.js +5 -1
- package/dist/src/planner/nodes/view-mutation-node.js.map +1 -1
- package/dist/src/planner/nodes/window-function.d.ts +3 -1
- package/dist/src/planner/nodes/window-function.d.ts.map +1 -1
- package/dist/src/planner/nodes/window-function.js +1 -0
- package/dist/src/planner/nodes/window-function.js.map +1 -1
- package/dist/src/planner/optimizer.d.ts +52 -8
- package/dist/src/planner/optimizer.d.ts.map +1 -1
- package/dist/src/planner/optimizer.js +1014 -895
- package/dist/src/planner/optimizer.js.map +1 -1
- 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 +235 -202
- package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-cte-optimization.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-cte-optimization.js +8 -1
- package/dist/src/planner/rules/cache/rule-cte-optimization.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.d.ts +7 -0
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.js +14 -1
- package/dist/src/planner/rules/cache/rule-in-subquery-cache.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts +2 -2
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +55 -56
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.js +6 -0
- package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts +25 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts.map +1 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js +176 -0
- package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js.map +1 -0
- package/dist/src/planner/rules/cache/rule-scalar-cse.d.ts +1 -1
- package/dist/src/planner/rules/cache/rule-scalar-cse.js +1 -1
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.d.ts +29 -0
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.d.ts.map +1 -0
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.js +66 -0
- package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.js.map +1 -0
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +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 +1 -54
- 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 +1 -1
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js +1 -1
- package/dist/src/planner/rules/join/rule-join-elimination.d.ts +1 -1
- package/dist/src/planner/rules/join/rule-join-elimination.js +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js +5 -6
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.d.ts +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts +2 -2
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js +4 -3
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-selectivity.d.ts +19 -0
- package/dist/src/planner/rules/predicate/rule-filter-selectivity.d.ts.map +1 -0
- package/dist/src/planner/rules/predicate/rule-filter-selectivity.js +48 -0
- package/dist/src/planner/rules/predicate/rule-filter-selectivity.js.map +1 -0
- package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js +12 -1
- package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.d.ts.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js +22 -20
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js.map +1 -1
- package/dist/src/planner/rules/shared/index-style-context.d.ts +39 -0
- package/dist/src/planner/rules/shared/index-style-context.d.ts.map +1 -0
- package/dist/src/planner/rules/shared/index-style-context.js +26 -0
- package/dist/src/planner/rules/shared/index-style-context.js.map +1 -0
- 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 +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.d.ts +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js +1 -1
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.d.ts +191 -0
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.d.ts.map +1 -0
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.js +623 -0
- package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.js.map +1 -0
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts +10 -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 +242 -51
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js.map +1 -1
- package/dist/src/planner/stats/analyze.d.ts.map +1 -1
- package/dist/src/planner/stats/analyze.js +2 -20
- package/dist/src/planner/stats/analyze.js.map +1 -1
- package/dist/src/planner/stats/histogram.d.ts.map +1 -1
- package/dist/src/planner/stats/histogram.js +4 -0
- package/dist/src/planner/stats/histogram.js.map +1 -1
- package/dist/src/planner/util/fd-utils.d.ts +2 -3
- package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
- package/dist/src/planner/util/fd-utils.js +2 -3
- package/dist/src/planner/util/fd-utils.js.map +1 -1
- package/dist/src/planner/validation/plan-validator.d.ts +16 -0
- package/dist/src/planner/validation/plan-validator.d.ts.map +1 -1
- package/dist/src/planner/validation/plan-validator.js +36 -0
- package/dist/src/planner/validation/plan-validator.js.map +1 -1
- package/dist/src/runtime/async-util.d.ts +26 -0
- package/dist/src/runtime/async-util.d.ts.map +1 -1
- package/dist/src/runtime/async-util.js +139 -55
- package/dist/src/runtime/async-util.js.map +1 -1
- package/dist/src/runtime/cache/shared-cache.d.ts +7 -0
- package/dist/src/runtime/cache/shared-cache.d.ts.map +1 -1
- package/dist/src/runtime/cache/shared-cache.js +34 -1
- package/dist/src/runtime/cache/shared-cache.js.map +1 -1
- package/dist/src/runtime/context-helpers.d.ts +32 -4
- package/dist/src/runtime/context-helpers.d.ts.map +1 -1
- package/dist/src/runtime/context-helpers.js +26 -9
- package/dist/src/runtime/context-helpers.js.map +1 -1
- package/dist/src/runtime/deferred-constraint-queue.d.ts.map +1 -1
- package/dist/src/runtime/deferred-constraint-queue.js +2 -1
- package/dist/src/runtime/deferred-constraint-queue.js.map +1 -1
- package/dist/src/runtime/emission-context.d.ts +27 -3
- package/dist/src/runtime/emission-context.d.ts.map +1 -1
- package/dist/src/runtime/emission-context.js +38 -7
- package/dist/src/runtime/emission-context.js.map +1 -1
- package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
- package/dist/src/runtime/emit/add-constraint.js +7 -1
- package/dist/src/runtime/emit/add-constraint.js.map +1 -1
- package/dist/src/runtime/emit/aggregate.d.ts +2 -3
- package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
- package/dist/src/runtime/emit/aggregate.js +23 -28
- package/dist/src/runtime/emit/aggregate.js.map +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/alter-table.js +98 -9
- 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 +13 -10
- package/dist/src/runtime/emit/asof-scan.js.map +1 -1
- package/dist/src/runtime/emit/async-gather.d.ts.map +1 -1
- package/dist/src/runtime/emit/async-gather.js +4 -3
- package/dist/src/runtime/emit/async-gather.js.map +1 -1
- package/dist/src/runtime/emit/between.d.ts.map +1 -1
- package/dist/src/runtime/emit/between.js +2 -1
- 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 +122 -14
- 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 +2 -1
- 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 +20 -8
- package/dist/src/runtime/emit/bloom-join.js.map +1 -1
- package/dist/src/runtime/emit/cache.d.ts +7 -3
- package/dist/src/runtime/emit/cache.d.ts.map +1 -1
- package/dist/src/runtime/emit/cache.js +25 -7
- package/dist/src/runtime/emit/cache.js.map +1 -1
- package/dist/src/runtime/emit/case.d.ts.map +1 -1
- package/dist/src/runtime/emit/case.js +66 -45
- package/dist/src/runtime/emit/case.js.map +1 -1
- package/dist/src/runtime/emit/cast.d.ts.map +1 -1
- package/dist/src/runtime/emit/cast.js +2 -1
- package/dist/src/runtime/emit/cast.js.map +1 -1
- package/dist/src/runtime/emit/constraint-check.d.ts.map +1 -1
- package/dist/src/runtime/emit/constraint-check.js +13 -7
- package/dist/src/runtime/emit/constraint-check.js.map +1 -1
- package/dist/src/runtime/emit/create-assertion.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-assertion.js +2 -1
- package/dist/src/runtime/emit/create-assertion.js.map +1 -1
- package/dist/src/runtime/emit/create-index.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-index.js +12 -1
- package/dist/src/runtime/emit/create-index.js.map +1 -1
- package/dist/src/runtime/emit/create-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-table.js +12 -1
- package/dist/src/runtime/emit/create-table.js.map +1 -1
- package/dist/src/runtime/emit/cte-reference.d.ts.map +1 -1
- package/dist/src/runtime/emit/cte-reference.js +2 -1
- package/dist/src/runtime/emit/cte-reference.js.map +1 -1
- package/dist/src/runtime/emit/cte.d.ts.map +1 -1
- package/dist/src/runtime/emit/cte.js +45 -21
- package/dist/src/runtime/emit/cte.js.map +1 -1
- package/dist/src/runtime/emit/ddl-transaction-policy.d.ts +50 -0
- package/dist/src/runtime/emit/ddl-transaction-policy.d.ts.map +1 -0
- package/dist/src/runtime/emit/ddl-transaction-policy.js +69 -0
- package/dist/src/runtime/emit/ddl-transaction-policy.js.map +1 -0
- package/dist/src/runtime/emit/delete.d.ts.map +1 -1
- package/dist/src/runtime/emit/delete.js +2 -1
- package/dist/src/runtime/emit/delete.js.map +1 -1
- package/dist/src/runtime/emit/distinct.d.ts.map +1 -1
- package/dist/src/runtime/emit/distinct.js +2 -1
- package/dist/src/runtime/emit/distinct.js.map +1 -1
- package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
- package/dist/src/runtime/emit/dml-executor.js +138 -74
- package/dist/src/runtime/emit/dml-executor.js.map +1 -1
- package/dist/src/runtime/emit/drop-assertion.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-assertion.js +2 -1
- package/dist/src/runtime/emit/drop-assertion.js.map +1 -1
- package/dist/src/runtime/emit/drop-index.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-index.js +17 -1
- package/dist/src/runtime/emit/drop-index.js.map +1 -1
- package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-table.js +11 -1
- package/dist/src/runtime/emit/drop-table.js.map +1 -1
- package/dist/src/runtime/emit/eager-prefetch.d.ts.map +1 -1
- package/dist/src/runtime/emit/eager-prefetch.js +2 -1
- package/dist/src/runtime/emit/eager-prefetch.js.map +1 -1
- package/dist/src/runtime/emit/envelope-scan.d.ts.map +1 -1
- package/dist/src/runtime/emit/envelope-scan.js +2 -1
- package/dist/src/runtime/emit/envelope-scan.js.map +1 -1
- package/dist/src/runtime/emit/fanout-lookup-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/fanout-lookup-join.js +3 -2
- package/dist/src/runtime/emit/fanout-lookup-join.js.map +1 -1
- package/dist/src/runtime/emit/filter.d.ts.map +1 -1
- package/dist/src/runtime/emit/filter.js +8 -3
- package/dist/src/runtime/emit/filter.js.map +1 -1
- package/dist/src/runtime/emit/hash-aggregate.d.ts.map +1 -1
- package/dist/src/runtime/emit/hash-aggregate.js +16 -11
- package/dist/src/runtime/emit/hash-aggregate.js.map +1 -1
- package/dist/src/runtime/emit/insert.d.ts.map +1 -1
- package/dist/src/runtime/emit/insert.js +2 -1
- package/dist/src/runtime/emit/insert.js.map +1 -1
- package/dist/src/runtime/emit/join.d.ts.map +1 -1
- package/dist/src/runtime/emit/join.js +20 -6
- package/dist/src/runtime/emit/join.js.map +1 -1
- package/dist/src/runtime/emit/limit-offset.d.ts.map +1 -1
- package/dist/src/runtime/emit/limit-offset.js +2 -1
- package/dist/src/runtime/emit/limit-offset.js.map +1 -1
- package/dist/src/runtime/emit/literal.d.ts.map +1 -1
- package/dist/src/runtime/emit/literal.js +2 -1
- package/dist/src/runtime/emit/literal.js.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts +22 -0
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.js +128 -16
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
- package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/materialized-view.js +6 -0
- package/dist/src/runtime/emit/materialized-view.js.map +1 -1
- package/dist/src/runtime/emit/merge-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/merge-join.js +14 -4
- package/dist/src/runtime/emit/merge-join.js.map +1 -1
- package/dist/src/runtime/emit/ordinal-slice.d.ts.map +1 -1
- package/dist/src/runtime/emit/ordinal-slice.js +2 -1
- package/dist/src/runtime/emit/ordinal-slice.js.map +1 -1
- package/dist/src/runtime/emit/pragma.d.ts.map +1 -1
- package/dist/src/runtime/emit/pragma.js +2 -1
- package/dist/src/runtime/emit/pragma.js.map +1 -1
- package/dist/src/runtime/emit/project.d.ts.map +1 -1
- package/dist/src/runtime/emit/project.js +7 -2
- package/dist/src/runtime/emit/project.js.map +1 -1
- package/dist/src/runtime/emit/recursive-cte.d.ts.map +1 -1
- package/dist/src/runtime/emit/recursive-cte.js +61 -2
- package/dist/src/runtime/emit/recursive-cte.js.map +1 -1
- package/dist/src/runtime/emit/returning.d.ts.map +1 -1
- package/dist/src/runtime/emit/returning.js +7 -2
- package/dist/src/runtime/emit/returning.js.map +1 -1
- package/dist/src/runtime/emit/scalar-function.d.ts.map +1 -1
- package/dist/src/runtime/emit/scalar-function.js +2 -1
- package/dist/src/runtime/emit/scalar-function.js.map +1 -1
- package/dist/src/runtime/emit/scan.d.ts.map +1 -1
- package/dist/src/runtime/emit/scan.js +55 -17
- package/dist/src/runtime/emit/scan.js.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.js +6 -5
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/runtime/emit/sequencing.d.ts.map +1 -1
- package/dist/src/runtime/emit/sequencing.js +2 -1
- package/dist/src/runtime/emit/sequencing.js.map +1 -1
- package/dist/src/runtime/emit/set-operation.d.ts.map +1 -1
- package/dist/src/runtime/emit/set-operation.js +6 -5
- package/dist/src/runtime/emit/set-operation.js.map +1 -1
- package/dist/src/runtime/emit/sink.d.ts.map +1 -1
- package/dist/src/runtime/emit/sink.js +2 -1
- package/dist/src/runtime/emit/sink.js.map +1 -1
- package/dist/src/runtime/emit/sort.d.ts.map +1 -1
- package/dist/src/runtime/emit/sort.js +2 -1
- package/dist/src/runtime/emit/sort.js.map +1 -1
- package/dist/src/runtime/emit/subquery.d.ts.map +1 -1
- package/dist/src/runtime/emit/subquery.js +42 -23
- package/dist/src/runtime/emit/subquery.js.map +1 -1
- package/dist/src/runtime/emit/table-valued-function.d.ts.map +1 -1
- package/dist/src/runtime/emit/table-valued-function.js +2 -1
- package/dist/src/runtime/emit/table-valued-function.js.map +1 -1
- package/dist/src/runtime/emit/temporal-arithmetic.d.ts.map +1 -1
- package/dist/src/runtime/emit/temporal-arithmetic.js +2 -1
- package/dist/src/runtime/emit/temporal-arithmetic.js.map +1 -1
- package/dist/src/runtime/emit/transaction.d.ts.map +1 -1
- package/dist/src/runtime/emit/transaction.js +2 -1
- package/dist/src/runtime/emit/transaction.js.map +1 -1
- package/dist/src/runtime/emit/unary.d.ts.map +1 -1
- package/dist/src/runtime/emit/unary.js +2 -1
- package/dist/src/runtime/emit/unary.js.map +1 -1
- package/dist/src/runtime/emit/update.d.ts.map +1 -1
- package/dist/src/runtime/emit/update.js +2 -1
- package/dist/src/runtime/emit/update.js.map +1 -1
- package/dist/src/runtime/emit/values.d.ts.map +1 -1
- package/dist/src/runtime/emit/values.js +2 -1
- package/dist/src/runtime/emit/values.js.map +1 -1
- package/dist/src/runtime/emit/view-mutation.d.ts +1 -1
- package/dist/src/runtime/emit/view-mutation.d.ts.map +1 -1
- package/dist/src/runtime/emit/view-mutation.js +6 -3
- package/dist/src/runtime/emit/view-mutation.js.map +1 -1
- package/dist/src/runtime/emit/window.d.ts.map +1 -1
- package/dist/src/runtime/emit/window.js +26 -9
- package/dist/src/runtime/emit/window.js.map +1 -1
- package/dist/src/runtime/emitters.d.ts +11 -3
- package/dist/src/runtime/emitters.d.ts.map +1 -1
- package/dist/src/runtime/emitters.js +13 -22
- package/dist/src/runtime/emitters.js.map +1 -1
- package/dist/src/runtime/foreign-key-actions.d.ts.map +1 -1
- package/dist/src/runtime/foreign-key-actions.js +39 -13
- package/dist/src/runtime/foreign-key-actions.js.map +1 -1
- package/dist/src/runtime/parallel-driver.d.ts +32 -4
- package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
- package/dist/src/runtime/parallel-driver.js +88 -18
- package/dist/src/runtime/parallel-driver.js.map +1 -1
- package/dist/src/runtime/scheduler.d.ts +56 -7
- package/dist/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/src/runtime/scheduler.js +223 -200
- package/dist/src/runtime/scheduler.js.map +1 -1
- package/dist/src/runtime/strict-flags.d.ts +22 -0
- package/dist/src/runtime/strict-flags.d.ts.map +1 -0
- package/dist/src/runtime/strict-flags.js +26 -0
- package/dist/src/runtime/strict-flags.js.map +1 -0
- package/dist/src/runtime/strict-fork.d.ts +4 -4
- package/dist/src/runtime/strict-fork.d.ts.map +1 -1
- package/dist/src/runtime/strict-fork.js +175 -31
- package/dist/src/runtime/strict-fork.js.map +1 -1
- package/dist/src/runtime/types.d.ts +102 -4
- package/dist/src/runtime/types.d.ts.map +1 -1
- package/dist/src/runtime/types.js +37 -0
- package/dist/src/runtime/types.js.map +1 -1
- package/dist/src/runtime/utils.d.ts.map +1 -1
- package/dist/src/runtime/utils.js +8 -17
- package/dist/src/runtime/utils.js.map +1 -1
- package/dist/src/schema/column.d.ts +8 -0
- package/dist/src/schema/column.d.ts.map +1 -1
- package/dist/src/schema/column.js.map +1 -1
- package/dist/src/schema/ddl-generator.js +5 -0
- package/dist/src/schema/ddl-generator.js.map +1 -1
- package/dist/src/schema/function.d.ts +79 -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-compiler.js +2 -2
- package/dist/src/schema/lens-compiler.js.map +1 -1
- package/dist/src/schema/lens-prover.d.ts.map +1 -1
- package/dist/src/schema/lens-prover.js +2 -3
- package/dist/src/schema/lens-prover.js.map +1 -1
- package/dist/src/schema/lens.d.ts +1 -1
- package/dist/src/schema/manager.d.ts.map +1 -1
- package/dist/src/schema/manager.js +7 -2
- package/dist/src/schema/manager.js.map +1 -1
- package/dist/src/schema/rename-rewriter.d.ts +61 -0
- package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
- package/dist/src/schema/rename-rewriter.js +77 -0
- package/dist/src/schema/rename-rewriter.js.map +1 -1
- package/dist/src/schema/table.d.ts +36 -8
- package/dist/src/schema/table.d.ts.map +1 -1
- package/dist/src/schema/table.js +61 -11
- package/dist/src/schema/table.js.map +1 -1
- package/dist/src/schema/unique-enforcement.d.ts +28 -5
- package/dist/src/schema/unique-enforcement.d.ts.map +1 -1
- package/dist/src/schema/unique-enforcement.js +37 -5
- package/dist/src/schema/unique-enforcement.js.map +1 -1
- package/dist/src/schema/view.d.ts +1 -1
- package/dist/src/schema/view.js +1 -1
- package/dist/src/types/json-type.d.ts.map +1 -1
- package/dist/src/types/json-type.js +18 -8
- package/dist/src/types/json-type.js.map +1 -1
- package/dist/src/types/logical-type.d.ts +26 -0
- package/dist/src/types/logical-type.d.ts.map +1 -1
- package/dist/src/types/logical-type.js.map +1 -1
- package/dist/src/util/comparison.d.ts +83 -59
- package/dist/src/util/comparison.d.ts.map +1 -1
- package/dist/src/util/comparison.js +191 -131
- package/dist/src/util/comparison.js.map +1 -1
- package/dist/src/util/json-canonical.d.ts +34 -0
- package/dist/src/util/json-canonical.d.ts.map +1 -0
- package/dist/src/util/json-canonical.js +32 -0
- package/dist/src/util/json-canonical.js.map +1 -0
- package/dist/src/util/key-serializer.d.ts +10 -3
- package/dist/src/util/key-serializer.d.ts.map +1 -1
- package/dist/src/util/key-serializer.js +43 -17
- package/dist/src/util/key-serializer.js.map +1 -1
- package/dist/src/util/key-tuple-codec.d.ts +37 -0
- package/dist/src/util/key-tuple-codec.d.ts.map +1 -0
- package/dist/src/util/key-tuple-codec.js +65 -0
- package/dist/src/util/key-tuple-codec.js.map +1 -0
- package/dist/src/util/latches.d.ts +29 -3
- package/dist/src/util/latches.d.ts.map +1 -1
- package/dist/src/util/latches.js +60 -6
- package/dist/src/util/latches.js.map +1 -1
- package/dist/src/util/patterns.d.ts +22 -8
- package/dist/src/util/patterns.d.ts.map +1 -1
- package/dist/src/util/patterns.js +87 -16
- package/dist/src/util/patterns.js.map +1 -1
- package/dist/src/util/qualified-name.d.ts +10 -0
- package/dist/src/util/qualified-name.d.ts.map +1 -0
- package/dist/src/util/qualified-name.js +15 -0
- package/dist/src/util/qualified-name.js.map +1 -0
- package/dist/src/vtab/backing-host.d.ts +8 -1
- package/dist/src/vtab/backing-host.d.ts.map +1 -1
- package/dist/src/vtab/backing-host.js +7 -0
- package/dist/src/vtab/backing-host.js.map +1 -1
- package/dist/src/vtab/best-access-plan.d.ts +18 -0
- package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
- package/dist/src/vtab/best-access-plan.js +15 -0
- package/dist/src/vtab/best-access-plan.js.map +1 -1
- package/dist/src/vtab/capabilities.d.ts +44 -0
- package/dist/src/vtab/capabilities.d.ts.map +1 -1
- package/dist/src/vtab/filter-info.d.ts +42 -0
- package/dist/src/vtab/filter-info.d.ts.map +1 -1
- package/dist/src/vtab/filter-info.js +137 -1
- package/dist/src/vtab/filter-info.js.map +1 -1
- package/dist/src/vtab/idx-str.d.ts +62 -0
- package/dist/src/vtab/idx-str.d.ts.map +1 -0
- package/dist/src/vtab/idx-str.js +130 -0
- package/dist/src/vtab/idx-str.js.map +1 -0
- package/dist/src/vtab/index-descriptor.d.ts +122 -0
- package/dist/src/vtab/index-descriptor.d.ts.map +1 -0
- package/dist/src/vtab/index-descriptor.js +104 -0
- package/dist/src/vtab/index-descriptor.js.map +1 -0
- package/dist/src/vtab/mapping-advertisement.d.ts +1 -1
- package/dist/src/vtab/memory/index.d.ts +12 -4
- package/dist/src/vtab/memory/index.d.ts.map +1 -1
- package/dist/src/vtab/memory/index.js +17 -9
- package/dist/src/vtab/memory/index.js.map +1 -1
- package/dist/src/vtab/memory/layer/base.d.ts +85 -31
- package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/base.js +127 -86
- package/dist/src/vtab/memory/layer/base.js.map +1 -1
- package/dist/src/vtab/memory/layer/connection.d.ts +27 -0
- package/dist/src/vtab/memory/layer/connection.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/connection.js +36 -1
- package/dist/src/vtab/memory/layer/connection.js.map +1 -1
- package/dist/src/vtab/memory/layer/interface.d.ts +24 -4
- package/dist/src/vtab/memory/layer/interface.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.d.ts +212 -16
- package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.js +754 -170
- package/dist/src/vtab/memory/layer/manager.js.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.d.ts +26 -1
- package/dist/src/vtab/memory/layer/plan-filter.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.js +29 -11
- package/dist/src/vtab/memory/layer/plan-filter.js.map +1 -1
- package/dist/src/vtab/memory/layer/safe-iterate.d.ts +2 -2
- package/dist/src/vtab/memory/layer/safe-iterate.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/safe-iterate.js +2 -2
- package/dist/src/vtab/memory/layer/safe-iterate.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.d.ts +5 -1
- package/dist/src/vtab/memory/layer/scan-layer.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.js +59 -24
- package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.js +13 -21
- package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.d.ts +169 -7
- package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.js +334 -9
- package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
- package/dist/src/vtab/memory/module.d.ts +11 -4
- package/dist/src/vtab/memory/module.d.ts.map +1 -1
- package/dist/src/vtab/memory/module.js +35 -16
- package/dist/src/vtab/memory/module.js.map +1 -1
- package/dist/src/vtab/memory/table.d.ts +30 -4
- package/dist/src/vtab/memory/table.d.ts.map +1 -1
- package/dist/src/vtab/memory/table.js +63 -15
- package/dist/src/vtab/memory/table.js.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.d.ts +9 -1
- package/dist/src/vtab/memory/utils/predicate.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.js +35 -14
- package/dist/src/vtab/memory/utils/predicate.js.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key-encode.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key-encode.js +12 -6
- package/dist/src/vtab/memory/utils/primary-key-encode.js.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key.d.ts +6 -1
- package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key.js +12 -8
- package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
- package/dist/src/vtab/module.d.ts +38 -14
- package/dist/src/vtab/module.d.ts.map +1 -1
- package/dist/src/vtab/table.d.ts +41 -2
- package/dist/src/vtab/table.d.ts.map +1 -1
- package/dist/src/vtab/table.js +19 -0
- package/dist/src/vtab/table.js.map +1 -1
- package/package.json +5 -4
- package/dist/src/planner/rules/cache/rule-materialization-advisory.d.ts +0 -18
- package/dist/src/planner/rules/cache/rule-materialization-advisory.d.ts.map +0 -1
- package/dist/src/planner/rules/cache/rule-materialization-advisory.js +0 -65
- package/dist/src/planner/rules/cache/rule-materialization-advisory.js.map +0 -1
|
@@ -0,0 +1,1423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialized-view maintenance — **plan builders**. The cost-gated builder that compiles
|
|
3
|
+
* an MV body into a {@link MaintenancePlan} (the bounded-delta arms plus the full-rebuild
|
|
4
|
+
* floor) and the residual-compile / source-stats helpers they share. Extracted from
|
|
5
|
+
* database-materialized-views.ts as free functions over {@link MaterializedViewManagerContext};
|
|
6
|
+
* the sole caller in the manager is `registerMaterializedView` → {@link buildMaintenancePlan}.
|
|
7
|
+
*/
|
|
8
|
+
import { QuereusError } from '../common/errors.js';
|
|
9
|
+
import { StatusCode } from '../common/types.js';
|
|
10
|
+
import { ColumnReferenceNode } from '../planner/nodes/reference.js';
|
|
11
|
+
import { AggregateFunctionCallNode } from '../planner/nodes/aggregate-function.js';
|
|
12
|
+
import { isAggregateFunctionSchema } from '../schema/function.js';
|
|
13
|
+
import { PhysicalType } from '../types/logical-type.js';
|
|
14
|
+
import { checkDeterministic } from '../planner/validation/determinism-validator.js';
|
|
15
|
+
import { emitPlanNode } from '../runtime/emitters.js';
|
|
16
|
+
import { EmissionContext } from '../runtime/emission-context.js';
|
|
17
|
+
import { Scheduler } from '../runtime/scheduler.js';
|
|
18
|
+
import { PlanNodeType } from '../planner/nodes/plan-node-type.js';
|
|
19
|
+
import { injectKeyFilter } from '../planner/analysis/key-filter.js';
|
|
20
|
+
import { keysOf } from '../planner/util/fd-utils.js';
|
|
21
|
+
import { deriveCoarsenedBackingKey, resolveValuePreservingSourceCol } from '../planner/analysis/coarsened-key.js';
|
|
22
|
+
import { proveOneToOneJoin } from '../planner/analysis/coverage-prover.js';
|
|
23
|
+
import { CapabilityDetectors } from '../planner/framework/characteristics.js';
|
|
24
|
+
import { selectMaintenanceStrategy, isFullRebuildPathological, seqScanCost, filterCost, projectCost, } from '../planner/cost/index.js';
|
|
25
|
+
import { tryResolveBackingHost } from '../runtime/emit/materialized-view-helpers.js';
|
|
26
|
+
import { compilePredicate } from '../vtab/memory/utils/predicate.js';
|
|
27
|
+
import { findNonReplicableFunction, findNonReplicableCollation, findNonDeterministic, normalizeCollation, isBinaryCollation, findAggregate, containsNodeType, containsAnyJoin, countNodeType, countJoins, findTableFunctionCall, collectTableRefs, collectProducingExprs, resolveTransitiveSourceCol, bodyWhereReferencesLookup, bodyWhereIsNonDeterministic, relationalAttributes, rootRelationalNode, cannotMaterialize, nonReplicableDerivationError, nonReplicableCollationDerivationError, isSingleRowEvaluable, compileSourceRowEvaluator, } from './database-materialized-views-analysis.js';
|
|
28
|
+
import { BINARY_COLLATION } from '../util/comparison.js';
|
|
29
|
+
/** Fallback source row estimate when the StatsProvider has no count (mirrors the
|
|
30
|
+
* optimizer's naive default). Only feeds the create-time maintenance cost gate. */
|
|
31
|
+
const DEFAULT_SOURCE_ROWS = 1000;
|
|
32
|
+
/** Create-time estimate of the fraction of changes to a **tighten-only** (min/max) delta
|
|
33
|
+
* aggregate that retract (a delete / update-old-image) and so re-derive their group from
|
|
34
|
+
* the key-filtered residual instead of pure arithmetic. Feeds the `'delta-aggregate'` cost
|
|
35
|
+
* blend ({@link maintenanceCost}); kept well below 1 so an insert-dominated body still
|
|
36
|
+
* prefers the delta arm, yet non-zero so a tighten body costs more than a pure-group one. */
|
|
37
|
+
const DELTA_TIGHTEN_FALLBACK_RATIO = 0.25;
|
|
38
|
+
/**
|
|
39
|
+
* Snapshot a backing table's physical primary key as the maintenance plans consume it: the
|
|
40
|
+
* declared `(index, desc, collation)` triple plus the collation comparator that name resolves
|
|
41
|
+
* to on this database. Every arm builds its `backingPkDefinition` through here, so the per-row
|
|
42
|
+
* key comparisons in `database-materialized-views-apply.ts` never do a registry lookup — and
|
|
43
|
+
* never silently degrade a custom collation to byte order.
|
|
44
|
+
*
|
|
45
|
+
* NOTE: this freezes the comparator into the plan. Re-registering a collation name with
|
|
46
|
+
* `db.registerCollation(...)` after the plan was built leaves the plan on the old comparator
|
|
47
|
+
* until the MV is re-registered — which any schema change does, but a bare `registerCollation`
|
|
48
|
+
* does not. Same exposure as index comparators, which resolve once at table open; if that ever
|
|
49
|
+
* needs fixing, fix it for both.
|
|
50
|
+
*
|
|
51
|
+
* NOTE: `resolver` throws on an unregistered collation name. Unreachable today — the memory
|
|
52
|
+
* module rejects the backing `create` first — so this never converts a working `create
|
|
53
|
+
* materialized view` into an error. If a backing host is ever added that tolerates an
|
|
54
|
+
* unregistered name on a key column, this becomes the first site to throw, at plan-build time.
|
|
55
|
+
*/
|
|
56
|
+
function resolveBackingPkColumns(backing, resolver) {
|
|
57
|
+
return backing.primaryKeyDefinition.map(d => ({
|
|
58
|
+
index: d.index,
|
|
59
|
+
desc: d.desc,
|
|
60
|
+
collation: d.collation,
|
|
61
|
+
collationFn: d.collation ? resolver(d.collation) : BINARY_COLLATION,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the row-time maintenance plan for an MV — **cost-gated, with a floor, never a
|
|
66
|
+
* shape allowlist**. The builder tries to match a bounded-delta arm by shape
|
|
67
|
+
* ({@link tryBuildBoundedDeltaArm}); a body that matches **none** falls through to the
|
|
68
|
+
* always-correct {@link buildFullRebuildPlan} floor (re-evaluate the whole body, replace
|
|
69
|
+
* the backing transactionally). **No body is rejected for its shape.** Only five
|
|
70
|
+
* create-time rejections remain, all non-shape (docs/invariants.md MV-006):
|
|
71
|
+
* - a **non-deterministic** body without `pragma nondeterministic_schema` — a hard reject
|
|
72
|
+
* in the matched arm (so the arm-specific determinism diagnostic survives) or, for a
|
|
73
|
+
* body matching no arm, in the floor's whole-body determinism check;
|
|
74
|
+
* - a **bag** (no provable unique key) — the floor's `keysOf` reject (a duplicate-producing
|
|
75
|
+
* body usually fails the set contract earlier, at create-fill);
|
|
76
|
+
* - a body with **no relational output**;
|
|
77
|
+
* - a body that **reads no source table** (`select 42`) — the floor's `sourceBases` reject;
|
|
78
|
+
* there is no source to index the plan under and no write that could dirty it;
|
|
79
|
+
* - a **full-rebuild-only body over a source past the size threshold**
|
|
80
|
+
* ({@link isFullRebuildPathological}, the `materialized_view_rebuild_row_threshold` option).
|
|
81
|
+
*
|
|
82
|
+
* The single source may itself be another MV's backing table (an MV-over-MV body):
|
|
83
|
+
* `building/select.ts` rewrites a reference to `mv1` into a `TableReference` against
|
|
84
|
+
* `mv1`'s backing table, so the source base is `mv1`'s backing base and the same checks
|
|
85
|
+
* evaluate against the (keyed `memory`) backing schema unchanged. A write to `mv1` then
|
|
86
|
+
* drives `mv2` via the cascade in {@link maintainRowTime}.
|
|
87
|
+
*
|
|
88
|
+
* Eligibility is a *cost choice* among the body's structurally-sound strategies
|
|
89
|
+
* ({@link selectMaintenanceStrategy}): the bounded-delta arms are preferred by the argmin
|
|
90
|
+
* cost gate, and full-rebuild is selected exactly when no bounded-delta arm is sound (an
|
|
91
|
+
* empty sound set resolves to the floor) — so an existing eligible shape is unaffected.
|
|
92
|
+
*/
|
|
93
|
+
export function buildMaintenancePlan(ctx, mv) {
|
|
94
|
+
const db = ctx;
|
|
95
|
+
// Analyze the MV's own body to compile maintenance; suppress the read-side
|
|
96
|
+
// rewrite so the body stays over its SOURCE table, not re-pointed at this
|
|
97
|
+
// MV's backing (which the maintenance plan is what keeps consistent).
|
|
98
|
+
const analyzed = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
|
|
99
|
+
const { plan } = ctx._buildPlan([mv.derivation.selectAst]);
|
|
100
|
+
return ctx.optimizer.optimizeForAnalysis(plan, db);
|
|
101
|
+
});
|
|
102
|
+
// Replicable-determinism gate — host-conditional, inert by default. A backing host
|
|
103
|
+
// whose backing replicates across peers (the sync-store) sets
|
|
104
|
+
// `requiresReplicableDerivations` so a platform-dependent UDF or collation cannot
|
|
105
|
+
// diverge peers. Checked here — after `analyzed`, before arm selection — so it applies
|
|
106
|
+
// regardless of which maintenance arm wins, over the SAME analyzed plan the determinism
|
|
107
|
+
// gate walks (nested calls, WHERE / GROUP BY / aggregate-arg / TVF-arg positions all
|
|
108
|
+
// reached). It sits NEXT TO, not in place of, the determinism gate: the two are
|
|
109
|
+
// orthogonal, so this is NOT lifted by `pragma nondeterministic_schema`. A memory/store
|
|
110
|
+
// host leaves the flag undefined ⇒ this block is skipped ⇒ zero behavior change.
|
|
111
|
+
// Idempotent (same body ⇒ same verdict), so it is also desirable on re-register /
|
|
112
|
+
// catalog import: a tampered catalog cannot smuggle a non-replicable body past a
|
|
113
|
+
// demanding host. Two gates of the same shape: a non-replicable FUNCTION (which the
|
|
114
|
+
// body walk's function-bearing nodes carry) and a non-replicable COLLATION (which rides
|
|
115
|
+
// each scalar node's resolved type plus the backing key's declared collations).
|
|
116
|
+
// Resolve the host LENIENTLY: at the create-time gate registration of an
|
|
117
|
+
// `alter table … set maintained` attach, a module that materializes its durable
|
|
118
|
+
// backing late (lamina's `ensureBackingForAttach`, after this gate) has no host
|
|
119
|
+
// yet. The host is used here ONLY for the host-conditional, default-inert
|
|
120
|
+
// `requiresReplicableDerivations` gate — a host that sets it (the synced-store
|
|
121
|
+
// flavor) always exists by plan-build time, so skipping the gate when the host
|
|
122
|
+
// is absent never lets a non-replicable body slip past. The reconcile resolves
|
|
123
|
+
// the host for real, and the maintenance arms re-resolve it per use.
|
|
124
|
+
const host = tryResolveBackingHost(db, mv);
|
|
125
|
+
if (host?.requiresReplicableDerivations) {
|
|
126
|
+
const offendingFn = findNonReplicableFunction(analyzed);
|
|
127
|
+
if (offendingFn)
|
|
128
|
+
throw nonReplicableDerivationError(mv.name, offendingFn);
|
|
129
|
+
const offendingCollation = findNonReplicableCollation(analyzed, mv, db);
|
|
130
|
+
if (offendingCollation)
|
|
131
|
+
throw nonReplicableCollationDerivationError(mv.name, offendingCollation);
|
|
132
|
+
}
|
|
133
|
+
// Try a bounded-delta arm; a shape that fits none falls through to the floor.
|
|
134
|
+
const boundedDelta = tryBuildBoundedDeltaArm(ctx, mv, analyzed);
|
|
135
|
+
// A residual arm additionally carries the whole-body scheduler — the per-statement
|
|
136
|
+
// degrade-to-rebuild escape: when a statement's distinct affected-key count makes k
|
|
137
|
+
// residual runs cost more than one rebuild (`shouldDegradeToRebuild`), the flush runs
|
|
138
|
+
// this and applies a single `'replace-all'` diff instead. Compiled here (not per arm
|
|
139
|
+
// builder) so the three residual arms share one compilation site; inverse-projection
|
|
140
|
+
// never degrades (always the cheapest arm) and the floor has its own bodyScheduler.
|
|
141
|
+
// NOTE: adds one whole-body optimize+emit per residual-MV (re)registration; if
|
|
142
|
+
// registration latency ever matters (e.g. bulk catalog import), compile this lazily
|
|
143
|
+
// on the first degrade instead.
|
|
144
|
+
if (boundedDelta && boundedDelta.kind !== 'inverse-projection' && boundedDelta.kind !== 'full-rebuild') {
|
|
145
|
+
boundedDelta.fullRebuildScheduler = compileWholeBodyScheduler(ctx, analyzed);
|
|
146
|
+
}
|
|
147
|
+
const plan = boundedDelta ?? buildFullRebuildPlan(ctx, mv, analyzed);
|
|
148
|
+
// Precompute the NOT-NULL ordering-seeded PK skew guard once, here at plan build —
|
|
149
|
+
// which `alter … drop not null` re-runs (it recompiles the row-time plan live), so the
|
|
150
|
+
// guarded set is recomputed at exactly the moment the skew can appear. Gated to
|
|
151
|
+
// non-empty so the field stays `undefined` (zero per-write cost) for the common case.
|
|
152
|
+
const guarded = computeNullGuardColumns(mv, analyzed);
|
|
153
|
+
plan.nullGuardColumns = guarded.length > 0 ? guarded : undefined;
|
|
154
|
+
return plan;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Backing columns that would silently store a NULL into a declared-NOT-NULL physical-PK
|
|
158
|
+
* column if the source loosened — the reachable "NOT-NULL ordering-seeded PK over a
|
|
159
|
+
* loosened source" skew, precomputed for the row-time guard
|
|
160
|
+
* ({@link MaterializedViewManager.maintainRowTime}). A backing column `i` is guarded when
|
|
161
|
+
* it is (1) declared NOT NULL, (2) a physical-PK member, AND (3) its re-derived body output
|
|
162
|
+
* column `i` is now nullable. Backing column `i` corresponds 1:1 to body output column `i`
|
|
163
|
+
* (`deriveBackingShape` builds the backing column list positionally from the body's output),
|
|
164
|
+
* and `deriveBackingShape`'s own `notNull = type.nullable === false` derivation means the
|
|
165
|
+
* body-nullability term agrees with the backing declaration by construction.
|
|
166
|
+
*
|
|
167
|
+
* The body-nullability term (3) is the real discriminator: the NOT-NULL/physical-PK set
|
|
168
|
+
* alone is non-empty for almost every MV (the logical-key PK column is normally the NOT-NULL
|
|
169
|
+
* source PK), so guarding on (1)+(2) only would scan essentially every maintained write.
|
|
170
|
+
* (3) is true only for the rare loosened-source skew, so the returned set — hence
|
|
171
|
+
* `plan.nullGuardColumns` — is empty (the field stays `undefined`) for the common case. A
|
|
172
|
+
* physical-PK column declared *nullable* (a nullable-source ordering column) fails (1) and is
|
|
173
|
+
* never guarded — it self-consistently stores NULL and must keep working.
|
|
174
|
+
*/
|
|
175
|
+
function computeNullGuardColumns(mv, analyzed) {
|
|
176
|
+
const root = rootRelationalNode(analyzed);
|
|
177
|
+
if (!root)
|
|
178
|
+
return [];
|
|
179
|
+
const bodyColumns = root.getType().columns;
|
|
180
|
+
const pkIndices = new Set(mv.primaryKeyDefinition.map(d => d.index));
|
|
181
|
+
const guarded = [];
|
|
182
|
+
for (let i = 0; i < mv.columns.length; i++) {
|
|
183
|
+
const col = mv.columns[i];
|
|
184
|
+
if (col.notNull !== true)
|
|
185
|
+
continue; // nullable-declared PK column stays permitted
|
|
186
|
+
if (!pkIndices.has(i))
|
|
187
|
+
continue; // physical-PK member only
|
|
188
|
+
const bodyCol = bodyColumns[i];
|
|
189
|
+
if (bodyCol && bodyCol.type.nullable !== false) { // body now yields nullable
|
|
190
|
+
guarded.push({ index: i, name: col.name });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return guarded;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Route the analyzed body to the matching bounded-delta arm, or return `null` when its
|
|
197
|
+
* shape fits **no** bounded-delta arm (the caller then builds the full-rebuild floor).
|
|
198
|
+
* Each arm builder likewise returns `null` on a sub-shape mismatch and falls through
|
|
199
|
+
* here. The arms keep only **determinism** as a hard reject (so their arm-specific
|
|
200
|
+
* determinism diagnostic survives — see the individual builders); every other mismatch
|
|
201
|
+
* is a `null` fall-through. Bag / no-output / size rejects live in the floor.
|
|
202
|
+
*/
|
|
203
|
+
export function tryBuildBoundedDeltaArm(ctx, mv, analyzed) {
|
|
204
|
+
// A body that reads no source table has no bounded-delta arm → floor (which rejects
|
|
205
|
+
// a sourceless body). (A self-join / TVF fan-out surfaces ≥2 refs or a TVF node.)
|
|
206
|
+
const tableRefs = [...collectTableRefs(analyzed).values()];
|
|
207
|
+
if (tableRefs.length === 0)
|
|
208
|
+
return null;
|
|
209
|
+
// Shapes no bounded-delta arm models — a window function reads across the partition,
|
|
210
|
+
// set ops / recursive CTEs / DISTINCT / row caps are out of the bounded-delta model.
|
|
211
|
+
// They are NOT rejected: a deterministic, keyed such body is maintained by the floor.
|
|
212
|
+
if (containsNodeType(analyzed, PlanNodeType.Window))
|
|
213
|
+
return null;
|
|
214
|
+
if (containsNodeType(analyzed, PlanNodeType.Distinct))
|
|
215
|
+
return null;
|
|
216
|
+
if (containsNodeType(analyzed, PlanNodeType.SetOperation))
|
|
217
|
+
return null;
|
|
218
|
+
if (containsNodeType(analyzed, PlanNodeType.RecursiveCTE))
|
|
219
|
+
return null;
|
|
220
|
+
if (mv.derivation.selectAst.type === 'select' && (mv.derivation.selectAst.limit !== undefined || mv.derivation.selectAst.offset !== undefined)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const tableRef = tableRefs[0];
|
|
224
|
+
const sourceSchema = tableRef.tableSchema;
|
|
225
|
+
const sourceBase = `${sourceSchema.schemaName}.${sourceSchema.name}`.toLowerCase();
|
|
226
|
+
// Single base source `T` joined to ONE lateral table-valued function — a fan-out
|
|
227
|
+
// body (each base row produces N backing rows) → the prefix-delete arm. Routed
|
|
228
|
+
// *before* the generic join branch below, because a lateral fan-out surfaces BOTH a
|
|
229
|
+
// Join and a TableFunctionCall. A multi-base TVF body falls to the floor.
|
|
230
|
+
if (containsNodeType(analyzed, PlanNodeType.TableFunctionCall)) {
|
|
231
|
+
if (tableRefs.length !== 1)
|
|
232
|
+
return null;
|
|
233
|
+
return buildLateralTvfPrefixDeletePlan(ctx, mv, analyzed, tableRef, sourceBase);
|
|
234
|
+
}
|
|
235
|
+
// Any join → the provably-1:1 join-residual arm. A fanning (non-1:1) join, an outer
|
|
236
|
+
// join, a >2-source join, an aggregate over a join, or a partial WHERE returns `null`
|
|
237
|
+
// from the builder → floor. (The lateral-TVF fan-out above is matched first because
|
|
238
|
+
// it also surfaces a join node.)
|
|
239
|
+
if (containsAnyJoin(analyzed)) {
|
|
240
|
+
return buildJoinResidualPlan(ctx, mv, analyzed, tableRefs);
|
|
241
|
+
}
|
|
242
|
+
// A non-join multi-source body (e.g. a WHERE-subquery over a second table) has no
|
|
243
|
+
// bounded-delta arm → floor.
|
|
244
|
+
if (tableRefs.length > 1)
|
|
245
|
+
return null;
|
|
246
|
+
// Single-source aggregate (`group by` over bare columns) → residual-recompute arm.
|
|
247
|
+
// Each changed source row belongs to exactly one group; maintenance recomputes that
|
|
248
|
+
// group's backing row from live state. A scalar aggregate (no GROUP BY) falls to the
|
|
249
|
+
// floor.
|
|
250
|
+
const aggregate = findAggregate(analyzed);
|
|
251
|
+
if (aggregate) {
|
|
252
|
+
return buildAggregateResidualPlan(ctx, mv, analyzed, tableRef, sourceBase, aggregate);
|
|
253
|
+
}
|
|
254
|
+
// The covering-index shape → inverse-projection arm (the default single-source arm).
|
|
255
|
+
return buildInverseProjectionPlan(ctx, mv, analyzed, tableRef, sourceBase);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Build an `'inverse-projection'` plan for the covering-index shape: a single
|
|
259
|
+
* row-preserving source `T` with a primary key, a linear
|
|
260
|
+
* `TableReference → optional Filter → Project → optional Sort` body, a projection that
|
|
261
|
+
* resolves every source PK column (and every backing-key column) to a **passthrough**
|
|
262
|
+
* source column — non-key columns may instead be a **deterministic scalar expression**
|
|
263
|
+
* over the source row — and a partial WHERE evaluable on a single source row. Returns
|
|
264
|
+
* `null` on any **shape** mismatch (the caller falls through to the full-rebuild floor);
|
|
265
|
+
* a **non-deterministic** computed column is the one hard reject (its arm-specific
|
|
266
|
+
* determinism diagnostic must survive rather than fall through to the floor's generic one).
|
|
267
|
+
*/
|
|
268
|
+
export function buildInverseProjectionPlan(ctx, mv, analyzed, tableRef, sourceBase) {
|
|
269
|
+
const db = ctx;
|
|
270
|
+
const sourceSchema = tableRef.tableSchema;
|
|
271
|
+
const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
|
|
272
|
+
if (sourcePkCols.length === 0)
|
|
273
|
+
return null; // source has no PK → floor
|
|
274
|
+
const backing = ctx._findTable(mv.name, mv.schemaName);
|
|
275
|
+
if (!backing) {
|
|
276
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
277
|
+
}
|
|
278
|
+
// Projection classification: each backing output column is either a passthrough
|
|
279
|
+
// source column (a pure permutation entry) or a deterministic scalar expression
|
|
280
|
+
// over the single source row. A passthrough makes maintenance a column copy; an
|
|
281
|
+
// expression column evaluates `project(sourceRow)` via the runtime (still a pure
|
|
282
|
+
// per-row function — O(log n), no body re-execution). PK / backing-key columns
|
|
283
|
+
// must stay passthrough (the backing key and the inverse-projection conflict map
|
|
284
|
+
// depend on it); non-key columns may be computed.
|
|
285
|
+
//
|
|
286
|
+
// "Passthrough" is value-preserving lineage (`resolveValuePreservingSourceCol`):
|
|
287
|
+
// a bare column reference, OR one wrapped in `collate` / a no-op `cast` — those
|
|
288
|
+
// wrappers copy the source VALUE verbatim, so the column-copy maintenance is
|
|
289
|
+
// exact. This is what lets the collation-weakening migration shape (`select b
|
|
290
|
+
// collate nocase as b from t`) register here with its coarsened backing key:
|
|
291
|
+
// the per-row upsert is keyed under the backing PK's (output) collation, so a
|
|
292
|
+
// colliding source row last-write-wins into the shared backing row, and a
|
|
293
|
+
// delete of one colliding sibling removes the shared row (the documented
|
|
294
|
+
// anomaly — docs/materialized-views.md § Coarsened backing keys).
|
|
295
|
+
const sourceAttrToCol = new Map();
|
|
296
|
+
const sourceDescriptor = [];
|
|
297
|
+
tableRef.getAttributes().forEach((a, i) => {
|
|
298
|
+
sourceAttrToCol.set(a.id, i);
|
|
299
|
+
sourceDescriptor[a.id] = i;
|
|
300
|
+
});
|
|
301
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
302
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
303
|
+
if (!rootAttrs)
|
|
304
|
+
return null; // no relational output → floor (which hard-rejects it)
|
|
305
|
+
const projectors = [];
|
|
306
|
+
for (let outCol = 0; outCol < rootAttrs.length; outCol++) {
|
|
307
|
+
const attr = rootAttrs[outCol];
|
|
308
|
+
const sourceCol = attr ? resolveValuePreservingSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
|
|
309
|
+
if (sourceCol !== undefined) {
|
|
310
|
+
projectors.push({ kind: 'passthrough', sourceCol });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// Computed column: a deterministic scalar over the source row. A
|
|
314
|
+
// non-deterministic producer is a HARD reject (the arm-specific determinism
|
|
315
|
+
// diagnostic must survive — so `random()` fails on *determinism*, not by silently
|
|
316
|
+
// falling to a still-rejected floor); a deterministic-but-unsupported *shape*
|
|
317
|
+
// (no resolvable producer, a subquery / cross-row reference, an async form)
|
|
318
|
+
// returns `null` → the floor.
|
|
319
|
+
const colName = attr?.name ?? `#${outCol}`;
|
|
320
|
+
const producing = attr ? producingByAttrId.get(attr.id) : undefined;
|
|
321
|
+
if (!producing)
|
|
322
|
+
return null;
|
|
323
|
+
const det = checkDeterministic(producing);
|
|
324
|
+
if (!det.valid) {
|
|
325
|
+
throw cannotMaterialize(mv.name, `it projects a non-deterministic expression column '${colName}' (${det.expression}); `
|
|
326
|
+
+ `a row-time backing value must be reproducible from the source row`);
|
|
327
|
+
}
|
|
328
|
+
if (!isSingleRowEvaluable(producing, sourceDescriptor))
|
|
329
|
+
return null;
|
|
330
|
+
let evalFn;
|
|
331
|
+
try {
|
|
332
|
+
evalFn = compileSourceRowEvaluator(db, producing, sourceDescriptor);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null; // not row-time maintainable as a single-row scalar → floor
|
|
336
|
+
}
|
|
337
|
+
projectors.push({ kind: 'expr', eval: evalFn });
|
|
338
|
+
}
|
|
339
|
+
// Every source PK column must be projected as a passthrough column so the backing
|
|
340
|
+
// key is a deterministic identity of the source row that `lookupCoveringConflicts`
|
|
341
|
+
// can invert. A PK column produced only via an expression (or not at all) breaks
|
|
342
|
+
// that recovery.
|
|
343
|
+
const passthroughSourceCols = new Set(projectors.flatMap(p => p.kind === 'passthrough' ? [p.sourceCol] : []));
|
|
344
|
+
for (const pk of sourcePkCols) {
|
|
345
|
+
if (!passthroughSourceCols.has(pk))
|
|
346
|
+
return null; // PK not passthrough-projected → floor
|
|
347
|
+
}
|
|
348
|
+
const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
|
|
349
|
+
// A computed column may never land in the backing primary key: the btree keys on
|
|
350
|
+
// it and `lookupCoveringConflicts` recovers the source PK from it, both of which
|
|
351
|
+
// require a passthrough source-column identity.
|
|
352
|
+
for (const d of backingPkDefinition) {
|
|
353
|
+
if (projectors[d.index]?.kind !== 'passthrough')
|
|
354
|
+
return null; // computed backing-key col → floor
|
|
355
|
+
}
|
|
356
|
+
// Partial WHERE must be evaluable on a single source row (no subqueries /
|
|
357
|
+
// cross-row references). `compilePredicate` throws on unsupported forms; an
|
|
358
|
+
// unsupported WHERE shape falls to the floor.
|
|
359
|
+
//
|
|
360
|
+
// No owning-table name is passed (lenient qualifier handling), deliberately: the
|
|
361
|
+
// view body may reference the source through a FROM alias, not the source table's
|
|
362
|
+
// own name, so validating against `sourceSchema.name` would wrongly reject a legal
|
|
363
|
+
// aliased body — and this site FLOORS to full-rebuild on any throw rather than
|
|
364
|
+
// erroring, turning a false rejection into a silent maintenance regression.
|
|
365
|
+
// NOTE: to make MV WHERE strict, thread the alias set from the view's FROM into
|
|
366
|
+
// this builder (not currently available here) and validate against it.
|
|
367
|
+
let predicate;
|
|
368
|
+
const bodyWhere = mv.derivation.selectAst.type === 'select' ? mv.derivation.selectAst.where : undefined;
|
|
369
|
+
if (bodyWhere) {
|
|
370
|
+
try {
|
|
371
|
+
predicate = compilePredicate(bodyWhere, sourceSchema.columns);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return null; // WHERE not evaluable on a single source row → floor
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// ── Cost gate (incremental-maintenance-cost-gate) ──
|
|
378
|
+
// The covering-index shape's only structurally-sound maintenance strategy is
|
|
379
|
+
// 'inverse-projection' (O(1) per changed row); 'full-rebuild' is the floor for bodies
|
|
380
|
+
// this arm did NOT match (reached via the `null` fall-through above), so it is not a
|
|
381
|
+
// competitor here. Eligibility is thus a cost choice among the sound strategies (argmin
|
|
382
|
+
// maintenanceCost); for this shape it resolves to inverse-projection while recording the
|
|
383
|
+
// choice + the cost inputs the runtime reuses.
|
|
384
|
+
const soundStrategies = ['inverse-projection'];
|
|
385
|
+
const sourceStats = estimateMaintenanceStats(ctx, sourceSchema, projectors.length, predicate !== undefined);
|
|
386
|
+
// Create-time change-cardinality estimate: ~1% of the source per statement (typical OLTP).
|
|
387
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
388
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
389
|
+
// Defensive: this arm's sound set is exactly ['inverse-projection']. A different choice
|
|
390
|
+
// would mean the set grew without the corresponding apply-arm — fail loud rather than
|
|
391
|
+
// register an unexecutable plan.
|
|
392
|
+
if (chosenStrategy !== 'inverse-projection') {
|
|
393
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
kind: 'inverse-projection',
|
|
397
|
+
mv,
|
|
398
|
+
sourceBase,
|
|
399
|
+
backingSchema: mv.schemaName,
|
|
400
|
+
backingTableName: mv.name,
|
|
401
|
+
chosenStrategy,
|
|
402
|
+
sourceStats,
|
|
403
|
+
backingPkDefinition,
|
|
404
|
+
projectors,
|
|
405
|
+
predicate,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Build a `'residual-recompute'` plan for a single-source aggregate body
|
|
410
|
+
* (`select g1,…, agg(…) from T [where P] group by g1,…` over **bare** group columns),
|
|
411
|
+
* or return `null` on a shape mismatch (the caller falls through to the full-rebuild
|
|
412
|
+
* floor). Each changed source row belongs to exactly one group `(g1,…)`; maintaining the
|
|
413
|
+
* MV means recomputing that group's backing row from live state — delete the old slice,
|
|
414
|
+
* run the group-keyed residual, upsert the recomputed slice (zero rows when the group
|
|
415
|
+
* emptied). See {@link ResidualRecomputePlan} and `docs/incremental-maintenance.md`
|
|
416
|
+
* § residual-recompute.
|
|
417
|
+
*
|
|
418
|
+
* A **non-deterministic** group/aggregate expression is the one hard reject (the
|
|
419
|
+
* arm-specific determinism diagnostic must survive); every other mismatch — a scalar
|
|
420
|
+
* aggregate, a computed group key, a backing key that is not the group key — returns
|
|
421
|
+
* `null` → the floor.
|
|
422
|
+
*
|
|
423
|
+
* NOTE: the group binding is derived **directly** from the aggregate node's bare GROUP
|
|
424
|
+
* BY columns, not via `extractBindings`. `analyzeRowSpecific`'s `'group'` classification
|
|
425
|
+
* additionally requires the group key to cover a *source* unique key (so it reports
|
|
426
|
+
* `'global'` for the common `group by <non-key>` body), which is the wrong test here —
|
|
427
|
+
* the backing is keyed by the group key regardless of whether it is a source key.
|
|
428
|
+
*/
|
|
429
|
+
export function buildAggregateResidualPlan(ctx, mv, analyzed, tableRef, sourceBase, aggregate) {
|
|
430
|
+
// A scalar aggregate (no GROUP BY) is one global row keyed by the empty key — no
|
|
431
|
+
// bounded-delta group binding, so it falls to the floor.
|
|
432
|
+
if (aggregate.groupBy.length === 0)
|
|
433
|
+
return null;
|
|
434
|
+
// Map T's output attributes to source column indices. T is a bare
|
|
435
|
+
// `TableReferenceNode`, so output-column index == source-column index.
|
|
436
|
+
const sourceAttrToCol = new Map();
|
|
437
|
+
tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
|
|
438
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
439
|
+
// Transitive provenance: chase output-attr → producing ColumnReference chains
|
|
440
|
+
// (Project-over-Aggregate adds a hop the single-hop `resolveSourceCol` cannot
|
|
441
|
+
// follow) until landing on a T source column, or `undefined`.
|
|
442
|
+
const resolveToSourceCol = (attrId) => resolveTransitiveSourceCol(attrId, sourceAttrToCol, producingByAttrId);
|
|
443
|
+
// Each GROUP BY expression must be a bare source column (a computed group key has
|
|
444
|
+
// no source-column index to bind / key the backing on) → otherwise the floor.
|
|
445
|
+
const groupColumns = [];
|
|
446
|
+
for (const expr of aggregate.groupBy) {
|
|
447
|
+
if (!(expr instanceof ColumnReferenceNode))
|
|
448
|
+
return null;
|
|
449
|
+
const sourceCol = sourceAttrToCol.get(expr.attributeId);
|
|
450
|
+
if (sourceCol === undefined)
|
|
451
|
+
return null;
|
|
452
|
+
groupColumns.push(sourceCol);
|
|
453
|
+
}
|
|
454
|
+
// Determinism: a residual must reproduce exactly what `select <body>` returns, so a
|
|
455
|
+
// volatile group/aggregate expression (random()/now()/volatile UDF) is a HARD reject.
|
|
456
|
+
for (const expr of aggregate.groupBy) {
|
|
457
|
+
const det = checkDeterministic(expr);
|
|
458
|
+
if (!det.valid)
|
|
459
|
+
throw cannotMaterialize(mv.name, `it groups by a non-deterministic expression (${det.expression})`);
|
|
460
|
+
}
|
|
461
|
+
for (const agg of aggregate.aggregates) {
|
|
462
|
+
const det = checkDeterministic(agg.expression);
|
|
463
|
+
if (!det.valid)
|
|
464
|
+
throw cannotMaterialize(mv.name, `it aggregates a non-deterministic expression (${det.expression})`);
|
|
465
|
+
}
|
|
466
|
+
// Backing table + its physical PK. The aggregate's group-key FD
|
|
467
|
+
// (`propagateAggregateFds`) makes the group key the backing key (via `keysOf`).
|
|
468
|
+
const backing = ctx._findTable(mv.name, mv.schemaName);
|
|
469
|
+
if (!backing) {
|
|
470
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
471
|
+
}
|
|
472
|
+
const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
|
|
473
|
+
// Map each backing-PK column back to the source group column it projects, so a
|
|
474
|
+
// changed row's old backing-slice delete key can be built. Every backing-PK column
|
|
475
|
+
// MUST resolve to a GROUP BY source column — else the backing key is not the group
|
|
476
|
+
// key and point-keyed delete+upsert would be unsound → fall to the floor.
|
|
477
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
478
|
+
if (!rootAttrs)
|
|
479
|
+
return null;
|
|
480
|
+
const groupColumnSet = new Set(groupColumns);
|
|
481
|
+
const backingPkSourceCols = [];
|
|
482
|
+
for (const d of backingPkDefinition) {
|
|
483
|
+
const attr = rootAttrs[d.index];
|
|
484
|
+
const sourceCol = attr ? resolveToSourceCol(attr.id) : undefined;
|
|
485
|
+
if (sourceCol === undefined || !groupColumnSet.has(sourceCol))
|
|
486
|
+
return null;
|
|
487
|
+
backingPkSourceCols.push(sourceCol);
|
|
488
|
+
}
|
|
489
|
+
// Compile + cache the group-keyed residual once (the body with `g1 = :gk0 AND …`
|
|
490
|
+
// injected on T). Re-run per affected group key against the live transaction.
|
|
491
|
+
const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
|
|
492
|
+
const residualScheduler = compileResidual(ctx, analyzed, relKey, groupColumns, 'gk');
|
|
493
|
+
if (!residualScheduler)
|
|
494
|
+
return null; // could not parameterize the residual → floor
|
|
495
|
+
// Delta-aggregate fast path: build the descriptor when EVERY stored aggregate
|
|
496
|
+
// column is delta-maintainable by its declared algebra (see the builder below).
|
|
497
|
+
// `undefined` on any gate failure — the arm then stays a plain residual, unchanged.
|
|
498
|
+
const delta = buildDeltaAggregateDescriptor(mv, analyzed, tableRef, groupColumns, backingPkDefinition, backingPkSourceCols, rootAttrs, sourceAttrToCol, producingByAttrId);
|
|
499
|
+
// ── Cost gate ──
|
|
500
|
+
// The residual is the structurally-sound incremental arm for an aggregate body;
|
|
501
|
+
// 'full-rebuild' is the always-correct floor for shapes where the residual is NOT
|
|
502
|
+
// sound, so (as with inverse-projection) it is not a competitor here. When the delta
|
|
503
|
+
// descriptor built, 'delta-aggregate' joins the sound set — an O(1)-per-change
|
|
504
|
+
// arithmetic RMW, strictly cheaper than the per-group residual under the cost model,
|
|
505
|
+
// so the argmin picks it. The descriptor is attached only when the gate chose it
|
|
506
|
+
// (cost-strategy decoupling: `kind` stays 'residual-recompute' either way).
|
|
507
|
+
const soundStrategies = delta
|
|
508
|
+
? ['delta-aggregate', 'residual-recompute']
|
|
509
|
+
: ['residual-recompute'];
|
|
510
|
+
const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
|
|
511
|
+
const sourceStats = estimateMaintenanceStats(ctx, tableRef.tableSchema, backing.columns.length, hasPredicate);
|
|
512
|
+
// A tighten-only (min/max) column re-derives a retracted group from the residual, so the
|
|
513
|
+
// delta arm's cost is no longer pure arithmetic — feed the create-time expected-rescan
|
|
514
|
+
// fraction into the `'delta-aggregate'` cost blend (maintenanceCost). A tighten body then
|
|
515
|
+
// costs strictly more than a pure-group body but, for this insert-dominated estimate, still
|
|
516
|
+
// below the always-residual arm, so the argmin keeps the delta arm.
|
|
517
|
+
if (delta?.hasTighten)
|
|
518
|
+
sourceStats.deltaTightenFallbackRatio = DELTA_TIGHTEN_FALLBACK_RATIO;
|
|
519
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
520
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
521
|
+
if (!soundStrategies.includes(chosenStrategy)) {
|
|
522
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
kind: 'residual-recompute',
|
|
526
|
+
mv,
|
|
527
|
+
sourceBase,
|
|
528
|
+
backingSchema: mv.schemaName,
|
|
529
|
+
backingTableName: mv.name,
|
|
530
|
+
chosenStrategy,
|
|
531
|
+
sourceStats,
|
|
532
|
+
binding: { kind: 'group', groupColumns: [...groupColumns] },
|
|
533
|
+
residualScheduler,
|
|
534
|
+
bindParamPrefix: 'gk',
|
|
535
|
+
bindColumns: groupColumns,
|
|
536
|
+
backingPkDefinition,
|
|
537
|
+
backingPkSourceCols,
|
|
538
|
+
delta: chosenStrategy === 'delta-aggregate' ? delta : undefined,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Build the {@link DeltaAggregateDescriptor} for a single-source aggregate body, or
|
|
543
|
+
* return `undefined` when any column fails the delta eligibility gate (the arm then
|
|
544
|
+
* stays a plain residual — a gate failure is never an error). Function-generic by
|
|
545
|
+
* construction: every judgment reads the aggregate call node's registry-resolved
|
|
546
|
+
* schema and its DECLARED {@link AggregateAlgebra} — never an aggregate-name list.
|
|
547
|
+
*
|
|
548
|
+
* The gate, per stored backing column:
|
|
549
|
+
* - a **group-key passthrough** column must resolve (transitively) to a GROUP BY
|
|
550
|
+
* source column that also backs a backing-PK position (so its flush value is the
|
|
551
|
+
* group key itself);
|
|
552
|
+
* - a directly **delta-maintainable aggregate** column must be a plain call (`agg(x)` /
|
|
553
|
+
* `agg()` — no DISTINCT, FILTER, ORDER BY, multi-arg) whose declared algebra has
|
|
554
|
+
* `merge` + `negate` + `decode`, with a **bare source column** argument (the delta
|
|
555
|
+
* steps the raw value);
|
|
556
|
+
* - **exact value domain**: the aggregate's declared return type is INTEGER-physical
|
|
557
|
+
* (count-shaped — exact regardless of argument), OR its argument column's static
|
|
558
|
+
* type is INTEGER-physical (integer sum). A REAL/NUMERIC/TEXT sum drifts under
|
|
559
|
+
* repeated add/subtract and would diverge byte-exactly from the live re-evaluation
|
|
560
|
+
* the oracle compares against — such a column disqualifies the whole MV (residual);
|
|
561
|
+
* - a **tighten-only** column (min/max, and any UDAF whose merge is a join-semilattice
|
|
562
|
+
* like `bit_or`/`bool_or`) — a plain call declaring `merge` + `decode` but NO `negate`
|
|
563
|
+
* — is admitted with `deltaClass: 'tighten'`. It needs no exact-value-domain gate
|
|
564
|
+
* (merge is idempotent selection, not accumulating arithmetic → no drift) and is never
|
|
565
|
+
* retraction-safe: an insert `merge`s the new value in cheaply, but a retracted group
|
|
566
|
+
* re-derives wholesale from the residual (via the descriptor's `hasTighten`) rather
|
|
567
|
+
* than trusting the arithmetic;
|
|
568
|
+
* - a **decomposition-maintained** column (the avg class) — a plain call whose algebra
|
|
569
|
+
* declares `decompose` (a scalar formula over sibling partials, e.g. avg ≡ sum/count)
|
|
570
|
+
* but is not itself directly maintainable — is admitted only when EVERY partial it
|
|
571
|
+
* names ({@link resolveDecomposePartial}) is also stored as a sibling column of this
|
|
572
|
+
* same body and is itself delta-maintainable. It carries no independent accumulation;
|
|
573
|
+
* at flush its value is `combine([finalized(partial) …])`. If any partial is missing
|
|
574
|
+
* or not delta-maintainable the whole MV falls to residual (avg is the first client of
|
|
575
|
+
* this class, not a special case);
|
|
576
|
+
* - any other column shape (a constant, an expression over the group key, …) fails.
|
|
577
|
+
*
|
|
578
|
+
* Whole-body requirements:
|
|
579
|
+
* - **no post-aggregate filter** (HAVING, or an outer WHERE over the aggregate
|
|
580
|
+
* output): such a filter breaks the arithmetic invariant "stored row == the
|
|
581
|
+
* group's full accumulator" — a filtered-out group has contributions but no
|
|
582
|
+
* stored row, so a later delta would rebuild it from the identity and understate
|
|
583
|
+
* the aggregate. The residual re-evaluates the filter per group and stays exact;
|
|
584
|
+
* - a **count(*) multiplicity witness** — a zero-arg delta-maintainable column with
|
|
585
|
+
* `decodeExact` (its stored value must reconstruct exactly, since it decides group
|
|
586
|
+
* emptiness under retraction). Without it, emptiness cannot be told from an
|
|
587
|
+
* all-NULL sum;
|
|
588
|
+
* - **BINARY collations** on every backing-PK (group key) column: the flush's stored-
|
|
589
|
+
* row read is the host's `scanEffective` equality-prefix seek, whose prefix compare
|
|
590
|
+
* is binary (scan-layer.ts). Unlike the prefix-delete arm there is no source-PK
|
|
591
|
+
* uniqueness collapsing collation classes to one byte form here, so a non-BINARY
|
|
592
|
+
* group key could miss a collation-equal / byte-different stored row — residual
|
|
593
|
+
* instead;
|
|
594
|
+
* - a body WHERE (if any) must compile to a single-source-row predicate
|
|
595
|
+
* (`compilePredicate`) so per-row contributions can be scoped exactly.
|
|
596
|
+
*
|
|
597
|
+
* Per-column `retractionSafe` (see {@link DeltaAggregateColumn}) is recorded rather
|
|
598
|
+
* than gated: a not-retraction-safe descriptor still serves the bulk-insert fast path,
|
|
599
|
+
* and the flush re-derives only the retracted groups through the residual.
|
|
600
|
+
*/
|
|
601
|
+
export function buildDeltaAggregateDescriptor(mv, analyzed, tableRef, groupColumns, backingPkDefinition, backingPkSourceCols, rootAttrs, sourceAttrToCol, producingByAttrId) {
|
|
602
|
+
const sourceSchema = tableRef.tableSchema;
|
|
603
|
+
const groupColumnSet = new Set(groupColumns);
|
|
604
|
+
// Post-aggregate filter gate: a Filter whose subtree contains the aggregate is a
|
|
605
|
+
// HAVING (or an outer WHERE over the aggregate output). It breaks the arithmetic
|
|
606
|
+
// invariant "stored row == the group's full accumulator" — a filtered-out group has
|
|
607
|
+
// contributions but no stored row, so a later delta would rebuild it from the
|
|
608
|
+
// identity and understate the aggregate. (The body WHERE sits BELOW the aggregate —
|
|
609
|
+
// its Filter subtree contains no aggregate node — and stays delta-eligible via the
|
|
610
|
+
// compiled scope predicate below.)
|
|
611
|
+
if (hasFilterAboveAggregate(analyzed))
|
|
612
|
+
return undefined;
|
|
613
|
+
// Binary-collation gate on the backing PK (the flush's binary equality-prefix read).
|
|
614
|
+
for (const d of backingPkDefinition) {
|
|
615
|
+
if (!isBinaryCollation(d.collation))
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
// Body WHERE → single-row scope predicate (contributions outside it are dropped,
|
|
619
|
+
// mirroring the residual's own WHERE). An uncompilable WHERE shape → residual.
|
|
620
|
+
let predicate;
|
|
621
|
+
const bodyWhere = mv.derivation.selectAst.type === 'select' ? mv.derivation.selectAst.where : undefined;
|
|
622
|
+
if (bodyWhere) {
|
|
623
|
+
try {
|
|
624
|
+
predicate = compilePredicate(bodyWhere, sourceSchema.columns);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Chase an output attr's producing chain to its terminal (non-ColumnReference)
|
|
631
|
+
// expression — the aggregate call node for an aggregate output column.
|
|
632
|
+
const resolveTerminalProducing = (attrId) => {
|
|
633
|
+
const seen = new Set();
|
|
634
|
+
let cur = attrId;
|
|
635
|
+
while (!seen.has(cur)) {
|
|
636
|
+
seen.add(cur);
|
|
637
|
+
const expr = producingByAttrId.get(cur);
|
|
638
|
+
if (expr instanceof ColumnReferenceNode) {
|
|
639
|
+
cur = expr.attributeId;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
return expr;
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
};
|
|
646
|
+
const aggColumns = [];
|
|
647
|
+
const groupOut = [];
|
|
648
|
+
// Decomposition-maintained columns (the avg class), collected raw here and bound to
|
|
649
|
+
// their sibling stored partials in a second pass below — the partials may project
|
|
650
|
+
// before OR after the decompose column, so they cannot be resolved inline.
|
|
651
|
+
const pendingDecompose = [];
|
|
652
|
+
for (let outCol = 0; outCol < rootAttrs.length; outCol++) {
|
|
653
|
+
const attr = rootAttrs[outCol];
|
|
654
|
+
if (!attr)
|
|
655
|
+
return undefined;
|
|
656
|
+
const sourceCol = resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId);
|
|
657
|
+
if (sourceCol !== undefined) {
|
|
658
|
+
// Group-key passthrough: its flush value is the group key at the matching
|
|
659
|
+
// backing-PK position (covers duplicate projections of a group column).
|
|
660
|
+
if (!groupColumnSet.has(sourceCol))
|
|
661
|
+
return undefined;
|
|
662
|
+
const pkPos = backingPkSourceCols.indexOf(sourceCol);
|
|
663
|
+
if (pkPos < 0)
|
|
664
|
+
return undefined;
|
|
665
|
+
groupOut.push({ backingCol: outCol, pkPos });
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const producing = resolveTerminalProducing(attr.id);
|
|
669
|
+
if (!(producing instanceof AggregateFunctionCallNode))
|
|
670
|
+
return undefined;
|
|
671
|
+
// Plain call only: DISTINCT folds duplicates (delta cannot), FILTER/ORDER BY
|
|
672
|
+
// change the contribution set, multi-arg has no single raw value to step.
|
|
673
|
+
if (producing.isDistinct || producing.filter || (producing.orderBy && producing.orderBy.length > 0))
|
|
674
|
+
return undefined;
|
|
675
|
+
if (producing.args.length > 1)
|
|
676
|
+
return undefined;
|
|
677
|
+
const schema = producing.functionSchema;
|
|
678
|
+
if (!isAggregateFunctionSchema(schema))
|
|
679
|
+
return undefined;
|
|
680
|
+
const algebra = schema.algebra;
|
|
681
|
+
// A bare source column argument (both classes below step / decompose over the raw
|
|
682
|
+
// value); a zero-arg (count(*)-shaped) call carries none.
|
|
683
|
+
let argSourceCol;
|
|
684
|
+
if (producing.args.length === 1) {
|
|
685
|
+
const arg = producing.args[0];
|
|
686
|
+
if (!(arg instanceof ColumnReferenceNode))
|
|
687
|
+
return undefined; // bare column args only
|
|
688
|
+
argSourceCol = resolveTransitiveSourceCol(arg.attributeId, sourceAttrToCol, producingByAttrId);
|
|
689
|
+
if (argSourceCol === undefined)
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
// Class routing — declaration-driven, never an aggregate-name list:
|
|
693
|
+
// - **directly delta-maintainable** (`merge` + `negate` (retraction) + `decode` —
|
|
694
|
+
// the backing stores finalized values, not accumulators): accumulate it as an
|
|
695
|
+
// ordinary partial;
|
|
696
|
+
// - else a **decomposition-maintained** column whose value is a formula over
|
|
697
|
+
// sibling partials (`algebra.decompose`, e.g. avg ≡ sum/count) — deferred to the
|
|
698
|
+
// pass below that binds each partial to a stored sibling; it accumulates nothing;
|
|
699
|
+
// - anything else disqualifies the whole MV → residual.
|
|
700
|
+
if (algebra?.negate && algebra.decode) {
|
|
701
|
+
// Exact numeric domain: an INTEGER-physical declared result (count-shaped — the
|
|
702
|
+
// accumulator never holds a float) or an INTEGER-physical argument column
|
|
703
|
+
// (integer sum). Anything else may drift under repeated add/subtract.
|
|
704
|
+
// NOTE: if a REAL-domain sum ever needs the fast path, it needs compensated
|
|
705
|
+
// (Kahan) accumulation or a periodic-rescan discipline — a design change, not a
|
|
706
|
+
// relaxation of this gate.
|
|
707
|
+
const resultExact = schema.returnType.logicalType.physicalType === PhysicalType.INTEGER;
|
|
708
|
+
const argExact = argSourceCol !== undefined
|
|
709
|
+
&& sourceSchema.columns[argSourceCol]?.logicalType.physicalType === PhysicalType.INTEGER;
|
|
710
|
+
if (!resultExact && !argExact)
|
|
711
|
+
return undefined;
|
|
712
|
+
const retractionSafe = algebra.decodeExact === true
|
|
713
|
+
|| (argSourceCol !== undefined && sourceSchema.columns[argSourceCol]?.notNull === true);
|
|
714
|
+
aggColumns.push({
|
|
715
|
+
backingCol: outCol,
|
|
716
|
+
schema,
|
|
717
|
+
algebra,
|
|
718
|
+
deltaClass: 'group',
|
|
719
|
+
argSourceCol,
|
|
720
|
+
isMultiplicity: producing.args.length === 0,
|
|
721
|
+
retractionSafe,
|
|
722
|
+
});
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
// Tighten-only (join-semilattice): `merge` + `decode` but NO `negate` — min/max, and
|
|
726
|
+
// any UDAF whose merge is idempotent selection (bit_or/bool_or). An insert `merge`s the
|
|
727
|
+
// new value in cheaply (min/max tighten toward the new extreme); a retraction cannot be
|
|
728
|
+
// undone arithmetically, so a retracted group falls back to the residual
|
|
729
|
+
// (retractionSafe:false, and the descriptor's hasTighten routes the whole group there
|
|
730
|
+
// whether or not a row is stored). NO exact-numeric-domain gate: merge is idempotent
|
|
731
|
+
// selection, not accumulating arithmetic, so there is no float drift to guard — a tighten
|
|
732
|
+
// column over any comparable domain (REAL/TEXT included) stays byte-exact vs the live
|
|
733
|
+
// re-evaluation. A negate+decode column that failed the exact-domain gate above already
|
|
734
|
+
// returned undefined, so only a decode-without-negate aggregate reaches here.
|
|
735
|
+
if (algebra?.decode && !algebra.negate) {
|
|
736
|
+
aggColumns.push({
|
|
737
|
+
backingCol: outCol,
|
|
738
|
+
schema,
|
|
739
|
+
algebra,
|
|
740
|
+
deltaClass: 'tighten',
|
|
741
|
+
argSourceCol,
|
|
742
|
+
isMultiplicity: producing.args.length === 0,
|
|
743
|
+
retractionSafe: false,
|
|
744
|
+
});
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (algebra?.decompose) {
|
|
748
|
+
pendingDecompose.push({ backingCol: outCol, argSourceCol, decomposition: algebra.decompose });
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
return undefined; // neither directly maintainable, tighten-only, nor decomposable → residual
|
|
752
|
+
}
|
|
753
|
+
// Multiplicity witness: a zero-arg delta-maintainable column whose decode is exact
|
|
754
|
+
// (its merged value decides group emptiness, so the reconstruction must survive
|
|
755
|
+
// retraction). Structural — not name-based. A decompose column is never the witness
|
|
756
|
+
// (it accumulates nothing — the requirement is a real stored count(*)).
|
|
757
|
+
const multiplicityIndex = aggColumns.findIndex(c => c.isMultiplicity && c.algebra.decodeExact === true);
|
|
758
|
+
if (multiplicityIndex < 0)
|
|
759
|
+
return undefined;
|
|
760
|
+
if (aggColumns.length === 0)
|
|
761
|
+
return undefined; // defensive — findAggregate implies ≥1
|
|
762
|
+
// Bind each decomposition column's partials to sibling STORED, delta-maintained columns
|
|
763
|
+
// (every `aggColumns` entry already passed the accumulator gate, so it is itself delta-
|
|
764
|
+
// maintainable). A partial that maps to no such sibling disqualifies the whole MV →
|
|
765
|
+
// residual (honest and visible: the user wrote a body without the partials the formula
|
|
766
|
+
// names). The decompose column's `combine` recomputes exactly per group at flush.
|
|
767
|
+
const decomposeColumns = [];
|
|
768
|
+
for (const pd of pendingDecompose) {
|
|
769
|
+
const partialIndices = [];
|
|
770
|
+
for (const p of pd.decomposition.partials) {
|
|
771
|
+
const idx = resolveDecomposePartial(p, pd.argSourceCol, aggColumns, sourceSchema);
|
|
772
|
+
if (idx === undefined)
|
|
773
|
+
return undefined;
|
|
774
|
+
partialIndices.push(idx);
|
|
775
|
+
}
|
|
776
|
+
decomposeColumns.push({ backingCol: pd.backingCol, partialIndices, combine: pd.decomposition.combine });
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
aggColumns,
|
|
780
|
+
decomposeColumns,
|
|
781
|
+
groupColumns: groupOut,
|
|
782
|
+
multiplicityIndex,
|
|
783
|
+
backingColumnCount: rootAttrs.length,
|
|
784
|
+
retractionSafe: aggColumns.every(c => c.retractionSafe),
|
|
785
|
+
hasTighten: aggColumns.some(c => c.deltaClass === 'tighten'),
|
|
786
|
+
predicate,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Resolve one {@link AggregateDecomposition} partial (e.g. avg's `sum` / `count`) to the
|
|
791
|
+
* index of a sibling stored, delta-maintainable aggregate column — an `aggColumns` entry
|
|
792
|
+
* matching the partial's function name and argument. Returns `undefined` when no such
|
|
793
|
+
* sibling is stored (the decompose column is then not maintainable → the MV falls to the
|
|
794
|
+
* residual). The write-side twin of `query-rewrite-matcher.resolveMergeablePartial`.
|
|
795
|
+
*
|
|
796
|
+
* `'same-arg'` targets a sibling over the decompose column's own argument (`f(thisArg)`);
|
|
797
|
+
* `'star'` targets a zero-arg (`count(*)`-shaped) sibling. The **count(*) fallback** mirrors
|
|
798
|
+
* the read-side relaxation from `feat-mv-agg-rollup-retarget`: a stored `count(*)` substitutes
|
|
799
|
+
* for a `count(x)` divisor only when `x` is declared **NOT NULL** — then `count(*)` excludes
|
|
800
|
+
* the same (zero) NULLs `count(x)` would, so the recombine stays exact.
|
|
801
|
+
*/
|
|
802
|
+
function resolveDecomposePartial(p, ownerArgSourceCol, aggColumns, sourceSchema) {
|
|
803
|
+
// NOTE: a partial resolves only to an `aggColumns` entry, which already passed the
|
|
804
|
+
// accumulator gate — a BARE source column argument plus the INTEGER-exact-domain check.
|
|
805
|
+
// That is what keeps the decomposed value byte-exact: a partial like `sum(log x)` (a
|
|
806
|
+
// non-bare, float-producing argument) can never be a stored delta-maintainable sibling,
|
|
807
|
+
// so a true geometric mean is NOT delta-decomposable and correctly falls to the residual.
|
|
808
|
+
const targetArg = p.arg === 'star' ? undefined : ownerArgSourceCol;
|
|
809
|
+
const funcName = p.func.toLowerCase();
|
|
810
|
+
const find = (name, argCol) => aggColumns.findIndex(c => c.schema.name.toLowerCase() === name && c.argSourceCol === argCol);
|
|
811
|
+
let idx = find(funcName, targetArg);
|
|
812
|
+
if (idx < 0 && funcName === 'count' && targetArg !== undefined
|
|
813
|
+
&& sourceSchema.columns[targetArg]?.notNull === true) {
|
|
814
|
+
idx = find('count', undefined); // count(*) qualifies as the count(x) divisor when x is NOT NULL
|
|
815
|
+
}
|
|
816
|
+
return idx < 0 ? undefined : idx;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* True when any Filter node's subtree contains an aggregate node — i.e. the plan
|
|
820
|
+
* filters the AGGREGATE OUTPUT (HAVING, or an outer WHERE over a grouped subquery).
|
|
821
|
+
* The body WHERE is the complement case: its Filter sits below the aggregate, so its
|
|
822
|
+
* subtree contains none. Used by {@link buildDeltaAggregateDescriptor}'s gate.
|
|
823
|
+
*/
|
|
824
|
+
function hasFilterAboveAggregate(node) {
|
|
825
|
+
if (node.nodeType === PlanNodeType.Filter && findAggregate(node) !== undefined)
|
|
826
|
+
return true;
|
|
827
|
+
for (const child of node.getChildren()) {
|
|
828
|
+
if (hasFilterAboveAggregate(child))
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Build a `'join-residual'` plan for a provably-1:1 row-preserving **inner/cross join**
|
|
835
|
+
* body (`select … from T join P on T.fk = P.id`), or return `null` on a shape mismatch
|
|
836
|
+
* (the caller falls through to the full-rebuild floor). The driving table `T` is the one
|
|
837
|
+
* whose PK the optimizer surfaced as the backing key (the 1:1 join collapses the composite
|
|
838
|
+
* product key to `T`'s PK); the other base ref is the lookup `P`. See {@link JoinResidualPlan}
|
|
839
|
+
* and `docs/incremental-maintenance.md` § join-residual.
|
|
840
|
+
*
|
|
841
|
+
* Soundness gates (a mismatch on any returns `null` → floor): exactly two base tables; no
|
|
842
|
+
* aggregate over the join; the backing PK is exactly `T`'s PK projected as passthrough
|
|
843
|
+
* columns (so each changed `T` row maps to one backing row and the reverse residual's rows
|
|
844
|
+
* carry the backing key); the join is provably 1:1 on `T` ({@link proveOneToOneJoin} — no
|
|
845
|
+
* row loss via NOT-NULL FK→PK RI, no fan-out via the join-frame `isUnique(T.pk)`); and the
|
|
846
|
+
* join is **inner/cross** (an outer join would make the lookup-side reverse residual unsound
|
|
847
|
+
* — filtering `P` drops the null-extended rows). A **non-deterministic** projection is the
|
|
848
|
+
* one hard reject (its arm-specific determinism diagnostic must survive).
|
|
849
|
+
*
|
|
850
|
+
* **A body WHERE is now accepted** (it is no longer a blanket reject): the predicate is
|
|
851
|
+
* classified by which base table(s) its columns reference (reusing the per-base-ref
|
|
852
|
+
* attribute→source-column maps below). A `T`-only predicate needs nothing extra — the
|
|
853
|
+
* forward residual already carries it and the membership set `{ T : T.fk = P.pk }` cannot
|
|
854
|
+
* move on a `P` write, so the lookup side stays upsert-only. A predicate referencing `P` (or
|
|
855
|
+
* both sides) switches the lookup side to a **delete-capable** reverse residual by building
|
|
856
|
+
* `lookupMembershipResidualScheduler` (the body with the WHERE stripped, keyed on `P`). See
|
|
857
|
+
* {@link JoinResidualPlan}'s "WHERE handling" note and {@link applyLookupResidual}.
|
|
858
|
+
*/
|
|
859
|
+
export function buildJoinResidualPlan(ctx, mv, analyzed, tableRefs) {
|
|
860
|
+
// A >2-source join or an aggregate over the join has no join-residual binding → floor.
|
|
861
|
+
// A body WHERE is no longer rejected here — it is classified (T-only vs P-referencing)
|
|
862
|
+
// below, after `T`/`P` are identified, and routed to the matching lookup-side strategy.
|
|
863
|
+
if (tableRefs.length !== 2)
|
|
864
|
+
return null;
|
|
865
|
+
if (findAggregate(analyzed))
|
|
866
|
+
return null;
|
|
867
|
+
const backing = ctx._findTable(mv.name, mv.schemaName);
|
|
868
|
+
if (!backing) {
|
|
869
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
870
|
+
}
|
|
871
|
+
const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
|
|
872
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
873
|
+
if (!rootAttrs)
|
|
874
|
+
return null;
|
|
875
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
876
|
+
// Per-base-ref attribute → source-column maps. `T` and `P` are bare
|
|
877
|
+
// TableReferenceNodes (output-col index == source-col index).
|
|
878
|
+
const refInfos = tableRefs.map(ref => {
|
|
879
|
+
const attrToCol = new Map();
|
|
880
|
+
ref.getAttributes().forEach((a, i) => attrToCol.set(a.id, i));
|
|
881
|
+
return { ref, attrToCol };
|
|
882
|
+
});
|
|
883
|
+
// Identify the driving table `T` as the one every backing-PK column resolves to, and
|
|
884
|
+
// map each backing-PK column to its `T` source column (the delete key the forward arm
|
|
885
|
+
// builds). A backing-PK column resolving to neither ref — or columns spanning both —
|
|
886
|
+
// means the backing is not keyed on a single source's PK (the join is not provably
|
|
887
|
+
// 1:1, or `keysOf` fell back to all-columns): reject.
|
|
888
|
+
let tIndex;
|
|
889
|
+
const backingPkSourceCols = [];
|
|
890
|
+
for (const d of backingPkDefinition) {
|
|
891
|
+
const attr = rootAttrs[d.index];
|
|
892
|
+
let resolvedRef;
|
|
893
|
+
let resolvedCol;
|
|
894
|
+
for (let i = 0; i < refInfos.length; i++) {
|
|
895
|
+
const sc = attr ? resolveTransitiveSourceCol(attr.id, refInfos[i].attrToCol, producingByAttrId) : undefined;
|
|
896
|
+
if (sc !== undefined) {
|
|
897
|
+
resolvedRef = i;
|
|
898
|
+
resolvedCol = sc;
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// A backing-PK column resolving to neither ref, or columns spanning both, means the
|
|
903
|
+
// backing is not keyed on a single source's PK (not provably 1:1) → fall to the floor.
|
|
904
|
+
if (resolvedRef === undefined)
|
|
905
|
+
return null;
|
|
906
|
+
if (tIndex === undefined)
|
|
907
|
+
tIndex = resolvedRef;
|
|
908
|
+
else if (tIndex !== resolvedRef)
|
|
909
|
+
return null;
|
|
910
|
+
backingPkSourceCols.push(resolvedCol);
|
|
911
|
+
}
|
|
912
|
+
if (tIndex === undefined)
|
|
913
|
+
return null;
|
|
914
|
+
const tRef = refInfos[tIndex].ref;
|
|
915
|
+
const pRef = refInfos[tIndex === 0 ? 1 : 0].ref;
|
|
916
|
+
const tSchema = tRef.tableSchema;
|
|
917
|
+
const pSchema = pRef.tableSchema;
|
|
918
|
+
const sourceBase = `${tSchema.schemaName}.${tSchema.name}`.toLowerCase();
|
|
919
|
+
const lookupBase = `${pSchema.schemaName}.${pSchema.name}`.toLowerCase();
|
|
920
|
+
if (sourceBase === lookupBase)
|
|
921
|
+
return null; // self-join → floor
|
|
922
|
+
// The backing key must be EXACTLY the driving source's PK (each `T` row → one backing
|
|
923
|
+
// row). `keysOf` surfaced `T.pk` for the 1:1 join; verify it resolved to a real PK key
|
|
924
|
+
// (not the all-columns fallback) by set-equality with `T`'s declared PK.
|
|
925
|
+
const tPkCols = tSchema.primaryKeyDefinition.map(d => d.index);
|
|
926
|
+
if (tPkCols.length === 0)
|
|
927
|
+
return null;
|
|
928
|
+
const backingPkSet = new Set(backingPkSourceCols);
|
|
929
|
+
if (backingPkSet.size !== tPkCols.length || !tPkCols.every(c => backingPkSet.has(c)))
|
|
930
|
+
return null;
|
|
931
|
+
// Prove the join is 1:1 on `T` (no row loss + no fan-out), reusing the coverage
|
|
932
|
+
// prover's shared join predicates over the analyzed body. A fanning / non-1:1 join
|
|
933
|
+
// falls to the floor.
|
|
934
|
+
const root = rootRelationalNode(analyzed);
|
|
935
|
+
if (!root)
|
|
936
|
+
return null;
|
|
937
|
+
const proof = proveOneToOneJoin(root, tSchema);
|
|
938
|
+
if (!proof.ok)
|
|
939
|
+
return null;
|
|
940
|
+
// Restrict to inner/cross: the lookup-side reverse residual filters `P`, which would
|
|
941
|
+
// drop an outer join's null-extended rows (unsound). An outer 1:1 join falls to the floor.
|
|
942
|
+
const topJoin = proof.topJoin;
|
|
943
|
+
const joinType = topJoin && CapabilityDetectors.isJoin(topJoin) ? topJoin.getJoinType() : undefined;
|
|
944
|
+
if (joinType !== 'inner' && joinType !== 'cross')
|
|
945
|
+
return null;
|
|
946
|
+
// Determinism: the residual must reproduce exactly what `select <body>` returns, so a
|
|
947
|
+
// volatile projection (random()/now()/volatile UDF) is a HARD reject.
|
|
948
|
+
for (const attr of rootAttrs) {
|
|
949
|
+
const producing = attr ? producingByAttrId.get(attr.id) : undefined;
|
|
950
|
+
if (!producing)
|
|
951
|
+
continue; // a bare passthrough column has no producing expr to check
|
|
952
|
+
const det = checkDeterministic(producing);
|
|
953
|
+
if (!det.valid) {
|
|
954
|
+
throw cannotMaterialize(mv.name, `it projects a non-deterministic expression (${det.expression}); a row-time backing value must be reproducible from the source rows`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
// Forward (`T`) residual: the body with `T.pk = :pk0 AND …` injected on `T`. Recomputes
|
|
958
|
+
// the one joined row for a changed `T` row (delegated to `applyForwardResidual`).
|
|
959
|
+
const tRelKey = `${sourceBase}#${tRef.id ?? 'unknown'}`;
|
|
960
|
+
const forwardResidual = compileResidual(ctx, analyzed, tRelKey, tPkCols, 'pk');
|
|
961
|
+
if (!forwardResidual)
|
|
962
|
+
return null;
|
|
963
|
+
// Reverse (`P`) **in-scope** residual: the body — WHERE retained — with `P.pk = :pk0 AND …`
|
|
964
|
+
// injected on `P`. Drives lookup-side maintenance — finds every currently in-scope joined
|
|
965
|
+
// row referencing a changed `P` row.
|
|
966
|
+
const pPkCols = pSchema.primaryKeyDefinition.map(d => d.index);
|
|
967
|
+
if (pPkCols.length === 0)
|
|
968
|
+
return null;
|
|
969
|
+
const pRelKey = `${lookupBase}#${pRef.id ?? 'unknown'}`;
|
|
970
|
+
const reverseResidual = compileResidual(ctx, analyzed, pRelKey, pPkCols, 'pk');
|
|
971
|
+
if (!reverseResidual)
|
|
972
|
+
return null;
|
|
973
|
+
// Classify the body WHERE by which base table(s) its columns reference: a predicate that
|
|
974
|
+
// references `P` (or both sides) can flip a row's WHERE membership on a `P` write, so the
|
|
975
|
+
// lookup side must become delete-capable; a `T`-only predicate cannot move membership, so
|
|
976
|
+
// the upsert-only reverse residual above stays sound. The membership residual is the body
|
|
977
|
+
// with the WHERE **stripped** and the key filter on `P` — it returns every currently
|
|
978
|
+
// referencing `T` row (regardless of scope) so its backing key can be deleted before the
|
|
979
|
+
// in-scope survivors are re-upserted. Absent for a no-WHERE / `T`-only-WHERE body.
|
|
980
|
+
const hasWhere = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
|
|
981
|
+
// A volatile WHERE would make every residual (which embeds it) irreproducible → fall to
|
|
982
|
+
// the floor's pragma-gated whole-body determinism reject, not an unsound bounded-delta arm.
|
|
983
|
+
if (hasWhere && bodyWhereIsNonDeterministic(analyzed))
|
|
984
|
+
return null;
|
|
985
|
+
const whereReferencesLookup = hasWhere
|
|
986
|
+
&& bodyWhereReferencesLookup(analyzed, refInfos[tIndex].attrToCol, producingByAttrId);
|
|
987
|
+
let lookupMembershipResidual;
|
|
988
|
+
if (whereReferencesLookup) {
|
|
989
|
+
const membership = compileLookupMembershipResidual(ctx, mv, lookupBase, pPkCols);
|
|
990
|
+
if (!membership)
|
|
991
|
+
return null; // could not strip + re-key the membership residual → floor
|
|
992
|
+
lookupMembershipResidual = membership;
|
|
993
|
+
}
|
|
994
|
+
// ── Cost gate (parity with the other residual arms) ──
|
|
995
|
+
const soundStrategies = ['residual-recompute'];
|
|
996
|
+
const sourceStats = estimateMaintenanceStats(ctx, tSchema, backing.columns.length, hasWhere);
|
|
997
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
998
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
999
|
+
if (chosenStrategy !== 'residual-recompute') {
|
|
1000
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
kind: 'join-residual',
|
|
1004
|
+
mv,
|
|
1005
|
+
sourceBase,
|
|
1006
|
+
backingSchema: mv.schemaName,
|
|
1007
|
+
backingTableName: mv.name,
|
|
1008
|
+
chosenStrategy,
|
|
1009
|
+
sourceStats,
|
|
1010
|
+
binding: { kind: 'row', keyColumns: [...tPkCols] },
|
|
1011
|
+
residualScheduler: forwardResidual,
|
|
1012
|
+
bindParamPrefix: 'pk',
|
|
1013
|
+
bindColumns: tPkCols,
|
|
1014
|
+
backingPkDefinition,
|
|
1015
|
+
backingPkSourceCols,
|
|
1016
|
+
lookupBase,
|
|
1017
|
+
lookupResidualScheduler: reverseResidual,
|
|
1018
|
+
lookupMembershipResidualScheduler: lookupMembershipResidual,
|
|
1019
|
+
lookupBindColumns: pPkCols,
|
|
1020
|
+
lookupBindParamPrefix: 'pk',
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Compile the **lookup membership** residual for the join-residual arm's delete-capable
|
|
1025
|
+
* lookup side: the MV body with its top-level WHERE **stripped** (membership only) and a
|
|
1026
|
+
* key-equality filter injected on the lookup `P`, keyed `pk0…`. The WHERE is stripped at the
|
|
1027
|
+
* AST level (a shallow clone dropping `where`) and the body re-built + re-analyzed, so only
|
|
1028
|
+
* the WHERE is removed — the join, its `ON` condition, and any projection sub-expressions are
|
|
1029
|
+
* preserved. Re-analysis assigns fresh node ids, so `P`'s reference is re-located by base name
|
|
1030
|
+
* to compute the injection target. Returns `null` if the lookup ref or the key-filter
|
|
1031
|
+
* injection could not be resolved (the caller then falls to the full-rebuild floor).
|
|
1032
|
+
*
|
|
1033
|
+
* Run per affected `P` key, this residual returns **every** `T` row currently joined to `P`
|
|
1034
|
+
* via the join's `ON` condition — irrespective of the WHERE — so {@link applyLookupResidual}
|
|
1035
|
+
* can delete each one's `T.pk` backing key before the in-scope residual re-upserts the
|
|
1036
|
+
* survivors (the membership set the WHERE-bearing reverse residual would otherwise never
|
|
1037
|
+
* shrink).
|
|
1038
|
+
*/
|
|
1039
|
+
export function compileLookupMembershipResidual(ctx, mv, lookupBase, pPkCols) {
|
|
1040
|
+
const db = ctx;
|
|
1041
|
+
const strippedAst = { ...mv.derivation.selectAst, where: undefined };
|
|
1042
|
+
const stripped = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
|
|
1043
|
+
const { plan } = ctx._buildPlan([strippedAst]);
|
|
1044
|
+
return ctx.optimizer.optimizeForAnalysis(plan, db);
|
|
1045
|
+
});
|
|
1046
|
+
// Re-locate `P` in the WHERE-stripped plan by base name (fresh node ids) to build the
|
|
1047
|
+
// injection target key the way `compileResidual`'s callers do.
|
|
1048
|
+
let pRelKey;
|
|
1049
|
+
for (const [relKey, ref] of collectTableRefs(stripped)) {
|
|
1050
|
+
if (`${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase() === lookupBase) {
|
|
1051
|
+
pRelKey = relKey;
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (pRelKey === undefined)
|
|
1056
|
+
return null;
|
|
1057
|
+
return compileResidual(ctx, stripped, pRelKey, pPkCols, 'pk');
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Build a `'full-rebuild'` plan — the always-correct floor — for an MV whose body matches
|
|
1061
|
+
* no bounded-delta arm, or throw with a non-shape diagnostic. This is the fall-through
|
|
1062
|
+
* builder {@link tryBuildBoundedDeltaArm} routes to on a `null` (no bounded-delta arm fits).
|
|
1063
|
+
* See {@link FullRebuildPlan} and `docs/mv-maintenance.md` § Full-rebuild floor /
|
|
1064
|
+
* § Primary key inference.
|
|
1065
|
+
*
|
|
1066
|
+
* Create-time rejections (none shape-based — the floor accepts general bodies):
|
|
1067
|
+
* - **bag** body with no provable unique key (`keysOf` over the optimized body root is
|
|
1068
|
+
* empty) — there is no row identity to materialize on. `keysOf` already gates its
|
|
1069
|
+
* all-columns fallback on `isSet`, so a non-empty result is a real key (a true column
|
|
1070
|
+
* key OR the all-columns key of a provable set) and an empty result is exactly a bag.
|
|
1071
|
+
* (A duplicate-producing body usually fails the set contract earlier, at create-fill.)
|
|
1072
|
+
* - **non-deterministic** body (any `random()`/`now()`/volatile UDF anywhere in the plan)
|
|
1073
|
+
* without `pragma nondeterministic_schema` — no maintenance could keep it equal to its
|
|
1074
|
+
* plain view (mirrors the per-arm determinism rejects and the DDL determinism gate);
|
|
1075
|
+
* - body with **no relational output** (degenerate);
|
|
1076
|
+
* - **size**: full-rebuild is the only sound strategy *and* the **largest** participating
|
|
1077
|
+
* source exceeds the `materialized_view_rebuild_row_threshold` option
|
|
1078
|
+
* ({@link isFullRebuildPathological}) — every DML write would re-scan that source.
|
|
1079
|
+
* `0` disables the size reject (accept any size).
|
|
1080
|
+
*/
|
|
1081
|
+
export function buildFullRebuildPlan(ctx, mv, analyzed) {
|
|
1082
|
+
const db = ctx;
|
|
1083
|
+
// Optimize the whole body ONCE — read-side MV rewrite suppressed so it reads its
|
|
1084
|
+
// sources, not the backing it populates — then derive the body's key + determinism
|
|
1085
|
+
// from, and compile its scheduler from, the SAME optimized plan.
|
|
1086
|
+
const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => ctx.optimizer.optimize(analyzed, db));
|
|
1087
|
+
const root = rootRelationalNode(optimized);
|
|
1088
|
+
if (!root)
|
|
1089
|
+
throw cannotMaterialize(mv.name, 'its body produced no relational output');
|
|
1090
|
+
// Backing key = the body's provable unique key. A bag (no provable key — a key-dropping
|
|
1091
|
+
// projection, a `union all` of overlapping inputs, …) has no row identity to key a
|
|
1092
|
+
// materialization on, so it must be a set. An all-columns pseudo-key is admitted only
|
|
1093
|
+
// when the body is provably a set (`keysOf` gates it on `isSet`); a bag with an
|
|
1094
|
+
// all-columns "key" still resolves to empty here and rejects, else duplicates would
|
|
1095
|
+
// collide on the backing key.
|
|
1096
|
+
//
|
|
1097
|
+
// One carve-out: a keyless body whose source key survives through value-preserving
|
|
1098
|
+
// passthrough lineage (the parallel-migration collation-weakening shape) is keyed on
|
|
1099
|
+
// the COARSENED lineage key instead of rejected — the same `deriveCoarsenedBackingKey`
|
|
1100
|
+
// derivation `deriveBackingShape` keyed the backing with, over the same fully-optimized
|
|
1101
|
+
// body, so the two agree by construction. Colliding rows then last-write-win under the
|
|
1102
|
+
// floor's collation-keyed `replace-all` diff (docs/materialized-views.md § Coarsened
|
|
1103
|
+
// backing keys); the create emitter owns the key-coarsening warning.
|
|
1104
|
+
if (keysOf(root).length === 0 && deriveCoarsenedBackingKey(root) === undefined) {
|
|
1105
|
+
throw cannotMaterialize(mv.name, 'its body has no provable unique key — it is a bag (e.g. a key-dropping '
|
|
1106
|
+
+ 'projection or a `union all` of overlapping inputs), so it must be a set');
|
|
1107
|
+
}
|
|
1108
|
+
// Whole-body determinism: a non-deterministic body can never be kept equal to its
|
|
1109
|
+
// plain view by any maintenance, so it is a hard reject unless the schema-determinism
|
|
1110
|
+
// gate is lifted. Mirrors the per-arm determinism rejects (and the DDL gate).
|
|
1111
|
+
if (!db.options.getBooleanOption('nondeterministic_schema')) {
|
|
1112
|
+
const nonDet = findNonDeterministic(analyzed);
|
|
1113
|
+
if (nonDet) {
|
|
1114
|
+
throw cannotMaterialize(mv.name, `its body is non-deterministic (${nonDet}); a materialized view body must be `
|
|
1115
|
+
+ 'reproducible to stay equal to its plain view (set `pragma nondeterministic_schema` to override)');
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Every source the body reads (set-op legs, every join source, …) so a write to any of
|
|
1119
|
+
// them triggers a rebuild. Collected from the (pre-physical) analyzed plan, where every
|
|
1120
|
+
// source is a bare `TableReferenceNode` — the optimized plan may have wrapped them in
|
|
1121
|
+
// physical access nodes.
|
|
1122
|
+
const tableRefs = [...collectTableRefs(analyzed).values()];
|
|
1123
|
+
const sourceBases = [...new Set(tableRefs.map(ref => `${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase()))];
|
|
1124
|
+
if (sourceBases.length === 0)
|
|
1125
|
+
throw cannotMaterialize(mv.name, 'its body reads no source table');
|
|
1126
|
+
const backing = ctx._findTable(mv.name, mv.schemaName);
|
|
1127
|
+
if (!backing) {
|
|
1128
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
1129
|
+
}
|
|
1130
|
+
// ── Cost gate + size reject ──
|
|
1131
|
+
// Full-rebuild is the floor: an EMPTY sound set resolves to it (`selectMaintenanceStrategy`).
|
|
1132
|
+
// Cost the rebuild against the LARGEST participating source — every write re-evaluates the
|
|
1133
|
+
// whole body, so the largest source it scans governs whether the per-write rebuild is
|
|
1134
|
+
// pathological (e.g. a tiny driving table joined to a huge lookup gates on the lookup).
|
|
1135
|
+
// Re-resolve each source's CURRENT schema (the analyzed plan node may carry a pre-`analyze`
|
|
1136
|
+
// snapshot whose `statistics` predates the latest counts) so the size gate reflects the
|
|
1137
|
+
// live source size at create time.
|
|
1138
|
+
const statsProvider = ctx.optimizer.getStats();
|
|
1139
|
+
let largestSchema = tableRefs[0].tableSchema;
|
|
1140
|
+
let largestRows = -1;
|
|
1141
|
+
for (const ref of tableRefs) {
|
|
1142
|
+
const live = liveSourceSchema(ctx, ref);
|
|
1143
|
+
const rows = statsProvider.tableRows(live) ?? DEFAULT_SOURCE_ROWS;
|
|
1144
|
+
if (rows > largestRows) {
|
|
1145
|
+
largestRows = rows;
|
|
1146
|
+
largestSchema = live;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const sourceStats = estimateMaintenanceStats(ctx, largestSchema, backing.columns.length, /*hasPredicate*/ false);
|
|
1150
|
+
// Size reject: full-rebuild is the only sound strategy here, so a source past the
|
|
1151
|
+
// configurable threshold makes every write pathological. `0` disables the reject.
|
|
1152
|
+
const rebuildThreshold = db.options.getNumberOption('materialized_view_rebuild_row_threshold');
|
|
1153
|
+
if (isFullRebuildPathological(sourceStats, rebuildThreshold)) {
|
|
1154
|
+
const largestBase = `${largestSchema.schemaName}.${largestSchema.name}`.toLowerCase();
|
|
1155
|
+
throw cannotMaterialize(mv.name, `its only sound maintenance strategy is a full body rebuild, but its largest source '${largestBase}' has `
|
|
1156
|
+
+ `~${sourceStats.tableRows} rows, over the materialized_view_rebuild_row_threshold (${rebuildThreshold}) — `
|
|
1157
|
+
+ `every write would re-scan it. Raise or disable the threshold `
|
|
1158
|
+
+ `(\`pragma materialized_view_rebuild_row_threshold = 0\`)`);
|
|
1159
|
+
}
|
|
1160
|
+
// Compile the whole optimized body once into a reusable scheduler (no key filter).
|
|
1161
|
+
const bodyScheduler = new Scheduler(emitPlanNode(optimized, new EmissionContext(db)));
|
|
1162
|
+
const chosenStrategy = selectMaintenanceStrategy([], Math.max(1, sourceStats.tableRows * 0.01), sourceStats);
|
|
1163
|
+
if (chosenStrategy !== 'full-rebuild') {
|
|
1164
|
+
throw new QuereusError(`Internal error: cost gate selected '${chosenStrategy}' for the full-rebuild floor of materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
kind: 'full-rebuild',
|
|
1168
|
+
mv,
|
|
1169
|
+
sourceBase: sourceBases[0],
|
|
1170
|
+
backingSchema: mv.schemaName,
|
|
1171
|
+
backingTableName: mv.name,
|
|
1172
|
+
chosenStrategy,
|
|
1173
|
+
sourceStats,
|
|
1174
|
+
bodyScheduler,
|
|
1175
|
+
sourceBases,
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Compile the key-filtered residual for a binding into a reusable {@link Scheduler}:
|
|
1180
|
+
* the analyzed body with a key-equality filter injected on `T`'s `TableReferenceNode`
|
|
1181
|
+
* (parameterized `${paramPrefix}0…`), then optimized + emitted. Mirrors the assertion
|
|
1182
|
+
* evaluator's residual compilation (`database-assertions.ts`) so the two cannot drift.
|
|
1183
|
+
* Returns `null` if `injectKeyFilter` could not target `T` (the arm builder then falls
|
|
1184
|
+
* through to the full-rebuild floor).
|
|
1185
|
+
*/
|
|
1186
|
+
export function compileResidual(ctx, analyzed, relKey, bindColumns, paramPrefix) {
|
|
1187
|
+
const db = ctx;
|
|
1188
|
+
const rewritten = injectKeyFilter(analyzed, relKey, bindColumns, paramPrefix);
|
|
1189
|
+
if (rewritten === analyzed)
|
|
1190
|
+
return null; // could not parameterize the residual → floor
|
|
1191
|
+
// Suppress the read-side rewrite: the residual is the MV's own body (+ a key
|
|
1192
|
+
// filter) compiled to maintain its backing, so it must stay over the source.
|
|
1193
|
+
const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => ctx.optimizer.optimize(rewritten, db));
|
|
1194
|
+
const instruction = emitPlanNode(optimized, new EmissionContext(db));
|
|
1195
|
+
return new Scheduler(instruction);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Compile the WHOLE optimized body (no key filter, read-side rewrite suppressed) into a
|
|
1199
|
+
* reusable {@link Scheduler} — the residual arms' per-statement degrade-to-rebuild
|
|
1200
|
+
* escape ({@link ResidualArmCommon.fullRebuildScheduler}). Mirrors the floor's own
|
|
1201
|
+
* `bodyScheduler` compilation in {@link buildFullRebuildPlan}: run to completion against
|
|
1202
|
+
* live mid-transaction source state, its rows feed a single `'replace-all'` keyed diff.
|
|
1203
|
+
*/
|
|
1204
|
+
function compileWholeBodyScheduler(ctx, analyzed) {
|
|
1205
|
+
const db = ctx;
|
|
1206
|
+
const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => ctx.optimizer.optimize(analyzed, db));
|
|
1207
|
+
return new Scheduler(emitPlanNode(optimized, new EmissionContext(db)));
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Build a `'prefix-delete'` plan for a single-source lateral-TVF fan-out body
|
|
1211
|
+
* (`select T.pk…, …, f.* from T cross join lateral tvf(<args over T>) f`), or return
|
|
1212
|
+
* `null` on a shape mismatch (the caller falls through to the full-rebuild floor). The
|
|
1213
|
+
* backing PK is the composite product key `(T.pk ∪ tvf-key)` that `keysOf` advertises
|
|
1214
|
+
* through the lateral join; the base PK is its leading prefix. See {@link PrefixDeletePlan}
|
|
1215
|
+
* and `docs/incremental-maintenance.md` § prefix-delete.
|
|
1216
|
+
*
|
|
1217
|
+
* Soundness gates (a mismatch on any returns `null` → floor): exactly one lateral TVF and
|
|
1218
|
+
* one join (no nested/multi TVF, no aggregate over the fan-out); the TVF advertises a
|
|
1219
|
+
* per-call key; the base PK projected and forming the **leading prefix** of the backing PK
|
|
1220
|
+
* with a non-empty TVF-key tail (so each base row's fan-out rows are individually
|
|
1221
|
+
* addressable and a by-prefix delete selects exactly one base row's slice). An `order by`
|
|
1222
|
+
* over the fan-out that reorders the composite key so the base PK no longer leads is a
|
|
1223
|
+
* `null` fall-through (the floor maintains it wholesale). The body's WHERE, if any, is part
|
|
1224
|
+
* of the residual (so an out-of-scope base row fans out to zero rows), exactly as in the
|
|
1225
|
+
* aggregate arm. A **non-deterministic** TVF / argument is the one hard reject (its
|
|
1226
|
+
* arm-specific determinism diagnostic must survive).
|
|
1227
|
+
*/
|
|
1228
|
+
export function buildLateralTvfPrefixDeletePlan(ctx, mv, analyzed, tableRef, sourceBase) {
|
|
1229
|
+
// Exactly one lateral TVF and one join. A second base table is already excluded by
|
|
1230
|
+
// the single-source check upstream; this falls to the floor for a second TVF / chained
|
|
1231
|
+
// lateral join (`t join lateral tvf1 join lateral tvf2`).
|
|
1232
|
+
if (countNodeType(analyzed, PlanNodeType.TableFunctionCall) !== 1)
|
|
1233
|
+
return null;
|
|
1234
|
+
if (countJoins(analyzed) !== 1)
|
|
1235
|
+
return null;
|
|
1236
|
+
// An aggregate over the fan-out is a different shape → floor (the TVF route is taken
|
|
1237
|
+
// before the aggregate route, so an `... group by` over a lateral TVF lands here).
|
|
1238
|
+
if (findAggregate(analyzed))
|
|
1239
|
+
return null;
|
|
1240
|
+
// Determinism: a residual must reproduce exactly what `select <body>` returns, so a
|
|
1241
|
+
// volatile TVF (or a volatile argument expression) is a HARD reject.
|
|
1242
|
+
const tvf = findTableFunctionCall(analyzed);
|
|
1243
|
+
if (!tvf) {
|
|
1244
|
+
// Unreachable — countNodeType(...) === 1 above guarantees one exists.
|
|
1245
|
+
throw new QuereusError(`Internal error: lateral TVF node not found for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1246
|
+
}
|
|
1247
|
+
if (tvf.physical.deterministic === false) {
|
|
1248
|
+
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)`);
|
|
1249
|
+
}
|
|
1250
|
+
for (const operand of tvf.operands) {
|
|
1251
|
+
const det = checkDeterministic(operand);
|
|
1252
|
+
if (!det.valid)
|
|
1253
|
+
throw cannotMaterialize(mv.name, `it passes a non-deterministic argument (${det.expression}) to the lateral table-valued function`);
|
|
1254
|
+
}
|
|
1255
|
+
// The lateral TVF must advertise a per-call key, so the composite product key is a
|
|
1256
|
+
// real column key `(base PK ∪ TVF key)` rather than the all-columns / `isSet`
|
|
1257
|
+
// fallback. Without one the fan-out rows are not individually addressable by a proper
|
|
1258
|
+
// key — the by-prefix delete + keyed upsert would be unsound — so fall to the floor.
|
|
1259
|
+
// `getType().keys` carries the validated advertisement (an out-of-range key
|
|
1260
|
+
// advertisement is dropped, leaving this empty), so it is the authoritative
|
|
1261
|
+
// "did the TVF advertise a usable key" signal.
|
|
1262
|
+
if (tvf.getType().keys.length === 0)
|
|
1263
|
+
return null;
|
|
1264
|
+
// Base T's PK source columns.
|
|
1265
|
+
const sourceSchema = tableRef.tableSchema;
|
|
1266
|
+
const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
|
|
1267
|
+
if (sourcePkCols.length === 0)
|
|
1268
|
+
return null;
|
|
1269
|
+
// Backing table + its physical PK (the composite product key).
|
|
1270
|
+
const backing = ctx._findTable(mv.name, mv.schemaName);
|
|
1271
|
+
if (!backing) {
|
|
1272
|
+
throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
|
|
1273
|
+
}
|
|
1274
|
+
const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
|
|
1275
|
+
// Map each output attribute to a base-T source column (or `undefined` for a TVF
|
|
1276
|
+
// output column). T's attributes pass through the join unchanged, so a base-PK
|
|
1277
|
+
// output column resolves to a T column while a TVF-key output column does not.
|
|
1278
|
+
const sourceAttrToCol = new Map();
|
|
1279
|
+
tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
|
|
1280
|
+
const producingByAttrId = collectProducingExprs(analyzed);
|
|
1281
|
+
const rootAttrs = relationalAttributes(analyzed);
|
|
1282
|
+
if (!rootAttrs)
|
|
1283
|
+
return null;
|
|
1284
|
+
// Prefix soundness: the LEADING `basePrefixLen` backing-PK columns must each
|
|
1285
|
+
// project (transitively) a distinct base-T PK column, their set must equal the base
|
|
1286
|
+
// PK, and there must be a non-empty TVF-key tail. So the base PK is the leading
|
|
1287
|
+
// prefix of the composite product key and the by-prefix delete selects exactly one
|
|
1288
|
+
// base row's fan-out. A composite key that did not form this way falls to the floor.
|
|
1289
|
+
const basePrefixLen = sourcePkCols.length;
|
|
1290
|
+
if (backingPkDefinition.length <= basePrefixLen)
|
|
1291
|
+
return null;
|
|
1292
|
+
const basePkSet = new Set(sourcePkCols);
|
|
1293
|
+
const leadingSourceCols = new Set();
|
|
1294
|
+
const backingPrefixSourceCols = [];
|
|
1295
|
+
for (let i = 0; i < basePrefixLen; i++) {
|
|
1296
|
+
const d = backingPkDefinition[i];
|
|
1297
|
+
const attr = rootAttrs[d.index];
|
|
1298
|
+
const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
|
|
1299
|
+
if (sc === undefined || !basePkSet.has(sc))
|
|
1300
|
+
return null; // base PK not the leading prefix → floor
|
|
1301
|
+
// Soundness precondition for the binary prefix scan (the property
|
|
1302
|
+
// `prefix-delete-noncase-collation-regression-test` locks in): the backing base-PK
|
|
1303
|
+
// column MUST inherit the source PK column's collation. The btree orders this prefix
|
|
1304
|
+
// by `d.collation`, but the keyed diff's existing-slice read (`scanEffective` with
|
|
1305
|
+
// the base prefix, in `applyPrefixDelete`) early-terminates the prefix scan on a
|
|
1306
|
+
// BINARY compare (scan-layer.ts) — sound ONLY because source-PK uniqueness under that
|
|
1307
|
+
// collation collapses each collation class to a single binary value, so a base row's
|
|
1308
|
+
// fan-out rows are binary-homogeneous and contiguous. A backing collation MORE
|
|
1309
|
+
// permissive than the source's would let collation-equal/binary-different base rows
|
|
1310
|
+
// interleave and break that. The backing column derives its collation from the body
|
|
1311
|
+
// relation's type (deriveBackingShape), so a mismatch is an internal derivation bug —
|
|
1312
|
+
// fail loud rather than register an unsound plan.
|
|
1313
|
+
const backingColl = normalizeCollation(d.collation);
|
|
1314
|
+
const sourceColl = normalizeCollation(sourceSchema.columns[sc]?.collation);
|
|
1315
|
+
if (backingColl !== sourceColl) {
|
|
1316
|
+
throw new QuereusError(`Internal error: materialized view '${mv.name}' backing base-PK column `
|
|
1317
|
+
+ `'${backing.columns[d.index]?.name ?? d.index}' has collation '${backingColl}' but its source `
|
|
1318
|
+
+ `primary-key column '${sourceSchema.columns[sc]?.name ?? sc}' has collation '${sourceColl}'; `
|
|
1319
|
+
+ `the prefix-delete arm's binary prefix scan requires the backing base-PK column to inherit the `
|
|
1320
|
+
+ `source PK collation (see scan-layer.ts early-termination)`, StatusCode.INTERNAL);
|
|
1321
|
+
}
|
|
1322
|
+
leadingSourceCols.add(sc);
|
|
1323
|
+
backingPrefixSourceCols.push(sc);
|
|
1324
|
+
}
|
|
1325
|
+
if (leadingSourceCols.size !== basePkSet.size)
|
|
1326
|
+
return null; // prefix does not cover the base PK → floor
|
|
1327
|
+
// The TVF-key tail must NOT re-use a base-PK column — else the fan-out rows would
|
|
1328
|
+
// not be distinguished and the "key" would be base-only (defensive: the product key
|
|
1329
|
+
// places the TVF key, a distinct relation's columns, in the tail). Otherwise → floor.
|
|
1330
|
+
for (let i = basePrefixLen; i < backingPkDefinition.length; i++) {
|
|
1331
|
+
const d = backingPkDefinition[i];
|
|
1332
|
+
const attr = rootAttrs[d.index];
|
|
1333
|
+
const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
|
|
1334
|
+
if (sc !== undefined && basePkSet.has(sc))
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
// Compile + cache the base-PK-keyed residual once (the body with `T.pk = :pk0 AND …`
|
|
1338
|
+
// injected on T). Re-run per affected base key against the live transaction; it
|
|
1339
|
+
// re-runs the lateral join + TVF for that single base row, fanning out to N rows.
|
|
1340
|
+
const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
|
|
1341
|
+
const residualScheduler = compileResidual(ctx, analyzed, relKey, sourcePkCols, 'pk');
|
|
1342
|
+
if (!residualScheduler)
|
|
1343
|
+
return null; // could not parameterize the residual → floor
|
|
1344
|
+
// ── Cost gate ──
|
|
1345
|
+
// The fan-out residual shares the residual-recompute cost shape (a key-filtered
|
|
1346
|
+
// re-execution of the body); the fan-out factor (rows per base key) is not known at
|
|
1347
|
+
// create, so we cost it as a residual and record the choice for substrate parity.
|
|
1348
|
+
// The synchronous reject-at-create / degrade-to-rebuild machinery stays dormant, as
|
|
1349
|
+
// it does for the other arms (a TVF whose fan-out is pathological is not detectable
|
|
1350
|
+
// without fan-out stats — deferred with the fanning-keyed-join follow-up).
|
|
1351
|
+
const soundStrategies = ['residual-recompute'];
|
|
1352
|
+
const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
|
|
1353
|
+
const sourceStats = estimateMaintenanceStats(ctx, sourceSchema, backing.columns.length, hasPredicate);
|
|
1354
|
+
const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
|
|
1355
|
+
const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
|
|
1356
|
+
if (chosenStrategy !== 'residual-recompute') {
|
|
1357
|
+
throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
kind: 'prefix-delete',
|
|
1361
|
+
mv,
|
|
1362
|
+
sourceBase,
|
|
1363
|
+
backingSchema: mv.schemaName,
|
|
1364
|
+
backingTableName: mv.name,
|
|
1365
|
+
chosenStrategy,
|
|
1366
|
+
sourceStats,
|
|
1367
|
+
binding: { kind: 'row', keyColumns: [...sourcePkCols] },
|
|
1368
|
+
residualScheduler,
|
|
1369
|
+
bindParamPrefix: 'pk',
|
|
1370
|
+
bindColumns: sourcePkCols,
|
|
1371
|
+
backingPkDefinition,
|
|
1372
|
+
basePrefixLength: basePrefixLen,
|
|
1373
|
+
backingPrefixSourceCols,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Assemble {@link MaintenanceSourceStats} for the cost gate from the optimizer's
|
|
1378
|
+
* StatsProvider and tuning. `tableRows` / `distinctGroupsEstimate` come from the
|
|
1379
|
+
* provider (heuristic defaults when absent); `forwardBodyCost` is estimated from the
|
|
1380
|
+
* forward cost helpers (a scan + optional filter + projection of the source — the
|
|
1381
|
+
* covering-index body shape); `fallbackRatio` carries the detection kernel's
|
|
1382
|
+
* `deltaPerRowFallbackRatio` for the no-stats residual path.
|
|
1383
|
+
*/
|
|
1384
|
+
/**
|
|
1385
|
+
* The CURRENT `TableSchema` of a source `TableReferenceNode`, re-resolved through the
|
|
1386
|
+
* schema manager. A plan node captures the schema as of plan-build; a later `analyze`
|
|
1387
|
+
* replaces the catalog entry with one carrying fresh `statistics`, so the stale captured
|
|
1388
|
+
* schema would report pre-`analyze` row counts. Re-resolving keeps the floor's size gate
|
|
1389
|
+
* on the live source size. Falls back to the node's captured schema if the name no longer
|
|
1390
|
+
* resolves (it always should — the body planned).
|
|
1391
|
+
*/
|
|
1392
|
+
export function liveSourceSchema(ctx, ref) {
|
|
1393
|
+
const captured = ref.tableSchema;
|
|
1394
|
+
return ctx._findTable(captured.name, captured.schemaName) ?? captured;
|
|
1395
|
+
}
|
|
1396
|
+
export function estimateMaintenanceStats(ctx, sourceSchema, projectionCount, hasPredicate) {
|
|
1397
|
+
const optimizer = ctx.optimizer;
|
|
1398
|
+
const statsProvider = optimizer.getStats();
|
|
1399
|
+
const tableRows = statsProvider.tableRows(sourceSchema) ?? DEFAULT_SOURCE_ROWS;
|
|
1400
|
+
const forwardBodyCost = seqScanCost(tableRows)
|
|
1401
|
+
+ (hasPredicate ? filterCost(tableRows) : 0)
|
|
1402
|
+
+ projectCost(tableRows, projectionCount);
|
|
1403
|
+
const stats = {
|
|
1404
|
+
tableRows,
|
|
1405
|
+
forwardBodyCost,
|
|
1406
|
+
fallbackRatio: optimizer.tuning.deltaPerRowFallbackRatio,
|
|
1407
|
+
};
|
|
1408
|
+
// `distinctValues` is an optional, per-column StatsProvider method. For the
|
|
1409
|
+
// covering-index shape the source PK is the grouping key; a single-column PK
|
|
1410
|
+
// yields a usable distinct-groups estimate (which only feeds the never-chosen-here
|
|
1411
|
+
// residual cost). Multi-column PKs leave it unset → residual takes the no-stats path.
|
|
1412
|
+
const pkDef = sourceSchema.primaryKeyDefinition;
|
|
1413
|
+
if (pkDef.length === 1 && statsProvider.distinctValues) {
|
|
1414
|
+
const pkColName = sourceSchema.columns[pkDef[0].index]?.name;
|
|
1415
|
+
if (pkColName !== undefined) {
|
|
1416
|
+
const distinct = statsProvider.distinctValues(sourceSchema, pkColName);
|
|
1417
|
+
if (distinct !== undefined)
|
|
1418
|
+
stats.distinctGroupsEstimate = distinct;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return stats;
|
|
1422
|
+
}
|
|
1423
|
+
//# sourceMappingURL=database-materialized-views-plan-builders.js.map
|