@intentius/chant-lexicon-sql 0.124.46 → 0.124.49
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/dist/change-set.d.ts +1 -1
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +2 -1
- package/dist/clickhouse/apply/statements.d.ts +9 -0
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.js +24 -0
- package/dist/clickhouse/import/live-export.d.ts +7 -3
- package/dist/clickhouse/import/live-export.d.ts.map +1 -1
- package/dist/clickhouse/import/live-export.js +21 -5
- package/dist/clickhouse/live/named-collections.d.ts +27 -0
- package/dist/clickhouse/live/named-collections.d.ts.map +1 -0
- package/dist/clickhouse/live/named-collections.js +53 -0
- package/dist/clickhouse/live/undeclarable.d.ts +15 -3
- package/dist/clickhouse/live/undeclarable.d.ts.map +1 -1
- package/dist/clickhouse/live/undeclarable.js +18 -4
- package/dist/clickhouse/named-collections.d.ts +83 -0
- package/dist/clickhouse/named-collections.d.ts.map +1 -0
- package/dist/clickhouse/named-collections.js +203 -0
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/parser.js +1 -1
- package/dist/clickhouse/plan/commands.d.ts +2 -1
- package/dist/clickhouse/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/plan/commands.js +17 -2
- package/dist/clickhouse/plan/diff.d.ts +15 -0
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.js +43 -1
- package/dist/clickhouse/plan/disruption.d.ts.map +1 -1
- package/dist/clickhouse/plan/disruption.js +5 -0
- package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
- package/dist/clickhouse/plan/rebuild-handoff.js +4 -0
- package/dist/clickhouse/plan/report.d.ts.map +1 -1
- package/dist/clickhouse/plan/report.js +6 -1
- package/dist/clickhouse/plan/rules.d.ts +5 -1
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.js +7 -3
- package/dist/clickhouse/plan/statements.d.ts +29 -0
- package/dist/clickhouse/plan/statements.d.ts.map +1 -0
- package/dist/clickhouse/plan/statements.js +334 -0
- package/dist/clickhouse/plan/stream.d.ts +40 -0
- package/dist/clickhouse/plan/stream.d.ts.map +1 -0
- package/dist/clickhouse/plan/stream.js +107 -0
- package/dist/clickhouse/rebuild/observe.d.ts +3 -1
- package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/observe.js +14 -1
- package/dist/clickhouse/review-catalog.d.ts +213 -0
- package/dist/clickhouse/review-catalog.d.ts.map +1 -0
- package/dist/clickhouse/review-catalog.js +330 -0
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/classifier-page.js +44 -4
- package/dist/codegen/clickhouse-module.d.ts.map +1 -1
- package/dist/codegen/clickhouse-module.js +3 -0
- package/dist/codegen/coverage-page.d.ts.map +1 -1
- package/dist/codegen/coverage-page.js +5 -3
- package/dist/codegen/docs.js +1 -1
- package/dist/codegen/postgres-classifier-page.js +2 -2
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/codegen/registry.js +10 -0
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/catalog.js +100 -0
- package/dist/composites/clickhouse.d.ts +2 -0
- package/dist/composites/clickhouse.d.ts.map +1 -1
- package/dist/composites/clickhouse.js +2 -0
- package/dist/composites/otel-tables.d.ts +100 -0
- package/dist/composites/otel-tables.d.ts.map +1 -0
- package/dist/composites/otel-tables.js +282 -0
- package/dist/composites/stream-ingest.d.ts +63 -0
- package/dist/composites/stream-ingest.d.ts.map +1 -0
- package/dist/composites/stream-ingest.js +39 -0
- package/dist/core/statement-class.d.ts +55 -0
- package/dist/core/statement-class.d.ts.map +1 -0
- package/dist/core/statement-class.js +43 -0
- package/dist/coverage/clickhouse.json +1 -1
- package/dist/coverage/postgres.json +12 -12
- package/dist/coverage.d.ts +16 -6
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +10 -5
- package/dist/drizzle-command.d.ts +17 -0
- package/dist/drizzle-command.d.ts.map +1 -0
- package/dist/drizzle-command.js +91 -0
- package/dist/drizzle.d.ts +70 -0
- package/dist/drizzle.d.ts.map +1 -0
- package/dist/drizzle.js +689 -0
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/clickhouse.js +8 -1
- package/dist/files/common.d.ts +13 -0
- package/dist/files/common.d.ts.map +1 -1
- package/dist/files/common.js +5 -0
- package/dist/files/postgres.d.ts.map +1 -1
- package/dist/files/postgres.js +35 -3
- package/dist/generated/clickhouse.d.ts +2 -0
- package/dist/generated/clickhouse.d.ts.map +1 -1
- package/dist/generated/clickhouse.js +2 -0
- package/dist/import-parser.d.ts +21 -1
- package/dist/import-parser.d.ts.map +1 -1
- package/dist/import-parser.js +79 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/integrity.json +7 -6
- package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
- package/dist/lint/post-synth/audit-entries.js +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/index.js +2 -0
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.js +3 -0
- package/dist/lint/post-synth/sqlch128.d.ts +18 -0
- package/dist/lint/post-synth/sqlch128.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch128.js +222 -0
- package/dist/lint/rules/postgres-templates.js +2 -2
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/lsp/postgres-template.js +21 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/postgres.js +1 -1
- package/dist/meta.json +50 -0
- package/dist/migrate/from-postgres/detect.d.ts +4 -0
- package/dist/migrate/from-postgres/detect.d.ts.map +1 -0
- package/dist/migrate/from-postgres/detect.js +7 -0
- package/dist/migrate/from-postgres/emit.d.ts +26 -0
- package/dist/migrate/from-postgres/emit.d.ts.map +1 -0
- package/dist/migrate/from-postgres/emit.js +86 -0
- package/dist/migrate/from-postgres/index.d.ts +51 -0
- package/dist/migrate/from-postgres/index.d.ts.map +1 -0
- package/dist/migrate/from-postgres/index.js +115 -0
- package/dist/migrate/from-postgres/translate.d.ts +73 -0
- package/dist/migrate/from-postgres/translate.d.ts.map +1 -0
- package/dist/migrate/from-postgres/translate.js +261 -0
- package/dist/migrate/from-postgres/types.d.ts +51 -0
- package/dist/migrate/from-postgres/types.d.ts.map +1 -0
- package/dist/migrate/from-postgres/types.js +211 -0
- package/dist/migration-classify.d.ts +23 -0
- package/dist/migration-classify.d.ts.map +1 -0
- package/dist/migration-classify.js +44 -0
- package/dist/migration-replay.d.ts +73 -0
- package/dist/migration-replay.d.ts.map +1 -0
- package/dist/migration-replay.js +294 -0
- package/dist/okf/index.md +11 -0
- package/dist/okf/rules/SQLCH128.md +11 -0
- package/dist/okf/types/PostgresForeignDataWrapper.md +9 -0
- package/dist/okf/types/PostgresForeignServer.md +9 -0
- package/dist/okf/types/PostgresForeignTable.md +9 -0
- package/dist/okf/types/PostgresOperator.md +9 -0
- package/dist/okf/types/PostgresOperatorClass.md +9 -0
- package/dist/okf/types/PostgresOperatorFamily.md +9 -0
- package/dist/okf/types/PostgresPublication.md +9 -0
- package/dist/okf/types/PostgresStatistics.md +9 -0
- package/dist/okf/types/PostgresTextSearchConfiguration.md +9 -0
- package/dist/okf/types/PostgresTextSearchDictionary.md +9 -0
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activities/postgres-migration.js +4 -0
- package/dist/op/activity-contracts.d.ts +20 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +2 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +49 -2
- package/dist/postgres/access/acl.js +1 -1
- package/dist/postgres/access/import.js +1 -1
- package/dist/postgres/apply/apply.js +13 -1
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/apply/statements.js +243 -4
- package/dist/postgres/constraint-names.d.ts +30 -0
- package/dist/postgres/constraint-names.d.ts.map +1 -0
- package/dist/postgres/constraint-names.js +72 -0
- package/dist/postgres/entities.d.ts +229 -2
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entities.js +326 -5
- package/dist/postgres/entity-types.d.ts +10 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/entity-types.js +10 -0
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/generator.js +65 -2
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/import/live-export.js +2 -0
- package/dist/postgres/live/catalog-kinds.d.ts +20 -0
- package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
- package/dist/postgres/live/catalog-kinds.js +452 -0
- package/dist/postgres/live/catalog.d.ts +2 -0
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/catalog.js +3 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.js +12 -2
- package/dist/postgres/migrate/backfill.d.ts +2 -0
- package/dist/postgres/migrate/backfill.d.ts.map +1 -1
- package/dist/postgres/migrate/backfill.js +7 -1
- package/dist/postgres/migrate/carry.d.ts +45 -1
- package/dist/postgres/migrate/carry.d.ts.map +1 -1
- package/dist/postgres/migrate/carry.js +72 -8
- package/dist/postgres/migrate/observe.d.ts +28 -4
- package/dist/postgres/migrate/observe.d.ts.map +1 -1
- package/dist/postgres/migrate/observe.js +45 -13
- package/dist/postgres/migrate/op.d.ts +26 -1
- package/dist/postgres/migrate/op.d.ts.map +1 -1
- package/dist/postgres/migrate/op.js +3 -1
- package/dist/postgres/migrate/replication.d.ts +42 -0
- package/dist/postgres/migrate/replication.d.ts.map +1 -1
- package/dist/postgres/migrate/replication.js +53 -0
- package/dist/postgres/migrate/steps.d.ts +35 -2
- package/dist/postgres/migrate/steps.d.ts.map +1 -1
- package/dist/postgres/migrate/steps.js +158 -12
- package/dist/postgres/parser.d.ts +144 -1
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/parser.js +370 -4
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/deep.js +43 -4
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/diff.js +154 -1
- package/dist/postgres/plan/normalize.d.ts +28 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.js +260 -1
- package/dist/postgres/plan/rules.d.ts +4 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/rules.js +6 -2
- package/dist/postgres/plan/schema.js +1 -1
- package/dist/postgres/plan/statements.d.ts +26 -0
- package/dist/postgres/plan/statements.d.ts.map +1 -0
- package/dist/postgres/plan/statements.js +364 -0
- package/dist/postgres/secret-options.d.ts +10 -0
- package/dist/postgres/secret-options.d.ts.map +1 -0
- package/dist/postgres/secret-options.js +9 -0
- package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
- package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
- package/dist/postgres/testing/roundtrip-schema.js +24 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +1 -1
- package/dist/rules/audit-entries.ts +1 -0
- package/dist/rules/postgres-names.ts +2 -0
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlch128.ts +230 -0
- package/dist/serializer.d.ts +5 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +13 -2
- package/dist/sql-types-command.d.ts +13 -0
- package/dist/sql-types-command.d.ts.map +1 -1
- package/dist/sql-types-command.js +31 -16
- package/dist/sql-types.d.ts +22 -0
- package/dist/sql-types.d.ts.map +1 -1
- package/dist/sql-types.js +10 -6
- package/dist/sqlserver/lexer.d.ts +37 -0
- package/dist/sqlserver/lexer.d.ts.map +1 -0
- package/dist/sqlserver/lexer.js +254 -0
- package/dist/sqlserver/parse.d.ts +185 -0
- package/dist/sqlserver/parse.d.ts.map +1 -0
- package/dist/sqlserver/parse.js +767 -0
- package/dist/sqlserver/translate.d.ts +34 -0
- package/dist/sqlserver/translate.d.ts.map +1 -0
- package/dist/sqlserver/translate.js +495 -0
- package/dist/sqlserver/types.d.ts +44 -0
- package/dist/sqlserver/types.d.ts.map +1 -0
- package/dist/sqlserver/types.js +133 -0
- package/dist/sqlserver/validate.d.ts +31 -0
- package/dist/sqlserver/validate.d.ts.map +1 -0
- package/dist/sqlserver/validate.js +67 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +10 -0
- package/package.json +2 -2
- package/src/change-set.ts +2 -1
- package/src/clickhouse/apply/statements.ts +25 -0
- package/src/clickhouse/import/live-export.ts +20 -5
- package/src/clickhouse/live/named-collections.e2e.test.ts +88 -0
- package/src/clickhouse/live/named-collections.ts +61 -0
- package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -3
- package/src/clickhouse/live/undeclarable.ts +20 -4
- package/src/clickhouse/named-collections.test.ts +187 -0
- package/src/clickhouse/named-collections.ts +224 -0
- package/src/clickhouse/parser.ts +2 -1
- package/src/clickhouse/plan/commands.ts +17 -2
- package/src/clickhouse/plan/diff.ts +57 -1
- package/src/clickhouse/plan/disruption.ts +5 -0
- package/src/clickhouse/plan/rebuild-handoff.ts +3 -0
- package/src/clickhouse/plan/report.ts +6 -1
- package/src/clickhouse/plan/rules.ts +14 -4
- package/src/clickhouse/plan/statements.test.ts +61 -0
- package/src/clickhouse/plan/statements.ts +306 -0
- package/src/clickhouse/plan/stream-s3queue.e2e.test.ts +148 -0
- package/src/clickhouse/plan/stream.test.ts +237 -0
- package/src/clickhouse/plan/stream.ts +111 -0
- package/src/clickhouse/rebuild/observe.ts +15 -1
- package/src/clickhouse/review-catalog.test.ts +181 -0
- package/src/clickhouse/review-catalog.ts +489 -0
- package/src/codegen/classifier-page.ts +44 -4
- package/src/codegen/clickhouse-module.ts +3 -0
- package/src/codegen/coverage-page.ts +5 -3
- package/src/codegen/docs.ts +1 -1
- package/src/codegen/postgres-classifier-page.ts +2 -2
- package/src/codegen/registry.ts +10 -0
- package/src/composites/catalog.test.ts +6 -1
- package/src/composites/catalog.ts +100 -0
- package/src/composites/clickhouse.ts +2 -0
- package/src/composites/composites.test.ts +1 -1
- package/src/composites/otel-tables.test.ts +118 -0
- package/src/composites/otel-tables.ts +339 -0
- package/src/composites/stream-ingest.test.ts +114 -0
- package/src/composites/stream-ingest.ts +82 -0
- package/src/core/statement-class.ts +88 -0
- package/src/core/undeclarable.test.ts +6 -5
- package/src/coverage/clickhouse.json +1 -1
- package/src/coverage/postgres.json +12 -12
- package/src/coverage.test.ts +27 -2
- package/src/coverage.ts +18 -7
- package/src/drizzle-command.ts +94 -0
- package/src/drizzle-kit.e2e.test.ts +89 -0
- package/src/drizzle-types.e2e.test.ts +67 -0
- package/src/drizzle.test.ts +358 -0
- package/src/drizzle.ts +742 -0
- package/src/files/clickhouse.ts +8 -1
- package/src/files/common.ts +14 -1
- package/src/files/postgres.ts +34 -3
- package/src/generated/clickhouse.ts +3 -0
- package/src/generated/lexicon-sql.json +50 -0
- package/src/import-parser.sqlserver.test.ts +92 -0
- package/src/import-parser.ts +79 -5
- package/src/index.ts +1 -1
- package/src/lint/post-synth/audit-entries.ts +1 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/postgres-checks.test.ts +8 -1
- package/src/lint/post-synth/postgres-names.ts +2 -0
- package/src/lint/post-synth/sqlch128.test.ts +107 -0
- package/src/lint/post-synth/sqlch128.ts +230 -0
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lsp/postgres-template.ts +20 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/migrate/from-postgres/cli.test.ts +165 -0
- package/src/migrate/from-postgres/detect.ts +7 -0
- package/src/migrate/from-postgres/emit.ts +96 -0
- package/src/migrate/from-postgres/example.test.ts +205 -0
- package/src/migrate/from-postgres/index.ts +122 -0
- package/src/migrate/from-postgres/translate.test.ts +242 -0
- package/src/migrate/from-postgres/translate.ts +359 -0
- package/src/migrate/from-postgres/types.test.ts +116 -0
- package/src/migrate/from-postgres/types.ts +236 -0
- package/src/migrate/from-postgres/validate.e2e.test.ts +44 -0
- package/src/migrate/from-postgres/workspace.e2e.test.ts +114 -0
- package/src/migration-classify.test.ts +150 -0
- package/src/migration-classify.ts +45 -0
- package/src/migration-replay.e2e.test.ts +155 -0
- package/src/migration-replay.test.ts +173 -0
- package/src/migration-replay.ts +289 -0
- package/src/op/__snapshots__/watch-pipeline.test.ts.snap +2 -2
- package/src/op/activities/postgres-migration.ts +4 -0
- package/src/op/activity-contracts.ts +2 -0
- package/src/op/watch-pipeline.test.ts +8 -1
- package/src/plugin.ts +51 -2
- package/src/postgres/access/acl.ts +1 -1
- package/src/postgres/access/import.ts +1 -1
- package/src/postgres/apply/apply.ts +14 -1
- package/src/postgres/apply/statements.ts +225 -4
- package/src/postgres/constraint-names.ts +71 -0
- package/src/postgres/database-kinds.test.ts +865 -0
- package/src/postgres/entities.ts +541 -5
- package/src/postgres/entity-types.ts +10 -0
- package/src/postgres/import/generator.ts +54 -2
- package/src/postgres/import/live-export.ts +1 -0
- package/src/postgres/live/catalog-kinds.ts +494 -0
- package/src/postgres/live/catalog.ts +6 -1
- package/src/postgres/live/database-kinds.e2e.test.ts +164 -0
- package/src/postgres/live/describe-resources.ts +13 -2
- package/src/postgres/live/undeclarable.e2e.test.ts +13 -6
- package/src/postgres/migrate/backfill.ts +13 -1
- package/src/postgres/migrate/carry.ts +103 -8
- package/src/postgres/migrate/followups.e2e.test.ts +218 -0
- package/src/postgres/migrate/followups.test.ts +199 -0
- package/src/postgres/migrate/kinds.e2e.test.ts +5 -5
- package/src/postgres/migrate/observe.ts +56 -15
- package/src/postgres/migrate/op.ts +28 -1
- package/src/postgres/migrate/partition.e2e.test.ts +129 -0
- package/src/postgres/migrate/partition.test.ts +206 -0
- package/src/postgres/migrate/replication.ts +66 -0
- package/src/postgres/migrate/steps.ts +157 -12
- package/src/postgres/migrate/subscriber.e2e.test.ts +141 -0
- package/src/postgres/migrate/subscriber.test.ts +59 -0
- package/src/postgres/parser.ts +455 -4
- package/src/postgres/plan/deep.ts +42 -4
- package/src/postgres/plan/diff.ts +137 -1
- package/src/postgres/plan/normalize.ts +277 -1
- package/src/postgres/plan/rules.ts +32 -4
- package/src/postgres/plan/schema.ts +1 -1
- package/src/postgres/plan/statements.test.ts +78 -0
- package/src/postgres/plan/statements.ts +299 -0
- package/src/postgres/secret-options.ts +10 -0
- package/src/postgres/testing/roundtrip-schema.ts +24 -1
- package/src/postgres.ts +33 -0
- package/src/serializer.ts +13 -2
- package/src/skills/chant-sql-postgres-migration.md +4 -3
- package/src/skills/chant-sql-postgres.md +11 -1
- package/src/skills/chant-sql.md +2 -1
- package/src/sql-files.test.ts +33 -0
- package/src/sql-types-command.ts +38 -16
- package/src/sql-types.ts +11 -6
- package/src/sqlserver/fixture.test.ts +186 -0
- package/src/sqlserver/import-cli.test.ts +61 -0
- package/src/sqlserver/lexer.ts +260 -0
- package/src/sqlserver/parse.ts +865 -0
- package/src/sqlserver/translate.test.ts +396 -0
- package/src/sqlserver/translate.ts +558 -0
- package/src/sqlserver/types.test.ts +102 -0
- package/src/sqlserver/types.ts +154 -0
- package/src/sqlserver/validate.e2e.test.ts +32 -0
- package/src/sqlserver/validate.test.ts +49 -0
- package/src/sqlserver/validate.ts +76 -0
- package/src/validate.ts +10 -0
- package/src/workspace-kinds.test.ts +11 -0
- package/workspace-kinds.json +100 -2
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* | Expand | add the new column, nullable, no default (a catalog change) |
|
|
21
21
|
* | Dual write | a trigger that keeps the new column written: computed from the old one for a type change, both ways for a rename |
|
|
22
22
|
* | Backfill | `UPDATE` per key range, each batch with its receipt in its own transaction, under `lock_timeout`, pausing while a replica is behind |
|
|
23
|
-
* | Carry over | each index on the old column built again on the new one `CONCURRENTLY
|
|
23
|
+
* | Carry over | each index on the old column built again on the new one `CONCURRENTLY` (on a partitioned table: `ON ONLY` the table, `CONCURRENTLY` per partition, then attached), each check and foreign key (another table's too) added `NOT VALID` and validated (`./carry.ts`) |
|
|
24
24
|
* | Verify | rows, mismatches and checksums of the new column against the old |
|
|
25
25
|
* | Approve | a gate bound to the plan and the verified new column |
|
|
26
26
|
* | Switch | NOT NULL proven by a validated check; then one short transaction: a type change swaps the columns by name, a rename finishes the new column; the carried indexes and constraints take their names (keys `USING INDEX`), and the views that read the column are made again from their declarations |
|
|
@@ -64,6 +64,29 @@ export interface PostgresMigrationOpConfig {
|
|
|
64
64
|
* and the verification all use it.
|
|
65
65
|
*/
|
|
66
66
|
using?: string;
|
|
67
|
+
/**
|
|
68
|
+
* A rename combined with a type change: the SQL expression that gives the
|
|
69
|
+
* old column's value from the new column's, over the row's columns (the
|
|
70
|
+
* new column by its declared name). `using` gives the new value from the
|
|
71
|
+
* old one. Both stay in force until the contract: a writer of the old name
|
|
72
|
+
* has `using` applied, a writer of the new name has `reverse` applied. The
|
|
73
|
+
* verification fails the run unless `reverse` over every new value gives
|
|
74
|
+
* the old value back. A new value the old type cannot hold fails the
|
|
75
|
+
* writer's statement until the contract drops the old column.
|
|
76
|
+
*/
|
|
77
|
+
reverse?: string;
|
|
78
|
+
/**
|
|
79
|
+
* A rename of a column with a default, an identity or a serial sequence.
|
|
80
|
+
* The declared default (or identity, or the serial's default) lands on the
|
|
81
|
+
* new column at the contract, not at the switch, so that until then a
|
|
82
|
+
* writer of either name gets the old column's default once. Two writes
|
|
83
|
+
* differ from the declaration until then, and `true` accepts both: an
|
|
84
|
+
* INSERT that names the new column with an explicit NULL gets the default
|
|
85
|
+
* instead of NULL, and `UPDATE ... SET <new> = DEFAULT` sets NULL. An
|
|
86
|
+
* `ApplyOp` between the switch and the contract must not set the default
|
|
87
|
+
* on the new column. Needs `gates: "own"` to end with the contract.
|
|
88
|
+
*/
|
|
89
|
+
defaultAtContract?: boolean;
|
|
67
90
|
/** The build output the declaration is read from. Default: `dist/schema.json`. */
|
|
68
91
|
output?: string;
|
|
69
92
|
/** Project directory to build. Default: `.`. */
|
|
@@ -138,6 +161,8 @@ export interface PostgresMigrationArgs {
|
|
|
138
161
|
buildPath: string;
|
|
139
162
|
environment?: string;
|
|
140
163
|
using?: string;
|
|
164
|
+
reverse?: string;
|
|
165
|
+
defaultAtContract?: boolean;
|
|
141
166
|
retain?: string;
|
|
142
167
|
batchSize?: number;
|
|
143
168
|
replicationLag?: { max?: string; wait?: string } | false;
|
|
@@ -178,6 +203,8 @@ export function PostgresMigrationOp(config: PostgresMigrationOpConfig): Postgres
|
|
|
178
203
|
buildPath: config.output ?? "dist/schema.json",
|
|
179
204
|
environment: config.env,
|
|
180
205
|
...(config.using !== undefined ? { using: config.using } : {}),
|
|
206
|
+
...(config.reverse !== undefined ? { reverse: config.reverse } : {}),
|
|
207
|
+
...(config.defaultAtContract ? { defaultAtContract: true } : {}),
|
|
181
208
|
...(config.retain ? { retain: config.retain } : {}),
|
|
182
209
|
...(config.batchSize !== undefined ? { batchSize: config.batchSize } : {}),
|
|
183
210
|
...(config.replicationLag !== undefined ? { replicationLag: config.replicationLag } : {}),
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An index on a partitioned table's migrated column (#3333), against the
|
|
3
|
+
* pinned server: the Op builds it per partition and attaches it, and what
|
|
4
|
+
* stays refused.
|
|
5
|
+
*
|
|
6
|
+
* - `text` to `numeric` on a column with a plain index and a partial unique
|
|
7
|
+
* index, on a table with a default partition, two partitions and a
|
|
8
|
+
* sub-partitioned one: every partition's index is built `CONCURRENTLY`
|
|
9
|
+
* (never under a lock that blocks writes), attached, and the partitioned
|
|
10
|
+
* index is valid and named as declared after the switch.
|
|
11
|
+
* - A write while the Op waits at its gate lands in the new column.
|
|
12
|
+
* - A unique constraint on the column stays refused, naming why.
|
|
13
|
+
*
|
|
14
|
+
* Needs Docker; skips cleanly without it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
21
|
+
import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
|
|
22
|
+
import { ApprovingLedger, runMigrationOp } from "../testing/migration";
|
|
23
|
+
import type { PostgresClient } from "../live/client";
|
|
24
|
+
import { planPgAgainstServer } from "../plan/commands";
|
|
25
|
+
import { postgresApply } from "../../op/activities/postgres-apply";
|
|
26
|
+
import { postgresMigrationPlan, type PostgresMigrationDeps } from "../../op/activities/postgres-migration";
|
|
27
|
+
import type { PostgresMigrationOpConfig } from "./op";
|
|
28
|
+
|
|
29
|
+
const enabled = await dockerAvailable();
|
|
30
|
+
let server: TestPostgres | undefined;
|
|
31
|
+
let admin: PostgresClient | undefined;
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-pg-partition-"));
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
if (!enabled) return;
|
|
36
|
+
server = await startTestPostgres();
|
|
37
|
+
admin = await server.connect();
|
|
38
|
+
}, 600_000);
|
|
39
|
+
|
|
40
|
+
afterAll(async () => {
|
|
41
|
+
await admin?.end();
|
|
42
|
+
await server?.stop();
|
|
43
|
+
rmSync(dir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
interface Obj {
|
|
47
|
+
export: string;
|
|
48
|
+
type: string;
|
|
49
|
+
ddl: string;
|
|
50
|
+
dependsOn?: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeBuild(file: string, objects: Obj[]): string {
|
|
54
|
+
writeFileSync(join(dir, file), JSON.stringify({ dialect: "postgres", postgresMajor: 18, applyOrder: objects.map((o) => o.export), objects: objects.map((o) => ({ dependsOn: [], ...o })) }));
|
|
55
|
+
return file;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MARKER = { stack: "e2e", env: "test" };
|
|
59
|
+
const profile = () => ({
|
|
60
|
+
config: { ownership: MARKER, sql: { profiles: { e2e: { url: server!.endpoint().url, password: { env: "PG_E2E_PASSWORD" } } } } },
|
|
61
|
+
env: { PG_E2E_PASSWORD: server!.endpoint().password },
|
|
62
|
+
});
|
|
63
|
+
const deps = (): PostgresMigrationDeps => ({ ...profile(), log: () => undefined });
|
|
64
|
+
const apply = (file: string) => postgresApply({ buildPath: join(dir, file), environment: "e2e" }, undefined, { ...profile(), log: () => undefined });
|
|
65
|
+
const plan = (file: string) => planPgAgainstServer("e2e", join(dir, file), profile());
|
|
66
|
+
const one = async <T>(sql: string): Promise<T> => (await admin!.query<T>(sql))[0]!;
|
|
67
|
+
const schemaObj = (name: string): Obj => ({ export: name, type: "Postgres::Schema", ddl: `CREATE SCHEMA ${name}` });
|
|
68
|
+
const table = (exportName: string, schema: string, ddl: string, dependsOn: string[] = []): Obj => ({ export: exportName, type: "Postgres::Table", dependsOn: [schema, ...dependsOn], ddl });
|
|
69
|
+
|
|
70
|
+
describe.skipIf(!enabled)("a plain and a partial unique index on a partitioned table's migrated column", () => {
|
|
71
|
+
const PI = schemaObj("pi");
|
|
72
|
+
const events = (t: string): Obj => table("events", "pi", `CREATE TABLE pi.events (id bigint, at date NOT NULL, amount ${t} NOT NULL, PRIMARY KEY (id, at)) PARTITION BY RANGE (at)`);
|
|
73
|
+
const part = (y: number): Obj => table(`events${y}`, "pi", `CREATE TABLE pi.events_${y} PARTITION OF pi.events FOR VALUES FROM ('${y}-01-01') TO ('${y + 1}-01-01')`, ["events"]);
|
|
74
|
+
const sub = table("events2027", "pi", "CREATE TABLE pi.events_2027 PARTITION OF pi.events FOR VALUES FROM ('2027-01-01') TO ('2028-01-01') PARTITION BY RANGE (at)", ["events"]);
|
|
75
|
+
const q1 = table("events2027q1", "pi", "CREATE TABLE pi.events_2027_q1 PARTITION OF pi.events_2027 FOR VALUES FROM ('2027-01-01') TO ('2027-04-01')", ["events2027"]);
|
|
76
|
+
const byAmount: Obj = { export: "eventsByAmount", type: "Postgres::Index", dependsOn: ["events"], ddl: "CREATE INDEX events_amount_idx ON pi.events (amount)" };
|
|
77
|
+
const uniq = (col: string): Obj => ({ export: "eventsUnique", type: "Postgres::Index", dependsOn: ["events"], ddl: `CREATE UNIQUE INDEX events_unique_idx ON pi.events (at, ${col}) WHERE id > 0` });
|
|
78
|
+
const objects = (t: string, col: string) => [PI, events(t), part(2025), part(2026), sub, q1, { ...byAmount, ddl: `CREATE INDEX events_amount_idx ON pi.events (${col})` }, uniq(col)];
|
|
79
|
+
|
|
80
|
+
test("each partition's index is built and attached, and the partitioned index takes its declared name", async () => {
|
|
81
|
+
await apply(writeBuild("pi-v1.json", objects("text", "amount")));
|
|
82
|
+
await admin!.query("INSERT INTO pi.events SELECT g, DATE '2025-01-01' + (g % 900), (g % 90) || '.5' FROM generate_series(1, 1500) g");
|
|
83
|
+
writeBuild("pi-v2.json", objects("numeric(10,2)", "amount"));
|
|
84
|
+
|
|
85
|
+
const ledger = new ApprovingLedger();
|
|
86
|
+
const config: PostgresMigrationOpConfig = { name: "migrate-events-amount", env: "e2e", table: "pi.events", column: "amount", build: false, path: dir, output: "pi-v2.json", retain: "0s", stack: MARKER.stack, ownershipEnv: MARKER.env, batchSize: 500 };
|
|
87
|
+
const statuses: string[] = [];
|
|
88
|
+
for (let i = 0; i < 4; i++) {
|
|
89
|
+
const r = await runMigrationOp(config, ledger.port, deps);
|
|
90
|
+
statuses.push(r.status);
|
|
91
|
+
if (i === 0) {
|
|
92
|
+
// Waiting at the gate: the partitioned working index is valid, every leaf partition has its own, and writes land in the new column.
|
|
93
|
+
expect(await one("SELECT indisvalid FROM pg_index WHERE indexrelid = 'pi.events_amount_idx__chant_new'::regclass")).toEqual({ indisvalid: true });
|
|
94
|
+
expect(await one("SELECT count(*)::int AS n FROM pg_index i JOIN pg_inherits h ON h.inhrelid = i.indexrelid WHERE h.inhparent = 'pi.events_amount_idx__chant_new'::regclass")).toEqual({ n: 3 });
|
|
95
|
+
await admin!.query("INSERT INTO pi.events VALUES (9000, DATE '2026-03-01', '2.25')");
|
|
96
|
+
expect(await one("SELECT amount__chant_new::text AS a FROM pi.events WHERE id = 9000")).toEqual({ a: "2.25" });
|
|
97
|
+
}
|
|
98
|
+
if (r.status !== "gated") break;
|
|
99
|
+
ledger.approveLast();
|
|
100
|
+
}
|
|
101
|
+
expect(statuses).toEqual(["gated", "gated", "ok"]);
|
|
102
|
+
expect(await one("SELECT i.indisvalid, c.relkind::text AS kind, pg_get_indexdef(c.oid) AS def FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid WHERE c.oid = 'pi.events_amount_idx'::regclass")).toEqual({
|
|
103
|
+
indisvalid: true,
|
|
104
|
+
kind: "I",
|
|
105
|
+
def: "CREATE INDEX events_amount_idx ON ONLY pi.events USING btree (amount)",
|
|
106
|
+
});
|
|
107
|
+
expect(await one("SELECT count(*)::int AS n FROM pg_class WHERE relnamespace = 'pi'::regnamespace AND relname LIKE '%\\_\\_chant\\_old'")).toEqual({ n: 0 });
|
|
108
|
+
// The query planner uses a partition's index on the new column.
|
|
109
|
+
await admin!.query("SET enable_seqscan = off");
|
|
110
|
+
expect(JSON.stringify(await admin!.query("EXPLAIN SELECT * FROM pi.events_2025 WHERE amount = 2.5"))).toContain("Index");
|
|
111
|
+
expect((await plan("pi-v2.json")).changes).toEqual([]);
|
|
112
|
+
}, 300_000);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe.skipIf(!enabled)("a key on a partitioned table's migrated column", () => {
|
|
116
|
+
const PK = schemaObj("pk");
|
|
117
|
+
const events = (t: string): Obj => table("events", "pk", `CREATE TABLE pk.events (id bigint, at date NOT NULL, amount ${t} NOT NULL, PRIMARY KEY (id, at, amount)) PARTITION BY RANGE (at)`);
|
|
118
|
+
const part: Obj = table("events2025", "pk", "CREATE TABLE pk.events_2025 PARTITION OF pk.events FOR VALUES FROM ('2025-01-01') TO ('2026-01-01')", ["events"]);
|
|
119
|
+
|
|
120
|
+
test("stays refused, naming why", async () => {
|
|
121
|
+
await apply(writeBuild("pk-v1.json", [PK, events("text"), part]));
|
|
122
|
+
writeBuild("pk-v2.json", [PK, events("numeric(10,2)"), part]);
|
|
123
|
+
const message = await postgresMigrationPlan({ table: "pk.events", column: "amount", buildPath: "pk-v2.json", environment: "e2e", stack: MARKER.stack, ownershipEnv: MARKER.env, cwd: dir }, undefined, deps()).then(
|
|
124
|
+
() => "",
|
|
125
|
+
(e: unknown) => String((e as Error).message),
|
|
126
|
+
);
|
|
127
|
+
expect(message).toMatch(/primary key events_pkey on pk\.events: on a partitioned table a key cannot be attached from an index built CONCURRENTLY/);
|
|
128
|
+
}, 300_000);
|
|
129
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An index on a partitioned table's migrated column (#3333), carried over
|
|
3
|
+
* per partition: the `ON ONLY` statement, each partition's `CONCURRENTLY`
|
|
4
|
+
* build, the attach, and a run that goes on from where the last stopped.
|
|
5
|
+
* The e2e is `partition.e2e.test.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import type { PostgresClient } from "../live/client";
|
|
10
|
+
import { carriedPartitionedIndexStatement, discoverDependents, partitionIndexName, partitionIndexStatement, type CarriedObject } from "./carry";
|
|
11
|
+
import { migrationNames } from "./names";
|
|
12
|
+
import { carryPartitionedIndex, type MigrationRun } from "./steps";
|
|
13
|
+
|
|
14
|
+
const DEF = "CREATE INDEX events_amount_idx ON ONLY app.events USING btree (amount)";
|
|
15
|
+
|
|
16
|
+
describe("the statements", () => {
|
|
17
|
+
test("a partitioned index is made again ON ONLY on the new column, without CONCURRENTLY", () => {
|
|
18
|
+
expect(carriedPartitionedIndexStatement(DEF, "events_amount_idx__chant_new", "amount", "amount__chant_new")).toBe("CREATE INDEX events_amount_idx__chant_new ON ONLY app.events USING btree (amount__chant_new)");
|
|
19
|
+
expect(carriedPartitionedIndexStatement("CREATE UNIQUE INDEX e ON ONLY app.events USING btree (id, amount) WHERE (amount > 0)", "e__chant_new", "amount", "login")).toBe(
|
|
20
|
+
"CREATE UNIQUE INDEX e__chant_new ON ONLY app.events USING btree (id, login) WHERE (login > 0)",
|
|
21
|
+
);
|
|
22
|
+
// Without ONLY in the definition, the statement still has it.
|
|
23
|
+
expect(carriedPartitionedIndexStatement("CREATE INDEX e ON app.events USING btree (amount)", "w", "amount", "n")).toBe("CREATE INDEX w ON ONLY app.events USING btree (n)");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a partition that holds rows gets a CONCURRENTLY build, one that is partitioned further gets ON ONLY", () => {
|
|
27
|
+
const c = { definition: "CREATE UNIQUE INDEX events_amount_idx__chant_new ON ONLY app.events USING btree (id, amount__chant_new) WHERE (amount__chant_new > 0)" };
|
|
28
|
+
expect(partitionIndexStatement(c, { ident: "app.events_2025", leaf: true }, "events_2025_events_amount_idx__chant_new")).toBe(
|
|
29
|
+
"CREATE UNIQUE INDEX CONCURRENTLY events_2025_events_amount_idx__chant_new ON app.events_2025 USING btree (id, amount__chant_new) WHERE (amount__chant_new > 0)",
|
|
30
|
+
);
|
|
31
|
+
expect(partitionIndexStatement(c, { ident: "app.events_2026", leaf: false }, "events_2026_x")).toBe(
|
|
32
|
+
"CREATE UNIQUE INDEX events_2026_x ON ONLY app.events_2026 USING btree (id, amount__chant_new) WHERE (amount__chant_new > 0)",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the partition's index name keeps within what Postgres keeps", () => {
|
|
37
|
+
expect(partitionIndexName({ name: "events_2025" }, "events_amount_idx__chant_new")).toBe("events_2025_events_amount_idx__chant_new");
|
|
38
|
+
expect(partitionIndexName({ name: "p".repeat(40) }, "i".repeat(40))).toHaveLength(63);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const names = migrationNames("app", "events", "amount", "type", "amount");
|
|
43
|
+
const c: CarriedObject = {
|
|
44
|
+
kind: "index",
|
|
45
|
+
name: "events_amount_idx",
|
|
46
|
+
table: "app.events",
|
|
47
|
+
tableName: "app.events",
|
|
48
|
+
tableOid: "100",
|
|
49
|
+
schema: "app",
|
|
50
|
+
working: "events_amount_idx__chant_new",
|
|
51
|
+
target: "events_amount_idx",
|
|
52
|
+
definition: "CREATE INDEX events_amount_idx__chant_new ON ONLY app.events USING btree (amount__chant_new)",
|
|
53
|
+
validated: true,
|
|
54
|
+
partitioned: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface Node {
|
|
58
|
+
oid: string;
|
|
59
|
+
name: string;
|
|
60
|
+
parent?: string;
|
|
61
|
+
leaf: boolean;
|
|
62
|
+
level: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A fake server for one partitioned index: the tree it has, and what of the working index exists already. */
|
|
66
|
+
function server(tree: Node[], have: { root?: boolean; attached?: string[]; standalone?: Record<string, { valid: boolean; comment: string | null }> } = {}) {
|
|
67
|
+
const statements: string[] = [];
|
|
68
|
+
const indexOid = (name: string) => `i-${name}`;
|
|
69
|
+
const query = async (sql: string, params: unknown[] = []): Promise<Record<string, unknown>[]> => {
|
|
70
|
+
if (/^(CREATE|ALTER INDEX|COMMENT|DROP)/.test(sql)) statements.push(sql);
|
|
71
|
+
if (sql.includes("pg_partition_tree")) return tree.map((n) => ({ oid: n.oid, schema: "app", name: n.name, parent: n.parent ?? null, leaf: n.leaf, kind: n.leaf ? "r" : "p", level: n.level }));
|
|
72
|
+
if (sql.includes("AS oid") && sql.includes("to_regclass")) {
|
|
73
|
+
const ident = String(params[0]);
|
|
74
|
+
if (ident === "app.events_amount_idx__chant_new") return [{ oid: have.root ? "root" : null }];
|
|
75
|
+
return [{ oid: null }];
|
|
76
|
+
}
|
|
77
|
+
if (sql.includes("FROM pg_catalog.pg_inherits h")) {
|
|
78
|
+
const parent = String(params[0]);
|
|
79
|
+
const partition = String(params[1]);
|
|
80
|
+
const hit = tree.find((n) => n.oid === partition);
|
|
81
|
+
if (parent === "root" && hit && have.attached?.includes(hit.name)) return [{ oid: indexOid(hit.name), schema: "app", name: `${hit.name}_auto_idx` }];
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
if (sql.includes("indisvalid AS valid")) {
|
|
85
|
+
const ident = String(params[0]);
|
|
86
|
+
const name = /\.(.+)$/.exec(ident)![1]!;
|
|
87
|
+
const s = have.standalone?.[name];
|
|
88
|
+
return s ? [{ valid: s.valid, comment: s.comment }] : [];
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
};
|
|
92
|
+
const run = { client: { query } as unknown as PostgresClient, target: { defaultSchema: "app" }, lockTimeoutMs: 1000, statementTimeoutMs: 1000, log: () => undefined } as unknown as MigrationRun;
|
|
93
|
+
return { run, statements: () => statements };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const TREE: Node[] = [
|
|
97
|
+
{ oid: "100", name: "events", leaf: false, level: 0 },
|
|
98
|
+
{ oid: "101", name: "events_2025", parent: "100", leaf: true, level: 1 },
|
|
99
|
+
{ oid: "102", name: "events_2026", parent: "100", leaf: true, level: 1 },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
describe("carrying a partitioned index over", () => {
|
|
103
|
+
test("ON ONLY first, each partition's index built CONCURRENTLY, then each attached", async () => {
|
|
104
|
+
const s = server(TREE);
|
|
105
|
+
expect(await carryPartitionedIndex(s.run, names, c, "the comment", undefined)).toBe(true);
|
|
106
|
+
const sql = s.statements();
|
|
107
|
+
expect(sql[0]).toBe(c.definition);
|
|
108
|
+
expect(sql[1]).toMatch(/^COMMENT ON INDEX app\.events_amount_idx__chant_new IS /);
|
|
109
|
+
expect(sql.filter((x) => x.startsWith("CREATE"))).toEqual([
|
|
110
|
+
c.definition,
|
|
111
|
+
"CREATE INDEX CONCURRENTLY events_2025_events_amount_idx__chant_new ON app.events_2025 USING btree (amount__chant_new)",
|
|
112
|
+
"CREATE INDEX CONCURRENTLY events_2026_events_amount_idx__chant_new ON app.events_2026 USING btree (amount__chant_new)",
|
|
113
|
+
]);
|
|
114
|
+
const attaches = sql.filter((x) => x.startsWith("ALTER INDEX"));
|
|
115
|
+
expect(attaches).toEqual([
|
|
116
|
+
"ALTER INDEX app.events_amount_idx__chant_new ATTACH PARTITION app.events_2025_events_amount_idx__chant_new",
|
|
117
|
+
"ALTER INDEX app.events_amount_idx__chant_new ATTACH PARTITION app.events_2026_events_amount_idx__chant_new",
|
|
118
|
+
]);
|
|
119
|
+
// Every attach comes after every build, and each partition's index carries the migration's trailer.
|
|
120
|
+
expect(sql.lastIndexOf(sql.filter((x) => x.startsWith("CREATE")).at(-1)!)).toBeLessThan(sql.indexOf(attaches[0]!));
|
|
121
|
+
expect(sql.filter((x) => x.startsWith("COMMENT ON INDEX app.events_2025_")).join()).toContain("migration=app.events.amount role=carry");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a run after a crash keeps the partitions already attached, drops an invalid build and goes on", async () => {
|
|
125
|
+
const s = server(TREE, { root: true, attached: ["events_2025"], standalone: { events_2026_events_amount_idx__chant_new: { valid: false, comment: null } } });
|
|
126
|
+
const state = { present: true, ready: false, marked: true };
|
|
127
|
+
await carryPartitionedIndex(s.run, names, c, "the comment", state);
|
|
128
|
+
const sql = s.statements();
|
|
129
|
+
expect(sql.some((x) => x.includes("events_2025"))).toBe(false);
|
|
130
|
+
expect(sql.filter((x) => !x.startsWith("COMMENT"))).toEqual([
|
|
131
|
+
"DROP INDEX CONCURRENTLY IF EXISTS app.events_2026_events_amount_idx__chant_new",
|
|
132
|
+
"CREATE INDEX CONCURRENTLY events_2026_events_amount_idx__chant_new ON app.events_2026 USING btree (amount__chant_new)",
|
|
133
|
+
"ALTER INDEX app.events_amount_idx__chant_new ATTACH PARTITION app.events_2026_events_amount_idx__chant_new",
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a valid partitioned index that is marked is left alone", async () => {
|
|
138
|
+
const s = server(TREE, { root: true });
|
|
139
|
+
expect(await carryPartitionedIndex(s.run, names, c, "the comment", { present: true, ready: true, marked: true })).toBe(false);
|
|
140
|
+
expect(s.statements()).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a partition that is partitioned further gets ON ONLY, and is attached after its own partitions", async () => {
|
|
144
|
+
const tree: Node[] = [
|
|
145
|
+
{ oid: "100", name: "events", leaf: false, level: 0 },
|
|
146
|
+
{ oid: "101", name: "events_2025", parent: "100", leaf: false, level: 1 },
|
|
147
|
+
{ oid: "103", name: "events_2025_q1", parent: "101", leaf: true, level: 2 },
|
|
148
|
+
];
|
|
149
|
+
const s = server(tree);
|
|
150
|
+
await carryPartitionedIndex(s.run, names, c, "the comment", undefined);
|
|
151
|
+
const sql = s.statements().filter((x) => !x.startsWith("COMMENT"));
|
|
152
|
+
expect(sql).toEqual([
|
|
153
|
+
c.definition,
|
|
154
|
+
"CREATE INDEX events_2025_events_amount_idx__chant_new ON ONLY app.events_2025 USING btree (amount__chant_new)",
|
|
155
|
+
"CREATE INDEX CONCURRENTLY events_2025_q1_events_amount_idx__chant_new ON app.events_2025_q1 USING btree (amount__chant_new)",
|
|
156
|
+
"ALTER INDEX app.events_2025_events_amount_idx__chant_new ATTACH PARTITION app.events_2025_q1_events_amount_idx__chant_new",
|
|
157
|
+
"ALTER INDEX app.events_amount_idx__chant_new ATTACH PARTITION app.events_2025_events_amount_idx__chant_new",
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("finding a partitioned index on the column", () => {
|
|
163
|
+
const find = async (tree: Array<{ name: string; kind: string }>, row: Record<string, unknown> = {}) => {
|
|
164
|
+
const query = async (sql: string): Promise<Record<string, unknown>[]> => {
|
|
165
|
+
if (sql.includes("pg_catalog.pg_depend d") && sql.includes("UNION")) return [{ kind: "class", objid: "9", objsubid: 0, what: "index", deptype: "n" }];
|
|
166
|
+
if (sql.includes("AS has_exprs")) {
|
|
167
|
+
return [{ oid: "9", name: "events_amount_idx", kind: "I", schema: "app", comment: null, def: DEF, valid: true, is_unique: false, replident: false, clustered: false, cols: ["amount"], has_exprs: false, ...row }];
|
|
168
|
+
}
|
|
169
|
+
if (sql.includes("pg_partition_tree")) return tree.map((t, i) => ({ oid: String(100 + i), schema: "app", name: t.name, parent: i === 0 ? null : "100", leaf: i > 0, kind: t.kind, level: i === 0 ? 0 : 1 }));
|
|
170
|
+
return [];
|
|
171
|
+
};
|
|
172
|
+
return discoverDependents({
|
|
173
|
+
client: { query } as unknown as PostgresClient,
|
|
174
|
+
names: migrationNames("app", "events", "amount", "type", "amount"),
|
|
175
|
+
oid: "100",
|
|
176
|
+
attnum: 3,
|
|
177
|
+
objects: [],
|
|
178
|
+
declared: { props: {}, canonical: { schema: "app", name: "events" } } as never,
|
|
179
|
+
defaultSchema: "app",
|
|
180
|
+
partitioned: true,
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
test("it is carried over, as a partitioned index", async () => {
|
|
185
|
+
const d = await find([{ name: "events", kind: "p" }, { name: "events_2025", kind: "r" }]);
|
|
186
|
+
expect(d.refused).toEqual([]);
|
|
187
|
+
expect(d.carried).toHaveLength(1);
|
|
188
|
+
expect(d.carried[0]).toMatchObject({
|
|
189
|
+
kind: "index",
|
|
190
|
+
partitioned: true,
|
|
191
|
+
working: "events_amount_idx__chant_new",
|
|
192
|
+
definition: "CREATE INDEX events_amount_idx__chant_new ON ONLY app.events USING btree (amount__chant_new)",
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a foreign table among the partitions refuses it, naming the partition", async () => {
|
|
197
|
+
const d = await find([{ name: "events", kind: "p" }, { name: "events_remote", kind: "f" }]);
|
|
198
|
+
expect(d.carried).toEqual([]);
|
|
199
|
+
expect(d.refused).toEqual([expect.stringMatching(/partitioned index app\.events_amount_idx: app\.events_remote is a foreign table/)]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("an INVALID one (an ON ONLY never finished) still refuses", async () => {
|
|
203
|
+
const d = await find([{ name: "events", kind: "p" }], { valid: false });
|
|
204
|
+
expect(d.refused).toEqual([expect.stringMatching(/index app\.events_amount_idx is INVALID/)]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -34,6 +34,32 @@
|
|
|
34
34
|
* publication sends the migrated column of, naming each publication. A
|
|
35
35
|
* publication with a column list (15 and later) that leaves the column out
|
|
36
36
|
* sends neither it nor the new one, and does not stop the migration.
|
|
37
|
+
*
|
|
38
|
+
* ## Subscriptions (#4405)
|
|
39
|
+
*
|
|
40
|
+
* The other side: a table that is the target of a subscription. The
|
|
41
|
+
* subscription's apply worker runs with `session_replication_role = replica`
|
|
42
|
+
* ("Logical Replication", Architecture,
|
|
43
|
+
* https://www.postgresql.org/docs/18/logical-replication-architecture.html),
|
|
44
|
+
* and a trigger made by plain `CREATE TRIGGER` is `ENABLE ORIGIN`: it fires
|
|
45
|
+
* only while the setting is `origin` or `local`
|
|
46
|
+
* (https://www.postgresql.org/docs/18/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE).
|
|
47
|
+
* Only `ENABLE REPLICA` and `ENABLE ALWAYS` triggers fire for the worker
|
|
48
|
+
* (https://www.postgresql.org/docs/18/sql-altertable.html, the TRIGGER
|
|
49
|
+
* clauses). The architecture page says "by default, triggers and rules will
|
|
50
|
+
* not fire on a subscriber" and points to `ENABLE TRIGGER` to change it. So the dual write
|
|
51
|
+
* never sees a replicated row, and the new column goes stale. The initial
|
|
52
|
+
* table copy is the exception: it is a `COPY` and fires plain triggers.
|
|
53
|
+
*
|
|
54
|
+
* `ENABLE ALWAYS` would make the trigger fire, and the Plan phase refuses
|
|
55
|
+
* the table instead, because the Op's later steps assume it controls every
|
|
56
|
+
* writer of the old column. The publisher keeps sending the old column's name
|
|
57
|
+
* and the subscriber applies by name, so a rename's contract (which drops the
|
|
58
|
+
* old column) would stop the apply at the next row, and the switch of a
|
|
59
|
+
* type change would change the column under a running apply worker. Neither
|
|
60
|
+
* order can be made inside this Op's one connection to one server; that is
|
|
61
|
+
* the same limit #3332 recorded for the publisher side. The refusal names
|
|
62
|
+
* each subscription.
|
|
37
63
|
*/
|
|
38
64
|
|
|
39
65
|
import type { PostgresClient } from "../live/client";
|
|
@@ -149,3 +175,43 @@ function sleepFor(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
149
175
|
);
|
|
150
176
|
});
|
|
151
177
|
}
|
|
178
|
+
|
|
179
|
+
/** One subscription that has the table, or a partition of it, as a target. */
|
|
180
|
+
export interface SubscriptionHit {
|
|
181
|
+
name: string;
|
|
182
|
+
enabled: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The subscriptions of the current database with a row in `pg_subscription_rel`
|
|
187
|
+
* for `schema.table` (and, on a partitioned table, for any of its
|
|
188
|
+
* partitions, which is where a subscription to a partitioned table lists the
|
|
189
|
+
* tables it applies to). Read at plan time; `pg_subscription_rel` is
|
|
190
|
+
* per database and `pg_subscription` is shared, so the join keeps this
|
|
191
|
+
* database's rows (`subdbid`).
|
|
192
|
+
*/
|
|
193
|
+
export async function subscriptionsOf(client: Pick<PostgresClient, "query">, schema: string, table: string, major: number): Promise<SubscriptionHit[]> {
|
|
194
|
+
const rel = "pg_catalog.to_regclass(pg_catalog.quote_ident($1) || '.' || pg_catalog.quote_ident($2))";
|
|
195
|
+
const tables = major >= 12 ? `SELECT relid FROM pg_catalog.pg_partition_tree(${rel})` : `SELECT ${rel}::oid AS relid`;
|
|
196
|
+
const rows = await client.query<{ subname: string; subenabled: boolean }>(
|
|
197
|
+
`SELECT DISTINCT s.subname, s.subenabled
|
|
198
|
+
FROM pg_catalog.pg_subscription_rel sr
|
|
199
|
+
JOIN pg_catalog.pg_subscription s ON s.oid = sr.srsubid
|
|
200
|
+
WHERE s.subdbid = (SELECT oid FROM pg_catalog.pg_database WHERE datname = pg_catalog.current_database())
|
|
201
|
+
AND sr.srrelid IN (${tables})
|
|
202
|
+
ORDER BY s.subname`,
|
|
203
|
+
[schema, table],
|
|
204
|
+
);
|
|
205
|
+
return rows.map((r) => ({ name: r.subname, enabled: r.subenabled }));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The Plan phase's refusal for a table that a subscription applies to. */
|
|
209
|
+
export function subscriptionRefusal(where: string, column: string, hits: SubscriptionHit[]): string {
|
|
210
|
+
return (
|
|
211
|
+
`${where} is a target of logical replication subscription(s) ${hits.map((h) => `${h.name}${h.enabled ? "" : " (disabled)"}`).join(", ")}. ` +
|
|
212
|
+
`A subscription's apply worker runs with session_replication_role = replica, where a trigger made by CREATE TRIGGER does not fire, so the dual write would never see a replicated row and the new column of ${column} would go stale ` +
|
|
213
|
+
`(https://www.postgresql.org/docs/18/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE). ` +
|
|
214
|
+
`ENABLE ALWAYS would fire it, but the publisher keeps sending the old column's name and the subscriber applies by name, so the contract (or the switch of a type change) would stop the apply, and one Op cannot change both servers. ` +
|
|
215
|
+
`Take the table out of the subscription (drop it from the publication and ALTER SUBSCRIPTION ... REFRESH PUBLICATION), or migrate the publisher and the subscriber by hand. A migration of both together is chant #3332.`
|
|
216
|
+
);
|
|
217
|
+
}
|