@quereus/quereus 3.2.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/src/common/datatype.d.ts +12 -0
- package/dist/src/common/datatype.d.ts.map +1 -1
- package/dist/src/common/datatype.js.map +1 -1
- package/dist/src/common/types.d.ts +24 -0
- package/dist/src/common/types.d.ts.map +1 -1
- package/dist/src/common/types.js.map +1 -1
- package/dist/src/core/database-assertions.d.ts +37 -9
- package/dist/src/core/database-assertions.d.ts.map +1 -1
- package/dist/src/core/database-assertions.js +62 -106
- package/dist/src/core/database-assertions.js.map +1 -1
- package/dist/src/core/database-events.d.ts +163 -0
- package/dist/src/core/database-events.d.ts.map +1 -1
- package/dist/src/core/database-events.js +235 -21
- package/dist/src/core/database-events.js.map +1 -1
- package/dist/src/core/database-external-changes.d.ts +28 -0
- package/dist/src/core/database-external-changes.d.ts.map +1 -0
- package/dist/src/core/database-external-changes.js +242 -0
- package/dist/src/core/database-external-changes.js.map +1 -0
- package/dist/src/core/database-internal.d.ts +50 -1
- package/dist/src/core/database-internal.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views.d.ts +1253 -0
- package/dist/src/core/database-materialized-views.d.ts.map +1 -0
- package/dist/src/core/database-materialized-views.js +3064 -0
- package/dist/src/core/database-materialized-views.js.map +1 -0
- package/dist/src/core/database-options.d.ts +4 -0
- package/dist/src/core/database-options.d.ts.map +1 -1
- package/dist/src/core/database-options.js +10 -0
- package/dist/src/core/database-options.js.map +1 -1
- package/dist/src/core/database-transaction.d.ts +19 -3
- package/dist/src/core/database-transaction.d.ts.map +1 -1
- package/dist/src/core/database-transaction.js +30 -3
- package/dist/src/core/database-transaction.js.map +1 -1
- package/dist/src/core/database-watchers.d.ts +19 -0
- package/dist/src/core/database-watchers.d.ts.map +1 -1
- package/dist/src/core/database-watchers.js +63 -3
- package/dist/src/core/database-watchers.js.map +1 -1
- package/dist/src/core/database.d.ts +203 -11
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +493 -29
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/derived-row-validator.d.ts +137 -0
- package/dist/src/core/derived-row-validator.d.ts.map +1 -0
- package/dist/src/core/derived-row-validator.js +314 -0
- package/dist/src/core/derived-row-validator.js.map +1 -0
- package/dist/src/core/statement.d.ts.map +1 -1
- package/dist/src/core/statement.js +30 -9
- package/dist/src/core/statement.js.map +1 -1
- package/dist/src/emit/ast-stringify.d.ts +135 -1
- package/dist/src/emit/ast-stringify.d.ts.map +1 -1
- package/dist/src/emit/ast-stringify.js +795 -120
- package/dist/src/emit/ast-stringify.js.map +1 -1
- package/dist/src/func/builtins/aggregate.d.ts.map +1 -1
- package/dist/src/func/builtins/aggregate.js +11 -10
- package/dist/src/func/builtins/aggregate.js.map +1 -1
- package/dist/src/func/builtins/builtin-window-functions.d.ts.map +1 -1
- package/dist/src/func/builtins/builtin-window-functions.js +32 -0
- package/dist/src/func/builtins/builtin-window-functions.js.map +1 -1
- package/dist/src/func/builtins/explain.d.ts +3 -0
- package/dist/src/func/builtins/explain.d.ts.map +1 -1
- package/dist/src/func/builtins/explain.js +229 -0
- package/dist/src/func/builtins/explain.js.map +1 -1
- package/dist/src/func/builtins/index.d.ts.map +1 -1
- package/dist/src/func/builtins/index.js +10 -2
- package/dist/src/func/builtins/index.js.map +1 -1
- package/dist/src/func/builtins/json.d.ts.map +1 -1
- package/dist/src/func/builtins/json.js +3 -2
- package/dist/src/func/builtins/json.js.map +1 -1
- package/dist/src/func/builtins/mutation.d.ts +2 -0
- package/dist/src/func/builtins/mutation.d.ts.map +1 -0
- package/dist/src/func/builtins/mutation.js +53 -0
- package/dist/src/func/builtins/mutation.js.map +1 -0
- package/dist/src/func/builtins/schema.d.ts +2 -0
- package/dist/src/func/builtins/schema.d.ts.map +1 -1
- package/dist/src/func/builtins/schema.js +713 -26
- package/dist/src/func/builtins/schema.js.map +1 -1
- package/dist/src/func/builtins/string.js +1 -1
- package/dist/src/func/builtins/string.js.map +1 -1
- package/dist/src/func/registration.d.ts +9 -0
- package/dist/src/func/registration.d.ts.map +1 -1
- package/dist/src/func/registration.js +4 -0
- package/dist/src/func/registration.js.map +1 -1
- package/dist/src/index.d.ts +25 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +27 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/parser/ast.d.ts +353 -21
- package/dist/src/parser/ast.d.ts.map +1 -1
- package/dist/src/parser/index.d.ts +14 -1
- package/dist/src/parser/index.d.ts.map +1 -1
- package/dist/src/parser/index.js +19 -0
- package/dist/src/parser/index.js.map +1 -1
- package/dist/src/parser/lexer.d.ts +9 -0
- package/dist/src/parser/lexer.d.ts.map +1 -1
- package/dist/src/parser/lexer.js +9 -0
- package/dist/src/parser/lexer.js.map +1 -1
- package/dist/src/parser/parser.d.ts +277 -8
- package/dist/src/parser/parser.d.ts.map +1 -1
- package/dist/src/parser/parser.js +1393 -471
- package/dist/src/parser/parser.js.map +1 -1
- package/dist/src/parser/visitor.d.ts.map +1 -1
- package/dist/src/parser/visitor.js +12 -8
- package/dist/src/parser/visitor.js.map +1 -1
- package/dist/src/planner/analysis/assertion-classifier.d.ts.map +1 -1
- package/dist/src/planner/analysis/assertion-classifier.js +4 -0
- package/dist/src/planner/analysis/assertion-classifier.js.map +1 -1
- package/dist/src/planner/analysis/assertion-hoist-cache.d.ts.map +1 -1
- package/dist/src/planner/analysis/assertion-hoist-cache.js +8 -4
- package/dist/src/planner/analysis/assertion-hoist-cache.js.map +1 -1
- package/dist/src/planner/analysis/authored-inverse.d.ts +22 -0
- package/dist/src/planner/analysis/authored-inverse.d.ts.map +1 -0
- package/dist/src/planner/analysis/authored-inverse.js +267 -0
- package/dist/src/planner/analysis/authored-inverse.js.map +1 -0
- package/dist/src/planner/analysis/binding-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/binding-extractor.js +9 -6
- package/dist/src/planner/analysis/binding-extractor.js.map +1 -1
- package/dist/src/planner/analysis/change-scope.d.ts +34 -4
- package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
- package/dist/src/planner/analysis/change-scope.js +115 -7
- package/dist/src/planner/analysis/change-scope.js.map +1 -1
- package/dist/src/planner/analysis/check-extraction.d.ts +36 -2
- package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/check-extraction.js +174 -46
- package/dist/src/planner/analysis/check-extraction.js.map +1 -1
- package/dist/src/planner/analysis/coarsened-key.d.ts +109 -0
- package/dist/src/planner/analysis/coarsened-key.d.ts.map +1 -0
- package/dist/src/planner/analysis/coarsened-key.js +228 -0
- package/dist/src/planner/analysis/coarsened-key.js.map +1 -0
- package/dist/src/planner/analysis/comparison-collation.d.ts +216 -0
- package/dist/src/planner/analysis/comparison-collation.d.ts.map +1 -0
- package/dist/src/planner/analysis/comparison-collation.js +341 -0
- package/dist/src/planner/analysis/comparison-collation.js.map +1 -0
- package/dist/src/planner/analysis/constraint-extractor.d.ts +13 -1
- package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.js +220 -21
- package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
- package/dist/src/planner/analysis/coverage-prover.d.ts +321 -0
- package/dist/src/planner/analysis/coverage-prover.d.ts.map +1 -0
- package/dist/src/planner/analysis/coverage-prover.js +1038 -0
- package/dist/src/planner/analysis/coverage-prover.js.map +1 -0
- package/dist/src/planner/analysis/key-filter.d.ts +22 -0
- package/dist/src/planner/analysis/key-filter.d.ts.map +1 -0
- package/dist/src/planner/analysis/key-filter.js +105 -0
- package/dist/src/planner/analysis/key-filter.js.map +1 -0
- package/dist/src/planner/analysis/partial-unique-extraction.d.ts +36 -1
- package/dist/src/planner/analysis/partial-unique-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/partial-unique-extraction.js +148 -22
- package/dist/src/planner/analysis/partial-unique-extraction.js.map +1 -1
- package/dist/src/planner/analysis/predicate-normalizer.d.ts.map +1 -1
- package/dist/src/planner/analysis/predicate-normalizer.js +30 -1
- package/dist/src/planner/analysis/predicate-normalizer.js.map +1 -1
- package/dist/src/planner/analysis/predicate-shape.d.ts +36 -1
- package/dist/src/planner/analysis/predicate-shape.d.ts.map +1 -1
- package/dist/src/planner/analysis/predicate-shape.js +51 -13
- package/dist/src/planner/analysis/predicate-shape.js.map +1 -1
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +314 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.js +1081 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -0
- package/dist/src/planner/analysis/scalar-invertibility.d.ts +92 -0
- package/dist/src/planner/analysis/scalar-invertibility.d.ts.map +1 -0
- package/dist/src/planner/analysis/scalar-invertibility.js +129 -0
- package/dist/src/planner/analysis/scalar-invertibility.js.map +1 -0
- package/dist/src/planner/analysis/update-lineage.d.ts +196 -0
- package/dist/src/planner/analysis/update-lineage.d.ts.map +1 -0
- package/dist/src/planner/analysis/update-lineage.js +322 -0
- package/dist/src/planner/analysis/update-lineage.js.map +1 -0
- package/dist/src/planner/analysis/view-complement.d.ts +42 -0
- package/dist/src/planner/analysis/view-complement.d.ts.map +1 -0
- package/dist/src/planner/analysis/view-complement.js +54 -0
- package/dist/src/planner/analysis/view-complement.js.map +1 -0
- package/dist/src/planner/building/alter-table.d.ts +1 -1
- package/dist/src/planner/building/alter-table.d.ts.map +1 -1
- package/dist/src/planner/building/alter-table.js +211 -2
- package/dist/src/planner/building/alter-table.js.map +1 -1
- package/dist/src/planner/building/block.d.ts.map +1 -1
- package/dist/src/planner/building/block.js +18 -1
- package/dist/src/planner/building/block.js.map +1 -1
- package/dist/src/planner/building/constraint-builder.d.ts +33 -5
- package/dist/src/planner/building/constraint-builder.d.ts.map +1 -1
- package/dist/src/planner/building/constraint-builder.js +63 -28
- package/dist/src/planner/building/constraint-builder.js.map +1 -1
- package/dist/src/planner/building/create-view.d.ts +9 -0
- package/dist/src/planner/building/create-view.d.ts.map +1 -1
- package/dist/src/planner/building/create-view.js +41 -12
- package/dist/src/planner/building/create-view.js.map +1 -1
- package/dist/src/planner/building/ddl.d.ts.map +1 -1
- package/dist/src/planner/building/ddl.js +94 -0
- package/dist/src/planner/building/ddl.js.map +1 -1
- package/dist/src/planner/building/declare-schema.d.ts +1 -0
- package/dist/src/planner/building/declare-schema.d.ts.map +1 -1
- package/dist/src/planner/building/declare-schema.js +4 -1
- package/dist/src/planner/building/declare-schema.js.map +1 -1
- package/dist/src/planner/building/default-scope.d.ts +26 -0
- package/dist/src/planner/building/default-scope.d.ts.map +1 -0
- package/dist/src/planner/building/default-scope.js +41 -0
- package/dist/src/planner/building/default-scope.js.map +1 -0
- package/dist/src/planner/building/delete.d.ts +19 -1
- package/dist/src/planner/building/delete.d.ts.map +1 -1
- package/dist/src/planner/building/delete.js +116 -34
- package/dist/src/planner/building/delete.js.map +1 -1
- package/dist/src/planner/building/dml-target.d.ts +118 -0
- package/dist/src/planner/building/dml-target.d.ts.map +1 -0
- package/dist/src/planner/building/dml-target.js +282 -0
- package/dist/src/planner/building/dml-target.js.map +1 -0
- package/dist/src/planner/building/drop-index.d.ts.map +1 -1
- package/dist/src/planner/building/drop-index.js +4 -1
- package/dist/src/planner/building/drop-index.js.map +1 -1
- package/dist/src/planner/building/drop-view.d.ts.map +1 -1
- package/dist/src/planner/building/drop-view.js +4 -2
- package/dist/src/planner/building/drop-view.js.map +1 -1
- package/dist/src/planner/building/expression.d.ts.map +1 -1
- package/dist/src/planner/building/expression.js +60 -21
- package/dist/src/planner/building/expression.js.map +1 -1
- package/dist/src/planner/building/foreign-key-builder.d.ts +30 -0
- package/dist/src/planner/building/foreign-key-builder.d.ts.map +1 -1
- package/dist/src/planner/building/foreign-key-builder.js +160 -129
- package/dist/src/planner/building/foreign-key-builder.js.map +1 -1
- package/dist/src/planner/building/insert.d.ts +45 -2
- package/dist/src/planner/building/insert.d.ts.map +1 -1
- package/dist/src/planner/building/insert.js +257 -88
- package/dist/src/planner/building/insert.js.map +1 -1
- package/dist/src/planner/building/lens-auxiliary-access.d.ts +22 -0
- package/dist/src/planner/building/lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/planner/building/lens-auxiliary-access.js +132 -0
- package/dist/src/planner/building/lens-auxiliary-access.js.map +1 -0
- package/dist/src/planner/building/materialized-view.d.ts +16 -0
- package/dist/src/planner/building/materialized-view.d.ts.map +1 -0
- package/dist/src/planner/building/materialized-view.js +57 -0
- package/dist/src/planner/building/materialized-view.js.map +1 -0
- package/dist/src/planner/building/returning-star.d.ts +32 -0
- package/dist/src/planner/building/returning-star.d.ts.map +1 -0
- package/dist/src/planner/building/returning-star.js +45 -0
- package/dist/src/planner/building/returning-star.js.map +1 -0
- package/dist/src/planner/building/select-aggregates.d.ts.map +1 -1
- package/dist/src/planner/building/select-aggregates.js +51 -13
- package/dist/src/planner/building/select-aggregates.js.map +1 -1
- package/dist/src/planner/building/select-compound.d.ts.map +1 -1
- package/dist/src/planner/building/select-compound.js +84 -11
- package/dist/src/planner/building/select-compound.js.map +1 -1
- package/dist/src/planner/building/select-context.d.ts +10 -2
- package/dist/src/planner/building/select-context.d.ts.map +1 -1
- package/dist/src/planner/building/select-context.js +7 -1
- package/dist/src/planner/building/select-context.js.map +1 -1
- package/dist/src/planner/building/select-modifiers.js +6 -0
- package/dist/src/planner/building/select-modifiers.js.map +1 -1
- package/dist/src/planner/building/select-ordinal.d.ts +18 -0
- package/dist/src/planner/building/select-ordinal.d.ts.map +1 -1
- package/dist/src/planner/building/select-ordinal.js +30 -0
- package/dist/src/planner/building/select-ordinal.js.map +1 -1
- package/dist/src/planner/building/select-projections.d.ts +8 -2
- package/dist/src/planner/building/select-projections.d.ts.map +1 -1
- package/dist/src/planner/building/select-projections.js +26 -4
- package/dist/src/planner/building/select-projections.js.map +1 -1
- package/dist/src/planner/building/select-window.d.ts.map +1 -1
- package/dist/src/planner/building/select-window.js +8 -5
- package/dist/src/planner/building/select-window.js.map +1 -1
- package/dist/src/planner/building/select.d.ts.map +1 -1
- package/dist/src/planner/building/select.js +164 -59
- package/dist/src/planner/building/select.js.map +1 -1
- package/dist/src/planner/building/set-object-tags.d.ts +7 -0
- package/dist/src/planner/building/set-object-tags.d.ts.map +1 -0
- package/dist/src/planner/building/set-object-tags.js +38 -0
- package/dist/src/planner/building/set-object-tags.js.map +1 -0
- package/dist/src/planner/building/tag-diagnostics.d.ts +27 -0
- package/dist/src/planner/building/tag-diagnostics.d.ts.map +1 -0
- package/dist/src/planner/building/tag-diagnostics.js +37 -0
- package/dist/src/planner/building/tag-diagnostics.js.map +1 -0
- package/dist/src/planner/building/update.d.ts +18 -1
- package/dist/src/planner/building/update.d.ts.map +1 -1
- package/dist/src/planner/building/update.js +134 -58
- package/dist/src/planner/building/update.js.map +1 -1
- package/dist/src/planner/building/view-mutation-builder.d.ts +15 -0
- package/dist/src/planner/building/view-mutation-builder.d.ts.map +1 -0
- package/dist/src/planner/building/view-mutation-builder.js +1158 -0
- package/dist/src/planner/building/view-mutation-builder.js.map +1 -0
- package/dist/src/planner/building/with.d.ts +11 -0
- package/dist/src/planner/building/with.d.ts.map +1 -1
- package/dist/src/planner/building/with.js +48 -10
- package/dist/src/planner/building/with.js.map +1 -1
- package/dist/src/planner/cost/index.d.ts +83 -0
- package/dist/src/planner/cost/index.d.ts.map +1 -1
- package/dist/src/planner/cost/index.js +114 -0
- package/dist/src/planner/cost/index.js.map +1 -1
- package/dist/src/planner/framework/characteristics.d.ts +38 -4
- package/dist/src/planner/framework/characteristics.d.ts.map +1 -1
- package/dist/src/planner/framework/characteristics.js +50 -6
- package/dist/src/planner/framework/characteristics.js.map +1 -1
- package/dist/src/planner/framework/pass.d.ts.map +1 -1
- package/dist/src/planner/framework/pass.js +2 -1
- package/dist/src/planner/framework/pass.js.map +1 -1
- package/dist/src/planner/framework/physical-utils.d.ts.map +1 -1
- package/dist/src/planner/framework/physical-utils.js +7 -1
- package/dist/src/planner/framework/physical-utils.js.map +1 -1
- package/dist/src/planner/framework/registry.d.ts +39 -1
- package/dist/src/planner/framework/registry.d.ts.map +1 -1
- package/dist/src/planner/framework/registry.js +18 -2
- package/dist/src/planner/framework/registry.js.map +1 -1
- package/dist/src/planner/mutation/backward-body.d.ts +131 -0
- package/dist/src/planner/mutation/backward-body.d.ts.map +1 -0
- package/dist/src/planner/mutation/backward-body.js +135 -0
- package/dist/src/planner/mutation/backward-body.js.map +1 -0
- package/dist/src/planner/mutation/cte-flatten.d.ts +17 -0
- package/dist/src/planner/mutation/cte-flatten.d.ts.map +1 -0
- package/dist/src/planner/mutation/cte-flatten.js +364 -0
- package/dist/src/planner/mutation/cte-flatten.js.map +1 -0
- package/dist/src/planner/mutation/decomposition.d.ts +273 -0
- package/dist/src/planner/mutation/decomposition.d.ts.map +1 -0
- package/dist/src/planner/mutation/decomposition.js +1719 -0
- package/dist/src/planner/mutation/decomposition.js.map +1 -0
- package/dist/src/planner/mutation/lens-enforcement.d.ts +165 -0
- package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -0
- package/dist/src/planner/mutation/lens-enforcement.js +745 -0
- package/dist/src/planner/mutation/lens-enforcement.js.map +1 -0
- package/dist/src/planner/mutation/multi-source.d.ts +568 -0
- package/dist/src/planner/mutation/multi-source.d.ts.map +1 -0
- package/dist/src/planner/mutation/multi-source.js +2915 -0
- package/dist/src/planner/mutation/multi-source.js.map +1 -0
- package/dist/src/planner/mutation/mutation-diagnostic.d.ts +37 -0
- package/dist/src/planner/mutation/mutation-diagnostic.d.ts.map +1 -0
- package/dist/src/planner/mutation/mutation-diagnostic.js +24 -0
- package/dist/src/planner/mutation/mutation-diagnostic.js.map +1 -0
- package/dist/src/planner/mutation/mutation-tags.d.ts +33 -0
- package/dist/src/planner/mutation/mutation-tags.d.ts.map +1 -0
- package/dist/src/planner/mutation/mutation-tags.js +31 -0
- package/dist/src/planner/mutation/mutation-tags.js.map +1 -0
- package/dist/src/planner/mutation/propagate.d.ts +97 -0
- package/dist/src/planner/mutation/propagate.d.ts.map +1 -0
- package/dist/src/planner/mutation/propagate.js +220 -0
- package/dist/src/planner/mutation/propagate.js.map +1 -0
- package/dist/src/planner/mutation/scope-transform.d.ts +181 -0
- package/dist/src/planner/mutation/scope-transform.d.ts.map +1 -0
- package/dist/src/planner/mutation/scope-transform.js +574 -0
- package/dist/src/planner/mutation/scope-transform.js.map +1 -0
- package/dist/src/planner/mutation/set-op.d.ts +242 -0
- package/dist/src/planner/mutation/set-op.d.ts.map +1 -0
- package/dist/src/planner/mutation/set-op.js +1687 -0
- package/dist/src/planner/mutation/set-op.js.map +1 -0
- package/dist/src/planner/mutation/single-source.d.ts +261 -0
- package/dist/src/planner/mutation/single-source.d.ts.map +1 -0
- package/dist/src/planner/mutation/single-source.js +1096 -0
- package/dist/src/planner/mutation/single-source.js.map +1 -0
- package/dist/src/planner/nodes/aggregate-node.d.ts +6 -4
- package/dist/src/planner/nodes/aggregate-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/aggregate-node.js +11 -9
- package/dist/src/planner/nodes/aggregate-node.js.map +1 -1
- package/dist/src/planner/nodes/alias-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/alias-node.js +5 -1
- package/dist/src/planner/nodes/alias-node.js.map +1 -1
- package/dist/src/planner/nodes/alter-table-node.d.ts +124 -1
- package/dist/src/planner/nodes/alter-table-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/alter-table-node.js +27 -0
- package/dist/src/planner/nodes/alter-table-node.js.map +1 -1
- package/dist/src/planner/nodes/analyze-node.d.ts +2 -1
- package/dist/src/planner/nodes/analyze-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/analyze-node.js +21 -1
- package/dist/src/planner/nodes/analyze-node.js.map +1 -1
- package/dist/src/planner/nodes/asserted-keys-node.d.ts +43 -0
- package/dist/src/planner/nodes/asserted-keys-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/asserted-keys-node.js +99 -0
- package/dist/src/planner/nodes/asserted-keys-node.js.map +1 -0
- package/dist/src/planner/nodes/async-gather-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/async-gather-node.js +33 -8
- package/dist/src/planner/nodes/async-gather-node.js.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.js +2 -1
- package/dist/src/planner/nodes/bloom-join-node.js.map +1 -1
- package/dist/src/planner/nodes/create-view-node.d.ts +7 -2
- package/dist/src/planner/nodes/create-view-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/create-view-node.js +4 -1
- package/dist/src/planner/nodes/create-view-node.js.map +1 -1
- package/dist/src/planner/nodes/declarative-schema.d.ts +13 -1
- package/dist/src/planner/nodes/declarative-schema.d.ts.map +1 -1
- package/dist/src/planner/nodes/declarative-schema.js +32 -0
- package/dist/src/planner/nodes/declarative-schema.js.map +1 -1
- package/dist/src/planner/nodes/distinct-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/distinct-node.js +2 -0
- package/dist/src/planner/nodes/distinct-node.js.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts +29 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.js +27 -3
- package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
- package/dist/src/planner/nodes/eager-prefetch-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/eager-prefetch-node.js +2 -0
- package/dist/src/planner/nodes/eager-prefetch-node.js.map +1 -1
- package/dist/src/planner/nodes/envelope-scan-node.d.ts +42 -0
- package/dist/src/planner/nodes/envelope-scan-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/envelope-scan-node.js +62 -0
- package/dist/src/planner/nodes/envelope-scan-node.js.map +1 -0
- package/dist/src/planner/nodes/fanout-lookup-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.js +11 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.js.map +1 -1
- package/dist/src/planner/nodes/filter.d.ts.map +1 -1
- package/dist/src/planner/nodes/filter.js +63 -13
- package/dist/src/planner/nodes/filter.js.map +1 -1
- package/dist/src/planner/nodes/hash-aggregate.d.ts.map +1 -1
- package/dist/src/planner/nodes/hash-aggregate.js +6 -16
- package/dist/src/planner/nodes/hash-aggregate.js.map +1 -1
- package/dist/src/planner/nodes/join-node.d.ts +41 -1
- package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/join-node.js +78 -8
- package/dist/src/planner/nodes/join-node.js.map +1 -1
- package/dist/src/planner/nodes/join-utils.d.ts +33 -6
- package/dist/src/planner/nodes/join-utils.d.ts.map +1 -1
- package/dist/src/planner/nodes/join-utils.js +131 -10
- package/dist/src/planner/nodes/join-utils.js.map +1 -1
- package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts +104 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js +91 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js.map +1 -0
- package/dist/src/planner/nodes/limit-offset.d.ts +12 -0
- package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
- package/dist/src/planner/nodes/limit-offset.js +52 -3
- package/dist/src/planner/nodes/limit-offset.js.map +1 -1
- package/dist/src/planner/nodes/materialized-view-nodes.d.ts +69 -0
- package/dist/src/planner/nodes/materialized-view-nodes.d.ts.map +1 -0
- package/dist/src/planner/nodes/materialized-view-nodes.js +111 -0
- package/dist/src/planner/nodes/materialized-view-nodes.js.map +1 -0
- package/dist/src/planner/nodes/merge-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/merge-join-node.js +2 -1
- package/dist/src/planner/nodes/merge-join-node.js.map +1 -1
- package/dist/src/planner/nodes/ordinal-slice-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/ordinal-slice-node.js +2 -0
- package/dist/src/planner/nodes/ordinal-slice-node.js.map +1 -1
- package/dist/src/planner/nodes/plan-node-type.d.ts +9 -0
- package/dist/src/planner/nodes/plan-node-type.d.ts.map +1 -1
- package/dist/src/planner/nodes/plan-node-type.js +9 -0
- package/dist/src/planner/nodes/plan-node-type.js.map +1 -1
- package/dist/src/planner/nodes/plan-node.d.ts +265 -5
- package/dist/src/planner/nodes/plan-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/plan-node.js.map +1 -1
- package/dist/src/planner/nodes/pragma.d.ts +2 -1
- package/dist/src/planner/nodes/pragma.d.ts.map +1 -1
- package/dist/src/planner/nodes/pragma.js +12 -0
- package/dist/src/planner/nodes/pragma.js.map +1 -1
- package/dist/src/planner/nodes/project-node.d.ts +14 -1
- package/dist/src/planner/nodes/project-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/project-node.js +103 -16
- package/dist/src/planner/nodes/project-node.js.map +1 -1
- package/dist/src/planner/nodes/reference.d.ts.map +1 -1
- package/dist/src/planner/nodes/reference.js +63 -30
- package/dist/src/planner/nodes/reference.js.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.js +7 -0
- package/dist/src/planner/nodes/retrieve-node.js.map +1 -1
- package/dist/src/planner/nodes/returning-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/returning-node.js +10 -3
- package/dist/src/planner/nodes/returning-node.js.map +1 -1
- package/dist/src/planner/nodes/scalar.d.ts +20 -0
- package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
- package/dist/src/planner/nodes/scalar.js +71 -14
- package/dist/src/planner/nodes/scalar.js.map +1 -1
- package/dist/src/planner/nodes/set-object-tags-node.d.ts +39 -0
- package/dist/src/planner/nodes/set-object-tags-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/set-object-tags-node.js +41 -0
- package/dist/src/planner/nodes/set-object-tags-node.js.map +1 -0
- package/dist/src/planner/nodes/set-operation-node.d.ts +123 -1
- package/dist/src/planner/nodes/set-operation-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/set-operation-node.js +302 -18
- package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
- package/dist/src/planner/nodes/single-row.d.ts.map +1 -1
- package/dist/src/planner/nodes/single-row.js +3 -0
- package/dist/src/planner/nodes/single-row.js.map +1 -1
- package/dist/src/planner/nodes/sort.d.ts.map +1 -1
- package/dist/src/planner/nodes/sort.js +8 -7
- package/dist/src/planner/nodes/sort.js.map +1 -1
- package/dist/src/planner/nodes/stream-aggregate.d.ts.map +1 -1
- package/dist/src/planner/nodes/stream-aggregate.js +8 -23
- package/dist/src/planner/nodes/stream-aggregate.js.map +1 -1
- package/dist/src/planner/nodes/subquery.d.ts +2 -0
- package/dist/src/planner/nodes/subquery.d.ts.map +1 -1
- package/dist/src/planner/nodes/subquery.js +18 -2
- package/dist/src/planner/nodes/subquery.js.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.js +23 -3
- package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
- package/dist/src/planner/nodes/table-function-call.js +6 -0
- package/dist/src/planner/nodes/table-function-call.js.map +1 -1
- package/dist/src/planner/nodes/values-node.d.ts +3 -1
- package/dist/src/planner/nodes/values-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/values-node.js +26 -0
- package/dist/src/planner/nodes/values-node.js.map +1 -1
- package/dist/src/planner/nodes/view-mutation-node.d.ts +259 -0
- package/dist/src/planner/nodes/view-mutation-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/view-mutation-node.js +273 -0
- package/dist/src/planner/nodes/view-mutation-node.js.map +1 -0
- package/dist/src/planner/nodes/window-function.d.ts +17 -1
- package/dist/src/planner/nodes/window-function.d.ts.map +1 -1
- package/dist/src/planner/nodes/window-function.js +15 -1
- package/dist/src/planner/nodes/window-function.js.map +1 -1
- package/dist/src/planner/nodes/window-node.js +3 -3
- package/dist/src/planner/nodes/window-node.js.map +1 -1
- package/dist/src/planner/optimizer.d.ts.map +1 -1
- package/dist/src/planner/optimizer.js +372 -39
- package/dist/src/planner/optimizer.js.map +1 -1
- package/dist/src/planner/planning-context.d.ts +1 -1
- package/dist/src/planner/planning-context.d.ts.map +1 -1
- package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts +70 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts.map +1 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.js +156 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.js.map +1 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts +31 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js +176 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js.map +1 -0
- package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
- package/dist/src/planner/rules/access/rule-select-access-path.js +435 -37
- package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-aggregate-streaming.d.ts.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-aggregate-streaming.js +8 -27
- package/dist/src/planner/rules/aggregate/rule-aggregate-streaming.js.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.d.ts +9 -3
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.d.ts.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js +56 -5
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts +39 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts.map +1 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +616 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js.map +1 -0
- package/dist/src/planner/rules/cache/rule-scalar-cse.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-scalar-cse.js +8 -1
- package/dist/src/planner/rules/cache/rule-scalar-cse.js.map +1 -1
- package/dist/src/planner/rules/distinct/rule-distinct-elimination.d.ts +8 -7
- package/dist/src/planner/rules/distinct/rule-distinct-elimination.d.ts.map +1 -1
- package/dist/src/planner/rules/distinct/rule-distinct-elimination.js +14 -21
- package/dist/src/planner/rules/distinct/rule-distinct-elimination.js.map +1 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.d.ts +36 -0
- package/dist/src/planner/rules/join/equi-pair-extractor.d.ts.map +1 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.js +42 -5
- package/dist/src/planner/rules/join/equi-pair-extractor.js.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +10 -0
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js +25 -9
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js.map +1 -1
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts +130 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js +206 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js.map +1 -0
- package/dist/src/planner/rules/join/rule-join-elimination.d.ts +67 -14
- package/dist/src/planner/rules/join/rule-join-elimination.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-elimination.js +81 -25
- package/dist/src/planner/rules/join/rule-join-elimination.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts +84 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.js +138 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.js.map +1 -0
- package/dist/src/planner/rules/join/rule-join-greedy-commute.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js +19 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.js +14 -2
- package/dist/src/planner/rules/join/rule-join-physical-selection.js.map +1 -1
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.js +5 -2
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.js.map +1 -1
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.js +4 -0
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.js.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js +10 -0
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js.map +1 -1
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts +286 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js +548 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js.map +1 -0
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js +9 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js +7 -0
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js.map +1 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js +10 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js +10 -1
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js +18 -0
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js +7 -0
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js +9 -0
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js +13 -3
- package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js +2 -2
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.d.ts.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.js +14 -0
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.js.map +1 -1
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.d.ts +16 -0
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.d.ts.map +1 -1
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js +47 -4
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js +8 -0
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js +7 -0
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js +12 -0
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js.map +1 -1
- package/dist/src/planner/rules/window/rule-monotonic-window.js +1 -1
- package/dist/src/planner/rules/window/rule-monotonic-window.js.map +1 -1
- package/dist/src/planner/type-utils.d.ts +14 -0
- package/dist/src/planner/type-utils.d.ts.map +1 -1
- package/dist/src/planner/type-utils.js +66 -21
- package/dist/src/planner/type-utils.js.map +1 -1
- package/dist/src/planner/util/fd-utils.d.ts +228 -36
- package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
- package/dist/src/planner/util/fd-utils.js +501 -84
- package/dist/src/planner/util/fd-utils.js.map +1 -1
- package/dist/src/planner/util/ind-utils.d.ts +27 -1
- package/dist/src/planner/util/ind-utils.d.ts.map +1 -1
- package/dist/src/planner/util/ind-utils.js +80 -6
- package/dist/src/planner/util/ind-utils.js.map +1 -1
- package/dist/src/planner/util/key-utils.d.ts +26 -3
- package/dist/src/planner/util/key-utils.d.ts.map +1 -1
- package/dist/src/planner/util/key-utils.js +182 -33
- package/dist/src/planner/util/key-utils.js.map +1 -1
- package/dist/src/planner/util/set-op-wrapper.d.ts +37 -0
- package/dist/src/planner/util/set-op-wrapper.d.ts.map +1 -0
- package/dist/src/planner/util/set-op-wrapper.js +82 -0
- package/dist/src/planner/util/set-op-wrapper.js.map +1 -0
- package/dist/src/planner/validation/plan-validator.d.ts.map +1 -1
- package/dist/src/planner/validation/plan-validator.js +1 -0
- package/dist/src/planner/validation/plan-validator.js.map +1 -1
- package/dist/src/runtime/context-helpers.d.ts +13 -1
- package/dist/src/runtime/context-helpers.d.ts.map +1 -1
- package/dist/src/runtime/context-helpers.js +7 -1
- package/dist/src/runtime/context-helpers.js.map +1 -1
- package/dist/src/runtime/delta-executor.d.ts +30 -1
- package/dist/src/runtime/delta-executor.d.ts.map +1 -1
- package/dist/src/runtime/delta-executor.js +38 -4
- package/dist/src/runtime/delta-executor.js.map +1 -1
- package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
- package/dist/src/runtime/emit/add-constraint.js +38 -5
- package/dist/src/runtime/emit/add-constraint.js.map +1 -1
- package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
- package/dist/src/runtime/emit/aggregate.js +10 -8
- package/dist/src/runtime/emit/aggregate.js.map +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/alter-table.js +664 -108
- package/dist/src/runtime/emit/alter-table.js.map +1 -1
- package/dist/src/runtime/emit/analyze.d.ts.map +1 -1
- package/dist/src/runtime/emit/analyze.js +2 -1
- package/dist/src/runtime/emit/analyze.js.map +1 -1
- package/dist/src/runtime/emit/asof-scan.d.ts.map +1 -1
- package/dist/src/runtime/emit/asof-scan.js +24 -9
- package/dist/src/runtime/emit/asof-scan.js.map +1 -1
- package/dist/src/runtime/emit/asserted-keys.d.ts +13 -0
- package/dist/src/runtime/emit/asserted-keys.d.ts.map +1 -0
- package/dist/src/runtime/emit/asserted-keys.js +13 -0
- package/dist/src/runtime/emit/asserted-keys.js.map +1 -0
- package/dist/src/runtime/emit/between.d.ts.map +1 -1
- package/dist/src/runtime/emit/between.js +24 -19
- package/dist/src/runtime/emit/between.js.map +1 -1
- package/dist/src/runtime/emit/binary.d.ts.map +1 -1
- package/dist/src/runtime/emit/binary.js +24 -36
- package/dist/src/runtime/emit/binary.js.map +1 -1
- package/dist/src/runtime/emit/block.d.ts.map +1 -1
- package/dist/src/runtime/emit/block.js +11 -2
- package/dist/src/runtime/emit/block.js.map +1 -1
- package/dist/src/runtime/emit/bloom-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/bloom-join.js +12 -4
- package/dist/src/runtime/emit/bloom-join.js.map +1 -1
- package/dist/src/runtime/emit/constraint-check.d.ts.map +1 -1
- package/dist/src/runtime/emit/constraint-check.js +50 -1
- package/dist/src/runtime/emit/constraint-check.js.map +1 -1
- package/dist/src/runtime/emit/create-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-table.js +8 -0
- package/dist/src/runtime/emit/create-table.js.map +1 -1
- package/dist/src/runtime/emit/create-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-view.js +16 -1
- package/dist/src/runtime/emit/create-view.js.map +1 -1
- package/dist/src/runtime/emit/delete.d.ts.map +1 -1
- package/dist/src/runtime/emit/delete.js +15 -5
- package/dist/src/runtime/emit/delete.js.map +1 -1
- package/dist/src/runtime/emit/dml-executor.d.ts +27 -0
- package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
- package/dist/src/runtime/emit/dml-executor.js +413 -193
- package/dist/src/runtime/emit/dml-executor.js.map +1 -1
- package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-table.js +10 -0
- package/dist/src/runtime/emit/drop-table.js.map +1 -1
- package/dist/src/runtime/emit/drop-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-view.js +17 -0
- package/dist/src/runtime/emit/drop-view.js.map +1 -1
- package/dist/src/runtime/emit/envelope-scan.d.ts +13 -0
- package/dist/src/runtime/emit/envelope-scan.d.ts.map +1 -0
- package/dist/src/runtime/emit/envelope-scan.js +22 -0
- package/dist/src/runtime/emit/envelope-scan.js.map +1 -0
- package/dist/src/runtime/emit/join.d.ts +10 -2
- package/dist/src/runtime/emit/join.d.ts.map +1 -1
- package/dist/src/runtime/emit/join.js +128 -38
- package/dist/src/runtime/emit/join.js.map +1 -1
- package/dist/src/runtime/emit/lens-auxiliary-access.d.ts +16 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.js +16 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.js.map +1 -0
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts +640 -0
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -0
- package/dist/src/runtime/emit/materialized-view-helpers.js +2576 -0
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -0
- package/dist/src/runtime/emit/materialized-view.d.ts +31 -0
- package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -0
- package/dist/src/runtime/emit/materialized-view.js +187 -0
- package/dist/src/runtime/emit/materialized-view.js.map +1 -0
- package/dist/src/runtime/emit/merge-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/merge-join.js +19 -5
- package/dist/src/runtime/emit/merge-join.js.map +1 -1
- package/dist/src/runtime/emit/project.d.ts.map +1 -1
- package/dist/src/runtime/emit/project.js +10 -5
- package/dist/src/runtime/emit/project.js.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.d.ts +1 -0
- package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.js +101 -5
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/runtime/emit/set-object-tags.d.ts +16 -0
- package/dist/src/runtime/emit/set-object-tags.d.ts.map +1 -0
- package/dist/src/runtime/emit/set-object-tags.js +57 -0
- package/dist/src/runtime/emit/set-object-tags.js.map +1 -0
- package/dist/src/runtime/emit/set-operation.d.ts.map +1 -1
- package/dist/src/runtime/emit/set-operation.js +140 -24
- package/dist/src/runtime/emit/set-operation.js.map +1 -1
- package/dist/src/runtime/emit/subquery.d.ts.map +1 -1
- package/dist/src/runtime/emit/subquery.js +110 -5
- package/dist/src/runtime/emit/subquery.js.map +1 -1
- package/dist/src/runtime/emit/unary.d.ts.map +1 -1
- package/dist/src/runtime/emit/unary.js +34 -6
- package/dist/src/runtime/emit/unary.js.map +1 -1
- package/dist/src/runtime/emit/view-mutation.d.ts +70 -0
- package/dist/src/runtime/emit/view-mutation.d.ts.map +1 -0
- package/dist/src/runtime/emit/view-mutation.js +299 -0
- package/dist/src/runtime/emit/view-mutation.js.map +1 -0
- package/dist/src/runtime/emit/window.js +29 -5
- package/dist/src/runtime/emit/window.js.map +1 -1
- package/dist/src/runtime/foreign-key-actions.d.ts +66 -3
- package/dist/src/runtime/foreign-key-actions.d.ts.map +1 -1
- package/dist/src/runtime/foreign-key-actions.js +580 -172
- package/dist/src/runtime/foreign-key-actions.js.map +1 -1
- package/dist/src/runtime/parallel-driver.d.ts +4 -1
- package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
- package/dist/src/runtime/parallel-driver.js +5 -1
- package/dist/src/runtime/parallel-driver.js.map +1 -1
- package/dist/src/runtime/register.d.ts.map +1 -1
- package/dist/src/runtime/register.js +17 -1
- package/dist/src/runtime/register.js.map +1 -1
- package/dist/src/runtime/types.d.ts +10 -0
- package/dist/src/runtime/types.d.ts.map +1 -1
- package/dist/src/runtime/types.js.map +1 -1
- package/dist/src/schema/basis-backfill.d.ts +63 -0
- package/dist/src/schema/basis-backfill.d.ts.map +1 -0
- package/dist/src/schema/basis-backfill.js +161 -0
- package/dist/src/schema/basis-backfill.js.map +1 -0
- package/dist/src/schema/catalog.d.ts +115 -1
- package/dist/src/schema/catalog.d.ts.map +1 -1
- package/dist/src/schema/catalog.js +249 -22
- package/dist/src/schema/catalog.js.map +1 -1
- package/dist/src/schema/change-events.d.ts +42 -1
- package/dist/src/schema/change-events.d.ts.map +1 -1
- package/dist/src/schema/change-events.js.map +1 -1
- package/dist/src/schema/column.d.ts +16 -0
- package/dist/src/schema/column.d.ts.map +1 -1
- package/dist/src/schema/column.js.map +1 -1
- package/dist/src/schema/constraint-builder.d.ts +182 -0
- package/dist/src/schema/constraint-builder.d.ts.map +1 -0
- package/dist/src/schema/constraint-builder.js +424 -0
- package/dist/src/schema/constraint-builder.js.map +1 -0
- package/dist/src/schema/ddl-generator.d.ts +86 -1
- package/dist/src/schema/ddl-generator.d.ts.map +1 -1
- package/dist/src/schema/ddl-generator.js +316 -20
- package/dist/src/schema/ddl-generator.js.map +1 -1
- package/dist/src/schema/declared-schema-manager.d.ts +51 -0
- package/dist/src/schema/declared-schema-manager.d.ts.map +1 -1
- package/dist/src/schema/declared-schema-manager.js +61 -0
- package/dist/src/schema/declared-schema-manager.js.map +1 -1
- package/dist/src/schema/derivation.d.ts +106 -0
- package/dist/src/schema/derivation.d.ts.map +1 -0
- package/dist/src/schema/derivation.js +25 -0
- package/dist/src/schema/derivation.js.map +1 -0
- package/dist/src/schema/function.d.ts +13 -0
- package/dist/src/schema/function.d.ts.map +1 -1
- package/dist/src/schema/function.js.map +1 -1
- package/dist/src/schema/lens-ack.d.ts +90 -0
- package/dist/src/schema/lens-ack.d.ts.map +1 -0
- package/dist/src/schema/lens-ack.js +361 -0
- package/dist/src/schema/lens-ack.js.map +1 -0
- package/dist/src/schema/lens-compiler.d.ts +62 -0
- package/dist/src/schema/lens-compiler.d.ts.map +1 -0
- package/dist/src/schema/lens-compiler.js +1594 -0
- package/dist/src/schema/lens-compiler.js.map +1 -0
- package/dist/src/schema/lens-fk-discovery.d.ts +175 -0
- package/dist/src/schema/lens-fk-discovery.d.ts.map +1 -0
- package/dist/src/schema/lens-fk-discovery.js +336 -0
- package/dist/src/schema/lens-fk-discovery.js.map +1 -0
- package/dist/src/schema/lens-prover.d.ts +336 -0
- package/dist/src/schema/lens-prover.d.ts.map +1 -0
- package/dist/src/schema/lens-prover.js +1988 -0
- package/dist/src/schema/lens-prover.js.map +1 -0
- package/dist/src/schema/lens.d.ts +254 -0
- package/dist/src/schema/lens.d.ts.map +1 -0
- package/dist/src/schema/lens.js +21 -0
- package/dist/src/schema/lens.js.map +1 -0
- package/dist/src/schema/manager.d.ts +676 -18
- package/dist/src/schema/manager.d.ts.map +1 -1
- package/dist/src/schema/manager.js +1573 -238
- package/dist/src/schema/manager.js.map +1 -1
- package/dist/src/schema/mapping-advertisement-tags.d.ts +39 -0
- package/dist/src/schema/mapping-advertisement-tags.d.ts.map +1 -0
- package/dist/src/schema/mapping-advertisement-tags.js +216 -0
- package/dist/src/schema/mapping-advertisement-tags.js.map +1 -0
- package/dist/src/schema/rename-rewriter.d.ts +45 -4
- package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
- package/dist/src/schema/rename-rewriter.js +412 -19
- package/dist/src/schema/rename-rewriter.js.map +1 -1
- package/dist/src/schema/reserved-tags-policy.d.ts +32 -0
- package/dist/src/schema/reserved-tags-policy.d.ts.map +1 -0
- package/dist/src/schema/reserved-tags-policy.js +34 -0
- package/dist/src/schema/reserved-tags-policy.js.map +1 -0
- package/dist/src/schema/reserved-tags.d.ts +170 -0
- package/dist/src/schema/reserved-tags.d.ts.map +1 -0
- package/dist/src/schema/reserved-tags.js +507 -0
- package/dist/src/schema/reserved-tags.js.map +1 -0
- package/dist/src/schema/schema-differ.d.ts +158 -2
- package/dist/src/schema/schema-differ.d.ts.map +1 -1
- package/dist/src/schema/schema-differ.js +1460 -78
- package/dist/src/schema/schema-differ.js.map +1 -1
- package/dist/src/schema/schema-hasher.d.ts +8 -3
- package/dist/src/schema/schema-hasher.d.ts.map +1 -1
- package/dist/src/schema/schema-hasher.js +22 -2
- package/dist/src/schema/schema-hasher.js.map +1 -1
- package/dist/src/schema/schema.d.ts +25 -1
- package/dist/src/schema/schema.d.ts.map +1 -1
- package/dist/src/schema/schema.js +36 -2
- package/dist/src/schema/schema.js.map +1 -1
- package/dist/src/schema/table.d.ts +259 -10
- package/dist/src/schema/table.d.ts.map +1 -1
- package/dist/src/schema/table.js +309 -26
- package/dist/src/schema/table.js.map +1 -1
- package/dist/src/schema/unique-enforcement.d.ts +78 -0
- package/dist/src/schema/unique-enforcement.d.ts.map +1 -0
- package/dist/src/schema/unique-enforcement.js +93 -0
- package/dist/src/schema/unique-enforcement.js.map +1 -0
- package/dist/src/schema/view.d.ts +83 -2
- package/dist/src/schema/view.d.ts.map +1 -1
- package/dist/src/schema/view.js +67 -1
- package/dist/src/schema/view.js.map +1 -1
- package/dist/src/schema/window-function.d.ts +9 -1
- package/dist/src/schema/window-function.d.ts.map +1 -1
- package/dist/src/schema/window-function.js.map +1 -1
- package/dist/src/types/temporal-types.d.ts.map +1 -1
- package/dist/src/types/temporal-types.js +71 -36
- package/dist/src/types/temporal-types.js.map +1 -1
- package/dist/src/util/comparison.d.ts +24 -0
- package/dist/src/util/comparison.d.ts.map +1 -1
- package/dist/src/util/comparison.js +34 -0
- package/dist/src/util/comparison.js.map +1 -1
- package/dist/src/util/mutation-statement.d.ts.map +1 -1
- package/dist/src/util/mutation-statement.js +4 -1
- package/dist/src/util/mutation-statement.js.map +1 -1
- package/dist/src/util/serialization.d.ts +9 -0
- package/dist/src/util/serialization.d.ts.map +1 -1
- package/dist/src/util/serialization.js +26 -0
- package/dist/src/util/serialization.js.map +1 -1
- package/dist/src/vtab/backing-host.d.ts +286 -0
- package/dist/src/vtab/backing-host.d.ts.map +1 -0
- package/dist/src/vtab/backing-host.js +118 -0
- package/dist/src/vtab/backing-host.js.map +1 -0
- package/dist/src/vtab/best-access-plan.d.ts +21 -0
- package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
- package/dist/src/vtab/best-access-plan.js.map +1 -1
- package/dist/src/vtab/capabilities.d.ts +5 -5
- package/dist/src/vtab/capabilities.d.ts.map +1 -1
- package/dist/src/vtab/mapping-advertisement.d.ts +163 -0
- package/dist/src/vtab/mapping-advertisement.d.ts.map +1 -0
- package/dist/src/vtab/mapping-advertisement.js +2 -0
- package/dist/src/vtab/mapping-advertisement.js.map +1 -0
- package/dist/src/vtab/memory/index.d.ts +64 -4
- package/dist/src/vtab/memory/index.d.ts.map +1 -1
- package/dist/src/vtab/memory/index.js +119 -12
- package/dist/src/vtab/memory/index.js.map +1 -1
- package/dist/src/vtab/memory/layer/base.d.ts +38 -1
- package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/base.js +112 -24
- package/dist/src/vtab/memory/layer/base.js.map +1 -1
- package/dist/src/vtab/memory/layer/manager.d.ts +291 -4
- package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.js +1050 -91
- package/dist/src/vtab/memory/layer/manager.js.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.js +35 -6
- package/dist/src/vtab/memory/layer/plan-filter.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.js +66 -14
- package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.d.ts +14 -0
- package/dist/src/vtab/memory/layer/scan-plan.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.js +27 -4
- package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.js +5 -1
- package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
- package/dist/src/vtab/memory/module.d.ts +17 -0
- package/dist/src/vtab/memory/module.d.ts.map +1 -1
- package/dist/src/vtab/memory/module.js +82 -3
- package/dist/src/vtab/memory/module.js.map +1 -1
- package/dist/src/vtab/memory/table.d.ts.map +1 -1
- package/dist/src/vtab/memory/table.js +15 -5
- package/dist/src/vtab/memory/table.js.map +1 -1
- package/dist/src/vtab/memory/types.d.ts +20 -2
- package/dist/src/vtab/memory/types.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.js +46 -24
- package/dist/src/vtab/memory/utils/predicate.js.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key-encode.d.ts +31 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.d.ts.map +1 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.js +101 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.js.map +1 -0
- package/dist/src/vtab/memory/utils/primary-key.d.ts +8 -0
- package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key.js +12 -5
- package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
- package/dist/src/vtab/module.d.ts +203 -4
- package/dist/src/vtab/module.d.ts.map +1 -1
- package/dist/src/vtab/table.d.ts +9 -0
- package/dist/src/vtab/table.d.ts.map +1 -1
- package/dist/src/vtab/table.js.map +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,2576 @@
|
|
|
1
|
+
import { QuereusError } from '../../common/errors.js';
|
|
2
|
+
import { StatusCode } from '../../common/types.js';
|
|
3
|
+
import { astToString, expressionToString, viewDefinitionToCanonicalString } from '../../emit/ast-stringify.js';
|
|
4
|
+
import { TableReferenceNode, ColumnReferenceNode } from '../../planner/nodes/reference.js';
|
|
5
|
+
import { Parser } from '../../parser/parser.js';
|
|
6
|
+
import { keysOf } from '../../planner/util/fd-utils.js';
|
|
7
|
+
import { proveCoverage } from '../../planner/analysis/coverage-prover.js';
|
|
8
|
+
import { deriveCoarsenedBackingKey } from '../../planner/analysis/coarsened-key.js';
|
|
9
|
+
import { buildColumnIndexMap, requireVtabModule, RowOpFlag } from '../../schema/table.js';
|
|
10
|
+
import { validateChecksOverExistingRows, validateForeignKeyOverExistingRows, maintainedTableCheckViolationError, maintainedTableFkViolationError, formatKeyValue, } from '../../schema/constraint-builder.js';
|
|
11
|
+
import { computeBodyHash } from '../../schema/view.js';
|
|
12
|
+
import { isMaintainedTable } from '../../schema/derivation.js';
|
|
13
|
+
import { renameTableInAst, renameColumnInAst } from '../../schema/rename-rewriter.js';
|
|
14
|
+
import { createLogger } from '../../common/logger.js';
|
|
15
|
+
import { compareSqlValues } from '../../util/comparison.js';
|
|
16
|
+
const log = createLogger('runtime:emit:materialized-view');
|
|
17
|
+
const warnLog = log.extend('warn');
|
|
18
|
+
// Canonical body-hash lives next to the MV schema definition so the declarative
|
|
19
|
+
// differ can share it without depending on the runtime layer. Re-exported here
|
|
20
|
+
// for the create/refresh emitters that already import from this module.
|
|
21
|
+
export { computeBodyHash };
|
|
22
|
+
/**
|
|
23
|
+
* Purpose-built diagnostic for a bag (duplicate-producing) materialized-view
|
|
24
|
+
* body. A v1 materialized view is a *keyed* derived relation: its body must
|
|
25
|
+
* produce a **set** (no duplicate rows under the backing-table key). This
|
|
26
|
+
* replaces the raw `UNIQUE constraint failed: <backing table> PK` message —
|
|
27
|
+
* which named a hidden implementation detail — with one that names the MV and
|
|
28
|
+
* explains the contract. Raised at create (loud, immediate) or at the next
|
|
29
|
+
* refresh if a duplicate-free body later becomes duplicate-producing.
|
|
30
|
+
*/
|
|
31
|
+
export function materializedViewNotASetError(schemaName, viewName) {
|
|
32
|
+
return new QuereusError(`materialized view '${schemaName}.${viewName}' body produces duplicate rows, `
|
|
33
|
+
+ `but a materialized view must be a set: its body needs a unique key. `
|
|
34
|
+
+ `Project the source's primary-key column(s) so every row is unique; for a `
|
|
35
|
+
+ `non-keyed result use a plain \`create view\` (live re-evaluation) or `
|
|
36
|
+
+ `\`create table ... as <body>\` (a one-off snapshot).`, StatusCode.CONSTRAINT);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Builds + optimizes the materialized-view body and derives the backing table's
|
|
40
|
+
* column list, primary key, body ordering, and source-table dependencies.
|
|
41
|
+
*
|
|
42
|
+
* Columns and types come straight from the optimized relation's
|
|
43
|
+
* {@link RelationalPlanNode.getType}; the PK is the first usable key from
|
|
44
|
+
* `keysOf` (all-columns fallback when none — such an MV is incremental-ineligible
|
|
45
|
+
* until Phase 2). Re-planning here is cheap relative to materialization and keeps
|
|
46
|
+
* the create/refresh emitters free of optimizer plumbing.
|
|
47
|
+
*/
|
|
48
|
+
export function deriveBackingShape(db, bodySql, explicitColumns) {
|
|
49
|
+
// Suppress the read-side rewrite: we are computing the MV body to derive/populate
|
|
50
|
+
// its OWN backing, so it must not be rewritten to read that backing.
|
|
51
|
+
return db.schemaManager.withSuppressedMaterializedViewRewrite(() => deriveBackingShapeUnguarded(db, bodySql, explicitColumns));
|
|
52
|
+
}
|
|
53
|
+
function deriveBackingShapeUnguarded(db, bodySql, explicitColumns) {
|
|
54
|
+
const plan = db.getPlan(bodySql);
|
|
55
|
+
const root = plan.getRelations()[0];
|
|
56
|
+
if (!root) {
|
|
57
|
+
throw new QuereusError('materialized view body produced no relation', StatusCode.INTERNAL);
|
|
58
|
+
}
|
|
59
|
+
const relType = root.getType();
|
|
60
|
+
const bodyColumns = relType.columns;
|
|
61
|
+
const names = explicitColumns && explicitColumns.length > 0
|
|
62
|
+
? explicitColumns
|
|
63
|
+
: bodyColumns.map((c, i) => c.name || `col${i}`);
|
|
64
|
+
const columns = bodyColumns.map((c, i) => {
|
|
65
|
+
const col = {
|
|
66
|
+
name: names[i] ?? `col${i}`,
|
|
67
|
+
logicalType: c.type.logicalType,
|
|
68
|
+
notNull: c.type.nullable === false,
|
|
69
|
+
primaryKey: false,
|
|
70
|
+
pkOrder: 0,
|
|
71
|
+
defaultValue: null,
|
|
72
|
+
collation: c.type.collationName ?? 'BINARY',
|
|
73
|
+
generated: false,
|
|
74
|
+
};
|
|
75
|
+
// Thread the output collation's PROVENANCE into backing-column explicitness:
|
|
76
|
+
// a deliberately-collated output column (an explicit `COLLATE`, or a column
|
|
77
|
+
// whose declared collation flows through unchanged) publishes an EXPLICIT
|
|
78
|
+
// backing collation, so the store module's PK-collation reconcile keeps the
|
|
79
|
+
// backing text PK under the published collation instead of re-keying it under
|
|
80
|
+
// the store default (NOCASE). A 'default'/absent source stays implicit (field
|
|
81
|
+
// left unset — matching ColumnSchema's "absent ⇒ implicit" contract), so a
|
|
82
|
+
// genuinely-implicit MV column preserves the historical store-default keying.
|
|
83
|
+
if (c.type.collationSource === 'explicit' || c.type.collationSource === 'declared') {
|
|
84
|
+
col.collationExplicit = true;
|
|
85
|
+
}
|
|
86
|
+
return col;
|
|
87
|
+
});
|
|
88
|
+
// First usable key from the unified surface. A keyless body is then offered the
|
|
89
|
+
// coarsened lineage key (the parallel-migration shape — see coarsened-key.ts):
|
|
90
|
+
// the projected source key, keyed under the OUTPUT collations, so create-fill
|
|
91
|
+
// rejects collisions loudly and steady-state maintenance merges them LWW. The
|
|
92
|
+
// all-columns fallback remains for bodies with neither (rejected at
|
|
93
|
+
// registration as a bag, exactly as before).
|
|
94
|
+
const keys = keysOf(root);
|
|
95
|
+
let pkIndices;
|
|
96
|
+
let coarsenedKey;
|
|
97
|
+
if (keys.length > 0) {
|
|
98
|
+
pkIndices = [...keys[0]];
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const lineageKey = deriveCoarsenedBackingKey(root);
|
|
102
|
+
if (lineageKey) {
|
|
103
|
+
pkIndices = [...lineageKey.keyIndices];
|
|
104
|
+
// Only a genuinely COARSENING key carries the warning payload; an
|
|
105
|
+
// equal/refining lineage key is a true unique key accepted silently.
|
|
106
|
+
if (lineageKey.coarsens)
|
|
107
|
+
coarsenedKey = buildCoarsenedKeyInfo(lineageKey, columns);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
pkIndices = columns.map((_c, i) => i);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const primaryKey = pkIndices.map(idx => ({ index: idx, desc: false }));
|
|
114
|
+
// A COARSENING key must be the backing's physical key EXACTLY: the loud
|
|
115
|
+
// create-fill and the LWW merge both rest on the backing btree equating
|
|
116
|
+
// colliding source keys, and the ordering-seeded physical PK
|
|
117
|
+
// (computeBackingPrimaryKey leads with the body's `order by` columns) would
|
|
118
|
+
// widen uniqueness past K' — colliding siblings would then coexist silently,
|
|
119
|
+
// defeating both. So drop the ordering seed for a coarsened key; the only
|
|
120
|
+
// cost is the clustering optimization (`mv.ordering` is informational). A
|
|
121
|
+
// non-coarsening lineage key is a true key, so the seed stays uniqueness-
|
|
122
|
+
// preserving there, exactly as for a `keysOf`-proved key.
|
|
123
|
+
const ordering = coarsenedKey
|
|
124
|
+
? undefined
|
|
125
|
+
: root.physical?.ordering?.map(o => ({ index: o.column, desc: o.desc }));
|
|
126
|
+
return {
|
|
127
|
+
columns,
|
|
128
|
+
primaryKey,
|
|
129
|
+
ordering: ordering && ordering.length > 0 ? ordering : undefined,
|
|
130
|
+
sourceTables: collectSourceTables(plan),
|
|
131
|
+
coarsenedKey,
|
|
132
|
+
allProvedKeys: keys.length > 0 ? keys.map(k => Array.from(k)) : undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Lift the structural {@link CoarsenedBackingKey} into the named, record-facing
|
|
136
|
+
* {@link CoarsenedKeyInfo} (backing column names instead of indices). */
|
|
137
|
+
function buildCoarsenedKeyInfo(key, columns) {
|
|
138
|
+
const nameOf = (idx) => columns[idx]?.name ?? `col${idx}`;
|
|
139
|
+
return {
|
|
140
|
+
columns: key.keyIndices.map(nameOf),
|
|
141
|
+
weakened: key.columns
|
|
142
|
+
.filter(c => c.coarsens)
|
|
143
|
+
.map(c => ({
|
|
144
|
+
column: nameOf(c.outputIndex),
|
|
145
|
+
sourceCollation: c.sourceCollation,
|
|
146
|
+
outputCollation: c.outputCollation,
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Walks the plan collecting qualified (lowercased) names of every base table referenced. */
|
|
151
|
+
function collectSourceTables(plan) {
|
|
152
|
+
const out = new Set();
|
|
153
|
+
const visited = new Set();
|
|
154
|
+
const walk = (node) => {
|
|
155
|
+
if (visited.has(node))
|
|
156
|
+
return;
|
|
157
|
+
visited.add(node);
|
|
158
|
+
if (node instanceof TableReferenceNode) {
|
|
159
|
+
out.add(`${node.tableSchema.schemaName}.${node.tableSchema.name}`.toLowerCase());
|
|
160
|
+
}
|
|
161
|
+
for (const c of node.getChildren())
|
|
162
|
+
walk(c);
|
|
163
|
+
for (const r of node.getRelations())
|
|
164
|
+
walk(r);
|
|
165
|
+
};
|
|
166
|
+
walk(plan);
|
|
167
|
+
return [...out];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Computes the backing table's *physical* primary key. When the body carries an
|
|
171
|
+
* `order by`, the ordering columns lead the key so the btree clusters (and scans)
|
|
172
|
+
* in the body's order — "seeding the backing-table ordering" — with the logical
|
|
173
|
+
* key (from `keysOf`) appended as a uniqueness-preserving tiebreaker. Without an
|
|
174
|
+
* `order by`, the physical key is just the logical key.
|
|
175
|
+
*
|
|
176
|
+
* NOTE: this diverges from `TableDerivation.logicalKey`, which keeps the
|
|
177
|
+
* logical `keysOf` identity. The covering ticket replaces this seeding with a
|
|
178
|
+
* proper materialized index.
|
|
179
|
+
*/
|
|
180
|
+
export function computeBackingPrimaryKey(shape) {
|
|
181
|
+
if (!shape.ordering || shape.ordering.length === 0) {
|
|
182
|
+
return shape.primaryKey;
|
|
183
|
+
}
|
|
184
|
+
const seeded = [];
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
for (const o of shape.ordering) {
|
|
187
|
+
if (!seen.has(o.index)) {
|
|
188
|
+
seeded.push({ index: o.index, desc: o.desc });
|
|
189
|
+
seen.add(o.index);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const k of shape.primaryKey) {
|
|
193
|
+
if (!seen.has(k.index)) {
|
|
194
|
+
seeded.push({ index: k.index, desc: k.desc });
|
|
195
|
+
seen.add(k.index);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return seeded.length > 0 ? seeded : shape.primaryKey;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Constructs the backing-table {@link TableSchema} for a materialized view from a
|
|
202
|
+
* derived {@link BackingShape}, hosted in `moduleName` (default `'memory'`).
|
|
203
|
+
* The capability check here is defense-in-depth — the create builder already
|
|
204
|
+
* gates, but the catalog-import path reaches this without it.
|
|
205
|
+
*/
|
|
206
|
+
export function buildBackingTableSchema(db, schemaName, backingTableName, shape, moduleName, moduleArgs,
|
|
207
|
+
/** Table-level metadata tags (the MV's `with tags (…)` — top-level on the unified record). */
|
|
208
|
+
tags) {
|
|
209
|
+
const resolvedModuleName = moduleName ?? 'memory';
|
|
210
|
+
const moduleInfo = db.schemaManager.getModule(resolvedModuleName);
|
|
211
|
+
if (!moduleInfo || !moduleInfo.module) {
|
|
212
|
+
throw new QuereusError(`no virtual table module named '${resolvedModuleName}'`, StatusCode.ERROR);
|
|
213
|
+
}
|
|
214
|
+
if (!moduleInfo.module.getBackingHost) {
|
|
215
|
+
throw new QuereusError(`module '${resolvedModuleName}' cannot host a materialized-view backing table (it does not implement the backing-host capability)`, StatusCode.UNSUPPORTED);
|
|
216
|
+
}
|
|
217
|
+
const backingPk = computeBackingPrimaryKey(shape);
|
|
218
|
+
const pkDefinition = backingPk.map(pk => ({
|
|
219
|
+
index: pk.index,
|
|
220
|
+
desc: pk.desc,
|
|
221
|
+
collation: shape.columns[pk.index]?.collation,
|
|
222
|
+
}));
|
|
223
|
+
// Reflect the physical PK in the column flags (cosmetic; the memory table reads
|
|
224
|
+
// `primaryKeyDefinition`, but catalog/introspection consults column flags).
|
|
225
|
+
backingPk.forEach((pk, order) => {
|
|
226
|
+
const col = shape.columns[pk.index];
|
|
227
|
+
if (col) {
|
|
228
|
+
col.primaryKey = true;
|
|
229
|
+
col.pkOrder = order + 1;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
name: backingTableName,
|
|
234
|
+
schemaName,
|
|
235
|
+
columns: Object.freeze(shape.columns),
|
|
236
|
+
columnIndexMap: buildColumnIndexMap(shape.columns),
|
|
237
|
+
primaryKeyDefinition: Object.freeze(pkDefinition),
|
|
238
|
+
checkConstraints: Object.freeze([]),
|
|
239
|
+
vtabModule: moduleInfo.module,
|
|
240
|
+
vtabModuleName: resolvedModuleName,
|
|
241
|
+
vtabArgs: moduleArgs ? { ...moduleArgs } : {},
|
|
242
|
+
vtabAuxData: moduleInfo.auxData,
|
|
243
|
+
isView: false,
|
|
244
|
+
estimatedRows: 0,
|
|
245
|
+
tags: tags && Object.keys(tags).length > 0 ? tags : undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** Runs the body to completion and returns its rows (raw `Row` arrays). Uses the
|
|
249
|
+
* no-transaction-management primitive — the caller is already inside DDL execution. */
|
|
250
|
+
export async function collectBodyRows(db, bodySql) {
|
|
251
|
+
// Suppress the read-side rewrite for the whole prepare+iterate: this body is run
|
|
252
|
+
// to (re)compute the MV's OWN backing (create fill / refresh rebuild), so it must
|
|
253
|
+
// recompute from the source, never read the backing it is populating.
|
|
254
|
+
return db.schemaManager.withSuppressedMaterializedViewRewriteAsync(async () => {
|
|
255
|
+
const stmt = db.prepare(bodySql);
|
|
256
|
+
try {
|
|
257
|
+
const rows = [];
|
|
258
|
+
for await (const row of stmt._iterateRowsRaw()) {
|
|
259
|
+
rows.push(row);
|
|
260
|
+
}
|
|
261
|
+
return rows;
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await stmt.finalize();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Throws the sited declared-column-arity diagnostic when `def`'s explicit column
|
|
270
|
+
* list disagrees with the body's output arity. Build-time creation already
|
|
271
|
+
* validated this (with a build-located diagnostic); this guards the import path —
|
|
272
|
+
* both the refill arm ({@link materializeView}) and the adopt gate check
|
|
273
|
+
* (`SchemaManager.tryAdoptPreExistingBacking`, which must raise it BEFORE the
|
|
274
|
+
* caller drops a durable backing: the entry can never materialize, so dropping
|
|
275
|
+
* would destroy rows for nothing). The refresh path deliberately does NOT share
|
|
276
|
+
* this — it reaches a legitimate mismatch after a source ALTER and has its own
|
|
277
|
+
* "drop and recreate" diagnostic.
|
|
278
|
+
*/
|
|
279
|
+
export function assertDeclaredColumnArity(def, shape) {
|
|
280
|
+
if (def.columns && def.columns.length > 0 && def.columns.length !== shape.columns.length) {
|
|
281
|
+
throw new QuereusError(`materialized view '${def.schemaName}.${def.viewName}' has ${def.columns.length} declared columns but body produces ${shape.columns.length}`, StatusCode.ERROR);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Builds the {@link TableDerivation} record for `def` over the derived
|
|
286
|
+
* `shape` — the single record formula shared by {@link materializeView} (refill)
|
|
287
|
+
* and {@link adoptMaterializedView} (adopt), so the two paths cannot drift: an
|
|
288
|
+
* adopted and a refilled maintained table are indistinguishable (fixed point:
|
|
289
|
+
* export DDL after adopt == after refill).
|
|
290
|
+
*
|
|
291
|
+
* `bodyHash` hashes the canonical DEFINITION (explicit columns + body — the body
|
|
292
|
+
* string carries any trailing `with defaults (…)` clause), NOT the executable
|
|
293
|
+
* bodySql — the declarative differ recomputes the same form from a declared MV,
|
|
294
|
+
* so a defaults-only or explicit-columns-only change is detected as drift.
|
|
295
|
+
* `def.bodySql` is the full body render (it carries the inert trailing
|
|
296
|
+
* `with defaults (…)` clause, which the read planner ignores — defaults are
|
|
297
|
+
* realized only in the view write-through rewrite): it feeds execution
|
|
298
|
+
* (collectBodyRows / deriveBackingShape / linkCoveredUniqueConstraints).
|
|
299
|
+
*/
|
|
300
|
+
function buildTableDerivation(def, shape) {
|
|
301
|
+
return {
|
|
302
|
+
selectAst: def.selectAst,
|
|
303
|
+
columns: def.columns,
|
|
304
|
+
logicalKey: shape.primaryKey,
|
|
305
|
+
coarsenedKey: shape.coarsenedKey,
|
|
306
|
+
bodyHash: computeBodyHash(viewDefinitionToCanonicalString(def.columns, def.selectAst)),
|
|
307
|
+
ordering: shape.ordering,
|
|
308
|
+
sourceTables: shape.sourceTables,
|
|
309
|
+
stale: false,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Rejects a body that references the maintained table being created. The
|
|
314
|
+
* unified model makes self-reference *lexically* possible mid-create (the
|
|
315
|
+
* table registers under the MV's own name before the fill runs), so the
|
|
316
|
+
* create/import paths reject it up front — a self-referential derivation can
|
|
317
|
+
* never be maintained coherently.
|
|
318
|
+
*/
|
|
319
|
+
function assertNoSelfReference(def, shape) {
|
|
320
|
+
const self = `${def.schemaName}.${def.viewName}`.toLowerCase();
|
|
321
|
+
if (shape.sourceTables.includes(self)) {
|
|
322
|
+
throw new QuereusError(`materialized view '${def.schemaName}.${def.viewName}' body may not reference the view itself`, StatusCode.ERROR);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The key-coarsening warning `docs/migration.md` § Convergence hazards
|
|
327
|
+
* specifies — emitted (structured logger, `warn` channel) when an MV
|
|
328
|
+
* materializes over a coarsened backing key, with `TableDerivation.coarsenedKey`
|
|
329
|
+
* as the record-side complement. Warn, don't reject: the merge-on-coarsen
|
|
330
|
+
* behavior is often exactly what the migration intends.
|
|
331
|
+
*/
|
|
332
|
+
function warnKeyCoarsening(schemaName, viewName, info) {
|
|
333
|
+
const detail = info.weakened
|
|
334
|
+
.map(w => `${w.column}: collation ${w.sourceCollation} → ${w.outputCollation}`)
|
|
335
|
+
.join(', ');
|
|
336
|
+
warnLog(`materialized view '%s.%s': backing key (%s) is coarser than the source primary key (%s); `
|
|
337
|
+
+ `colliding source rows will last-write-win until they are merged`, schemaName, viewName, info.columns.join(', '), detail);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The materialize core shared by `emitCreateMaterializedView` and the
|
|
341
|
+
* catalog-import path (`SchemaManager.importMaterializedView`): derive the
|
|
342
|
+
* backing shape from the planned body → create the maintained table under the
|
|
343
|
+
* MV's own name in the declared backing-host module (memory default) → fill it
|
|
344
|
+
* from the body → attach the {@link TableDerivation} → compile + register
|
|
345
|
+
* row-time write-through maintenance. Returns the registered maintained table.
|
|
346
|
+
*
|
|
347
|
+
* Fires `table_added` for the table (it is created like any table) but
|
|
348
|
+
* deliberately does NOT fire `materialized_view_added` — the create emitter
|
|
349
|
+
* notifies after this returns, while import stays silent (a store rehydrating
|
|
350
|
+
* its own catalog must not re-emit persistence events).
|
|
351
|
+
*
|
|
352
|
+
* Rollback-on-throw: a fill failure (including the "must be a set"
|
|
353
|
+
* duplicate-key gate) drops the half-built table; a registration failure (the
|
|
354
|
+
* mandatory row-time eligibility gate runs there) drops the table — derivation
|
|
355
|
+
* and all — either way the schema is left exactly as before the call.
|
|
356
|
+
* Existence/collision checks are the caller's job (the create emitter checks
|
|
357
|
+
* before calling; on import a duplicate surfaces as a table-name conflict).
|
|
358
|
+
*
|
|
359
|
+
* `preDerivedShape` short-circuits the shape derivation for a caller that
|
|
360
|
+
* already planned the body (the import path derives it once for its gates).
|
|
361
|
+
*/
|
|
362
|
+
export async function materializeView(db, def, preDerivedShape) {
|
|
363
|
+
const sm = db.schemaManager;
|
|
364
|
+
const shape = preDerivedShape ?? deriveBackingShape(db, def.bodySql, def.columns);
|
|
365
|
+
// Lives here — not in deriveBackingShape — because the refresh path reaches a
|
|
366
|
+
// legitimate mismatch after a source ALTER (see the assert's docstring).
|
|
367
|
+
assertDeclaredColumnArity(def, shape);
|
|
368
|
+
// The table registers under the MV's own name BEFORE the fill runs, so a
|
|
369
|
+
// self-referential body must be rejected up front (it would otherwise read
|
|
370
|
+
// the empty table being populated).
|
|
371
|
+
assertNoSelfReference(def, shape);
|
|
372
|
+
const backingSchema = buildBackingTableSchema(db, def.schemaName, def.viewName, shape, def.backingModuleName, def.backingModuleArgs, def.tags);
|
|
373
|
+
const completeBacking = await sm.createBackingTable(backingSchema);
|
|
374
|
+
try {
|
|
375
|
+
const rows = await collectBodyRows(db, def.bodySql);
|
|
376
|
+
const host = resolveBackingHost(db, completeBacking);
|
|
377
|
+
// `replaceContents` runs NO derived-row constraint validation: this caller's
|
|
378
|
+
// backing is the MV-sugar shape (`buildBackingTableSchema` hard-codes empty
|
|
379
|
+
// checkConstraints and carries no foreignKeys), so there is nothing to
|
|
380
|
+
// validate. The constraint-bearing refresh path that DOES need validation
|
|
381
|
+
// over a `replaceContents`-style whole-set swap runs it in `rebuildBacking`
|
|
382
|
+
// (pending-layer `replace-all` + `validateDeclaredConstraintsOverContents`),
|
|
383
|
+
// not here.
|
|
384
|
+
await host.replaceContents(rows, () => materializedViewNotASetError(def.schemaName, def.viewName));
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
// Roll back: drop the table, do not attach a derivation.
|
|
388
|
+
try {
|
|
389
|
+
await sm.dropTable(def.schemaName, def.viewName, /*ifExists*/ true);
|
|
390
|
+
}
|
|
391
|
+
catch { /* best-effort cleanup */ }
|
|
392
|
+
throw e;
|
|
393
|
+
}
|
|
394
|
+
const maintained = sm.attachDerivation(def.schemaName, def.viewName, buildTableDerivation(def, shape));
|
|
395
|
+
// Eagerly record the constraint↔structure link if this MV covers a UNIQUE
|
|
396
|
+
// constraint (informational — enforcement still routes through the
|
|
397
|
+
// synchronously-maintained auto-index).
|
|
398
|
+
linkCoveredUniqueConstraints(db, maintained, def.bodySql);
|
|
399
|
+
// Compile + register row-time write-through maintenance. The mandatory
|
|
400
|
+
// eligibility gate runs here (it needs the analyzed body) and throws on a
|
|
401
|
+
// body that is not row-time maintainable — roll the whole MV back so an
|
|
402
|
+
// ineligible body errors cleanly.
|
|
403
|
+
try {
|
|
404
|
+
db.registerMaterializedView(maintained);
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
unlinkCoveredUniqueConstraints(db, maintained);
|
|
408
|
+
try {
|
|
409
|
+
await sm.dropTable(def.schemaName, def.viewName, /*ifExists*/ true);
|
|
410
|
+
}
|
|
411
|
+
catch { /* best-effort cleanup */ }
|
|
412
|
+
throw e;
|
|
413
|
+
}
|
|
414
|
+
// After the MV fully materialized (a fill/registration failure must error, not
|
|
415
|
+
// warn): surface the key-coarsening hazard the coarsened backing key carries.
|
|
416
|
+
if (maintained.derivation.coarsenedKey) {
|
|
417
|
+
warnKeyCoarsening(def.schemaName, def.viewName, maintained.derivation.coarsenedKey);
|
|
418
|
+
}
|
|
419
|
+
return maintained;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The adopt-without-refill counterpart of {@link materializeView}: the
|
|
423
|
+
* registration tail without create+fill, for the catalog-import path
|
|
424
|
+
* (`SchemaManager.importMaterializedView`) when a pre-existing durable backing
|
|
425
|
+
* passed every adopt gate (same module, shape match, all sources same-module
|
|
426
|
+
* with upstream maintained tables themselves adopted, caller-attested
|
|
427
|
+
* `trustBackings`). The table's rows are trusted as-is — no body execution.
|
|
428
|
+
*
|
|
429
|
+
* **Backing schema re-stamp.** `preExisting` is a phase-1 DDL round-trip and
|
|
430
|
+
* loses ScalarType fidelity the refill path would carry (the registry-interned
|
|
431
|
+
* logical types survive only by name in DDL). Re-registering the body-derived
|
|
432
|
+
* {@link buildBackingTableSchema} result — shape-verified identical by the
|
|
433
|
+
* caller's `backingShapeMatches` gate — makes post-adopt state equivalent to
|
|
434
|
+
* post-refill state for the row-time plan `registerMaterializedView` binds.
|
|
435
|
+
* Module identity/args come from `def` exactly as the refill path's
|
|
436
|
+
* `buildBackingTableSchema` call does (gate 1 verified the registered module
|
|
437
|
+
* matches); `estimatedRows` carries over from the registered schema (the rows
|
|
438
|
+
* are preserved, so the prior estimate stays truthful). The module-side LIVE
|
|
439
|
+
* table instance still caches the phase-1 schema — the importing host
|
|
440
|
+
* reconciles it after import (the store module's `rehydrateCatalog` runs
|
|
441
|
+
* `StoreTable.updateSchema` over every connected table); reads are unaffected
|
|
442
|
+
* either way since the shapes are identical.
|
|
443
|
+
*
|
|
444
|
+
* Rollback on a registration failure (the mandatory row-time eligibility gate
|
|
445
|
+
* runs there): unlink + detach the derivation + rethrow — the table stays
|
|
446
|
+
* REGISTERED, reverting to its plain (derivation-less) state. Dropping a
|
|
447
|
+
* durable backing on a registration error would destroy the very rows a later
|
|
448
|
+
* retry could adopt; the caller records the throw as a per-entry rehydration
|
|
449
|
+
* error.
|
|
450
|
+
*/
|
|
451
|
+
export async function adoptMaterializedView(db, def, preExisting, shape) {
|
|
452
|
+
const sm = db.schemaManager;
|
|
453
|
+
const schema = sm.getSchemaOrFail(def.schemaName);
|
|
454
|
+
assertNoSelfReference(def, shape);
|
|
455
|
+
const stamped = buildBackingTableSchema(db, def.schemaName, def.viewName, shape, def.backingModuleName, def.backingModuleArgs, def.tags);
|
|
456
|
+
schema.addTable({ ...stamped, estimatedRows: preExisting.estimatedRows ?? 0 });
|
|
457
|
+
const maintained = sm.attachDerivation(def.schemaName, def.viewName, buildTableDerivation(def, shape));
|
|
458
|
+
linkCoveredUniqueConstraints(db, maintained, def.bodySql);
|
|
459
|
+
try {
|
|
460
|
+
db.registerMaterializedView(maintained);
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
unlinkCoveredUniqueConstraints(db, maintained);
|
|
464
|
+
// Detach the derivation: the table reverts to a plain table (re-stamped
|
|
465
|
+
// schema is shape-identical to its phase-1 state) — deliberately NOT dropped.
|
|
466
|
+
const { derivation: _derivation, ...plain } = maintained;
|
|
467
|
+
schema.addTable(plain);
|
|
468
|
+
throw e;
|
|
469
|
+
}
|
|
470
|
+
return maintained;
|
|
471
|
+
}
|
|
472
|
+
/* ──────────────── attach / detach lifecycle verbs ────────────────
|
|
473
|
+
* The maintained-table lifecycle verbs: `create table … maintained as <body>`
|
|
474
|
+
* (attach-to-empty), `alter table … set maintained as <body>` (attach /
|
|
475
|
+
* re-attach with verify-by-diff reconcile), and `alter table … drop maintained`
|
|
476
|
+
* (detach). The attach core never trusts existing rows blindly and never refills
|
|
477
|
+
* wholesale: it re-derives the body and reconciles by keyed diff (the
|
|
478
|
+
* 'replace-all' MaintenanceOp), so identical derivable content means ZERO row
|
|
479
|
+
* writes and zero reported changes, while divergence resolves derived-wins with
|
|
480
|
+
* only the genuine per-row changes reported (and cascaded to consumer maintained
|
|
481
|
+
* tables). Blind trust remains the rehydrate fast path's domain, where
|
|
482
|
+
* clean-shutdown attestation gates it (`SchemaManager.tryAdoptPreExistingBacking`). */
|
|
483
|
+
/**
|
|
484
|
+
* Names the first difference between a table's declared/live shape and the
|
|
485
|
+
* derived body `shape` — the attach-time strict shape check (null when the body
|
|
486
|
+
* derives exactly the declared shape). Unlike {@link describeBackingShapeMismatch}
|
|
487
|
+
* (the structural, name-blind refresh check) this one is part of the
|
|
488
|
+
* declared-shape contract and therefore compares column NAMES too: the declared
|
|
489
|
+
* layout is the frozen basis, so the body must be aliased to produce it
|
|
490
|
+
* verbatim — names, types, not-null, collations, and the physical primary key
|
|
491
|
+
* (order, direction, per-component collation). Not-null is exact in BOTH
|
|
492
|
+
* directions: tolerating a body-notNull/declared-nullable skew would make the
|
|
493
|
+
* next refresh's reshape pass "tighten" the declared column, silently mutating
|
|
494
|
+
* the frozen basis.
|
|
495
|
+
*
|
|
496
|
+
* `skipNames` drops the per-column NAME comparison for the `create table …
|
|
497
|
+
* maintained (columns) as` form: there the authored rename list is the
|
|
498
|
+
* authoritative output-name vector (body outputs are renamed positionally to it),
|
|
499
|
+
* so a body whose natural names differ from the declared columns is accepted as a
|
|
500
|
+
* positional rename. Everything else — column count, types, not-null (both ways),
|
|
501
|
+
* collations, and the physical primary key — stays strict.
|
|
502
|
+
*/
|
|
503
|
+
function describeAttachShapeMismatch(table, shape, skipNames = false) {
|
|
504
|
+
if (table.columns.length !== shape.columns.length) {
|
|
505
|
+
return `body produces ${shape.columns.length} columns but the table declares ${table.columns.length}`;
|
|
506
|
+
}
|
|
507
|
+
for (let i = 0; i < shape.columns.length; i++) {
|
|
508
|
+
const declared = table.columns[i];
|
|
509
|
+
const derived = shape.columns[i];
|
|
510
|
+
if (!skipNames && declared.name.toLowerCase() !== derived.name.toLowerCase()) {
|
|
511
|
+
return `body output column ${i + 1} is named '${derived.name}' but the table declares '${declared.name}' (alias the body output to match the declared shape)`;
|
|
512
|
+
}
|
|
513
|
+
if (!backingTypeMatches(declared, derived)) {
|
|
514
|
+
return `column '${declared.name}': body derives type ${derived.logicalType.name} but the table declares ${declared.logicalType.name}`;
|
|
515
|
+
}
|
|
516
|
+
if (!backingNotNullMatches(declared, derived)) {
|
|
517
|
+
return `column '${declared.name}': body derives ${derived.notNull ? 'not null' : 'nullable'} but the table declares ${declared.notNull ? 'not null' : 'nullable'}`;
|
|
518
|
+
}
|
|
519
|
+
if (!backingCollationMatches(declared, derived)) {
|
|
520
|
+
return `column '${declared.name}': body derives collation ${derived.collation ?? 'BINARY'} but the table declares ${declared.collation ?? 'BINARY'}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const derivedPk = computeBackingPrimaryKey(shape);
|
|
524
|
+
const declaredPk = table.primaryKeyDefinition;
|
|
525
|
+
if (declaredPk.length !== derivedPk.length) {
|
|
526
|
+
return `body derives a ${derivedPk.length}-column primary key but the table declares ${declaredPk.length} (a body \`order by\` seeds the derived key — see computeBackingPrimaryKey)`;
|
|
527
|
+
}
|
|
528
|
+
for (let k = 0; k < derivedPk.length; k++) {
|
|
529
|
+
const declaredCol = table.columns[declaredPk[k].index];
|
|
530
|
+
const derivedCol = shape.columns[derivedPk[k].index];
|
|
531
|
+
if (declaredPk[k].index !== derivedPk[k].index) {
|
|
532
|
+
return `primary-key component ${k + 1}: body derives '${derivedCol?.name}' but the table declares '${declaredCol?.name}'`;
|
|
533
|
+
}
|
|
534
|
+
if ((declaredPk[k].desc === true) !== (derivedPk[k].desc === true)) {
|
|
535
|
+
return `primary-key component ${k + 1} ('${declaredCol?.name}'): direction differs`;
|
|
536
|
+
}
|
|
537
|
+
const declaredColl = declaredPk[k].collation ?? declaredCol?.collation ?? 'BINARY';
|
|
538
|
+
const derivedColl = derivedCol?.collation ?? 'BINARY';
|
|
539
|
+
if (declaredColl !== derivedColl) {
|
|
540
|
+
return `primary-key component ${k + 1} ('${declaredCol?.name}'): body derives collation ${derivedColl} but the table declares ${declaredColl}`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Rejects an attach whose body would close a derivation cycle. Create-MV can
|
|
547
|
+
* never form one (a consumer is created after its producer), but attach can:
|
|
548
|
+
* `alter table A set maintained as select … from B` where B's derivation
|
|
549
|
+
* (transitively) reads A — including the degenerate self-reference (`… from A`).
|
|
550
|
+
* Walks the sourceTables→derivation edges of the LIVE catalog from the new
|
|
551
|
+
* body's sources; reaching the attach target names the cycle path in the
|
|
552
|
+
* diagnostic. The maintenance cascade's depth guard
|
|
553
|
+
* (`assertCascadeDepth`) stays as defense-in-depth behind this.
|
|
554
|
+
*/
|
|
555
|
+
function assertNoDerivationCycle(db, schemaName, tableName, sourceTables) {
|
|
556
|
+
const target = `${schemaName}.${tableName}`.toLowerCase();
|
|
557
|
+
const sm = db.schemaManager;
|
|
558
|
+
const visited = new Set();
|
|
559
|
+
const walk = (qualified, path) => {
|
|
560
|
+
if (qualified === target) {
|
|
561
|
+
// Render in data-flow order, closing the loop on the target. `path` is the
|
|
562
|
+
// derived-from chain outward from the new body (path[0] = a body source,
|
|
563
|
+
// path[last] = the table derived from the target), so data flows
|
|
564
|
+
// target → path[last] → … → path[0] → target.
|
|
565
|
+
const cycle = [target, ...[...path].reverse(), target].join(' → ');
|
|
566
|
+
throw new QuereusError(`cannot attach derivation to '${schemaName}.${tableName}': the body would create a derivation cycle (${cycle})`, StatusCode.ERROR);
|
|
567
|
+
}
|
|
568
|
+
if (visited.has(qualified))
|
|
569
|
+
return;
|
|
570
|
+
visited.add(qualified);
|
|
571
|
+
const dot = qualified.indexOf('.');
|
|
572
|
+
const srcSchema = dot >= 0 ? qualified.slice(0, dot) : 'main';
|
|
573
|
+
const srcName = dot >= 0 ? qualified.slice(dot + 1) : qualified;
|
|
574
|
+
const source = sm.getTable(srcSchema, srcName);
|
|
575
|
+
if (source && isMaintainedTable(source)) {
|
|
576
|
+
for (const next of source.derivation.sourceTables)
|
|
577
|
+
walk(next, [...path, qualified]);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
for (const src of sourceTables)
|
|
581
|
+
walk(src, []);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* The loud "must be a set" reject for attach, BEFORE any catalog or data
|
|
585
|
+
* mutation: the keyed reconcile diff would otherwise last-write-win duplicate
|
|
586
|
+
* derived keys silently. Collation-aware pairing — duplicates are detected
|
|
587
|
+
* under the backing primary-key collations (the same key identity the
|
|
588
|
+
* 'replace-all' diff uses), so a coarsened-key collision present in the source
|
|
589
|
+
* rejects here, naming the colliding key. `pk` is the SHAPE-derived physical
|
|
590
|
+
* key ({@link computeBackingPrimaryKey} over the derived shape): the rows are
|
|
591
|
+
* indexed by the shape, and under a reshape-on-attach the table's own PK
|
|
592
|
+
* definition may carry pre-reshape column indices. Equivalent to the table's
|
|
593
|
+
* PK whenever the shapes match (the strict attach check verifies index, desc,
|
|
594
|
+
* and collation equality).
|
|
595
|
+
*
|
|
596
|
+
* `onDuplicate` overrides the default attach-time diagnostic with a caller-built
|
|
597
|
+
* one (receiving the rendered colliding key values) — the refresh path threads
|
|
598
|
+
* {@link materializedViewNotASetError} through {@link assertRefreshRowsAreSet} so
|
|
599
|
+
* its constraint-bearing branch rejects duplicates identically to the
|
|
600
|
+
* `replaceContents` fast path, single-sourcing the collation-aware dup detection.
|
|
601
|
+
*/
|
|
602
|
+
function assertDerivedRowsAreSet(rows, pk, schemaName, name, onDuplicate) {
|
|
603
|
+
if (rows.length < 2)
|
|
604
|
+
return;
|
|
605
|
+
const compareKeys = (ra, rb) => {
|
|
606
|
+
for (const c of pk) {
|
|
607
|
+
const cmp = compareSqlValues(ra[c.index], rb[c.index], c.collation ?? 'BINARY');
|
|
608
|
+
if (cmp !== 0)
|
|
609
|
+
return cmp;
|
|
610
|
+
}
|
|
611
|
+
return 0;
|
|
612
|
+
};
|
|
613
|
+
const order = rows.map((_r, i) => i).sort((a, b) => compareKeys(rows[a], rows[b]));
|
|
614
|
+
for (let i = 1; i < order.length; i++) {
|
|
615
|
+
if (compareKeys(rows[order[i - 1]], rows[order[i]]) === 0) {
|
|
616
|
+
const keyVals = pk.map(c => formatKeyValue(rows[order[i]][c.index])).join(', ');
|
|
617
|
+
throw onDuplicate?.(keyVals) ?? new QuereusError(`cannot attach derivation to '${schemaName}.${name}': the body produces duplicate rows for primary key (${keyVals}), but a maintained table must be a set — `
|
|
618
|
+
+ `project a unique key or merge the colliding source rows first`, StatusCode.CONSTRAINT);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Refresh's duplicate-derived-key reject — the constraint-bearing
|
|
624
|
+
* {@link rebuildBacking} branch's parity with the `replaceContents` fast path,
|
|
625
|
+
* which rejects duplicate backing PKs via {@link materializedViewNotASetError}.
|
|
626
|
+
* `applyMaintenance('replace-all')` would otherwise silently LWW-merge colliding
|
|
627
|
+
* keys, so this raises the IDENTICAL diagnostic BEFORE the pending-layer reconcile,
|
|
628
|
+
* keeping the two refresh branches indistinguishable on duplicate handling.
|
|
629
|
+
* Delegates to {@link assertDerivedRowsAreSet} so the collation-aware detection
|
|
630
|
+
* stays single-sourced.
|
|
631
|
+
*/
|
|
632
|
+
function assertRefreshRowsAreSet(rows, pk, schemaName, name) {
|
|
633
|
+
assertDerivedRowsAreSet(rows, pk, schemaName, name, () => materializedViewNotASetError(schemaName, name));
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Resolve (or lazily create + register) the table's backing connection for the
|
|
637
|
+
* current transaction — the same discipline as the maintenance manager's
|
|
638
|
+
* `getBackingConnection`, so the reconcile's pending writes ride the
|
|
639
|
+
* coordinated commit in lockstep with the statement, and a `select` from the
|
|
640
|
+
* table inside the same transaction observes them (reads-own-writes).
|
|
641
|
+
*/
|
|
642
|
+
async function resolveAttachConnection(db, host, qualifiedName) {
|
|
643
|
+
for (const c of db.getConnectionsForTable(qualifiedName)) {
|
|
644
|
+
if (host.ownsConnection(c))
|
|
645
|
+
return c;
|
|
646
|
+
}
|
|
647
|
+
const conn = host.connect();
|
|
648
|
+
await db.registerConnection(conn);
|
|
649
|
+
return conn;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Whether `mt` declares ≥1 constraint the {@link rebuildBacking} refresh path must
|
|
653
|
+
* validate over the recomputed row set — the same predicate
|
|
654
|
+
* {@link validateDeclaredConstraintsOverContents} gates on: any CHECK whose op-mask
|
|
655
|
+
* intersects INSERT | UPDATE (the derived-row op-mask collapse — a derived row's
|
|
656
|
+
* presence is neither a user INSERT nor UPDATE), or any child-side FK.
|
|
657
|
+
*
|
|
658
|
+
* The FK term is additionally gated on `pragma foreign_keys`: with enforcement off
|
|
659
|
+
* the bulk FK scan no-ops, so an FK-only maintained table keeps the zero-overhead
|
|
660
|
+
* `replaceContents` fast path rather than spinning up a connection for a no-op scan.
|
|
661
|
+
* A table also declaring an applicable CHECK always takes the validating branch
|
|
662
|
+
* regardless of the pragma.
|
|
663
|
+
*/
|
|
664
|
+
function hasApplicableConstraints(db, mt) {
|
|
665
|
+
const hasCheck = mt.checkConstraints.some(c => (c.operations & (RowOpFlag.INSERT | RowOpFlag.UPDATE)) !== 0);
|
|
666
|
+
if (hasCheck)
|
|
667
|
+
return true;
|
|
668
|
+
const fks = mt.foreignKeys ?? [];
|
|
669
|
+
return fks.length > 0 && db.options.getBooleanOption('foreign_keys');
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Bulk derived-row constraint validation for the attach paths (create-fill and
|
|
673
|
+
* attach/re-attach reconcile): after the `'replace-all'` reconcile lands the
|
|
674
|
+
* derived row set in the connection's pending layer, scan the table's EFFECTIVE
|
|
675
|
+
* (pending-over-committed) contents against every declared CHECK whose op-mask
|
|
676
|
+
* intersects INSERT | UPDATE (the derived-row op-mask collapse — a derived row's
|
|
677
|
+
* presence is neither a user INSERT nor UPDATE, see docs/materialized-views.md)
|
|
678
|
+
* and every declared child-side FK (pragma-gated inside the FK validator,
|
|
679
|
+
* MATCH SIMPLE). Post-reconcile contents are exactly the derived set, so this
|
|
680
|
+
* validates every row the table will hold — which is also why detach can never
|
|
681
|
+
* strand a violator. Zero overhead when nothing is declared (every MV-sugar
|
|
682
|
+
* backing: `buildBackingTableSchema` hard-codes empty constraints).
|
|
683
|
+
*
|
|
684
|
+
* The scan is a plain table read of the backing (a maintained table resolves
|
|
685
|
+
* through the ORDINARY table path in `building/select.ts` — never a
|
|
686
|
+
* re-derivation), observing the pending reconcile writes through the registered
|
|
687
|
+
* attach connection (reads-own-writes). An `old.`/`new.`-qualified CHECK —
|
|
688
|
+
* which this SQL scan could not resolve — was already rejected at registration
|
|
689
|
+
* (`buildDerivedRowValidator`), which runs before this validation on every
|
|
690
|
+
* create/attach path.
|
|
691
|
+
*
|
|
692
|
+
* Declared-constraint folding: the optimizer trusts a declared CHECK / FK as a
|
|
693
|
+
* proven invariant (`ruleFilterContradiction` / `ruleAntiJoinFkEmpty`), and —
|
|
694
|
+
* unlike the ALTER ADD paths — the constraints under validation are already on
|
|
695
|
+
* the LIVE record here. So the live record is swapped for a constraint-stripped
|
|
696
|
+
* clone for the duration of the scans (the ADD COLUMN intermediate-schema
|
|
697
|
+
* discipline, see `runtime/emit/alter-table.ts`), then restored.
|
|
698
|
+
*/
|
|
699
|
+
async function validateDeclaredConstraintsOverContents(db, mt) {
|
|
700
|
+
const applicableChecks = mt.checkConstraints.filter(c => (c.operations & (RowOpFlag.INSERT | RowOpFlag.UPDATE)) !== 0);
|
|
701
|
+
const fks = mt.foreignKeys ?? [];
|
|
702
|
+
if (applicableChecks.length === 0 && fks.length === 0)
|
|
703
|
+
return;
|
|
704
|
+
const schema = db.schemaManager.getSchemaOrFail(mt.schemaName);
|
|
705
|
+
const stripped = { ...mt, checkConstraints: Object.freeze([]), foreignKeys: undefined };
|
|
706
|
+
schema.addTable(stripped);
|
|
707
|
+
try {
|
|
708
|
+
await validateChecksOverExistingRows(db, mt, applicableChecks, (check, exprSql) => maintainedTableCheckViolationError(mt.schemaName, mt.name, check.name ?? `_check_${mt.checkConstraints.indexOf(check)}`, exprSql));
|
|
709
|
+
for (const fk of fks) {
|
|
710
|
+
await validateForeignKeyOverExistingRows(db, mt, fk, () => maintainedTableFkViolationError(mt.schemaName, mt.name, fk.name ?? `_fk_${mt.name}`, fk.referencedSchema ?? mt.schemaName, fk.referencedTable));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
finally {
|
|
714
|
+
schema.addTable(mt);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* The attach core shared by `alter table … set maintained as` (fresh attach and
|
|
719
|
+
* re-attach) and `create table … maintained as` (attach-to-empty, via
|
|
720
|
+
* {@link createMaintainedTable}): verify-by-diff, never trust, never refill
|
|
721
|
+
* wholesale.
|
|
722
|
+
*
|
|
723
|
+
* Sequence — every gate runs BEFORE any catalog or data mutation:
|
|
724
|
+
* 1. backing-host capability (defense-in-depth; the builders gate with a sited
|
|
725
|
+
* error);
|
|
726
|
+
* 2. derive the body's shape (rewrite-suppressed) and run the STRICT
|
|
727
|
+
* declared-shape check ({@link describeAttachShapeMismatch} — names
|
|
728
|
+
* included);
|
|
729
|
+
* 3. cycle / self-reference check over the live derivation graph;
|
|
730
|
+
* 4. evaluate the body once and reject duplicate derived keys (the loud
|
|
731
|
+
* "must be a set" reject);
|
|
732
|
+
* 5. catalog flip (`attachDerivation`) + maintenance registration — the
|
|
733
|
+
* create-time gates (determinism, keyed-or-coarsened body, full-rebuild
|
|
734
|
+
* size threshold) run inside `registerMaterializedView`, before any row is
|
|
735
|
+
* written; a throw restores the prior record (and the prior plan, on
|
|
736
|
+
* re-attach);
|
|
737
|
+
* 6. reconcile-by-diff: one `'replace-all'` op against the table's effective
|
|
738
|
+
* contents through the backing host — collation-aware pairing,
|
|
739
|
+
* byte-faithful identical-row skip, so identical content writes nothing and
|
|
740
|
+
* divergence resolves derived-wins with the minimal genuine
|
|
741
|
+
* {@link BackingRowChange}s. The writes land in the connection's PENDING
|
|
742
|
+
* state, committing/rolling back in lockstep with the statement;
|
|
743
|
+
* 7. covering links (clear the prior body's, stamp the new body's), cascade
|
|
744
|
+
* the genuine changes to consumer maintained tables, fire
|
|
745
|
+
* `materialized_view_added` (fresh) / `materialized_view_modified`
|
|
746
|
+
* (re-attach) so store catalogs re-persist the canonical table-form DDL,
|
|
747
|
+
* and surface the key-coarsening warning exactly as create does.
|
|
748
|
+
*
|
|
749
|
+
* `recordedColumns` is recorded verbatim as `derivation.columns` (the lossless
|
|
750
|
+
* implicit/explicit signal the persist + import paths already use): the authored
|
|
751
|
+
* column names for the explicit forms — `create table … maintained (columns) as`
|
|
752
|
+
* AND the re-attach verb `set maintained (columns) as` — or `undefined` for the
|
|
753
|
+
* implicit forms — `create table … maintained as` (which reshapes its source on
|
|
754
|
+
* reopen) AND the implicit re-attach verb `set maintained as` (which reshapes to
|
|
755
|
+
* follow the body's natural names). When `positionalRename` is set — every
|
|
756
|
+
* explicit form — the body outputs are renamed positionally to `recordedColumns`
|
|
757
|
+
* and the per-column name check is skipped (the authored list is the authoritative
|
|
758
|
+
* output-name vector); otherwise the strict declared-shape check (names included)
|
|
759
|
+
* applies. `buildTableDerivation` hashes `recordedColumns` into `bodyHash`, so
|
|
760
|
+
* live exec and catalog import of the same canonical DDL agree on both the record
|
|
761
|
+
* and the hash — making attach/create → persist → reopen a fixed point.
|
|
762
|
+
*
|
|
763
|
+
* An explicit list whose arity disagrees with the body raises a sited error (the
|
|
764
|
+
* list-vs-body arity guard) before anything is recorded — `deriveBackingShape`
|
|
765
|
+
* sizes the shape to the body, so a surplus/short list would otherwise persist a
|
|
766
|
+
* miscounted `derivation.columns`.
|
|
767
|
+
*
|
|
768
|
+
* **Reshape-on-attach (`allowReshape`).** The verb path (`set maintained [(cols)]
|
|
769
|
+
* as` — manual AND differ-emitted) passes `allowReshape = true`; create and any
|
|
770
|
+
* non-verb caller pass `false` and keep the strict declared-shape error. Two
|
|
771
|
+
* reshape triggers, both reusing the two-phase splice + restore handlers below:
|
|
772
|
+
*
|
|
773
|
+
* - **Implicit call** (no rename list): on a strict-shape mismatch the backing
|
|
774
|
+
* reshapes in place to follow the body's natural names — the same "the body
|
|
775
|
+
* owns an implicit table's shape" contract the refresh reshape and the implicit
|
|
776
|
+
* table form's reopen honor. Now permitted over a prior-EXPLICIT record too:
|
|
777
|
+
* `set maintained as <body>` over an `(a, b)` table abandons the authored list
|
|
778
|
+
* and relabels the backing to the body's names, recording an implicit
|
|
779
|
+
* derivation (the deliberate "go implicit" re-attach).
|
|
780
|
+
* - **Explicit call** (`positionalRename`): a same-arity output-NAME drift
|
|
781
|
+
* `(a, b) → (a, c)` produces no strict mismatch (names are skipped), yet the
|
|
782
|
+
* backing must be relabeled to the recorded names — classified as a reshape.
|
|
783
|
+
* The derived shape carries the TARGET names, so {@link classifyBackingReshape}
|
|
784
|
+
* emits a pure positional RENAME (`b → c`); a renamed PK output column is
|
|
785
|
+
* matched through the rename map (not a key change). A count/type/PK delta is
|
|
786
|
+
* still the strict error, and a reorder/swap (`(a, b) → (b, a)`) classifies as
|
|
787
|
+
* inexpressible.
|
|
788
|
+
*
|
|
789
|
+
* An inexpressible delta (interleave / physical-PK change, or a host module
|
|
790
|
+
* without `alterTable`) raises {@link inexpressibleReshapeError} with the table
|
|
791
|
+
* untouched. An expressible plan splices around the verify-by-diff reconcile —
|
|
792
|
+
* see the sequencing notes inside.
|
|
793
|
+
*/
|
|
794
|
+
export async function attachMaintainedDerivation(db, table, select, recordedColumns, positionalRename = false, allowReshape = false,
|
|
795
|
+
/**
|
|
796
|
+
* When true, a FAILED FRESH attach discards (via {@link VirtualTableModule.discardBackingForAttach})
|
|
797
|
+
* any backing store {@link VirtualTableModule.ensureBackingForAttach} created IN
|
|
798
|
+
* THIS attach. Set by the `set maintained` ATTACH verb ({@link runSetMaintained}),
|
|
799
|
+
* which owns its own backing cleanup. NOT set by `create table … maintained`
|
|
800
|
+
* ({@link createMaintainedTable}) — there the store was created by the prior
|
|
801
|
+
* `createTable(preferBacking)`, and the create path's own `dropTable` cleanup
|
|
802
|
+
* retires it; a discard here would double-drop and strand the catalog entry.
|
|
803
|
+
*/
|
|
804
|
+
discardBackingOnFailure = false) {
|
|
805
|
+
const sm = db.schemaManager;
|
|
806
|
+
const schemaName = table.schemaName;
|
|
807
|
+
const name = table.name;
|
|
808
|
+
const schema = sm.getSchemaOrFail(schemaName);
|
|
809
|
+
const module = requireVtabModule(table);
|
|
810
|
+
if (!module.getBackingHost) {
|
|
811
|
+
throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': module '${table.vtabModuleName}' cannot host a maintained table (it does not implement the backing-host capability)`, StatusCode.UNSUPPORTED);
|
|
812
|
+
}
|
|
813
|
+
const bodySql = astToString(select);
|
|
814
|
+
// With an authored rename list (`maintained (columns)` create form) the body is
|
|
815
|
+
// renamed positionally to it and the name check skipped; otherwise natural output
|
|
816
|
+
// names with the strict declared-shape check (the body must already be aliased to
|
|
817
|
+
// the declared names — the attach verb / implicit-create posture).
|
|
818
|
+
const shape = deriveBackingShape(db, bodySql, positionalRename ? recordedColumns : undefined);
|
|
819
|
+
// Explicit rename-list arity guard. `deriveBackingShape` sizes the shape to the
|
|
820
|
+
// BODY's arity (a surplus rename name is dropped, a missing one padded), so a
|
|
821
|
+
// list whose length disagrees with the body would otherwise record an
|
|
822
|
+
// over/under-counted `derivation.columns` over the backing. The CREATE path
|
|
823
|
+
// catches this via its table-vs-body count check before reaching here (the
|
|
824
|
+
// freshly-created table mirrors the list); on re-attach the existing table can
|
|
825
|
+
// match the body while the list does not, so guard the list-vs-body arity
|
|
826
|
+
// directly. The implicit form (`recordedColumns === undefined`) is exempt.
|
|
827
|
+
if (recordedColumns !== undefined && recordedColumns.length !== shape.columns.length) {
|
|
828
|
+
throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': the rename list declares ${recordedColumns.length} columns but the body produces ${shape.columns.length}`, StatusCode.ERROR);
|
|
829
|
+
}
|
|
830
|
+
const mismatch = describeAttachShapeMismatch(table, shape, positionalRename);
|
|
831
|
+
// Reshape-on-attach (see the docstring). The verb (`allowReshape`) reshapes the
|
|
832
|
+
// backing in place instead of erroring on a shape change; create and any
|
|
833
|
+
// non-verb caller keep the strict error. Two reshape triggers:
|
|
834
|
+
// - IMPLICIT call (no rename list): a strict mismatch follows the body's
|
|
835
|
+
// natural names — now also over a prior-explicit record (the deliberate
|
|
836
|
+
// "go implicit" re-attach); the explicit forms keep the strict count/type/PK
|
|
837
|
+
// error instead.
|
|
838
|
+
// - EXPLICIT call (`positionalRename`): a same-arity NAME drift `(a,b)→(a,c)`
|
|
839
|
+
// produces NO mismatch under `skipNames`, yet the backing must be relabeled
|
|
840
|
+
// to the recorded names — classify that as a reshape too. The shape carries
|
|
841
|
+
// the TARGET names, so `classifyBackingReshape` emits a pure positional
|
|
842
|
+
// RENAME; a reorder/swap classifies inexpressible (table untouched) and a
|
|
843
|
+
// renamed PK column is matched through the rename map (not a key change).
|
|
844
|
+
const strictMismatchReshape = mismatch !== null && allowReshape && !positionalRename && recordedColumns === undefined;
|
|
845
|
+
const explicitNameDriftReshape = mismatch === null && positionalRename && allowReshape
|
|
846
|
+
&& table.columns.some((c, i) => c.name.toLowerCase() !== shape.columns[i].name.toLowerCase());
|
|
847
|
+
let reshapePlan;
|
|
848
|
+
if (mismatch && !strictMismatchReshape) {
|
|
849
|
+
throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': ${mismatch}`, StatusCode.ERROR);
|
|
850
|
+
}
|
|
851
|
+
if (strictMismatchReshape || explicitNameDriftReshape) {
|
|
852
|
+
if (!module.alterTable) {
|
|
853
|
+
throw inexpressibleReshapeError(schemaName, name, `its backing module '${table.vtabModuleName}' does not support in-place ALTER`);
|
|
854
|
+
}
|
|
855
|
+
const classification = classifyBackingReshape(table, shape);
|
|
856
|
+
if (!classification.expressible) {
|
|
857
|
+
throw inexpressibleReshapeError(schemaName, name, classification.reason);
|
|
858
|
+
}
|
|
859
|
+
reshapePlan = classification.plan;
|
|
860
|
+
}
|
|
861
|
+
assertNoDerivationCycle(db, schemaName, name, shape.sourceTables);
|
|
862
|
+
const rows = await collectBodyRows(db, bodySql);
|
|
863
|
+
// Shape-derived physical key (see assertDerivedRowsAreSet): under a reshape the
|
|
864
|
+
// table's own PK definition may carry pre-reshape indices; equivalent otherwise.
|
|
865
|
+
const shapePk = computeBackingPrimaryKey(shape)
|
|
866
|
+
.map(c => ({ index: c.index, collation: shape.columns[c.index]?.collation }));
|
|
867
|
+
assertDerivedRowsAreSet(rows, shapePk, schemaName, name);
|
|
868
|
+
const def = {
|
|
869
|
+
schemaName,
|
|
870
|
+
viewName: name,
|
|
871
|
+
selectAst: select,
|
|
872
|
+
bodySql,
|
|
873
|
+
// Recorded as authored: declared names for the explicit forms, undefined for
|
|
874
|
+
// the implicit create form — the lossless signal persist + import already use.
|
|
875
|
+
// Any `with defaults (…)` rides inside `select` (→ derivation.selectAst).
|
|
876
|
+
columns: recordedColumns,
|
|
877
|
+
};
|
|
878
|
+
const prior = schema.getTable(name) ?? table;
|
|
879
|
+
const priorMaintained = isMaintainedTable(prior) ? prior : undefined;
|
|
880
|
+
// Undo the catalog flip after a gate/reconcile failure: restore the prior
|
|
881
|
+
// record and, on re-attach, the prior row-time plan (registerMaterializedView
|
|
882
|
+
// released it when registering the new one).
|
|
883
|
+
const restorePrior = () => {
|
|
884
|
+
schema.addTable(prior);
|
|
885
|
+
if (priorMaintained) {
|
|
886
|
+
if (!priorMaintained.derivation.stale) {
|
|
887
|
+
try {
|
|
888
|
+
db.registerMaterializedView(priorMaintained);
|
|
889
|
+
}
|
|
890
|
+
catch (e) {
|
|
891
|
+
// The prior plan registered before, so this should not throw; if it
|
|
892
|
+
// does, fail safe: stale (reads re-validate) beats silently live.
|
|
893
|
+
db.markMaterializedViewStale(priorMaintained);
|
|
894
|
+
log('Re-registering the prior derivation of %s.%s failed during attach rollback; marked stale: %s', schemaName, name, e instanceof Error ? e.message : String(e));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
db.unregisterMaterializedView(schemaName, name);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
// Defensive-guard input: capture whether the gate-time backing host is absent
|
|
903
|
+
// BEFORE the gate registration below. Resolved against `table` — the pre-reshape
|
|
904
|
+
// catalog record the gate registration also resolves against
|
|
905
|
+
// (`tryResolveBackingHost` keys only on schema+name, never the shape). A module
|
|
906
|
+
// that materializes its host LATE (`getBackingHost` undefined until
|
|
907
|
+
// `ensureBackingForAttach`) reads absent here, so the replicable-determinism gate
|
|
908
|
+
// inside `registerMaterializedView` was skipped; the guard after the late seam
|
|
909
|
+
// re-checks once the now-present host is in hand. See the eager-resolution
|
|
910
|
+
// invariant on `BackingHost.requiresReplicableDerivations`.
|
|
911
|
+
const gateHostAbsent = tryResolveBackingHost(db, table) === undefined;
|
|
912
|
+
const maintained = sm.attachDerivation(schemaName, name, buildTableDerivation(def, shape));
|
|
913
|
+
try {
|
|
914
|
+
// The create-time gates (determinism, keyed-or-coarsened body, relational
|
|
915
|
+
// output, full-rebuild size threshold) run here — identical to create.
|
|
916
|
+
// Under a reshape this registration is a GATE only: the catalog still
|
|
917
|
+
// holds the pre-reshape columns, so the plan it builds may classify into
|
|
918
|
+
// the full-rebuild floor where the final record fits a bounded-delta arm;
|
|
919
|
+
// the post-reshape re-registration below rebuilds the binding plan, and
|
|
920
|
+
// nothing exercises the interim plan inside this DDL statement.
|
|
921
|
+
db.registerMaterializedView(maintained);
|
|
922
|
+
}
|
|
923
|
+
catch (e) {
|
|
924
|
+
restorePrior();
|
|
925
|
+
throw e;
|
|
926
|
+
}
|
|
927
|
+
// Failure restore once the module's live schema has (partially) reshaped:
|
|
928
|
+
// module column ops are NOT transactional, so restoring the PRIOR record would
|
|
929
|
+
// strand a catalog/module divergence. Keep the catalog tracking the module
|
|
930
|
+
// instead — fresh attach: the table reverts to a plain (derivation-less) table
|
|
931
|
+
// at the reshaped schema; re-attach: the prior derivation rides the reshaped
|
|
932
|
+
// backing marked STALE (its body no longer derives this shape — a later
|
|
933
|
+
// refresh reshapes it back). Coherent and re-runnable either way.
|
|
934
|
+
const restoreReshaped = (moduleSchema) => {
|
|
935
|
+
if (priorMaintained) {
|
|
936
|
+
const restored = graftReshapedRecord(moduleSchema, priorMaintained);
|
|
937
|
+
schema.addTable(restored);
|
|
938
|
+
db.markMaterializedViewStale(restored);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
const { derivation: _derivation, ...plain } = moduleSchema;
|
|
942
|
+
schema.addTable(plain);
|
|
943
|
+
db.unregisterMaterializedView(schemaName, name);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
let live = maintained;
|
|
947
|
+
let changes;
|
|
948
|
+
let current = table;
|
|
949
|
+
let moduleMutated = false;
|
|
950
|
+
let reconcileCommitted = false;
|
|
951
|
+
try {
|
|
952
|
+
if (reshapePlan) {
|
|
953
|
+
// Pre-reconcile structural ops (rename/add/loosen/drop — none throw on
|
|
954
|
+
// data), then re-register the reshaped schema with the new derivation so
|
|
955
|
+
// the reconcile resolves the reshaped backing. Mirrors
|
|
956
|
+
// reshapeBackingInPlace's pre batch; ops address columns by name.
|
|
957
|
+
for (const op of reshapePlan.preReconcileOps) {
|
|
958
|
+
current = await module.alterTable(db, schemaName, name, reshapeOpToChange(op));
|
|
959
|
+
moduleMutated = true;
|
|
960
|
+
}
|
|
961
|
+
live = graftReshapedRecord(current, maintained);
|
|
962
|
+
schema.addTable(live);
|
|
963
|
+
}
|
|
964
|
+
// Materialize the durable backing store the reconcile will write into,
|
|
965
|
+
// BEFORE resolving the host. A module whose `getBackingHost` resolves over a
|
|
966
|
+
// SEPARATE durable store (e.g. lamina) needs the store created here — the
|
|
967
|
+
// attach core only RESOLVES the host, never creates it, and on the
|
|
968
|
+
// non-reshape path there is no other async module call beforehand. Placed
|
|
969
|
+
// AFTER the reshape `preReconcileOps` + `schema.addTable(live)` so `live`
|
|
970
|
+
// carries the reshaped shape and the store is sized to it. A no-op for
|
|
971
|
+
// modules that omit the hook (memory hosts the live table directly).
|
|
972
|
+
await module.ensureBackingForAttach?.(db, schemaName, name, live);
|
|
973
|
+
// Verify-by-diff reconcile against the (possibly reshaped) backing: the
|
|
974
|
+
// re-resolved host keys the 'replace-all' diff by the module's CURRENT
|
|
975
|
+
// physical PK, so a reshape that shifted PK column indices stays aligned.
|
|
976
|
+
const host = resolveBackingHost(db, live);
|
|
977
|
+
// Defensive guard (defense-in-depth — see `tryResolveBackingHost` and the
|
|
978
|
+
// eager-resolution invariant on `BackingHost.requiresReplicableDerivations`).
|
|
979
|
+
// The gate-time host was absent, so the replicable-determinism gate inside
|
|
980
|
+
// `registerMaterializedView` could not run — yet the now-resolved host DEMANDS
|
|
981
|
+
// replicable derivations. A demanding host MUST resolve eagerly (at plan-build
|
|
982
|
+
// time, before this late-backing seam); reaching here means it violated that
|
|
983
|
+
// contract and a non-replicable body would have slipped the gate. Fail loud
|
|
984
|
+
// rather than corrupt peers. The throw is inside this try, so the catch runs
|
|
985
|
+
// `restorePrior()` / `discardBackingForAttach` cleanup and the statement rolls
|
|
986
|
+
// back — the table reverts to ordinary, untouched. INTERNAL because reaching it
|
|
987
|
+
// is a host-author contract violation, not user error. Single-sited after the
|
|
988
|
+
// sole `resolveBackingHost(db, live)`, so it covers the reshape arm too.
|
|
989
|
+
if (gateHostAbsent && host.requiresReplicableDerivations) {
|
|
990
|
+
throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': its backing host requires `
|
|
991
|
+
+ `replicable derivations but did not resolve until after the durable backing was `
|
|
992
|
+
+ `materialized, so the replicable-determinism gate could not run. A host that sets `
|
|
993
|
+
+ `requiresReplicableDerivations must resolve via getBackingHost at plan-build time `
|
|
994
|
+
+ `(before ensureBackingForAttach).`, StatusCode.INTERNAL);
|
|
995
|
+
}
|
|
996
|
+
const conn = await resolveAttachConnection(db, host, `${schemaName}.${name}`);
|
|
997
|
+
changes = await host.applyMaintenance(conn, [{ kind: 'replace-all', rows }]);
|
|
998
|
+
// Declared CHECK / child-side FK over the reconciled (derived) row set —
|
|
999
|
+
// inside this try so a violation restores the prior record; the pending
|
|
1000
|
+
// reconcile writes roll back with the failing statement.
|
|
1001
|
+
await validateDeclaredConstraintsOverContents(db, live);
|
|
1002
|
+
if (reshapePlan && reshapePlan.postReconcileOps.length > 0) {
|
|
1003
|
+
// Data-validating attribute ops (retype / recollate / tighten NOT NULL)
|
|
1004
|
+
// must validate the RECONCILED body rows, not the stale backing — but the
|
|
1005
|
+
// module's alterTable scans COMMITTED contents (memory's alterColumn walks
|
|
1006
|
+
// the base layer) while the reconcile above sits in the connection's
|
|
1007
|
+
// PENDING layer. So commit the reconcile eagerly first (refresh-parity
|
|
1008
|
+
// commit-first semantics — the structural ops above are already
|
|
1009
|
+
// non-transactional, so the reshaping attach is DDL-grade atomicity
|
|
1010
|
+
// regardless; the later coordinated commit no-ops). Then mirror
|
|
1011
|
+
// reshapeBackingInPlace's post batch: re-register the catalog after EACH
|
|
1012
|
+
// op so a mid-batch throw cannot strand catalog/module divergence.
|
|
1013
|
+
await conn.commit();
|
|
1014
|
+
reconcileCommitted = true;
|
|
1015
|
+
for (const op of reshapePlan.postReconcileOps) {
|
|
1016
|
+
current = await module.alterTable(db, schemaName, name, reshapeOpToChange(op));
|
|
1017
|
+
live = graftReshapedRecord(current, maintained);
|
|
1018
|
+
schema.addTable(live);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (reshapePlan) {
|
|
1022
|
+
// Final binding: the early registration gated against the pre-reshape
|
|
1023
|
+
// record; re-register (idempotent) so the row-time plan binds the
|
|
1024
|
+
// RESHAPED backing's columns and physical PK.
|
|
1025
|
+
db.registerMaterializedView(live);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
catch (e) {
|
|
1029
|
+
if (reconcileCommitted) {
|
|
1030
|
+
// The reconciled rows are already committed and the catalog tracks the
|
|
1031
|
+
// module per-op — leave the new record in place, stale (reads re-validate;
|
|
1032
|
+
// a refresh applies the remaining attribute reshape).
|
|
1033
|
+
db.markMaterializedViewStale(live);
|
|
1034
|
+
}
|
|
1035
|
+
else if (moduleMutated) {
|
|
1036
|
+
restoreReshaped(current);
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
restorePrior();
|
|
1040
|
+
}
|
|
1041
|
+
// Discard a backing store freshly created by `ensureBackingForAttach` for a
|
|
1042
|
+
// FAILED FRESH attach (no prior derivation): the table reverts to ordinary,
|
|
1043
|
+
// whose storage still holds the pre-attach rows, so the just-created (empty /
|
|
1044
|
+
// rolled-back) store must be dropped — otherwise the module would keep routing
|
|
1045
|
+
// reads to it. A re-attach (priorMaintained) reused the existing store, which
|
|
1046
|
+
// `restorePrior` keeps for the restored prior derivation, so it is NOT
|
|
1047
|
+
// discarded. The reconcile-committed branch keeps its committed store (stale).
|
|
1048
|
+
if (discardBackingOnFailure && !reconcileCommitted && !priorMaintained) {
|
|
1049
|
+
await module.discardBackingForAttach?.(db, schemaName, name);
|
|
1050
|
+
}
|
|
1051
|
+
throw e;
|
|
1052
|
+
}
|
|
1053
|
+
if (priorMaintained)
|
|
1054
|
+
unlinkCoveredUniqueConstraints(db, priorMaintained);
|
|
1055
|
+
linkCoveredUniqueConstraints(db, live, bodySql);
|
|
1056
|
+
if (reshapePlan) {
|
|
1057
|
+
// The table's column SHAPE changed, and the modified-event channel has no
|
|
1058
|
+
// maintenance listener — fire the same single table_modified the refresh
|
|
1059
|
+
// reshape fires, BEFORE the row cascade below, so consumer maintained
|
|
1060
|
+
// tables go stale (and their released plans never receive shape-shifted
|
|
1061
|
+
// rows); cached plans scanning the table directly recompile.
|
|
1062
|
+
sm.getChangeNotifier().notifyChange({
|
|
1063
|
+
type: 'table_modified',
|
|
1064
|
+
schemaName, objectName: name,
|
|
1065
|
+
oldObject: prior, newObject: live,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
// Cascade the GENUINE reconcile changes to consumer maintained tables: the
|
|
1069
|
+
// reconcile wrote this table through the privileged surface, so the DML
|
|
1070
|
+
// boundary never saw the writes. Identical content produced zero changes and
|
|
1071
|
+
// therefore zero dispatch. Full-rebuild consumers defer + drain once,
|
|
1072
|
+
// mirroring the statement flush.
|
|
1073
|
+
if (changes.length > 0) {
|
|
1074
|
+
const base = `${schemaName}.${name}`;
|
|
1075
|
+
const deferred = new Set();
|
|
1076
|
+
for (const change of changes) {
|
|
1077
|
+
await db._maintainRowTimeCoveringStructures(base, change, undefined, deferred);
|
|
1078
|
+
}
|
|
1079
|
+
await db._flushDeferredRebuilds(deferred);
|
|
1080
|
+
}
|
|
1081
|
+
sm.getChangeNotifier().notifyChange(priorMaintained
|
|
1082
|
+
? {
|
|
1083
|
+
type: 'materialized_view_modified',
|
|
1084
|
+
schemaName, objectName: name,
|
|
1085
|
+
oldObject: priorMaintained, newObject: live,
|
|
1086
|
+
}
|
|
1087
|
+
: {
|
|
1088
|
+
type: 'materialized_view_added',
|
|
1089
|
+
schemaName, objectName: name,
|
|
1090
|
+
newObject: live,
|
|
1091
|
+
});
|
|
1092
|
+
if (live.derivation.coarsenedKey) {
|
|
1093
|
+
warnKeyCoarsening(schemaName, name, live.derivation.coarsenedKey);
|
|
1094
|
+
}
|
|
1095
|
+
return live;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Detach a maintained table's derivation — `alter table … drop maintained`.
|
|
1099
|
+
* Catalog-only: nothing physical changes. The row-time plan is released, the
|
|
1100
|
+
* covering-structure link un-stamped (UNIQUE enforcement falls back to the
|
|
1101
|
+
* auto-index), and the registered record swapped for the same table minus the
|
|
1102
|
+
* derivation — rows, indexes, module identity, and tags all stay; staleness
|
|
1103
|
+
* state lives on the derivation and leaves with it. The table becomes ordinary
|
|
1104
|
+
* and user-writable.
|
|
1105
|
+
*
|
|
1106
|
+
* Fires `materialized_view_removed` ONLY: the maintenance manager releases any
|
|
1107
|
+
* remaining plan, store catalogs delete the persisted maintained entry (a
|
|
1108
|
+
* store-hosted table's plain bundle is already clause-free), and cached
|
|
1109
|
+
* statement plans over the table invalidate (a cached write-through plan
|
|
1110
|
+
* compiled against the old derivation must not survive the flip). Deliberately
|
|
1111
|
+
* NO `table_modified`: the table's shape and rows are unchanged, so consumer
|
|
1112
|
+
* maintained tables reading it stay live — subsequent user writes drive their
|
|
1113
|
+
* maintenance exactly like any source write.
|
|
1114
|
+
*/
|
|
1115
|
+
export function detachMaintainedDerivation(db, mv) {
|
|
1116
|
+
const sm = db.schemaManager;
|
|
1117
|
+
const schema = sm.getSchemaOrFail(mv.schemaName);
|
|
1118
|
+
db.unregisterMaterializedView(mv.schemaName, mv.name);
|
|
1119
|
+
unlinkCoveredUniqueConstraints(db, mv);
|
|
1120
|
+
const live = schema.getTable(mv.name);
|
|
1121
|
+
const source = live && isMaintainedTable(live) ? live : mv;
|
|
1122
|
+
const { derivation: _derivation, ...plain } = source;
|
|
1123
|
+
schema.addTable(plain);
|
|
1124
|
+
sm.getChangeNotifier().notifyChange({
|
|
1125
|
+
type: 'materialized_view_removed',
|
|
1126
|
+
schemaName: mv.schemaName,
|
|
1127
|
+
objectName: mv.name,
|
|
1128
|
+
oldObject: source,
|
|
1129
|
+
});
|
|
1130
|
+
return plain;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* `create table … maintained as <body>` — the declared-shape authoring form,
|
|
1134
|
+
* executed all-or-nothing:
|
|
1135
|
+
*
|
|
1136
|
+
* - an existing table/view + `if not exists` skips ENTIRELY (never a
|
|
1137
|
+
* half-attach); without it, the standard already-exists error — both before
|
|
1138
|
+
* the body is planned;
|
|
1139
|
+
* - the declared shape is verified against the derived body shape BEFORE any
|
|
1140
|
+
* catalog registration ({@link SchemaManager.buildDeclaredTableSchema} builds
|
|
1141
|
+
* the schema the CREATE would register, without registering it);
|
|
1142
|
+
* - then the table registers through the ordinary `createTable` path (declared
|
|
1143
|
+
* constraints and defaults intact) and the shared {@link attachMaintainedDerivation}
|
|
1144
|
+
* core runs — attach-to-empty: the reconcile diff against an empty table IS
|
|
1145
|
+
* the fill, applied to the connection's pending state so it commits in
|
|
1146
|
+
* lockstep with the statement (no `replaceContents` commit-first caveat);
|
|
1147
|
+
* - any failure past registration (duplicate derived keys, a maintenance gate)
|
|
1148
|
+
* drops the just-created table — the schema is left exactly as before.
|
|
1149
|
+
*
|
|
1150
|
+
* The attach core re-derives the body AFTER the table registers, so a body that
|
|
1151
|
+
* resolves differently once the new name exists (e.g. a same-name reference that
|
|
1152
|
+
* becomes a self-reference) is caught by the cycle check and rolled back.
|
|
1153
|
+
*/
|
|
1154
|
+
export async function createMaintainedTable(db, stmt) {
|
|
1155
|
+
const sm = db.schemaManager;
|
|
1156
|
+
const schemaName = stmt.table.schema ? sm.canonicalSchemaName(stmt.table.schema) : sm.getCurrentSchemaName();
|
|
1157
|
+
const name = stmt.table.name;
|
|
1158
|
+
if (sm.getTable(schemaName, name) || sm.getView(schemaName, name)) {
|
|
1159
|
+
if (stmt.ifNotExists)
|
|
1160
|
+
return undefined;
|
|
1161
|
+
throw new QuereusError(`Table ${schemaName}.${name} already exists`, StatusCode.CONSTRAINT, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
|
|
1162
|
+
}
|
|
1163
|
+
// An authored `maintained (columns)` list is the explicit/arity-locked form: the
|
|
1164
|
+
// body is renamed positionally to the declared names (name check skipped) and the
|
|
1165
|
+
// declared names are recorded as `derivation.columns`. No list is the implicit
|
|
1166
|
+
// form: the strict name check applies and `derivation.columns` is undefined, so
|
|
1167
|
+
// the canonical DDL omits the clause and the table reshapes its source on reopen.
|
|
1168
|
+
const list = stmt.maintained.columns;
|
|
1169
|
+
const explicit = list !== undefined && list.length > 0;
|
|
1170
|
+
const declared = sm.buildDeclaredTableSchema(stmt);
|
|
1171
|
+
const recordedColumns = explicit ? declared.columns.map(c => c.name) : undefined;
|
|
1172
|
+
const bodySql = astToString(stmt.maintained.select);
|
|
1173
|
+
const shape = deriveBackingShape(db, bodySql, explicit ? recordedColumns : undefined);
|
|
1174
|
+
const mismatch = describeAttachShapeMismatch(declared, shape, explicit);
|
|
1175
|
+
if (mismatch) {
|
|
1176
|
+
throw new QuereusError(`cannot create maintained table '${schemaName}.${name}': ${mismatch}`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
|
|
1177
|
+
}
|
|
1178
|
+
// `preferBacking = true`: route the create through the module's durable backing
|
|
1179
|
+
// seam (`createBacking?() ?? create()`) so a durable-backing module (lamina)
|
|
1180
|
+
// builds the basis `RowStore` that `resolveBackingHost` → `getBackingHost`
|
|
1181
|
+
// resolves below for the attach-to-empty fill. Without this the table is an
|
|
1182
|
+
// ordinary relational collection with no basis store and the fill throws
|
|
1183
|
+
// `backing host not found`. Memory (no `createBacking`) falls through to `create`.
|
|
1184
|
+
const table = await sm.createTable(stmt, /*preferBacking*/ true);
|
|
1185
|
+
try {
|
|
1186
|
+
return await attachMaintainedDerivation(db, table, stmt.maintained.select, explicit ? table.columns.map(c => c.name) : undefined, explicit);
|
|
1187
|
+
}
|
|
1188
|
+
catch (e) {
|
|
1189
|
+
try {
|
|
1190
|
+
await sm.dropTable(schemaName, name, /*ifExists*/ true);
|
|
1191
|
+
}
|
|
1192
|
+
catch { /* best-effort cleanup */ }
|
|
1193
|
+
throw e;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Full-rebuild of a maintained table's contents: re-run the body to completion
|
|
1198
|
+
* and swap the table to the recomputed set. The always-correct path the two
|
|
1199
|
+
* `refresh materialized view` arms funnel through — the fast (data-only) path
|
|
1200
|
+
* (`backingShapeMatches` ⇒ direct `rebuildBacking`) and the reshape arm
|
|
1201
|
+
* (`reshapeBackingInPlace`, between its pre- and post-reconcile structural ops).
|
|
1202
|
+
* It is used by NOTHING else: create/import (`materializeView`) calls
|
|
1203
|
+
* `replaceContents` directly, and the incremental manager's full-rebuild arm
|
|
1204
|
+
* (`applyFullRebuild` in `core/database-materialized-views.ts`) does its own
|
|
1205
|
+
* `applyMaintenance` + per-delta validation (`validateDerivedChanges`).
|
|
1206
|
+
*
|
|
1207
|
+
* **Constraint-bearing branch.** When the maintained table declares ≥1 applicable
|
|
1208
|
+
* CHECK or (FK-enforcement-on) child-side FK ({@link hasApplicableConstraints}),
|
|
1209
|
+
* the swap mirrors the attach core instead of calling `replaceContents`: reject
|
|
1210
|
+
* duplicate derived keys ({@link assertRefreshRowsAreSet} — parity with
|
|
1211
|
+
* `replaceContents`'s set gate), land the recomputed set in the connection's
|
|
1212
|
+
* PENDING layer via `applyMaintenance('replace-all')`, run the eager bulk
|
|
1213
|
+
* anti-join / `not (<check>)` scan ({@link validateDeclaredConstraintsOverContents})
|
|
1214
|
+
* which throws the maintained-table-attributed CONSTRAINT diagnostic on the first
|
|
1215
|
+
* violator BEFORE the swap is committed (the failing statement unwinds and the
|
|
1216
|
+
* pending reconcile is discarded by statement-level rollback — the pre-refresh
|
|
1217
|
+
* COMMITTED contents stay intact), then `conn.commit()`.
|
|
1218
|
+
*
|
|
1219
|
+
* The commit is **commit-first parity** and load-bearing two ways: (1)
|
|
1220
|
+
* `replaceContents` already swaps committed state (a `begin; refresh; rollback`
|
|
1221
|
+
* does NOT undo a refresh today), so committing here preserves that exact
|
|
1222
|
+
* observable behavior; (2) on the reshape arm, `reshapeBackingInPlace`'s
|
|
1223
|
+
* post-reconcile data-validating ops (retype/recollate/tighten-NOT-NULL) scan
|
|
1224
|
+
* COMMITTED contents after this returns, so they must see the rebuilt rows —
|
|
1225
|
+
* `replaceContents` gives that implicitly, the pending-layer branch matches it by
|
|
1226
|
+
* committing (as the attach reshape path does before its own post-reconcile ops).
|
|
1227
|
+
*
|
|
1228
|
+
* The real-world trigger is a STALE table: a body-relevant source change released
|
|
1229
|
+
* the MV's row-time plan, subsequent source writes drifted unvalidated, and a
|
|
1230
|
+
* refresh recomputes that drifted set — so this scan is where a declared CHECK/FK
|
|
1231
|
+
* is enforced over rows that never crossed the maintenance boundary. A
|
|
1232
|
+
* continuously-maintained table re-derives an already-validated set, so the scan
|
|
1233
|
+
* is redundant-but-cheap there.
|
|
1234
|
+
*
|
|
1235
|
+
* Constraint-less maintained tables and every MV-sugar backing take the untouched
|
|
1236
|
+
* `replaceContents` fast path — no connection, no scan, byte-for-byte the prior
|
|
1237
|
+
* behavior. The caller is responsible for staleness re-validation when relevant;
|
|
1238
|
+
* this helper assumes the derivation body plans. Throws if the table is missing
|
|
1239
|
+
* from the catalog.
|
|
1240
|
+
*/
|
|
1241
|
+
export async function rebuildBacking(db, mv) {
|
|
1242
|
+
const bodySql = astToString(mv.derivation.selectAst);
|
|
1243
|
+
const rows = await collectBodyRows(db, bodySql);
|
|
1244
|
+
const backing = db.schemaManager.getTable(mv.schemaName, mv.name);
|
|
1245
|
+
if (!backing) {
|
|
1246
|
+
throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during rebuild`, StatusCode.INTERNAL);
|
|
1247
|
+
}
|
|
1248
|
+
const host = resolveBackingHost(db, backing);
|
|
1249
|
+
if (!isMaintainedTable(backing) || !hasApplicableConstraints(db, backing)) {
|
|
1250
|
+
// Fast path: nothing declared to validate (every MV-sugar backing, and a
|
|
1251
|
+
// constraint-less table-form maintained table). `replaceContents` swaps
|
|
1252
|
+
// COMMITTED contents and runs no derived-row validation — byte-for-byte the
|
|
1253
|
+
// historical path. (A pragma-off FK-only table also lands here: its bulk FK
|
|
1254
|
+
// scan would no-op anyway — see hasApplicableConstraints.)
|
|
1255
|
+
await host.replaceContents(rows, () => materializedViewNotASetError(mv.schemaName, mv.name));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
// Constraint-bearing branch: pending-layer replace-all + eager bulk scan, then a
|
|
1259
|
+
// commit-first commit (see the docstring). `shapePk` is the live backing's
|
|
1260
|
+
// physical key — re-derived shape matches it on the fast path, and on the reshape
|
|
1261
|
+
// arm the catalog was already re-registered with the post-reshape PK before this
|
|
1262
|
+
// runs, so the live `primaryKeyDefinition` is the correct keying either way.
|
|
1263
|
+
const shapePk = backing.primaryKeyDefinition.map(c => ({
|
|
1264
|
+
index: c.index,
|
|
1265
|
+
collation: c.collation ?? backing.columns[c.index]?.collation,
|
|
1266
|
+
}));
|
|
1267
|
+
assertRefreshRowsAreSet(rows, shapePk, mv.schemaName, mv.name);
|
|
1268
|
+
const conn = await resolveAttachConnection(db, host, `${mv.schemaName}.${mv.name}`);
|
|
1269
|
+
await host.applyMaintenance(conn, [{ kind: 'replace-all', rows }]);
|
|
1270
|
+
// Throws the maintained-table-attributed diagnostic BEFORE the commit; on a
|
|
1271
|
+
// violation the failing statement unwinds and discards the pending reconcile,
|
|
1272
|
+
// leaving the pre-refresh committed contents intact (the MV stays stale, so the
|
|
1273
|
+
// next read re-validates rather than serving the rejected set).
|
|
1274
|
+
//
|
|
1275
|
+
// Documented limitation (collation-sensitive CHECK on the reshape arm): on the
|
|
1276
|
+
// reshape path this scan validates the rows in their PRE-recollate physical form
|
|
1277
|
+
// — the catalog column still carries the OLD collation here, and any
|
|
1278
|
+
// `recollate` op runs post-reconcile in reshapeBackingInPlace, AFTER this commit.
|
|
1279
|
+
// So a CHECK whose truth flips under a recollate-during-reshape (e.g. `v <> 'abc'`
|
|
1280
|
+
// with v recollated BINARY → NOCASE over a row 'ABC') passes here and is then
|
|
1281
|
+
// recollated into a violating state. The same class of corner applies to a
|
|
1282
|
+
// `retype` op (it shares this `postReconcileOps` batch): a CHECK whose truth flips
|
|
1283
|
+
// under the column's NEW affinity (e.g. `v < '9'` retyped TEXT → INTEGER over a
|
|
1284
|
+
// row '10' — lexicographic-true then numeric-false) passes here and is retyped into
|
|
1285
|
+
// violation. Not closed: this commit is load-bearing (commit-first parity + the
|
|
1286
|
+
// post-reconcile ops scan committed contents), and the attach reshape path uses the
|
|
1287
|
+
// identical ordering. See docs/materialized-views.md § REFRESH MATERIALIZED VIEW
|
|
1288
|
+
// "Known limitation — collation-sensitive CHECK" / "… type-sensitive CHECK" and
|
|
1289
|
+
// maintained-table-refresh-revalidation.spec.ts § "reshape arm: collation-
|
|
1290
|
+
// sensitive CHECK" / "reshape arm: type-sensitive CHECK".
|
|
1291
|
+
await validateDeclaredConstraintsOverContents(db, backing);
|
|
1292
|
+
await conn.commit();
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* True iff the live backing `TableSchema` is structurally identical to what the
|
|
1296
|
+
* derived `shape` would build — so a `refresh` can take the data-only fast path
|
|
1297
|
+
* (`rebuildBacking`, preserving the backing identity and warm caches) instead of
|
|
1298
|
+
* rebuilding the backing table. Compares, in order:
|
|
1299
|
+
* - column **count**;
|
|
1300
|
+
* - per column: **name** (case-insensitive — matching the matcher's name compare),
|
|
1301
|
+
* **logical type**, **not-null**, **collation**;
|
|
1302
|
+
* - the **physical** PK ({@link computeBackingPrimaryKey} vs the backing's
|
|
1303
|
+
* `primaryKeyDefinition`, by index + desc + collation, in order).
|
|
1304
|
+
*
|
|
1305
|
+
* Returns false when a source schema change has shifted the body's output shape
|
|
1306
|
+
* (most visibly a `select *` body whose new source column interleaves into the
|
|
1307
|
+
* output) — the caller then rebuilds the backing to match the re-planned body.
|
|
1308
|
+
*/
|
|
1309
|
+
export function backingShapeMatches(current, shape) {
|
|
1310
|
+
if (!backingShapeMatchesStructurally(current, shape))
|
|
1311
|
+
return false;
|
|
1312
|
+
for (let i = 0; i < shape.columns.length; i++) {
|
|
1313
|
+
if (current.columns[i].name.toLowerCase() !== shape.columns[i].name.toLowerCase())
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* The structural (name-blind) half of {@link backingShapeMatches}: column count,
|
|
1320
|
+
* per-column logical type / not-null / collation, and the physical PK. The rename
|
|
1321
|
+
* propagation ({@link propagateColumnRenameToMaterializedViews}) uses it to assert
|
|
1322
|
+
* a source column rename produced a *pure name shift* in the body's output before
|
|
1323
|
+
* carrying the new names onto the live backing — anything structural is not a
|
|
1324
|
+
* rename outcome and fails the propagation instead of rebuilding data.
|
|
1325
|
+
*/
|
|
1326
|
+
function backingShapeMatchesStructurally(current, shape) {
|
|
1327
|
+
return describeBackingShapeMismatch(current, shape) === null;
|
|
1328
|
+
}
|
|
1329
|
+
/* ──────────────── shared backing-column comparison primitives ────────────────
|
|
1330
|
+
* The per-column attribute comparisons below are the single shape-diff vocabulary
|
|
1331
|
+
* shared by the positional {@link describeBackingShapeMismatch} (the rename
|
|
1332
|
+
* propagation's "pure name shift?" assertion) and the alignment-based
|
|
1333
|
+
* {@link classifyBackingReshape} (refresh's in-place reshape gate) — neither rolls
|
|
1334
|
+
* its own column compare. All compare by NAME / normalized value, not identity:
|
|
1335
|
+
* logical types resolve through the (name-interned) registry, but a module may
|
|
1336
|
+
* rebuild its TableSchema with fresh instances after an ALTER (the store module
|
|
1337
|
+
* does), so object identity is spuriously false. */
|
|
1338
|
+
/** The two columns carry the same logical type (by interned type name). */
|
|
1339
|
+
function backingTypeMatches(a, b) {
|
|
1340
|
+
return a.logicalType.name.toUpperCase() === b.logicalType.name.toUpperCase();
|
|
1341
|
+
}
|
|
1342
|
+
/** The two columns agree on NOT NULL. */
|
|
1343
|
+
function backingNotNullMatches(a, b) {
|
|
1344
|
+
return (a.notNull === true) === (b.notNull === true);
|
|
1345
|
+
}
|
|
1346
|
+
/** The two columns agree on declared collation (absent ⇒ BINARY). */
|
|
1347
|
+
function backingCollationMatches(a, b) {
|
|
1348
|
+
return (a.collation ?? 'BINARY') === (b.collation ?? 'BINARY');
|
|
1349
|
+
}
|
|
1350
|
+
/** Names the first structural difference between the live backing and the derived
|
|
1351
|
+
* shape (null when structurally identical) — the diagnostic half of
|
|
1352
|
+
* {@link backingShapeMatchesStructurally}. Deliberately **positional** (column i
|
|
1353
|
+
* vs column i): it answers "is this a pure positional name shift (or identical)?"
|
|
1354
|
+
* for the rename-propagation pass, which only ever carries names. The richer
|
|
1355
|
+
* alignment that tolerates appended / dropped / renamed columns is
|
|
1356
|
+
* {@link classifyBackingReshape}; both share the per-column predicates above. */
|
|
1357
|
+
function describeBackingShapeMismatch(current, shape) {
|
|
1358
|
+
if (current.columns.length !== shape.columns.length) {
|
|
1359
|
+
return `column count ${current.columns.length} → ${shape.columns.length}`;
|
|
1360
|
+
}
|
|
1361
|
+
for (let i = 0; i < shape.columns.length; i++) {
|
|
1362
|
+
const a = current.columns[i];
|
|
1363
|
+
const b = shape.columns[i];
|
|
1364
|
+
if (!backingTypeMatches(a, b)) {
|
|
1365
|
+
return `column ${i} type ${a.logicalType.name} → ${b.logicalType.name}`;
|
|
1366
|
+
}
|
|
1367
|
+
if (!backingNotNullMatches(a, b)) {
|
|
1368
|
+
return `column ${i} not-null ${a.notNull === true} → ${b.notNull === true}`;
|
|
1369
|
+
}
|
|
1370
|
+
if (!backingCollationMatches(a, b)) {
|
|
1371
|
+
return `column ${i} collation ${a.collation ?? 'BINARY'} → ${b.collation ?? 'BINARY'}`;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const shapePk = computeBackingPrimaryKey(shape);
|
|
1375
|
+
const currentPk = current.primaryKeyDefinition;
|
|
1376
|
+
if (currentPk.length !== shapePk.length) {
|
|
1377
|
+
return `primary-key length ${currentPk.length} → ${shapePk.length}`;
|
|
1378
|
+
}
|
|
1379
|
+
for (let i = 0; i < shapePk.length; i++) {
|
|
1380
|
+
if (currentPk[i].index !== shapePk[i].index) {
|
|
1381
|
+
return `primary-key column ${i} index ${currentPk[i].index} → ${shapePk[i].index}`;
|
|
1382
|
+
}
|
|
1383
|
+
if ((currentPk[i].desc === true) !== (shapePk[i].desc === true)) {
|
|
1384
|
+
return `primary-key column ${i} direction`;
|
|
1385
|
+
}
|
|
1386
|
+
const shapeColl = shape.columns[shapePk[i].index]?.collation ?? 'BINARY';
|
|
1387
|
+
if ((currentPk[i].collation ?? 'BINARY') !== shapeColl) {
|
|
1388
|
+
return `primary-key column ${i} collation ${currentPk[i].collation ?? 'BINARY'} → ${shapeColl}`;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
/* ──────────────── body-irrelevant source change: recompile, never skip ────────────────
|
|
1394
|
+
* A `table_modified` whose old/new differ only in fields a body cannot read —
|
|
1395
|
+
* constraint metadata (CHECK exprs, FK targets, UNIQUE sets, index predicates),
|
|
1396
|
+
* `statistics`/`estimatedRows` (ANALYZE), `tags`, column defaults — cannot change
|
|
1397
|
+
* what a dependent MV's body *evaluates to*. But it CAN change what the body
|
|
1398
|
+
* **compiles to**: CHECK constraints seed domain facts (`ruleFilterContradiction`
|
|
1399
|
+
* may have folded a filter — or the whole body — away against a CHECK that no
|
|
1400
|
+
* longer holds), and `proveOneToOneJoin`'s join-residual arm rests on NOT-NULL
|
|
1401
|
+
* FK→PK referential integrity. So the MV manager's schema-change listener routes
|
|
1402
|
+
* live dependents of a qualifying event through an in-place RECOMPILE
|
|
1403
|
+
* ({@link tryRecompileMaterializedViewLive}) instead of marking them stale —
|
|
1404
|
+
* recompile, never skip. Any failure falls back to the mark-stale path. */
|
|
1405
|
+
/** The per-column fields a body can observe: name, logical type, NOT NULL,
|
|
1406
|
+
* collation (absent ⇒ BINARY), and the generated expression. `defaultValue`
|
|
1407
|
+
* and per-column conflict metadata are deliberately IGNORED — a body reads
|
|
1408
|
+
* stored values, never source defaults; the recompile-not-skip discipline
|
|
1409
|
+
* covers any optimizer-level concern. */
|
|
1410
|
+
function bodyRelevantColumnMatches(a, b) {
|
|
1411
|
+
return a.name.toLowerCase() === b.name.toLowerCase()
|
|
1412
|
+
&& backingTypeMatches(a, b)
|
|
1413
|
+
&& backingNotNullMatches(a, b)
|
|
1414
|
+
&& backingCollationMatches(a, b)
|
|
1415
|
+
&& (a.generated === true) === (b.generated === true)
|
|
1416
|
+
&& (!a.generated || sameGeneratedExpr(a, b));
|
|
1417
|
+
}
|
|
1418
|
+
function sameGeneratedExpr(a, b) {
|
|
1419
|
+
if ((a.generatedExpr === undefined) !== (b.generatedExpr === undefined))
|
|
1420
|
+
return false;
|
|
1421
|
+
if (!a.generatedExpr || !b.generatedExpr)
|
|
1422
|
+
return true;
|
|
1423
|
+
return expressionToString(a.generatedExpr) === expressionToString(b.generatedExpr);
|
|
1424
|
+
}
|
|
1425
|
+
/** Pairwise physical-PK identity (`index`, `desc`, effective per-component
|
|
1426
|
+
* collation — explicit, else the keyed column's, else BINARY). */
|
|
1427
|
+
function samePrimaryKeyDefinition(a, b) {
|
|
1428
|
+
if (a.primaryKeyDefinition.length !== b.primaryKeyDefinition.length)
|
|
1429
|
+
return false;
|
|
1430
|
+
return a.primaryKeyDefinition.every((pa, i) => {
|
|
1431
|
+
const pb = b.primaryKeyDefinition[i];
|
|
1432
|
+
const collA = pa.collation ?? a.columns[pa.index]?.collation ?? 'BINARY';
|
|
1433
|
+
const collB = pb.collation ?? b.columns[pb.index]?.collation ?? 'BINARY';
|
|
1434
|
+
return pa.index === pb.index
|
|
1435
|
+
&& (pa.desc === true) === (pb.desc === true)
|
|
1436
|
+
&& collA === collB;
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* True when a `table_modified` event's old→new transition is **body-irrelevant**:
|
|
1441
|
+
* same table name and schema, columns pairwise identical in every body-relevant
|
|
1442
|
+
* field ({@link bodyRelevantColumnMatches}), and an identical physical primary
|
|
1443
|
+
* key. Everything else may differ — `checkConstraints`, `foreignKeys`,
|
|
1444
|
+
* `uniqueConstraints`, `indexes`, `statistics`, `estimatedRows`, `tags`,
|
|
1445
|
+
* `primaryKeyDefaultConflict`, defaults. A qualifying event cannot change what a
|
|
1446
|
+
* dependent body evaluates to, only what it compiles to — see the section note
|
|
1447
|
+
* above for why dependents are recompiled rather than skipped.
|
|
1448
|
+
*
|
|
1449
|
+
* **Reference-equality guard (load-bearing coupling).** The MV manager's
|
|
1450
|
+
* `emitBackingInvalidation` fires a synthetic `table_modified` on an MV's own
|
|
1451
|
+
* backing with the SAME object as `oldObject` and `newObject` — the event that
|
|
1452
|
+
* cascades staleness down MV-over-MV chains. It must classify as body-RELEVANT,
|
|
1453
|
+
* hence `oldObject === newObject` short-circuits to false here. Every genuine
|
|
1454
|
+
* emitter passes distinct old/new objects. If either side changes, change both
|
|
1455
|
+
* (see the matching comment in `emitBackingInvalidation`,
|
|
1456
|
+
* core/database-materialized-views.ts).
|
|
1457
|
+
*/
|
|
1458
|
+
export function isBodyIrrelevantTableChange(oldObject, newObject) {
|
|
1459
|
+
if (oldObject === newObject)
|
|
1460
|
+
return false;
|
|
1461
|
+
if (oldObject.name.toLowerCase() !== newObject.name.toLowerCase())
|
|
1462
|
+
return false;
|
|
1463
|
+
if (oldObject.schemaName.toLowerCase() !== newObject.schemaName.toLowerCase())
|
|
1464
|
+
return false;
|
|
1465
|
+
if (oldObject.columns.length !== newObject.columns.length)
|
|
1466
|
+
return false;
|
|
1467
|
+
for (let i = 0; i < oldObject.columns.length; i++) {
|
|
1468
|
+
if (!bodyRelevantColumnMatches(oldObject.columns[i], newObject.columns[i]))
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
return samePrimaryKeyDefinition(oldObject, newObject);
|
|
1472
|
+
}
|
|
1473
|
+
/** Structural (name-blind) column-only check: count + per-column type/not-null/collation,
|
|
1474
|
+
* WITHOUT comparing the physical PK. Used by the superkey relaxation in
|
|
1475
|
+
* `tryRecompileMaterializedViewLive` to gate the PK-changing case where column
|
|
1476
|
+
* attributes are otherwise identical. */
|
|
1477
|
+
function backingColumnsStructurallyMatch(current, shape) {
|
|
1478
|
+
if (current.columns.length !== shape.columns.length)
|
|
1479
|
+
return false;
|
|
1480
|
+
for (let i = 0; i < shape.columns.length; i++) {
|
|
1481
|
+
const a = current.columns[i];
|
|
1482
|
+
const b = shape.columns[i];
|
|
1483
|
+
if (!backingTypeMatches(a, b))
|
|
1484
|
+
return false;
|
|
1485
|
+
if (!backingNotNullMatches(a, b))
|
|
1486
|
+
return false;
|
|
1487
|
+
if (!backingCollationMatches(a, b))
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
/** Returns true when the live backing's physical PK column set is a superkey of the
|
|
1493
|
+
* re-planned body — i.e., some proved minimal key from `shape.allProvedKeys` is
|
|
1494
|
+
* entirely contained in the backing PK's column set. Returns false when
|
|
1495
|
+
* `allProvedKeys` is absent (coarsened-lineage or all-columns path). */
|
|
1496
|
+
function isBackingPkASuperkeyInShape(current, shape) {
|
|
1497
|
+
if (!shape.allProvedKeys)
|
|
1498
|
+
return false;
|
|
1499
|
+
const backingPkCols = new Set(current.primaryKeyDefinition.map(pk => pk.index));
|
|
1500
|
+
return shape.allProvedKeys.some(k => k.every(idx => backingPkCols.has(idx)));
|
|
1501
|
+
}
|
|
1502
|
+
/* ──────────────── content-stability proof (structural-ALTER keep-live) ────────────────
|
|
1503
|
+
* For a CONSTRAINT-only `table_modified`, re-derived backing-shape identity implies
|
|
1504
|
+
* content identity (a constraint cannot change what stored rows the body evaluates
|
|
1505
|
+
* to, only what the body compiles to), so a recompile against the new catalog is
|
|
1506
|
+
* sufficient — that is the constraint-only path. For a STRUCTURAL ALTER (ADD / DROP /
|
|
1507
|
+
* ALTER COLUMN) the same argument does NOT carry: shape identity ⇏ content identity.
|
|
1508
|
+
* The classic hazard is `alter column v set collate nocase` (or `set data type`) on a
|
|
1509
|
+
* column the body uses only in a WHERE / join / group / order position — the output
|
|
1510
|
+
* shape is unchanged (v is unprojected), yet the row set the predicate admits, hence
|
|
1511
|
+
* the backing content, changes. So a structural keep-live additionally proves the
|
|
1512
|
+
* value-semantics of the change is DISJOINT from everything the body reads. The two
|
|
1513
|
+
* helpers below compute the two sides of that proof; the gate lives in
|
|
1514
|
+
* {@link tryRecompileMaterializedViewLive}. */
|
|
1515
|
+
/**
|
|
1516
|
+
* The columns whose **value semantics** the `oldObject → newObject` transition
|
|
1517
|
+
* changed: present **by name in both** schemas and differing in logical type or
|
|
1518
|
+
* collation. Returns lowercased column names (the column-index map key). NOT NULL,
|
|
1519
|
+
* default, generated-expr-unchanged, and add/drop are deliberately excluded —
|
|
1520
|
+
* NOT NULL / default are content-irrelevant (a body reads stored values, never
|
|
1521
|
+
* constraints or defaults), and an add/drop that the body reads is already caught
|
|
1522
|
+
* upstream (a `select *` reshapes ⇒ shape mismatch; a referenced dropped column
|
|
1523
|
+
* fails re-derivation; a referenced added column cannot exist in the authored body).
|
|
1524
|
+
* So the set is EMPTY for every change except ALTER COLUMN SET DATA TYPE / SET
|
|
1525
|
+
* COLLATE — making the disjointness proof a no-op (today's behavior) elsewhere.
|
|
1526
|
+
*/
|
|
1527
|
+
function valueSemanticsChangedColumns(oldObject, newObject) {
|
|
1528
|
+
const out = new Set();
|
|
1529
|
+
for (const newCol of newObject.columns) {
|
|
1530
|
+
const oldCol = oldObject.columns.find(c => c.name.toLowerCase() === newCol.name.toLowerCase());
|
|
1531
|
+
if (!oldCol)
|
|
1532
|
+
continue; // added column — no value-semantics change to an existing column
|
|
1533
|
+
if (!backingTypeMatches(oldCol, newCol) || !backingCollationMatches(oldCol, newCol)) {
|
|
1534
|
+
out.add(newCol.name.toLowerCase());
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return out;
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* The set of source-column indices (in `qualifiedSource`'s POST-ALTER schema) that a
|
|
1541
|
+
* materialized-view body **reads** — directly, through a predicate / join / group /
|
|
1542
|
+
* order position, or transitively through a generated column. The disjointness half
|
|
1543
|
+
* of the structural-ALTER content-stability gate (see the section note and
|
|
1544
|
+
* {@link tryRecompileMaterializedViewLive}): a value-semantics change (type /
|
|
1545
|
+
* collation) to a column NOT in this set cannot alter what the body evaluates to.
|
|
1546
|
+
*
|
|
1547
|
+
* **Why the un-optimized built plan, not `db.getPlan`.** The optimizer can absorb a
|
|
1548
|
+
* `where v = 'x'` predicate into an access-method seek key, dropping the explicit
|
|
1549
|
+
* {@link ColumnReferenceNode} from the tree — walking the optimized plan would MISS
|
|
1550
|
+
* that reference and falsely conclude disjoint (UNSOUND). The un-optimized built plan
|
|
1551
|
+
* (`db._buildPlan`) carries every reference explicitly in its projection / filter /
|
|
1552
|
+
* join / group / order nodes. Over-approximation is the safe direction: an extra
|
|
1553
|
+
* column in the read set only ever causes MORE staleness, never an unsound keep-live.
|
|
1554
|
+
*
|
|
1555
|
+
* Mechanics: walk the built tree (children AND relations, like {@link collectSourceTables},
|
|
1556
|
+
* so nested subqueries / EXISTS / correlated refs are reached) collecting every
|
|
1557
|
+
* `ColumnReferenceNode.attributeId`; for every `TableReferenceNode` whose qualified
|
|
1558
|
+
* name equals `qualifiedSource` (several for a self-join) map the collected attribute
|
|
1559
|
+
* ids back to its column indices via `getAttributes()`; union over occurrences. Then
|
|
1560
|
+
* expand the set DOWNWARD through `generatedColumnDependencies` to a fixed point —
|
|
1561
|
+
* reading a generated column reads its dependency columns even when the body never
|
|
1562
|
+
* names them (safe whether or not the planner inlines generated columns: if it does,
|
|
1563
|
+
* the dep already appears as a direct reference and the closure is a no-op; if it
|
|
1564
|
+
* does not, the closure is load-bearing).
|
|
1565
|
+
*
|
|
1566
|
+
* The rewrite is suppressed for the same reason {@link deriveBackingShape} suppresses
|
|
1567
|
+
* it. Any exception propagates to {@link tryRecompileMaterializedViewLive}'s try/catch,
|
|
1568
|
+
* which treats a failed analysis as "could not prove disjoint" ⇒ stale (the safe
|
|
1569
|
+
* default) — it must never be swallowed into a false "disjoint" conclusion.
|
|
1570
|
+
*/
|
|
1571
|
+
export function referencedSourceColumns(db, bodySql, qualifiedSource) {
|
|
1572
|
+
const targetName = qualifiedSource.toLowerCase();
|
|
1573
|
+
return db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
|
|
1574
|
+
const ast = new Parser().parse(bodySql);
|
|
1575
|
+
const { plan } = db._buildPlan([ast]);
|
|
1576
|
+
const referencedAttrIds = new Set();
|
|
1577
|
+
const sourceRefs = [];
|
|
1578
|
+
const visited = new Set();
|
|
1579
|
+
const walk = (node) => {
|
|
1580
|
+
if (visited.has(node))
|
|
1581
|
+
return;
|
|
1582
|
+
visited.add(node);
|
|
1583
|
+
if (node instanceof ColumnReferenceNode) {
|
|
1584
|
+
referencedAttrIds.add(node.attributeId);
|
|
1585
|
+
}
|
|
1586
|
+
else if (node instanceof TableReferenceNode
|
|
1587
|
+
&& `${node.tableSchema.schemaName}.${node.tableSchema.name}`.toLowerCase() === targetName) {
|
|
1588
|
+
sourceRefs.push(node);
|
|
1589
|
+
}
|
|
1590
|
+
for (const c of node.getChildren())
|
|
1591
|
+
walk(c);
|
|
1592
|
+
for (const r of node.getRelations())
|
|
1593
|
+
walk(r);
|
|
1594
|
+
};
|
|
1595
|
+
walk(plan);
|
|
1596
|
+
const cols = new Set();
|
|
1597
|
+
let deps;
|
|
1598
|
+
for (const ref of sourceRefs) {
|
|
1599
|
+
const attrs = ref.getAttributes();
|
|
1600
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
1601
|
+
if (referencedAttrIds.has(attrs[i].id))
|
|
1602
|
+
cols.add(i);
|
|
1603
|
+
}
|
|
1604
|
+
// The TableReferenceNode is built from the live (post-ALTER) catalog, so its
|
|
1605
|
+
// schema IS `newObject`; all S occurrences share it.
|
|
1606
|
+
deps ??= ref.tableSchema.generatedColumnDependencies;
|
|
1607
|
+
}
|
|
1608
|
+
if (deps)
|
|
1609
|
+
expandGeneratedDependencyClosure(cols, deps);
|
|
1610
|
+
return cols;
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
/** Expand `cols` downward through `deps` (generated-column index → dependency column
|
|
1614
|
+
* indices) to a fixed point: a read of a generated column is a read of its dependency
|
|
1615
|
+
* columns, which may themselves be generated. */
|
|
1616
|
+
function expandGeneratedDependencyClosure(cols, deps) {
|
|
1617
|
+
const queue = [...cols];
|
|
1618
|
+
while (queue.length > 0) {
|
|
1619
|
+
const idx = queue.pop();
|
|
1620
|
+
const depIndices = deps.get(idx);
|
|
1621
|
+
if (!depIndices)
|
|
1622
|
+
continue;
|
|
1623
|
+
for (const d of depIndices) {
|
|
1624
|
+
if (!cols.has(d)) {
|
|
1625
|
+
cols.add(d);
|
|
1626
|
+
queue.push(d);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Recompile a LIVE materialized view's row-time plan in place after a **genuine**
|
|
1633
|
+
* source `table_modified` (`oldObject !== newObject` — constraint/stats/tags-only OR
|
|
1634
|
+
* structural ADD/DROP/ALTER COLUMN), gated by shape re-derivation and — for a
|
|
1635
|
+
* structural value-semantics change — a content-stability proof. The same discipline
|
|
1636
|
+
* as {@link restoreUnaffectedMaterializedViews}. Fully synchronous (the schema-change
|
|
1637
|
+
* listener is sync; shape derivation, schema lookups, the disjointness analysis, and
|
|
1638
|
+
* registration all are). Never throws: logs and returns `false` on any failure, and
|
|
1639
|
+
* the caller falls back to the mark-stale path. On success the MV stays live —
|
|
1640
|
+
* `stale` untouched, row-time plan rebuilt against the new catalog, no backing
|
|
1641
|
+
* invalidation (the backing stays maintained, so cached plans reading it remain
|
|
1642
|
+
* correct).
|
|
1643
|
+
*
|
|
1644
|
+
* **Structural-ALTER soundness (why a recompile is not enough on its own).** For a
|
|
1645
|
+
* constraint-only change, re-derived backing-shape identity IMPLIES content identity —
|
|
1646
|
+
* a constraint cannot change what stored rows the body evaluates to, only what the
|
|
1647
|
+
* body compiles to. For a structural ALTER that argument does NOT carry: shape
|
|
1648
|
+
* identity ⇏ content identity. `alter column v set collate nocase` / `set data type`
|
|
1649
|
+
* on a column the body reads only in a WHERE / join / group / order position leaves
|
|
1650
|
+
* the output shape identical while changing the admitted row set — the backing content
|
|
1651
|
+
* diverges from a fresh body evaluation. So the structural keep-live adds a final
|
|
1652
|
+
* **content-stability gate** proving the change's value-semantics-changed columns are
|
|
1653
|
+
* disjoint from everything the body reads (the {@link valueSemanticsChangedColumns} ∩
|
|
1654
|
+
* {@link referencedSourceColumns} proof). That changed set is EMPTY for constraint-only,
|
|
1655
|
+
* ADD, DROP, NOT NULL, and DEFAULT changes, so the proof is a no-op there and preserves
|
|
1656
|
+
* today's behavior exactly; it does real work only for ALTER COLUMN type/collation.
|
|
1657
|
+
*
|
|
1658
|
+
* Gates, in order — each failure is a stale fallback:
|
|
1659
|
+
* 1. `deriveBackingShape` throws when the body no longer plans against the
|
|
1660
|
+
* post-change catalog (e.g. a rename-cascade constraint rewrite observed
|
|
1661
|
+
* mid-statement, while a co-source's rename has landed but this MV's body
|
|
1662
|
+
* rewrite has not — the rename propagation's own MV loop restores it later).
|
|
1663
|
+
* A DROP COLUMN the body references directly throws here too.
|
|
1664
|
+
* 2. `sameSourceTables`: the re-planned source set must equal the recorded one.
|
|
1665
|
+
* An FK drop can un-eliminate a previously FK/PK-eliminated join (growing
|
|
1666
|
+
* the set); a constraint change can let `ruleFilterContradiction` fold a
|
|
1667
|
+
* source out of the plan entirely (shrinking it). Either way the record is
|
|
1668
|
+
* out of sync with the body's plan — leave it to REFRESH, which re-derives.
|
|
1669
|
+
* 3. `backingColumnsStructurallyMatch` + `isBackingPkASuperkeyInShape`: the column
|
|
1670
|
+
* structural attributes (type / not-null / collation) must match positionally,
|
|
1671
|
+
* AND the live backing's physical PK column set must be a superkey of the
|
|
1672
|
+
* re-planned body (some proved minimal key ⊆ backing PK columns). This forces
|
|
1673
|
+
* staleness when a dropped UNIQUE un-proves the recorded backing key (`keysOf`
|
|
1674
|
+
* falls back to a smaller key or all-columns → no proved key ⊆ old PK). An
|
|
1675
|
+
* ADD CONSTRAINT UNIQUE that subsumes the compound key passes: the new minimal
|
|
1676
|
+
* key is a subset of the old compound backing PK. A `select *` body over an
|
|
1677
|
+
* ADD/DROP COLUMN reshapes its output here ⇒ shape mismatch ⇒ stale. A PROJECTED
|
|
1678
|
+
* column whose type/collation changed shifts the output column ⇒ shape mismatch
|
|
1679
|
+
* ⇒ stale. Re-registers with the EXISTING backing (PK unchanged) on a pass.
|
|
1680
|
+
* 4. Content-stability gate (structural value-semantics ALTER only — see above):
|
|
1681
|
+
* if any value-semantics-changed column (type/collation) is read by the body
|
|
1682
|
+
* (transitively through generated columns), the backing content is unstable ⇒
|
|
1683
|
+
* stale. Empty changed set ⇒ no-op. A failure to build the disjointness analysis
|
|
1684
|
+
* propagates to the outer try/catch ⇒ stale (could not prove disjoint).
|
|
1685
|
+
* 5. `registerMaterializedView` re-runs arm selection / eligibility / cost
|
|
1686
|
+
* gating (`buildMaintenancePlan`) against the new catalog and throws on the
|
|
1687
|
+
* create-time gates (non-determinism, bag/no-key floor, full-rebuild
|
|
1688
|
+
* pathology against fresh ANALYZE stats — defensible: the alternative is
|
|
1689
|
+
* unbounded per-write rebuild cost). Registration is event-silent, so the
|
|
1690
|
+
* success path fires no nested schema-change notifications.
|
|
1691
|
+
*
|
|
1692
|
+
* `oldObject`/`newObject` are the genuine event's distinct schemas. The synthetic
|
|
1693
|
+
* backing-invalidation event (same object as old/new) is excluded by the caller's
|
|
1694
|
+
* `oldObject !== newObject` guard — it must cascade staleness, never recompile.
|
|
1695
|
+
*
|
|
1696
|
+
* Deliberately NOT {@link restoreMaterializedViewLive}: that path is async, may
|
|
1697
|
+
* rename backing columns, and clears `stale` — the wrong discipline here, where
|
|
1698
|
+
* the MV is live throughout and a pre-existing `stale` flag must stay untouched.
|
|
1699
|
+
*/
|
|
1700
|
+
export function tryRecompileMaterializedViewLive(db, mv, oldObject, newObject) {
|
|
1701
|
+
try {
|
|
1702
|
+
const d = mv.derivation;
|
|
1703
|
+
const bodySql = astToString(d.selectAst);
|
|
1704
|
+
const shape = deriveBackingShape(db, bodySql, d.columns);
|
|
1705
|
+
if (!sameSourceTables(d.sourceTables, shape.sourceTables)) {
|
|
1706
|
+
log('Marking materialized view %s.%s stale instead of recompiling: re-planned source tables (%s) disagree with the recorded set (%s) — REFRESH re-derives', mv.schemaName, mv.name, shape.sourceTables.join(', '), d.sourceTables.join(', '));
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
const schema = db.schemaManager.getSchemaOrFail(mv.schemaName);
|
|
1710
|
+
const live = schema.getTable(mv.name);
|
|
1711
|
+
const backing = isMaintainedTable(live) ? live : mv;
|
|
1712
|
+
const mismatch = describeBackingShapeMismatch(backing, shape);
|
|
1713
|
+
if (mismatch) {
|
|
1714
|
+
// Relaxed superkey gate: columns match structurally AND the existing backing
|
|
1715
|
+
// PK column set is still a superkey of the re-planned body (some proved
|
|
1716
|
+
// minimal key is ⊆ the backing PK's column set). Covers ADD CONSTRAINT UNIQUE
|
|
1717
|
+
// that subsumes the compound key — keysOf now returns a smaller key first,
|
|
1718
|
+
// changing the physical PK shape, but the old backing PK is still uniquely
|
|
1719
|
+
// identifying. Re-register with the EXISTING backing (unchanged PK).
|
|
1720
|
+
if (!backingColumnsStructurallyMatch(backing, shape) || !isBackingPkASuperkeyInShape(backing, shape)) {
|
|
1721
|
+
log('Marking materialized view %s.%s stale instead of recompiling: backing shape mismatch (%s) — REFRESH rebuilds it', mv.schemaName, mv.name, mismatch);
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
log('Recompiling materialized view %s.%s with existing backing PK (superkey check passed): %s', mv.schemaName, mv.name, mismatch);
|
|
1725
|
+
}
|
|
1726
|
+
// Name-stability gate. The recompile re-registers against the EXISTING backing, so
|
|
1727
|
+
// it is sound only when the re-derived body's output column NAMES still match the
|
|
1728
|
+
// backing's. `describeBackingShapeMismatch` is deliberately name-blind (it serves the
|
|
1729
|
+
// rename propagation's pure-positional-name-shift detection), so a column RENAME under
|
|
1730
|
+
// a `select *`-style body re-derives a name-blind-identical shape — keeping it live
|
|
1731
|
+
// here would leave the backing column under its OLD name. Decline so the
|
|
1732
|
+
// rename-propagation pass owns the backing rename (restoreUnaffectedMaterializedViews);
|
|
1733
|
+
// an explicit-column body naming the renamed column already declined upstream
|
|
1734
|
+
// (deriveBackingShape threw).
|
|
1735
|
+
if (shape.columns.some((c, i) => c.name.toLowerCase() !== (backing.columns[i]?.name ?? '').toLowerCase())) {
|
|
1736
|
+
log('Marking materialized view %s.%s stale instead of recompiling: re-derived output names shifted (a column rename) — the rename-propagation pass owns the backing rename', mv.schemaName, mv.name);
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
// Content-stability gate. EMPTY for constraint-only / ADD / DROP / NOT NULL /
|
|
1740
|
+
// DEFAULT (no-op — exactly today's behavior); for an ALTER COLUMN type/collation
|
|
1741
|
+
// it proves the change is disjoint from every column the body reads (directly or
|
|
1742
|
+
// transitively through generated columns), else the backing content is unstable.
|
|
1743
|
+
const valueChanged = valueSemanticsChangedColumns(oldObject, newObject);
|
|
1744
|
+
if (valueChanged.size > 0) {
|
|
1745
|
+
const source = `${newObject.schemaName}.${newObject.name}`.toLowerCase();
|
|
1746
|
+
const read = referencedSourceColumns(db, bodySql, source);
|
|
1747
|
+
const collidingName = [...valueChanged].find(name => {
|
|
1748
|
+
const idx = newObject.columnIndexMap.get(name);
|
|
1749
|
+
return idx !== undefined && read.has(idx);
|
|
1750
|
+
});
|
|
1751
|
+
if (collidingName !== undefined) {
|
|
1752
|
+
log("Marking materialized view %s.%s stale instead of recompiling: a value-semantics ALTER (type/collation) on '%s' — a column the body reads — changes backing content; REFRESH re-derives", mv.schemaName, mv.name, collidingName);
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
log('Recompiling materialized view %s.%s after a value-semantics ALTER (type/collation) on column(s) the body does not read (%s)', mv.schemaName, mv.name, [...valueChanged].join(', '));
|
|
1756
|
+
}
|
|
1757
|
+
db.registerMaterializedView(backing);
|
|
1758
|
+
log('Recompiled materialized view %s.%s in place after a genuine source change', mv.schemaName, mv.name);
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
catch (e) {
|
|
1762
|
+
log('Marking materialized view %s.%s stale instead of recompiling after a genuine source change: %s', mv.schemaName, mv.name, e instanceof Error ? e.message : String(e));
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Classifies the column-level delta old(`current`)→new(`shape`) for an
|
|
1768
|
+
* identity-preserving refresh reshape. **Expressible in place** — returns the
|
|
1769
|
+
* ordered module-op plan — iff the change is any combination of **trailing**
|
|
1770
|
+
* appended columns, dropped columns, positionally renamed columns, and per-column
|
|
1771
|
+
* attribute (type / collation / not-null) changes, with the surviving columns'
|
|
1772
|
+
* relative order preserved and the physical primary key unchanged. Otherwise
|
|
1773
|
+
* **inexpressible** (the caller raises a sited error and leaves the table
|
|
1774
|
+
* untouched):
|
|
1775
|
+
*
|
|
1776
|
+
* - an **interleaving** reorder — a new column landing mid-table (the canonical
|
|
1777
|
+
* `select *` body whose new source column lands before existing outputs):
|
|
1778
|
+
* append-only `addColumn` cannot place it, and renaming survivors to fake it
|
|
1779
|
+
* would silently re-map values;
|
|
1780
|
+
* - a **physical-PK definition change** (column set, order, direction,
|
|
1781
|
+
* collation, or a key column's type) — a maintained table's PK is its
|
|
1782
|
+
* replicated row identity; silently re-keying it is the fatality drop+recreate
|
|
1783
|
+
* was.
|
|
1784
|
+
*
|
|
1785
|
+
* Surviving columns are matched by **name** (case-insensitive — the only stable
|
|
1786
|
+
* identity a derived backing carries); a name absent on both sides at an aligned
|
|
1787
|
+
* position is a positional rename (the value-preserving trace
|
|
1788
|
+
* {@link renameShiftedBackingColumns} already uses). Shares the per-column
|
|
1789
|
+
* predicates with {@link describeBackingShapeMismatch} (the positional pure-name-
|
|
1790
|
+
* shift check) rather than re-implementing the column compare.
|
|
1791
|
+
*
|
|
1792
|
+
* The resulting plan is two-phase (see {@link ReshapePlan}): the structural,
|
|
1793
|
+
* data-lossless ops (`rename`/`add`/`loosenNotNull`/`drop`) go pre-reconcile; the
|
|
1794
|
+
* data-validating attribute shifts (`retype`/`recollate`) and every deferred
|
|
1795
|
+
* NOT NULL `tightenNotNull` go post-reconcile, so they validate the reconciled
|
|
1796
|
+
* body rows rather than the discarded backing.
|
|
1797
|
+
*/
|
|
1798
|
+
function classifyBackingReshape(current, shape) {
|
|
1799
|
+
const cur = current.columns;
|
|
1800
|
+
const sh = shape.columns;
|
|
1801
|
+
const curNames = new Set(cur.map(c => c.name.toLowerCase()));
|
|
1802
|
+
const shNames = new Set(sh.map(c => c.name.toLowerCase()));
|
|
1803
|
+
const renames = [];
|
|
1804
|
+
const adds = [];
|
|
1805
|
+
const loosens = []; // pre-reconcile: NOT NULL loosen never throws on data
|
|
1806
|
+
const drops = [];
|
|
1807
|
+
const postReconcileOps = []; // retype / recollate / tightenNotNull — validate the reconciled body
|
|
1808
|
+
// lower(oldName) → lower(newName), for the rename-aware PK comparison below.
|
|
1809
|
+
const renameMap = new Map();
|
|
1810
|
+
// A survivor's attribute shift. The data-validating shifts (type/collation
|
|
1811
|
+
// retype, NOT NULL *tightening*) defer to the post-reconcile batch — the live
|
|
1812
|
+
// rows may still violate them, but the re-derived body rows will not. A NOT NULL
|
|
1813
|
+
// *loosening* never throws on data, so it stays pre-reconcile. `name` is the
|
|
1814
|
+
// column's post-rename (new) name.
|
|
1815
|
+
const recordAttrShift = (from, to, name) => {
|
|
1816
|
+
if (!backingTypeMatches(from, to))
|
|
1817
|
+
postReconcileOps.push({ kind: 'retype', name, newTypeName: to.logicalType.name });
|
|
1818
|
+
if (!backingCollationMatches(from, to))
|
|
1819
|
+
postReconcileOps.push({ kind: 'recollate', name, collation: to.collation ?? 'BINARY' });
|
|
1820
|
+
if (!backingNotNullMatches(from, to)) {
|
|
1821
|
+
if (to.notNull === true)
|
|
1822
|
+
postReconcileOps.push({ kind: 'tightenNotNull', name });
|
|
1823
|
+
else
|
|
1824
|
+
loosens.push({ kind: 'loosenNotNull', name });
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
let i = 0, j = 0;
|
|
1828
|
+
while (i < cur.length && j < sh.length) {
|
|
1829
|
+
const cc = cur[i], sc = sh[j];
|
|
1830
|
+
const cn = cc.name.toLowerCase(), sn = sc.name.toLowerCase();
|
|
1831
|
+
if (cn === sn) {
|
|
1832
|
+
recordAttrShift(cc, sc, sc.name);
|
|
1833
|
+
i++;
|
|
1834
|
+
j++;
|
|
1835
|
+
}
|
|
1836
|
+
else if (!shNames.has(cn) && !curNames.has(sn)) {
|
|
1837
|
+
// Aligned position, both names "extra" ⇒ positional rename cc → sc.
|
|
1838
|
+
renames.push({ kind: 'rename', oldName: cc.name, oldCol: cc, newName: sc.name });
|
|
1839
|
+
renameMap.set(cn, sn);
|
|
1840
|
+
recordAttrShift(cc, sc, sc.name); // attr ops reference the post-rename name
|
|
1841
|
+
i++;
|
|
1842
|
+
j++;
|
|
1843
|
+
}
|
|
1844
|
+
else if (!shNames.has(cn)) {
|
|
1845
|
+
// cc's name is gone from the new shape ⇒ dropped; sc matches a later survivor.
|
|
1846
|
+
drops.push({ kind: 'drop', name: cc.name });
|
|
1847
|
+
i++;
|
|
1848
|
+
}
|
|
1849
|
+
else if (!curNames.has(sn)) {
|
|
1850
|
+
// A genuinely new column appearing before the current survivors are
|
|
1851
|
+
// exhausted ⇒ a mid-table insert, not a trailing append.
|
|
1852
|
+
return { expressible: false, reason: `new column '${sc.name}' lands mid-table (an interleaving reshape, not a trailing append)` };
|
|
1853
|
+
}
|
|
1854
|
+
else {
|
|
1855
|
+
// Both names exist on the opposite side but are not aligned here ⇒ a reorder/swap.
|
|
1856
|
+
return { expressible: false, reason: `columns '${cc.name}' and '${sc.name}' are reordered` };
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
for (; i < cur.length; i++) {
|
|
1860
|
+
const cc = cur[i];
|
|
1861
|
+
if (!shNames.has(cc.name.toLowerCase()))
|
|
1862
|
+
drops.push({ kind: 'drop', name: cc.name });
|
|
1863
|
+
else
|
|
1864
|
+
return { expressible: false, reason: `column '${cc.name}' is reordered` };
|
|
1865
|
+
}
|
|
1866
|
+
for (; j < sh.length; j++) {
|
|
1867
|
+
const sc = sh[j];
|
|
1868
|
+
if (!curNames.has(sc.name.toLowerCase())) {
|
|
1869
|
+
// Added NULLABLE pre-reconcile (the reconcile fills it); any NOT NULL is
|
|
1870
|
+
// asserted post-reconcile against the filled rows, joining the tighten batch.
|
|
1871
|
+
adds.push({ kind: 'add', col: sc });
|
|
1872
|
+
if (sc.notNull === true)
|
|
1873
|
+
postReconcileOps.push({ kind: 'tightenNotNull', name: sc.name });
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
return { expressible: false, reason: `column '${sc.name}' is reordered` };
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const pkReason = describePhysicalPkChange(current, shape, renameMap);
|
|
1880
|
+
if (pkReason)
|
|
1881
|
+
return { expressible: false, reason: pkReason };
|
|
1882
|
+
// Pre-reconcile: the structural, data-lossless ops only (renames + adds before
|
|
1883
|
+
// drops, so a mid-sequence failure leaves a re-derivable state). The
|
|
1884
|
+
// data-validating attribute shifts + NOT NULL tightenings run post-reconcile
|
|
1885
|
+
// against the reconciled body, never the discarded backing.
|
|
1886
|
+
return {
|
|
1887
|
+
expressible: true,
|
|
1888
|
+
plan: {
|
|
1889
|
+
preReconcileOps: [...renames, ...adds, ...loosens, ...drops],
|
|
1890
|
+
postReconcileOps,
|
|
1891
|
+
},
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Compares the live backing's physical primary key to the re-derived shape's
|
|
1896
|
+
* ({@link computeBackingPrimaryKey}) **by column name through the reshape's rename
|
|
1897
|
+
* map** — not by index, which add/drop shift. Any change to the key's column set,
|
|
1898
|
+
* order, direction, collation, or a key column's **type** makes the reshape
|
|
1899
|
+
* inexpressible: a maintained table's PK is its replicated row identity, and
|
|
1900
|
+
* re-keying replicated row identity in place is refused. Returns a reason string,
|
|
1901
|
+
* or null when the key is unchanged. (A renamed key column is *not* a key change —
|
|
1902
|
+
* the rename map carries its new name; but a renamed-*and*-retyped key column still
|
|
1903
|
+
* trips the type check, because the comparison is on the underlying column schemas,
|
|
1904
|
+
* whose type identity a rename does not change.)
|
|
1905
|
+
*/
|
|
1906
|
+
function describePhysicalPkChange(current, shape, renameMap) {
|
|
1907
|
+
const shapePk = computeBackingPrimaryKey(shape);
|
|
1908
|
+
const currentPk = current.primaryKeyDefinition;
|
|
1909
|
+
if (currentPk.length !== shapePk.length) {
|
|
1910
|
+
return `primary-key column count ${currentPk.length} → ${shapePk.length}`;
|
|
1911
|
+
}
|
|
1912
|
+
for (let k = 0; k < shapePk.length; k++) {
|
|
1913
|
+
const curCol = current.columns[currentPk[k].index];
|
|
1914
|
+
const shCol = shape.columns[shapePk[k].index];
|
|
1915
|
+
const curName = renameMap.get(curCol.name.toLowerCase()) ?? curCol.name.toLowerCase();
|
|
1916
|
+
if (curName !== shCol.name.toLowerCase()) {
|
|
1917
|
+
return `primary-key column ${k} '${curCol.name}' → '${shCol.name}'`;
|
|
1918
|
+
}
|
|
1919
|
+
if (!backingTypeMatches(curCol, shCol)) {
|
|
1920
|
+
return `primary-key column ${k} '${curCol.name}' type ${curCol.logicalType.name} → ${shCol.logicalType.name}`;
|
|
1921
|
+
}
|
|
1922
|
+
if ((currentPk[k].desc === true) !== (shapePk[k].desc === true)) {
|
|
1923
|
+
return `primary-key column ${k} direction`;
|
|
1924
|
+
}
|
|
1925
|
+
const curColl = currentPk[k].collation ?? curCol.collation ?? 'BINARY';
|
|
1926
|
+
const shColl = shCol.collation ?? 'BINARY';
|
|
1927
|
+
if (curColl !== shColl) {
|
|
1928
|
+
return `primary-key column ${k} collation ${curColl} → ${shColl}`;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
/** Lifts a {@link ReshapeColumnOp} onto the module's `SchemaChangeInfo` surface. */
|
|
1934
|
+
function reshapeOpToChange(op) {
|
|
1935
|
+
switch (op.kind) {
|
|
1936
|
+
case 'rename':
|
|
1937
|
+
// Preserve the OLD column's attributes (type / not-null / collation / PK)
|
|
1938
|
+
// under the new name — attribute shifts ride separate alter ops.
|
|
1939
|
+
return { type: 'renameColumn', oldName: op.oldName, newName: op.newName, newColumnDefAst: backingColumnDef(op.oldCol, op.newName) };
|
|
1940
|
+
case 'add': {
|
|
1941
|
+
// Add NULLABLE: real values arrive with the reconcile, and any NOT NULL is
|
|
1942
|
+
// asserted post-reconcile so a non-empty backing never trips "ADD NOT NULL
|
|
1943
|
+
// without a default". An added column is never a PK column (a PK change is
|
|
1944
|
+
// inexpressible), so force non-PK in the lifted def.
|
|
1945
|
+
const nullable = { ...op.col, notNull: false, primaryKey: false, pkOrder: 0, pkDirection: undefined };
|
|
1946
|
+
return { type: 'addColumn', columnDef: backingColumnDef(nullable, op.col.name) };
|
|
1947
|
+
}
|
|
1948
|
+
case 'retype':
|
|
1949
|
+
return { type: 'alterColumn', columnName: op.name, setDataType: op.newTypeName };
|
|
1950
|
+
case 'recollate':
|
|
1951
|
+
return { type: 'alterColumn', columnName: op.name, setCollation: op.collation };
|
|
1952
|
+
case 'loosenNotNull':
|
|
1953
|
+
return { type: 'alterColumn', columnName: op.name, setNotNull: false };
|
|
1954
|
+
case 'tightenNotNull':
|
|
1955
|
+
return { type: 'alterColumn', columnName: op.name, setNotNull: true };
|
|
1956
|
+
case 'drop':
|
|
1957
|
+
return { type: 'dropColumn', columnName: op.name };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Rebuild a maintained-table catalog record from the backing module's post-reshape
|
|
1962
|
+
* `TableSchema`. `module.alterTable` returns ONLY the physical column shape — it
|
|
1963
|
+
* tracks neither the catalog-only `derivation` nor the catalog-only `tags`, so a
|
|
1964
|
+
* bare `{ ...moduleSchema, derivation }` graft silently drops the table's tags.
|
|
1965
|
+
* Graft both from the authoritative catalog record so a reshaping re-attach
|
|
1966
|
+
* preserves any tags a concurrent SET TAGS routed through ALTER MATERIALIZED VIEW
|
|
1967
|
+
* (and a refresh-driven reshape never wipes existing tags). A non-reshaping
|
|
1968
|
+
* re-attach keeps the whole record, tags included — this restores parity.
|
|
1969
|
+
*/
|
|
1970
|
+
function graftReshapedRecord(moduleSchema, source) {
|
|
1971
|
+
return { ...moduleSchema, derivation: source.derivation, tags: source.tags };
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* The sited error a refresh raises when the re-derived body shape cannot be
|
|
1975
|
+
* reconciled onto the live maintained table in place — an interleaving column
|
|
1976
|
+
* reorder or a physical-PK definition change (or a host module without
|
|
1977
|
+
* `alterTable`). The table and its rows are left **untouched** and the derivation
|
|
1978
|
+
* stays `stale`, recoverable exactly as the message says. Replaces the former
|
|
1979
|
+
* silent drop+recreate: a maintained table's PK / positional identity is its
|
|
1980
|
+
* replicated row identity, so an incompatible reshape is an actionable error, not
|
|
1981
|
+
* a new incarnation.
|
|
1982
|
+
*/
|
|
1983
|
+
function inexpressibleReshapeError(schemaName, name, reason) {
|
|
1984
|
+
return new QuereusError(`the derivation's output shape changed incompatibly with table '${schemaName}.${name}' (${reason}); `
|
|
1985
|
+
+ `alter the table to the new shape and re-attach, or drop and recreate`, StatusCode.ERROR);
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Identity-preserving reshape of a maintained table whose re-derived body shape
|
|
1989
|
+
* shifted — the refresh path's replacement for the former drop+recreate. Classify
|
|
1990
|
+
* the column delta; an inexpressible delta (interleave / PK-definition change)
|
|
1991
|
+
* raises the sited error with the table untouched, an expressible one reshapes in
|
|
1992
|
+
* place. The shape-match fast path (`backingShapeMatches` ⇒ data-only
|
|
1993
|
+
* `rebuildBacking`) is the caller's and is untouched.
|
|
1994
|
+
*/
|
|
1995
|
+
export async function reshapeBacking(db, mv, shape) {
|
|
1996
|
+
const classification = classifyBackingReshape(mv, shape);
|
|
1997
|
+
if (!classification.expressible) {
|
|
1998
|
+
throw inexpressibleReshapeError(mv.schemaName, mv.name, classification.reason);
|
|
1999
|
+
}
|
|
2000
|
+
return reshapeBackingInPlace(db, mv, shape, classification.plan);
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Executes an expressible in-place reshape in two phases around the data reconcile:
|
|
2004
|
+
*
|
|
2005
|
+
* 1. apply the **pre-reconcile** structural ops (renames/adds/loosens/drops) →
|
|
2006
|
+
* 2. re-register the reshaped (structural) schema + (shape-updated) derivation →
|
|
2007
|
+
* 3. data-reconcile via the shared {@link rebuildBacking} (re-run the body, swap
|
|
2008
|
+
* contents) → 4. apply the **post-reconcile** data-validating ops
|
|
2009
|
+
* (retype/recollate/tighten-NOT-NULL) → 5. re-register the final schema →
|
|
2010
|
+
* 6. fire one `table_modified`.
|
|
2011
|
+
*
|
|
2012
|
+
* The **same table incarnation throughout** — the backing-host instance stays
|
|
2013
|
+
* owned, no `table_removed`/`table_added` — so a replicated basis table's row
|
|
2014
|
+
* metadata survives; consumer maintained tables go stale via the single
|
|
2015
|
+
* `table_modified` and recover by their own refresh, exactly as for any source
|
|
2016
|
+
* alter. Returns the reshaped maintained table for the caller to re-register
|
|
2017
|
+
* maintenance on.
|
|
2018
|
+
*
|
|
2019
|
+
* **Why the data-validating ops defer.** A retype (physical convert), a recollate
|
|
2020
|
+
* (re-key + unique re-validate), and a NOT NULL tighten each scan the rows and
|
|
2021
|
+
* throw on a violation — but the pre-reconcile rows are about to be discarded by
|
|
2022
|
+
* step 3. Running them pre-reconcile would validate the stale backing (which may
|
|
2023
|
+
* still hold pre-narrowing values, e.g. an MV gone stale on an unrelated source
|
|
2024
|
+
* change whose data-fix was never maintained in) and spuriously throw a
|
|
2025
|
+
* MISMATCH/CONSTRAINT on a reshape the fresh body satisfies. Deferring them past
|
|
2026
|
+
* the reconcile validates the re-derived body rows instead. This is sound because
|
|
2027
|
+
* the reconcile's insert paths do NOT validate values against the column schema
|
|
2028
|
+
* (`MemoryTable.replaceBaseLayer` PK-extracts + inserts raw; the store backing-host
|
|
2029
|
+
* `replaceContents` puts serialized rows by keyed diff), so a body value conforming
|
|
2030
|
+
* to the NEW attribute enters the still-OLD-typed column unvalidated, and the
|
|
2031
|
+
* post-reconcile op then converts/re-keys/asserts the clean body data successfully.
|
|
2032
|
+
* The added-NULLABLE / deferred-tighten behavior for new NOT NULL columns is the
|
|
2033
|
+
* same mechanism (a non-empty backing never trips "ADD NOT NULL without a default").
|
|
2034
|
+
*
|
|
2035
|
+
* **Recoverability.** Only the data-lossless structural ops run before step 2's
|
|
2036
|
+
* `schema.addTable`, so the window in which the catalog schema and the module's
|
|
2037
|
+
* live schema could diverge on a partial failure no longer arises in practice. A
|
|
2038
|
+
* genuine post-reconcile throw (a body the new attribute still cannot satisfy)
|
|
2039
|
+
* happens AFTER the catalog is consistently re-registered with the reconciled body,
|
|
2040
|
+
* so the caller leaves the MV `stale` over a coherent, re-runnable table that
|
|
2041
|
+
* converges once the underlying data is fixed.
|
|
2042
|
+
*/
|
|
2043
|
+
async function reshapeBackingInPlace(db, mv, shape, plan) {
|
|
2044
|
+
const sm = db.schemaManager;
|
|
2045
|
+
const schema = sm.getSchemaOrFail(mv.schemaName);
|
|
2046
|
+
const backing = schema.getTable(mv.name);
|
|
2047
|
+
if (!backing) {
|
|
2048
|
+
throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during reshape`, StatusCode.INTERNAL);
|
|
2049
|
+
}
|
|
2050
|
+
const module = requireVtabModule(backing);
|
|
2051
|
+
if (!module.alterTable) {
|
|
2052
|
+
throw inexpressibleReshapeError(mv.schemaName, mv.name, `its backing module '${backing.vtabModuleName}' does not support in-place ALTER`);
|
|
2053
|
+
}
|
|
2054
|
+
// Pre-reconcile structural ops (renames/adds/loosens/drops — none throw on data).
|
|
2055
|
+
// Each addresses its column by name, so the fresh schema each call returns need
|
|
2056
|
+
// not be threaded by index; track only the latest.
|
|
2057
|
+
let current = backing;
|
|
2058
|
+
for (const op of plan.preReconcileOps) {
|
|
2059
|
+
current = await module.alterTable(db, mv.schemaName, mv.name, reshapeOpToChange(op));
|
|
2060
|
+
}
|
|
2061
|
+
// Re-register the reshaped schema with the (shape-updated) derivation BEFORE the
|
|
2062
|
+
// reconcile, so `rebuildBacking` resolves the reshaped table from the catalog.
|
|
2063
|
+
// alterTable returns a fresh derivation-less TableSchema; carry the derivation.
|
|
2064
|
+
mv.derivation.logicalKey = shape.primaryKey;
|
|
2065
|
+
mv.derivation.coarsenedKey = shape.coarsenedKey;
|
|
2066
|
+
mv.derivation.ordering = shape.ordering;
|
|
2067
|
+
mv.derivation.sourceTables = shape.sourceTables;
|
|
2068
|
+
let live = graftReshapedRecord(current, mv);
|
|
2069
|
+
schema.addTable(live);
|
|
2070
|
+
// Data reconcile: re-run the body and swap contents (the identity-preserving
|
|
2071
|
+
// data-only path — same host, same incarnation).
|
|
2072
|
+
await rebuildBacking(db, live);
|
|
2073
|
+
// Post-reconcile data-validating ops (retype / recollate / tighten NOT NULL): the
|
|
2074
|
+
// reconciled body rows satisfy the new attribute where the discarded backing
|
|
2075
|
+
// might not, so each validates the fresh data, not the stale rows. Re-register
|
|
2076
|
+
// the catalog after EACH op (not once after the loop): a data-validating op can
|
|
2077
|
+
// throw, and unlike the pre-reconcile batch the module schema mutates per op, so
|
|
2078
|
+
// a single post-loop register would leave the catalog behind the module — the
|
|
2079
|
+
// very catalog/module divergence this two-phase split exists to avoid — on a
|
|
2080
|
+
// partial throw. Per-op registration keeps the catalog tracking the module so a
|
|
2081
|
+
// mid-batch failure leaves a coherent, re-runnable table.
|
|
2082
|
+
//
|
|
2083
|
+
// NOTE: a `recollate` here applies AFTER step 3's rebuildBacking has already
|
|
2084
|
+
// validated + committed the rows under the OLD collation — so a collation-
|
|
2085
|
+
// sensitive declared CHECK whose truth flips under this recollate is not caught
|
|
2086
|
+
// by that scan. A `retype` in this same batch is the affinity analog: a CHECK
|
|
2087
|
+
// whose truth flips under the column's NEW logical type (e.g. `v < '9'` retyped
|
|
2088
|
+
// TEXT → INTEGER) is likewise validated under the OLD type and missed (documented
|
|
2089
|
+
// limitation; see the note in rebuildBacking's constraint-bearing branch and
|
|
2090
|
+
// docs/materialized-views.md).
|
|
2091
|
+
for (const op of plan.postReconcileOps) {
|
|
2092
|
+
current = await module.alterTable(db, mv.schemaName, mv.name, reshapeOpToChange(op));
|
|
2093
|
+
live = graftReshapedRecord(current, mv);
|
|
2094
|
+
schema.addTable(live);
|
|
2095
|
+
}
|
|
2096
|
+
// One engine-level event for the whole reshape: invalidate cached plans scanning
|
|
2097
|
+
// the table directly and cascade staleness to consumer MVs — table_modified, NOT
|
|
2098
|
+
// table_removed/added, since the incarnation is preserved.
|
|
2099
|
+
sm.getChangeNotifier().notifyChange({
|
|
2100
|
+
type: 'table_modified',
|
|
2101
|
+
schemaName: mv.schemaName,
|
|
2102
|
+
objectName: mv.name,
|
|
2103
|
+
oldObject: backing,
|
|
2104
|
+
newObject: live,
|
|
2105
|
+
});
|
|
2106
|
+
return live;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Resolves the {@link BackingHost} for a materialized view's backing table via
|
|
2110
|
+
* the owning module's backing-host capability (`vtab/backing-host.ts`). INTERNAL
|
|
2111
|
+
* when the module lacks the capability or does not know the table — a backing
|
|
2112
|
+
* table is engine-created on a capability-checked module, so either is a bug.
|
|
2113
|
+
*/
|
|
2114
|
+
export function resolveBackingHost(db, backingSchema) {
|
|
2115
|
+
const module = requireVtabModule(backingSchema);
|
|
2116
|
+
if (!module.getBackingHost) {
|
|
2117
|
+
throw new QuereusError(`materialized view backing table '${backingSchema.name}' is owned by module `
|
|
2118
|
+
+ `'${backingSchema.vtabModuleName}', which does not implement the backing-host capability`, StatusCode.INTERNAL);
|
|
2119
|
+
}
|
|
2120
|
+
const host = module.getBackingHost(db, backingSchema.schemaName, backingSchema.name);
|
|
2121
|
+
if (!host) {
|
|
2122
|
+
throw new QuereusError(`backing host not found for '${backingSchema.schemaName}.${backingSchema.name}'`, StatusCode.INTERNAL);
|
|
2123
|
+
}
|
|
2124
|
+
return host;
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Lenient counterpart of {@link resolveBackingHost}: returns the backing host, or
|
|
2128
|
+
* `undefined` when the owning module cannot (yet) resolve one — instead of
|
|
2129
|
+
* throwing. Used at maintenance-PLAN-BUILD time (the create-time gate registration),
|
|
2130
|
+
* where the only use of the host is the host-conditional, default-inert
|
|
2131
|
+
* `requiresReplicableDerivations` gate. A module that materializes its durable
|
|
2132
|
+
* backing LATE in the attach flow (e.g. lamina's `ensureBackingForAttach`, which
|
|
2133
|
+
* runs after the gate registration) has no host yet at plan-build time; the host is
|
|
2134
|
+
* resolved for real at the reconcile, and the steady-state maintenance arms
|
|
2135
|
+
* re-resolve it per use. Skipping the replicable gate when the host is absent is
|
|
2136
|
+
* sound: a host that sets `requiresReplicableDerivations` (the synced-store flavor)
|
|
2137
|
+
* always exists by plan-build time, so the gate still binds it — the
|
|
2138
|
+
* eager-resolution invariant on {@link BackingHost.requiresReplicableDerivations}.
|
|
2139
|
+
*
|
|
2140
|
+
* That soundness rests on the invariant being honored, not on prose: a host that
|
|
2141
|
+
* BOTH demands replicable derivations AND defers its host to the late seam would
|
|
2142
|
+
* skip the gate here unnoticed. The attach core's defensive guard
|
|
2143
|
+
* ({@link attachMaintainedDerivation}) re-checks once the late host is in hand and
|
|
2144
|
+
* raises a loud INTERNAL error in exactly that case, so this lenient skip can never
|
|
2145
|
+
* silently let a non-replicable body through.
|
|
2146
|
+
*/
|
|
2147
|
+
export function tryResolveBackingHost(db, backingSchema) {
|
|
2148
|
+
const module = requireVtabModule(backingSchema);
|
|
2149
|
+
return module.getBackingHost?.(db, backingSchema.schemaName, backingSchema.name);
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Eagerly records the constraint↔structure link when this MV covers a UNIQUE
|
|
2153
|
+
* constraint on one of its single source tables. Runs the coverage prover
|
|
2154
|
+
* (`coverage-prover.ts`) over the optimized body and, on the first match, stamps
|
|
2155
|
+
* the MV's `origin`/`covers` reverse link and the constraint's
|
|
2156
|
+
* `coveringStructureName` forward pointer (the source of truth). Informational
|
|
2157
|
+
* in this ticket — nothing enforces through the MV's backing table yet.
|
|
2158
|
+
*
|
|
2159
|
+
* Best-effort and side-effect-bounded: the body has already planned (during
|
|
2160
|
+
* shape derivation), so re-planning here is cheap and safe; a non-covering MV
|
|
2161
|
+
* simply records nothing.
|
|
2162
|
+
*/
|
|
2163
|
+
export function linkCoveredUniqueConstraints(db, mv, bodySql) {
|
|
2164
|
+
// The coverage prover reasons over the body's SOURCE table; suppress the
|
|
2165
|
+
// read-side rewrite so the body is not re-pointed at this MV's own backing.
|
|
2166
|
+
const root = db.schemaManager.withSuppressedMaterializedViewRewrite(() => db.getPlan(bodySql).getRelations()[0]);
|
|
2167
|
+
if (!root)
|
|
2168
|
+
return;
|
|
2169
|
+
const sm = db.schemaManager;
|
|
2170
|
+
for (const qualified of mv.derivation.sourceTables) {
|
|
2171
|
+
const dot = qualified.indexOf('.');
|
|
2172
|
+
const schemaName = dot >= 0 ? qualified.slice(0, dot) : 'main';
|
|
2173
|
+
const tableName = dot >= 0 ? qualified.slice(dot + 1) : qualified;
|
|
2174
|
+
const table = sm.getTable(schemaName, tableName);
|
|
2175
|
+
if (!table || !table.uniqueConstraints)
|
|
2176
|
+
continue;
|
|
2177
|
+
for (const uc of table.uniqueConstraints) {
|
|
2178
|
+
const result = proveCoverage(root, mv, uc, table);
|
|
2179
|
+
if (result.covers) {
|
|
2180
|
+
mv.derivation.covers = { schemaName: table.schemaName, tableName: table.name, constraintName: uc.name };
|
|
2181
|
+
// Forward pointer is the source of truth (see docs/schema.md).
|
|
2182
|
+
uc.coveringStructureName = mv.name;
|
|
2183
|
+
return; // singular back-pointer: link the first covered constraint.
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Clears the constraint↔structure link a covering MV established (drop path).
|
|
2190
|
+
* Matches on the forward pointer (`coveringStructureName === mv.name`) so it
|
|
2191
|
+
* works for unnamed constraints too; no enforcement demotion — physical schemas
|
|
2192
|
+
* still enforce via the implicit auto-index.
|
|
2193
|
+
*/
|
|
2194
|
+
export function unlinkCoveredUniqueConstraints(db, mv) {
|
|
2195
|
+
if (!mv.derivation.covers)
|
|
2196
|
+
return;
|
|
2197
|
+
const table = db.schemaManager.getTable(mv.derivation.covers.schemaName, mv.derivation.covers.tableName);
|
|
2198
|
+
if (!table?.uniqueConstraints)
|
|
2199
|
+
return;
|
|
2200
|
+
for (const uc of table.uniqueConstraints) {
|
|
2201
|
+
if (uc.coveringStructureName === mv.name)
|
|
2202
|
+
uc.coveringStructureName = undefined;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
/** Re-validates a stale MV's body against the current source schemas. Throws the
|
|
2206
|
+
* staleness diagnostic when the body no longer plans. Returns the optimized
|
|
2207
|
+
* relational root on success. */
|
|
2208
|
+
export function revalidateBody(db, mvName, bodySql) {
|
|
2209
|
+
let root;
|
|
2210
|
+
try {
|
|
2211
|
+
// Re-validate the body against the SOURCE schemas; suppress the read-side
|
|
2212
|
+
// rewrite so it is not re-pointed at this MV's own backing.
|
|
2213
|
+
root = db.schemaManager.withSuppressedMaterializedViewRewrite(() => db.getPlan(bodySql).getRelations()[0]);
|
|
2214
|
+
}
|
|
2215
|
+
catch (e) {
|
|
2216
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2217
|
+
throw new QuereusError(`materialized view '${mvName}' is stale; a source changed in an incompatible way — drop and recreate (${message})`, StatusCode.ERROR, e instanceof Error ? e : undefined);
|
|
2218
|
+
}
|
|
2219
|
+
if (!root) {
|
|
2220
|
+
throw new QuereusError(`materialized view '${mvName}' is stale; a source changed in an incompatible way — drop and recreate`, StatusCode.ERROR);
|
|
2221
|
+
}
|
|
2222
|
+
return root;
|
|
2223
|
+
}
|
|
2224
|
+
/* ──────────────── ALTER … RENAME propagation into MV bodies ──────────────── */
|
|
2225
|
+
/**
|
|
2226
|
+
* Lowercased `schema.name` keys of every MV that is stale *right now*. The rename
|
|
2227
|
+
* emitters snapshot this BEFORE the statement's first schema-change notify, so the
|
|
2228
|
+
* propagation pass can distinguish "stale from this very rename statement" (safe to
|
|
2229
|
+
* clear after a successful in-place rewrite — no DML can interleave within the
|
|
2230
|
+
* statement) from "stale from an earlier un-refreshed change" (the backing may
|
|
2231
|
+
* already be behind — writes during staleness are not maintained — so only a
|
|
2232
|
+
* successful REFRESH may clear it).
|
|
2233
|
+
*/
|
|
2234
|
+
export function snapshotStaleMaterializedViews(db) {
|
|
2235
|
+
const out = new Set();
|
|
2236
|
+
for (const mv of db.schemaManager.getAllMaintainedTables()) {
|
|
2237
|
+
if (mv.derivation.stale)
|
|
2238
|
+
out.add(mvStaleKey(mv));
|
|
2239
|
+
}
|
|
2240
|
+
return out;
|
|
2241
|
+
}
|
|
2242
|
+
function mvStaleKey(mv) {
|
|
2243
|
+
return `${mv.schemaName}.${mv.name}`.toLowerCase();
|
|
2244
|
+
}
|
|
2245
|
+
/** All maintained tables registered in `schema`, snapshotted (the propagation
|
|
2246
|
+
* loops re-register tables mid-iteration). */
|
|
2247
|
+
function maintainedTablesOf(schema) {
|
|
2248
|
+
return Array.from(schema.getAllTables()).filter(isMaintainedTable);
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Rewrites every dependent materialized view in `schema` after a source TABLE
|
|
2252
|
+
* RENAME — the MV mirror of the plain-view loop in `propagateTableRenameInSchema`
|
|
2253
|
+
* ("MV ≡ faster view"): the caller applies the same same-schema gate, and the body
|
|
2254
|
+
* `selectAst` is mutated in place by the same `renameTableInAst` walker. An MV is
|
|
2255
|
+
* processed when its body AST changed, its `insert defaults` clause changed (an
|
|
2256
|
+
* expr subquery can name the renamed table even when the body doesn't), OR its
|
|
2257
|
+
* `sourceTables` carries the old base — the latter catches a body that reads the
|
|
2258
|
+
* renamed table *through a plain view* (the view's AST was rewritten by the view
|
|
2259
|
+
* loop, but this MV's own AST never names the table while its row-time plan is
|
|
2260
|
+
* still keyed under the old base).
|
|
2261
|
+
*
|
|
2262
|
+
* Per processed MV the derived fields are recomputed on a shallow clone
|
|
2263
|
+
* (`sourceTables` re-keyed old→new, `bodyHash`, regenerated `sql`, the `covers`
|
|
2264
|
+
* reverse link), then {@link applyMaterializedViewRewrite} re-registers row-time
|
|
2265
|
+
* maintenance / preserves pre-existing staleness and fires
|
|
2266
|
+
* `materialized_view_modified`. Failures mark the MV stale and propagation
|
|
2267
|
+
* continues — best-effort, like the rest of the rename propagation.
|
|
2268
|
+
*/
|
|
2269
|
+
export async function propagateTableRenameToMaterializedViews(db, schema, renamedSchemaName, oldName, newName, preStale) {
|
|
2270
|
+
const schemaLower = renamedSchemaName.toLowerCase();
|
|
2271
|
+
const oldBase = `${schemaLower}.${oldName.toLowerCase()}`;
|
|
2272
|
+
const newBase = `${schemaLower}.${newName.toLowerCase()}`;
|
|
2273
|
+
for (const mv of maintainedTablesOf(schema)) {
|
|
2274
|
+
try {
|
|
2275
|
+
const d = mv.derivation;
|
|
2276
|
+
// The body walk also descends the trailing `with defaults (…)` clause
|
|
2277
|
+
// (now on `selectAst.defaults`), so a defaults-expr subquery naming the
|
|
2278
|
+
// renamed table flips `bodyChanged` even when the body never names it.
|
|
2279
|
+
const bodyChanged = renameTableInAst(d.selectAst, oldName, newName, renamedSchemaName);
|
|
2280
|
+
if (!bodyChanged && !d.sourceTables.includes(oldBase))
|
|
2281
|
+
continue;
|
|
2282
|
+
const covers = d.covers
|
|
2283
|
+
&& d.covers.schemaName.toLowerCase() === schemaLower
|
|
2284
|
+
&& d.covers.tableName.toLowerCase() === oldName.toLowerCase()
|
|
2285
|
+
? { ...d.covers, tableName: newName }
|
|
2286
|
+
: d.covers;
|
|
2287
|
+
await applyMaterializedViewRewrite(db, schema, mv, {
|
|
2288
|
+
sourceTables: d.sourceTables.map(s => (s === oldBase ? newBase : s)),
|
|
2289
|
+
covers,
|
|
2290
|
+
}, preStale, /*renamedColumns*/ false);
|
|
2291
|
+
}
|
|
2292
|
+
catch (e) {
|
|
2293
|
+
failMaterializedViewRenamePropagation(db, schema, mv, e);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Rewrites every dependent materialized view in `schema` after a source COLUMN
|
|
2299
|
+
* RENAME — the MV mirror of the plain-view loop in `propagateColumnRenameInSchema`
|
|
2300
|
+
* (same same-schema gate at the caller, same in-place `renameColumnInAst` walk).
|
|
2301
|
+
* The body walk also descends the trailing `with defaults (…)` clause (now on
|
|
2302
|
+
* `selectAst.defaults`): the clause target is typically a projected-away NOT NULL
|
|
2303
|
+
* column the body never mentions, so its rewrite still flips `bodyChanged` and
|
|
2304
|
+
* forces the re-hash / regenerate-DDL / fire-event path. An MV the walk does not
|
|
2305
|
+
* touch that the schema-change listener marked stale (an unreferenced-column
|
|
2306
|
+
* rename, a `select *` body) is restored by the
|
|
2307
|
+
* {@link restoreUnaffectedMaterializedViews} pass the ALTER emitter runs after
|
|
2308
|
+
* all per-schema loops. A changed BODY can shift the MV's *exposed output names*
|
|
2309
|
+
* (a bare passthrough projection of the renamed column — plain-view parity),
|
|
2310
|
+
* which {@link applyMaterializedViewRewrite} carries onto the live backing table.
|
|
2311
|
+
*/
|
|
2312
|
+
export async function propagateColumnRenameToMaterializedViews(db, schema, renamedSchemaName, tableName, oldCol, newCol, preStale, resolveColumnInSource) {
|
|
2313
|
+
for (const mv of maintainedTablesOf(schema)) {
|
|
2314
|
+
try {
|
|
2315
|
+
const d = mv.derivation;
|
|
2316
|
+
// `resolveColumnInSource` keeps the body walk scope-aware for a defaults-expr
|
|
2317
|
+
// subquery referencing a like-named column on its own FROM — plain-view /
|
|
2318
|
+
// differ-reconcile parity (see `renameColumnInAst`).
|
|
2319
|
+
const bodyChanged = renameColumnInAst(d.selectAst, tableName, oldCol, newCol, renamedSchemaName, resolveColumnInSource);
|
|
2320
|
+
if (!bodyChanged)
|
|
2321
|
+
continue;
|
|
2322
|
+
await applyMaterializedViewRewrite(db, schema, mv, {}, preStale, /*renamedColumns*/ true);
|
|
2323
|
+
}
|
|
2324
|
+
catch (e) {
|
|
2325
|
+
failMaterializedViewRenamePropagation(db, schema, mv, e);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* The per-MV core both rename propagations share. `mv.selectAst` — including its
|
|
2331
|
+
* trailing `with defaults (…)` clause — has already been rewritten in place;
|
|
2332
|
+
* `overrides` carries the recomputed catalog fields — `sourceTables` / `covers`
|
|
2333
|
+
* (table rename). The remaining derived fields are recomputed on a shallow clone
|
|
2334
|
+
* (mirroring the tag setters — `oldObject` in the event shares the rewritten AST,
|
|
2335
|
+
* only the derived fields differ) and swapped into the catalog. The `bodyHash`
|
|
2336
|
+
* and regenerated `sql` both read the rewritten body (defaults included), so they
|
|
2337
|
+
* agree with each other and with what the differ recomputes from the post-rename
|
|
2338
|
+
* declared form; the `materialized_view_modified` → store re-persist path
|
|
2339
|
+
* round-trips the new name.
|
|
2340
|
+
*
|
|
2341
|
+
* Staleness discipline: `stale` means the row-time plan was released and the
|
|
2342
|
+
* backing may already be BEHIND, so a flag that predates this statement is never
|
|
2343
|
+
* cleared — the body/sql/hash/sources are still rewritten (a later REFRESH then
|
|
2344
|
+
* resolves the new name; today it cannot), but maintenance is NOT re-registered
|
|
2345
|
+
* and the backing columns are NOT renamed (refresh's shape-mismatch rebuild owns
|
|
2346
|
+
* that). An MV that was live before the statement is fully restored: backing
|
|
2347
|
+
* column names follow the body's output names (column rename only), row-time
|
|
2348
|
+
* maintenance re-plans against the already-renamed catalog (re-keying the
|
|
2349
|
+
* source-base index, recomputing `sourceScope`), and the staleness this very
|
|
2350
|
+
* statement's events set is cleared — no DML can interleave within the statement,
|
|
2351
|
+
* so the backing cannot be behind.
|
|
2352
|
+
*/
|
|
2353
|
+
async function applyMaterializedViewRewrite(db, schema, mv, overrides, preStale, renamedColumns) {
|
|
2354
|
+
const wasPreStale = preStale.has(mvStaleKey(mv));
|
|
2355
|
+
const d = mv.derivation;
|
|
2356
|
+
const bodySql = astToString(d.selectAst);
|
|
2357
|
+
if (overrides.sourceTables)
|
|
2358
|
+
d.sourceTables = overrides.sourceTables;
|
|
2359
|
+
if ('covers' in overrides)
|
|
2360
|
+
d.covers = overrides.covers;
|
|
2361
|
+
// Canonical-definition hash (columns + body — the body string carries the
|
|
2362
|
+
// rewritten `with defaults (…)` clause) — must match the formula stamped at
|
|
2363
|
+
// create / recomputed by the differ, or every post-rename diff would churn a
|
|
2364
|
+
// spurious rebuild. `bodySql` also feeds renameShiftedBackingColumns below. The
|
|
2365
|
+
// DDL itself is rendered on demand from the unified record, so no stored `sql`.
|
|
2366
|
+
d.bodyHash = computeBodyHash(viewDefinitionToCanonicalString(d.columns, d.selectAst));
|
|
2367
|
+
if (!wasPreStale) {
|
|
2368
|
+
// Only a changed BODY can shift output names; a table rename / clause-only
|
|
2369
|
+
// change skips the backing-name pass (no re-plan needed).
|
|
2370
|
+
await restoreMaterializedViewLive(db, schema, mv, renamedColumns ? { bodySql } : undefined);
|
|
2371
|
+
}
|
|
2372
|
+
// Fired for still-stale MVs too: the rewritten body must re-persist so a
|
|
2373
|
+
// post-reopen REFRESH resolves the new name. The registered table object is
|
|
2374
|
+
// re-fetched — the backing-name pass may have swapped it.
|
|
2375
|
+
const live = schema.getTable(mv.name) ?? mv;
|
|
2376
|
+
db.schemaManager.getChangeNotifier().notifyChange({
|
|
2377
|
+
type: 'materialized_view_modified',
|
|
2378
|
+
schemaName: mv.schemaName,
|
|
2379
|
+
objectName: mv.name,
|
|
2380
|
+
oldObject: mv,
|
|
2381
|
+
newObject: live,
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* The shared restore tail both per-MV restore paths run — the changed-AST rewrite
|
|
2386
|
+
* ({@link applyMaterializedViewRewrite}) and the provably-unaffected restoration
|
|
2387
|
+
* pass ({@link restoreUnaffectedMaterializedViews}) — so the restore discipline
|
|
2388
|
+
* cannot drift between them: carry any body output-name shift onto the live
|
|
2389
|
+
* backing (`backingNames` present), re-register row-time maintenance, and only
|
|
2390
|
+
* then clear `stale`.
|
|
2391
|
+
*
|
|
2392
|
+
* `backingNames` is absent when the body's output names provably did not move (a
|
|
2393
|
+
* table rename / clause-only change), skipping the backing-name pass and its body
|
|
2394
|
+
* re-plan; when present, `shape` short-circuits the re-derivation for a caller
|
|
2395
|
+
* that already planned the body.
|
|
2396
|
+
*/
|
|
2397
|
+
async function restoreMaterializedViewLive(db, schema, mv, backingNames) {
|
|
2398
|
+
if (backingNames) {
|
|
2399
|
+
await renameShiftedBackingColumns(db, schema, mv, backingNames.bodySql, backingNames.shape);
|
|
2400
|
+
}
|
|
2401
|
+
// Re-register BEFORE clearing `stale`: if registration throws, the caller's
|
|
2402
|
+
// failure path leaves the MV stale rather than serving an unmaintained backing.
|
|
2403
|
+
// Register the LIVE registered table (the backing-name pass may have swapped
|
|
2404
|
+
// the catalog object); the shared derivation rides either way.
|
|
2405
|
+
const live = schema.getTable(mv.name);
|
|
2406
|
+
db.registerMaterializedView(isMaintainedTable(live) ? live : mv);
|
|
2407
|
+
mv.derivation.stale = false;
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Restores every dependent MV that THIS rename statement marked stale but the
|
|
2411
|
+
* rename provably did not affect. Runs once at the end of the table-/column-rename
|
|
2412
|
+
* propagation, after all per-schema loops — so every body rewrite, backing-column
|
|
2413
|
+
* rename, and cascade event has already fired and the catalog is fully renamed.
|
|
2414
|
+
*
|
|
2415
|
+
* The schema-change listener marks **every** MV whose `sourceTables` includes a
|
|
2416
|
+
* `table_modified` table stale (and detaches its row-time plan), but the rename
|
|
2417
|
+
* propagation only restores MVs it processes (changed AST / clause, or — table
|
|
2418
|
+
* rename — `sourceTables` carrying the old base). An MV the rename does not touch
|
|
2419
|
+
* fell through stale-but-valid: reads silently served the now-unmaintained backing
|
|
2420
|
+
* and writes never propagated until a manual REFRESH. Three concrete shapes: a
|
|
2421
|
+
* column rename the body never references; a rename whose only effect on another
|
|
2422
|
+
* source is a constraint rewrite (e.g. an FK `references` target) firing that
|
|
2423
|
+
* source's `table_modified`; and a `select *` body whose output is a pure name
|
|
2424
|
+
* shift (the AST is unchanged, so the body rewrite never sees it).
|
|
2425
|
+
*
|
|
2426
|
+
* Per candidate (`stale` now, not stale at the pre-statement snapshot — a
|
|
2427
|
+
* pre-existing flag means the backing may be BEHIND and only REFRESH may clear it):
|
|
2428
|
+
* re-derive the backing shape from the body against the renamed catalog; a
|
|
2429
|
+
* **structural** mismatch is not a rename no-op → leave stale (REFRESH's
|
|
2430
|
+
* shape-mismatch rebuild owns it); otherwise run the shared restore tail —
|
|
2431
|
+
* {@link renameShiftedBackingColumns} carries a pure name shift onto the live
|
|
2432
|
+
* backing (no-op when names already match; its backing `table_modified`
|
|
2433
|
+
* deliberately cascades staleness to chained MVs referencing the old output name),
|
|
2434
|
+
* then re-register row-time maintenance and clear `stale`.
|
|
2435
|
+
*
|
|
2436
|
+
* Deliberately fires NO `materialized_view_modified`: the MV record (AST, hash,
|
|
2437
|
+
* sql, sourceTables) is unchanged here — `stale` is runtime state, not persisted.
|
|
2438
|
+
* Walks all schemas (the listener marks cross-schema dependents too), in creation
|
|
2439
|
+
* order — topological for same-schema MV chains, so a producer restores before its
|
|
2440
|
+
* consumer is examined. A chained MV whose body references a renamed-away producer
|
|
2441
|
+
* output name fails shape derivation and stays stale (staleness-diagnostic parity
|
|
2442
|
+
* with a broken plain-view chain). Best-effort like the rest of the propagation:
|
|
2443
|
+
* a per-MV failure logs, leaves that MV stale, and continues.
|
|
2444
|
+
*/
|
|
2445
|
+
export async function restoreUnaffectedMaterializedViews(db, preStale) {
|
|
2446
|
+
for (const mv of db.schemaManager.getAllMaintainedTables()) {
|
|
2447
|
+
if (!mv.derivation.stale || preStale.has(mvStaleKey(mv)))
|
|
2448
|
+
continue;
|
|
2449
|
+
try {
|
|
2450
|
+
const schema = db.schemaManager.getSchemaOrFail(mv.schemaName);
|
|
2451
|
+
const d = mv.derivation;
|
|
2452
|
+
const bodySql = astToString(d.selectAst);
|
|
2453
|
+
// Throws when the body no longer plans against the renamed catalog
|
|
2454
|
+
// (e.g. a chained MV referencing a renamed-away output name) → catch
|
|
2455
|
+
// below leaves it stale.
|
|
2456
|
+
const shape = deriveBackingShape(db, bodySql, d.columns);
|
|
2457
|
+
// The retry of a failure-marked MV must not revive an inconsistent record: a
|
|
2458
|
+
// rewrite that threw between the in-place AST mutation and the derived-field
|
|
2459
|
+
// re-key leaves the OLD derivation (un-re-keyed `sourceTables`) holding the
|
|
2460
|
+
// rewritten body. Registering that would compute `sourceScope` (and key the
|
|
2461
|
+
// read-side rewrite) off the wrong bases — leave it stale instead.
|
|
2462
|
+
if (!sameSourceTables(d.sourceTables, shape.sourceTables)) {
|
|
2463
|
+
log('Leaving materialized view %s.%s stale after rename: recorded sourceTables disagree with the re-planned body — REFRESH recovers', mv.schemaName, mv.name);
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
const backing = schema.getTable(mv.name);
|
|
2467
|
+
if (!backing) {
|
|
2468
|
+
throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during restore`, StatusCode.INTERNAL);
|
|
2469
|
+
}
|
|
2470
|
+
const mismatch = describeBackingShapeMismatch(backing, shape);
|
|
2471
|
+
if (mismatch) {
|
|
2472
|
+
log('Leaving materialized view %s.%s stale after rename: backing shape mismatch (%s) — REFRESH rebuilds it', mv.schemaName, mv.name, mismatch);
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
await restoreMaterializedViewLive(db, schema, mv, { bodySql, shape });
|
|
2476
|
+
}
|
|
2477
|
+
catch (e) {
|
|
2478
|
+
log('Could not restore materialized view %s.%s after rename; leaving it stale: %s', mv.schemaName, mv.name, e instanceof Error ? e.message : String(e));
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
/** Set-equality over qualified (already-lowercased) source-table lists. Order is
|
|
2483
|
+
* irrelevant — both sides come from `collectSourceTables`' Set walk. */
|
|
2484
|
+
function sameSourceTables(a, b) {
|
|
2485
|
+
if (a.length !== b.length)
|
|
2486
|
+
return false;
|
|
2487
|
+
const set = new Set(a);
|
|
2488
|
+
return b.every(s => set.has(s));
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Carries a column-rename-induced output-name shift onto the MV's live backing
|
|
2492
|
+
* table. The backing's column names were derived from the body's output names at
|
|
2493
|
+
* create ({@link deriveBackingShape}); after the body rewrite a bare passthrough
|
|
2494
|
+
* projection of the renamed column exposes the NEW name, so the backing follows —
|
|
2495
|
+
* positionally, data-preserving, via the host module's own `alterTable` (a host
|
|
2496
|
+
* without `alterTable` throws UNSUPPORTED and the caller's failure path leaves
|
|
2497
|
+
* the MV stale). Explicit-column MVs (`mv(a, b)`) and
|
|
2498
|
+
* expression-aliased outputs produce no mismatch and no-op. Any structural
|
|
2499
|
+
* difference (count / types / PK) is NOT a rename outcome — throw so the caller's
|
|
2500
|
+
* failure path leaves the MV stale rather than rebuilding data here.
|
|
2501
|
+
*
|
|
2502
|
+
* The backing `table_modified` fired on a real rename deliberately cascades: a
|
|
2503
|
+
* chained MV whose body references the OLD output name is marked stale by the
|
|
2504
|
+
* manager's listener and surfaces the staleness diagnostic on its next read
|
|
2505
|
+
* (parity with a broken plain-view chain — strictly better than silently freezing),
|
|
2506
|
+
* and cached plans scanning the backing directly recompile against the new names.
|
|
2507
|
+
*/
|
|
2508
|
+
async function renameShiftedBackingColumns(db, schema, mv, bodySql, preDerivedShape) {
|
|
2509
|
+
const shape = preDerivedShape ?? deriveBackingShape(db, bodySql, mv.derivation.columns);
|
|
2510
|
+
const backing = schema.getTable(mv.name);
|
|
2511
|
+
if (!backing) {
|
|
2512
|
+
throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during backing-column rename`, StatusCode.INTERNAL);
|
|
2513
|
+
}
|
|
2514
|
+
const mismatch = describeBackingShapeMismatch(backing, shape);
|
|
2515
|
+
if (mismatch) {
|
|
2516
|
+
throw new QuereusError(`materialized view '${mv.schemaName}.${mv.name}': source column rename shifted the body's backing shape structurally (beyond a pure name shift): ${mismatch}`, StatusCode.INTERNAL);
|
|
2517
|
+
}
|
|
2518
|
+
const module = requireVtabModule(backing);
|
|
2519
|
+
let current = backing;
|
|
2520
|
+
for (let i = 0; i < shape.columns.length; i++) {
|
|
2521
|
+
const liveCol = current.columns[i];
|
|
2522
|
+
const newName = shape.columns[i].name;
|
|
2523
|
+
if (liveCol.name.toLowerCase() === newName.toLowerCase())
|
|
2524
|
+
continue;
|
|
2525
|
+
if (!module.alterTable) {
|
|
2526
|
+
throw new QuereusError(`module for backing table '${backing.name}' does not support ALTER TABLE`, StatusCode.UNSUPPORTED);
|
|
2527
|
+
}
|
|
2528
|
+
current = await module.alterTable(db, mv.schemaName, backing.name, {
|
|
2529
|
+
type: 'renameColumn',
|
|
2530
|
+
oldName: liveCol.name,
|
|
2531
|
+
newName,
|
|
2532
|
+
newColumnDefAst: backingColumnDef(liveCol, newName),
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
if (current !== backing) {
|
|
2536
|
+
// The module's alterTable returns a fresh TableSchema that does NOT carry
|
|
2537
|
+
// the derivation or the catalog-only tags — re-graft both so the registered
|
|
2538
|
+
// record stays maintained and keeps its tags.
|
|
2539
|
+
const renamed = graftReshapedRecord(current, mv);
|
|
2540
|
+
schema.addTable(renamed);
|
|
2541
|
+
db.schemaManager.getChangeNotifier().notifyChange({
|
|
2542
|
+
type: 'table_modified',
|
|
2543
|
+
schemaName: mv.schemaName,
|
|
2544
|
+
objectName: backing.name,
|
|
2545
|
+
oldObject: backing,
|
|
2546
|
+
newObject: renamed,
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/** Minimal ColumnDef AST for a backing-column rename. Backing columns carry only
|
|
2551
|
+
* type / not-null / PK / collation — never defaults or generated expressions
|
|
2552
|
+
* (see {@link buildBackingTableSchema}) — so the lift is total. */
|
|
2553
|
+
function backingColumnDef(col, newName) {
|
|
2554
|
+
const constraints = [col.notNull ? { type: 'notNull' } : { type: 'null' }];
|
|
2555
|
+
if (col.primaryKey)
|
|
2556
|
+
constraints.push({ type: 'primaryKey', direction: col.pkDirection });
|
|
2557
|
+
if (col.collation && col.collation !== 'BINARY')
|
|
2558
|
+
constraints.push({ type: 'collate', collation: col.collation });
|
|
2559
|
+
return { name: newName, dataType: col.logicalType.name, constraints };
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Failure path for one MV's rename rewrite: whatever partial state the rewrite
|
|
2563
|
+
* reached (AST possibly mutated, catalog record possibly swapped), the MV must not
|
|
2564
|
+
* keep serving its backing as if live — force-mark it stale, release its row-time
|
|
2565
|
+
* plan, and invalidate cached backing reads so the next reference re-hits the
|
|
2566
|
+
* build-time stale guard. A pre-existing stale flag is unaffected (it is never
|
|
2567
|
+
* cleared here). The caller continues with the remaining MVs.
|
|
2568
|
+
*/
|
|
2569
|
+
function failMaterializedViewRenamePropagation(db, schema, mv, cause) {
|
|
2570
|
+
log('Rename propagation failed for materialized view %s.%s; leaving it stale: %s', mv.schemaName, mv.name, cause instanceof Error ? cause.message : String(cause));
|
|
2571
|
+
// A swap may or may not have landed before the throw — mark whichever object
|
|
2572
|
+
// the catalog currently holds (the shared derivation rides either).
|
|
2573
|
+
const live = schema.getTable(mv.name);
|
|
2574
|
+
db.markMaterializedViewStale(isMaintainedTable(live) ? live : mv);
|
|
2575
|
+
}
|
|
2576
|
+
//# sourceMappingURL=materialized-view-helpers.js.map
|