@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
|
@@ -25,7 +25,7 @@ import { col, migrationNames, MIGRATION_TRAILER_KEY, type MigrationNames } from
|
|
|
25
25
|
import { MigrationRefusal, observeMigration, type MigrationObservation } from "./observe";
|
|
26
26
|
import { postgresReceiptStore, receiptAddress } from "./receipts";
|
|
27
27
|
import type { ReplicationLagBound } from "./replication";
|
|
28
|
-
import { addCarriedConstraint, carriedNotReady, carriedSwitchStatements, requiresNotNull, SERIAL_TYPES, storedType, type CarriedObject } from "./carry";
|
|
28
|
+
import { addCarriedConstraint, carriedNotReady, carriedSwitchStatements, partitionIndexName, partitionIndexStatement, partitionTree, requiresNotNull, SERIAL_TYPES, storedType, type CarriedObject, type CarriedState } from "./carry";
|
|
29
29
|
|
|
30
30
|
/** What every step is given. */
|
|
31
31
|
export interface MigrationRun {
|
|
@@ -39,6 +39,10 @@ export interface MigrationRun {
|
|
|
39
39
|
column: string;
|
|
40
40
|
/** The type change's expression over the old row's columns. */
|
|
41
41
|
using?: string;
|
|
42
|
+
/** A rename with a type change: the expression that gives the old column's value from the new column's. */
|
|
43
|
+
reverse?: string;
|
|
44
|
+
/** A rename of a column with a default, serial or identity: the declared default lands on the new column at the contract. */
|
|
45
|
+
defaultAtContract?: boolean;
|
|
42
46
|
/** This project's ownership marker, stamped on every working object. */
|
|
43
47
|
marker?: OwnershipMarker;
|
|
44
48
|
/** The major the build targets. */
|
|
@@ -70,6 +74,8 @@ export const observe = (run: MigrationRun): Promise<MigrationObservation> =>
|
|
|
70
74
|
...(run.objects ? { objects: run.objects } : {}),
|
|
71
75
|
...(run.marker ? { marker: run.marker } : {}),
|
|
72
76
|
...(run.using !== undefined ? { using: run.using } : {}),
|
|
77
|
+
...(run.reverse !== undefined ? { reverse: run.reverse } : {}),
|
|
78
|
+
...(run.defaultAtContract ? { defaultAtContract: true } : {}),
|
|
73
79
|
...(run.major !== undefined ? { major: run.major } : {}),
|
|
74
80
|
});
|
|
75
81
|
|
|
@@ -174,7 +180,7 @@ export interface ExpandResult {
|
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
/** The new column's type: the declared one, with its collation when it declares one. */
|
|
177
|
-
const newColumnType = (o: MigrationObservation): string => `${o.names.change === "rename" ? o.source!.type : storedType(o.column.type)}${o.column.collate ? ` COLLATE ${o.column.collate}` : ""}`;
|
|
183
|
+
const newColumnType = (o: MigrationObservation): string => `${o.names.change === "rename" && o.reverse === undefined ? o.source!.type : storedType(o.column.type)}${o.column.collate ? ` COLLATE ${o.column.collate}` : ""}`;
|
|
178
184
|
|
|
179
185
|
/**
|
|
180
186
|
* Expand: add the new column, nullable and with no default, which changes
|
|
@@ -211,10 +217,27 @@ export async function expand(run: MigrationRun): Promise<ExpandResult> {
|
|
|
211
217
|
* An insert takes the new column's value when it is given and the old one's
|
|
212
218
|
* otherwise; an update takes whichever of the two it changed.
|
|
213
219
|
*/
|
|
214
|
-
export function dualWriteBody(o: Pick<MigrationObservation, "names" | "expression">): string {
|
|
220
|
+
export function dualWriteBody(o: Pick<MigrationObservation, "names" | "expression"> & { reverse?: string }): string {
|
|
215
221
|
const n = o.names;
|
|
216
222
|
const nw = `NEW.${col(n.newColumn)}`;
|
|
217
223
|
const src = `NEW.${col(n.source)}`;
|
|
224
|
+
if (n.change === "rename" && o.reverse !== undefined) {
|
|
225
|
+
// A rename with a type change: a writer of the old name has `expression` applied, a writer of the new name `reverse`, each over the row as it is.
|
|
226
|
+
const fwd = `(SELECT ${o.expression} FROM (SELECT NEW.*) AS ${col(n.table)})`;
|
|
227
|
+
const back = `(SELECT ${o.reverse} FROM (SELECT NEW.*) AS ${col(n.table)})`;
|
|
228
|
+
return [
|
|
229
|
+
`BEGIN`,
|
|
230
|
+
` IF TG_OP = 'INSERT' THEN`,
|
|
231
|
+
` IF ${nw} IS NULL THEN ${nw} := ${fwd}; ELSE ${src} := ${back}; END IF;`,
|
|
232
|
+
` ELSIF ${nw} IS DISTINCT FROM OLD.${col(n.newColumn)} THEN`,
|
|
233
|
+
` ${src} := ${back};`,
|
|
234
|
+
` ELSE`,
|
|
235
|
+
` ${nw} := ${fwd};`,
|
|
236
|
+
` END IF;`,
|
|
237
|
+
` RETURN NEW;`,
|
|
238
|
+
`END`,
|
|
239
|
+
].join("\n");
|
|
240
|
+
}
|
|
218
241
|
if (n.change === "type") {
|
|
219
242
|
return [`BEGIN`, ` ${nw} := (SELECT ${o.expression} FROM (SELECT NEW.*) AS ${col(n.table)});`, ` RETURN NEW;`, `END`].join("\n");
|
|
220
243
|
}
|
|
@@ -233,7 +256,7 @@ export function dualWriteBody(o: Pick<MigrationObservation, "names" | "expressio
|
|
|
233
256
|
}
|
|
234
257
|
|
|
235
258
|
/** The statements that start the dual write: the function, the trigger, their comments. */
|
|
236
|
-
export function dualWriteStatements(o: Pick<MigrationObservation, "names" | "expression"
|
|
259
|
+
export function dualWriteStatements(o: Pick<MigrationObservation, "names" | "expression"> & { reverse?: string }, marker: OwnershipMarker | undefined, defaultSchema: string): string[] {
|
|
237
260
|
const n = o.names;
|
|
238
261
|
return [
|
|
239
262
|
`CREATE OR REPLACE FUNCTION ${n.qualifiedFn}() RETURNS trigger LANGUAGE plpgsql SET search_path = ${quoteIdent(defaultSchema)} AS $chant$\n${dualWriteBody(o)}\n$chant$`,
|
|
@@ -304,6 +327,11 @@ export async function carryOver(run: MigrationRun): Promise<CarryResult> {
|
|
|
304
327
|
result.carried.push(`${c.kind} ${c.tableName}.${c.name} -> ${c.target}`);
|
|
305
328
|
const state = o.carriedStates.get(c.working);
|
|
306
329
|
const comment = workingComment(n, run.marker, "carry", `${c.kind} ${c.name} on the new column`, { of: c.name });
|
|
330
|
+
if (c.kind === "index" && c.partitioned) {
|
|
331
|
+
if (await carryPartitionedIndex(run, n, c, comment, state)) result.built++;
|
|
332
|
+
else result.ready++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
307
335
|
if (c.kind === "index" || c.kind === "key") {
|
|
308
336
|
const ident = qualifiedIndex(c, c.working);
|
|
309
337
|
if (state?.ready) {
|
|
@@ -340,6 +368,85 @@ export async function carryOver(run: MigrationRun): Promise<CarryResult> {
|
|
|
340
368
|
return result;
|
|
341
369
|
}
|
|
342
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Carry over one index of a partitioned table (#3333). A partitioned index
|
|
373
|
+
* cannot be built `CONCURRENTLY`, so it is made the way the Postgres docs
|
|
374
|
+
* give for adding one without blocking writes: the partitioned index
|
|
375
|
+
* `ON ONLY` the table (nothing is built, and it is INVALID), each leaf
|
|
376
|
+
* partition's index built `CONCURRENTLY` on its own, then
|
|
377
|
+
* `ALTER INDEX ... ATTACH PARTITION` for each, deepest first. The partitioned
|
|
378
|
+
* index becomes valid when its last partition is attached. A partition
|
|
379
|
+
* attached to the table meanwhile gets its index from Postgres, and one that
|
|
380
|
+
* already has an index attached to this one is left alone, so a run after a
|
|
381
|
+
* crash goes on from where the last stopped. Returns whether anything was
|
|
382
|
+
* made.
|
|
383
|
+
*/
|
|
384
|
+
export async function carryPartitionedIndex(run: MigrationRun, n: MigrationNames, c: CarriedObject, comment: string, state: CarriedState | undefined): Promise<boolean> {
|
|
385
|
+
if (state?.ready && state.marked) return false;
|
|
386
|
+
const root = qualifiedIndex(c, c.working);
|
|
387
|
+
const one = async <T>(sql: string, params: unknown[]): Promise<T | undefined> => (await run.client.query<T>(sql, params))[0];
|
|
388
|
+
const oidOf = async (ident: string): Promise<string | undefined> => (await one<{ oid: string | null }>("SELECT pg_catalog.to_regclass($1)::oid::text AS oid", [ident]))?.oid ?? undefined;
|
|
389
|
+
/** The index on `partitionOid` that is attached to `parentIndexOid`, whoever made it. */
|
|
390
|
+
const attachedIndex = async (parentIndexOid: string, partitionOid: string): Promise<{ oid: string; ident: string } | undefined> => {
|
|
391
|
+
const row = await one<{ oid: string; schema: string; name: string }>(
|
|
392
|
+
`SELECT x.oid::text AS oid, s.nspname AS schema, x.relname AS name FROM pg_catalog.pg_inherits h JOIN pg_catalog.pg_index i ON i.indexrelid = h.inhrelid
|
|
393
|
+
JOIN pg_catalog.pg_class x ON x.oid = i.indexrelid JOIN pg_catalog.pg_namespace s ON s.oid = x.relnamespace WHERE h.inhparent = $1::oid AND i.indrelid = $2::oid`,
|
|
394
|
+
[parentIndexOid, partitionOid],
|
|
395
|
+
);
|
|
396
|
+
return row ? { oid: String(row.oid), ident: `${quoteIdent(row.schema)}.${quoteIdent(row.name)}` } : undefined;
|
|
397
|
+
};
|
|
398
|
+
if (!state?.present) {
|
|
399
|
+
await inTransaction(run, async (exec) => {
|
|
400
|
+
await exec(c.definition);
|
|
401
|
+
await exec(`COMMENT ON INDEX ${root} IS ${pgString(comment)}`);
|
|
402
|
+
});
|
|
403
|
+
} else if (!state.marked) {
|
|
404
|
+
await inTransaction(run, (exec) => exec(`COMMENT ON INDEX ${root} IS ${pgString(comment)}`));
|
|
405
|
+
}
|
|
406
|
+
const tree = (await partitionTree(run.client, c.tableOid)).filter((p) => p.oid !== c.tableOid);
|
|
407
|
+
const byOid = new Map(tree.map((p) => [p.oid, p]));
|
|
408
|
+
const ours = (p: { schema: string; name: string }) => `${quoteIdent(p.schema)}.${quoteIdent(partitionIndexName(p, c.working))}`;
|
|
409
|
+
const rootIndex = { oid: (await oidOf(root))!, ident: root };
|
|
410
|
+
/** The index a partition's index attaches to: the partitioned index above it, as it is now. */
|
|
411
|
+
const above = async (p: { parent?: string }): Promise<{ oid: string; ident: string }> => {
|
|
412
|
+
if (p.parent === c.tableOid || p.parent === undefined) return rootIndex;
|
|
413
|
+
const parent = byOid.get(p.parent)!;
|
|
414
|
+
const up = await above(parent);
|
|
415
|
+
return (await attachedIndex(up.oid, parent.oid)) ?? { oid: (await oidOf(ours(parent)))!, ident: ours(parent) };
|
|
416
|
+
};
|
|
417
|
+
// Top down: each partition without an index attached to its parent's gets a standalone one.
|
|
418
|
+
const pending: Array<{ p: (typeof tree)[number]; parent: { oid: string; ident: string } }> = [];
|
|
419
|
+
for (const p of tree) {
|
|
420
|
+
const parent = await above(p);
|
|
421
|
+
if (await attachedIndex(parent.oid, p.oid)) continue;
|
|
422
|
+
const ident = ours(p);
|
|
423
|
+
const cur = await one<{ valid: boolean; comment: string | null }>(
|
|
424
|
+
"SELECT i.indisvalid AS valid, pg_catalog.obj_description(i.indexrelid, 'pg_class') AS comment FROM pg_catalog.pg_index i WHERE i.indexrelid = pg_catalog.to_regclass($1)",
|
|
425
|
+
[ident],
|
|
426
|
+
);
|
|
427
|
+
if (cur && !cur.valid) await outsideTransaction(run, `DROP INDEX CONCURRENTLY IF EXISTS ${ident}`);
|
|
428
|
+
if (!cur || !cur.valid) {
|
|
429
|
+
const statement = partitionIndexStatement(c, p, partitionIndexName(p, c.working));
|
|
430
|
+
const dropInvalid = async () => {
|
|
431
|
+
const row = await one<{ invalid: boolean }>("SELECT NOT i.indisvalid AS invalid FROM pg_catalog.pg_index i WHERE i.indexrelid = pg_catalog.to_regclass($1)", [ident]);
|
|
432
|
+
if (row?.invalid) await run.client.query(`DROP INDEX CONCURRENTLY IF EXISTS ${ident}`);
|
|
433
|
+
};
|
|
434
|
+
if (p.leaf) await outsideTransaction(run, statement, { scan: true, cleanup: dropInvalid });
|
|
435
|
+
else await inTransaction(run, (exec) => exec(statement));
|
|
436
|
+
}
|
|
437
|
+
if (!cur || !cur.valid || cur.comment == null) {
|
|
438
|
+
const partitionComment = workingComment(n, run.marker, "carry", `${c.kind} ${c.name} on the new column, for partition ${p.name}`, { of: c.name });
|
|
439
|
+
await inTransaction(run, (exec) => exec(`COMMENT ON INDEX ${ident} IS ${pgString(partitionComment)}`));
|
|
440
|
+
}
|
|
441
|
+
pending.push({ p, parent });
|
|
442
|
+
}
|
|
443
|
+
// Bottom up: a partition's index is valid once its own partitions are attached.
|
|
444
|
+
for (const { p, parent } of [...pending].sort((a, b) => b.p.level - a.p.level)) {
|
|
445
|
+
await inTransaction(run, (exec) => exec(`ALTER INDEX ${parent.ident} ATTACH PARTITION ${ours(p)}`));
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
343
450
|
// ── switch ─────────────────────────────────────────────────────────────
|
|
344
451
|
|
|
345
452
|
export interface SwitchResult {
|
|
@@ -373,13 +480,17 @@ export interface SequenceState {
|
|
|
373
480
|
* `serial` column's sequence is handed over with `OWNED BY`, takes the
|
|
374
481
|
* declared integer type, and becomes the new column's default.
|
|
375
482
|
*/
|
|
376
|
-
function sequenceStatements(o: MigrationObservation, state: SequenceState | undefined): { before: string[]; after: string[] } {
|
|
483
|
+
function sequenceStatements(o: MigrationObservation, state: SequenceState | undefined, at: "switch" | "contract" = "switch"): { before: string[]; after: string[] } {
|
|
377
484
|
const seq = o.sequence;
|
|
378
485
|
const n = o.names;
|
|
379
486
|
const t = n.qualifiedTable;
|
|
380
|
-
if (!seq || n.change !== "type") return { before: [], after: [] };
|
|
487
|
+
if (!seq || n.change !== (at === "switch" ? "type" : "rename")) return { before: [], after: [] };
|
|
488
|
+
if (at === "contract" && seq.kind === "serial") {
|
|
489
|
+
// The sequence goes to the new column before the old one is dropped, which would drop it; the default is set after.
|
|
490
|
+
return { before: [`ALTER SEQUENCE ${seq.ident} OWNED BY ${t}.${col(n.column)}`], after: [] };
|
|
491
|
+
}
|
|
381
492
|
if (seq.kind === "identity") {
|
|
382
|
-
if (!state) throw new Error(`${n.key}: the
|
|
493
|
+
if (!state) throw new Error(`${n.key}: the ${at} moves the identity of ${n.source} and needs its sequence's state, read under the switch's lock`);
|
|
383
494
|
const g = o.column.generated;
|
|
384
495
|
const options = [`SEQUENCE NAME ${seq.ident}`, ...(g?.options ? [g.options.trim().replace(/^\(|\)$/g, "")] : [])].join(" ");
|
|
385
496
|
return {
|
|
@@ -540,6 +651,31 @@ export const identityOf = (marker: OwnershipMarker | undefined): { stack?: strin
|
|
|
540
651
|
* catalog change under ACCESS EXCLUSIVE). The migration's receipts go with
|
|
541
652
|
* them. Before the date, say until when and drop nothing.
|
|
542
653
|
*/
|
|
654
|
+
/**
|
|
655
|
+
* The contract's statements. Always: the trigger and its function, then the
|
|
656
|
+
* old column. A rename of a column with a default, serial or identity
|
|
657
|
+
* (`defaultAtContract`) also gives the declared default to the new column
|
|
658
|
+
* here, in the same transaction, so no writer sees both columns defaulted:
|
|
659
|
+
* a serial's sequence is handed over before the old column is dropped (which
|
|
660
|
+
* would drop it), an identity is dropped from the old column and added to
|
|
661
|
+
* the new one under the same sequence name, set to where the old one stood.
|
|
662
|
+
*/
|
|
663
|
+
export function contractStatements(o: MigrationObservation, sequenceState?: SequenceState): string[] {
|
|
664
|
+
const n = o.names;
|
|
665
|
+
const t = n.qualifiedTable;
|
|
666
|
+
const old = o.old!;
|
|
667
|
+
const sequence = sequenceStatements(o, sequenceState, "contract");
|
|
668
|
+
const dflt = o.column.default ?? (o.sequence?.kind === "serial" ? old.default : undefined);
|
|
669
|
+
const out: string[] = [];
|
|
670
|
+
if (o.trigger) out.push(`DROP TRIGGER ${col(n.trigger)} ON ${t}`);
|
|
671
|
+
if (o.fn) out.push(`DROP FUNCTION ${n.qualifiedFn}()`);
|
|
672
|
+
out.push(...sequence.before);
|
|
673
|
+
out.push(`ALTER TABLE ${t} DROP COLUMN ${col(old.name)}`);
|
|
674
|
+
if (dflt !== undefined && n.change === "rename" && o.sequence?.kind !== "identity") out.push(`ALTER TABLE ${t} ALTER COLUMN ${col(n.column)} SET DEFAULT ${dflt}`);
|
|
675
|
+
out.push(...sequence.after);
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
|
|
543
679
|
export async function contract(run: MigrationRun): Promise<ContractResult> {
|
|
544
680
|
const plan = await retainPlan(run);
|
|
545
681
|
if (!plan.oldColumn) return { state: plan.state, dropped: false };
|
|
@@ -550,9 +686,14 @@ export async function contract(run: MigrationRun): Promise<ContractResult> {
|
|
|
550
686
|
const o = await observe(run);
|
|
551
687
|
const n = o.names;
|
|
552
688
|
await inTransaction(run, async (exec) => {
|
|
553
|
-
|
|
554
|
-
if (o.
|
|
555
|
-
|
|
689
|
+
let state: SequenceState | undefined;
|
|
690
|
+
if (o.sequence?.kind === "identity" && n.change === "rename") {
|
|
691
|
+
// Writers wait from here, so the sequence's position read next is where the moved identity carries on.
|
|
692
|
+
await exec(`LOCK TABLE ${n.qualifiedTable} IN ACCESS EXCLUSIVE MODE`);
|
|
693
|
+
const [row] = await exec(`SELECT last_value::text AS last, is_called AS called FROM ${o.sequence.ident}`);
|
|
694
|
+
state = { lastValue: String(row?.last), isCalled: row?.called === true };
|
|
695
|
+
}
|
|
696
|
+
for (const sql of contractStatements(o, state)) await exec(sql);
|
|
556
697
|
});
|
|
557
698
|
await postgresReceiptStore(run.client, n.schema, identityOf(run.marker)).forget(batchPrefix(identityOf(run.marker), n.key));
|
|
558
699
|
return { state: "done", dropped: true, oldColumn: plan.oldColumn };
|
|
@@ -619,7 +760,11 @@ export async function compensate(run: Pick<MigrationRun, "client" | "target" | "
|
|
|
619
760
|
).filter((c) => ours(c.comment, "carry"));
|
|
620
761
|
const carriedIndexes = (
|
|
621
762
|
await run.client.query<{ name: string; comment: string | null }>(
|
|
622
|
-
|
|
763
|
+
// The table's own indexes, and those on its partitions that no partitioned index has taken yet (an attached one goes with the partitioned index).
|
|
764
|
+
`SELECT i.indexrelid::pg_catalog.regclass::text AS name, pg_catalog.obj_description(i.indexrelid, 'pg_class') AS comment
|
|
765
|
+
FROM pg_catalog.pg_index i
|
|
766
|
+
WHERE (i.indrelid = $1::oid OR (i.indrelid IN (SELECT t.relid FROM pg_catalog.pg_partition_tree($1::oid::pg_catalog.regclass) t) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_inherits h WHERE h.inhrelid = i.indexrelid)))
|
|
767
|
+
ORDER BY i.indrelid <> $1::oid, 1`,
|
|
623
768
|
[rel.oid],
|
|
624
769
|
)
|
|
625
770
|
).filter((c) => ours(c.comment, "carry"));
|
|
@@ -643,7 +788,7 @@ export async function compensate(run: Pick<MigrationRun, "client" | "target" | "
|
|
|
643
788
|
dropped.push(`constraint ${c.name} on ${c.rel}`);
|
|
644
789
|
}
|
|
645
790
|
for (const c of carriedIndexes) {
|
|
646
|
-
await exec(`DROP INDEX ${c.name}`);
|
|
791
|
+
await exec(`DROP INDEX IF EXISTS ${c.name}`);
|
|
647
792
|
dropped.push(`index ${c.name}`);
|
|
648
793
|
}
|
|
649
794
|
for (const c of newColumns) {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A table that is the target of a logical replication subscription (#4405),
|
|
3
|
+
* against two pinned servers with `wal_level = logical`, a publisher and a
|
|
4
|
+
* subscriber on one Docker network:
|
|
5
|
+
*
|
|
6
|
+
* - The fact the refusal rests on: a trigger made by plain `CREATE TRIGGER`
|
|
7
|
+
* does not fire for the subscription's apply worker, and one set
|
|
8
|
+
* `ENABLE ALWAYS` does.
|
|
9
|
+
* - The Plan phase refuses the subscriber's table, naming the subscription,
|
|
10
|
+
* and stops refusing it once the table is out of the publication and the
|
|
11
|
+
* subscription has refreshed.
|
|
12
|
+
*
|
|
13
|
+
* Needs Docker; skips cleanly without it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { promisify } from "node:util";
|
|
21
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
|
+
import { dockerAvailable } from "../testing/server";
|
|
23
|
+
import { connectPostgres, type PostgresClient } from "../live/client";
|
|
24
|
+
import { postgresMigrationPlan, type PostgresMigrationDeps } from "../../op/activities/postgres-migration";
|
|
25
|
+
import { postgresImage, POSTGRES_LATEST_MAJOR } from "../../spec/postgres-pin";
|
|
26
|
+
|
|
27
|
+
const run = promisify(execFile);
|
|
28
|
+
const enabled = await dockerAvailable();
|
|
29
|
+
const PASSWORD = "chant";
|
|
30
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
31
|
+
const network = `chant-sub-net-${suffix}`;
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-pg-subscriber-"));
|
|
33
|
+
const MARKER = { stack: "e2e", env: "test" };
|
|
34
|
+
|
|
35
|
+
interface Server {
|
|
36
|
+
name: string;
|
|
37
|
+
port: string;
|
|
38
|
+
client: PostgresClient;
|
|
39
|
+
}
|
|
40
|
+
let pub: Server | undefined;
|
|
41
|
+
let sub: Server | undefined;
|
|
42
|
+
|
|
43
|
+
async function start(role: string): Promise<Server> {
|
|
44
|
+
const name = `chant-sub-${role}-${suffix}`;
|
|
45
|
+
await run("docker", [
|
|
46
|
+
"run", "-d", "--name", name, "--network", network, "-p", "127.0.0.1::5432", "-e", `POSTGRES_PASSWORD=${PASSWORD}`,
|
|
47
|
+
postgresImage(POSTGRES_LATEST_MAJOR), "-c", "wal_level=logical",
|
|
48
|
+
]);
|
|
49
|
+
const port = (await run("docker", ["port", name, "5432/tcp"])).stdout.trim().split(":").pop()!;
|
|
50
|
+
for (let i = 0; i < 120; i++) {
|
|
51
|
+
try {
|
|
52
|
+
const client = await connectPostgres({ url: `postgres://postgres@127.0.0.1:${port}/postgres`, password: PASSWORD });
|
|
53
|
+
await client.query("SELECT 1");
|
|
54
|
+
return { name, port, client };
|
|
55
|
+
} catch {
|
|
56
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`${name} did not accept a connection`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beforeAll(async () => {
|
|
63
|
+
if (!enabled) return;
|
|
64
|
+
await run("docker", ["network", "create", network]);
|
|
65
|
+
[pub, sub] = await Promise.all([start("pub"), start("sub")]);
|
|
66
|
+
}, 600_000);
|
|
67
|
+
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
for (const s of [pub, sub]) {
|
|
70
|
+
await s?.client.end().catch(() => undefined);
|
|
71
|
+
if (s) await run("docker", ["rm", "-f", "-v", s.name]).catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
if (enabled) await run("docker", ["network", "rm", network]).catch(() => undefined);
|
|
74
|
+
rmSync(dir, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const waitFor = async (what: string, check: () => Promise<boolean>) => {
|
|
78
|
+
for (let i = 0; i < 60; i++) {
|
|
79
|
+
if (await check()) return;
|
|
80
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`timed out waiting for ${what}`);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const TABLE = "CREATE TABLE app.users (\n id bigint PRIMARY KEY,\n email text NOT NULL\n)";
|
|
86
|
+
const deps = (): PostgresMigrationDeps => ({
|
|
87
|
+
config: { ownership: MARKER, sql: { profiles: { e2e: { url: `postgres://postgres@127.0.0.1:${sub!.port}/postgres`, password: { env: "PG_E2E_PASSWORD" } } } } },
|
|
88
|
+
env: { PG_E2E_PASSWORD: PASSWORD },
|
|
89
|
+
log: () => undefined,
|
|
90
|
+
});
|
|
91
|
+
/** The Plan phase of a rename of email to login, on the subscriber. */
|
|
92
|
+
function planRename(): Promise<string> {
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(dir, "v2.json"),
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
dialect: "postgres",
|
|
97
|
+
postgresMajor: 18,
|
|
98
|
+
applyOrder: ["app", "users"],
|
|
99
|
+
objects: [
|
|
100
|
+
{ export: "app", type: "Postgres::Schema", ddl: "CREATE SCHEMA app", dependsOn: [] },
|
|
101
|
+
{ export: "users", type: "Postgres::Table", dependsOn: ["app"], ddl: "CREATE TABLE app.users (\n id bigint PRIMARY KEY,\n login text NOT NULL -- previously: email\n)" },
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
return postgresMigrationPlan({ table: "app.users", column: "login", buildPath: "v2.json", environment: "e2e", stack: MARKER.stack, ownershipEnv: MARKER.env, cwd: dir } as never, undefined, deps()).then(
|
|
106
|
+
() => "",
|
|
107
|
+
(e: unknown) => String((e as Error).message),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe.skipIf(!enabled)("a subscription's target table", () => {
|
|
112
|
+
test("the apply worker fires an ENABLE ALWAYS trigger and not a default one", async () => {
|
|
113
|
+
for (const s of [pub!, sub!]) await s.client.query(`CREATE SCHEMA app`).then(() => s.client.query(TABLE));
|
|
114
|
+
await pub!.client.query("CREATE PUBLICATION pub FOR TABLE app.users");
|
|
115
|
+
await sub!.client.query(`CREATE SUBSCRIPTION sub CONNECTION 'host=${pub!.name} port=5432 user=postgres password=${PASSWORD} dbname=postgres' PUBLICATION pub`);
|
|
116
|
+
await waitFor("the initial sync", async () => (await sub!.client.query<{ n: string }>("SELECT count(*)::text AS n FROM pg_subscription_rel WHERE srsubstate <> 'r'"))[0]!.n === "0");
|
|
117
|
+
|
|
118
|
+
await sub!.client.query("ALTER TABLE app.users ADD COLUMN seen text");
|
|
119
|
+
await sub!.client.query("CREATE FUNCTION app.mark() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.seen := TG_NAME; RETURN NEW; END $$");
|
|
120
|
+
await sub!.client.query("CREATE TRIGGER t_default BEFORE INSERT ON app.users FOR EACH ROW EXECUTE FUNCTION app.mark()");
|
|
121
|
+
await pub!.client.query("INSERT INTO app.users VALUES (1, 'a')");
|
|
122
|
+
await waitFor("row 1", async () => (await sub!.client.query("SELECT 1 FROM app.users WHERE id = 1")).length === 1);
|
|
123
|
+
expect(await sub!.client.query("SELECT seen FROM app.users WHERE id = 1")).toEqual([{ seen: null }]);
|
|
124
|
+
|
|
125
|
+
await sub!.client.query("ALTER TABLE app.users ENABLE ALWAYS TRIGGER t_default");
|
|
126
|
+
await pub!.client.query("INSERT INTO app.users VALUES (2, 'b')");
|
|
127
|
+
await waitFor("row 2", async () => (await sub!.client.query("SELECT 1 FROM app.users WHERE id = 2")).length === 1);
|
|
128
|
+
expect(await sub!.client.query("SELECT seen FROM app.users WHERE id = 2")).toEqual([{ seen: "t_default" }]);
|
|
129
|
+
await sub!.client.query("DROP TRIGGER t_default ON app.users");
|
|
130
|
+
await sub!.client.query("ALTER TABLE app.users DROP COLUMN seen");
|
|
131
|
+
}, 300_000);
|
|
132
|
+
|
|
133
|
+
test("the Plan phase refuses it naming the subscription, and not once the table has left the publication", async () => {
|
|
134
|
+
expect(await planRename()).toMatch(/target of logical replication subscription\(s\) sub\b/);
|
|
135
|
+
|
|
136
|
+
await pub!.client.query("ALTER PUBLICATION pub DROP TABLE app.users");
|
|
137
|
+
await sub!.client.query("ALTER SUBSCRIPTION sub REFRESH PUBLICATION");
|
|
138
|
+
await waitFor("the table to leave the subscription", async () => (await sub!.client.query("SELECT 1 FROM pg_subscription_rel")).length === 0);
|
|
139
|
+
expect(await planRename()).not.toMatch(/subscription/);
|
|
140
|
+
}, 300_000);
|
|
141
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Plan phase's read of the subscriptions a table is the target of
|
|
3
|
+
* (#4405), and its refusal. The e2e is `subscriber.e2e.test.ts`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import type { PostgresClient } from "../live/client";
|
|
8
|
+
import { subscriptionRefusal, subscriptionsOf } from "./replication";
|
|
9
|
+
|
|
10
|
+
function fake(rows: unknown[]) {
|
|
11
|
+
const calls: { sql: string; params?: readonly unknown[] }[] = [];
|
|
12
|
+
const client = {
|
|
13
|
+
query: async (sql: string, params?: readonly unknown[]) => {
|
|
14
|
+
calls.push({ sql, params });
|
|
15
|
+
return rows;
|
|
16
|
+
},
|
|
17
|
+
} as unknown as Pick<PostgresClient, "query">;
|
|
18
|
+
return { client, calls };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("subscriptionsOf", () => {
|
|
22
|
+
test("reads pg_subscription_rel and pg_subscription for this database, by schema and table", async () => {
|
|
23
|
+
const { client, calls } = fake([{ subname: "sub_a", subenabled: true }, { subname: "sub_b", subenabled: false }]);
|
|
24
|
+
expect(await subscriptionsOf(client, "app", "users", 18)).toEqual([{ name: "sub_a", enabled: true }, { name: "sub_b", enabled: false }]);
|
|
25
|
+
expect(calls).toHaveLength(1);
|
|
26
|
+
expect(calls[0]!.params).toEqual(["app", "users"]);
|
|
27
|
+
expect(calls[0]!.sql).toContain("pg_catalog.pg_subscription_rel");
|
|
28
|
+
expect(calls[0]!.sql).toContain("pg_catalog.pg_subscription s");
|
|
29
|
+
expect(calls[0]!.sql).toContain("s.subdbid");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a partitioned table is matched through its partitions; before 12 the table alone", async () => {
|
|
33
|
+
const now = fake([]);
|
|
34
|
+
await subscriptionsOf(now.client, "app", "events", 18);
|
|
35
|
+
expect(now.calls[0]!.sql).toContain("pg_partition_tree");
|
|
36
|
+
const old = fake([]);
|
|
37
|
+
await subscriptionsOf(old.client, "app", "events", 11);
|
|
38
|
+
expect(old.calls[0]!.sql).not.toContain("pg_partition_tree");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("no row means no subscription", async () => {
|
|
42
|
+
expect(await subscriptionsOf(fake([]).client, "app", "users", 18)).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("subscriptionRefusal", () => {
|
|
47
|
+
const message = subscriptionRefusal("app.users", "login", [{ name: "sub_a", enabled: true }, { name: "sub_b", enabled: false }]);
|
|
48
|
+
|
|
49
|
+
test("names each subscription, and a disabled one as such", () => {
|
|
50
|
+
expect(message).toContain("sub_a, sub_b (disabled)");
|
|
51
|
+
expect(message.startsWith("app.users is a target")).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("gives the reason and the way out", () => {
|
|
55
|
+
expect(message).toContain("session_replication_role = replica");
|
|
56
|
+
expect(message).toContain("ENABLE ALWAYS");
|
|
57
|
+
expect(message).toContain("REFRESH PUBLICATION");
|
|
58
|
+
});
|
|
59
|
+
});
|