@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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renames the Op refused in #3322 and makes since (#3331), against the
|
|
3
|
+
* pinned server:
|
|
4
|
+
*
|
|
5
|
+
* - A rename of a column with a default (`defaultAtContract`): writers of
|
|
6
|
+
* either name during the migration, the new column without the default
|
|
7
|
+
* until the contract, and the default there once it ends.
|
|
8
|
+
* - A rename of a `serial` column: the sequence stays on the old column
|
|
9
|
+
* until the contract and is handed to the new one there.
|
|
10
|
+
* - A rename of an identity column: the identity moves to the new column at
|
|
11
|
+
* the contract and carries on where the old one stood.
|
|
12
|
+
* - A rename with a type change (`reverse`): writers of either name, and a
|
|
13
|
+
* `reverse` that does not give the old value back fails the verification.
|
|
14
|
+
*
|
|
15
|
+
* Needs Docker; skips cleanly without it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
|
+
import { OpRunFailure } from "@intentius/chant/op";
|
|
23
|
+
import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
|
|
24
|
+
import { ApprovingLedger, runMigrationOp } from "../testing/migration";
|
|
25
|
+
import type { PostgresClient } from "../live/client";
|
|
26
|
+
import { planPgAgainstServer } from "../plan/commands";
|
|
27
|
+
import { postgresApply } from "../../op/activities/postgres-apply";
|
|
28
|
+
import { postgresMigrationPlan, type PostgresMigrationDeps } from "../../op/activities/postgres-migration";
|
|
29
|
+
import type { PostgresMigrationOpConfig } from "./op";
|
|
30
|
+
|
|
31
|
+
const enabled = await dockerAvailable();
|
|
32
|
+
let server: TestPostgres | undefined;
|
|
33
|
+
let admin: PostgresClient | undefined;
|
|
34
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-pg-followups-"));
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
if (!enabled) return;
|
|
38
|
+
server = await startTestPostgres();
|
|
39
|
+
admin = await server.connect();
|
|
40
|
+
}, 600_000);
|
|
41
|
+
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
await admin?.end();
|
|
44
|
+
await server?.stop();
|
|
45
|
+
rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
interface Obj {
|
|
49
|
+
export: string;
|
|
50
|
+
type: string;
|
|
51
|
+
ddl: string;
|
|
52
|
+
dependsOn?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeBuild(file: string, objects: Obj[]): string {
|
|
56
|
+
writeFileSync(join(dir, file), JSON.stringify({ dialect: "postgres", postgresMajor: 18, applyOrder: objects.map((o) => o.export), objects: objects.map((o) => ({ dependsOn: [], ...o })) }));
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const MARKER = { stack: "e2e", env: "test" };
|
|
61
|
+
const profile = () => ({
|
|
62
|
+
config: { ownership: MARKER, sql: { profiles: { e2e: { url: server!.endpoint().url, password: { env: "PG_E2E_PASSWORD" } } } } },
|
|
63
|
+
env: { PG_E2E_PASSWORD: server!.endpoint().password },
|
|
64
|
+
});
|
|
65
|
+
const deps = (): PostgresMigrationDeps => ({ ...profile(), log: () => undefined });
|
|
66
|
+
const apply = (file: string) => postgresApply({ buildPath: join(dir, file), environment: "e2e" }, undefined, { ...profile(), log: () => undefined });
|
|
67
|
+
const plan = (file: string) => planPgAgainstServer("e2e", join(dir, file), profile());
|
|
68
|
+
const one = async <T>(sql: string): Promise<T> => (await admin!.query<T>(sql))[0]!;
|
|
69
|
+
const schemaObj = (name: string): Obj => ({ export: name, type: "Postgres::Schema", ddl: `CREATE SCHEMA ${name}` });
|
|
70
|
+
const table = (exportName: string, schema: string, ddl: string): Obj => ({ export: exportName, type: "Postgres::Table", dependsOn: [schema], ddl });
|
|
71
|
+
/** A table of a `bigint` key and one more column, last, with the `-- previously:` hint a rename is declared by. */
|
|
72
|
+
const keyed = (exportName: string, schema: string, column: string, previously?: string): Obj =>
|
|
73
|
+
table(exportName, schema, `CREATE TABLE ${schema}.${exportName} (\n id bigint PRIMARY KEY,\n ${column}${previously ? ` -- previously: ${previously}` : ""}\n)`);
|
|
74
|
+
const config = (over: Pick<PostgresMigrationOpConfig, "name" | "table" | "column" | "output"> & Partial<PostgresMigrationOpConfig>): PostgresMigrationOpConfig => ({
|
|
75
|
+
env: "e2e",
|
|
76
|
+
build: false,
|
|
77
|
+
path: dir,
|
|
78
|
+
retain: "0s",
|
|
79
|
+
stack: MARKER.stack,
|
|
80
|
+
ownershipEnv: MARKER.env,
|
|
81
|
+
...over,
|
|
82
|
+
});
|
|
83
|
+
const refusal = (tableName: string, column: string, output: string, extra: Partial<PostgresMigrationOpConfig> = {}) =>
|
|
84
|
+
postgresMigrationPlan({ table: tableName, column, buildPath: output, environment: "e2e", stack: MARKER.stack, ownershipEnv: MARKER.env, cwd: dir, ...extra } as never, undefined, deps()).then(
|
|
85
|
+
() => "",
|
|
86
|
+
(e: unknown) => String((e as Error).message),
|
|
87
|
+
);
|
|
88
|
+
const defaultOf = async (t: string, c: string) =>
|
|
89
|
+
(await one<{ d: string | null }>(`SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS d FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum WHERE a.attrelid = '${t}'::regclass AND a.attname = '${c}'`)).d;
|
|
90
|
+
|
|
91
|
+
/** Run the Op until it is done, approving each gate; `atGate` runs after the first run, with both columns written and nothing switched. */
|
|
92
|
+
async function runToEnd(c: PostgresMigrationOpConfig, atGate?: () => Promise<void>) {
|
|
93
|
+
const ledger = new ApprovingLedger();
|
|
94
|
+
const runs = [];
|
|
95
|
+
for (let i = 0; i < 4; i++) {
|
|
96
|
+
const r = await runMigrationOp(c, ledger.port, deps);
|
|
97
|
+
runs.push(r);
|
|
98
|
+
if (i === 0) await atGate?.();
|
|
99
|
+
if (r.status !== "gated") break;
|
|
100
|
+
ledger.approveLast();
|
|
101
|
+
}
|
|
102
|
+
return runs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe.skipIf(!enabled)("a rename of a column with a default", () => {
|
|
106
|
+
const DF = schemaObj("df");
|
|
107
|
+
const users = (c: string, previously?: string) => keyed("users", "df", `${c} text NOT NULL DEFAULT 'none'`, previously);
|
|
108
|
+
|
|
109
|
+
test("refused without defaultAtContract; with it, writers of either name agree and the default lands at the contract", async () => {
|
|
110
|
+
await apply(writeBuild("df-v1.json", [DF, users("email")]));
|
|
111
|
+
await admin!.query("INSERT INTO df.users (id, email) SELECT g, 'u' || g FROM generate_series(1, 500) g");
|
|
112
|
+
writeBuild("df-v2.json", [DF, users("login", "email")]);
|
|
113
|
+
expect(await refusal("df.users", "login", "df-v2.json")).toMatch(/Set defaultAtContract: true/);
|
|
114
|
+
|
|
115
|
+
const runs = await runToEnd(config({ name: "migrate-df-login", table: "df.users", column: "login", output: "df-v2.json", defaultAtContract: true, batchSize: 200 }), async () => {
|
|
116
|
+
// Nothing is switched: the new column has no default, the old one keeps its own.
|
|
117
|
+
expect(await defaultOf("df.users", "login")).toBeNull();
|
|
118
|
+
expect(await defaultOf("df.users", "email")).toBe("'none'::text");
|
|
119
|
+
await admin!.query("INSERT INTO df.users (id) VALUES (1001)"); // neither name
|
|
120
|
+
await admin!.query("INSERT INTO df.users (id, email) VALUES (1002, 'old')"); // the old name
|
|
121
|
+
await admin!.query("INSERT INTO df.users (id, login) VALUES (1003, 'new')"); // the new name
|
|
122
|
+
await admin!.query("UPDATE df.users SET login = 'new2' WHERE id = 1002");
|
|
123
|
+
await admin!.query("UPDATE df.users SET email = 'old2' WHERE id = 1003");
|
|
124
|
+
});
|
|
125
|
+
expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
|
|
126
|
+
expect(await admin!.query("SELECT id::int AS id, login FROM df.users WHERE id > 1000 ORDER BY id")).toEqual([
|
|
127
|
+
{ id: 1001, login: "none" },
|
|
128
|
+
{ id: 1002, login: "new2" },
|
|
129
|
+
{ id: 1003, login: "old2" },
|
|
130
|
+
]);
|
|
131
|
+
expect(await defaultOf("df.users", "login")).toBe("'none'::text");
|
|
132
|
+
expect(await admin!.query("SELECT attname FROM pg_attribute WHERE attrelid = 'df.users'::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum")).toEqual([{ attname: "id" }, { attname: "login" }]);
|
|
133
|
+
expect((await plan("df-v2.json")).changes).toEqual([]);
|
|
134
|
+
}, 300_000);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe.skipIf(!enabled)("a rename of a serial column", () => {
|
|
138
|
+
const SR = schemaObj("sq");
|
|
139
|
+
const items = (c: string, previously?: string) => keyed("items", "sq", `${c} serial`, previously);
|
|
140
|
+
|
|
141
|
+
test("the sequence stays with the old column until the contract, then belongs to the new one and stays its default", async () => {
|
|
142
|
+
await apply(writeBuild("sq-v1.json", [SR, items("n")]));
|
|
143
|
+
await admin!.query("INSERT INTO sq.items (id) SELECT g FROM generate_series(1, 300) g");
|
|
144
|
+
writeBuild("sq-v2.json", [SR, items("num", "n")]);
|
|
145
|
+
|
|
146
|
+
const runs = await runToEnd(config({ name: "migrate-items-num", table: "sq.items", column: "num", output: "sq-v2.json", defaultAtContract: true }), async () => {
|
|
147
|
+
await admin!.query("INSERT INTO sq.items (id) VALUES (1001)"); // takes the old default; the trigger copies it
|
|
148
|
+
await admin!.query("INSERT INTO sq.items (id, num) VALUES (1002, 7000)"); // the new name
|
|
149
|
+
expect(await one("SELECT n = num AS same FROM sq.items WHERE id = 1001")).toEqual({ same: true });
|
|
150
|
+
expect(await one("SELECT n AS n FROM sq.items WHERE id = 1002")).toEqual({ n: 7000 });
|
|
151
|
+
});
|
|
152
|
+
expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
|
|
153
|
+
expect(await one("SELECT pg_catalog.pg_get_serial_sequence('sq.items', 'num') AS seq")).toEqual({ seq: "sq.items_n_seq" });
|
|
154
|
+
const next = await one<{ id: number; num: number }>("INSERT INTO sq.items (id) VALUES (2000) RETURNING id::int AS id, num");
|
|
155
|
+
expect(next.num).toBeGreaterThan(300);
|
|
156
|
+
expect((await plan("sq-v2.json")).changes).toEqual([]);
|
|
157
|
+
}, 300_000);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe.skipIf(!enabled)("a rename of an identity column", () => {
|
|
161
|
+
const ID = schemaObj("ix");
|
|
162
|
+
const things = (c: string, previously?: string) => keyed("things", "ix", `${c} integer GENERATED ALWAYS AS IDENTITY`, previously);
|
|
163
|
+
|
|
164
|
+
test("the identity moves to the new column at the contract and carries on where the old one stood", async () => {
|
|
165
|
+
await apply(writeBuild("ix-v1.json", [ID, things("n")]));
|
|
166
|
+
await admin!.query("INSERT INTO ix.things (id) SELECT g FROM generate_series(1, 120) g");
|
|
167
|
+
writeBuild("ix-v2.json", [ID, things("num", "n")]);
|
|
168
|
+
|
|
169
|
+
const runs = await runToEnd(config({ name: "migrate-things-num", table: "ix.things", column: "num", output: "ix-v2.json", defaultAtContract: true }), async () => {
|
|
170
|
+
await admin!.query("INSERT INTO ix.things (id) VALUES (1001)"); // the old identity gives 121; the trigger copies it
|
|
171
|
+
expect(await one("SELECT n = num AS same FROM ix.things WHERE id = 1001")).toEqual({ same: true });
|
|
172
|
+
// A writer of the new name gives a value; the trigger sets the old column's ALWAYS identity from it.
|
|
173
|
+
await admin!.query("INSERT INTO ix.things (id, num) VALUES (1002, 5000)");
|
|
174
|
+
expect(await one("SELECT n AS n FROM ix.things WHERE id = 1002")).toEqual({ n: 5000 });
|
|
175
|
+
});
|
|
176
|
+
expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
|
|
177
|
+
expect(await one("SELECT attidentity::text AS identity FROM pg_attribute WHERE attrelid = 'ix.things'::regclass AND attname = 'num'")).toEqual({ identity: "a" });
|
|
178
|
+
const next = await one<{ num: number }>("INSERT INTO ix.things (id) VALUES (2000) RETURNING num");
|
|
179
|
+
expect(next.num).toBeGreaterThan(120);
|
|
180
|
+
expect((await plan("ix-v2.json")).changes).toEqual([]);
|
|
181
|
+
}, 300_000);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe.skipIf(!enabled)("a rename with a type change", () => {
|
|
185
|
+
const RT = schemaObj("rt");
|
|
186
|
+
const people = (column: string, previously?: string) => keyed("people", "rt", column, previously);
|
|
187
|
+
|
|
188
|
+
test("refused without reverse; with it, writers of either name agree and a wrong reverse fails the verification", async () => {
|
|
189
|
+
await apply(writeBuild("rt-v1.json", [RT, people("age_text text NOT NULL")]));
|
|
190
|
+
await admin!.query("INSERT INTO rt.people SELECT g, (g % 90)::text FROM generate_series(1, 400) g");
|
|
191
|
+
writeBuild("rt-v2.json", [RT, people("age integer NOT NULL", "age_text")]);
|
|
192
|
+
expect(await refusal("rt.people", "age", "rt-v2.json")).toMatch(/give that expression as reverse/);
|
|
193
|
+
|
|
194
|
+
// A reverse that does not give the old value back: nothing is switched and onFailure drops what the expand added.
|
|
195
|
+
const bad = await runMigrationOp(config({ name: "migrate-people-age", table: "rt.people", column: "age", output: "rt-v2.json", using: "CAST(age_text AS integer)", reverse: "CAST(age AS text) || 'x'" }), new ApprovingLedger().port, deps).then(
|
|
196
|
+
() => undefined,
|
|
197
|
+
(e: unknown) => e,
|
|
198
|
+
);
|
|
199
|
+
expect(bad).toBeInstanceOf(OpRunFailure);
|
|
200
|
+
expect((bad as OpRunFailure).result.records.find((x) => x.fn === "postgresMigrationVerify")?.error).toMatch(/must give the old value back/);
|
|
201
|
+
expect(await one("SELECT count(*)::int AS n FROM pg_attribute WHERE attrelid = 'rt.people'::regclass AND attname = 'age' AND NOT attisdropped")).toEqual({ n: 0 });
|
|
202
|
+
|
|
203
|
+
const runs = await runToEnd(config({ name: "migrate-people-age", table: "rt.people", column: "age", output: "rt-v2.json", using: "CAST(age_text AS integer)", reverse: "CAST(age AS text)" }), async () => {
|
|
204
|
+
await admin!.query("INSERT INTO rt.people (id, age_text) VALUES (1001, '41')"); // the old name
|
|
205
|
+
await admin!.query("INSERT INTO rt.people (id, age) VALUES (1002, 42)"); // the new name
|
|
206
|
+
await admin!.query("UPDATE rt.people SET age = 43 WHERE id = 1001");
|
|
207
|
+
await admin!.query("UPDATE rt.people SET age_text = '44' WHERE id = 1002");
|
|
208
|
+
expect(await admin!.query("SELECT id::int AS id, age, age_text FROM rt.people WHERE id > 1000 ORDER BY id")).toEqual([
|
|
209
|
+
{ id: 1001, age: 43, age_text: "43" },
|
|
210
|
+
{ id: 1002, age: 44, age_text: "44" },
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
expect(runs.map((r) => r.status)).toEqual(["gated", "gated", "ok"]);
|
|
214
|
+
expect(await one("SELECT pg_catalog.format_type(atttypid, atttypmod) AS t FROM pg_attribute WHERE attrelid = 'rt.people'::regclass AND attname = 'age'")).toEqual({ t: "integer" });
|
|
215
|
+
expect(await admin!.query("SELECT attname FROM pg_attribute WHERE attrelid = 'rt.people'::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum")).toEqual([{ attname: "id" }, { attname: "age" }]);
|
|
216
|
+
expect((await plan("rt-v2.json")).changes).toEqual([]);
|
|
217
|
+
}, 300_000);
|
|
218
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The migration Op's follow-ups to #3322 that need no server (#3331): a
|
|
3
|
+
* rename combined with a type change (`reverse`), and a rename of a column
|
|
4
|
+
* with a default, a serial or an identity (`defaultAtContract`). The dual
|
|
5
|
+
* write's body, the contract's statements and the refusals; the e2e is
|
|
6
|
+
* `followups.e2e.test.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import { validateActivitySteps, collectActivityContracts, loadActivityContracts, mergeActivityContracts, type ActivityContract, type OpConfig } from "@intentius/chant/op";
|
|
11
|
+
import * as contractsModule from "../../op/activity-contracts";
|
|
12
|
+
import type { PostgresClient } from "../live/client";
|
|
13
|
+
import type { PgChange } from "../plan/diff";
|
|
14
|
+
import { PostgresMigrationOp, type PostgresMigrationOpConfig } from "./op";
|
|
15
|
+
import { migrationNames } from "./names";
|
|
16
|
+
import { dualWriteBody, contractStatements } from "./steps";
|
|
17
|
+
import { migrationPlanSubject, refuseUnsupported, type MigrationObservation, type RelationRow, type ServerColumn } from "./observe";
|
|
18
|
+
|
|
19
|
+
const BASE: PostgresMigrationOpConfig = { name: "migrate-users-login", env: "prod", table: "app.users", column: "login", build: false };
|
|
20
|
+
const propsOf = (config: PostgresMigrationOpConfig) => (PostgresMigrationOp(config).op as unknown as { props: OpConfig }).props;
|
|
21
|
+
|
|
22
|
+
const names = migrationNames("app", "users", "login", "rename", "email");
|
|
23
|
+
|
|
24
|
+
const observation = (over: Partial<MigrationObservation> = {}): MigrationObservation =>
|
|
25
|
+
({
|
|
26
|
+
state: "switched",
|
|
27
|
+
names,
|
|
28
|
+
declared: {} as MigrationObservation["declared"],
|
|
29
|
+
column: { name: "login", type: "text" },
|
|
30
|
+
oid: "16384",
|
|
31
|
+
batchKey: "id",
|
|
32
|
+
keyColumns: [{ name: "id", type: "bigint" }],
|
|
33
|
+
carried: [],
|
|
34
|
+
carriedStates: new Map(),
|
|
35
|
+
views: [],
|
|
36
|
+
partitioned: false,
|
|
37
|
+
columns: new Map(),
|
|
38
|
+
changes: [],
|
|
39
|
+
expression: '"email"',
|
|
40
|
+
publications: [],
|
|
41
|
+
major: 18,
|
|
42
|
+
old: { attnum: 2, name: "email", type: "text", notNull: true, generated: false, identity: false },
|
|
43
|
+
trigger: { name: names.trigger },
|
|
44
|
+
fn: { name: names.fn },
|
|
45
|
+
...over,
|
|
46
|
+
}) as MigrationObservation;
|
|
47
|
+
|
|
48
|
+
describe("a rename with a type change", () => {
|
|
49
|
+
const retyped = migrationNames("app", "users", "age", "rename", "age_text");
|
|
50
|
+
const o = { names: retyped, expression: "CAST(age_text AS integer)", reverse: "CAST(age AS text)" };
|
|
51
|
+
|
|
52
|
+
test("a writer of the old name has the expression applied, a writer of the new name the reverse", () => {
|
|
53
|
+
const body = dualWriteBody(o);
|
|
54
|
+
expect(body).toContain("IF NEW.age IS NULL THEN NEW.age := (SELECT CAST(age_text AS integer) FROM (SELECT NEW.*) AS users); ELSE NEW.age_text := (SELECT CAST(age AS text) FROM (SELECT NEW.*) AS users); END IF;");
|
|
55
|
+
expect(body).toContain("ELSIF NEW.age IS DISTINCT FROM OLD.age THEN\n NEW.age_text := (SELECT CAST(age AS text) FROM (SELECT NEW.*) AS users);");
|
|
56
|
+
expect(body).toContain("ELSE\n NEW.age := (SELECT CAST(age_text AS integer) FROM (SELECT NEW.*) AS users);");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a plain rename's body is unchanged", () => {
|
|
60
|
+
expect(dualWriteBody({ names, expression: '"email"' })).toContain("IF NEW.login IS NULL THEN NEW.login := NEW.email; ELSE NEW.email := NEW.login; END IF;");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("the reverse is part of what the gate binds", () => {
|
|
64
|
+
const subject = (reverse?: string) =>
|
|
65
|
+
migrationPlanSubject(observation({ state: "migrate", source: { attnum: 2, name: "age_text", type: "text", notNull: false, generated: false, identity: false }, ...(reverse ? { reverse } : {}) }));
|
|
66
|
+
expect(subject("CAST(age AS text)")).toMatchObject({ reverse: "CAST(age AS text)" });
|
|
67
|
+
expect(subject()).not.toHaveProperty("reverse");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("the contract of a rename that kept its default", () => {
|
|
72
|
+
const sequence = { kind: "serial", schema: "app", name: "users_seq_seq", ident: "app.users_seq_seq" } as const;
|
|
73
|
+
|
|
74
|
+
test("a plain default is set on the new column in the contract's transaction, after the old column goes", () => {
|
|
75
|
+
const o = observation({ column: { name: "login", type: "text", default: "'none'" } });
|
|
76
|
+
expect(contractStatements(o)).toEqual([
|
|
77
|
+
"DROP TRIGGER login__chant_sync ON app.users",
|
|
78
|
+
"DROP FUNCTION app.users__login__chant_sync()",
|
|
79
|
+
"ALTER TABLE app.users DROP COLUMN email",
|
|
80
|
+
"ALTER TABLE app.users ALTER COLUMN login SET DEFAULT 'none'",
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a serial's sequence is handed to the new column before the old one is dropped, and stays its default", () => {
|
|
85
|
+
const o = observation({
|
|
86
|
+
column: { name: "login", type: "serial" },
|
|
87
|
+
old: { attnum: 2, name: "email", type: "integer", notNull: true, default: "nextval('app.users_seq_seq'::regclass)", generated: false, identity: false },
|
|
88
|
+
sequence,
|
|
89
|
+
});
|
|
90
|
+
expect(contractStatements(o)).toEqual([
|
|
91
|
+
"DROP TRIGGER login__chant_sync ON app.users",
|
|
92
|
+
"DROP FUNCTION app.users__login__chant_sync()",
|
|
93
|
+
"ALTER SEQUENCE app.users_seq_seq OWNED BY app.users.login",
|
|
94
|
+
"ALTER TABLE app.users DROP COLUMN email",
|
|
95
|
+
"ALTER TABLE app.users ALTER COLUMN login SET DEFAULT nextval('app.users_seq_seq'::regclass)",
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("an identity moves to the new column under its sequence's name and carries on where the old one stood", () => {
|
|
100
|
+
const o = observation({
|
|
101
|
+
column: { name: "login", type: "integer", generated: { kind: "identity", always: true, options: "(CACHE 5)" } },
|
|
102
|
+
old: { attnum: 2, name: "email", type: "integer", notNull: true, generated: false, identity: true },
|
|
103
|
+
sequence: { ...sequence, kind: "identity" },
|
|
104
|
+
});
|
|
105
|
+
expect(contractStatements(o, { lastValue: "41", isCalled: true })).toEqual([
|
|
106
|
+
"DROP TRIGGER login__chant_sync ON app.users",
|
|
107
|
+
"DROP FUNCTION app.users__login__chant_sync()",
|
|
108
|
+
"ALTER TABLE app.users ALTER COLUMN email DROP IDENTITY",
|
|
109
|
+
"ALTER TABLE app.users DROP COLUMN email",
|
|
110
|
+
"ALTER TABLE app.users ALTER COLUMN login ADD GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME app.users_seq_seq CACHE 5)",
|
|
111
|
+
"SELECT pg_catalog.setval('app.users_seq_seq', 41, true)",
|
|
112
|
+
]);
|
|
113
|
+
expect(() => contractStatements(o)).toThrow(/needs its sequence's state/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a rename without a default drops the old column and nothing else, as before", () => {
|
|
117
|
+
expect(contractStatements(observation())).toEqual([
|
|
118
|
+
"DROP TRIGGER login__chant_sync ON app.users",
|
|
119
|
+
"DROP FUNCTION app.users__login__chant_sync()",
|
|
120
|
+
"ALTER TABLE app.users DROP COLUMN email",
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a type change's contract is the plain drop: its trigger went at the switch", () => {
|
|
125
|
+
const t = migrationNames("app", "orders", "amount", "type", "amount");
|
|
126
|
+
const o = observation({
|
|
127
|
+
names: t,
|
|
128
|
+
old: { attnum: 3, name: t.oldColumn, type: "text", notNull: false, generated: false, identity: false },
|
|
129
|
+
column: { name: "amount", type: "numeric", default: "0" },
|
|
130
|
+
});
|
|
131
|
+
delete (o as { trigger?: unknown }).trigger;
|
|
132
|
+
delete (o as { fn?: unknown }).fn;
|
|
133
|
+
expect(contractStatements(o)).toEqual(["ALTER TABLE app.orders DROP COLUMN amount__chant_old"]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("what the Op still refuses", () => {
|
|
138
|
+
const rel: RelationRow = { oid: "16384", kind: "r", partition: false, parents: null, children: null, partkey: null, partattrs: null };
|
|
139
|
+
const source = (over: Partial<ServerColumn> = {}): ServerColumn => ({ attnum: 2, name: "email", type: "text", notNull: false, generated: false, identity: false, ...over });
|
|
140
|
+
const rename = { rule: "SQLPG205", object: "app.users", field: "columns.login", before: "email", after: "login" } as PgChange;
|
|
141
|
+
const retype = { rule: "SQLPG208", object: "app.users", field: "columns.login.type", before: "text", after: "integer" } as PgChange;
|
|
142
|
+
// Every query answers nothing, except the primary key's.
|
|
143
|
+
const client = { query: async (sql: string) => (sql.includes("contype = 'p'") ? [{ name: "id", type: "bigint" }] : []) } as unknown as PostgresClient;
|
|
144
|
+
const run = (extra: { reverse?: string; defaultAtContract?: boolean }, src: ServerColumn, column: { name: string; type: string; default?: string }, changes: PgChange[], isRename = true) =>
|
|
145
|
+
refuseUnsupported(
|
|
146
|
+
{ client, target: { defaultSchema: "app" }, declared: { props: {}, canonical: { schema: "app", name: "users" } }, column: "login", ...extra } as never,
|
|
147
|
+
names,
|
|
148
|
+
rel,
|
|
149
|
+
src,
|
|
150
|
+
column as never,
|
|
151
|
+
changes,
|
|
152
|
+
isRename,
|
|
153
|
+
changes.find((c) => c.rule !== "SQLPG205"),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
test("a rename with a type change names reverse, and goes on with it", async () => {
|
|
157
|
+
await expect(run({}, source(), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(/give that expression as reverse/);
|
|
158
|
+
await expect(run({ reverse: "CAST(login AS text)" }, source(), { name: "login", type: "integer" }, [rename, retype])).resolves.toMatchObject({ batchKey: "id" });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("reverse is refused where nothing needs it, and may not carry the function's quote", async () => {
|
|
162
|
+
await expect(run({ reverse: "x" }, source(), { name: "login", type: "text" }, [rename])).rejects.toThrow(/reverse is for a rename combined with a type change, and this migration is a rename only/);
|
|
163
|
+
await expect(run({ reverse: "x" }, source(), { name: "login", type: "integer" }, [retype], false)).rejects.toThrow(/a type change without a rename/);
|
|
164
|
+
await expect(run({ reverse: "$chant$" }, source(), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(/may not contain \$chant\$/);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a rename with a type change and a default or identity is refused whatever is set", async () => {
|
|
168
|
+
const msg = /also changes its type[\s\S]*Rename first/;
|
|
169
|
+
await expect(run({ reverse: "x", defaultAtContract: true }, source({ default: "'a'::text" }), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(msg);
|
|
170
|
+
await expect(run({ reverse: "x", defaultAtContract: true }, source({ identity: true }), { name: "login", type: "integer" }, [rename, retype])).rejects.toThrow(msg);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a rename of a column with a default needs defaultAtContract, and says what it gives up", async () => {
|
|
174
|
+
const withDefault = source({ default: "'none'::text" });
|
|
175
|
+
const refused = run({}, withDefault, { name: "login", type: "text", default: "'none'" }, [rename]);
|
|
176
|
+
await expect(refused).rejects.toThrow(/Set defaultAtContract: true/);
|
|
177
|
+
await expect(run({}, withDefault, { name: "login", type: "text", default: "'none'" }, [rename])).rejects.toThrow(/explicit NULL gets the default, and UPDATE \.\.\. SET <new> = DEFAULT sets NULL/);
|
|
178
|
+
await expect(run({}, source({ identity: true }), { name: "login", type: "integer" }, [rename])).rejects.toThrow(/Set defaultAtContract: true/);
|
|
179
|
+
await expect(run({ defaultAtContract: true }, withDefault, { name: "login", type: "text", default: "'none'" }, [rename])).resolves.toMatchObject({ batchKey: "id" });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("defaultAtContract is refused on a rename that has no default to wait for", async () => {
|
|
183
|
+
await expect(run({ defaultAtContract: true }, source(), { name: "login", type: "text" }, [rename])).rejects.toThrow(/defaultAtContract is for a rename of a column with a default/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("the Op's configuration", () => {
|
|
188
|
+
test("reverse and defaultAtContract reach every step, and the steps still pass the activity contracts", async () => {
|
|
189
|
+
const config: PostgresMigrationOpConfig = { ...BASE, using: "CAST(age_text AS integer)", reverse: "CAST(age AS text)", defaultAtContract: true };
|
|
190
|
+
const props = propsOf(config);
|
|
191
|
+
const steps = props.phases.flatMap((p) => p.steps).filter((s) => s.kind === "activity" && (s as { fn: string }).fn.startsWith("postgresMigration")) as Array<{ args: Record<string, unknown> }>;
|
|
192
|
+
expect(steps).toHaveLength(9);
|
|
193
|
+
for (const s of steps) expect(s.args).toMatchObject({ reverse: "CAST(age AS text)", defaultAtContract: true });
|
|
194
|
+
const contracts = new Map<string, ActivityContract>();
|
|
195
|
+
collectActivityContracts(contractsModule as unknown as Record<string, unknown>, contracts);
|
|
196
|
+
expect(validateActivitySteps(props, mergeActivityContracts(await loadActivityContracts([]), contracts))).toEqual([]);
|
|
197
|
+
for (const s of propsOf(BASE).phases.flatMap((p) => p.steps)) if (s.kind === "activity") expect((s as { args?: Record<string, unknown> }).args ?? {}).not.toHaveProperty("defaultAtContract");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - A column of a partitioned table: every partition follows the
|
|
11
11
|
* partitioned table.
|
|
12
12
|
* - Refused, each naming why: a partition, an inheritance tree, a column in
|
|
13
|
-
* the partition key,
|
|
13
|
+
* the partition key, a key on a partitioned table, a generated column,
|
|
14
14
|
* a rename of an identity column.
|
|
15
15
|
*
|
|
16
16
|
* Needs Docker; skips cleanly without it.
|
|
@@ -177,7 +177,7 @@ describe.skipIf(!enabled)("what the Op refuses among the rest", () => {
|
|
|
177
177
|
(e: unknown) => String((e as Error).message),
|
|
178
178
|
);
|
|
179
179
|
|
|
180
|
-
test("a partition, an inheritance tree, the partition key,
|
|
180
|
+
test("a partition, an inheritance tree, the partition key, a key on a partitioned table, a generated column, a renamed identity", async () => {
|
|
181
181
|
const parted = (v: string, key = "at") => table("parted", "rf", `CREATE TABLE rf.parted (id bigint, at date, v ${v}, PRIMARY KEY (id, at)) PARTITION BY RANGE (${key})`);
|
|
182
182
|
const p1 = table("parted1", "rf", "CREATE TABLE rf.parted1 PARTITION OF rf.parted FOR VALUES FROM ('2025-01-01') TO ('2026-01-01')", ["parted"]);
|
|
183
183
|
const parent = (v: string) => table("parent", "rf", `CREATE TABLE rf.parent (id bigint PRIMARY KEY, v ${v})`);
|
|
@@ -185,16 +185,16 @@ describe.skipIf(!enabled)("what the Op refuses among the rest", () => {
|
|
|
185
185
|
const ident = (c: string, hint = "") => table("ident", "rf", `CREATE TABLE rf.ident (\n ${c} integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,${hint}\n v text\n)`);
|
|
186
186
|
await apply(writeBuild("rf-v1.json", [RF, parted("integer"), p1, parent("integer"), gen("integer"), ident("id")]));
|
|
187
187
|
await admin!.query("CREATE TABLE rf.child (extra text) INHERITS (rf.parent)");
|
|
188
|
-
await admin!.query("
|
|
188
|
+
await admin!.query("ALTER TABLE rf.parted ADD CONSTRAINT parted_v_key UNIQUE (id, at, v)");
|
|
189
189
|
writeBuild("rf-v2.json", [RF, parted("bigint"), p1, parent("bigint"), gen("bigint"), ident("key", " -- previously: id")]);
|
|
190
190
|
|
|
191
191
|
expect(await refusal("rf.parted1", "v", "rf-v2.json")).toMatch(/^rf\.parted1 is a partition of rf\.parted\. A partition's columns are its partitioned table's: run the Op on rf\.parted/);
|
|
192
192
|
expect(await refusal("rf.parent", "v", "rf-v2.json")).toMatch(/^rf\.parent has inheritance children \(rf\.child\)\. The Op does not migrate inheritance trees: a write to a child table does not fire the parent's dual-write trigger/);
|
|
193
|
-
expect(await refusal("rf.parted", "v", "rf-v2.json")).toMatch(/
|
|
193
|
+
expect(await refusal("rf.parted", "v", "rf-v2.json")).toMatch(/unique constraint parted_v_key on rf\.parted: on a partitioned table a key cannot be attached from an index built CONCURRENTLY/);
|
|
194
194
|
expect(await refusal("rf.gen", "g", "rf-v2.json")).toMatch(/^rf\.gen\.g is a generated column\. There is no expand and contract for one/);
|
|
195
195
|
expect(await refusal("rf.ident", "key", "rf-v2.json")).toMatch(/sequence rf\.ident_id_seq gives the column its values \(an identity column\); during a rename/);
|
|
196
196
|
|
|
197
|
-
await admin!.query("
|
|
197
|
+
await admin!.query("ALTER TABLE rf.parted DROP CONSTRAINT parted_v_key");
|
|
198
198
|
writeBuild("rf-v3.json", [RF, parted("integer", "v"), p1]);
|
|
199
199
|
await admin!.query("DROP TABLE rf.parted");
|
|
200
200
|
await apply(writeBuild("rf-v3.json", [RF, table("parted", "rf", "CREATE TABLE rf.parted (id bigint, at date, v integer, PRIMARY KEY (id, v)) PARTITION BY RANGE (v)")]));
|
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
* - `done`: the server holds the declared column and nothing of the
|
|
25
25
|
* migration is left.
|
|
26
26
|
*
|
|
27
|
+
* A partitioned table's indexes are carried over per partition (`./steps.ts`).
|
|
27
28
|
* Anything else is refused with a {@link MigrationRefusal}, naming what to
|
|
28
29
|
* do: a table that is not there, a partition (run it on the partitioned
|
|
29
30
|
* table) or an inheritance tree; no primary key to batch by; a column in the
|
|
30
31
|
* partition key; a generated column; a column change the Op does not make
|
|
31
|
-
* (a rename and a type change
|
|
32
|
-
* or a sequence); something that uses the column and is
|
|
32
|
+
* (a rename and a type change without `reverse`, a rename of a column with a
|
|
33
|
+
* default or a sequence without `defaultAtContract`); something that uses the column and is
|
|
33
34
|
* not carried over to the new one (`./carry.ts`: indexes, key, check and
|
|
34
35
|
* foreign key constraints and declared views are); a publication that sends
|
|
35
36
|
* it; a working object chant did not make.
|
|
@@ -48,9 +49,9 @@ import { POSTGRES_ENTITY_TYPES } from "../entity-types";
|
|
|
48
49
|
import type { ColumnDef } from "../entities";
|
|
49
50
|
import type { DeclaredPgObject } from "../apply/statements";
|
|
50
51
|
import { col, migrationNames, MIGRATION_TRAILER_KEY, pgName, type MigrationChange, type MigrationNames } from "./names";
|
|
51
|
-
import { publicationsOf, type PublicationHit } from "./replication";
|
|
52
|
+
import { publicationsOf, subscriptionRefusal, subscriptionsOf, type PublicationHit } from "./replication";
|
|
52
53
|
import { keyText, type KeyColumn } from "./batches";
|
|
53
|
-
import { carriedStates, carriedSubject, discoverDependents, replaceColumn, storedType, type CarriedObject, type CarriedState, type CarriedView, type ColumnSequence, type Dependents } from "./carry";
|
|
54
|
+
import { carriedStates, carriedSubject, columnSequence, discoverDependents, replaceColumn, storedType, type CarriedObject, type CarriedState, type CarriedView, type ColumnSequence, type Dependents } from "./carry";
|
|
54
55
|
|
|
55
56
|
/** A refusal: the migration cannot start, or cannot go on, for a reason a person has to act on. */
|
|
56
57
|
export class MigrationRefusal extends Error {
|
|
@@ -102,7 +103,7 @@ export interface MigrationObservation {
|
|
|
102
103
|
carriedStates: Map<string, CarriedState>;
|
|
103
104
|
/** The views that read the old column, made again at the switch. */
|
|
104
105
|
views: CarriedView[];
|
|
105
|
-
/** The sequence that gives the old column its values (identity or serial)
|
|
106
|
+
/** The sequence that gives the old column its values (identity or serial): moved to the new column at the switch for a type change, at the contract for a rename. */
|
|
106
107
|
sequence?: ColumnSequence;
|
|
107
108
|
/** The table is partitioned: every step runs on it, and its partitions follow. */
|
|
108
109
|
partitioned: boolean;
|
|
@@ -118,6 +119,10 @@ export interface MigrationObservation {
|
|
|
118
119
|
changes: PgChange[];
|
|
119
120
|
/** The expression the new column's value is computed with, over the old row's columns. */
|
|
120
121
|
expression: string;
|
|
122
|
+
/** A rename with a type change: the expression over the new row's columns that gives the old column's value. */
|
|
123
|
+
reverse?: string;
|
|
124
|
+
/** A rename keeps its default, serial or identity on the old column until the contract (`defaultAtContract`). */
|
|
125
|
+
defaultAtContract?: boolean;
|
|
121
126
|
publications: PublicationHit[];
|
|
122
127
|
major: number;
|
|
123
128
|
}
|
|
@@ -132,6 +137,10 @@ export interface ObserveInput {
|
|
|
132
137
|
objects?: readonly DeclaredPgObject[];
|
|
133
138
|
/** The type change's expression, over the old row's columns. Default: `CAST(<column> AS <declared type>)`. */
|
|
134
139
|
using?: string;
|
|
140
|
+
/** A rename with a type change: the expression over the new row's columns that gives the old column's value. */
|
|
141
|
+
reverse?: string;
|
|
142
|
+
/** A rename of a column with a default, serial or identity: the declared default lands on the new column at the contract, not the switch. */
|
|
143
|
+
defaultAtContract?: boolean;
|
|
135
144
|
/** The major the build targets; the server's own wins. */
|
|
136
145
|
major?: number;
|
|
137
146
|
}
|
|
@@ -216,7 +225,7 @@ async function columnChanges(input: ObserveInput): Promise<{ changes: PgChange[]
|
|
|
216
225
|
|
|
217
226
|
const ruleText = (c: PgChange) => `${c.rule} ${PG_CLASSIFIER_RULES[c.rule].title} (${c.field}${c.before !== undefined || c.after !== undefined ? `: ${c.before ?? "-"} -> ${c.after ?? "-"}` : ""})`;
|
|
218
227
|
|
|
219
|
-
interface RelationRow {
|
|
228
|
+
export interface RelationRow {
|
|
220
229
|
oid: string;
|
|
221
230
|
kind: string;
|
|
222
231
|
partition: boolean;
|
|
@@ -371,7 +380,8 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
|
|
|
371
380
|
);
|
|
372
381
|
},
|
|
373
382
|
);
|
|
374
|
-
|
|
383
|
+
const retyped = change === "rename" && input.reverse !== undefined;
|
|
384
|
+
if (change === "type" || retyped) {
|
|
375
385
|
expression = input.using ?? `CAST(${col(sourceName)} AS ${storedType(column.type)})`;
|
|
376
386
|
if (expression.includes("$chant$")) throw new MigrationRefusal(`${where}: the using expression may not contain $chant$, which quotes the dual-write function's body`);
|
|
377
387
|
} else if (input.using !== undefined) {
|
|
@@ -379,7 +389,7 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
|
|
|
379
389
|
}
|
|
380
390
|
// The declared type as the classifier compared it: the server's spelling (character varying(10), not varchar(10)).
|
|
381
391
|
const serial = storedType(column.type) !== column.type;
|
|
382
|
-
const expectedType = change === "type" ? (serial ? storedType(column.type)! : String(typeChange?.after ?? column.type)) : source.type;
|
|
392
|
+
const expectedType = change === "type" || retyped ? (serial ? storedType(column.type)! : String(typeChange?.after ?? column.type)) : source.type;
|
|
383
393
|
if (newColumn && newColumn.type !== expectedType) {
|
|
384
394
|
throw new MigrationRefusal(
|
|
385
395
|
`${where}: ${newColumn.name} was made as ${newColumn.type}, from another declaration of ${input.column} (now ${column.type}). The run fails so onFailure drops it, and the next run starts again from the current declaration.`,
|
|
@@ -395,6 +405,14 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
|
|
|
395
405
|
`A migration of the publisher and the subscriber together is chant #3332.`,
|
|
396
406
|
);
|
|
397
407
|
}
|
|
408
|
+
const subscriptions = await subscriptionsOf(client, schema, table, major);
|
|
409
|
+
if (subscriptions.length > 0) throw new MigrationRefusal(subscriptionRefusal(where, input.column, subscriptions));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// After a rename's switch the old column still holds the default or the identity the contract moves, and its sequence is read for it.
|
|
413
|
+
if (state === "switched" && old && (old.identity || old.default?.includes("nextval("))) {
|
|
414
|
+
const seq = await columnSequence(client, rel.oid, old.attnum);
|
|
415
|
+
if (seq) dependents = { ...dependents, sequence: seq };
|
|
398
416
|
}
|
|
399
417
|
|
|
400
418
|
return {
|
|
@@ -419,13 +437,15 @@ export async function observeMigration(input: ObserveInput): Promise<MigrationOb
|
|
|
419
437
|
...(check ? { check } : {}),
|
|
420
438
|
changes,
|
|
421
439
|
expression,
|
|
440
|
+
...(input.reverse !== undefined && state === "migrate" ? { reverse: input.reverse } : {}),
|
|
441
|
+
...(input.defaultAtContract && state === "migrate" ? { defaultAtContract: true } : {}),
|
|
422
442
|
publications,
|
|
423
443
|
major,
|
|
424
444
|
};
|
|
425
445
|
}
|
|
426
446
|
|
|
427
447
|
/** The checks that hold only while there is something to migrate. Returns the batch key and what the Op carries over. */
|
|
428
|
-
async function refuseUnsupported(
|
|
448
|
+
export async function refuseUnsupported(
|
|
429
449
|
input: ObserveInput,
|
|
430
450
|
names: MigrationNames,
|
|
431
451
|
rel: RelationRow,
|
|
@@ -437,20 +457,38 @@ async function refuseUnsupported(
|
|
|
437
457
|
): Promise<{ batchKey: string; keyColumns: KeyColumn[]; dependents: Pick<Dependents, "carried" | "views" | "sequence"> }> {
|
|
438
458
|
const where = `${names.schema}.${names.table}`;
|
|
439
459
|
const oid = rel.oid;
|
|
440
|
-
if (rename && typeChange) {
|
|
460
|
+
if (rename && typeChange && input.reverse === undefined) {
|
|
441
461
|
throw new MigrationRefusal(
|
|
442
462
|
`${where}: ${source.name} is renamed to ${names.column} and its type changes (${typeChange.before} -> ${typeChange.after}) in one declaration. ` +
|
|
443
|
-
`
|
|
444
|
-
`(
|
|
463
|
+
`Both names stay written until the contract, so a writer of the new name needs its value converted back to the old type: give that expression as reverse ` +
|
|
464
|
+
`(over the row's columns, the new column by its declared name; using gives the new value from the old one). The verification fails the run unless reverse over every new value gives the old one back. ` +
|
|
465
|
+
`Or migrate one at a time: declare the rename with the old type, run the Op to its end, then change the type.`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (input.reverse !== undefined && !(rename && typeChange)) {
|
|
469
|
+
throw new MigrationRefusal(`${where}.${names.column}: reverse is for a rename combined with a type change, and this migration is ${rename ? "a rename only" : "a type change without a rename"}`);
|
|
470
|
+
}
|
|
471
|
+
if (input.reverse !== undefined && input.reverse.includes("$chant$")) {
|
|
472
|
+
throw new MigrationRefusal(`${where}: the reverse expression may not contain $chant$, which quotes the dual-write function's body`);
|
|
473
|
+
}
|
|
474
|
+
const hasDefault = source.default !== undefined || column.default !== undefined;
|
|
475
|
+
if (rename && typeChange && (hasDefault || source.identity || column.generated?.kind === "identity")) {
|
|
476
|
+
throw new MigrationRefusal(
|
|
477
|
+
`${where}: ${source.name} has a default, serial or identity, and its rename also changes its type. The default is an expression of the old type; converting it, and the sequence's type, ` +
|
|
478
|
+
`is a third conversion on top of using and reverse that nothing verifies. Rename first (defaultAtContract: true), run the Op to its end, then change the type.`,
|
|
445
479
|
);
|
|
446
480
|
}
|
|
447
|
-
if (rename && (
|
|
481
|
+
if (rename && (hasDefault || source.identity) && !input.defaultAtContract) {
|
|
448
482
|
throw new MigrationRefusal(
|
|
449
|
-
`${where}: ${source.name} has a default (${source.default ?? column.default}). During a rename both columns are written, each from the other, ` +
|
|
483
|
+
`${where}: ${source.name} has a default (${source.default ?? column.default ?? "identity"}). During a rename both columns are written, each from the other, ` +
|
|
450
484
|
`and a row inserted by a writer that names one column would get the other's default, which the trigger cannot tell from a value it was given. ` +
|
|
451
|
-
`
|
|
485
|
+
`Set defaultAtContract: true to leave the new column without the default until the contract, which gives the default to the new column in the contract's transaction; ` +
|
|
486
|
+
`until then an INSERT that names the new column with an explicit NULL gets the default, and UPDATE ... SET <new> = DEFAULT sets NULL. Or drop the default first (the applier does), rename, then declare it again.`,
|
|
452
487
|
);
|
|
453
488
|
}
|
|
489
|
+
if (input.defaultAtContract && !(rename && (hasDefault || source.identity))) {
|
|
490
|
+
throw new MigrationRefusal(`${where}.${names.column}: defaultAtContract is for a rename of a column with a default, serial or identity, and this migration is not one`);
|
|
491
|
+
}
|
|
454
492
|
if (source.generated || (column.generated && column.generated.kind !== "identity")) {
|
|
455
493
|
throw new MigrationRefusal(
|
|
456
494
|
`${where}.${source.name} is a generated column. There is no expand and contract for one: Postgres cannot make an existing column generated, so the new column could only be added generated, ` +
|
|
@@ -475,6 +513,7 @@ async function refuseUnsupported(
|
|
|
475
513
|
declared: input.declared,
|
|
476
514
|
defaultSchema: input.target.defaultSchema,
|
|
477
515
|
partitioned: rel.kind === "p",
|
|
516
|
+
...(input.defaultAtContract ? { keepSequence: true } : {}),
|
|
478
517
|
});
|
|
479
518
|
if (dependents.refused.length > 0) {
|
|
480
519
|
throw new MigrationRefusal(
|
|
@@ -510,6 +549,8 @@ export function migrationPlanSubject(o: MigrationObservation): Record<string, un
|
|
|
510
549
|
from: o.source ? { name: o.source.name, type: o.source.type, notNull: o.source.notNull, default: o.source.default ?? null } : null,
|
|
511
550
|
to: { name: o.column.name, type: o.column.type ?? null, notNull: o.column.notNull === true, default: o.column.default ?? null, comment: o.column.comment ?? null },
|
|
512
551
|
expression: o.expression,
|
|
552
|
+
...(o.reverse !== undefined ? { reverse: o.reverse } : {}),
|
|
553
|
+
...(o.defaultAtContract ? { defaultAtContract: true } : {}),
|
|
513
554
|
batchKey: o.batchKey,
|
|
514
555
|
rules: o.changes.map((c) => c.rule),
|
|
515
556
|
...(o.carried.length > 0 || o.views.length > 0 || o.sequence ? { dependents: carriedSubject(o) } : {}),
|