@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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goose and golang-migrate directories replayed into a Postgres emulator
|
|
3
|
+
* (#4400): the replay records the tables and columns, a dbt member links to
|
|
4
|
+
* one of them, `chant workspace check` passes, and a migration that drops the
|
|
5
|
+
* linked column fails it after the next replay. Also the `--check` of the
|
|
6
|
+
* recorded file.
|
|
7
|
+
*
|
|
8
|
+
* Needs Docker; skips cleanly without it. One throwaway `postgres` container,
|
|
9
|
+
* a fresh database for each replay. Not part of the unit suite.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
17
|
+
import { cleanScratch, declaration, REPO, repo } from "../../../packages/core/src/workspace/__fixtures__/contract-repo";
|
|
18
|
+
import { workingTree } from "@intentius/chant/workspace/tree";
|
|
19
|
+
import { REPLAY_FILE } from "@intentius/chant/workspace/migration-dirs";
|
|
20
|
+
import { dockerAvailable, startTestPostgres, type TestPostgres } from "./postgres/testing/server";
|
|
21
|
+
import { renderSnapshot, replayDirectory, type ReplayDb, type ReplayKind } from "./migration-replay";
|
|
22
|
+
|
|
23
|
+
const enabled = await dockerAvailable();
|
|
24
|
+
const TIMEOUT = 600_000;
|
|
25
|
+
const LEXICON = "@intentius/chant-lexicon-sql";
|
|
26
|
+
const VERSION = (JSON.parse(readFileSync(join(REPO, "lexicons", "sql", "package.json"), "utf-8")) as { version: string }).version;
|
|
27
|
+
let server: TestPostgres | undefined;
|
|
28
|
+
let databases = 0;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
if (enabled) server = await startTestPostgres();
|
|
32
|
+
}, TIMEOUT);
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
await server?.stop();
|
|
36
|
+
cleanScratch();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function chant(cwd: string, ...argv: string[]) {
|
|
40
|
+
return spawnSync(process.execPath, ["--import", pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href, join(REPO, "packages/core/src/cli/main.ts"), ...argv], {
|
|
41
|
+
cwd,
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
timeout: TIMEOUT,
|
|
44
|
+
env: { ...process.env, NO_COLOR: "1", TSX_DISABLE_CACHE: "1" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** An empty database on the emulator, as a {@link ReplayDb}. */
|
|
49
|
+
async function emptyDb(): Promise<ReplayDb> {
|
|
50
|
+
const admin = await server!.connect();
|
|
51
|
+
const name = `replay_${String((databases += 1))}`;
|
|
52
|
+
await admin.query(`CREATE DATABASE ${name}`);
|
|
53
|
+
await admin.end();
|
|
54
|
+
const client = await server!.connect(name);
|
|
55
|
+
return { dialect: "postgres", run: async (sql) => void (await client.query(sql)), rows: (sql) => client.query(sql), close: () => client.end() };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const SOURCES = "version: 2\n\nsources:\n - name: shop\n schema: public\n tables:\n - name: orders\n columns:\n - name: id\n - name: customer_id\n";
|
|
59
|
+
|
|
60
|
+
const FIXTURES: Record<"goose" | "golang-migrate", { first: Record<string, string>; drop: Record<string, string> }> = {
|
|
61
|
+
goose: {
|
|
62
|
+
first: {
|
|
63
|
+
"db/00001_orders.sql": "-- +goose Up\nCREATE TABLE orders (id bigint PRIMARY KEY, customer_id bigint NOT NULL);\n-- +goose Down\nDROP TABLE orders;\n",
|
|
64
|
+
},
|
|
65
|
+
drop: { "db/00002_drop_customer.sql": "-- +goose Up\nALTER TABLE orders DROP COLUMN customer_id;\n-- +goose Down\nALTER TABLE orders ADD COLUMN customer_id bigint;\n" },
|
|
66
|
+
},
|
|
67
|
+
"golang-migrate": {
|
|
68
|
+
first: {
|
|
69
|
+
"db/1_orders.up.sql": "CREATE TABLE orders (id bigint PRIMARY KEY, customer_id bigint NOT NULL);\n",
|
|
70
|
+
"db/1_orders.down.sql": "DROP TABLE orders;\n",
|
|
71
|
+
},
|
|
72
|
+
drop: { "db/2_drop_customer.up.sql": "ALTER TABLE orders DROP COLUMN customer_id;\n", "db/2_drop_customer.down.sql": "ALTER TABLE orders ADD COLUMN customer_id bigint;\n" },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
async function workspace(kind: "goose" | "golang-migrate", extra: Record<string, string>): Promise<string> {
|
|
77
|
+
const root = repo({
|
|
78
|
+
"chant.workspace.json": declaration(
|
|
79
|
+
[
|
|
80
|
+
{ name: "db", dir: "db", kind },
|
|
81
|
+
{ name: "analytics", dir: "analytics", kind: "dbt", fields: { sqlMember: "db" } },
|
|
82
|
+
],
|
|
83
|
+
{ pins: [{ package: LEXICON, version: VERSION }] },
|
|
84
|
+
),
|
|
85
|
+
"analytics/dbt_project.yml": "name: analytics\nversion: '1.0'\nprofile: analytics\n",
|
|
86
|
+
"analytics/models/staging/sources.yml": SOURCES,
|
|
87
|
+
".gitignore": "node_modules\n",
|
|
88
|
+
...FIXTURES[kind].first,
|
|
89
|
+
...extra,
|
|
90
|
+
});
|
|
91
|
+
symlinkSync(join(REPO, "node_modules"), join(root, "node_modules"), "dir");
|
|
92
|
+
return root;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Replay `root/db` into a fresh database and write the snapshot beside the migrations. */
|
|
96
|
+
async function record(root: string, kind: ReplayKind): Promise<void> {
|
|
97
|
+
const db = await emptyDb();
|
|
98
|
+
try {
|
|
99
|
+
const snapshot = await replayDirectory(db, kind, workingTree(join(root, "db")), "");
|
|
100
|
+
writeFileSync(join(root, "db", REPLAY_FILE), renderSnapshot(snapshot));
|
|
101
|
+
} finally {
|
|
102
|
+
await db.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe.skipIf(!enabled)("migration directories replayed into the emulator (#4400)", () => {
|
|
107
|
+
for (const kind of ["goose", "golang-migrate"] as const) {
|
|
108
|
+
test(
|
|
109
|
+
`${kind}: the replayed columns are outputs a dbt member links to, and a migration that drops a linked column fails workspace check`,
|
|
110
|
+
async () => {
|
|
111
|
+
const ok = await workspace(kind, {});
|
|
112
|
+
await record(ok, kind);
|
|
113
|
+
expect(JSON.parse(readFileSync(join(ok, "db", REPLAY_FILE), "utf-8")).outputs).toEqual(["public.orders", "public.orders.id", "public.orders.customer_id"]);
|
|
114
|
+
const passed = chant(ok, "workspace", "check");
|
|
115
|
+
expect(passed.status, passed.stdout + passed.stderr).toBe(0);
|
|
116
|
+
|
|
117
|
+
const dropped = await workspace(kind, FIXTURES[kind].drop);
|
|
118
|
+
await record(dropped, kind);
|
|
119
|
+
const failed = chant(dropped, "workspace", "check");
|
|
120
|
+
expect(failed.status).toBe(1);
|
|
121
|
+
const text = failed.stdout + failed.stderr;
|
|
122
|
+
expect(text).toContain("analytics/models/staging/sources.yml:10");
|
|
123
|
+
expect(text).toContain("WSP093");
|
|
124
|
+
expect(text).toMatch(/link to db output public\.orders\.customer_id .*does not resolve/);
|
|
125
|
+
},
|
|
126
|
+
TIMEOUT,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
test(
|
|
130
|
+
`${kind}: a migration added after the replay leaves the outputs unresolved until the replay runs again`,
|
|
131
|
+
async () => {
|
|
132
|
+
const root = await workspace(kind, {});
|
|
133
|
+
await record(root, kind);
|
|
134
|
+
for (const [path, text] of Object.entries(FIXTURES[kind].drop)) writeFileSync(join(root, path), text);
|
|
135
|
+
const stale = chant(root, "workspace", "check");
|
|
136
|
+
expect(stale.stdout + stale.stderr).toContain("changed since chant.replay.json was recorded");
|
|
137
|
+
},
|
|
138
|
+
TIMEOUT,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
test(
|
|
143
|
+
"a migration that fails stops the replay and names its file",
|
|
144
|
+
async () => {
|
|
145
|
+
const root = await workspace("golang-migrate", { "db/2_bad.up.sql": "ALTER TABLE nowhere ADD COLUMN x int;\n" });
|
|
146
|
+
const db = await emptyDb();
|
|
147
|
+
try {
|
|
148
|
+
await expect(replayDirectory(db, "golang-migrate", workingTree(join(root, "db")), "")).rejects.toThrow(/2_bad\.up\.sql: statement 1 failed/);
|
|
149
|
+
} finally {
|
|
150
|
+
await db.close();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
TIMEOUT,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaying a migration directory (#4400), against a fake server: the
|
|
3
|
+
* statements it sends, the catalog names it records, and the round trip into
|
|
4
|
+
* the kind reader's outputs. The emulator runs in `migration-replay.e2e.test.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { LINK_READERS } from "@intentius/chant/workspace/kind-link-readers";
|
|
9
|
+
import { REPLAY_FILE } from "@intentius/chant/workspace/migration-dirs";
|
|
10
|
+
import type { WorkspaceTree } from "@intentius/chant/workspace/tree";
|
|
11
|
+
import { applySteps, gooseUpStatements, readCatalogOutputs, renderSnapshot, replayDirectory, replaySteps, splitStatements, type ReplayDb } from "./migration-replay";
|
|
12
|
+
|
|
13
|
+
function memoryTree(files: Record<string, string>): WorkspaceTree {
|
|
14
|
+
const dirs = new Set([""]);
|
|
15
|
+
for (const f of Object.keys(files)) {
|
|
16
|
+
const parts = f.split("/");
|
|
17
|
+
for (let i = 1; i < parts.length; i++) dirs.add(parts.slice(0, i).join("/"));
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
label: "",
|
|
21
|
+
stat: (p) => (p in files ? "file" : dirs.has(p) ? "dir" : undefined),
|
|
22
|
+
list(p) {
|
|
23
|
+
if (!dirs.has(p)) return undefined;
|
|
24
|
+
const prefix = p === "" ? "" : `${p}/`;
|
|
25
|
+
const seen = new Map<string, "file" | "dir">();
|
|
26
|
+
for (const f of Object.keys(files)) {
|
|
27
|
+
if (!f.startsWith(prefix)) continue;
|
|
28
|
+
const rest = f.slice(prefix.length);
|
|
29
|
+
seen.set(rest.split("/")[0]!, rest.includes("/") ? "dir" : "file");
|
|
30
|
+
}
|
|
31
|
+
return [...seen].map(([name, type]) => ({ name, type }));
|
|
32
|
+
},
|
|
33
|
+
read(p) {
|
|
34
|
+
if (!(p in files)) throw new Error(`no such file ${p}`);
|
|
35
|
+
return files[p]!;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A server that records what it is sent and answers the catalog query with `catalog`. */
|
|
41
|
+
function fakeDb(dialect: "postgres" | "clickhouse", catalog: () => Record<string, unknown>[], failOn?: RegExp): ReplayDb & { sent: string[] } {
|
|
42
|
+
const sent: string[] = [];
|
|
43
|
+
return {
|
|
44
|
+
dialect,
|
|
45
|
+
sent,
|
|
46
|
+
async run(sql) {
|
|
47
|
+
if (failOn?.test(sql)) throw new Error("relation does not exist");
|
|
48
|
+
sent.push(sql);
|
|
49
|
+
},
|
|
50
|
+
rows: async () => catalog(),
|
|
51
|
+
close: async () => {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("splitting statements", () => {
|
|
56
|
+
test("semicolons inside strings, identifiers, comments and dollar quotes do not end a statement", () => {
|
|
57
|
+
const sql = `create table a (x text default 'a;b');\n-- c; d\ncreate function f() returns int as $$ begin return 1; end $$ language plpgsql;\n/* x; */ select "a;b" from a;\n;;`;
|
|
58
|
+
expect(splitStatements(sql)).toEqual([
|
|
59
|
+
"create table a (x text default 'a;b')",
|
|
60
|
+
"-- c; d\ncreate function f() returns int as $$ begin return 1; end $$ language plpgsql",
|
|
61
|
+
'/* x; */ select "a;b" from a',
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a final statement without a semicolon is kept, and a comment alone is dropped", () => {
|
|
66
|
+
expect(splitStatements("select 1;\n-- done\n")).toEqual(["select 1"]);
|
|
67
|
+
expect(splitStatements("select 1;\nselect 2")).toEqual(["select 1", "select 2"]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("goose Up statements", () => {
|
|
72
|
+
test("only the Up section runs, and StatementBegin to StatementEnd is one statement", () => {
|
|
73
|
+
const text = [
|
|
74
|
+
"-- +goose Up",
|
|
75
|
+
"create table a (id int);",
|
|
76
|
+
"-- +goose StatementBegin",
|
|
77
|
+
"create function f() returns int as $$ begin return 1; end; $$ language plpgsql;",
|
|
78
|
+
"select 1;",
|
|
79
|
+
"-- +goose StatementEnd",
|
|
80
|
+
"alter table a add c int;",
|
|
81
|
+
"-- +goose Down",
|
|
82
|
+
"drop table a;",
|
|
83
|
+
].join("\n");
|
|
84
|
+
expect(gooseUpStatements(text)).toEqual(["create table a (id int)", "create function f() returns int as $$ begin return 1; end; $$ language plpgsql;\nselect 1;", "alter table a add c int"]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("the steps per kind", () => {
|
|
89
|
+
test("golang-migrate runs the up files in version order, never the down files", () => {
|
|
90
|
+
const { steps } = replaySteps("golang-migrate", memoryTree({ "10_b.up.sql": "create table b (id int);", "2_a.up.sql": "create table a (id int);", "2_a.down.sql": "drop table a;" }), "");
|
|
91
|
+
expect(steps.map((s) => [s.migration.file, s.statements])).toEqual([["2_a.up.sql", ["create table a (id int)"]], ["10_b.up.sql", ["create table b (id int)"]]]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("Flyway substitutes placeholders, runs versioned then repeatable, and skips undo and callbacks", () => {
|
|
95
|
+
const { steps } = replaySteps(
|
|
96
|
+
"flyway",
|
|
97
|
+
memoryTree({
|
|
98
|
+
"flyway.conf": "flyway.placeholders.t=orders\n",
|
|
99
|
+
"V1__a.sql": "create table ${t} (id int);",
|
|
100
|
+
"R__v.sql": "create or replace view v as select 1;",
|
|
101
|
+
"U1__a.sql": "drop table orders;",
|
|
102
|
+
"beforeMigrate.sql": "select 1;",
|
|
103
|
+
}),
|
|
104
|
+
"",
|
|
105
|
+
);
|
|
106
|
+
expect(steps.map((s) => s.statements)).toEqual([["create table orders (id int)"], ["create or replace view v as select 1"]]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a Flyway placeholder with no value fails naming the file and line", () => {
|
|
110
|
+
expect(() => replaySteps("flyway", memoryTree({ "V1__a.sql": "select 1;\ncreate table ${nope} (id int);" }), "")).toThrow(/V1__a\.sql:2: no value for the placeholder \$\{nope\}/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("replaying", () => {
|
|
115
|
+
test("a failing statement names the migration file", async () => {
|
|
116
|
+
const { steps } = replaySteps("golang-migrate", memoryTree({ "1_a.up.sql": "create table a (id int);", "2_b.up.sql": "alter table missing add c int;" }), "");
|
|
117
|
+
await expect(applySteps(fakeDb("postgres", () => [], /missing/), steps)).rejects.toThrow(/2_b\.up\.sql: statement 1 failed: relation does not exist/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("the catalog rows become <schema>.<table> and <schema>.<table>.<column>", async () => {
|
|
121
|
+
const db = fakeDb("clickhouse", () => [
|
|
122
|
+
{ schema: "app", relation: "events", col: "ts" },
|
|
123
|
+
{ schema: "app", relation: "events", col: "kind" },
|
|
124
|
+
{ schema: "app", relation: "empty", col: null },
|
|
125
|
+
]);
|
|
126
|
+
expect(await readCatalogOutputs(db)).toEqual(["app.events", "app.events.ts", "app.events.kind", "app.empty"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a server that already has tables is refused", async () => {
|
|
130
|
+
await expect(replayDirectory(fakeDb("postgres", () => [{ schema: "public", relation: "old", col: "id" }]), "goose", memoryTree({}), "")).rejects.toThrow(/already has public\.old/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a dialect mismatch is refused before anything runs", async () => {
|
|
134
|
+
const db = fakeDb("clickhouse", () => []);
|
|
135
|
+
await expect(replayDirectory(db, "goose", memoryTree({ "1_a.sql": "-- +goose Up\nselect 1;" }), "")).rejects.toThrow(/migrations are postgres and the server is clickhouse/);
|
|
136
|
+
expect(db.sent).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("the snapshot a replay writes is what the kind reader reads back as outputs, until a migration changes", async () => {
|
|
140
|
+
const files: Record<string, string> = {
|
|
141
|
+
"1_users.sql": "-- +goose Up\ncreate table users (id int, email text);\n-- +goose Down\ndrop table users;\n",
|
|
142
|
+
"2_orders.sql": "-- +goose Up\ncreate table orders (id int, user_id int);\n",
|
|
143
|
+
};
|
|
144
|
+
let created = false;
|
|
145
|
+
const db = fakeDb("postgres", () =>
|
|
146
|
+
created
|
|
147
|
+
? [
|
|
148
|
+
{ schema: "public", relation: "users", col: "id" },
|
|
149
|
+
{ schema: "public", relation: "users", col: "email" },
|
|
150
|
+
{ schema: "public", relation: "orders", col: "id" },
|
|
151
|
+
{ schema: "public", relation: "orders", col: "user_id" },
|
|
152
|
+
]
|
|
153
|
+
: [],
|
|
154
|
+
);
|
|
155
|
+
const original = db.run;
|
|
156
|
+
db.run = async (sql) => {
|
|
157
|
+
await original(sql);
|
|
158
|
+
created = true;
|
|
159
|
+
};
|
|
160
|
+
const snapshot = await replayDirectory(db, "goose", memoryTree(files), "");
|
|
161
|
+
expect(db.sent).toEqual(["create table users (id int, email text)", "create table orders (id int, user_id int)"]);
|
|
162
|
+
expect(snapshot.outputs).toEqual(["public.users", "public.users.id", "public.users.email", "public.orders", "public.orders.id", "public.orders.user_id"]);
|
|
163
|
+
|
|
164
|
+
const withSnapshot = { ...files, [REPLAY_FILE]: renderSnapshot(snapshot) };
|
|
165
|
+
const read = (f: Record<string, string>) => LINK_READERS["goose-dir"]({ member: { name: "db" } as never, dir: "", fields: null, tree: memoryTree(f) });
|
|
166
|
+
expect(read(withSnapshot).outputs!.map((o) => o.name)).toEqual(snapshot.outputs);
|
|
167
|
+
expect(read(withSnapshot).gaps).toEqual([]);
|
|
168
|
+
|
|
169
|
+
const dropped = read({ ...withSnapshot, "3_drop.sql": "-- +goose Up\nalter table orders drop column user_id;\n" });
|
|
170
|
+
expect(dropped.outputs).toBeUndefined();
|
|
171
|
+
expect(dropped.gaps[0]).toMatchObject({ outputs: true, message: expect.stringContaining("changed since") });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay a migration directory into an empty server and read the catalog
|
|
3
|
+
* (#4400). The workspace kinds for goose, golang-migrate,
|
|
4
|
+
* clickhouse-migrations and Flyway list a directory's migrations without
|
|
5
|
+
* running anything (`packages/core/src/workspace/migration-dirs.ts`); the
|
|
6
|
+
* tables and columns they produce come from here.
|
|
7
|
+
*
|
|
8
|
+
* chant sql replay <dir> --kind <kind> --env <env> [--dir-field migrationsDir=<sub>] [--check]
|
|
9
|
+
*
|
|
10
|
+
* The replay applies each migration's up statements in the order the kind's
|
|
11
|
+
* reader lists them, then reads the tables, views and columns the server has
|
|
12
|
+
* and writes them as `chant.replay.json` beside the migrations, named by the
|
|
13
|
+
* contract of #3931: `<schema>.<table>` and `<schema>.<table>.<column>` for
|
|
14
|
+
* Postgres, `<database>.<table>` and `<database>.<table>.<column>` for
|
|
15
|
+
* ClickHouse. The file also holds a digest of the files it ran, so the
|
|
16
|
+
* reader knows when a migration changed after it. `--check` replays and
|
|
17
|
+
* compares instead of writing, exiting 1 when the file differs, the way
|
|
18
|
+
* `chant sql types --check` does.
|
|
19
|
+
*
|
|
20
|
+
* What it runs per kind:
|
|
21
|
+
* - goose: the `-- +goose Up` section of each file; `StatementBegin` to
|
|
22
|
+
* `StatementEnd` is one statement.
|
|
23
|
+
* - golang-migrate and clickhouse-migrations: the whole up file.
|
|
24
|
+
* - Flyway: the SQL files of the locations, versioned in version order and
|
|
25
|
+
* then repeatable ones, with placeholders substituted. Baseline and undo
|
|
26
|
+
* files are not run.
|
|
27
|
+
*
|
|
28
|
+
* The server must hold no table, view or column of its own: the replay
|
|
29
|
+
* refuses a server that does, since it cannot tell what the migrations made.
|
|
30
|
+
* It runs the migrations' SQL against the server `--env` binds, so point it
|
|
31
|
+
* at a throwaway server or emulator.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { join, relative, resolve } from "node:path";
|
|
36
|
+
import type { CommandGroupContext } from "@intentius/chant/cli/command-group";
|
|
37
|
+
import { LINK_READERS, type KindReadMigration, type KindReaderInput } from "@intentius/chant/workspace/kind-link-readers";
|
|
38
|
+
import { migrationDigest, REPLAY_FILE, replayFiles, type ReplaySnapshot } from "@intentius/chant/workspace/migration-dirs";
|
|
39
|
+
import { findFlywayConfig, readFlywayConfig, substitute } from "@intentius/chant/workspace/flyway-layout";
|
|
40
|
+
import { workingTree, type WorkspaceTree } from "@intentius/chant/workspace/tree";
|
|
41
|
+
|
|
42
|
+
export const REPLAY_KINDS = ["goose", "golang-migrate", "clickhouse-migrations", "flyway"] as const;
|
|
43
|
+
export type ReplayKind = (typeof REPLAY_KINDS)[number];
|
|
44
|
+
|
|
45
|
+
const READER = { goose: "goose-dir", "golang-migrate": "golang-migrate-dir", "clickhouse-migrations": "clickhouse-migrations-dir", flyway: "flyway-dir" } as const;
|
|
46
|
+
|
|
47
|
+
/** One server the replay runs on. */
|
|
48
|
+
export interface ReplayDb {
|
|
49
|
+
dialect: "postgres" | "clickhouse";
|
|
50
|
+
run(sql: string): Promise<void>;
|
|
51
|
+
rows(sql: string): Promise<Record<string, unknown>[]>;
|
|
52
|
+
close(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Split SQL text into statements at `;`. Single-quoted strings, quoted
|
|
57
|
+
* identifiers, `--` and `/* *\/` comments and Postgres dollar-quoted bodies
|
|
58
|
+
* hold semicolons without ending the statement. A statement of only
|
|
59
|
+
* comments is dropped.
|
|
60
|
+
*/
|
|
61
|
+
export function splitStatements(text: string): string[] {
|
|
62
|
+
const out: string[] = [];
|
|
63
|
+
let start = 0;
|
|
64
|
+
let i = 0;
|
|
65
|
+
const flush = (end: number) => {
|
|
66
|
+
const s = text.slice(start, end).trim();
|
|
67
|
+
if (s !== "" && s.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim() !== "") out.push(s);
|
|
68
|
+
start = end + 1;
|
|
69
|
+
};
|
|
70
|
+
while (i < text.length) {
|
|
71
|
+
const c = text[i]!;
|
|
72
|
+
const two = text.slice(i, i + 2);
|
|
73
|
+
if (two === "--") {
|
|
74
|
+
const nl = text.indexOf("\n", i);
|
|
75
|
+
i = nl < 0 ? text.length : nl + 1;
|
|
76
|
+
} else if (two === "/*") {
|
|
77
|
+
const end = text.indexOf("*/", i + 2);
|
|
78
|
+
i = end < 0 ? text.length : end + 2;
|
|
79
|
+
} else if (c === "'" || c === '"' || c === "`") {
|
|
80
|
+
i++;
|
|
81
|
+
while (i < text.length) {
|
|
82
|
+
if (text[i] === "\\" && c !== '"') i += 2;
|
|
83
|
+
else if (text[i] === c) {
|
|
84
|
+
if (text[i + 1] === c) i += 2;
|
|
85
|
+
else break;
|
|
86
|
+
} else i++;
|
|
87
|
+
}
|
|
88
|
+
i++;
|
|
89
|
+
} else if (c === "$") {
|
|
90
|
+
const tag = /^\$[A-Za-z_]*\$/.exec(text.slice(i))?.[0];
|
|
91
|
+
if (tag) {
|
|
92
|
+
const end = text.indexOf(tag, i + tag.length);
|
|
93
|
+
i = end < 0 ? text.length : end + tag.length;
|
|
94
|
+
} else i++;
|
|
95
|
+
} else if (c === ";") {
|
|
96
|
+
flush(i);
|
|
97
|
+
i++;
|
|
98
|
+
} else i++;
|
|
99
|
+
}
|
|
100
|
+
flush(text.length);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** goose's Up statements: the lines between `-- +goose Up` and `-- +goose Down`; a StatementBegin to StatementEnd block is one statement. */
|
|
105
|
+
export function gooseUpStatements(text: string): string[] {
|
|
106
|
+
const out: string[] = [];
|
|
107
|
+
let section: "none" | "up" | "down" = "none";
|
|
108
|
+
let block: string[] | undefined;
|
|
109
|
+
let plain: string[] = [];
|
|
110
|
+
const annotation = (l: string) => {
|
|
111
|
+
const t = l.trim();
|
|
112
|
+
return t.startsWith("--") && t.includes("+goose") ? t.replace("+goose", "").replace(/^--\s*/, "").trim().toLowerCase() : undefined;
|
|
113
|
+
};
|
|
114
|
+
const flushPlain = () => {
|
|
115
|
+
out.push(...splitStatements(plain.join("\n")));
|
|
116
|
+
plain = [];
|
|
117
|
+
};
|
|
118
|
+
for (const line of text.split(/\r?\n/)) {
|
|
119
|
+
const a = annotation(line);
|
|
120
|
+
if (a === "up") section = "up";
|
|
121
|
+
else if (a === "down") {
|
|
122
|
+
flushPlain();
|
|
123
|
+
section = "down";
|
|
124
|
+
} else if (a === "statementbegin" && section === "up") {
|
|
125
|
+
flushPlain();
|
|
126
|
+
block = [];
|
|
127
|
+
} else if (a === "statementend" && section === "up" && block) {
|
|
128
|
+
const s = block.join("\n").trim();
|
|
129
|
+
if (s !== "") out.push(s);
|
|
130
|
+
block = undefined;
|
|
131
|
+
} else if (section === "up" && a === undefined) (block ?? plain).push(line);
|
|
132
|
+
}
|
|
133
|
+
flushPlain();
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ReplayStep {
|
|
138
|
+
migration: KindReadMigration;
|
|
139
|
+
statements: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The statements to run, per migration, in order. Throws when a Flyway placeholder has no value. */
|
|
143
|
+
export function replaySteps(kind: ReplayKind, tree: WorkspaceTree, dir: string, fields: KindReaderInput["fields"] = null): { steps: ReplayStep[]; files: string[]; dialect: string } {
|
|
144
|
+
const read = LINK_READERS[READER[kind]]({ member: { name: "replay" } as never, dir, fields, tree });
|
|
145
|
+
const migrations = (read.migrations ?? []).filter((m) => m.type === "versioned" || m.type === "repeatable");
|
|
146
|
+
const config = kind === "flyway" ? readFlywayConfig(tree, findFlywayConfig(tree, dir), dir) : undefined;
|
|
147
|
+
const steps = migrations.map((migration) => {
|
|
148
|
+
const text = tree.read(migration.file);
|
|
149
|
+
const statements = kind === "goose" ? gooseUpStatements(text) : splitStatements(config ? substitute(text, migration.file, config) : text);
|
|
150
|
+
return { migration, statements };
|
|
151
|
+
});
|
|
152
|
+
const extra = config?.file !== undefined ? [config.file] : [];
|
|
153
|
+
return { steps, files: replayFiles(tree, read.migrations ?? [], extra), dialect: read.migrations?.[0]?.dialect ?? "postgres" };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Apply each migration's statements; a failure names the migration file and the statement. */
|
|
157
|
+
export async function applySteps(db: ReplayDb, steps: readonly ReplayStep[]): Promise<void> {
|
|
158
|
+
for (const { migration, statements } of steps) {
|
|
159
|
+
for (const [i, sql] of statements.entries()) {
|
|
160
|
+
try {
|
|
161
|
+
await db.run(sql);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
throw new Error(`${migration.file}: statement ${String(i + 1)} failed: ${(err as Error).message}\n${sql.slice(0, 200)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const POSTGRES_CATALOG = `
|
|
170
|
+
select n.nspname as schema, c.relname as relation, a.attname as col
|
|
171
|
+
from pg_class c
|
|
172
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
173
|
+
left join pg_attribute a on a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped
|
|
174
|
+
where c.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
175
|
+
and n.nspname not in ('pg_catalog', 'information_schema')
|
|
176
|
+
and n.nspname not like 'pg\\_toast%' and n.nspname not like 'pg\\_temp%'
|
|
177
|
+
order by 1, 2, a.attnum`;
|
|
178
|
+
|
|
179
|
+
const CLICKHOUSE_CATALOG = `
|
|
180
|
+
select t.database as schema, t.name as relation, c.name as col
|
|
181
|
+
from system.tables t
|
|
182
|
+
left join system.columns c on c.database = t.database and c.table = t.name
|
|
183
|
+
where t.database not in ('system', 'INFORMATION_SCHEMA', 'information_schema')
|
|
184
|
+
and t.is_temporary = 0
|
|
185
|
+
order by t.database, t.name, c.position`;
|
|
186
|
+
|
|
187
|
+
/** The tables, views and columns the server has, as workspace output names (#3931). */
|
|
188
|
+
export async function readCatalogOutputs(db: ReplayDb): Promise<string[]> {
|
|
189
|
+
const rows = await db.rows(db.dialect === "postgres" ? POSTGRES_CATALOG : CLICKHOUSE_CATALOG);
|
|
190
|
+
const out: string[] = [];
|
|
191
|
+
const seen = new Set<string>();
|
|
192
|
+
for (const r of rows) {
|
|
193
|
+
const rel = `${String(r.schema)}.${String(r.relation)}`;
|
|
194
|
+
if (!seen.has(rel)) {
|
|
195
|
+
seen.add(rel);
|
|
196
|
+
out.push(rel);
|
|
197
|
+
}
|
|
198
|
+
if (r.col !== null && r.col !== undefined) out.push(`${rel}.${String(r.col)}`);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Replay a directory into `db`, which must hold nothing, and return the snapshot to record. */
|
|
204
|
+
export async function replayDirectory(db: ReplayDb, kind: ReplayKind, tree: WorkspaceTree, dir: string, fields: KindReaderInput["fields"] = null): Promise<ReplaySnapshot> {
|
|
205
|
+
const before = await readCatalogOutputs(db);
|
|
206
|
+
if (before.length > 0) throw new Error(`the server already has ${before[0]!.split(".").slice(0, 2).join(".")}${before.length > 1 ? ` and ${String(before.length - 1)} more names` : ""}; replay needs a server with no tables or views of its own, so the tables it finds are the migrations' (use a throwaway server or emulator)`);
|
|
207
|
+
const { steps, files, dialect } = replaySteps(kind, tree, dir, fields);
|
|
208
|
+
if (dialect !== db.dialect) throw new Error(`the migrations are ${dialect} and the server is ${db.dialect}; set the member's dialect field or bind the matching server`);
|
|
209
|
+
await applySteps(db, steps);
|
|
210
|
+
return { schema: 1, dialect, digest: migrationDigest((f) => tree.read(f), files), outputs: await readCatalogOutputs(db) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const renderSnapshot = (s: ReplaySnapshot): string => `${JSON.stringify(s, null, 2)}\n`;
|
|
214
|
+
|
|
215
|
+
const USAGE = "usage: chant sql replay <dir> --kind goose|golang-migrate|clickhouse-migrations|flyway --env <env> [--migrations-dir <sub>] [--dialect postgres|clickhouse] [--check]";
|
|
216
|
+
|
|
217
|
+
/** Open the server `--env` binds, of the migrations' dialect. */
|
|
218
|
+
async function openBound(dialect: "postgres" | "clickhouse", environment: string): Promise<ReplayDb> {
|
|
219
|
+
if (dialect === "postgres") {
|
|
220
|
+
const { bindPostgres } = await import("./postgres/live/bind");
|
|
221
|
+
const { client } = await bindPostgres({ environment });
|
|
222
|
+
return { dialect, run: async (sql) => void (await client.query(sql)), rows: (sql) => client.query(sql), close: () => client.end() };
|
|
223
|
+
}
|
|
224
|
+
const { bindClickHouse } = await import("./clickhouse/live/bind");
|
|
225
|
+
const { clickhouseQuery } = await import("./clickhouse/http");
|
|
226
|
+
const target = await bindClickHouse({ environment });
|
|
227
|
+
return { dialect, run: async (sql) => void (await clickhouseQuery(target.endpoint, sql)), rows: (sql) => clickhouseQuery(target.endpoint, sql), async close() {} };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function runReplay(ctx: CommandGroupContext): Promise<number> {
|
|
231
|
+
const args = ctx.rawArgs;
|
|
232
|
+
let kind: string | undefined;
|
|
233
|
+
let env: string | undefined;
|
|
234
|
+
let sub: string | undefined;
|
|
235
|
+
let dialect: string | undefined;
|
|
236
|
+
const check = args.includes("--check");
|
|
237
|
+
const positional: string[] = [];
|
|
238
|
+
for (let i = 0; i < args.length; i++) {
|
|
239
|
+
const a = args[i]!;
|
|
240
|
+
if (a === "--kind") kind = args[++i];
|
|
241
|
+
else if (a === "--env") env = args[++i];
|
|
242
|
+
else if (a === "--migrations-dir") sub = args[++i];
|
|
243
|
+
else if (a === "--dialect") dialect = args[++i];
|
|
244
|
+
else if (a === "--check") continue;
|
|
245
|
+
else if (a.startsWith("--")) {
|
|
246
|
+
console.error(`unknown argument ${a}\n${USAGE}`);
|
|
247
|
+
return 1;
|
|
248
|
+
} else positional.push(a);
|
|
249
|
+
}
|
|
250
|
+
if (positional.length !== 1 || !kind || !(REPLAY_KINDS as readonly string[]).includes(kind) || !env) {
|
|
251
|
+
console.error(USAGE);
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
const abs = resolve(process.cwd(), positional[0]!);
|
|
255
|
+
const tree = workingTree(abs);
|
|
256
|
+
const fields = { ...(sub ? { migrationsDir: sub } : {}), ...(dialect ? { dialect } : {}) } as KindReaderInput["fields"];
|
|
257
|
+
const { steps, dialect: found } = replaySteps(kind as ReplayKind, tree, "", fields);
|
|
258
|
+
if (steps.length === 0) {
|
|
259
|
+
console.error(`chant sql replay: ${relative(process.cwd(), abs) || "."} lists no ${kind} migrations`);
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
const db = await openBound(found as "postgres" | "clickhouse", env);
|
|
263
|
+
let snapshot: ReplaySnapshot;
|
|
264
|
+
try {
|
|
265
|
+
snapshot = await replayDirectory(db, kind as ReplayKind, tree, "", fields);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(`chant sql replay: ${(err as Error).message}`);
|
|
268
|
+
return 1;
|
|
269
|
+
} finally {
|
|
270
|
+
await db.close();
|
|
271
|
+
}
|
|
272
|
+
const file = join(abs, REPLAY_FILE);
|
|
273
|
+
const text = renderSnapshot(snapshot);
|
|
274
|
+
const current = (() => {
|
|
275
|
+
try {
|
|
276
|
+
return readFileSync(file, "utf8");
|
|
277
|
+
} catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
})();
|
|
281
|
+
if (check) {
|
|
282
|
+
if (current === text) return 0;
|
|
283
|
+
console.error(`chant sql replay --check: ${relative(process.cwd(), file)} ${current === undefined ? "is missing" : "differs from a replay of the migrations"}; run chant sql replay and commit it`);
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
writeFileSync(file, text);
|
|
287
|
+
console.log(`recorded ${String(snapshot.outputs.length)} names from ${String(steps.length)} migrations in ${relative(process.cwd(), file)}`);
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
CLICKHOUSE_READER_USER: chant_reader
|
|
21
21
|
CLICKHOUSE_READER_PASSWORD: '\${{ secrets.CLICKHOUSE_READER_PASSWORD }}'
|
|
22
22
|
steps:
|
|
23
|
-
- uses: https://code.forgejo.org/actions/checkout@v7
|
|
23
|
+
- uses: https://code.forgejo.org/actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
24
24
|
- run: npm ci
|
|
25
25
|
- run: echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH"
|
|
26
26
|
- run: chant run schema-watch
|
|
@@ -52,7 +52,7 @@ jobs:
|
|
|
52
52
|
CLICKHOUSE_READER_USER: chant_reader
|
|
53
53
|
CLICKHOUSE_READER_PASSWORD: '\${{ secrets.CLICKHOUSE_READER_PASSWORD }}'
|
|
54
54
|
steps:
|
|
55
|
-
- uses: actions/checkout@v7
|
|
55
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
56
56
|
- run: npm ci
|
|
57
57
|
- run: echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH"
|
|
58
58
|
- run: chant run schema-watch
|
|
@@ -109,6 +109,8 @@ async function bound<T>(args: PostgresMigrationArgs, signal: AbortSignal | undef
|
|
|
109
109
|
objects,
|
|
110
110
|
column: args.column,
|
|
111
111
|
...(args.using !== undefined ? { using: args.using } : {}),
|
|
112
|
+
...(args.reverse !== undefined ? { reverse: args.reverse } : {}),
|
|
113
|
+
...(args.defaultAtContract ? { defaultAtContract: true } : {}),
|
|
112
114
|
...(marker ? { marker } : {}),
|
|
113
115
|
...(major !== undefined ? { major } : {}),
|
|
114
116
|
batchSize: args.batchSize ?? 1000,
|
|
@@ -158,6 +160,8 @@ export async function postgresMigrationPlan(args: PostgresMigrationArgs, signal?
|
|
|
158
160
|
column: run.column,
|
|
159
161
|
...(run.marker ? { marker: run.marker } : {}),
|
|
160
162
|
...(run.using !== undefined ? { using: run.using } : {}),
|
|
163
|
+
...(run.reverse !== undefined ? { reverse: run.reverse } : {}),
|
|
164
|
+
...(run.defaultAtContract ? { defaultAtContract: true } : {}),
|
|
161
165
|
...(run.major !== undefined ? { major: run.major } : {}),
|
|
162
166
|
});
|
|
163
167
|
const n = o.names;
|
|
@@ -124,6 +124,8 @@ const migrationArgs = z.strictObject({
|
|
|
124
124
|
buildPath: z.string(),
|
|
125
125
|
environment: z.string().optional(),
|
|
126
126
|
using: z.string().optional(),
|
|
127
|
+
reverse: z.string().optional(),
|
|
128
|
+
defaultAtContract: z.boolean().optional(),
|
|
127
129
|
retain: z.string().optional(),
|
|
128
130
|
batchSize: z.number().int().positive().optional(),
|
|
129
131
|
replicationLag: z.union([z.literal(false), z.strictObject({ max: z.string().optional(), wait: z.string().optional() })]).optional(),
|