@quereus/quereus 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/src/common/datatype.d.ts +12 -0
- package/dist/src/common/datatype.d.ts.map +1 -1
- package/dist/src/common/datatype.js.map +1 -1
- package/dist/src/common/types.d.ts +24 -0
- package/dist/src/common/types.d.ts.map +1 -1
- package/dist/src/common/types.js.map +1 -1
- package/dist/src/core/database-assertions.d.ts +37 -9
- package/dist/src/core/database-assertions.d.ts.map +1 -1
- package/dist/src/core/database-assertions.js +62 -110
- package/dist/src/core/database-assertions.js.map +1 -1
- package/dist/src/core/database-events.d.ts +163 -0
- package/dist/src/core/database-events.d.ts.map +1 -1
- package/dist/src/core/database-events.js +235 -21
- package/dist/src/core/database-events.js.map +1 -1
- package/dist/src/core/database-external-changes.d.ts +28 -0
- package/dist/src/core/database-external-changes.d.ts.map +1 -0
- package/dist/src/core/database-external-changes.js +242 -0
- package/dist/src/core/database-external-changes.js.map +1 -0
- package/dist/src/core/database-internal.d.ts +50 -1
- package/dist/src/core/database-internal.d.ts.map +1 -1
- package/dist/src/core/database-materialized-views.d.ts +1253 -0
- package/dist/src/core/database-materialized-views.d.ts.map +1 -0
- package/dist/src/core/database-materialized-views.js +3064 -0
- package/dist/src/core/database-materialized-views.js.map +1 -0
- package/dist/src/core/database-options.d.ts +4 -0
- package/dist/src/core/database-options.d.ts.map +1 -1
- package/dist/src/core/database-options.js +10 -0
- package/dist/src/core/database-options.js.map +1 -1
- package/dist/src/core/database-transaction.d.ts +19 -3
- package/dist/src/core/database-transaction.d.ts.map +1 -1
- package/dist/src/core/database-transaction.js +30 -3
- package/dist/src/core/database-transaction.js.map +1 -1
- package/dist/src/core/database-watchers.d.ts +19 -0
- package/dist/src/core/database-watchers.d.ts.map +1 -1
- package/dist/src/core/database-watchers.js +63 -3
- package/dist/src/core/database-watchers.js.map +1 -1
- package/dist/src/core/database.d.ts +203 -11
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +493 -29
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/derived-row-validator.d.ts +137 -0
- package/dist/src/core/derived-row-validator.d.ts.map +1 -0
- package/dist/src/core/derived-row-validator.js +314 -0
- package/dist/src/core/derived-row-validator.js.map +1 -0
- package/dist/src/core/statement.d.ts.map +1 -1
- package/dist/src/core/statement.js +30 -9
- package/dist/src/core/statement.js.map +1 -1
- package/dist/src/emit/ast-stringify.d.ts +135 -1
- package/dist/src/emit/ast-stringify.d.ts.map +1 -1
- package/dist/src/emit/ast-stringify.js +793 -118
- package/dist/src/emit/ast-stringify.js.map +1 -1
- package/dist/src/func/builtins/aggregate.d.ts.map +1 -1
- package/dist/src/func/builtins/aggregate.js +11 -10
- package/dist/src/func/builtins/aggregate.js.map +1 -1
- package/dist/src/func/builtins/builtin-window-functions.d.ts.map +1 -1
- package/dist/src/func/builtins/builtin-window-functions.js +32 -0
- package/dist/src/func/builtins/builtin-window-functions.js.map +1 -1
- package/dist/src/func/builtins/explain.d.ts +3 -0
- package/dist/src/func/builtins/explain.d.ts.map +1 -1
- package/dist/src/func/builtins/explain.js +229 -0
- package/dist/src/func/builtins/explain.js.map +1 -1
- package/dist/src/func/builtins/index.d.ts.map +1 -1
- package/dist/src/func/builtins/index.js +10 -2
- package/dist/src/func/builtins/index.js.map +1 -1
- package/dist/src/func/builtins/json.d.ts.map +1 -1
- package/dist/src/func/builtins/json.js +3 -2
- package/dist/src/func/builtins/json.js.map +1 -1
- package/dist/src/func/builtins/mutation.d.ts +2 -0
- package/dist/src/func/builtins/mutation.d.ts.map +1 -0
- package/dist/src/func/builtins/mutation.js +53 -0
- package/dist/src/func/builtins/mutation.js.map +1 -0
- package/dist/src/func/builtins/schema.d.ts +2 -0
- package/dist/src/func/builtins/schema.d.ts.map +1 -1
- package/dist/src/func/builtins/schema.js +713 -26
- package/dist/src/func/builtins/schema.js.map +1 -1
- package/dist/src/func/builtins/string.js +1 -1
- package/dist/src/func/builtins/string.js.map +1 -1
- package/dist/src/func/registration.d.ts +9 -0
- package/dist/src/func/registration.d.ts.map +1 -1
- package/dist/src/func/registration.js +4 -0
- package/dist/src/func/registration.js.map +1 -1
- package/dist/src/index.d.ts +25 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +27 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/parser/ast.d.ts +353 -21
- package/dist/src/parser/ast.d.ts.map +1 -1
- package/dist/src/parser/index.d.ts +14 -1
- package/dist/src/parser/index.d.ts.map +1 -1
- package/dist/src/parser/index.js +19 -0
- package/dist/src/parser/index.js.map +1 -1
- package/dist/src/parser/lexer.d.ts +9 -0
- package/dist/src/parser/lexer.d.ts.map +1 -1
- package/dist/src/parser/lexer.js +9 -0
- package/dist/src/parser/lexer.js.map +1 -1
- package/dist/src/parser/parser.d.ts +276 -7
- package/dist/src/parser/parser.d.ts.map +1 -1
- package/dist/src/parser/parser.js +1387 -469
- package/dist/src/parser/parser.js.map +1 -1
- package/dist/src/parser/visitor.d.ts.map +1 -1
- package/dist/src/parser/visitor.js +12 -8
- package/dist/src/parser/visitor.js.map +1 -1
- package/dist/src/planner/analysis/assertion-classifier.d.ts.map +1 -1
- package/dist/src/planner/analysis/assertion-classifier.js +4 -0
- package/dist/src/planner/analysis/assertion-classifier.js.map +1 -1
- package/dist/src/planner/analysis/assertion-hoist-cache.d.ts.map +1 -1
- package/dist/src/planner/analysis/assertion-hoist-cache.js +8 -4
- package/dist/src/planner/analysis/assertion-hoist-cache.js.map +1 -1
- package/dist/src/planner/analysis/authored-inverse.d.ts +22 -0
- package/dist/src/planner/analysis/authored-inverse.d.ts.map +1 -0
- package/dist/src/planner/analysis/authored-inverse.js +267 -0
- package/dist/src/planner/analysis/authored-inverse.js.map +1 -0
- package/dist/src/planner/analysis/change-scope.d.ts +34 -4
- package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
- package/dist/src/planner/analysis/change-scope.js +108 -7
- package/dist/src/planner/analysis/change-scope.js.map +1 -1
- package/dist/src/planner/analysis/check-extraction.d.ts +36 -2
- package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/check-extraction.js +174 -46
- package/dist/src/planner/analysis/check-extraction.js.map +1 -1
- package/dist/src/planner/analysis/coarsened-key.d.ts +109 -0
- package/dist/src/planner/analysis/coarsened-key.d.ts.map +1 -0
- package/dist/src/planner/analysis/coarsened-key.js +228 -0
- package/dist/src/planner/analysis/coarsened-key.js.map +1 -0
- package/dist/src/planner/analysis/comparison-collation.d.ts +216 -0
- package/dist/src/planner/analysis/comparison-collation.d.ts.map +1 -0
- package/dist/src/planner/analysis/comparison-collation.js +341 -0
- package/dist/src/planner/analysis/comparison-collation.js.map +1 -0
- package/dist/src/planner/analysis/constraint-extractor.d.ts +3 -1
- package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.js +192 -9
- package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
- package/dist/src/planner/analysis/coverage-prover.d.ts +321 -0
- package/dist/src/planner/analysis/coverage-prover.d.ts.map +1 -0
- package/dist/src/planner/analysis/coverage-prover.js +1038 -0
- package/dist/src/planner/analysis/coverage-prover.js.map +1 -0
- package/dist/src/planner/analysis/key-filter.d.ts +22 -0
- package/dist/src/planner/analysis/key-filter.d.ts.map +1 -0
- package/dist/src/planner/analysis/key-filter.js +105 -0
- package/dist/src/planner/analysis/key-filter.js.map +1 -0
- package/dist/src/planner/analysis/partial-unique-extraction.d.ts +36 -1
- package/dist/src/planner/analysis/partial-unique-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/partial-unique-extraction.js +148 -22
- package/dist/src/planner/analysis/partial-unique-extraction.js.map +1 -1
- package/dist/src/planner/analysis/predicate-normalizer.d.ts.map +1 -1
- package/dist/src/planner/analysis/predicate-normalizer.js +30 -1
- package/dist/src/planner/analysis/predicate-normalizer.js.map +1 -1
- package/dist/src/planner/analysis/predicate-shape.d.ts +36 -1
- package/dist/src/planner/analysis/predicate-shape.d.ts.map +1 -1
- package/dist/src/planner/analysis/predicate-shape.js +51 -13
- package/dist/src/planner/analysis/predicate-shape.js.map +1 -1
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +314 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.js +1081 -0
- package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -0
- package/dist/src/planner/analysis/scalar-invertibility.d.ts +92 -0
- package/dist/src/planner/analysis/scalar-invertibility.d.ts.map +1 -0
- package/dist/src/planner/analysis/scalar-invertibility.js +129 -0
- package/dist/src/planner/analysis/scalar-invertibility.js.map +1 -0
- package/dist/src/planner/analysis/update-lineage.d.ts +196 -0
- package/dist/src/planner/analysis/update-lineage.d.ts.map +1 -0
- package/dist/src/planner/analysis/update-lineage.js +322 -0
- package/dist/src/planner/analysis/update-lineage.js.map +1 -0
- package/dist/src/planner/analysis/view-complement.d.ts +42 -0
- package/dist/src/planner/analysis/view-complement.d.ts.map +1 -0
- package/dist/src/planner/analysis/view-complement.js +54 -0
- package/dist/src/planner/analysis/view-complement.js.map +1 -0
- package/dist/src/planner/building/alter-table.d.ts +1 -1
- package/dist/src/planner/building/alter-table.d.ts.map +1 -1
- package/dist/src/planner/building/alter-table.js +211 -2
- package/dist/src/planner/building/alter-table.js.map +1 -1
- package/dist/src/planner/building/block.d.ts.map +1 -1
- package/dist/src/planner/building/block.js +18 -1
- package/dist/src/planner/building/block.js.map +1 -1
- package/dist/src/planner/building/constraint-builder.d.ts +33 -5
- package/dist/src/planner/building/constraint-builder.d.ts.map +1 -1
- package/dist/src/planner/building/constraint-builder.js +63 -28
- package/dist/src/planner/building/constraint-builder.js.map +1 -1
- package/dist/src/planner/building/create-view.d.ts +9 -0
- package/dist/src/planner/building/create-view.d.ts.map +1 -1
- package/dist/src/planner/building/create-view.js +41 -12
- package/dist/src/planner/building/create-view.js.map +1 -1
- package/dist/src/planner/building/ddl.d.ts.map +1 -1
- package/dist/src/planner/building/ddl.js +94 -0
- package/dist/src/planner/building/ddl.js.map +1 -1
- package/dist/src/planner/building/declare-schema.d.ts +1 -0
- package/dist/src/planner/building/declare-schema.d.ts.map +1 -1
- package/dist/src/planner/building/declare-schema.js +4 -1
- package/dist/src/planner/building/declare-schema.js.map +1 -1
- package/dist/src/planner/building/default-scope.d.ts +26 -0
- package/dist/src/planner/building/default-scope.d.ts.map +1 -0
- package/dist/src/planner/building/default-scope.js +41 -0
- package/dist/src/planner/building/default-scope.js.map +1 -0
- package/dist/src/planner/building/delete.d.ts +19 -1
- package/dist/src/planner/building/delete.d.ts.map +1 -1
- package/dist/src/planner/building/delete.js +109 -30
- package/dist/src/planner/building/delete.js.map +1 -1
- package/dist/src/planner/building/dml-target.d.ts +118 -0
- package/dist/src/planner/building/dml-target.d.ts.map +1 -0
- package/dist/src/planner/building/dml-target.js +282 -0
- package/dist/src/planner/building/dml-target.js.map +1 -0
- package/dist/src/planner/building/drop-index.d.ts.map +1 -1
- package/dist/src/planner/building/drop-index.js +4 -1
- package/dist/src/planner/building/drop-index.js.map +1 -1
- package/dist/src/planner/building/drop-view.d.ts.map +1 -1
- package/dist/src/planner/building/drop-view.js +4 -2
- package/dist/src/planner/building/drop-view.js.map +1 -1
- package/dist/src/planner/building/expression.d.ts.map +1 -1
- package/dist/src/planner/building/expression.js +60 -21
- package/dist/src/planner/building/expression.js.map +1 -1
- package/dist/src/planner/building/foreign-key-builder.d.ts +30 -0
- package/dist/src/planner/building/foreign-key-builder.d.ts.map +1 -1
- package/dist/src/planner/building/foreign-key-builder.js +160 -129
- package/dist/src/planner/building/foreign-key-builder.js.map +1 -1
- package/dist/src/planner/building/insert.d.ts +45 -2
- package/dist/src/planner/building/insert.d.ts.map +1 -1
- package/dist/src/planner/building/insert.js +257 -88
- package/dist/src/planner/building/insert.js.map +1 -1
- package/dist/src/planner/building/lens-auxiliary-access.d.ts +22 -0
- package/dist/src/planner/building/lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/planner/building/lens-auxiliary-access.js +132 -0
- package/dist/src/planner/building/lens-auxiliary-access.js.map +1 -0
- package/dist/src/planner/building/materialized-view.d.ts +16 -0
- package/dist/src/planner/building/materialized-view.d.ts.map +1 -0
- package/dist/src/planner/building/materialized-view.js +57 -0
- package/dist/src/planner/building/materialized-view.js.map +1 -0
- package/dist/src/planner/building/returning-star.d.ts +32 -0
- package/dist/src/planner/building/returning-star.d.ts.map +1 -0
- package/dist/src/planner/building/returning-star.js +45 -0
- package/dist/src/planner/building/returning-star.js.map +1 -0
- package/dist/src/planner/building/select-aggregates.d.ts.map +1 -1
- package/dist/src/planner/building/select-aggregates.js +47 -0
- package/dist/src/planner/building/select-aggregates.js.map +1 -1
- package/dist/src/planner/building/select-compound.d.ts.map +1 -1
- package/dist/src/planner/building/select-compound.js +84 -11
- package/dist/src/planner/building/select-compound.js.map +1 -1
- package/dist/src/planner/building/select-context.d.ts +10 -2
- package/dist/src/planner/building/select-context.d.ts.map +1 -1
- package/dist/src/planner/building/select-context.js +7 -1
- package/dist/src/planner/building/select-context.js.map +1 -1
- package/dist/src/planner/building/select-modifiers.js +6 -0
- package/dist/src/planner/building/select-modifiers.js.map +1 -1
- package/dist/src/planner/building/select-ordinal.d.ts +18 -0
- package/dist/src/planner/building/select-ordinal.d.ts.map +1 -1
- package/dist/src/planner/building/select-ordinal.js +30 -0
- package/dist/src/planner/building/select-ordinal.js.map +1 -1
- package/dist/src/planner/building/select-projections.d.ts +8 -2
- package/dist/src/planner/building/select-projections.d.ts.map +1 -1
- package/dist/src/planner/building/select-projections.js +26 -4
- package/dist/src/planner/building/select-projections.js.map +1 -1
- package/dist/src/planner/building/select-window.d.ts.map +1 -1
- package/dist/src/planner/building/select-window.js +8 -5
- package/dist/src/planner/building/select-window.js.map +1 -1
- package/dist/src/planner/building/select.d.ts.map +1 -1
- package/dist/src/planner/building/select.js +164 -59
- package/dist/src/planner/building/select.js.map +1 -1
- package/dist/src/planner/building/set-object-tags.d.ts +7 -0
- package/dist/src/planner/building/set-object-tags.d.ts.map +1 -0
- package/dist/src/planner/building/set-object-tags.js +38 -0
- package/dist/src/planner/building/set-object-tags.js.map +1 -0
- package/dist/src/planner/building/tag-diagnostics.d.ts +27 -0
- package/dist/src/planner/building/tag-diagnostics.d.ts.map +1 -0
- package/dist/src/planner/building/tag-diagnostics.js +37 -0
- package/dist/src/planner/building/tag-diagnostics.js.map +1 -0
- package/dist/src/planner/building/update.d.ts +18 -1
- package/dist/src/planner/building/update.d.ts.map +1 -1
- package/dist/src/planner/building/update.js +134 -58
- package/dist/src/planner/building/update.js.map +1 -1
- package/dist/src/planner/building/view-mutation-builder.d.ts +15 -0
- package/dist/src/planner/building/view-mutation-builder.d.ts.map +1 -0
- package/dist/src/planner/building/view-mutation-builder.js +1158 -0
- package/dist/src/planner/building/view-mutation-builder.js.map +1 -0
- package/dist/src/planner/building/with.d.ts +11 -0
- package/dist/src/planner/building/with.d.ts.map +1 -1
- package/dist/src/planner/building/with.js +48 -10
- package/dist/src/planner/building/with.js.map +1 -1
- package/dist/src/planner/cost/index.d.ts +83 -0
- package/dist/src/planner/cost/index.d.ts.map +1 -1
- package/dist/src/planner/cost/index.js +114 -0
- package/dist/src/planner/cost/index.js.map +1 -1
- package/dist/src/planner/framework/characteristics.d.ts +38 -4
- package/dist/src/planner/framework/characteristics.d.ts.map +1 -1
- package/dist/src/planner/framework/characteristics.js +50 -6
- package/dist/src/planner/framework/characteristics.js.map +1 -1
- package/dist/src/planner/framework/pass.d.ts.map +1 -1
- package/dist/src/planner/framework/pass.js +2 -1
- package/dist/src/planner/framework/pass.js.map +1 -1
- package/dist/src/planner/framework/registry.d.ts +39 -1
- package/dist/src/planner/framework/registry.d.ts.map +1 -1
- package/dist/src/planner/framework/registry.js +18 -2
- package/dist/src/planner/framework/registry.js.map +1 -1
- package/dist/src/planner/mutation/backward-body.d.ts +131 -0
- package/dist/src/planner/mutation/backward-body.d.ts.map +1 -0
- package/dist/src/planner/mutation/backward-body.js +135 -0
- package/dist/src/planner/mutation/backward-body.js.map +1 -0
- package/dist/src/planner/mutation/cte-flatten.d.ts +17 -0
- package/dist/src/planner/mutation/cte-flatten.d.ts.map +1 -0
- package/dist/src/planner/mutation/cte-flatten.js +364 -0
- package/dist/src/planner/mutation/cte-flatten.js.map +1 -0
- package/dist/src/planner/mutation/decomposition.d.ts +273 -0
- package/dist/src/planner/mutation/decomposition.d.ts.map +1 -0
- package/dist/src/planner/mutation/decomposition.js +1719 -0
- package/dist/src/planner/mutation/decomposition.js.map +1 -0
- package/dist/src/planner/mutation/lens-enforcement.d.ts +165 -0
- package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -0
- package/dist/src/planner/mutation/lens-enforcement.js +745 -0
- package/dist/src/planner/mutation/lens-enforcement.js.map +1 -0
- package/dist/src/planner/mutation/multi-source.d.ts +568 -0
- package/dist/src/planner/mutation/multi-source.d.ts.map +1 -0
- package/dist/src/planner/mutation/multi-source.js +2915 -0
- package/dist/src/planner/mutation/multi-source.js.map +1 -0
- package/dist/src/planner/mutation/mutation-diagnostic.d.ts +37 -0
- package/dist/src/planner/mutation/mutation-diagnostic.d.ts.map +1 -0
- package/dist/src/planner/mutation/mutation-diagnostic.js +24 -0
- package/dist/src/planner/mutation/mutation-diagnostic.js.map +1 -0
- package/dist/src/planner/mutation/mutation-tags.d.ts +33 -0
- package/dist/src/planner/mutation/mutation-tags.d.ts.map +1 -0
- package/dist/src/planner/mutation/mutation-tags.js +31 -0
- package/dist/src/planner/mutation/mutation-tags.js.map +1 -0
- package/dist/src/planner/mutation/propagate.d.ts +97 -0
- package/dist/src/planner/mutation/propagate.d.ts.map +1 -0
- package/dist/src/planner/mutation/propagate.js +220 -0
- package/dist/src/planner/mutation/propagate.js.map +1 -0
- package/dist/src/planner/mutation/scope-transform.d.ts +181 -0
- package/dist/src/planner/mutation/scope-transform.d.ts.map +1 -0
- package/dist/src/planner/mutation/scope-transform.js +574 -0
- package/dist/src/planner/mutation/scope-transform.js.map +1 -0
- package/dist/src/planner/mutation/set-op.d.ts +242 -0
- package/dist/src/planner/mutation/set-op.d.ts.map +1 -0
- package/dist/src/planner/mutation/set-op.js +1687 -0
- package/dist/src/planner/mutation/set-op.js.map +1 -0
- package/dist/src/planner/mutation/single-source.d.ts +261 -0
- package/dist/src/planner/mutation/single-source.d.ts.map +1 -0
- package/dist/src/planner/mutation/single-source.js +1096 -0
- package/dist/src/planner/mutation/single-source.js.map +1 -0
- package/dist/src/planner/nodes/aggregate-node.js +3 -3
- package/dist/src/planner/nodes/aggregate-node.js.map +1 -1
- package/dist/src/planner/nodes/alias-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/alias-node.js +5 -1
- package/dist/src/planner/nodes/alias-node.js.map +1 -1
- package/dist/src/planner/nodes/alter-table-node.d.ts +124 -1
- package/dist/src/planner/nodes/alter-table-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/alter-table-node.js +27 -0
- package/dist/src/planner/nodes/alter-table-node.js.map +1 -1
- package/dist/src/planner/nodes/analyze-node.d.ts +2 -1
- package/dist/src/planner/nodes/analyze-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/analyze-node.js +18 -1
- package/dist/src/planner/nodes/analyze-node.js.map +1 -1
- package/dist/src/planner/nodes/asserted-keys-node.d.ts +43 -0
- package/dist/src/planner/nodes/asserted-keys-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/asserted-keys-node.js +99 -0
- package/dist/src/planner/nodes/asserted-keys-node.js.map +1 -0
- package/dist/src/planner/nodes/async-gather-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/async-gather-node.js +33 -8
- package/dist/src/planner/nodes/async-gather-node.js.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/bloom-join-node.js +2 -1
- package/dist/src/planner/nodes/bloom-join-node.js.map +1 -1
- package/dist/src/planner/nodes/create-view-node.d.ts +7 -2
- package/dist/src/planner/nodes/create-view-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/create-view-node.js +4 -1
- package/dist/src/planner/nodes/create-view-node.js.map +1 -1
- package/dist/src/planner/nodes/declarative-schema.d.ts +13 -1
- package/dist/src/planner/nodes/declarative-schema.d.ts.map +1 -1
- package/dist/src/planner/nodes/declarative-schema.js +32 -0
- package/dist/src/planner/nodes/declarative-schema.js.map +1 -1
- package/dist/src/planner/nodes/distinct-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/distinct-node.js +2 -0
- package/dist/src/planner/nodes/distinct-node.js.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts +29 -1
- package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/dml-executor-node.js +27 -3
- package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
- package/dist/src/planner/nodes/eager-prefetch-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/eager-prefetch-node.js +2 -0
- package/dist/src/planner/nodes/eager-prefetch-node.js.map +1 -1
- package/dist/src/planner/nodes/envelope-scan-node.d.ts +42 -0
- package/dist/src/planner/nodes/envelope-scan-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/envelope-scan-node.js +62 -0
- package/dist/src/planner/nodes/envelope-scan-node.js.map +1 -0
- package/dist/src/planner/nodes/fanout-lookup-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.js +11 -1
- package/dist/src/planner/nodes/fanout-lookup-join-node.js.map +1 -1
- package/dist/src/planner/nodes/filter.d.ts.map +1 -1
- package/dist/src/planner/nodes/filter.js +63 -13
- package/dist/src/planner/nodes/filter.js.map +1 -1
- package/dist/src/planner/nodes/join-node.d.ts +41 -1
- package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/join-node.js +78 -8
- package/dist/src/planner/nodes/join-node.js.map +1 -1
- package/dist/src/planner/nodes/join-utils.d.ts +33 -6
- package/dist/src/planner/nodes/join-utils.d.ts.map +1 -1
- package/dist/src/planner/nodes/join-utils.js +124 -9
- package/dist/src/planner/nodes/join-utils.js.map +1 -1
- package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts +104 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js +91 -0
- package/dist/src/planner/nodes/lens-auxiliary-access-node.js.map +1 -0
- package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
- package/dist/src/planner/nodes/limit-offset.js +4 -5
- package/dist/src/planner/nodes/limit-offset.js.map +1 -1
- package/dist/src/planner/nodes/materialized-view-nodes.d.ts +69 -0
- package/dist/src/planner/nodes/materialized-view-nodes.d.ts.map +1 -0
- package/dist/src/planner/nodes/materialized-view-nodes.js +111 -0
- package/dist/src/planner/nodes/materialized-view-nodes.js.map +1 -0
- package/dist/src/planner/nodes/merge-join-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/merge-join-node.js +2 -1
- package/dist/src/planner/nodes/merge-join-node.js.map +1 -1
- package/dist/src/planner/nodes/ordinal-slice-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/ordinal-slice-node.js +2 -0
- package/dist/src/planner/nodes/ordinal-slice-node.js.map +1 -1
- package/dist/src/planner/nodes/plan-node-type.d.ts +9 -0
- package/dist/src/planner/nodes/plan-node-type.d.ts.map +1 -1
- package/dist/src/planner/nodes/plan-node-type.js +9 -0
- package/dist/src/planner/nodes/plan-node-type.js.map +1 -1
- package/dist/src/planner/nodes/plan-node.d.ts +265 -5
- package/dist/src/planner/nodes/plan-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/plan-node.js.map +1 -1
- package/dist/src/planner/nodes/pragma.d.ts +2 -1
- package/dist/src/planner/nodes/pragma.d.ts.map +1 -1
- package/dist/src/planner/nodes/pragma.js +12 -0
- package/dist/src/planner/nodes/pragma.js.map +1 -1
- package/dist/src/planner/nodes/project-node.d.ts +14 -1
- package/dist/src/planner/nodes/project-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/project-node.js +85 -11
- package/dist/src/planner/nodes/project-node.js.map +1 -1
- package/dist/src/planner/nodes/reference.d.ts.map +1 -1
- package/dist/src/planner/nodes/reference.js +62 -27
- package/dist/src/planner/nodes/reference.js.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/retrieve-node.js +7 -0
- package/dist/src/planner/nodes/retrieve-node.js.map +1 -1
- package/dist/src/planner/nodes/returning-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/returning-node.js +10 -3
- package/dist/src/planner/nodes/returning-node.js.map +1 -1
- package/dist/src/planner/nodes/scalar.d.ts +20 -0
- package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
- package/dist/src/planner/nodes/scalar.js +71 -14
- package/dist/src/planner/nodes/scalar.js.map +1 -1
- package/dist/src/planner/nodes/set-object-tags-node.d.ts +39 -0
- package/dist/src/planner/nodes/set-object-tags-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/set-object-tags-node.js +41 -0
- package/dist/src/planner/nodes/set-object-tags-node.js.map +1 -0
- package/dist/src/planner/nodes/set-operation-node.d.ts +123 -1
- package/dist/src/planner/nodes/set-operation-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/set-operation-node.js +291 -18
- package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
- package/dist/src/planner/nodes/single-row.d.ts.map +1 -1
- package/dist/src/planner/nodes/single-row.js +3 -0
- package/dist/src/planner/nodes/single-row.js.map +1 -1
- package/dist/src/planner/nodes/sort.d.ts.map +1 -1
- package/dist/src/planner/nodes/sort.js +7 -6
- package/dist/src/planner/nodes/sort.js.map +1 -1
- package/dist/src/planner/nodes/subquery.d.ts +2 -0
- package/dist/src/planner/nodes/subquery.d.ts.map +1 -1
- package/dist/src/planner/nodes/subquery.js +18 -2
- package/dist/src/planner/nodes/subquery.js.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
- package/dist/src/planner/nodes/table-access-nodes.js +23 -3
- package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
- package/dist/src/planner/nodes/table-function-call.js +6 -0
- package/dist/src/planner/nodes/table-function-call.js.map +1 -1
- package/dist/src/planner/nodes/values-node.d.ts +1 -0
- package/dist/src/planner/nodes/values-node.d.ts.map +1 -1
- package/dist/src/planner/nodes/values-node.js +16 -6
- package/dist/src/planner/nodes/values-node.js.map +1 -1
- package/dist/src/planner/nodes/view-mutation-node.d.ts +259 -0
- package/dist/src/planner/nodes/view-mutation-node.d.ts.map +1 -0
- package/dist/src/planner/nodes/view-mutation-node.js +273 -0
- package/dist/src/planner/nodes/view-mutation-node.js.map +1 -0
- package/dist/src/planner/nodes/window-function.d.ts +17 -1
- package/dist/src/planner/nodes/window-function.d.ts.map +1 -1
- package/dist/src/planner/nodes/window-function.js +15 -1
- package/dist/src/planner/nodes/window-function.js.map +1 -1
- package/dist/src/planner/nodes/window-node.js +2 -2
- package/dist/src/planner/nodes/window-node.js.map +1 -1
- package/dist/src/planner/optimizer.d.ts.map +1 -1
- package/dist/src/planner/optimizer.js +372 -39
- package/dist/src/planner/optimizer.js.map +1 -1
- package/dist/src/planner/planning-context.d.ts +1 -1
- package/dist/src/planner/planning-context.d.ts.map +1 -1
- package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts +70 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts.map +1 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.js +156 -0
- package/dist/src/planner/rules/access/lens-access-form-matcher.js.map +1 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts +31 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js +176 -0
- package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js.map +1 -0
- package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
- package/dist/src/planner/rules/access/rule-select-access-path.js +435 -37
- package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.d.ts.map +1 -1
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js +9 -0
- package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js.map +1 -1
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts +39 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts.map +1 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +616 -0
- package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js.map +1 -0
- package/dist/src/planner/rules/cache/rule-scalar-cse.d.ts.map +1 -1
- package/dist/src/planner/rules/cache/rule-scalar-cse.js +8 -1
- package/dist/src/planner/rules/cache/rule-scalar-cse.js.map +1 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.d.ts +36 -0
- package/dist/src/planner/rules/join/equi-pair-extractor.d.ts.map +1 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.js +38 -1
- package/dist/src/planner/rules/join/equi-pair-extractor.js.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +10 -0
- package/dist/src/planner/rules/join/rule-fanout-batched-outer.js.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js +19 -1
- package/dist/src/planner/rules/join/rule-fanout-lookup-join.js.map +1 -1
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts +130 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js +206 -0
- package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js.map +1 -0
- package/dist/src/planner/rules/join/rule-join-elimination.d.ts +67 -14
- package/dist/src/planner/rules/join/rule-join-elimination.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-elimination.js +81 -25
- package/dist/src/planner/rules/join/rule-join-elimination.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts +84 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.js +138 -0
- package/dist/src/planner/rules/join/rule-join-existence-pruning.js.map +1 -0
- package/dist/src/planner/rules/join/rule-join-greedy-commute.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js +9 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.js +12 -1
- package/dist/src/planner/rules/join/rule-join-physical-selection.js.map +1 -1
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.js +4 -0
- package/dist/src/planner/rules/join/rule-lateral-top1-asof.js.map +1 -1
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.js +4 -0
- package/dist/src/planner/rules/join/rule-monotonic-merge-join.js.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.d.ts.map +1 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js +10 -0
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js.map +1 -1
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts +286 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts.map +1 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js +548 -0
- package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js.map +1 -0
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js +9 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js +7 -0
- package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js.map +1 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.d.ts.map +1 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js +10 -1
- package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js +9 -0
- package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js +18 -0
- package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js +7 -0
- package/dist/src/planner/rules/predicate/rule-filter-contradiction.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.d.ts.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js +9 -0
- package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js.map +1 -1
- package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js +13 -3
- package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.d.ts.map +1 -1
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.js +14 -0
- package/dist/src/planner/rules/retrieve/rule-projection-pruning.js.map +1 -1
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.d.ts +1 -1
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js +4 -4
- package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js +8 -0
- package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js +7 -0
- package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js.map +1 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts.map +1 -1
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js +12 -0
- package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js.map +1 -1
- package/dist/src/planner/type-utils.d.ts +14 -0
- package/dist/src/planner/type-utils.d.ts.map +1 -1
- package/dist/src/planner/type-utils.js +66 -21
- package/dist/src/planner/type-utils.js.map +1 -1
- package/dist/src/planner/util/fd-utils.d.ts +177 -43
- package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
- package/dist/src/planner/util/fd-utils.js +396 -101
- package/dist/src/planner/util/fd-utils.js.map +1 -1
- package/dist/src/planner/util/ind-utils.d.ts +27 -1
- package/dist/src/planner/util/ind-utils.d.ts.map +1 -1
- package/dist/src/planner/util/ind-utils.js +80 -6
- package/dist/src/planner/util/ind-utils.js.map +1 -1
- package/dist/src/planner/util/key-utils.d.ts.map +1 -1
- package/dist/src/planner/util/key-utils.js +81 -12
- package/dist/src/planner/util/key-utils.js.map +1 -1
- package/dist/src/planner/util/set-op-wrapper.d.ts +37 -0
- package/dist/src/planner/util/set-op-wrapper.d.ts.map +1 -0
- package/dist/src/planner/util/set-op-wrapper.js +82 -0
- package/dist/src/planner/util/set-op-wrapper.js.map +1 -0
- package/dist/src/planner/validation/plan-validator.d.ts.map +1 -1
- package/dist/src/planner/validation/plan-validator.js +1 -0
- package/dist/src/planner/validation/plan-validator.js.map +1 -1
- package/dist/src/runtime/context-helpers.d.ts +13 -1
- package/dist/src/runtime/context-helpers.d.ts.map +1 -1
- package/dist/src/runtime/context-helpers.js +7 -1
- package/dist/src/runtime/context-helpers.js.map +1 -1
- package/dist/src/runtime/delta-executor.d.ts +30 -1
- package/dist/src/runtime/delta-executor.d.ts.map +1 -1
- package/dist/src/runtime/delta-executor.js +29 -4
- package/dist/src/runtime/delta-executor.js.map +1 -1
- package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
- package/dist/src/runtime/emit/add-constraint.js +38 -5
- package/dist/src/runtime/emit/add-constraint.js.map +1 -1
- package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
- package/dist/src/runtime/emit/aggregate.js +10 -8
- package/dist/src/runtime/emit/aggregate.js.map +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/alter-table.js +664 -108
- package/dist/src/runtime/emit/alter-table.js.map +1 -1
- package/dist/src/runtime/emit/analyze.d.ts.map +1 -1
- package/dist/src/runtime/emit/analyze.js +2 -1
- package/dist/src/runtime/emit/analyze.js.map +1 -1
- package/dist/src/runtime/emit/asof-scan.d.ts.map +1 -1
- package/dist/src/runtime/emit/asof-scan.js +18 -5
- package/dist/src/runtime/emit/asof-scan.js.map +1 -1
- package/dist/src/runtime/emit/asserted-keys.d.ts +13 -0
- package/dist/src/runtime/emit/asserted-keys.d.ts.map +1 -0
- package/dist/src/runtime/emit/asserted-keys.js +13 -0
- package/dist/src/runtime/emit/asserted-keys.js.map +1 -0
- package/dist/src/runtime/emit/between.d.ts.map +1 -1
- package/dist/src/runtime/emit/between.js +24 -19
- package/dist/src/runtime/emit/between.js.map +1 -1
- package/dist/src/runtime/emit/binary.d.ts.map +1 -1
- package/dist/src/runtime/emit/binary.js +5 -9
- package/dist/src/runtime/emit/binary.js.map +1 -1
- package/dist/src/runtime/emit/block.d.ts.map +1 -1
- package/dist/src/runtime/emit/block.js +11 -2
- package/dist/src/runtime/emit/block.js.map +1 -1
- package/dist/src/runtime/emit/bloom-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/bloom-join.js +8 -2
- package/dist/src/runtime/emit/bloom-join.js.map +1 -1
- package/dist/src/runtime/emit/constraint-check.js +15 -0
- package/dist/src/runtime/emit/constraint-check.js.map +1 -1
- package/dist/src/runtime/emit/create-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-table.js +8 -0
- package/dist/src/runtime/emit/create-table.js.map +1 -1
- package/dist/src/runtime/emit/create-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/create-view.js +16 -1
- package/dist/src/runtime/emit/create-view.js.map +1 -1
- package/dist/src/runtime/emit/dml-executor.d.ts +27 -0
- package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
- package/dist/src/runtime/emit/dml-executor.js +413 -193
- package/dist/src/runtime/emit/dml-executor.js.map +1 -1
- package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-table.js +10 -0
- package/dist/src/runtime/emit/drop-table.js.map +1 -1
- package/dist/src/runtime/emit/drop-view.d.ts.map +1 -1
- package/dist/src/runtime/emit/drop-view.js +17 -0
- package/dist/src/runtime/emit/drop-view.js.map +1 -1
- package/dist/src/runtime/emit/envelope-scan.d.ts +13 -0
- package/dist/src/runtime/emit/envelope-scan.d.ts.map +1 -0
- package/dist/src/runtime/emit/envelope-scan.js +22 -0
- package/dist/src/runtime/emit/envelope-scan.js.map +1 -0
- package/dist/src/runtime/emit/join.d.ts +10 -2
- package/dist/src/runtime/emit/join.d.ts.map +1 -1
- package/dist/src/runtime/emit/join.js +128 -38
- package/dist/src/runtime/emit/join.js.map +1 -1
- package/dist/src/runtime/emit/lens-auxiliary-access.d.ts +16 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.d.ts.map +1 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.js +16 -0
- package/dist/src/runtime/emit/lens-auxiliary-access.js.map +1 -0
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts +640 -0
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -0
- package/dist/src/runtime/emit/materialized-view-helpers.js +2576 -0
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -0
- package/dist/src/runtime/emit/materialized-view.d.ts +31 -0
- package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -0
- package/dist/src/runtime/emit/materialized-view.js +187 -0
- package/dist/src/runtime/emit/materialized-view.js.map +1 -0
- package/dist/src/runtime/emit/merge-join.d.ts.map +1 -1
- package/dist/src/runtime/emit/merge-join.js +15 -3
- package/dist/src/runtime/emit/merge-join.js.map +1 -1
- package/dist/src/runtime/emit/project.d.ts.map +1 -1
- package/dist/src/runtime/emit/project.js +10 -5
- package/dist/src/runtime/emit/project.js.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.d.ts +1 -0
- package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.js +101 -5
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/runtime/emit/set-object-tags.d.ts +16 -0
- package/dist/src/runtime/emit/set-object-tags.d.ts.map +1 -0
- package/dist/src/runtime/emit/set-object-tags.js +57 -0
- package/dist/src/runtime/emit/set-object-tags.js.map +1 -0
- package/dist/src/runtime/emit/set-operation.d.ts.map +1 -1
- package/dist/src/runtime/emit/set-operation.js +140 -24
- package/dist/src/runtime/emit/set-operation.js.map +1 -1
- package/dist/src/runtime/emit/subquery.d.ts.map +1 -1
- package/dist/src/runtime/emit/subquery.js +110 -5
- package/dist/src/runtime/emit/subquery.js.map +1 -1
- package/dist/src/runtime/emit/unary.d.ts.map +1 -1
- package/dist/src/runtime/emit/unary.js +34 -6
- package/dist/src/runtime/emit/unary.js.map +1 -1
- package/dist/src/runtime/emit/view-mutation.d.ts +70 -0
- package/dist/src/runtime/emit/view-mutation.d.ts.map +1 -0
- package/dist/src/runtime/emit/view-mutation.js +299 -0
- package/dist/src/runtime/emit/view-mutation.js.map +1 -0
- package/dist/src/runtime/emit/window.js +29 -5
- package/dist/src/runtime/emit/window.js.map +1 -1
- package/dist/src/runtime/foreign-key-actions.d.ts +66 -3
- package/dist/src/runtime/foreign-key-actions.d.ts.map +1 -1
- package/dist/src/runtime/foreign-key-actions.js +580 -172
- package/dist/src/runtime/foreign-key-actions.js.map +1 -1
- package/dist/src/runtime/parallel-driver.d.ts +4 -1
- package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
- package/dist/src/runtime/parallel-driver.js +5 -1
- package/dist/src/runtime/parallel-driver.js.map +1 -1
- package/dist/src/runtime/register.d.ts.map +1 -1
- package/dist/src/runtime/register.js +17 -1
- package/dist/src/runtime/register.js.map +1 -1
- package/dist/src/runtime/types.d.ts +10 -0
- package/dist/src/runtime/types.d.ts.map +1 -1
- package/dist/src/runtime/types.js.map +1 -1
- package/dist/src/schema/basis-backfill.d.ts +63 -0
- package/dist/src/schema/basis-backfill.d.ts.map +1 -0
- package/dist/src/schema/basis-backfill.js +161 -0
- package/dist/src/schema/basis-backfill.js.map +1 -0
- package/dist/src/schema/catalog.d.ts +115 -1
- package/dist/src/schema/catalog.d.ts.map +1 -1
- package/dist/src/schema/catalog.js +249 -22
- package/dist/src/schema/catalog.js.map +1 -1
- package/dist/src/schema/change-events.d.ts +42 -1
- package/dist/src/schema/change-events.d.ts.map +1 -1
- package/dist/src/schema/change-events.js.map +1 -1
- package/dist/src/schema/column.d.ts +16 -0
- package/dist/src/schema/column.d.ts.map +1 -1
- package/dist/src/schema/column.js.map +1 -1
- package/dist/src/schema/constraint-builder.d.ts +182 -0
- package/dist/src/schema/constraint-builder.d.ts.map +1 -0
- package/dist/src/schema/constraint-builder.js +424 -0
- package/dist/src/schema/constraint-builder.js.map +1 -0
- package/dist/src/schema/ddl-generator.d.ts +86 -1
- package/dist/src/schema/ddl-generator.d.ts.map +1 -1
- package/dist/src/schema/ddl-generator.js +316 -20
- package/dist/src/schema/ddl-generator.js.map +1 -1
- package/dist/src/schema/declared-schema-manager.d.ts +51 -0
- package/dist/src/schema/declared-schema-manager.d.ts.map +1 -1
- package/dist/src/schema/declared-schema-manager.js +61 -0
- package/dist/src/schema/declared-schema-manager.js.map +1 -1
- package/dist/src/schema/derivation.d.ts +106 -0
- package/dist/src/schema/derivation.d.ts.map +1 -0
- package/dist/src/schema/derivation.js +25 -0
- package/dist/src/schema/derivation.js.map +1 -0
- package/dist/src/schema/function.d.ts +13 -0
- package/dist/src/schema/function.d.ts.map +1 -1
- package/dist/src/schema/function.js.map +1 -1
- package/dist/src/schema/lens-ack.d.ts +90 -0
- package/dist/src/schema/lens-ack.d.ts.map +1 -0
- package/dist/src/schema/lens-ack.js +361 -0
- package/dist/src/schema/lens-ack.js.map +1 -0
- package/dist/src/schema/lens-compiler.d.ts +62 -0
- package/dist/src/schema/lens-compiler.d.ts.map +1 -0
- package/dist/src/schema/lens-compiler.js +1594 -0
- package/dist/src/schema/lens-compiler.js.map +1 -0
- package/dist/src/schema/lens-fk-discovery.d.ts +175 -0
- package/dist/src/schema/lens-fk-discovery.d.ts.map +1 -0
- package/dist/src/schema/lens-fk-discovery.js +336 -0
- package/dist/src/schema/lens-fk-discovery.js.map +1 -0
- package/dist/src/schema/lens-prover.d.ts +336 -0
- package/dist/src/schema/lens-prover.d.ts.map +1 -0
- package/dist/src/schema/lens-prover.js +1988 -0
- package/dist/src/schema/lens-prover.js.map +1 -0
- package/dist/src/schema/lens.d.ts +254 -0
- package/dist/src/schema/lens.d.ts.map +1 -0
- package/dist/src/schema/lens.js +21 -0
- package/dist/src/schema/lens.js.map +1 -0
- package/dist/src/schema/manager.d.ts +676 -18
- package/dist/src/schema/manager.d.ts.map +1 -1
- package/dist/src/schema/manager.js +1573 -238
- package/dist/src/schema/manager.js.map +1 -1
- package/dist/src/schema/mapping-advertisement-tags.d.ts +39 -0
- package/dist/src/schema/mapping-advertisement-tags.d.ts.map +1 -0
- package/dist/src/schema/mapping-advertisement-tags.js +216 -0
- package/dist/src/schema/mapping-advertisement-tags.js.map +1 -0
- package/dist/src/schema/rename-rewriter.d.ts +45 -4
- package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
- package/dist/src/schema/rename-rewriter.js +412 -19
- package/dist/src/schema/rename-rewriter.js.map +1 -1
- package/dist/src/schema/reserved-tags-policy.d.ts +32 -0
- package/dist/src/schema/reserved-tags-policy.d.ts.map +1 -0
- package/dist/src/schema/reserved-tags-policy.js +34 -0
- package/dist/src/schema/reserved-tags-policy.js.map +1 -0
- package/dist/src/schema/reserved-tags.d.ts +170 -0
- package/dist/src/schema/reserved-tags.d.ts.map +1 -0
- package/dist/src/schema/reserved-tags.js +507 -0
- package/dist/src/schema/reserved-tags.js.map +1 -0
- package/dist/src/schema/schema-differ.d.ts +158 -2
- package/dist/src/schema/schema-differ.d.ts.map +1 -1
- package/dist/src/schema/schema-differ.js +1460 -78
- package/dist/src/schema/schema-differ.js.map +1 -1
- package/dist/src/schema/schema-hasher.d.ts +8 -3
- package/dist/src/schema/schema-hasher.d.ts.map +1 -1
- package/dist/src/schema/schema-hasher.js +22 -2
- package/dist/src/schema/schema-hasher.js.map +1 -1
- package/dist/src/schema/schema.d.ts +25 -1
- package/dist/src/schema/schema.d.ts.map +1 -1
- package/dist/src/schema/schema.js +36 -2
- package/dist/src/schema/schema.js.map +1 -1
- package/dist/src/schema/table.d.ts +259 -10
- package/dist/src/schema/table.d.ts.map +1 -1
- package/dist/src/schema/table.js +309 -26
- package/dist/src/schema/table.js.map +1 -1
- package/dist/src/schema/unique-enforcement.d.ts +78 -0
- package/dist/src/schema/unique-enforcement.d.ts.map +1 -0
- package/dist/src/schema/unique-enforcement.js +93 -0
- package/dist/src/schema/unique-enforcement.js.map +1 -0
- package/dist/src/schema/view.d.ts +83 -2
- package/dist/src/schema/view.d.ts.map +1 -1
- package/dist/src/schema/view.js +67 -1
- package/dist/src/schema/view.js.map +1 -1
- package/dist/src/schema/window-function.d.ts +9 -1
- package/dist/src/schema/window-function.d.ts.map +1 -1
- package/dist/src/schema/window-function.js.map +1 -1
- package/dist/src/util/comparison.d.ts +24 -0
- package/dist/src/util/comparison.d.ts.map +1 -1
- package/dist/src/util/comparison.js +34 -0
- package/dist/src/util/comparison.js.map +1 -1
- package/dist/src/util/mutation-statement.d.ts.map +1 -1
- package/dist/src/util/mutation-statement.js +4 -1
- package/dist/src/util/mutation-statement.js.map +1 -1
- package/dist/src/util/serialization.d.ts +9 -0
- package/dist/src/util/serialization.d.ts.map +1 -1
- package/dist/src/util/serialization.js +26 -0
- package/dist/src/util/serialization.js.map +1 -1
- package/dist/src/vtab/backing-host.d.ts +286 -0
- package/dist/src/vtab/backing-host.d.ts.map +1 -0
- package/dist/src/vtab/backing-host.js +118 -0
- package/dist/src/vtab/backing-host.js.map +1 -0
- package/dist/src/vtab/best-access-plan.d.ts +21 -0
- package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
- package/dist/src/vtab/best-access-plan.js.map +1 -1
- package/dist/src/vtab/capabilities.d.ts +5 -5
- package/dist/src/vtab/capabilities.d.ts.map +1 -1
- package/dist/src/vtab/mapping-advertisement.d.ts +163 -0
- package/dist/src/vtab/mapping-advertisement.d.ts.map +1 -0
- package/dist/src/vtab/mapping-advertisement.js +2 -0
- package/dist/src/vtab/mapping-advertisement.js.map +1 -0
- package/dist/src/vtab/memory/index.d.ts +64 -4
- package/dist/src/vtab/memory/index.d.ts.map +1 -1
- package/dist/src/vtab/memory/index.js +119 -12
- package/dist/src/vtab/memory/index.js.map +1 -1
- package/dist/src/vtab/memory/layer/base.d.ts +38 -1
- package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/base.js +112 -24
- package/dist/src/vtab/memory/layer/base.js.map +1 -1
- package/dist/src/vtab/memory/layer/manager.d.ts +291 -4
- package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.js +1050 -91
- package/dist/src/vtab/memory/layer/manager.js.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/plan-filter.js +35 -6
- package/dist/src/vtab/memory/layer/plan-filter.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-layer.js +66 -14
- package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.d.ts +14 -0
- package/dist/src/vtab/memory/layer/scan-plan.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/scan-plan.js +27 -4
- package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/transaction.js +5 -1
- package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
- package/dist/src/vtab/memory/module.d.ts +17 -0
- package/dist/src/vtab/memory/module.d.ts.map +1 -1
- package/dist/src/vtab/memory/module.js +82 -3
- package/dist/src/vtab/memory/module.js.map +1 -1
- package/dist/src/vtab/memory/table.d.ts.map +1 -1
- package/dist/src/vtab/memory/table.js +15 -5
- package/dist/src/vtab/memory/table.js.map +1 -1
- package/dist/src/vtab/memory/types.d.ts +20 -2
- package/dist/src/vtab/memory/types.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/predicate.js +46 -24
- package/dist/src/vtab/memory/utils/predicate.js.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key-encode.d.ts +31 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.d.ts.map +1 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.js +101 -0
- package/dist/src/vtab/memory/utils/primary-key-encode.js.map +1 -0
- package/dist/src/vtab/memory/utils/primary-key.d.ts +8 -0
- package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key.js +12 -5
- package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
- package/dist/src/vtab/module.d.ts +203 -4
- package/dist/src/vtab/module.d.ts.map +1 -1
- package/dist/src/vtab/table.d.ts +9 -0
- package/dist/src/vtab/table.d.ts.map +1 -1
- package/dist/src/vtab/table.js.map +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,1988 @@
|
|
|
1
|
+
import { resolvePkDefaultConflict, findDeclaredKey, findGoverningBasisKeys } from './table.js';
|
|
2
|
+
import { addFd, superkeyToFd } from '../planner/util/fd-utils.js';
|
|
3
|
+
import { proveEffectiveKeyUnique } from '../planner/analysis/coverage-prover.js';
|
|
4
|
+
import { resolveBaseSite } from '../planner/analysis/update-lineage.js';
|
|
5
|
+
import { viewComplement } from '../planner/analysis/view-complement.js';
|
|
6
|
+
import { getTrustedCheckExtraction, containsNonDeterministicCall } from '../planner/analysis/check-extraction.js';
|
|
7
|
+
import { createRuntimeExpressionEvaluator } from '../planner/analysis/const-evaluator.js';
|
|
8
|
+
import { classifyViewBody } from '../planner/mutation/propagate.js';
|
|
9
|
+
import { substituteNewRefs, transformExpr } from '../planner/mutation/scope-transform.js';
|
|
10
|
+
import { ProjectNode } from '../planner/nodes/project-node.js';
|
|
11
|
+
import { PlanNodeType } from '../planner/nodes/plan-node-type.js';
|
|
12
|
+
import { astToString, expressionToString } from '../emit/ast-stringify.js';
|
|
13
|
+
import { PhysicalType } from '../types/logical-type.js';
|
|
14
|
+
import { getReservedTagByTemplate, LENS_WRITABLE_INTENT_TAG } from './reserved-tags.js';
|
|
15
|
+
import { createLogger } from '../common/logger.js';
|
|
16
|
+
import { ConflictResolution, FunctionFlags } from '../common/constants.js';
|
|
17
|
+
import { compareSqlValues } from '../util/comparison.js';
|
|
18
|
+
const log = createLogger('schema:lens-prover');
|
|
19
|
+
/**
|
|
20
|
+
* The warning-severity advisory codes that flow through ack/escalation
|
|
21
|
+
* governance (`lens-ack.ts`). This is the single authoritative source of the
|
|
22
|
+
* governable vocabulary: a policy (`error-on` / `require-ack`) may legitimately
|
|
23
|
+
* name any of these, and only these. Keeping the list here means it cannot drift
|
|
24
|
+
* from what the prover actually emits.
|
|
25
|
+
*/
|
|
26
|
+
const ADVISORY_CODE_LIST = [
|
|
27
|
+
'lens.pk-not-reconstructible',
|
|
28
|
+
'lens.no-backing-index',
|
|
29
|
+
'lens.no-answering-structure',
|
|
30
|
+
'lens.partial-override',
|
|
31
|
+
'lens.getput-lossy',
|
|
32
|
+
'lens.over-restrictive-basis-key',
|
|
33
|
+
];
|
|
34
|
+
/** The advisory codes a policy may escalate, as a runtime set (drift-locked by a unit test). */
|
|
35
|
+
export const ACKNOWLEDGEABLE_ADVISORY_CODES = new Set(ADVISORY_CODE_LIST);
|
|
36
|
+
/**
|
|
37
|
+
* Proves one lens slot and classifies its constraints. Pure analysis — does not
|
|
38
|
+
* mutate the slot or the catalog; the caller (`lens-compiler.ts`) records the
|
|
39
|
+
* `obligations` / `readOnly` on the slot and aggregates the diagnostics into the
|
|
40
|
+
* deploy report.
|
|
41
|
+
*/
|
|
42
|
+
export function proveLens(slot, db) {
|
|
43
|
+
const ctx = buildProveContext(slot, db);
|
|
44
|
+
const errors = [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
checkColumnCoverage(ctx, errors);
|
|
47
|
+
checkTypeAndNullability(ctx, errors);
|
|
48
|
+
// Run the round-trip enumeration ONCE, up front: it produces both the
|
|
49
|
+
// proven-bijection verdict per authored column (the same `{proved, injective}`
|
|
50
|
+
// that suppresses `lens.getput-lossy`) and the cached enumeration the
|
|
51
|
+
// round-trip diagnostics consume. Hoisting it ahead of key reconstructibility
|
|
52
|
+
// is what lets a bijective authored PK column count as reconstructible
|
|
53
|
+
// (writable) instead of read-only — a bijection maps a written logical key to
|
|
54
|
+
// exactly one basis key and back. {@link emitRoundTrip} consumes the cache; it
|
|
55
|
+
// never re-runs the enumeration.
|
|
56
|
+
const rt = analyzeRoundTrip(ctx);
|
|
57
|
+
const bijectiveAuthored = bijectiveAuthoredColumns(rt);
|
|
58
|
+
const readOnly = checkKeyReconstructibility(ctx, bijectiveAuthored, warnings);
|
|
59
|
+
emitRoundTrip(ctx, rt, readOnly, errors, warnings);
|
|
60
|
+
const obligations = classifyObligations(ctx, readOnly, bijectiveAuthored, errors, warnings);
|
|
61
|
+
checkAnsweringStructures(ctx, warnings);
|
|
62
|
+
checkPartialOverride(ctx, warnings);
|
|
63
|
+
return { errors, warnings, obligations, readOnly };
|
|
64
|
+
}
|
|
65
|
+
function buildProveContext(slot, db) {
|
|
66
|
+
const table = slot.logicalTable;
|
|
67
|
+
const logicalColIndex = new Map();
|
|
68
|
+
table.columns.forEach((c, i) => logicalColIndex.set(c.name.toLowerCase(), i));
|
|
69
|
+
const { outputIndex, outputColumns } = buildOutputIndex(slot);
|
|
70
|
+
const basisSchemaName = slot.defaultBasis.schemaName;
|
|
71
|
+
return {
|
|
72
|
+
slot,
|
|
73
|
+
db,
|
|
74
|
+
table,
|
|
75
|
+
logicalColIndex,
|
|
76
|
+
outputColumns,
|
|
77
|
+
outputIndex,
|
|
78
|
+
root: planBody(db, slot.compiledBody),
|
|
79
|
+
basisSource: resolveSingleBasisSource(db.schemaManager, slot.compiledBody, basisSchemaName),
|
|
80
|
+
basisSchemaName,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The output-index map for a lens slot: logical column name (lower) →
|
|
85
|
+
* body-output column index, plus the columns in declaration order. The single
|
|
86
|
+
* source of truth for the body's output-column-index space — shared by
|
|
87
|
+
* {@link buildProveContext} and {@link computeLensAssertedKeyFds} so the two can
|
|
88
|
+
* never drift (the FD-contribution columns must land in exactly the space the
|
|
89
|
+
* prover proved its keys in).
|
|
90
|
+
*/
|
|
91
|
+
function buildOutputIndex(slot) {
|
|
92
|
+
const outputColumns = [];
|
|
93
|
+
const outputIndex = new Map();
|
|
94
|
+
for (const p of slot.columnProvenance) {
|
|
95
|
+
outputIndex.set(p.logicalColumn.toLowerCase(), outputColumns.length);
|
|
96
|
+
outputColumns.push(p.logicalColumn);
|
|
97
|
+
}
|
|
98
|
+
return { outputIndex, outputColumns };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Plans + optimizes the compiled body so `physical.fds` and output column types
|
|
102
|
+
* are available. Returns undefined (graceful) if planning throws — the body the
|
|
103
|
+
* compiler produced should plan, but a prover that itself crashes the deploy is
|
|
104
|
+
* worse than one that skips plan-derived checks.
|
|
105
|
+
*/
|
|
106
|
+
function planBody(db, body) {
|
|
107
|
+
try {
|
|
108
|
+
return db.getPlan(astToString(body)).getRelations()[0];
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
log('lens-prover: body failed to plan, skipping plan-derived checks: %O', e);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** The single basis `table` source of a body, or undefined for a multi-source / opaque FROM. */
|
|
116
|
+
function resolveSingleBasisSource(schemaManager, body, basisSchemaName) {
|
|
117
|
+
const from = body.from;
|
|
118
|
+
if (!from || from.length !== 1)
|
|
119
|
+
return undefined;
|
|
120
|
+
const node = from[0];
|
|
121
|
+
if (node.type !== 'table')
|
|
122
|
+
return undefined;
|
|
123
|
+
const schemaName = node.table.schema ?? basisSchemaName;
|
|
124
|
+
return schemaManager.getSchema(schemaName)?.getTable(node.table.name);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The single basis `table` source of a lens slot's compiled body, or undefined for
|
|
128
|
+
* a multi-source / opaque FROM — the exported slot-level entry point. Reused by the
|
|
129
|
+
* lens FK-redundancy detector (`planner/mutation/lens-enforcement.ts`) so it walks
|
|
130
|
+
* the same single-source `from` the prover does, resolving a bare table name against
|
|
131
|
+
* the slot's own default basis schema. Reads only the catalog, so it is safe over a
|
|
132
|
+
* lightweight (un-planned) caller.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveSlotBasisSource(slot, schemaManager) {
|
|
135
|
+
return resolveSingleBasisSource(schemaManager, slot.compiledBody, slot.defaultBasis.schemaName);
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Error: column coverage (lens.uncovered-column)
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
/**
|
|
141
|
+
* Every logical column resolves to a basis expression. The compiler's gap-fill
|
|
142
|
+
* path already errors on an uncovered column before the prover runs, so this is
|
|
143
|
+
* the formal restatement / backstop: a provenance entry must exist for every
|
|
144
|
+
* column and must be `override` or `default`.
|
|
145
|
+
*/
|
|
146
|
+
function checkColumnCoverage(ctx, errors) {
|
|
147
|
+
const provByName = new Map(ctx.slot.columnProvenance.map(p => [p.logicalColumn.toLowerCase(), p]));
|
|
148
|
+
for (const col of ctx.table.columns) {
|
|
149
|
+
const p = provByName.get(col.name.toLowerCase());
|
|
150
|
+
if (!p) {
|
|
151
|
+
errors.push({
|
|
152
|
+
code: 'lens.uncovered-column',
|
|
153
|
+
severity: 'error',
|
|
154
|
+
site: { table: ctx.table.name, column: col.name },
|
|
155
|
+
message: `lens: logical column '${ctx.table.schemaName}.${ctx.table.name}.${col.name}' is not covered by the compiled body (no override mapping and no default gap-fill)`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function physicalFamily(pt) {
|
|
161
|
+
switch (pt) {
|
|
162
|
+
case PhysicalType.INTEGER:
|
|
163
|
+
case PhysicalType.REAL: return 'numeric';
|
|
164
|
+
case PhysicalType.TEXT: return 'text';
|
|
165
|
+
case PhysicalType.BLOB: return 'blob';
|
|
166
|
+
case PhysicalType.BOOLEAN: return 'boolean';
|
|
167
|
+
default: return 'other'; // NULL / OBJECT — permissive
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Conservative cross-family compatibility. `other` (NULL/OBJECT) is compatible
|
|
172
|
+
* with anything; numeric and boolean are mutually compatible (SQLite stores
|
|
173
|
+
* booleans as integers). Only a clear cross-family mismatch (numeric↔text,
|
|
174
|
+
* text↔blob, …) is reported, so a faithfully-aligned basis never false-errors.
|
|
175
|
+
*/
|
|
176
|
+
function familiesCompatible(a, b) {
|
|
177
|
+
if (a === b)
|
|
178
|
+
return true;
|
|
179
|
+
if (a === 'other' || b === 'other')
|
|
180
|
+
return true;
|
|
181
|
+
const numericish = (f) => f === 'numeric' || f === 'boolean';
|
|
182
|
+
return numericish(a) && numericish(b);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Each mapped column's basis-derived type & nullability satisfy the logical
|
|
186
|
+
* declaration. A nullable basis expression under a `not null` logical column
|
|
187
|
+
* errors unless the logical column supplies a total default. Read off the
|
|
188
|
+
* optimized body's output relation; skipped when the body did not plan.
|
|
189
|
+
*/
|
|
190
|
+
function checkTypeAndNullability(ctx, errors) {
|
|
191
|
+
if (!ctx.root)
|
|
192
|
+
return;
|
|
193
|
+
const outCols = ctx.root.getType().columns;
|
|
194
|
+
for (const col of ctx.table.columns) {
|
|
195
|
+
const oi = ctx.outputIndex.get(col.name.toLowerCase());
|
|
196
|
+
if (oi === undefined)
|
|
197
|
+
continue; // defensive: column absent from the body output
|
|
198
|
+
const outCol = outCols[oi];
|
|
199
|
+
if (!outCol)
|
|
200
|
+
continue;
|
|
201
|
+
const outType = outCol.type;
|
|
202
|
+
if (outType.typeClass !== 'scalar')
|
|
203
|
+
continue;
|
|
204
|
+
const declared = physicalFamily(col.logicalType.physicalType);
|
|
205
|
+
const basis = physicalFamily(outType.logicalType.physicalType);
|
|
206
|
+
if (!familiesCompatible(declared, basis)) {
|
|
207
|
+
errors.push({
|
|
208
|
+
code: 'lens.type-mismatch',
|
|
209
|
+
severity: 'error',
|
|
210
|
+
site: { table: ctx.table.name, column: col.name },
|
|
211
|
+
message: `lens: logical column '${ctx.table.name}.${col.name}' declares type '${col.logicalType.name}' but its basis-derived expression has type '${outType.logicalType.name}' (incompatible storage affinities)`,
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Nullability: not-null logical column over a nullable basis expression with
|
|
216
|
+
// no total default is unsound (a NULL could be read into a not-null column).
|
|
217
|
+
if (col.notNull && outType.nullable === true && col.defaultValue === null) {
|
|
218
|
+
errors.push({
|
|
219
|
+
code: 'lens.nullability-mismatch',
|
|
220
|
+
severity: 'error',
|
|
221
|
+
site: { table: ctx.table.name, column: col.name },
|
|
222
|
+
message: `lens: logical column '${ctx.table.name}.${col.name}' is declared NOT NULL but its basis-derived expression is nullable and no default supplies a value`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Error→read-only: key reconstructibility (lens.pk-not-reconstructible)
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
/**
|
|
231
|
+
* For a writable logical table the logical PK must be reconstructible at the lens
|
|
232
|
+
* boundary — each PK column either maps to a plain (invertible) basis column
|
|
233
|
+
* projection ({@link isReconstructibleColumn}) **or** is an authored
|
|
234
|
+
* (`with inverse`) column whose forward/inverse pair the round-trip enumeration
|
|
235
|
+
* proved a **bijection** (`bijectiveAuthored`). A bijective key is fully
|
|
236
|
+
* reconstructible: a written logical key maps to exactly one basis key and back.
|
|
237
|
+
* When a PK column is neither (a computed / aggregated / non-injective authored
|
|
238
|
+
* column), the table is **read-only**: reads still work, but any mutation errors
|
|
239
|
+
* at the lens (`planner/mutation/single-source.ts` `analyzeView` raises). This is
|
|
240
|
+
* not a deploy-blocking error — the table deploys read-only — so it surfaces as a
|
|
241
|
+
* warning, and `readOnly` is set on the slot. The empty (singleton) PK is
|
|
242
|
+
* vacuously reconstructible.
|
|
243
|
+
*
|
|
244
|
+
* @returns whether the table is read-only.
|
|
245
|
+
*/
|
|
246
|
+
function checkKeyReconstructibility(ctx, bijectiveAuthored, warnings) {
|
|
247
|
+
const pk = ctx.table.primaryKeyDefinition;
|
|
248
|
+
if (pk.length === 0)
|
|
249
|
+
return false; // singleton — 0-or-1 row, vacuously reconstructible
|
|
250
|
+
const unreconstructible = [];
|
|
251
|
+
for (const pkCol of pk) {
|
|
252
|
+
const name = ctx.table.columns[pkCol.index]?.name;
|
|
253
|
+
if (name === undefined || !(isReconstructibleColumn(ctx, name) || bijectiveAuthored.has(name.toLowerCase()))) {
|
|
254
|
+
unreconstructible.push(name ?? `#${pkCol.index}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (unreconstructible.length === 0)
|
|
258
|
+
return false;
|
|
259
|
+
warnings.push({
|
|
260
|
+
code: 'lens.pk-not-reconstructible',
|
|
261
|
+
severity: 'warning',
|
|
262
|
+
site: { table: ctx.table.name },
|
|
263
|
+
message: `lens: logical table '${ctx.table.name}' is read-only — its primary key is not reconstructible at the lens boundary (column(s) ${unreconstructible.map(c => `'${c}'`).join(', ')} have no invertible basis write path); mutations against it will error`,
|
|
264
|
+
});
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A logical column is **bare-reconstructible** iff its body-output projection term
|
|
269
|
+
* is a plain column reference (so a written value maps straight back to a basis
|
|
270
|
+
* column). A computed (non-column) projection has no bare write path — but an
|
|
271
|
+
* authored (`with inverse`) computed column may still be key-reconstructible by a
|
|
272
|
+
* proven bijection; that case is handled by the `bijectiveAuthored` set, not here.
|
|
273
|
+
*/
|
|
274
|
+
function isReconstructibleColumn(ctx, columnName) {
|
|
275
|
+
const oi = ctx.outputIndex.get(columnName.toLowerCase());
|
|
276
|
+
if (oi === undefined)
|
|
277
|
+
return false; // not in the body output — not writable through the lens
|
|
278
|
+
const rc = ctx.slot.compiledBody.columns[oi];
|
|
279
|
+
return rc?.type === 'column' && rc.expr.type === 'column';
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Round-trip (GetPut / PutGet) over the writable fragment, **computed at deploy**
|
|
283
|
+
* from the predicate-honest complement ({@link viewComplement}). Because Quereus
|
|
284
|
+
* resolves the Bancilhon–Spyratos ambiguity by predicate-honest fan-out, the
|
|
285
|
+
* complement is *determined, not chosen*, which makes the two laws decidable over
|
|
286
|
+
* the single-source projection-and-filter fragment with no theorem prover:
|
|
287
|
+
*
|
|
288
|
+
* - **GetPut** ("read a row, write the same values back ⇒ base unchanged") holds
|
|
289
|
+
* iff `put` leaves the complement **fixed**: no writable column's backward
|
|
290
|
+
* write path targets a base column the complement lists as *hidden*.
|
|
291
|
+
* - **PutGet** ("write a value through the view, read it back ⇒ get the written
|
|
292
|
+
* value") holds iff, for every column the lens presents as **writable**, the
|
|
293
|
+
* composed `get ∘ put` is the identity on the writable value, and any `domain`
|
|
294
|
+
* restriction the column's inverse carries is **entailed** by the residual
|
|
295
|
+
* predicate.
|
|
296
|
+
*
|
|
297
|
+
* This is the *analysis* half, split from the diagnostic emission
|
|
298
|
+
* ({@link emitRoundTrip}) so the per-authored-column enumeration runs **exactly
|
|
299
|
+
* once**: each authored verdict's {@link provePutGetByEnumeration} result is cached
|
|
300
|
+
* keyed by the logical column (lowercased), and both the bijection-set consumer and
|
|
301
|
+
* the emitter read the cache. The split is a pure refactor — the cached enumeration
|
|
302
|
+
* is byte-identical to what the formerly-inline authored-inverse check computed.
|
|
303
|
+
*
|
|
304
|
+
* Degrade-to-safe: yields no verdicts (and an empty enum map) whenever the
|
|
305
|
+
* complement cannot characterize the body (out of the single-source projection-and-
|
|
306
|
+
* filter fragment, lineage not threaded, or a non-negation-free residual) — today's
|
|
307
|
+
* behaviour, the mutation-time and key-reconstructibility nets still govern. The
|
|
308
|
+
* body is planned **logically** ({@link planLogicalBody}, not `ctx.root`) so the
|
|
309
|
+
* Project/Filter/TableReference operator tree threading `updateLineage` survives.
|
|
310
|
+
* See `docs/lens.md` § Round-trip and `docs/view-updateability.md`
|
|
311
|
+
* § The predicate-honest complement.
|
|
312
|
+
*/
|
|
313
|
+
function analyzeRoundTrip(ctx) {
|
|
314
|
+
const authoredEnum = new Map();
|
|
315
|
+
const root = planLogicalBody(ctx);
|
|
316
|
+
if (!root)
|
|
317
|
+
return { authoredEnum }; // body failed to plan logically → safe verdict
|
|
318
|
+
const verdicts = computeRoundTrip(root);
|
|
319
|
+
if (!verdicts)
|
|
320
|
+
return { authoredEnum }; // out of fragment / indeterminate complement → safe verdict
|
|
321
|
+
verdicts.forEach((v, i) => {
|
|
322
|
+
if (!v.authored)
|
|
323
|
+
return;
|
|
324
|
+
// Site at the *logical* column (the contract spelling), positionally aligned
|
|
325
|
+
// with the body output — the same space {@link emitRoundTrip} re-derives it in.
|
|
326
|
+
const column = ctx.outputColumns[i] ?? v.name;
|
|
327
|
+
authoredEnum.set(column.toLowerCase(), provePutGetByEnumeration(ctx, column, v.authored, v.forward));
|
|
328
|
+
});
|
|
329
|
+
return { verdicts, authoredEnum };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The authored logical columns (lowercased) the round-trip enumeration proved a
|
|
333
|
+
* **bijection** (`{kind:'proved', injective:true}`) — the same verdict that
|
|
334
|
+
* suppresses `lens.getput-lossy`. A bijective authored column is key-reconstructible
|
|
335
|
+
* (a written logical key maps to exactly one basis key and back) and its logical key
|
|
336
|
+
* is intrinsically unique, so this set gates both {@link checkKeyReconstructibility}
|
|
337
|
+
* and the bijection-transport `proved` classification in {@link classifyKeyConstraint}.
|
|
338
|
+
* A column outside the set (non-injective, indeterminate, or a violation) confers no
|
|
339
|
+
* reconstructibility.
|
|
340
|
+
*/
|
|
341
|
+
function bijectiveAuthoredColumns(rt) {
|
|
342
|
+
const set = new Set();
|
|
343
|
+
for (const [column, enumResult] of rt.authoredEnum) {
|
|
344
|
+
if (enumResult.kind === 'proved' && enumResult.injective)
|
|
345
|
+
set.add(column);
|
|
346
|
+
}
|
|
347
|
+
return set;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Emits the round-trip diagnostics from the cached {@link RoundTripAnalysis} — the
|
|
351
|
+
* *diagnostic* half, split from {@link analyzeRoundTrip} (which already ran the
|
|
352
|
+
* enumeration). The firing rule has three branches:
|
|
353
|
+
* 1. a column the lens presents as writable (a `base` {@link ResolvedBaseSite})
|
|
354
|
+
* whose round-trip the analysis cannot prove faithful (`v.writable &&
|
|
355
|
+
* !v.faithful`) — the original rule, unchanged.
|
|
356
|
+
* 2. a `computed`/opaque output column (`!v.writable`) the author *declared*
|
|
357
|
+
* writable via the `quereus.lens.writable = true` intent tag
|
|
358
|
+
* ({@link intentWritable}): the round-trip law's stronger reading makes this
|
|
359
|
+
* an authoring error, not a derived column.
|
|
360
|
+
* 3. an **authored** (`with inverse`) column ({@link emitAuthoredInverseDiagnostics}):
|
|
361
|
+
* writable by construction (it satisfies the writable intent exactly as an
|
|
362
|
+
* inferred inverse does — branch 2 never fires for it). PutGet is checked by
|
|
363
|
+
* *composition* (the cached enumeration): a value that fails to reproduce is the
|
|
364
|
+
* hard `lens.putget-violation` error; GetPut is surrendered by design for a
|
|
365
|
+
* non-injective forward and surfaces as the acknowledgeable `lens.getput-lossy`
|
|
366
|
+
* advisory — suppressed only when the enumeration also proves the forward
|
|
367
|
+
* bijective over the basis domain.
|
|
368
|
+
*
|
|
369
|
+
* An opaque column carrying no intent tag (or `= false`) is *not* a deploy error
|
|
370
|
+
* — it is an intentional read-only/derived column (its write reds `no-inverse` at
|
|
371
|
+
* mutation time, as today), per the prover's soundness-over-completeness principle
|
|
372
|
+
* and the no-over-block requirement (`docs/lens.md` § Computed and Generated
|
|
373
|
+
* Columns). The intent is a deploy-policy input, not a property of the body's
|
|
374
|
+
* complement, so it lives here in the diagnostic wrapper — `computeRoundTrip` and
|
|
375
|
+
* `roundTripObstruction` are untouched. The branch keys off the round-trip
|
|
376
|
+
* verdict's `v.writable` (which admits an invertible *composed* expression like
|
|
377
|
+
* `(speed + 1) - 2`), NOT `isReconstructibleColumn` (the bare-column test, which
|
|
378
|
+
* would false-fire on such a chain).
|
|
379
|
+
*
|
|
380
|
+
* Degrade-to-safe: emits nothing when {@link analyzeRoundTrip} produced no verdicts —
|
|
381
|
+
* so an out-of-fragment opaque column tagged writable does not deploy-block; it still
|
|
382
|
+
* reds `no-inverse` at mutation time. This completeness gap is intentional.
|
|
383
|
+
*/
|
|
384
|
+
function emitRoundTrip(ctx, rt, readOnly, errors, warnings) {
|
|
385
|
+
if (!rt.verdicts)
|
|
386
|
+
return; // degrade-to-safe: no per-column verdicts to emit
|
|
387
|
+
rt.verdicts.forEach((v, i) => {
|
|
388
|
+
// Site at the *logical* column (the contract spelling), positionally aligned
|
|
389
|
+
// with the body output — the same space {@link analyzeRoundTrip} cached it in.
|
|
390
|
+
const column = ctx.outputColumns[i] ?? v.name;
|
|
391
|
+
// (3) An authored (`with inverse`) column: writable by construction (the
|
|
392
|
+
// intent branch below never fires), PutGet checked by composition, GetPut
|
|
393
|
+
// surrendered into the `lens.getput-lossy` advisory. Consumes the cached
|
|
394
|
+
// enumeration (always present for an authored verdict — same iteration).
|
|
395
|
+
if (v.authored) {
|
|
396
|
+
const enumResult = rt.authoredEnum.get(column.toLowerCase());
|
|
397
|
+
if (enumResult)
|
|
398
|
+
emitAuthoredInverseDiagnostics(ctx, column, enumResult, readOnly, errors, warnings);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// (1) A column the lens presents as writable whose round-trip cannot be
|
|
402
|
+
// proved faithful — the original firing rule, unchanged.
|
|
403
|
+
if (v.writable && !v.faithful) {
|
|
404
|
+
errors.push({
|
|
405
|
+
code: 'lens.non-invertible',
|
|
406
|
+
severity: 'error',
|
|
407
|
+
site: { table: ctx.table.name, column },
|
|
408
|
+
message: `lens: writable column '${ctx.table.name}.${column}' is not faithfully invertible at the lens boundary (${v.obstruction}); its GetPut/PutGet round-trip cannot be proved, so the declared write path is unsound`,
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// (2) An opaque / read-only column the author declared writable via the
|
|
413
|
+
// `quereus.lens.writable = true` intent tag. Today it would be silently
|
|
414
|
+
// admitted read-only; the asserted intent turns that into an authoring error.
|
|
415
|
+
if (!v.writable && intentWritable(ctx, column)) {
|
|
416
|
+
errors.push({
|
|
417
|
+
code: 'lens.non-invertible',
|
|
418
|
+
severity: 'error',
|
|
419
|
+
site: { table: ctx.table.name, column },
|
|
420
|
+
message: `lens: column '${ctx.table.name}.${column}' is declared writable ('${LENS_WRITABLE_INTENT_TAG}' = true) but its lens body is computed/opaque with no invertible write path; the round-trip law cannot be satisfied, so the declared writable intent is unsound (map it to an invertible basis expression, or drop the tag to deploy it read-only)`,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Whether the logical column named `column` carries the writable-intent signal
|
|
427
|
+
* (`quereus.lens.writable = true`). Resolves the logical column case-insensitively
|
|
428
|
+
* via the same `logicalColIndex` the rest of the prover uses, then reads the tag
|
|
429
|
+
* as a real boolean (`=== true`): `validateReservedTags` has already rejected a
|
|
430
|
+
* non-boolean value at deploy, so a surviving non-`true` value is `false`/absent.
|
|
431
|
+
*/
|
|
432
|
+
function intentWritable(ctx, column) {
|
|
433
|
+
const li = ctx.logicalColIndex.get(column.toLowerCase());
|
|
434
|
+
if (li === undefined)
|
|
435
|
+
return false;
|
|
436
|
+
return ctx.table.columns[li]?.tags?.[LENS_WRITABLE_INTENT_TAG] === true;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Plan the lens body **logically** (the `view_info`/`column_info` and mutation
|
|
440
|
+
* substrate path via `_buildPlan`, *not* the optimized `ctx.root`), so the clean
|
|
441
|
+
* Project/Filter/TableReference operator tree — and the `updateLineage` it threads
|
|
442
|
+
* — survives (the optimizer degrades a structure-rewriting node's lineage to
|
|
443
|
+
* `computed`; `docs/view-updateability.md` § surface authority). Guarded with the
|
|
444
|
+
* same graceful-degradation `try/catch` as {@link planBody}.
|
|
445
|
+
*/
|
|
446
|
+
function planLogicalBody(ctx) {
|
|
447
|
+
try {
|
|
448
|
+
const { plan } = ctx.db._buildPlan([ctx.slot.compiledBody]);
|
|
449
|
+
return plan.getRelations()[0];
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
log('lens-prover: round-trip body failed to plan logically, degrading to safe: %O', e);
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* The computed per-column GetPut/PutGet verdict over a planned **logical** body —
|
|
458
|
+
* the deploy-time predicate {@link analyzeRoundTrip} / {@link emitRoundTrip} consume,
|
|
459
|
+
* exported so the operational round-trip harness (`test/property.spec.ts` § View
|
|
460
|
+
* Round-Trip Laws) can assert it agrees with the operational law per column.
|
|
461
|
+
*
|
|
462
|
+
* Returns `undefined` (the degrade-to-safe signal) for any body the complement
|
|
463
|
+
* does not characterize: not single-source projection-and-filter (multi-source /
|
|
464
|
+
* join / aggregate / set-op / VALUES / recursive-CTE / LIMIT / OFFSET / DISTINCT),
|
|
465
|
+
* `updateLineage` not threaded, or a residual predicate that is not negation-free.
|
|
466
|
+
* Otherwise one verdict per output column, in attribute order.
|
|
467
|
+
*
|
|
468
|
+
* Each writable site is read through {@link resolveBaseSite} — the same n-way
|
|
469
|
+
* reader the single-source, join, and decomposition put paths share — so the
|
|
470
|
+
* GetPut hidden-column and PutGet inverse-domain checks already generalize past
|
|
471
|
+
* single-source when the complement is later defined on the join/decomposition
|
|
472
|
+
* fragment (`view-write-through-shape-gaps`); only the fragment gate here is
|
|
473
|
+
* single-source-only.
|
|
474
|
+
*/
|
|
475
|
+
export function computeRoundTrip(root) {
|
|
476
|
+
if (!isSingleSourceProjectionFilter(root))
|
|
477
|
+
return undefined;
|
|
478
|
+
const lineage = root.physical.updateLineage;
|
|
479
|
+
if (!lineage)
|
|
480
|
+
return undefined; // lineage not threaded ⇒ complement cannot be characterized
|
|
481
|
+
const complement = viewComplement(root);
|
|
482
|
+
if (complement.residualPredicate && !isNegationFree(complement.residualPredicate)) {
|
|
483
|
+
return undefined; // a non-negation-free residual signals the complement is not honestly determined
|
|
484
|
+
}
|
|
485
|
+
const hidden = new Set(complement.hiddenColumns.map(h => h.column.toLowerCase()));
|
|
486
|
+
const forwardByAttr = collectForwardExprs(root);
|
|
487
|
+
return root.getAttributes().map((attr) => {
|
|
488
|
+
const site = resolveBaseSite(lineage.get(attr.id));
|
|
489
|
+
if (!site.writable) {
|
|
490
|
+
return { attrId: attr.id, name: attr.name, writable: false, faithful: false };
|
|
491
|
+
}
|
|
492
|
+
// An authored (`with inverse`) put: the structural obstructions below are
|
|
493
|
+
// inapplicable (no single verbatim base column, no registry inverse) — the
|
|
494
|
+
// verdict is writable+faithful with the authored payload attached for the
|
|
495
|
+
// prover's dedicated law treatment (enumeration + lossy advisory).
|
|
496
|
+
if (site.authored) {
|
|
497
|
+
return { attrId: attr.id, name: attr.name, writable: true, faithful: true, authored: site.authored, forward: forwardByAttr.get(attr.id) };
|
|
498
|
+
}
|
|
499
|
+
const obstruction = roundTripObstruction(site, hidden, forwardByAttr.get(attr.id), complement.residualPredicate);
|
|
500
|
+
return { attrId: attr.id, name: attr.name, writable: true, faithful: obstruction === undefined, obstruction };
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The GetPut / PutGet obstruction for a writable column, or `undefined` when its
|
|
505
|
+
* round-trip is proved faithful:
|
|
506
|
+
* - **GetPut** — `put` leaves the complement fixed: the writable base column is
|
|
507
|
+
* not one the complement lists as hidden (holds structurally over the single-
|
|
508
|
+
* source fragment — a guard that reds the day a shape violates it).
|
|
509
|
+
* - **PutGet** — `get ∘ put` reproduces the written value ({@link getPutComposesToIdentity},
|
|
510
|
+
* over the closed registry vocabulary), and any inverse `domain` is entailed by
|
|
511
|
+
* the residual. The shipped registry is faithful with unrestricted domains, so
|
|
512
|
+
* this returns `undefined` for it — the seam stays correct as the registry
|
|
513
|
+
* grows a domain-restricted or composed profile.
|
|
514
|
+
*/
|
|
515
|
+
function roundTripObstruction(site, hidden, forward, residual) {
|
|
516
|
+
if (site.baseColumn !== undefined && hidden.has(site.baseColumn.toLowerCase())) {
|
|
517
|
+
return `GetPut: the write-back targets base column '${site.baseColumn}', which the view-complement holds fixed`;
|
|
518
|
+
}
|
|
519
|
+
// `get ∘ put = id` is verifiable only with the forward `get` expression; if it is
|
|
520
|
+
// unavailable (no Project node found) degrade to safe — the shipped registry is
|
|
521
|
+
// faithful by construction, so a missing forward never masks a real violation.
|
|
522
|
+
if (site.inverse !== undefined && forward !== undefined && !getPutComposesToIdentity(forward, site.inverse)) {
|
|
523
|
+
return `PutGet: the 'put' inverse does not reproduce the written value back through 'get'`;
|
|
524
|
+
}
|
|
525
|
+
if (site.domain !== undefined && !domainEntailedBy(site.domain, residual)) {
|
|
526
|
+
return `PutGet: the inverse's domain restriction is not entailed by the view predicate`;
|
|
527
|
+
}
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* The single-source projection-and-filter fragment gate. Reuses {@link classifyViewBody}
|
|
532
|
+
* (the substrate's shape classifier) to reject multi-source / join / aggregate /
|
|
533
|
+
* set-op / VALUES / recursive-CTE bodies, then additionally rejects LIMIT / OFFSET /
|
|
534
|
+
* DISTINCT — which that classifier tolerates as pass-through (so its walk can reach
|
|
535
|
+
* the base table) but the complement does not characterize.
|
|
536
|
+
*/
|
|
537
|
+
function isSingleSourceProjectionFilter(root) {
|
|
538
|
+
if (classifyViewBody(root).kind !== 'single-source')
|
|
539
|
+
return false;
|
|
540
|
+
let windowed = false;
|
|
541
|
+
const visit = (n) => {
|
|
542
|
+
if (n.nodeType === PlanNodeType.LimitOffset || n.nodeType === PlanNodeType.Distinct)
|
|
543
|
+
windowed = true;
|
|
544
|
+
for (const child of n.getRelations())
|
|
545
|
+
visit(child);
|
|
546
|
+
};
|
|
547
|
+
visit(root);
|
|
548
|
+
return !windowed;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* The forward `get` expression per output attribute, read off the topmost
|
|
552
|
+
* {@link ProjectNode}'s projection list (which the planner aligns 1:1 with output
|
|
553
|
+
* attributes, expanding `select *`). The `get` half of the round-trip; the `put`
|
|
554
|
+
* half is the site's `inverse`.
|
|
555
|
+
*/
|
|
556
|
+
function collectForwardExprs(root) {
|
|
557
|
+
const map = new Map();
|
|
558
|
+
const project = findProjectNode(root);
|
|
559
|
+
if (project) {
|
|
560
|
+
for (const p of project.getProjections())
|
|
561
|
+
map.set(p.attributeId, p.node.expression);
|
|
562
|
+
}
|
|
563
|
+
return map;
|
|
564
|
+
}
|
|
565
|
+
/** The topmost {@link ProjectNode} in a planned body's relational spine, or undefined. */
|
|
566
|
+
function findProjectNode(node) {
|
|
567
|
+
if (node instanceof ProjectNode)
|
|
568
|
+
return node;
|
|
569
|
+
for (const child of node.getRelations()) {
|
|
570
|
+
const found = findProjectNode(child);
|
|
571
|
+
if (found)
|
|
572
|
+
return found;
|
|
573
|
+
}
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
/** Numeric probes for the `get ∘ put = id` check — distinct points pin any affine map. */
|
|
577
|
+
const ROUND_TRIP_PROBES = [7, 13, -5];
|
|
578
|
+
/**
|
|
579
|
+
* PutGet identity probe: is the composed `get ∘ put` the identity on the writable
|
|
580
|
+
* value? For each probe `w`, lowers it through the `put` inverse to a base value
|
|
581
|
+
* and re-applies the forward `get`, requiring `get(put(w)) === w`. Sound because a
|
|
582
|
+
* writable column's `get` and the inverse's `put` are built **only** from the
|
|
583
|
+
* law-gated invertibility registry's closed vocabulary (column ref, `± k`, no-op
|
|
584
|
+
* cast, collate), which {@link evalClosed} evaluates exactly; an expression outside
|
|
585
|
+
* it yields `undefined` and reds (the analysis cannot prove faithfulness).
|
|
586
|
+
*
|
|
587
|
+
* Exported as the pure core the operational harness's injected-violation self-test
|
|
588
|
+
* drives (an unfaithful forward/inverse pair must red), mirroring the harness's
|
|
589
|
+
* `injected-widening` / `injected-getput` cores.
|
|
590
|
+
*/
|
|
591
|
+
export function getPutComposesToIdentity(forward, inverse) {
|
|
592
|
+
for (const w of ROUND_TRIP_PROBES) {
|
|
593
|
+
const baseVal = evalClosed(inverse({ type: 'literal', value: w }), undefined); // put: written → base
|
|
594
|
+
if (baseVal === undefined)
|
|
595
|
+
return false;
|
|
596
|
+
const got = evalClosed(forward, baseVal); // get: base → written
|
|
597
|
+
if (got === undefined || got !== w)
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Synchronous evaluator over the **closed** invertibility-registry vocabulary —
|
|
604
|
+
* literal (int/real), the bound column (`columnValue`), `+`/`-`/`*` binary, unary
|
|
605
|
+
* `±`, and the value-preserving `cast`/`collate` wrappers. Returns `undefined` for
|
|
606
|
+
* anything outside that set (the signal that the expression is not a registry
|
|
607
|
+
* round-trip term). Total and side-effect-free — NOT a general expression
|
|
608
|
+
* interpreter; the writable fragment never contains anything else.
|
|
609
|
+
*/
|
|
610
|
+
function evalClosed(expr, columnValue) {
|
|
611
|
+
switch (expr.type) {
|
|
612
|
+
case 'literal': {
|
|
613
|
+
const v = expr.value;
|
|
614
|
+
if (typeof v === 'number')
|
|
615
|
+
return v;
|
|
616
|
+
if (typeof v === 'bigint')
|
|
617
|
+
return Number(v);
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
case 'column':
|
|
621
|
+
return columnValue;
|
|
622
|
+
case 'cast':
|
|
623
|
+
return evalClosed(expr.expr, columnValue);
|
|
624
|
+
case 'collate':
|
|
625
|
+
return evalClosed(expr.expr, columnValue);
|
|
626
|
+
case 'unary': {
|
|
627
|
+
const u = expr;
|
|
628
|
+
const o = evalClosed(u.expr, columnValue);
|
|
629
|
+
if (o === undefined)
|
|
630
|
+
return undefined;
|
|
631
|
+
if (u.operator === '-')
|
|
632
|
+
return -o;
|
|
633
|
+
if (u.operator === '+')
|
|
634
|
+
return o;
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
case 'binary': {
|
|
638
|
+
const b = expr;
|
|
639
|
+
const l = evalClosed(b.left, columnValue);
|
|
640
|
+
const r = evalClosed(b.right, columnValue);
|
|
641
|
+
if (l === undefined || r === undefined)
|
|
642
|
+
return undefined;
|
|
643
|
+
switch (b.operator) {
|
|
644
|
+
case '+': return l + r;
|
|
645
|
+
case '-': return l - r;
|
|
646
|
+
case '*': return l * r;
|
|
647
|
+
default: return undefined;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
default:
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Whether a residual predicate is negation-free — `viewComplement` carries the σ
|
|
656
|
+
* conjuncts verbatim, so the presence of `not` / `is not null` / `!=` (`<>`) /
|
|
657
|
+
* `not between` means the complement is **not** honestly determined and the
|
|
658
|
+
* round-trip check must degrade to the safe verdict. A reflective walk mirroring
|
|
659
|
+
* {@link collectColumnRefNames}.
|
|
660
|
+
*/
|
|
661
|
+
function isNegationFree(expr) {
|
|
662
|
+
const stack = [expr];
|
|
663
|
+
while (stack.length > 0) {
|
|
664
|
+
const node = stack.pop();
|
|
665
|
+
if (node.type === 'unary') {
|
|
666
|
+
const op = node.operator;
|
|
667
|
+
if (op === 'NOT' || op === 'IS NOT NULL')
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
if (node.type === 'binary' && (node.operator === '!=' || node.operator === '<>')) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
if (node.type === 'between' && node.not === true)
|
|
674
|
+
return false;
|
|
675
|
+
for (const key of Object.keys(node)) {
|
|
676
|
+
const value = node[key];
|
|
677
|
+
if (!value)
|
|
678
|
+
continue;
|
|
679
|
+
if (Array.isArray(value)) {
|
|
680
|
+
for (const item of value) {
|
|
681
|
+
if (item && typeof item === 'object' && 'type' in item)
|
|
682
|
+
stack.push(item);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
else if (typeof value === 'object' && 'type' in value) {
|
|
686
|
+
stack.push(value);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Best-effort structural entailment of an inverse's `domain` by the residual
|
|
694
|
+
* predicate — the residual's AND-conjuncts include the domain verbatim. Unreachable
|
|
695
|
+
* today (the shipped registry's profiles carry no `domain`); the conservative seam
|
|
696
|
+
* for a future domain-restricted profile (an un-entailed domain ⇒ a value the view
|
|
697
|
+
* admits could be stored that `get` cannot reproduce ⇒ red).
|
|
698
|
+
*/
|
|
699
|
+
function domainEntailedBy(domain, residual) {
|
|
700
|
+
if (residual === undefined)
|
|
701
|
+
return false;
|
|
702
|
+
const conjuncts = [];
|
|
703
|
+
const split = (e) => {
|
|
704
|
+
if (e.type === 'binary' && e.operator === 'AND') {
|
|
705
|
+
split(e.left);
|
|
706
|
+
split(e.right);
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
conjuncts.push(e);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
split(residual);
|
|
713
|
+
const target = expressionToString(domain);
|
|
714
|
+
return conjuncts.some(c => expressionToString(c) === target);
|
|
715
|
+
}
|
|
716
|
+
/** Enumeration cap — a CHECK `in (...)` domain larger than this degrades to safe. */
|
|
717
|
+
const ENUM_DOMAIN_CAP = 64;
|
|
718
|
+
/**
|
|
719
|
+
* Emits the law-treatment diagnostics for one authored-inverse column (firing-rule
|
|
720
|
+
* branch 3) from its **cached** {@link PutGetEnumeration} (computed once in
|
|
721
|
+
* {@link analyzeRoundTrip}, never re-run here):
|
|
722
|
+
*
|
|
723
|
+
* - **PutGet** (`forward(inverse(v)) ≡ v`) — proved by composition over the
|
|
724
|
+
* logical column's enumerable CHECK `in (...)` domain
|
|
725
|
+
* ({@link provePutGetByEnumeration}). A value that fails to reproduce is the
|
|
726
|
+
* hard `lens.putget-violation` error (a put that loses the written value is
|
|
727
|
+
* never acceptable), sited at the column and naming the offending value. No
|
|
728
|
+
* enumerable domain / non-const-foldable composition → degrade to safe
|
|
729
|
+
* (admit; mutation-time behavior governs — the prover's usual posture, no
|
|
730
|
+
* advisory for the unverified case).
|
|
731
|
+
* - **GetPut** — surrendered by design for a non-injective forward (a
|
|
732
|
+
* write-through normalizes the base value to the inverse's representative):
|
|
733
|
+
* the acknowledgeable `lens.getput-lossy` advisory, suppressed only when the
|
|
734
|
+
* enumeration also proves the forward bijective
|
|
735
|
+
* ({@link proveForwardInjective}). Suppressed wholesale on a read-only table
|
|
736
|
+
* (mutations never run the put — same gate as `lens.no-backing-index`); the
|
|
737
|
+
* PutGet error is NOT read-only-gated, mirroring branch (1)'s posture that a
|
|
738
|
+
* provably unsound declared write path is an authoring error regardless.
|
|
739
|
+
*
|
|
740
|
+
* The advisory's fingerprint carries the rendered domain values, so a CHECK
|
|
741
|
+
* list change (the domain gains a value) re-surfaces an acknowledgment.
|
|
742
|
+
*/
|
|
743
|
+
function emitAuthoredInverseDiagnostics(ctx, column, result, readOnly, errors, warnings) {
|
|
744
|
+
if (result.kind === 'violation') {
|
|
745
|
+
errors.push({
|
|
746
|
+
code: 'lens.putget-violation',
|
|
747
|
+
severity: 'error',
|
|
748
|
+
site: { table: ctx.table.name, column },
|
|
749
|
+
message: `lens: authored inverse on '${ctx.table.name}.${column}' violates PutGet — writing ${renderSqlValue(result.value)} stores a basis image that reads back as ${renderSqlValue(result.got)}; forward(inverse(v)) must reproduce every value of the column's CHECK domain (fix the 'with inverse' expression or the forward mapping)`,
|
|
750
|
+
});
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (result.kind === 'proved' && result.injective)
|
|
754
|
+
return; // bijective over the enumerated domains ⇒ GetPut holds too
|
|
755
|
+
if (readOnly)
|
|
756
|
+
return; // the put never runs on a read-only table — the lossy advisory is moot
|
|
757
|
+
warnings.push({
|
|
758
|
+
code: 'lens.getput-lossy',
|
|
759
|
+
severity: 'warning',
|
|
760
|
+
site: { table: ctx.table.name, column },
|
|
761
|
+
message: `lens: column '${ctx.table.name}.${column}' writes through an authored inverse whose forward mapping is not proven injective — GetPut is surrendered (a write-through normalizes the stored basis value to the inverse's representative). Acknowledge with 'quereus.lens.ack.getput-lossy:${column.toLowerCase()}' if intentional, or make the forward bijective over enumerable CHECK domains.`,
|
|
762
|
+
fingerprintInputs: {
|
|
763
|
+
...buildFingerprint(ctx, [column], false),
|
|
764
|
+
...(result.domain ? { domainValues: result.domain.map(renderSqlValue).sort() } : {}),
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* PutGet by composition: per logical-domain value `v`, lower `v` through every
|
|
770
|
+
* authored put (substituting the `new.<col>` refs with the literal), then re-read
|
|
771
|
+
* it through the forward `get` with each referenced base column bound to its put
|
|
772
|
+
* image — requiring `forward(inverse(v)) ≡ v` under SQL value equality.
|
|
773
|
+
*
|
|
774
|
+
* Preconditions (any miss ⇒ `indeterminate`, the degrade-to-safe signal):
|
|
775
|
+
* a forward expression resolved off the projection; a single basis source (the
|
|
776
|
+
* forward's column refs name-match against put targets — ambiguous past
|
|
777
|
+
* single-source); an inverse that is a function of the written column alone
|
|
778
|
+
* (every `new.*` ref resolves to this column); a deterministic, subquery-free
|
|
779
|
+
* forward + puts ({@link constEvaluable} — the composition is evaluated with the
|
|
780
|
+
* const evaluator only, never a vtab read); and every forward column ref covered
|
|
781
|
+
* by a put target. A definite per-value violation wins over another value's
|
|
782
|
+
* evaluation failure (it is a proven law break either way).
|
|
783
|
+
*/
|
|
784
|
+
function provePutGetByEnumeration(ctx, column, authored, forward) {
|
|
785
|
+
const li = ctx.logicalColIndex.get(column.toLowerCase());
|
|
786
|
+
// Trusted accessor: a module declaring `permitsGrandfatheredCheckViolators`
|
|
787
|
+
// yields no enumerable domain (a logical table carries no module, so this is
|
|
788
|
+
// a no-op gate here today — but the accessor keeps every prover read of CHECK
|
|
789
|
+
// facts behind the capability gate).
|
|
790
|
+
const domain = li !== undefined ? enumerableDomain(getTrustedCheckExtraction(ctx.table), li) : undefined;
|
|
791
|
+
if (!domain)
|
|
792
|
+
return { kind: 'indeterminate' };
|
|
793
|
+
const oi = ctx.outputIndex.get(column.toLowerCase());
|
|
794
|
+
if (forward === undefined || ctx.basisSource === undefined || oi === undefined)
|
|
795
|
+
return { kind: 'indeterminate', domain };
|
|
796
|
+
for (const refIdx of authored.newRefIndex.values()) {
|
|
797
|
+
if (refIdx !== oi)
|
|
798
|
+
return { kind: 'indeterminate', domain };
|
|
799
|
+
}
|
|
800
|
+
if (!constEvaluable(ctx.db, forward) || authored.puts.some(p => !constEvaluable(ctx.db, p.expr))) {
|
|
801
|
+
return { kind: 'indeterminate', domain };
|
|
802
|
+
}
|
|
803
|
+
const putTargets = new Set(authored.puts.map(p => p.baseColumn.toLowerCase()));
|
|
804
|
+
if (collectColumnRefNames(forward).some(n => !putTargets.has(n.toLowerCase()))) {
|
|
805
|
+
return { kind: 'indeterminate', domain }; // the forward reads a base column the inverse does not determine
|
|
806
|
+
}
|
|
807
|
+
// `forward(inverse(v))` plus the inverse's basis image, or undefined when any
|
|
808
|
+
// step is not const-evaluable.
|
|
809
|
+
const composition = (v) => {
|
|
810
|
+
const base = new Map();
|
|
811
|
+
for (const p of authored.puts) {
|
|
812
|
+
const bv = evalDeployConstant(ctx.db, substituteNewRefs(p.expr, () => ({ type: 'literal', value: v })));
|
|
813
|
+
if (bv === undefined)
|
|
814
|
+
return undefined;
|
|
815
|
+
base.set(p.baseColumn.toLowerCase(), bv);
|
|
816
|
+
}
|
|
817
|
+
const got = evalDeployConstant(ctx.db, substituteBaseRefs(forward, base));
|
|
818
|
+
return got === undefined ? undefined : { got, base };
|
|
819
|
+
};
|
|
820
|
+
let indeterminate = false;
|
|
821
|
+
const putImages = [];
|
|
822
|
+
for (const v of domain) {
|
|
823
|
+
const r = composition(v);
|
|
824
|
+
if (r === undefined) {
|
|
825
|
+
indeterminate = true;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (!sqlValueEquals(r.got, v))
|
|
829
|
+
return { kind: 'violation', value: v, got: r.got, domain };
|
|
830
|
+
putImages.push(r.base);
|
|
831
|
+
}
|
|
832
|
+
if (indeterminate)
|
|
833
|
+
return { kind: 'indeterminate', domain };
|
|
834
|
+
return { kind: 'proved', injective: proveForwardInjective(ctx, authored, forward, domain, putImages), domain };
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Forward injectivity over the basis column's own enumerable CHECK domain. With
|
|
838
|
+
* PutGet proved over the logical domain, an injective forward whose image stays
|
|
839
|
+
* *inside* that logical domain — **and** an inverse whose images stay inside the
|
|
840
|
+
* basis domain (`putImages`, recorded by the PutGet enumeration) — makes the
|
|
841
|
+
* pair bijective between the two enumerated domains, so GetPut holds
|
|
842
|
+
* (`put(get(b)) = b` for every basis value) and the lossy advisory is
|
|
843
|
+
* suppressed. The put-image membership check is load-bearing: PutGet forces the
|
|
844
|
+
* inverse injective into the basis domain (|logical| ≤ |basis|), forward
|
|
845
|
+
* injectivity forces |basis| ≤ |logical|, so both are bijections and the
|
|
846
|
+
* inverse is exactly forward⁻¹ — without it, an inverse image *outside* the
|
|
847
|
+
* basis domain (which PutGet can still pass through the forward's catch-all
|
|
848
|
+
* arm) breaks the counting and GetPut fails for a real stored value.
|
|
849
|
+
* Conservative: requires a single put target backed by a NOT NULL basis column
|
|
850
|
+
* (a nullable basis admits a value outside the enumeration) and a
|
|
851
|
+
* const-evaluable, never-NULL forward image at every basis value. The forward's
|
|
852
|
+
* refs ⊆ put targets was already established by the caller, so the single
|
|
853
|
+
* binding covers every ref.
|
|
854
|
+
*/
|
|
855
|
+
function proveForwardInjective(ctx, authored, forward, logicalDomain, putImages) {
|
|
856
|
+
const basis = ctx.basisSource;
|
|
857
|
+
if (!basis || authored.puts.length !== 1)
|
|
858
|
+
return false;
|
|
859
|
+
const put = authored.puts[0];
|
|
860
|
+
const bi = basis.columnIndexMap.get(put.baseColumn.toLowerCase());
|
|
861
|
+
if (bi === undefined || !basis.columns[bi]?.notNull)
|
|
862
|
+
return false;
|
|
863
|
+
// Trusted accessor: when the basis module declares
|
|
864
|
+
// `permitsGrandfatheredCheckViolators`, stored basis rows may violate the
|
|
865
|
+
// declared CHECK, so its enum domain cannot witness the bijection — the
|
|
866
|
+
// extraction is empty and the injectivity proof conservatively fails
|
|
867
|
+
// (matching the gate on `TableReferenceNode.computePhysical`).
|
|
868
|
+
const basisDomain = enumerableDomain(getTrustedCheckExtraction(basis), bi);
|
|
869
|
+
if (!basisDomain)
|
|
870
|
+
return false;
|
|
871
|
+
for (const image of putImages) {
|
|
872
|
+
const bv = image.get(put.baseColumn.toLowerCase());
|
|
873
|
+
if (bv === undefined || !basisDomain.some(b => sqlValueEquals(b, bv))) {
|
|
874
|
+
return false; // the inverse writes outside the basis domain — not a bijection witness
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const seen = [];
|
|
878
|
+
for (const b of basisDomain) {
|
|
879
|
+
const got = evalDeployConstant(ctx.db, substituteBaseRefs(forward, new Map([[put.baseColumn.toLowerCase(), b]])));
|
|
880
|
+
if (got === undefined || got === null)
|
|
881
|
+
return false;
|
|
882
|
+
if (!logicalDomain.some(v => sqlValueEquals(v, got)))
|
|
883
|
+
return false; // image escapes the PutGet-proved domain
|
|
884
|
+
if (seen.some(s => sqlValueEquals(s, got)))
|
|
885
|
+
return false; // two basis values collapse — not injective
|
|
886
|
+
seen.push(got);
|
|
887
|
+
}
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* The enumerable CHECK domain of one column: the literal `in (...)` value list,
|
|
892
|
+
* intersected across multiple enum CHECKs and filtered through any recognized
|
|
893
|
+
* range CHECK on the same column — the enumeration must never include a value
|
|
894
|
+
* the declared CHECK surface already excludes, since a false
|
|
895
|
+
* `lens.putget-violation` would block a sound deploy. NULLs are dropped (an
|
|
896
|
+
* `in` list never admits one). Undefined when no enum constraint exists, the
|
|
897
|
+
* filtered domain is empty, or it exceeds {@link ENUM_DOMAIN_CAP}.
|
|
898
|
+
*/
|
|
899
|
+
function enumerableDomain(extraction, columnIndex) {
|
|
900
|
+
let values;
|
|
901
|
+
for (const dc of extraction.domainConstraints) {
|
|
902
|
+
if (dc.column !== columnIndex || dc.kind !== 'enum')
|
|
903
|
+
continue;
|
|
904
|
+
const list = dc.values.filter(v => v !== null);
|
|
905
|
+
values = values === undefined ? list : values.filter(v => list.some(w => sqlValueEquals(v, w)));
|
|
906
|
+
}
|
|
907
|
+
if (values === undefined)
|
|
908
|
+
return undefined;
|
|
909
|
+
for (const dc of extraction.domainConstraints) {
|
|
910
|
+
if (dc.column !== columnIndex || dc.kind !== 'range')
|
|
911
|
+
continue;
|
|
912
|
+
values = values.filter(v => withinRange(v, dc));
|
|
913
|
+
}
|
|
914
|
+
if (values.length === 0 || values.length > ENUM_DOMAIN_CAP)
|
|
915
|
+
return undefined;
|
|
916
|
+
return values;
|
|
917
|
+
}
|
|
918
|
+
/** Whether `v` satisfies a recognized range domain constraint. */
|
|
919
|
+
function withinRange(v, r) {
|
|
920
|
+
if (r.min !== undefined) {
|
|
921
|
+
const c = compareSqlValues(v, r.min);
|
|
922
|
+
if (r.minInclusive ? c < 0 : c <= 0)
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
if (r.max !== undefined) {
|
|
926
|
+
const c = compareSqlValues(v, r.max);
|
|
927
|
+
if (r.maxInclusive ? c > 0 : c >= 0)
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Whether an expression is sound to fold at deploy with the const evaluator:
|
|
934
|
+
* deterministic functions only and no subquery —
|
|
935
|
+
* {@link containsNonDeterministicCall} flags both (a vtab read can never be a
|
|
936
|
+
* deploy-time constant). An unregistered function is treated deterministic; its
|
|
937
|
+
* evaluation failing falls through {@link evalDeployConstant}'s degrade anyway.
|
|
938
|
+
*/
|
|
939
|
+
function constEvaluable(db, expr) {
|
|
940
|
+
const isDeterministic = (name, argc) => {
|
|
941
|
+
const fn = db.schemaManager.findFunction(name, argc) ?? db.schemaManager.findFunction(name, -1);
|
|
942
|
+
return fn ? (fn.flags & FunctionFlags.DETERMINISTIC) !== 0 : true;
|
|
943
|
+
};
|
|
944
|
+
return !containsNonDeterministicCall(expr, isDeterministic);
|
|
945
|
+
}
|
|
946
|
+
/** Replace each (qualifier-agnostic) base-column reference with its literal image. */
|
|
947
|
+
function substituteBaseRefs(expr, baseImage) {
|
|
948
|
+
return transformExpr(expr, col => {
|
|
949
|
+
const v = baseImage.get(col.name.toLowerCase());
|
|
950
|
+
return v === undefined ? undefined : { type: 'literal', value: v };
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Evaluate a column-free scalar expression at deploy via the engine's own const
|
|
955
|
+
* evaluator (`createRuntimeExpressionEvaluator`) — never a vtab read; the
|
|
956
|
+
* expression is planned as a bare one-column SELECT so it builds in an empty
|
|
957
|
+
* scope. Returns undefined (the degrade-to-safe signal) when the expression
|
|
958
|
+
* fails to build, evaluates asynchronously (not a deploy-time constant), or
|
|
959
|
+
* throws.
|
|
960
|
+
*/
|
|
961
|
+
function evalDeployConstant(db, expr) {
|
|
962
|
+
try {
|
|
963
|
+
const stmt = { type: 'select', columns: [{ type: 'column', expr }] };
|
|
964
|
+
const { plan } = db._buildPlan([stmt]);
|
|
965
|
+
const root = plan.getRelations()[0];
|
|
966
|
+
const node = root === undefined ? undefined : findProjectNode(root)?.getProjections()[0]?.node;
|
|
967
|
+
if (!node)
|
|
968
|
+
return undefined;
|
|
969
|
+
const value = createRuntimeExpressionEvaluator(db)(node);
|
|
970
|
+
if (value instanceof Promise) {
|
|
971
|
+
void value.catch(() => undefined); // async ⇒ not a deploy-time constant; never crash the deploy on it
|
|
972
|
+
return undefined;
|
|
973
|
+
}
|
|
974
|
+
return value;
|
|
975
|
+
}
|
|
976
|
+
catch (e) {
|
|
977
|
+
log('lens-prover: authored-inverse composition failed to const-evaluate, degrading to safe: %O', e);
|
|
978
|
+
return undefined;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Maps each authored-inverse logical column (lowercased) to its **forward** `get`
|
|
983
|
+
* expression (basis terms), when that forward is row-local-enforceable — a
|
|
984
|
+
* subquery-free scalar over basis column refs of a **single-source** body. This
|
|
985
|
+
* is the agreement predicate between the prover's CHECK realizability classifier
|
|
986
|
+
* ({@link classifyCheckConstraint}: a CHECK referencing an authored column is
|
|
987
|
+
* row-local exactly when this map has the column) and the write-time
|
|
988
|
+
* logical→basis rewrite (`planner/mutation/lens-enforcement.ts`), which
|
|
989
|
+
* substitutes the forward — `NEW.`-qualified — for the column ref so the CHECK
|
|
990
|
+
* evaluates over the basis write row's logical image. The two must accept the
|
|
991
|
+
* same set, or a deploy-admitted CHECK would crash at write plan time.
|
|
992
|
+
*
|
|
993
|
+
* The single-source condition is load-bearing: on a multi-source body the
|
|
994
|
+
* forward may read a column of a *different member* than the one the put
|
|
995
|
+
* writes, and the substituted `NEW.<col>` on that member's write row does not
|
|
996
|
+
* carry the value — the CHECK would pass vacuously (3VL unknown) instead of
|
|
997
|
+
* enforcing. The clause's put targets are bare column names, so the slot AST
|
|
998
|
+
* alone cannot prove every forward ref lives on the put member; single-source
|
|
999
|
+
* is the decidable conservative gate (the same posture as the prover's PutGet
|
|
1000
|
+
* enumeration). A multi-source CHECK over an authored column therefore reds
|
|
1001
|
+
* `lens.unrealizable-constraint` at deploy rather than deploying un-enforced.
|
|
1002
|
+
*/
|
|
1003
|
+
export function authoredForwardMap(slot) {
|
|
1004
|
+
const map = new Map();
|
|
1005
|
+
const from = slot.compiledBody.from;
|
|
1006
|
+
if (!from || from.length !== 1 || from[0].type !== 'table')
|
|
1007
|
+
return map;
|
|
1008
|
+
slot.columnProvenance.forEach((p, i) => {
|
|
1009
|
+
const rc = slot.compiledBody.columns[i];
|
|
1010
|
+
if (rc && rc.type === 'column' && rc.inverse && rc.inverse.length > 0 && !containsRelationalOperand(rc.expr)) {
|
|
1011
|
+
map.set(p.logicalColumn.toLowerCase(), rc.expr);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
return map;
|
|
1015
|
+
}
|
|
1016
|
+
/** Reflective walk: does the expression contain a relational operand (subquery / exists / in-subquery)? */
|
|
1017
|
+
function containsRelationalOperand(expr) {
|
|
1018
|
+
const stack = [expr];
|
|
1019
|
+
while (stack.length > 0) {
|
|
1020
|
+
const node = stack.pop();
|
|
1021
|
+
if (node.type === 'subquery' || node.type === 'exists')
|
|
1022
|
+
return true;
|
|
1023
|
+
if (node.type === 'in' && node.subquery)
|
|
1024
|
+
return true;
|
|
1025
|
+
for (const key of Object.keys(node)) {
|
|
1026
|
+
const value = node[key];
|
|
1027
|
+
if (!value)
|
|
1028
|
+
continue;
|
|
1029
|
+
if (Array.isArray(value)) {
|
|
1030
|
+
for (const item of value) {
|
|
1031
|
+
if (item && typeof item === 'object' && 'type' in item)
|
|
1032
|
+
stack.push(item);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
else if (typeof value === 'object' && 'type' in value) {
|
|
1036
|
+
stack.push(value);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
/** SQL value equality for the enumeration (NULL equals only NULL — identity, not three-valued `=`). */
|
|
1043
|
+
function sqlValueEquals(a, b) {
|
|
1044
|
+
if (a === null || b === null)
|
|
1045
|
+
return a === null && b === null;
|
|
1046
|
+
return compareSqlValues(a, b) === 0;
|
|
1047
|
+
}
|
|
1048
|
+
/** Render a domain value for a sited message / fingerprint (text quoted, so '1' ≠ 1). */
|
|
1049
|
+
function renderSqlValue(v) {
|
|
1050
|
+
if (v === null)
|
|
1051
|
+
return 'NULL';
|
|
1052
|
+
return typeof v === 'string' ? `'${v}'` : String(v);
|
|
1053
|
+
}
|
|
1054
|
+
// ---------------------------------------------------------------------------
|
|
1055
|
+
// Constraint realizability + obligation classification
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
/**
|
|
1058
|
+
* Classifies every attached logical constraint into a {@link ConstraintObligation}
|
|
1059
|
+
* and, in the process, performs the *constraint realizability* error check
|
|
1060
|
+
* (`lens.unrealizable-constraint`): a constraint referencing a column with no
|
|
1061
|
+
* write path (computed lineage) is neither provable nor attachable.
|
|
1062
|
+
* Set-level constraints with no covering structure emit `lens.no-backing-index`.
|
|
1063
|
+
*/
|
|
1064
|
+
function classifyObligations(ctx, readOnly, bijectiveAuthored, errors, warnings) {
|
|
1065
|
+
const obligations = [];
|
|
1066
|
+
for (const c of ctx.slot.attachedConstraints) {
|
|
1067
|
+
obligations.push(classifyConstraint(ctx, c, readOnly, bijectiveAuthored, errors, warnings));
|
|
1068
|
+
}
|
|
1069
|
+
return obligations;
|
|
1070
|
+
}
|
|
1071
|
+
function classifyConstraint(ctx, constraint, readOnly, bijectiveAuthored, errors, warnings) {
|
|
1072
|
+
switch (constraint.kind) {
|
|
1073
|
+
case 'primaryKey':
|
|
1074
|
+
return classifyKeyConstraint(ctx, constraint, constraint.columns.map(c => c.index), 'primary key', true, readOnly, bijectiveAuthored, errors, warnings);
|
|
1075
|
+
case 'unique':
|
|
1076
|
+
return classifyKeyConstraint(ctx, constraint, constraint.constraint.columns, constraintLabel(constraint), false, readOnly, bijectiveAuthored, errors, warnings);
|
|
1077
|
+
case 'check':
|
|
1078
|
+
return classifyCheckConstraint(ctx, constraint, errors);
|
|
1079
|
+
case 'foreignKey':
|
|
1080
|
+
return { constraint, kind: 'enforced-fk' };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/** A short human label for a constraint, for sited messages. */
|
|
1084
|
+
function constraintLabel(constraint) {
|
|
1085
|
+
switch (constraint.kind) {
|
|
1086
|
+
case 'primaryKey': return 'primary key';
|
|
1087
|
+
case 'check': return constraint.constraint.name ? `check '${constraint.constraint.name}'` : 'check';
|
|
1088
|
+
case 'unique': return constraint.constraint.name ? `unique '${constraint.constraint.name}'` : 'unique';
|
|
1089
|
+
case 'foreignKey': return constraint.constraint.name ? `foreign key '${constraint.constraint.name}'` : 'foreign key';
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* The effective constraint-level default conflict action a duplicate key would
|
|
1094
|
+
* resolve to absent a statement-level OR clause. A key declaring REPLACE / IGNORE
|
|
1095
|
+
* here is rejected at deploy when the realizing path cannot honor it: the
|
|
1096
|
+
* commit-time set-level scan can only ABORT (see {@link classifyKeyConstraint}),
|
|
1097
|
+
* and the row-time path honors the *basis* UC's action, not the logical key's
|
|
1098
|
+
* (see {@link rejectRowTimeConflictAction}) — both raise `lens.unenforceable-conflict-action`.
|
|
1099
|
+
*
|
|
1100
|
+
* - `unique` → the constraint's own `defaultConflict`.
|
|
1101
|
+
* - `primaryKey` → {@link resolvePkDefaultConflict}: table-level
|
|
1102
|
+
* `PRIMARY KEY (...) ON CONFLICT <action>` (`TableSchema.primaryKeyDefaultConflict`),
|
|
1103
|
+
* else the column-level `ColumnSchema.defaultConflict` on **any** PK column —
|
|
1104
|
+
* the precedence the runtime resolvers actually use, so the deploy-time check
|
|
1105
|
+
* agrees with what a duplicate would resolve to. A non-first PK column's
|
|
1106
|
+
* `not null on conflict replace` counts (it sets `defaultConflict` too); the
|
|
1107
|
+
* PK's action is NOT on the `LogicalConstraint` node, so it must come from `ctx.table`.
|
|
1108
|
+
*
|
|
1109
|
+
* Returns undefined when no action is declared (⇒ ABORT, which the scan honors).
|
|
1110
|
+
*/
|
|
1111
|
+
function effectiveKeyDefaultConflict(ctx, constraint) {
|
|
1112
|
+
switch (constraint.kind) {
|
|
1113
|
+
case 'unique':
|
|
1114
|
+
return constraint.constraint.defaultConflict;
|
|
1115
|
+
case 'primaryKey':
|
|
1116
|
+
return resolvePkDefaultConflict(ctx.table);
|
|
1117
|
+
default:
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/** Render a conflict action for a sited message; an absent action resolves to ABORT. */
|
|
1122
|
+
function conflictActionName(action) {
|
|
1123
|
+
return ConflictResolution[action ?? ConflictResolution.ABORT].toLowerCase();
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Classifies a key constraint (primary key / unique). Empty key ⇒ vacuous
|
|
1127
|
+
* (singleton).
|
|
1128
|
+
*
|
|
1129
|
+
* A column with no write path (computed lineage) is handled by class:
|
|
1130
|
+
* - a **unique** over such a column is `lens.unrealizable-constraint` (you
|
|
1131
|
+
* declared uniqueness on a value with no write path — it can be neither proved
|
|
1132
|
+
* nor enforced), *unless* the column is authored-**bijective**
|
|
1133
|
+
* (`bijectiveAuthored`): the proven bijection transports uniqueness to/from its
|
|
1134
|
+
* put target, so it has a sound write path and is admitted to the key (proved
|
|
1135
|
+
* via transport when the put target is a basis key, else the commit-time scan
|
|
1136
|
+
* over the forward image enforces it) — the same realizability a bijective PK
|
|
1137
|
+
* column enjoys;
|
|
1138
|
+
* - a **primary key** over such a column makes the whole table *read-only*
|
|
1139
|
+
* (owned by {@link checkKeyReconstructibility}, surfaced as the
|
|
1140
|
+
* `lens.pk-not-reconstructible` warning) — NOT a blocking error, because the
|
|
1141
|
+
* table still deploys for reads.
|
|
1142
|
+
*
|
|
1143
|
+
* Otherwise: proved by the body's effective key (`proveEffectiveKeyUnique`) **or**
|
|
1144
|
+
* by **bijection transport** ({@link proveKeyByBijectionTransport} — every key column
|
|
1145
|
+
* bare-reconstructible or authored-bijective, mapping to a declared basis key over
|
|
1146
|
+
* the put targets) ⇒ `proved`; else `enforced-set-level`, row-time when a basis
|
|
1147
|
+
* covering structure answers it, commit-time (+ `lens.no-backing-index` warning) when
|
|
1148
|
+
* none does. The warning is suppressed for a read-only table — its set-level
|
|
1149
|
+
* enforcement is moot.
|
|
1150
|
+
*
|
|
1151
|
+
* Whenever the key classifies `proved`, a **governing** basis key (if any) resolves a
|
|
1152
|
+
* write-through duplicate, not the logical key — so a logical `on conflict
|
|
1153
|
+
* replace`/`ignore` the governing key does not itself carry would be silently dropped.
|
|
1154
|
+
* That check ({@link rejectBasisGovernedConflictActionForProvedKey}) is **decoupled**
|
|
1155
|
+
* from the transport proof's exact-match/single-source gate: it identifies the governing
|
|
1156
|
+
* basis keys by a SUBSET search over the mapped basis columns
|
|
1157
|
+
* ({@link findGoverningBasisKeys}) — so a logical key that is a strict superkey of a
|
|
1158
|
+
* smaller basis key (body-proved, transport-undefined) is covered, not just an exact
|
|
1159
|
+
* transport match — and rejects conservatively when governance cannot be pinned (a
|
|
1160
|
+
* multi-source body, where the 1:1 column mapping and the superkey soundness argument do
|
|
1161
|
+
* not transfer). A genuinely basis-keyless body proof (a GROUP BY aggregate over plain
|
|
1162
|
+
* columns with no basis UC over the key, an FD-closure key, etc.) subsumes no declared
|
|
1163
|
+
* basis key, so its `on conflict` is vacuous and deploys clean. Mirrors the row-time and
|
|
1164
|
+
* commit-time arms.
|
|
1165
|
+
*
|
|
1166
|
+
* A strict superkey of a NOT-NULL basis key is *over*-enforced — distinct from the
|
|
1167
|
+
* conflict-action mismatch above. The logical `unique(a, b)` permits two rows differing
|
|
1168
|
+
* only in `b`, but the basis NOT-NULL `unique(a)` that body-proved it rejects them: the
|
|
1169
|
+
* basis enforces uniqueness on a *subset* of the logical key's columns, so it is stricter
|
|
1170
|
+
* than the logical declaration advertises. That is sound (every logical invariant still
|
|
1171
|
+
* holds) but surprising, so it surfaces as the acknowledgeable warning
|
|
1172
|
+
* `lens.over-restrictive-basis-key` ({@link warnOverRestrictiveBasisKey}), emitted from this
|
|
1173
|
+
* `proved` branch beside the conflict-action check (the only branch a `proved` logical key
|
|
1174
|
+
* can sit over a strict-subset NOT-NULL basis key — a nullable basis sub-key yields only a
|
|
1175
|
+
* guarded FD and never classifies `proved`). It is a warning, not an error: the schema is
|
|
1176
|
+
* sound, just enforced more tightly than declared.
|
|
1177
|
+
*/
|
|
1178
|
+
function classifyKeyConstraint(ctx, constraint, logicalColumns, label, isPrimaryKey, readOnly, bijectiveAuthored, errors, warnings) {
|
|
1179
|
+
if (logicalColumns.length === 0) {
|
|
1180
|
+
return { constraint, kind: 'vacuous' };
|
|
1181
|
+
}
|
|
1182
|
+
const columnNames = logicalColumns.map(i => ctx.table.columns[i]?.name ?? `#${i}`);
|
|
1183
|
+
const outCols = [];
|
|
1184
|
+
for (const li of logicalColumns) {
|
|
1185
|
+
const name = ctx.table.columns[li]?.name;
|
|
1186
|
+
const oi = name !== undefined ? ctx.outputIndex.get(name.toLowerCase()) : undefined;
|
|
1187
|
+
const reachable = oi !== undefined && isReconstructibleColumn(ctx, name);
|
|
1188
|
+
// An authored-bijective column is not bare-reconstructible, but the proven
|
|
1189
|
+
// bijection transports uniqueness to/from its put target — so it has a sound
|
|
1190
|
+
// write path and is admitted to the key just like a bijective PK column (the
|
|
1191
|
+
// bijection-transport proof below maps it to its basis column, and absent a
|
|
1192
|
+
// basis key the commit-time scan over the forward image enforces it). A
|
|
1193
|
+
// non-bijective authored column (or a computed/opaque column) still has no
|
|
1194
|
+
// proven write path: uniqueness over it is neither provable nor enforceable.
|
|
1195
|
+
const authoredBijective = name !== undefined && bijectiveAuthored.has(name.toLowerCase());
|
|
1196
|
+
if (!reachable && !isPrimaryKey && !authoredBijective) {
|
|
1197
|
+
errors.push({
|
|
1198
|
+
code: 'lens.unrealizable-constraint',
|
|
1199
|
+
severity: 'error',
|
|
1200
|
+
site: { table: ctx.table.name, constraint: label, column: name },
|
|
1201
|
+
message: `lens: ${label} on '${ctx.table.name}' references column '${name ?? `#${li}`}', which has no write path at the lens boundary (computed lineage); the constraint can be neither proved nor enforced`,
|
|
1202
|
+
});
|
|
1203
|
+
return { constraint, kind: 'enforced-set-level', mode: 'commit-time' };
|
|
1204
|
+
}
|
|
1205
|
+
// A PK over an unreachable column (read-only, warned elsewhere) or an
|
|
1206
|
+
// authored-bijective key column: fall through to classify the obligation
|
|
1207
|
+
// without a blocking error.
|
|
1208
|
+
if (oi !== undefined)
|
|
1209
|
+
outCols.push(oi);
|
|
1210
|
+
}
|
|
1211
|
+
// A bijection-transport proof classifies the key `proved` via an authored bijection
|
|
1212
|
+
// onto an *exact* declared basis key (a strict superset does not prove the smaller
|
|
1213
|
+
// key's uniqueness, so exact-match is correct for the proof). It is NOT the gate for
|
|
1214
|
+
// the conflict-action check below — governance is identified independently by subset
|
|
1215
|
+
// (see {@link rejectBasisGovernedConflictActionForProvedKey}).
|
|
1216
|
+
const transport = proveKeyByBijectionTransport(ctx, logicalColumns, bijectiveAuthored);
|
|
1217
|
+
// Body proves it? (e.g. unique(x,y) over `group by x,y`, or a faithful projection
|
|
1218
|
+
// of a basis key.) Only when every column resolved to the output.
|
|
1219
|
+
const bodyProvesKey = ctx.root != null
|
|
1220
|
+
&& outCols.length === logicalColumns.length
|
|
1221
|
+
&& proveEffectiveKeyUnique(ctx.root, outCols).proved;
|
|
1222
|
+
// Proved by BODY or by BIJECTION TRANSPORT (every key column bare-reconstructible
|
|
1223
|
+
// or authored-bijective, mapping to a declared basis key). Both are `proved` —
|
|
1224
|
+
// zero runtime enforcement, a governing basis key forbids the colliding tuple.
|
|
1225
|
+
// Transport subsumes both the basis-PK and basis-UNIQUE cases, so an authored key
|
|
1226
|
+
// never needs the row-time/covering path.
|
|
1227
|
+
if (bodyProvesKey || transport) {
|
|
1228
|
+
// A governing basis key (a declared basis key whose columns ⊆ the logical key's
|
|
1229
|
+
// mapped basis columns) resolves a write-through duplicate, not the logical key —
|
|
1230
|
+
// so a logical `on conflict replace`/`ignore` it does not itself carry is silently
|
|
1231
|
+
// dropped. Reject the mismatch regardless of which arm proved the key (a populated
|
|
1232
|
+
// `errors` blocks the deploy regardless of the obligation kind, exactly as the
|
|
1233
|
+
// row-time arm does). A genuinely basis-keyless proof governs no basis key, so its
|
|
1234
|
+
// `on conflict` is vacuous and left untouched.
|
|
1235
|
+
rejectBasisGovernedConflictActionForProvedKey(ctx, constraint, logicalColumns, bijectiveAuthored, label, columnNames, readOnly, errors);
|
|
1236
|
+
// Diagnostic-only: a logical key proved over a strict-subset NOT-NULL basis key is
|
|
1237
|
+
// sound but over-enforced (the basis rejects writes the logical schema permits).
|
|
1238
|
+
// Surface the surprise as a warning; this does not alter the proved classification.
|
|
1239
|
+
warnOverRestrictiveBasisKey(ctx, logicalColumns, bijectiveAuthored, label, columnNames, readOnly, warnings);
|
|
1240
|
+
return { constraint, kind: 'proved' };
|
|
1241
|
+
}
|
|
1242
|
+
// Not proved → enforced set-level. Row-time iff a basis row-time covering
|
|
1243
|
+
// structure answers it (a non-stale covering MV); commit-time otherwise.
|
|
1244
|
+
const covering = findBasisCovering(ctx, logicalColumns);
|
|
1245
|
+
if (covering) {
|
|
1246
|
+
rejectRowTimeConflictAction(ctx, constraint, covering, label, columnNames, readOnly, errors);
|
|
1247
|
+
return { constraint, kind: 'enforced-set-level', mode: 'row-time', structure: covering.ref };
|
|
1248
|
+
}
|
|
1249
|
+
if (!readOnly) {
|
|
1250
|
+
warnings.push({
|
|
1251
|
+
code: 'lens.no-backing-index',
|
|
1252
|
+
severity: 'warning',
|
|
1253
|
+
site: { table: ctx.table.name, constraint: label },
|
|
1254
|
+
message: `lens: ${label} on '${ctx.table.name}' (${columnNames.map(c => `'${c}'`).join(', ')}) has no basis covering structure — it enforces via an O(n) commit-time scan. Add an explicit basis covering materialized view (order by the constraint columns) to upgrade to row-time enforcement; row-time conflict resolution (insert or replace / or ignore) requires that structure and is otherwise rejected.`,
|
|
1255
|
+
fingerprintInputs: buildFingerprint(ctx, columnNames, false),
|
|
1256
|
+
});
|
|
1257
|
+
// A commit-time scan can only ABORT. A constraint-level `on conflict
|
|
1258
|
+
// replace`/`ignore` (the PK's via `ctx.table`, a UNIQUE's via its own
|
|
1259
|
+
// `defaultConflict`) is an action the scan can never honor — an unsound
|
|
1260
|
+
// schema. Block it at deploy here rather than silently over-ABORTing per
|
|
1261
|
+
// write: the statement-level gate (`rejectLensSetLevelConflictResolution`)
|
|
1262
|
+
// only inspects `req.stmt.onConflict`, so the constraint-level channel never
|
|
1263
|
+
// reaches it. ABORT / FAIL / ROLLBACK (and no declared action) are fine.
|
|
1264
|
+
const effectiveConflict = effectiveKeyDefaultConflict(ctx, constraint);
|
|
1265
|
+
if (effectiveConflict === ConflictResolution.REPLACE || effectiveConflict === ConflictResolution.IGNORE) {
|
|
1266
|
+
errors.push({
|
|
1267
|
+
code: 'lens.unenforceable-conflict-action',
|
|
1268
|
+
severity: 'error',
|
|
1269
|
+
site: { table: ctx.table.name, constraint: label },
|
|
1270
|
+
message: `lens: ${label} on '${ctx.table.name}' (${columnNames.map(c => `'${c}'`).join(', ')}) declares 'on conflict replace/ignore' but has no basis covering structure, so the action cannot be honored (a commit-time scan can only ABORT). Add a basis covering materialized view (order by the key columns) to upgrade to row-time enforcement, or drop the conflict action.`,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
// Defensive (close-before-reachable): the commit-time count synthesis
|
|
1274
|
+
// (`synthesizeUniqueCountExpr`) counts ALL logical rows matching the key — it
|
|
1275
|
+
// does not scope by a partial-UNIQUE predicate, so a partial logical UNIQUE
|
|
1276
|
+
// would over-count and falsely ABORT an out-of-scope duplicate. A logical
|
|
1277
|
+
// declaration never sets `predicate` today (only `CREATE UNIQUE INDEX … WHERE`
|
|
1278
|
+
// does, a path the declaration surface never takes), so this guards an
|
|
1279
|
+
// invariant rather than a reachable case — reject loudly if it ever opens.
|
|
1280
|
+
if (constraint.kind === 'unique' && constraint.constraint.predicate !== undefined) {
|
|
1281
|
+
errors.push({
|
|
1282
|
+
code: 'lens.unrealizable-constraint',
|
|
1283
|
+
severity: 'error',
|
|
1284
|
+
site: { table: ctx.table.name, constraint: label },
|
|
1285
|
+
message: `lens: ${label} on '${ctx.table.name}' (${columnNames.map(c => `'${c}'`).join(', ')}) is a partial UNIQUE (declares a predicate), which is unsupported for commit-time set-level enforcement (the O(n) count scan cannot scope by the partial predicate). Add a basis covering materialized view to upgrade to row-time enforcement, or remove the partial predicate.`,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return { constraint, kind: 'enforced-set-level', mode: 'commit-time' };
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Shared core for the two basis-governed set-level conflict-action rejecters
|
|
1293
|
+
* (row-time covering UC, proved-transport basis key). A basis-governed key is
|
|
1294
|
+
* enforced by the **basis** key, whose conflict action resolves a duplicate as
|
|
1295
|
+
* `statement-OR ?? basis-key.defaultConflict ?? ABORT` — the *logical* key's own
|
|
1296
|
+
* `defaultConflict` is never consulted. So a logical `on conflict replace`/`ignore`
|
|
1297
|
+
* the basis key does NOT itself carry is silently dropped to ABORT at write time
|
|
1298
|
+
* (with no statement-level OR), violating the declared action. Reject it at deploy.
|
|
1299
|
+
*
|
|
1300
|
+
* Fires only when the logical effective action is REPLACE / IGNORE *and* differs
|
|
1301
|
+
* from the basis key's own action: when they match, the basis key already resolves
|
|
1302
|
+
* the declared action for free (the documented remediation), so there is nothing to
|
|
1303
|
+
* reject. ABORT / FAIL / ROLLBACK (and no declared action) never reject. Gated on
|
|
1304
|
+
* `!readOnly`: a read-only table never writes, so the action is moot. The two
|
|
1305
|
+
* callers differ only in `basis` — where the governing action and its label come
|
|
1306
|
+
* from — so both funnel into this one diagnostic.
|
|
1307
|
+
*/
|
|
1308
|
+
function rejectBasisGovernedConflictAction(ctx, constraint, label, columnNames, readOnly, errors, basis) {
|
|
1309
|
+
if (readOnly)
|
|
1310
|
+
return;
|
|
1311
|
+
const eff = effectiveKeyDefaultConflict(ctx, constraint);
|
|
1312
|
+
if (eff !== ConflictResolution.REPLACE && eff !== ConflictResolution.IGNORE)
|
|
1313
|
+
return;
|
|
1314
|
+
if (eff === basis.conflict)
|
|
1315
|
+
return; // basis key honors it for free — the documented remediation
|
|
1316
|
+
errors.push({
|
|
1317
|
+
code: 'lens.unenforceable-conflict-action',
|
|
1318
|
+
severity: 'error',
|
|
1319
|
+
site: { table: ctx.table.name, constraint: label },
|
|
1320
|
+
message: `lens: ${label} on '${ctx.table.name}' (${columnNames.map(c => `'${c}'`).join(', ')}) declares 'on conflict ${conflictActionName(eff)}', but its backing ${basis.label} resolves a duplicate to '${conflictActionName(basis.conflict)}' — the write path honors the basis key's action, not the logical key's, so the declared action would be silently dropped. Declare the matching 'on conflict ${conflictActionName(eff)}' on the basis key, or drop the logical conflict action.`,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Row-time sibling of the commit-time `lens.unenforceable-conflict-action` block.
|
|
1325
|
+
* A row-time key is enforced by re-planning the lens write against the basis UC,
|
|
1326
|
+
* whose conflict action resolves as `statement-OR ?? basis-uc.defaultConflict ??
|
|
1327
|
+
* ABORT` (the memory / isolation / store resolvers all agree) — the *logical*
|
|
1328
|
+
* key's own `defaultConflict` is never consulted in that re-plan. So a logical
|
|
1329
|
+
* `on conflict replace`/`ignore` the backing basis UC does NOT itself carry is
|
|
1330
|
+
* silently dropped to ABORT at write time (with no statement-level OR), violating
|
|
1331
|
+
* the declared action. Delegates to {@link rejectBasisGovernedConflictAction} with
|
|
1332
|
+
* the covering structure's basis UC as the governing key.
|
|
1333
|
+
*/
|
|
1334
|
+
function rejectRowTimeConflictAction(ctx, constraint, covering, label, columnNames, readOnly, errors) {
|
|
1335
|
+
rejectBasisGovernedConflictAction(ctx, constraint, label, columnNames, readOnly, errors, {
|
|
1336
|
+
conflict: covering.uc.defaultConflict,
|
|
1337
|
+
label: `covering structure '${covering.ref.name}'`,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* The `proved`-key sibling of the row-time/commit-time `lens.unenforceable-conflict-action`
|
|
1342
|
+
* blocks, **decoupled** from the bijection-transport exact-match/single-source gate. A
|
|
1343
|
+
* `proved` key (by the body or by transport) is enforced for free by whatever declared
|
|
1344
|
+
* basis key stands behind the proof — never the logical key — so a logical `on conflict
|
|
1345
|
+
* replace`/`ignore` that governing key does not itself carry is silently dropped.
|
|
1346
|
+
*
|
|
1347
|
+
* Single-source: the governing basis keys are every declared basis key whose column set
|
|
1348
|
+
* is a **subset** of the logical key's mapped basis columns ({@link findGoverningBasisKeys}).
|
|
1349
|
+
* Soundness: the logical key ⊇ any such basis key K (as column sets, after the 1:1 mapping),
|
|
1350
|
+
* so two rows equal on the full logical key are equal on K — K fires on *every* logical-key
|
|
1351
|
+
* write-through duplicate. So:
|
|
1352
|
+
* - **no governing key** ⇒ genuinely basis-keyless (a GROUP BY aggregate, an FD-closure
|
|
1353
|
+
* key, …) ⇒ vacuous `on conflict`, deploy clean;
|
|
1354
|
+
* - **any governing key carries a different action** ⇒ that key fires first and drops the
|
|
1355
|
+
* declared action ⇒ reject. When several subset keys disagree, the basis enforcement
|
|
1356
|
+
* order that decides which fires first is not soundly pinnable at deploy, so reject the
|
|
1357
|
+
* moment the *first* mismatch surfaces (the ticket blesses this conservatism);
|
|
1358
|
+
* - **all governing keys carry the matching action** ⇒ honored for free ⇒ deploy clean.
|
|
1359
|
+
* The `notNull` gate that {@link proveKeyByBijectionTransport} carries is deliberately NOT
|
|
1360
|
+
* applied to the mapping here: that gate is a `proved`-classification concern (a nullable
|
|
1361
|
+
* basis key is NULL-skipping, so it cannot back an *unconditional* proved FD); governance
|
|
1362
|
+
* asks only which basis key fires on a *non-null* write-through duplicate, which a nullable
|
|
1363
|
+
* subset basis key still governs.
|
|
1364
|
+
*
|
|
1365
|
+
* Multi-source (the mapping is undefined — no single `basisSource`): the 1:1 logical→basis
|
|
1366
|
+
* column mapping the subset search needs does not exist, and the superkey soundness argument
|
|
1367
|
+
* does not transfer across a decomposition (the logical columns come from different basis
|
|
1368
|
+
* rows). Governance cannot be pinned soundly, so reject conservatively. A genuinely
|
|
1369
|
+
* basis-keyless multi-source REPLACE shape is over-rejected by this; it is niche (and
|
|
1370
|
+
* conflict resolution over a decomposition write is itself not clearly supported), so the
|
|
1371
|
+
* over-rejection is acceptable — the escape hatch is to drop the conflict action or declare
|
|
1372
|
+
* it on the basis key.
|
|
1373
|
+
*
|
|
1374
|
+
* Funnels every mismatch through {@link rejectBasisGovernedConflictAction} (the single
|
|
1375
|
+
* diagnostic emitter), passing the first mismatched governing key as `basis`.
|
|
1376
|
+
*/
|
|
1377
|
+
function rejectBasisGovernedConflictActionForProvedKey(ctx, constraint, logicalColumns, bijectiveAuthored, label, columnNames, readOnly, errors) {
|
|
1378
|
+
if (readOnly)
|
|
1379
|
+
return;
|
|
1380
|
+
const eff = effectiveKeyDefaultConflict(ctx, constraint);
|
|
1381
|
+
if (eff !== ConflictResolution.REPLACE && eff !== ConflictResolution.IGNORE)
|
|
1382
|
+
return;
|
|
1383
|
+
const basis = ctx.basisSource;
|
|
1384
|
+
const basisCols = basis ? mapLogicalKeyToBasisColumns(ctx, logicalColumns, bijectiveAuthored) : undefined;
|
|
1385
|
+
if (!basis || basisCols === undefined) {
|
|
1386
|
+
// Multi-source / unmappable: governance cannot be pinned — reject conservatively.
|
|
1387
|
+
errors.push({
|
|
1388
|
+
code: 'lens.unenforceable-conflict-action',
|
|
1389
|
+
severity: 'error',
|
|
1390
|
+
site: { table: ctx.table.name, constraint: label },
|
|
1391
|
+
message: `lens: ${label} on '${ctx.table.name}' (${columnNames.map(c => `'${c}'`).join(', ')}) declares 'on conflict ${conflictActionName(eff)}', but its body has no single-source basis-column mapping (multi-source / decomposition), so the governing basis key whose action a write-through duplicate actually resolves to cannot be pinned at deploy. Declare the matching 'on conflict ${conflictActionName(eff)}' on the basis key, or drop the logical conflict action.`,
|
|
1392
|
+
});
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
// Reject the first governing basis key whose action differs from the declared one;
|
|
1396
|
+
// `rejectBasisGovernedConflictAction`'s `eff === basis.conflict` early-return makes
|
|
1397
|
+
// the all-match (and no-governing-key) cases deploy clean.
|
|
1398
|
+
const mismatched = findGoverningBasisKeys(basis, basisCols)
|
|
1399
|
+
.find(m => basisKeyDefaultConflict(basis, m) !== eff);
|
|
1400
|
+
if (!mismatched)
|
|
1401
|
+
return;
|
|
1402
|
+
rejectBasisGovernedConflictAction(ctx, constraint, label, columnNames, readOnly, errors, {
|
|
1403
|
+
conflict: basisKeyDefaultConflict(basis, mismatched),
|
|
1404
|
+
label: basisKeyLabel(basis, mismatched),
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Warns when a `proved` logical key is a strict **superkey** of a NOT-NULL basis key —
|
|
1409
|
+
* the over-enforcement sibling of {@link rejectBasisGovernedConflictActionForProvedKey},
|
|
1410
|
+
* emitted from the same `proved` branch. A logical `unique(a, b)` whose body proof rests on
|
|
1411
|
+
* a basis NOT-NULL `unique(a)` (or basis PK `(a)`) is intrinsically unique — but the basis
|
|
1412
|
+
* enforces uniqueness on a *subset* of the logical key's columns, so it rejects two rows
|
|
1413
|
+
* differing only in `b` that the logical schema advertises as valid. The schema is sound
|
|
1414
|
+
* (every logical invariant still holds), just stricter than declared — so this is the
|
|
1415
|
+
* acknowledgeable warning `lens.over-restrictive-basis-key`, never an error.
|
|
1416
|
+
*
|
|
1417
|
+
* Fires when, after mapping the logical key 1:1 to basis columns
|
|
1418
|
+
* ({@link mapLogicalKeyToBasisColumns}), a governing basis key
|
|
1419
|
+
* ({@link findGoverningBasisKeys}) is a **strict** subset of the mapped columns
|
|
1420
|
+
* (`governingKey.length < mappedBasisCols.length`). The strict filter excludes an
|
|
1421
|
+
* exact-match basis key, which enforces exactly the logical key and is fully realizable —
|
|
1422
|
+
* it must not warn. One warning per logical key; when several strict-subset basis keys
|
|
1423
|
+
* exist, the first in {@link findGoverningBasisKeys} enumeration order (the basis PK if it
|
|
1424
|
+
* governs, else the first declared UNIQUE) names the message — not the smallest by arity.
|
|
1425
|
+
*
|
|
1426
|
+
* Scoped to single-source `proved` keys:
|
|
1427
|
+
* - `readOnly` ⇒ early return (a read-only table never writes, so the over-enforcement
|
|
1428
|
+
* never materializes — the same gate as `lens.no-backing-index`);
|
|
1429
|
+
* - multi-source / unmappable ⇒ {@link mapLogicalKeyToBasisColumns} is undefined ⇒ no
|
|
1430
|
+
* advisory (the 1:1 logical→basis mapping the subset search needs does not exist, and
|
|
1431
|
+
* the superkey argument does not transfer across a decomposition — a documented gap);
|
|
1432
|
+
* - a nullable basis sub-key contributes only a guarded FD, so the logical key never
|
|
1433
|
+
* classifies `proved` and this check never sees it (a documented gap).
|
|
1434
|
+
*
|
|
1435
|
+
* Diagnostic-only: it does not alter the proved classification or the returned obligation.
|
|
1436
|
+
*/
|
|
1437
|
+
function warnOverRestrictiveBasisKey(ctx, logicalColumns, bijectiveAuthored, label, columnNames, readOnly, warnings) {
|
|
1438
|
+
if (readOnly)
|
|
1439
|
+
return;
|
|
1440
|
+
const basis = ctx.basisSource;
|
|
1441
|
+
const basisCols = basis ? mapLogicalKeyToBasisColumns(ctx, logicalColumns, bijectiveAuthored) : undefined;
|
|
1442
|
+
if (!basis || basisCols === undefined)
|
|
1443
|
+
return; // multi-source / unmappable: no sound subset relation to report
|
|
1444
|
+
// Strict-subset governing keys only — an exact-match basis key enforces exactly the
|
|
1445
|
+
// logical key (fully realizable) and must not warn.
|
|
1446
|
+
const strictSubset = findGoverningBasisKeys(basis, basisCols)
|
|
1447
|
+
.filter(m => basisKeyColumnIndices(basis, m).length < basisCols.length);
|
|
1448
|
+
if (strictSubset.length === 0)
|
|
1449
|
+
return;
|
|
1450
|
+
const governing = strictSubset[0];
|
|
1451
|
+
const basisKeyColumns = basisKeyColumnIndices(basis, governing)
|
|
1452
|
+
.map(c => basis.columns[c]?.name ?? `#${c}`)
|
|
1453
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1454
|
+
warnings.push({
|
|
1455
|
+
code: 'lens.over-restrictive-basis-key',
|
|
1456
|
+
severity: 'warning',
|
|
1457
|
+
site: { table: ctx.table.name, constraint: label },
|
|
1458
|
+
message: `lens: ${label} on '${ctx.table.name}' (${columnNames.map(c => `'${c}'`).join(', ')}) is a strict superkey of ${basisKeyLabel(basis, governing)} — the basis enforces uniqueness on a subset of the logical key's columns, so it will reject writes the logical schema permits (two rows differing only outside the basis key's columns cannot coexist). This is sound but stricter than the logical declaration advertises; widen the basis key to match, or narrow the logical key, to make the logical contract faithful.`,
|
|
1459
|
+
fingerprintInputs: {
|
|
1460
|
+
constraintColumns: [...columnNames],
|
|
1461
|
+
basisRelation: `${basis.schemaName.toLowerCase()}.${basis.name.toLowerCase()}`,
|
|
1462
|
+
basisKeyColumns,
|
|
1463
|
+
},
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
/** The basis-column indices a matched basis key covers — the PK definition or the UNIQUE's columns. */
|
|
1467
|
+
function basisKeyColumnIndices(basis, match) {
|
|
1468
|
+
return match.kind === 'primaryKey'
|
|
1469
|
+
? basis.primaryKeyDefinition.map(p => p.index)
|
|
1470
|
+
: match.constraint.columns;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Classifies a `check` constraint. A check referencing a column with no write
|
|
1474
|
+
* path (computed lineage) is unrealizable (error). Otherwise it is row-local —
|
|
1475
|
+
* evaluable on the projected row at the write boundary. (Vacuous-by-body-predicate
|
|
1476
|
+
* detection is deferred; a row-local check is always sound, just possibly redundant.)
|
|
1477
|
+
*
|
|
1478
|
+
* An **authored-inverse** column ({@link authoredForwardMap}) has a write path —
|
|
1479
|
+
* the put expressions — and its CHECK stays row-local: the write-time rewrite
|
|
1480
|
+
* substitutes the column's forward `get` (`NEW.`-qualified basis terms) for the
|
|
1481
|
+
* ref, so the CHECK evaluates over the written basis row's logical image. The
|
|
1482
|
+
* map already excludes a forward the rewrite cannot substitute (subquery-
|
|
1483
|
+
* bearing), keeping deploy acceptance and write-time enforceability in lockstep.
|
|
1484
|
+
*/
|
|
1485
|
+
function classifyCheckConstraint(ctx, constraint, errors) {
|
|
1486
|
+
const label = constraintLabel(constraint);
|
|
1487
|
+
const authoredForwards = authoredForwardMap(ctx.slot);
|
|
1488
|
+
for (const ref of collectColumnRefNames(constraint.constraint.expr)) {
|
|
1489
|
+
const li = ctx.logicalColIndex.get(ref.toLowerCase());
|
|
1490
|
+
if (li === undefined)
|
|
1491
|
+
continue; // not a logical column of this table — leave to body resolution
|
|
1492
|
+
if (!isReconstructibleColumn(ctx, ref) && !authoredForwards.has(ref.toLowerCase())) {
|
|
1493
|
+
errors.push({
|
|
1494
|
+
code: 'lens.unrealizable-constraint',
|
|
1495
|
+
severity: 'error',
|
|
1496
|
+
site: { table: ctx.table.name, constraint: label, column: ref },
|
|
1497
|
+
message: `lens: ${label} on '${ctx.table.name}' references column '${ref}', which has computed lineage (no write path); a check over it cannot be enforced at the lens boundary`,
|
|
1498
|
+
});
|
|
1499
|
+
return { constraint, kind: 'enforced-row-local' };
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return { constraint, kind: 'enforced-row-local' };
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Resolves the basis covering structure AND the basis UNIQUE constraint backing a
|
|
1506
|
+
* logical key: maps each logical column → its basis column (via the single-source
|
|
1507
|
+
* body projection), finds a matching basis UNIQUE constraint, and returns a
|
|
1508
|
+
* row-time covering MV reference (`coveringStructureName` /
|
|
1509
|
+
* `_findRowTimeCoveringStructure`) together with that basis UC. Returning the UC
|
|
1510
|
+
* lets two callers inspect it: {@link classifyKeyConstraint}'s row-time
|
|
1511
|
+
* conflict-action check (via {@link rejectRowTimeConflictAction} — the basis UC's
|
|
1512
|
+
* `defaultConflict` is the action the write path actually honors) and the
|
|
1513
|
+
* plan-time FD re-validation ({@link computeLensAssertedKeyFds}, which re-confirms
|
|
1514
|
+
* currency and the basis UC's partial predicate against the *current* catalog).
|
|
1515
|
+
*
|
|
1516
|
+
* Conservative: a multi-source body, an unmapped column, or a missing basis
|
|
1517
|
+
* UC/structure all yield `undefined` (⇒ commit-time scan). The retired auto-index
|
|
1518
|
+
* is deliberately NOT consulted for a logical schema — the explicit covering MV is
|
|
1519
|
+
* the sole row-time structure (`docs/lens.md` § Constraint Attachment). Reads no
|
|
1520
|
+
* `ctx.root`, so it is safe over a lightweight (un-planned) context.
|
|
1521
|
+
*/
|
|
1522
|
+
function findBasisCovering(ctx, logicalColumns) {
|
|
1523
|
+
const basis = ctx.basisSource;
|
|
1524
|
+
if (!basis)
|
|
1525
|
+
return undefined;
|
|
1526
|
+
const basisCols = [];
|
|
1527
|
+
for (const li of logicalColumns) {
|
|
1528
|
+
const name = ctx.table.columns[li]?.name;
|
|
1529
|
+
const bc = name !== undefined ? mappedBasisColumn(ctx, name, basis) : undefined;
|
|
1530
|
+
if (bc === undefined)
|
|
1531
|
+
return undefined;
|
|
1532
|
+
basisCols.push(bc);
|
|
1533
|
+
}
|
|
1534
|
+
const basisColSet = new Set(basisCols);
|
|
1535
|
+
const matching = (basis.uniqueConstraints ?? []).find(uc => uc.columns.length === basisCols.length && uc.columns.every(c => basisColSet.has(c)));
|
|
1536
|
+
if (!matching)
|
|
1537
|
+
return undefined;
|
|
1538
|
+
// Row-time iff a non-stale row-time covering MV answers the basis UC. A
|
|
1539
|
+
// merely *linked* (`coveringStructureName`) but stale / not-row-time-maintained
|
|
1540
|
+
// MV does NOT qualify — claiming row-time there would be unsound, so we fall
|
|
1541
|
+
// through to the commit-time scan.
|
|
1542
|
+
const rowTime = ctx.db._findRowTimeCoveringStructure(basis.schemaName, basis.name, matching);
|
|
1543
|
+
return rowTime ? { ref: { kind: 'materialized-view', name: rowTime.name }, uc: matching } : undefined;
|
|
1544
|
+
}
|
|
1545
|
+
/** The basis column index a logical column maps to under a single-source body, or undefined. */
|
|
1546
|
+
function mappedBasisColumn(ctx, logicalColumn, basis) {
|
|
1547
|
+
const oi = ctx.outputIndex.get(logicalColumn.toLowerCase());
|
|
1548
|
+
if (oi === undefined)
|
|
1549
|
+
return undefined;
|
|
1550
|
+
const rc = ctx.slot.compiledBody.columns[oi];
|
|
1551
|
+
if (rc?.type !== 'column' || rc.expr.type !== 'column')
|
|
1552
|
+
return undefined;
|
|
1553
|
+
return basis.columnIndexMap.get(rc.expr.name.toLowerCase());
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Proves a logical key `proved` by **transporting** it onto a declared basis key
|
|
1557
|
+
* through a bijection. Every key column must be either bare-reconstructible (maps
|
|
1558
|
+
* to its basis column via {@link mappedBasisColumn}) or authored-bijective (in
|
|
1559
|
+
* `bijectiveAuthored`, maps to its single put-target basis column via
|
|
1560
|
+
* {@link authoredPutTargetBasisColumn}); the resulting basis columns must exactly
|
|
1561
|
+
* form a declared basis key ({@link findDeclaredKey} — the basis PK or a non-partial
|
|
1562
|
+
* basis UNIQUE). Returns the matched basis key ({@link TransportProof}) on success,
|
|
1563
|
+
* or `undefined`.
|
|
1564
|
+
*
|
|
1565
|
+
* Soundness: a bijection is injective, so distinct logical keys map to distinct
|
|
1566
|
+
* basis keys; the basis key forbids two rows sharing that tuple, so the logical key
|
|
1567
|
+
* is intrinsically unique with zero runtime enforcement. This subsumes both the
|
|
1568
|
+
* basis-PK and basis-UNIQUE cases (a basis UNIQUE alone entails it via the
|
|
1569
|
+
* bijection — no covering MV required). Absent a declared basis key over the put
|
|
1570
|
+
* targets it returns `undefined` and the caller falls to the commit-time fallback
|
|
1571
|
+
* (the honest O(n) scan over the forward image), never an unsound `proved`.
|
|
1572
|
+
*
|
|
1573
|
+
* Every key column must additionally be declared **NOT NULL**: a basis key is
|
|
1574
|
+
* NULL-skipping (SQL UNIQUE permits multiple all-NULL rows), so a nullable key is
|
|
1575
|
+
* only *conditionally* unique — the existing row-time path classifies that with a
|
|
1576
|
+
* guarded FD (`key → others [guard: key IS NOT NULL]`), which the unconditional
|
|
1577
|
+
* `proved` FD would unsoundly override. A PK column is always NOT NULL; an
|
|
1578
|
+
* authored-bijective column whose logical column is NOT NULL has a non-null,
|
|
1579
|
+
* unconditionally-unique key. A nullable key column therefore defers to
|
|
1580
|
+
* row-time/commit-time rather than taking the transport shortcut.
|
|
1581
|
+
*
|
|
1582
|
+
* Conservative: a multi-source body (no `basisSource`), a key column that is
|
|
1583
|
+
* neither bare nor authored-bijective, a nullable key column, an authored column
|
|
1584
|
+
* with more than one put target / a put target that is not a single bare basis
|
|
1585
|
+
* column, or any unmapped column all yield `undefined`.
|
|
1586
|
+
*/
|
|
1587
|
+
function proveKeyByBijectionTransport(ctx, logicalColumns, bijectiveAuthored) {
|
|
1588
|
+
const basis = ctx.basisSource;
|
|
1589
|
+
if (!basis)
|
|
1590
|
+
return undefined;
|
|
1591
|
+
// A nullable key is only conditionally unique over a NULL-skipping basis key; the
|
|
1592
|
+
// unconditional `proved` FD would be unsound. Defer to row-time/commit-time. This is
|
|
1593
|
+
// the proof's gate, NOT the governance mapper's ({@link mapLogicalKeyToBasisColumns}
|
|
1594
|
+
// omits it — a nullable subset basis key still governs a non-null duplicate).
|
|
1595
|
+
for (const li of logicalColumns) {
|
|
1596
|
+
if (!ctx.table.columns[li]?.notNull)
|
|
1597
|
+
return undefined;
|
|
1598
|
+
}
|
|
1599
|
+
const basisCols = mapLogicalKeyToBasisColumns(ctx, logicalColumns, bijectiveAuthored);
|
|
1600
|
+
if (basisCols === undefined)
|
|
1601
|
+
return undefined;
|
|
1602
|
+
const match = findDeclaredKey(basis, basisCols);
|
|
1603
|
+
return match ? { basis, match } : undefined;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Maps a logical key's columns to their basis column indices under a single-source
|
|
1607
|
+
* body — each key column either bare-reconstructible (maps to its basis column via
|
|
1608
|
+
* {@link mappedBasisColumn}) or authored-bijective (maps to its single put-target basis
|
|
1609
|
+
* column via {@link authoredPutTargetBasisColumn}). The shared mapping loop behind both
|
|
1610
|
+
* the `proved` classification ({@link proveKeyByBijectionTransport}, which adds the
|
|
1611
|
+
* `notNull`/`findDeclaredKey` exact-match gates) and the conflict-action governance check
|
|
1612
|
+
* ({@link rejectBasisGovernedConflictActionForProvedKey}, which subset-searches the result).
|
|
1613
|
+
*
|
|
1614
|
+
* Deliberately carries NO `notNull` gate: that gate is a `proved`-classification concern
|
|
1615
|
+
* (a nullable basis key is NULL-skipping, so it cannot back an *unconditional* proved FD),
|
|
1616
|
+
* whereas governance asks only which basis key fires on a *non-null* write-through
|
|
1617
|
+
* duplicate — which a nullable subset basis key still governs.
|
|
1618
|
+
*
|
|
1619
|
+
* Returns `undefined` (the conservative signal) for a multi-source body (no `basisSource`),
|
|
1620
|
+
* a key column that is neither bare-reconstructible nor authored-bijective, an authored
|
|
1621
|
+
* column with more than one put target / a non-bare put target, or any unmapped column.
|
|
1622
|
+
*/
|
|
1623
|
+
function mapLogicalKeyToBasisColumns(ctx, logicalColumns, bijectiveAuthored) {
|
|
1624
|
+
const basis = ctx.basisSource;
|
|
1625
|
+
if (!basis)
|
|
1626
|
+
return undefined;
|
|
1627
|
+
const basisCols = [];
|
|
1628
|
+
for (const li of logicalColumns) {
|
|
1629
|
+
const col = ctx.table.columns[li];
|
|
1630
|
+
if (!col)
|
|
1631
|
+
return undefined;
|
|
1632
|
+
const name = col.name;
|
|
1633
|
+
let bc;
|
|
1634
|
+
if (isReconstructibleColumn(ctx, name)) {
|
|
1635
|
+
bc = mappedBasisColumn(ctx, name, basis);
|
|
1636
|
+
}
|
|
1637
|
+
else if (bijectiveAuthored.has(name.toLowerCase())) {
|
|
1638
|
+
bc = authoredPutTargetBasisColumn(ctx, name, basis);
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
return undefined; // neither bare-reconstructible nor authored-bijective
|
|
1642
|
+
}
|
|
1643
|
+
if (bc === undefined)
|
|
1644
|
+
return undefined;
|
|
1645
|
+
basisCols.push(bc);
|
|
1646
|
+
}
|
|
1647
|
+
return basisCols;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* The governing conflict action of a matched basis key — the action a duplicate
|
|
1651
|
+
* actually resolves to. Mirrors {@link effectiveKeyDefaultConflict}'s two arms exactly:
|
|
1652
|
+
* a PK uses {@link resolvePkDefaultConflict} (which also folds in any column-level
|
|
1653
|
+
* `defaultConflict`); a UNIQUE uses its own `defaultConflict`. Takes `(basis, match)` —
|
|
1654
|
+
* the pair a {@link TransportProof} carries, but also any subset-matched governing key —
|
|
1655
|
+
* so the transport and governance callers share it.
|
|
1656
|
+
*/
|
|
1657
|
+
function basisKeyDefaultConflict(basis, match) {
|
|
1658
|
+
return match.kind === 'primaryKey'
|
|
1659
|
+
? resolvePkDefaultConflict(basis)
|
|
1660
|
+
: match.constraint.defaultConflict;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* A human-readable label for a matched basis key, for the conflict-action diagnostic.
|
|
1664
|
+
* There is no MV here (the proof/governance needs none), so name the basis key itself —
|
|
1665
|
+
* the PK / UNIQUE on the basis table, falling back to the column list when a UNIQUE is
|
|
1666
|
+
* unnamed (e.g. `basis unique (code)`).
|
|
1667
|
+
*/
|
|
1668
|
+
function basisKeyLabel(basis, match) {
|
|
1669
|
+
if (match.kind === 'primaryKey')
|
|
1670
|
+
return `basis primary key on '${basis.name}'`;
|
|
1671
|
+
const uc = match.constraint;
|
|
1672
|
+
const ucName = uc.name !== undefined
|
|
1673
|
+
? `'${uc.name}'`
|
|
1674
|
+
: `(${uc.columns.map(c => basis.columns[c]?.name ?? `#${c}`).join(', ')})`;
|
|
1675
|
+
return `basis unique ${ucName}`;
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* The basis column an authored (`with inverse`) logical column writes to, when its
|
|
1679
|
+
* put is a **single** assignment to one bare basis column — the put-target the
|
|
1680
|
+
* bijection transports onto a basis key. Reads the compiled body's authored
|
|
1681
|
+
* `inverse` clause directly (the same `ResultColumnInverse[]` the lineage threads).
|
|
1682
|
+
* Returns undefined for an authored put with more than one target (a multi-column
|
|
1683
|
+
* fan-out cannot land on a single basis key column) or a target name that does not
|
|
1684
|
+
* resolve to a basis column.
|
|
1685
|
+
*/
|
|
1686
|
+
function authoredPutTargetBasisColumn(ctx, logicalColumn, basis) {
|
|
1687
|
+
const oi = ctx.outputIndex.get(logicalColumn.toLowerCase());
|
|
1688
|
+
if (oi === undefined)
|
|
1689
|
+
return undefined;
|
|
1690
|
+
const rc = ctx.slot.compiledBody.columns[oi];
|
|
1691
|
+
if (rc?.type !== 'column' || !rc.inverse || rc.inverse.length !== 1)
|
|
1692
|
+
return undefined;
|
|
1693
|
+
return basis.columnIndexMap.get(rc.inverse[0].column.toLowerCase());
|
|
1694
|
+
}
|
|
1695
|
+
// ---------------------------------------------------------------------------
|
|
1696
|
+
// Read-side: declared-key FD contribution to the optimizer (the inlined-view
|
|
1697
|
+
// boundary). See docs/lens.md § Constraint Attachment and docs/optimizer.md
|
|
1698
|
+
// § Functional Dependency Tracking.
|
|
1699
|
+
// ---------------------------------------------------------------------------
|
|
1700
|
+
/**
|
|
1701
|
+
* The declared logical keys a lens *proves* or *actively enforces*, encoded as
|
|
1702
|
+
* physical functional dependencies in the body's **output**-column-index space,
|
|
1703
|
+
* for the optimizer to consume at the inlined-view boundary
|
|
1704
|
+
* (`planner/nodes/asserted-keys-node.ts`, wired in `planner/building/select.ts`).
|
|
1705
|
+
*
|
|
1706
|
+
* Soundness is gated by the prover's per-constraint {@link ConstraintObligation}
|
|
1707
|
+
* kind — a false key FD is a *correctness* defect (it can make
|
|
1708
|
+
* DISTINCT/join-elimination/order-by-pruning drop real rows), so the gate
|
|
1709
|
+
* under-claims exactly like every other FD-propagation rule:
|
|
1710
|
+
*
|
|
1711
|
+
* - `proved` — the body intrinsically guarantees the key (the same FD surface
|
|
1712
|
+
* the optimizer derives locally); contribute the **unconditional** key FD.
|
|
1713
|
+
* Redundant-but-harmless when local propagation already surfaces it (`addFd`
|
|
1714
|
+
* subsumes), load-bearing when the inlining context loses it.
|
|
1715
|
+
* - `vacuous` — the empty (singleton) key; contribute `∅ → all_cols` (≤1-row).
|
|
1716
|
+
* - `enforced-set-level` `row-time` — a covering structure enforces uniqueness
|
|
1717
|
+
* per row-write, but only over the **non-null** tuples a plain (NULL-skipping)
|
|
1718
|
+
* UNIQUE governs — SQL UNIQUE permits multiple all-/any-NULL rows, so the key
|
|
1719
|
+
* is conditionally unique. Contribute a **guarded** FD `key → others
|
|
1720
|
+
* [guard: key IS NOT NULL]` (the same shape a partial UNIQUE emits), and only
|
|
1721
|
+
* when the covering structure re-validates against the *current* catalog (it
|
|
1722
|
+
* can be dropped / go stale out-of-band between deploys) and the backing basis
|
|
1723
|
+
* UC is non-partial (so NULL-skip is the *entire* uniqueness scope).
|
|
1724
|
+
* - `enforced-set-level` `commit-time` — **excluded**. Detection-only at commit;
|
|
1725
|
+
* a duplicate can transiently exist mid-statement (read-own-writes / Halloween),
|
|
1726
|
+
* so assuming the FD mid-statement is unsound.
|
|
1727
|
+
* - `enforced-row-local` / `enforced-fk` — not uniqueness facts; excluded.
|
|
1728
|
+
*
|
|
1729
|
+
* Returns the (deduped) FD list, or `[]` when nothing is contributable — the
|
|
1730
|
+
* wiring site inlines no node for an empty list (plain views / MVs / unenforced
|
|
1731
|
+
* keys produce none).
|
|
1732
|
+
*/
|
|
1733
|
+
export function computeLensAssertedKeyFds(slot, db) {
|
|
1734
|
+
if (!slot.obligations || slot.obligations.length === 0)
|
|
1735
|
+
return [];
|
|
1736
|
+
const { outputIndex, outputColumns } = buildOutputIndex(slot);
|
|
1737
|
+
const outColCount = outputColumns.length;
|
|
1738
|
+
if (outColCount === 0)
|
|
1739
|
+
return [];
|
|
1740
|
+
let fds = [];
|
|
1741
|
+
for (const ob of slot.obligations) {
|
|
1742
|
+
const fd = assertedFdForObligation(ob, slot, db, outputIndex, outColCount);
|
|
1743
|
+
if (fd)
|
|
1744
|
+
fds = addFd(fds, fd);
|
|
1745
|
+
}
|
|
1746
|
+
return fds;
|
|
1747
|
+
}
|
|
1748
|
+
/** The asserted key FD for one obligation, or undefined when it contributes none. */
|
|
1749
|
+
function assertedFdForObligation(ob, slot, db, outputIndex, outColCount) {
|
|
1750
|
+
const c = ob.constraint;
|
|
1751
|
+
if (c.kind !== 'primaryKey' && c.kind !== 'unique')
|
|
1752
|
+
return undefined; // not a key fact
|
|
1753
|
+
switch (ob.kind) {
|
|
1754
|
+
case 'vacuous':
|
|
1755
|
+
// The empty (singleton) key ⇒ `∅ → all_cols` (≤1-row). superkeyToFd([], n)
|
|
1756
|
+
// is the canonical encoding.
|
|
1757
|
+
return superkeyToFd([], outColCount);
|
|
1758
|
+
case 'proved':
|
|
1759
|
+
// The body unconditionally guarantees the key. Contribute it unconditionally.
|
|
1760
|
+
return encodeKeyFd(logicalKeyColumns(c), slot.logicalTable, outputIndex, outColCount, undefined);
|
|
1761
|
+
case 'enforced-set-level': {
|
|
1762
|
+
if (ob.mode !== 'row-time')
|
|
1763
|
+
return undefined; // commit-time gated out (unsound mid-statement)
|
|
1764
|
+
const logicalCols = logicalKeyColumns(c);
|
|
1765
|
+
// Re-validate the covering structure against the current catalog (currency)
|
|
1766
|
+
// and require a non-partial basis UC (so IS-NOT-NULL is the full scope).
|
|
1767
|
+
if (!revalidateRowTime(slot, db, logicalCols))
|
|
1768
|
+
return undefined;
|
|
1769
|
+
const guard = buildNotNullGuard(logicalCols, slot.logicalTable, outputIndex);
|
|
1770
|
+
if (!guard)
|
|
1771
|
+
return undefined; // no nullable key column ⇒ would be `proved`, not row-time; skip
|
|
1772
|
+
return encodeKeyFd(logicalCols, slot.logicalTable, outputIndex, outColCount, guard);
|
|
1773
|
+
}
|
|
1774
|
+
default:
|
|
1775
|
+
return undefined; // enforced-row-local / enforced-fk
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
/** The logical column indices forming a primary-key / unique constraint. */
|
|
1779
|
+
function logicalKeyColumns(c) {
|
|
1780
|
+
return c.kind === 'primaryKey' ? c.columns.map(col => col.index) : c.constraint.columns;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Encode `key → others` over the body's output columns. Maps each logical key
|
|
1784
|
+
* column → its output index; a key column with no output index
|
|
1785
|
+
* (not emitted) makes the key inexpressible (⇒ undefined). The
|
|
1786
|
+
* all-columns key has no non-trivial FD encoding (`superkeyToFd` ⇒ undefined) and
|
|
1787
|
+
* is skipped (v1). When `guard` is supplied the FD activates only under a
|
|
1788
|
+
* surrounding predicate that entails it (the nullable / partial-UNIQUE case).
|
|
1789
|
+
*/
|
|
1790
|
+
function encodeKeyFd(logicalColumns, table, outputIndex, outColCount, guard) {
|
|
1791
|
+
const outCols = [];
|
|
1792
|
+
for (const li of logicalColumns) {
|
|
1793
|
+
const name = table.columns[li]?.name;
|
|
1794
|
+
const oi = name !== undefined ? outputIndex.get(name.toLowerCase()) : undefined;
|
|
1795
|
+
if (oi === undefined)
|
|
1796
|
+
return undefined; // not emitted ⇒ not in the readable relation
|
|
1797
|
+
outCols.push(oi);
|
|
1798
|
+
}
|
|
1799
|
+
const fd = superkeyToFd(outCols, outColCount);
|
|
1800
|
+
if (!fd)
|
|
1801
|
+
return undefined;
|
|
1802
|
+
return guard ? { ...fd, guard } : fd;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* The `key IS NOT NULL` guard for a conditionally-unique (NULL-skipping) key — one
|
|
1806
|
+
* `is-null negated:true` clause per **nullable** key column (a NOT-NULL column
|
|
1807
|
+
* needs none; the guard checker discharges it from type info). Returns undefined
|
|
1808
|
+
* when every key column is already NOT NULL — that key is unconditional and would
|
|
1809
|
+
* have classified `proved`, so a row-time obligation over it is skipped defensively.
|
|
1810
|
+
*/
|
|
1811
|
+
function buildNotNullGuard(logicalColumns, table, outputIndex) {
|
|
1812
|
+
const clauses = [];
|
|
1813
|
+
for (const li of logicalColumns) {
|
|
1814
|
+
const col = table.columns[li];
|
|
1815
|
+
if (!col)
|
|
1816
|
+
return undefined;
|
|
1817
|
+
if (col.notNull)
|
|
1818
|
+
continue;
|
|
1819
|
+
const oi = outputIndex.get(col.name.toLowerCase());
|
|
1820
|
+
if (oi === undefined)
|
|
1821
|
+
return undefined;
|
|
1822
|
+
clauses.push({ kind: 'is-null', column: oi, negated: true });
|
|
1823
|
+
}
|
|
1824
|
+
return clauses.length > 0 ? { clauses } : undefined;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Re-confirm at plan time that a row-time obligation's covering structure is
|
|
1828
|
+
* still valid: a covering MV can be dropped or go stale out-of-band between
|
|
1829
|
+
* deploys (the basis is a physical schema whose DDL does not re-run the prover),
|
|
1830
|
+
* so the deploy-time snapshot must be re-validated. Also requires the backing
|
|
1831
|
+
* basis UC to be **non-partial** — a partial UNIQUE (`… where P`) makes the
|
|
1832
|
+
* uniqueness scope `P`, not merely NULL-skip, which the IS-NOT-NULL guard would
|
|
1833
|
+
* not capture. Cheap: re-resolves the covering structure against the current
|
|
1834
|
+
* catalog without re-planning the body.
|
|
1835
|
+
*/
|
|
1836
|
+
function revalidateRowTime(slot, db, logicalColumns) {
|
|
1837
|
+
const ctx = buildLiteProveContext(slot, db);
|
|
1838
|
+
const covering = findBasisCovering(ctx, logicalColumns);
|
|
1839
|
+
return covering !== undefined && covering.uc.predicate === undefined;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* A lightweight prove context for plan-time covering re-resolution — every field
|
|
1843
|
+
* {@link findBasisCovering} reads, with `root` left undefined (the body is NOT
|
|
1844
|
+
* re-planned; the covering resolver is plan-independent). Keeps the per-read cost
|
|
1845
|
+
* to a couple of map builds + a catalog lookup.
|
|
1846
|
+
*/
|
|
1847
|
+
function buildLiteProveContext(slot, db) {
|
|
1848
|
+
const table = slot.logicalTable;
|
|
1849
|
+
const logicalColIndex = new Map();
|
|
1850
|
+
table.columns.forEach((c, i) => logicalColIndex.set(c.name.toLowerCase(), i));
|
|
1851
|
+
const { outputIndex, outputColumns } = buildOutputIndex(slot);
|
|
1852
|
+
const basisSchemaName = slot.defaultBasis.schemaName;
|
|
1853
|
+
return {
|
|
1854
|
+
slot,
|
|
1855
|
+
db,
|
|
1856
|
+
table,
|
|
1857
|
+
logicalColIndex,
|
|
1858
|
+
outputColumns,
|
|
1859
|
+
outputIndex,
|
|
1860
|
+
root: undefined,
|
|
1861
|
+
basisSource: resolveSingleBasisSource(db.schemaManager, slot.compiledBody, basisSchemaName),
|
|
1862
|
+
basisSchemaName,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
// ---------------------------------------------------------------------------
|
|
1866
|
+
// Warning: no answering structure for a declared access pattern
|
|
1867
|
+
// ---------------------------------------------------------------------------
|
|
1868
|
+
/**
|
|
1869
|
+
* `quereus.lens.access.<col>` declares an expected lookup/ordering. The advisory
|
|
1870
|
+
* fires when no basis ordering/index serves it. v1 best-effort: a single-source
|
|
1871
|
+
* body's basis table must carry an index whose leading column is the access
|
|
1872
|
+
* column (or have that column as the leading PK column); otherwise reads scan.
|
|
1873
|
+
*/
|
|
1874
|
+
function checkAnsweringStructures(ctx, warnings) {
|
|
1875
|
+
const accessTags = getReservedTagByTemplate(ctx.table.tags, 'quereus.lens.access.<col>');
|
|
1876
|
+
if (accessTags.length === 0)
|
|
1877
|
+
return;
|
|
1878
|
+
const basis = ctx.basisSource;
|
|
1879
|
+
for (const tag of accessTags) {
|
|
1880
|
+
const col = tag.segment;
|
|
1881
|
+
if (basisServesAccess(ctx, basis, col))
|
|
1882
|
+
continue;
|
|
1883
|
+
warnings.push({
|
|
1884
|
+
code: 'lens.no-answering-structure',
|
|
1885
|
+
severity: 'warning',
|
|
1886
|
+
site: { table: ctx.table.name, column: col },
|
|
1887
|
+
message: `lens: declared access pattern 'quereus.lens.access.${col}' on '${ctx.table.name}' has no answering basis ordering or index — reads on '${col}' will scan. Add a basis index/covering materialized view ordered by '${col}'.`,
|
|
1888
|
+
fingerprintInputs: buildFingerprint(ctx, [col], false),
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
/** True iff the basis single source has an index / PK whose leading column is `accessCol`. */
|
|
1893
|
+
function basisServesAccess(ctx, basis, accessCol) {
|
|
1894
|
+
if (!basis)
|
|
1895
|
+
return false;
|
|
1896
|
+
const basisCol = mappedBasisColumn(ctx, accessCol, basis);
|
|
1897
|
+
if (basisCol === undefined)
|
|
1898
|
+
return false;
|
|
1899
|
+
if (basis.primaryKeyDefinition[0]?.index === basisCol)
|
|
1900
|
+
return true;
|
|
1901
|
+
for (const idx of basis.indexes ?? []) {
|
|
1902
|
+
if (idx.columns[0]?.index === basisCol)
|
|
1903
|
+
return true;
|
|
1904
|
+
}
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
// ---------------------------------------------------------------------------
|
|
1908
|
+
// Warning: partial override (informational)
|
|
1909
|
+
// ---------------------------------------------------------------------------
|
|
1910
|
+
/**
|
|
1911
|
+
* When an override covers only some columns and the remainder took the default
|
|
1912
|
+
* alignment, list which columns were override-authored vs gap-filled, read
|
|
1913
|
+
* straight off the slot's `columnProvenance`.
|
|
1914
|
+
*/
|
|
1915
|
+
function checkPartialOverride(ctx, warnings) {
|
|
1916
|
+
const authored = ctx.slot.columnProvenance.filter(p => p.source === 'override').map(p => p.logicalColumn);
|
|
1917
|
+
const gapFilled = ctx.slot.columnProvenance.filter(p => p.source === 'default').map(p => p.logicalColumn);
|
|
1918
|
+
if (authored.length === 0 || gapFilled.length === 0)
|
|
1919
|
+
return; // pure override or pure default — not partial
|
|
1920
|
+
warnings.push({
|
|
1921
|
+
code: 'lens.partial-override',
|
|
1922
|
+
severity: 'warning',
|
|
1923
|
+
site: { table: ctx.table.name },
|
|
1924
|
+
message: `lens: '${ctx.table.name}' is a partial override — override-authored column(s): ${authored.map(c => `'${c}'`).join(', ')}; default gap-filled column(s): ${gapFilled.map(c => `'${c}'`).join(', ')}`,
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
// ---------------------------------------------------------------------------
|
|
1928
|
+
// Fingerprint inputs (consumed by the sibling acknowledgment ticket)
|
|
1929
|
+
// ---------------------------------------------------------------------------
|
|
1930
|
+
function buildFingerprint(ctx, columnNames, hasCoveringStructure) {
|
|
1931
|
+
const basis = ctx.basisSource;
|
|
1932
|
+
return {
|
|
1933
|
+
constraintColumns: [...columnNames].sort((a, b) => a.localeCompare(b)),
|
|
1934
|
+
hasCoveringStructure,
|
|
1935
|
+
cardinalityBand: cardinalityBand(basis?.estimatedRows),
|
|
1936
|
+
basisRelation: basis ? `${basis.schemaName.toLowerCase()}.${basis.name.toLowerCase()}` : undefined,
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
/** Coarse cardinality band so an acknowledgment survives ordinary row-count churn. */
|
|
1940
|
+
function cardinalityBand(rows) {
|
|
1941
|
+
if (rows === undefined)
|
|
1942
|
+
return 'unknown';
|
|
1943
|
+
if (rows === 0)
|
|
1944
|
+
return 'empty';
|
|
1945
|
+
if (rows < 1_000)
|
|
1946
|
+
return 'small';
|
|
1947
|
+
if (rows < 1_000_000)
|
|
1948
|
+
return 'medium';
|
|
1949
|
+
return 'large';
|
|
1950
|
+
}
|
|
1951
|
+
// ---------------------------------------------------------------------------
|
|
1952
|
+
// Shared utility
|
|
1953
|
+
// ---------------------------------------------------------------------------
|
|
1954
|
+
/**
|
|
1955
|
+
* Collects every `column` reference name in an expression (best-effort reflective walk,
|
|
1956
|
+
* qualifier-stripped — returns each `column` node's `.name`). Shared with the lens write
|
|
1957
|
+
* side (`planner/mutation/lens-enforcement.ts`), which maps these refs through the slot's
|
|
1958
|
+
* logical→basis projection to derive a row-local CHECK's `referencedWriteRowColumns`
|
|
1959
|
+
* metadata — keeping the gate's notion of "write-row column" consistent with the prover's
|
|
1960
|
+
* notion of "row-local" (both use this walk + logical-column membership; see
|
|
1961
|
+
* {@link classifyCheckConstraint}).
|
|
1962
|
+
*/
|
|
1963
|
+
export function collectColumnRefNames(expr) {
|
|
1964
|
+
const names = [];
|
|
1965
|
+
const stack = [expr];
|
|
1966
|
+
while (stack.length > 0) {
|
|
1967
|
+
const node = stack.pop();
|
|
1968
|
+
if (node.type === 'column' && typeof node.name === 'string') {
|
|
1969
|
+
names.push(node.name);
|
|
1970
|
+
}
|
|
1971
|
+
for (const key of Object.keys(node)) {
|
|
1972
|
+
const value = node[key];
|
|
1973
|
+
if (!value)
|
|
1974
|
+
continue;
|
|
1975
|
+
if (Array.isArray(value)) {
|
|
1976
|
+
for (const item of value) {
|
|
1977
|
+
if (item && typeof item === 'object' && 'type' in item)
|
|
1978
|
+
stack.push(item);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
else if (typeof value === 'object' && 'type' in value) {
|
|
1982
|
+
stack.push(value);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return names;
|
|
1987
|
+
}
|
|
1988
|
+
//# sourceMappingURL=lens-prover.js.map
|