@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,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream engines (#4396): a change to a Kafka or S3Queue table is SQLCH225
|
|
3
|
+
* and not SQLCH223, the plan holds nothing back for it, the statements go
|
|
4
|
+
* detach, drop, create, attach, and a replacement that loses position says so.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { canonicalObject } from "./normalize";
|
|
9
|
+
import { diffSchemas, type SchemaObject } from "./diff";
|
|
10
|
+
import { renderDiff } from "./report";
|
|
11
|
+
import { classifyDisruption } from "./disruption";
|
|
12
|
+
import { CLASSIFIER_RULES } from "./rules";
|
|
13
|
+
import { rebuildOpSuggestions } from "./rebuild-handoff";
|
|
14
|
+
import { isStreamEngine, readsTable, STREAM_ENGINES, streamEngineArguments } from "./stream";
|
|
15
|
+
import { planStatements, type DeclaredObject } from "../apply/statements";
|
|
16
|
+
import { CLICKHOUSE_ENTITY_TYPES } from "../entities";
|
|
17
|
+
import { observeRebuild, RebuildRefusal } from "../rebuild/observe";
|
|
18
|
+
import type { ClickHouseTarget } from "../live/bind";
|
|
19
|
+
|
|
20
|
+
const KAFKA = (topics: string, group = "ingest", extra = "") =>
|
|
21
|
+
`CREATE TABLE shop.events_stream (user_id UInt64, kind String) ENGINE = Kafka(kafka_creds) SETTINGS kafka_topic_list = '${topics}', kafka_group_name = '${group}', kafka_format = 'JSONEachRow'${extra}`;
|
|
22
|
+
const S3QUEUE = (path: string, mode = "unordered") =>
|
|
23
|
+
`CREATE TABLE shop.files_stream (user_id UInt64) ENGINE = S3Queue(lake_creds) SETTINGS mode = '${mode}', keeper_path = '${path}'`;
|
|
24
|
+
const MV = "CREATE MATERIALIZED VIEW shop.events_mv TO shop.events AS SELECT user_id, kind FROM shop.events_stream";
|
|
25
|
+
const MV_OTHER = "CREATE MATERIALIZED VIEW shop.audit_mv TO shop.audit AS SELECT user_id FROM events_stream";
|
|
26
|
+
const MV_UNRELATED = "CREATE MATERIALIZED VIEW shop.other_mv TO shop.other AS SELECT user_id FROM shop.users";
|
|
27
|
+
|
|
28
|
+
const obj = (key: string, ddl: string): SchemaObject => ({ key, canonical: canonicalObject(ddl) });
|
|
29
|
+
const one = (key: string, before: string, after: string) => diffSchemas([obj(key, before)], [obj(key, after)]);
|
|
30
|
+
|
|
31
|
+
describe("stream engines", () => {
|
|
32
|
+
test("the six engines are the ones the pinned catalog has", () => {
|
|
33
|
+
expect([...STREAM_ENGINES].sort()).toEqual(["AzureQueue", "FileLog", "Kafka", "NATS", "RabbitMQ", "S3Queue"]);
|
|
34
|
+
expect(isStreamEngine("MergeTree")).toBe(false);
|
|
35
|
+
expect(isStreamEngine(undefined)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the rule is its own class, with a rule id in the SQLCH2xx range", () => {
|
|
39
|
+
expect(CLASSIFIER_RULES.SQLCH225.class).toBe("replace");
|
|
40
|
+
expect(CLASSIFIER_RULES.SQLCH223.class).toBe("rebuild");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("classification", () => {
|
|
45
|
+
test("a different topic list is SQLCH225, not SQLCH223, and the plan refuses nothing", () => {
|
|
46
|
+
const diff = one("events_stream", KAFKA("events"), KAFKA("events,clicks"));
|
|
47
|
+
expect(diff.changes.map((c) => [c.rule, c.class, c.field])).toEqual([["SQLCH225", "replace", "settings.kafka_topic_list"]]);
|
|
48
|
+
expect(diff.rebuilds).toEqual([]);
|
|
49
|
+
expect(rebuildOpSuggestions(diff, new Map([["events_stream", canonicalObject(KAFKA("events,clicks"))]]), "prod")).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a topic list given as an engine argument is the engine changing", () => {
|
|
53
|
+
const positional = (topic: string) => `CREATE TABLE shop.q (a String) ENGINE = Kafka('broker:9092', '${topic}', 'g', 'JSONEachRow')`;
|
|
54
|
+
const diff = one("q", positional("a"), positional("b"));
|
|
55
|
+
expect(diff.changes.map((c) => [c.rule, c.field])).toEqual([["SQLCH225", "engine"]]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a format, a column and a setting added are all the stream rule", () => {
|
|
59
|
+
const diff = one(
|
|
60
|
+
"events_stream",
|
|
61
|
+
KAFKA("events"),
|
|
62
|
+
KAFKA("events", "ingest", ", kafka_num_consumers = 2").replace("kafka_format = 'JSONEachRow'", "kafka_format = 'CSV'").replace("kind String", "kind String, ts DateTime"),
|
|
63
|
+
);
|
|
64
|
+
expect(new Set(diff.changes.map((c) => c.rule))).toEqual(new Set(["SQLCH225"]));
|
|
65
|
+
expect(diff.changes.map((c) => c.field).sort()).toEqual(["columns.ts", "settings.kafka_format", "settings.kafka_num_consumers"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("S3Queue, RabbitMQ and NATS tables are stream tables too", () => {
|
|
69
|
+
expect(one("f", S3QUEUE("/q/a"), S3QUEUE("/q/a", "ordered")).changes.map((c) => c.rule)).toEqual(["SQLCH225"]);
|
|
70
|
+
const nats = (subject: string) => `CREATE TABLE shop.n (a String) ENGINE = NATS(nats_creds) SETTINGS nats_subjects = '${subject}', nats_format = 'JSONEachRow'`;
|
|
71
|
+
expect(one("n", nats("a"), nats("b")).changes.map((c) => c.rule)).toEqual(["SQLCH225"]);
|
|
72
|
+
const rabbit = (queue: string) => `CREATE TABLE shop.r (a String) ENGINE = RabbitMQ(rabbit_creds) SETTINGS rabbitmq_queue_base = '${queue}', rabbitmq_format = 'JSONEachRow'`;
|
|
73
|
+
expect(one("r", rabbit("a"), rabbit("b")).changes.map((c) => c.rule)).toEqual(["SQLCH225"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a comment alone stays metadata", () => {
|
|
77
|
+
const diff = one("events_stream", KAFKA("events"), `${KAFKA("events")} COMMENT 'ingest'`);
|
|
78
|
+
expect(diff.changes.map((c) => c.rule)).toEqual(["SQLCH203"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a MergeTree engine change is still SQLCH223", () => {
|
|
82
|
+
const mt = (v: string) => `CREATE TABLE shop.t (a UInt64, v UInt64) ENGINE = ReplacingMergeTree(${v}) ORDER BY a`;
|
|
83
|
+
expect(one("t", mt("v"), mt("a")).changes.map((c) => c.rule)).toEqual(["SQLCH223"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a table that becomes a stream table is a rebuild with a note, and gets no Op suggestion", () => {
|
|
87
|
+
const mt = "CREATE TABLE shop.events_stream (user_id UInt64, kind String) ENGINE = MergeTree ORDER BY user_id";
|
|
88
|
+
const diff = one("events_stream", mt, KAFKA("events"));
|
|
89
|
+
expect(diff.changes[0]).toMatchObject({ rule: "SQLCH223", class: "rebuild" });
|
|
90
|
+
expect(diff.changes[0]!.note).toMatch(/stream engine/);
|
|
91
|
+
expect(rebuildOpSuggestions(diff, new Map([["events_stream", canonicalObject(KAFKA("events"))]]), "prod")).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("the report counts the class and names the views that are detached around the replacement", () => {
|
|
95
|
+
const diff = diffSchemas(
|
|
96
|
+
[obj("events_stream", KAFKA("events")), obj("events_mv", MV), obj("audit_mv", MV_OTHER), obj("other_mv", MV_UNRELATED)],
|
|
97
|
+
[obj("events_stream", KAFKA("clicks")), obj("events_mv", MV), obj("audit_mv", MV_OTHER), obj("other_mv", MV_UNRELATED)],
|
|
98
|
+
);
|
|
99
|
+
const text = renderDiff(diff);
|
|
100
|
+
expect(text).toMatch(/\[replace table\] settings\.kafka_topic_list/);
|
|
101
|
+
expect(text).toMatch(/SQLCH225 Change a stream table/);
|
|
102
|
+
expect(text).toMatch(/1 replace table/);
|
|
103
|
+
expect(text).toMatch(/hint: shop\.events_stream is replaced: shop\.events_mv, shop\.audit_mv read it and are detached first and attached after/);
|
|
104
|
+
expect(text).not.toMatch(/Refused/);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("position", () => {
|
|
109
|
+
test("a different kafka_group_name warns, naming both groups", () => {
|
|
110
|
+
const [c] = one("events_stream", KAFKA("events", "ingest"), KAFKA("events", "ingest-v2")).changes;
|
|
111
|
+
expect(c!.rule).toBe("SQLCH225");
|
|
112
|
+
expect(c!.note).toMatch(/loses its position: kafka_group_name changes from ingest to ingest-v2/);
|
|
113
|
+
expect(c!.note).toMatch(/committed offsets, or from the start of the topic/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a group given as an engine argument is read too", () => {
|
|
117
|
+
const positional = (g: string) => `CREATE TABLE shop.q (a String) ENGINE = Kafka('broker:9092', 't', '${g}', 'JSONEachRow')`;
|
|
118
|
+
expect(one("q", positional("g1"), positional("g2")).changes[0]!.note).toMatch(/from g1 to g2/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a topic list change keeps the group, so there is no warning", () => {
|
|
122
|
+
expect(one("events_stream", KAFKA("events"), KAFKA("clicks")).changes[0]!.note).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a different S3Queue keeper_path warns that every file is processed again", () => {
|
|
126
|
+
const [c] = one("files_stream", S3QUEUE("/q/a"), S3QUEUE("/q/b")).changes;
|
|
127
|
+
expect(c!.rule).toBe("SQLCH225");
|
|
128
|
+
expect(c!.note).toMatch(/keeper_path changes from \/q\/a to \/q\/b: .*every file already seen is processed again/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("the same keeper_path with another mode does not warn", () => {
|
|
132
|
+
expect(one("files_stream", S3QUEUE("/q/a"), S3QUEUE("/q/a", "ordered")).changes[0]!.note).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("reading a table", () => {
|
|
137
|
+
const table = canonicalObject(KAFKA("events"));
|
|
138
|
+
test("a view reads it by a bare name in its own database, or by a qualified one", () => {
|
|
139
|
+
expect(readsTable(canonicalObject(MV), table)).toBe(true);
|
|
140
|
+
expect(readsTable(canonicalObject(MV_OTHER), table)).toBe(true);
|
|
141
|
+
expect(readsTable(canonicalObject("CREATE MATERIALIZED VIEW audit.mv TO audit.t AS SELECT a FROM shop.events_stream"), table)).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
test("a view of another table, another database's table of the same name, or a plain view is not a reader", () => {
|
|
144
|
+
expect(readsTable(canonicalObject(MV_UNRELATED), table)).toBe(false);
|
|
145
|
+
expect(readsTable(canonicalObject("CREATE MATERIALIZED VIEW audit.mv TO audit.t AS SELECT a FROM events_stream"), table)).toBe(false);
|
|
146
|
+
expect(readsTable(canonicalObject("CREATE VIEW shop.v AS SELECT a FROM shop.events_stream"), table)).toBe(false);
|
|
147
|
+
expect(readsTable(canonicalObject("CREATE MATERIALIZED VIEW shop.mv TO shop.t AS SELECT a FROM shop.events_stream_old"), table)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
test("engine arguments split outside quotes and parentheses", () => {
|
|
150
|
+
expect(streamEngineArguments("Kafka('a, b:9092', 't', 'g', 'JSONEachRow')")).toEqual(["a, b:9092", "t", "g", "JSONEachRow"]);
|
|
151
|
+
expect(streamEngineArguments("Kafka")).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const declared = (key: string, type: DeclaredObject["type"], ddl: string): DeclaredObject => ({ exportName: key, type, key, ddl, canonical: canonicalObject(ddl), dependsOn: [] });
|
|
156
|
+
|
|
157
|
+
describe("the statements", () => {
|
|
158
|
+
const before = [KAFKA("events"), MV, MV_OTHER, MV_UNRELATED].map((d) => canonicalObject(d));
|
|
159
|
+
const names = ["shop.events_stream", "shop.events_mv", "shop.audit_mv", "shop.other_mv"];
|
|
160
|
+
const current = new Map(names.map((n, i) => [n, before[i]!]));
|
|
161
|
+
const after = [
|
|
162
|
+
declared("shop.events_stream", CLICKHOUSE_ENTITY_TYPES.table, KAFKA("clicks")),
|
|
163
|
+
declared("shop.events_mv", CLICKHOUSE_ENTITY_TYPES.materializedView, MV),
|
|
164
|
+
declared("shop.audit_mv", CLICKHOUSE_ENTITY_TYPES.materializedView, MV_OTHER),
|
|
165
|
+
declared("shop.other_mv", CLICKHOUSE_ENTITY_TYPES.materializedView, MV_UNRELATED),
|
|
166
|
+
];
|
|
167
|
+
const changes = diffSchemas(
|
|
168
|
+
names.map((n, i) => ({ key: n, canonical: before[i]! })),
|
|
169
|
+
after.map((o) => ({ key: o.key, canonical: o.canonical })),
|
|
170
|
+
).changes;
|
|
171
|
+
const stepsOf = (plan: ReturnType<typeof planStatements>, i = 0) => (plan.objects[i] as { steps: { sql: string; rule: string; class: string }[] }).steps;
|
|
172
|
+
|
|
173
|
+
test("detach the views that read the table, replace it, attach them again", () => {
|
|
174
|
+
const plan = planStatements({ declared: after, changes, current });
|
|
175
|
+
expect(plan.objects[0]!.verdict).toBe("alter");
|
|
176
|
+
expect(stepsOf(plan).map((s) => s.sql.replace(/\s+/g, " "))).toEqual([
|
|
177
|
+
"DETACH TABLE `shop`.`events_mv`",
|
|
178
|
+
"DETACH TABLE `shop`.`audit_mv`",
|
|
179
|
+
"DROP TABLE `shop`.`events_stream` SYNC",
|
|
180
|
+
expect.stringMatching(/^CREATE TABLE shop\.events_stream \(user_id UInt64, kind String\) ENGINE = Kafka\(kafka_creds\) SETTINGS kafka_topic_list = 'clicks', .* COMMENT '.*managed-by=chant/),
|
|
181
|
+
"ATTACH TABLE `shop`.`events_mv`",
|
|
182
|
+
"ATTACH TABLE `shop`.`audit_mv`",
|
|
183
|
+
]);
|
|
184
|
+
expect(stepsOf(plan).every((s) => s.rule === "SQLCH225" && s.class === "replace")).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("the views themselves have nothing to do, and the unrelated one is not touched", () => {
|
|
188
|
+
const plan = planStatements({ declared: after, changes, current });
|
|
189
|
+
expect(plan.objects.slice(1).map((o) => [o.verdict, (o as { steps: unknown[] }).steps.length])).toEqual([
|
|
190
|
+
["alter", 0],
|
|
191
|
+
["alter", 0],
|
|
192
|
+
["alter", 0],
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a view the build drops in the same plan is not detached", () => {
|
|
197
|
+
const gone = [...changes, { object: "shop.audit_mv", field: "object", rule: "SQLCH250" as const, class: "drop" as const }];
|
|
198
|
+
const plan = planStatements({ declared: after.slice(0, 2), changes: gone, current });
|
|
199
|
+
expect(stepsOf(plan).filter((s) => s.sql.startsWith("DETACH")).map((s) => s.sql)).toEqual(["DETACH TABLE `shop`.`events_mv`"]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("on a cluster each statement runs ON CLUSTER", () => {
|
|
203
|
+
const plan = planStatements({ declared: after, changes, current, topology: { kind: "cluster", cluster: "main" } as never });
|
|
204
|
+
expect(stepsOf(plan).every((s) => /ON CLUSTER/.test(s.sql))).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("classifyDisruption", () => {
|
|
209
|
+
test("a stream engine's setting replaces; a MergeTree setting stays in place", () => {
|
|
210
|
+
const out = classifyDisruption({
|
|
211
|
+
environment: "prod",
|
|
212
|
+
changes: [
|
|
213
|
+
{ name: "events_stream", type: "ClickHouse::Table", deltas: [{ path: "settings.kafka_topic_list" } as never] },
|
|
214
|
+
{ name: "events", type: "ClickHouse::Table", deltas: [{ path: "settings.merge_with_ttl_timeout" } as never] },
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
expect(out.events_stream).toMatchObject({ disruption: "replace" });
|
|
218
|
+
expect(out.events_stream!.detail).toMatch(/SQLCH225/);
|
|
219
|
+
expect(out.events).toMatchObject({ disruption: "in-place" });
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("ClickHouseRebuildOp's observation", () => {
|
|
224
|
+
const target = { endpoint: { url: "http://unreachable.invalid" }, defaultDatabase: "shop" } as unknown as ClickHouseTarget;
|
|
225
|
+
|
|
226
|
+
test.each([
|
|
227
|
+
["Kafka", KAFKA("events")],
|
|
228
|
+
["S3Queue", S3QUEUE("/q/a")],
|
|
229
|
+
])("refuses a %s table by name, before it asks the server anything", async (engine, ddl) => {
|
|
230
|
+
const o = declared("shop.t", CLICKHOUSE_ENTITY_TYPES.table, ddl);
|
|
231
|
+
const err = await observeRebuild(target, o, undefined).catch((e: unknown) => e);
|
|
232
|
+
expect(err).toBeInstanceOf(RebuildRefusal);
|
|
233
|
+
expect((err as Error).message).toContain(`the ${engine} engine`);
|
|
234
|
+
expect((err as Error).message).toMatch(/SQLCH225/);
|
|
235
|
+
expect((err as Error).message).toMatch(/stores no rows/);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream engines (#4396): Kafka, S3Queue, AzureQueue, RabbitMQ, NATS and
|
|
3
|
+
* FileLog. A table on one of them stores no rows. It is a consumer: the
|
|
4
|
+
* materialized views that read it pull messages (or files) through it into a
|
|
5
|
+
* target, and the position it has reached lives outside the table, in the
|
|
6
|
+
* broker's committed offsets or in Keeper.
|
|
7
|
+
*
|
|
8
|
+
* What follows from that, and what this file holds:
|
|
9
|
+
*
|
|
10
|
+
* - There is no ALTER for the engine's arguments, and nothing to copy to a
|
|
11
|
+
* new table, so a change is neither `metadata` nor a `rebuild`: the table is
|
|
12
|
+
* replaced, with the views that read it detached around the replacement
|
|
13
|
+
* (SQLCH225, `./rules.ts`).
|
|
14
|
+
* - A replacement can lose the table's position. The two changes that do
|
|
15
|
+
* are found here and reported on the change as a note.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { CanonicalObject } from "./normalize";
|
|
19
|
+
|
|
20
|
+
/** The stream engines, as `system.tables.engine` names them. */
|
|
21
|
+
export const STREAM_ENGINES: ReadonlySet<string> = new Set(["Kafka", "S3Queue", "AzureQueue", "RabbitMQ", "NATS", "FileLog"]);
|
|
22
|
+
|
|
23
|
+
/** Whether an engine name is a stream engine. */
|
|
24
|
+
export const isStreamEngine = (engine: string | undefined): boolean => engine !== undefined && STREAM_ENGINES.has(engine);
|
|
25
|
+
|
|
26
|
+
/** Whether a canonical table is on a stream engine. */
|
|
27
|
+
export const isStreamTable = (o: CanonicalObject): boolean => o.kind === "table" && isStreamEngine(o.engineName);
|
|
28
|
+
|
|
29
|
+
/** The arguments inside an engine's parentheses, split at top-level commas, string quotes removed. Empty when it has none. */
|
|
30
|
+
export function streamEngineArguments(engine: string | undefined): string[] {
|
|
31
|
+
const open = engine?.indexOf("(") ?? -1;
|
|
32
|
+
if (engine === undefined || open < 0 || !engine.endsWith(")")) return [];
|
|
33
|
+
const inner = engine.slice(open + 1, -1);
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
let depth = 0;
|
|
36
|
+
let quote: string | undefined;
|
|
37
|
+
let cur = "";
|
|
38
|
+
for (let i = 0; i < inner.length; i++) {
|
|
39
|
+
const ch = inner[i]!;
|
|
40
|
+
if (quote) {
|
|
41
|
+
cur += ch;
|
|
42
|
+
if (ch === "\\") cur += inner[++i] ?? "";
|
|
43
|
+
else if (ch === quote) quote = undefined;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === "'" || ch === '"' || ch === "`") quote = ch;
|
|
47
|
+
if (ch === "(" || ch === "[") depth++;
|
|
48
|
+
if (ch === ")" || ch === "]") depth--;
|
|
49
|
+
if (ch === "," && depth === 0) {
|
|
50
|
+
out.push(cur.trim());
|
|
51
|
+
cur = "";
|
|
52
|
+
} else cur += ch;
|
|
53
|
+
}
|
|
54
|
+
if (cur.trim()) out.push(cur.trim());
|
|
55
|
+
return out.map((a) => (/^'.*'$/.test(a) ? a.slice(1, -1) : a));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Kafka's positional arguments, in the order the engine takes them. */
|
|
59
|
+
const KAFKA_POSITIONAL = ["kafka_broker_list", "kafka_topic_list", "kafka_group_name", "kafka_format"];
|
|
60
|
+
|
|
61
|
+
/** A Kafka setting, from SETTINGS or from the engine's positional arguments. */
|
|
62
|
+
function kafkaSetting(o: CanonicalObject, name: string): string | undefined {
|
|
63
|
+
const at = KAFKA_POSITIONAL.indexOf(name);
|
|
64
|
+
return o.settings[name] ?? (at >= 0 ? streamEngineArguments(o.engine)[at] : undefined);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a replacement would lose, in a sentence each (#4396):
|
|
69
|
+
*
|
|
70
|
+
* - a different `kafka_group_name` is a different consumer group: it starts
|
|
71
|
+
* from the group's committed offsets, or from the topic's start when the
|
|
72
|
+
* group has none (`auto.offset.reset`);
|
|
73
|
+
* - a different S3Queue `keeper_path` is a different place for the table's
|
|
74
|
+
* record of processed files, so every file already seen is processed again.
|
|
75
|
+
*
|
|
76
|
+
* Neither is a refusal: a new group or path is sometimes the point.
|
|
77
|
+
*/
|
|
78
|
+
export function positionWarnings(before: CanonicalObject, after: CanonicalObject): string[] {
|
|
79
|
+
const out: string[] = [];
|
|
80
|
+
if (before.engineName === "Kafka" && after.engineName === "Kafka") {
|
|
81
|
+
const b = kafkaSetting(before, "kafka_group_name");
|
|
82
|
+
const a = kafkaSetting(after, "kafka_group_name");
|
|
83
|
+
if (b !== a) {
|
|
84
|
+
out.push(
|
|
85
|
+
`kafka_group_name changes from ${b ?? "none"} to ${a ?? "none"}: the new group starts from its own committed offsets, or from the start of the topic when it has none, ` +
|
|
86
|
+
"so messages already consumed may be read again or skipped",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (before.engineName === "S3Queue" && after.engineName === "S3Queue") {
|
|
91
|
+
const b = before.settings.keeper_path;
|
|
92
|
+
const a = after.settings.keeper_path;
|
|
93
|
+
if (b !== a) {
|
|
94
|
+
out.push(
|
|
95
|
+
`keeper_path changes from ${b ?? "none"} to ${a ?? "none"}: the new path holds no record of processed files, so every file already seen is processed again`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether a materialized view reads from the table: its SELECT names the table, qualified or, in the table's own database, bare. */
|
|
103
|
+
export function readsTable(view: CanonicalObject, table: CanonicalObject): boolean {
|
|
104
|
+
if (view.kind !== "materializedView" || view.select === undefined) return false;
|
|
105
|
+
const escaped = table.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106
|
+
const sameDatabase = view.database === table.database;
|
|
107
|
+
const database = table.database?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
108
|
+
// The canonical SELECT drops the view's own database from a name, so a bare name is a table in the view's database.
|
|
109
|
+
const names = [...(sameDatabase ? [escaped] : []), ...(database ? [`${database}\\s*\\.\\s*${escaped}`] : [])];
|
|
110
|
+
return names.some((n) => new RegExp(`(?:^|[^\\w.])${n}(?![\\w.])`).test(view.select!));
|
|
111
|
+
}
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Anything else is refused with a {@link RebuildRefusal}: a database that is
|
|
29
29
|
* neither Atomic nor Replicated, a table that is not there, a change that is
|
|
30
|
-
* not a rebuild, a working object chant did not make
|
|
30
|
+
* not a rebuild, a working object chant did not make, a table on a stream
|
|
31
|
+
* engine (Kafka, S3Queue, AzureQueue, RabbitMQ, NATS, FileLog), which holds no
|
|
32
|
+
* rows to copy and is replaced by the applier instead (#4396).
|
|
31
33
|
*
|
|
32
34
|
* A Replicated database (#3249) is Atomic on every replica, with its DDL run
|
|
33
35
|
* on each through Keeper, so the same EXCHANGE swaps the table everywhere.
|
|
@@ -47,6 +49,7 @@ import { dropFormattingOnly } from "../plan/server-format";
|
|
|
47
49
|
import { CLASSIFIER_RULES } from "../plan/rules";
|
|
48
50
|
import { CLICKHOUSE_ENTITY_TYPES } from "../entities";
|
|
49
51
|
import { carriesMarker, readTrailerPairs, REBUILD_TRAILER_KEY, stripMarkerFromStatement } from "../ownership";
|
|
52
|
+
import { isStreamEngine } from "../plan/stream";
|
|
50
53
|
import { ident, isRebuild, qualifiedIdent, sqlString, type DeclaredObject } from "../apply/statements";
|
|
51
54
|
|
|
52
55
|
/** How the new table keeps up with writes to the old one while it is filled. */
|
|
@@ -250,10 +253,20 @@ function ownWorkingObject(o: ServerObject | undefined, names: RebuildNames, role
|
|
|
250
253
|
return o;
|
|
251
254
|
}
|
|
252
255
|
|
|
256
|
+
/** The refusal for a stream-engine table, naming the engine and where its change goes instead (#4396). */
|
|
257
|
+
function streamRefusal(key: string, engine: string, side: "declared" | "live"): RebuildRefusal {
|
|
258
|
+
return new RebuildRefusal(
|
|
259
|
+
`${key} is ${side === "declared" ? "declared on" : "on the server as"} the ${engine} engine, a stream engine: it stores no rows, so there is nothing to backfill, and a direct SELECT from it either fails ` +
|
|
260
|
+
`or consumes messages. A change to it is SQLCH225 (replace a stream table), which the applier makes in place: it detaches the materialized views that read the table, drops and creates it, and attaches the views again ` +
|
|
261
|
+
`(ApplyOp with target "clickhouse").`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
253
265
|
/** Observe the rebuild of the declared table on the server. */
|
|
254
266
|
export async function observeRebuild(target: ClickHouseTarget, declared: DeclaredObject, marker: OwnershipMarker | undefined): Promise<RebuildObservation> {
|
|
255
267
|
const database = declared.canonical.database ?? target.defaultDatabase;
|
|
256
268
|
const names = rebuildNames(database, declared.canonical.name);
|
|
269
|
+
if (isStreamEngine(declared.canonical.engineName)) throw streamRefusal(names.key, declared.canonical.engineName!, "declared");
|
|
257
270
|
|
|
258
271
|
const [db] = await clickhouseQuery<{ engine: string }>(target.endpoint, `SELECT engine FROM system.databases WHERE name = ${sqlString(database)}`);
|
|
259
272
|
if (!db) throw new RebuildRefusal(`database ${database} is not on the server, so there is no ${names.key} to rebuild; the applier creates it`);
|
|
@@ -272,6 +285,7 @@ export async function observeRebuild(target: ClickHouseTarget, declared: Declare
|
|
|
272
285
|
// Between EXCHANGE and the end of the swap, the table's own name holds the
|
|
273
286
|
// new table, still carrying role=new, and the new table's name holds the
|
|
274
287
|
// old table until it is renamed; the swap step finishes that.
|
|
288
|
+
if (isStreamEngine(liveRow.engine)) throw streamRefusal(names.key, liveRow.engine, "live");
|
|
275
289
|
const exchanged = liveRow.pairs?.get(REBUILD_TRAILER_KEY) === names.key && liveRow.pairs.get("role") === "new";
|
|
276
290
|
const newTable = exchanged ? undefined : ownWorkingObject(objects.get(names.newName), names, "new", marker);
|
|
277
291
|
const exchangedOld = exchanged ? objects.get(names.newName) : undefined;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { MERGE_TREE_SETTINGS, TABLE_ENGINES, TYPE_FAMILIES } from "../generated/clickhouse";
|
|
3
|
+
import {
|
|
4
|
+
CDC_DELETED_COLUMN,
|
|
5
|
+
CDC_VERSION_COLUMN,
|
|
6
|
+
REVIEW_RULES,
|
|
7
|
+
constraintReview,
|
|
8
|
+
engineReview,
|
|
9
|
+
findingText,
|
|
10
|
+
lowCardinalityReview,
|
|
11
|
+
nullableReview,
|
|
12
|
+
sortKeyReview,
|
|
13
|
+
} from "./review-catalog";
|
|
14
|
+
|
|
15
|
+
const types = { id: "Int64", ts: "DateTime64(3)", tenant: "String", maybe: "Nullable(Int32)" };
|
|
16
|
+
|
|
17
|
+
describe("sortKeyReview", () => {
|
|
18
|
+
test("a columnstore ORDER outranks a clustering key, a primary key and a unique key", () => {
|
|
19
|
+
const r = sortKeyReview({
|
|
20
|
+
object: "t",
|
|
21
|
+
columnTypes: types,
|
|
22
|
+
candidates: [
|
|
23
|
+
{ kind: "unique-key", columns: ["tenant"], source: "UNIQUE u" },
|
|
24
|
+
{ kind: "primary-key", columns: ["id"], source: "PRIMARY KEY pk" },
|
|
25
|
+
{ kind: "clustering-key", columns: ["ts"], source: "CLUSTERED INDEX cx" },
|
|
26
|
+
{ kind: "explicit-order", columns: ["tenant", "ts"], source: "COLUMNSTORE ORDER" },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
expect(r.orderBy).toEqual(["tenant", "ts"]);
|
|
30
|
+
expect(r.finding.ruleId).toBe("SQLRV001");
|
|
31
|
+
expect(r.finding.evidence[0]).toBe("COLUMNSTORE ORDER (tenant, ts)");
|
|
32
|
+
// The three it passed over are named in the order of their rank.
|
|
33
|
+
expect(r.finding.evidence.slice(1)).toEqual(["passed over: CLUSTERED INDEX cx (ts)", "passed over: PRIMARY KEY pk (id)", "passed over: UNIQUE u (tenant)"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("with no candidate the key is tuple() and the finding says why", () => {
|
|
37
|
+
const r = sortKeyReview({ object: "t", columnTypes: types, candidates: [] });
|
|
38
|
+
expect(r.orderBy).toEqual([]);
|
|
39
|
+
expect(r.finding.message).toContain("tuple()");
|
|
40
|
+
expect(r.finding.evidence).toEqual(["no clustering key, primary key or unique key in the source"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a Nullable key column sets allow_nullable_key, a setting the pinned server has", () => {
|
|
44
|
+
const r = sortKeyReview({ object: "t", columnTypes: types, candidates: [{ kind: "primary-key", columns: ["maybe"], source: "PRIMARY KEY pk" }] });
|
|
45
|
+
expect(r.settings).toEqual({ allow_nullable_key: "1" });
|
|
46
|
+
expect(Object.keys(MERGE_TREE_SETTINGS)).toContain("allow_nullable_key");
|
|
47
|
+
expect(r.finding.evidence.join(" ")).toContain("allow_nullable_key = 1");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a descending column is said to be ascending here", () => {
|
|
51
|
+
const r = sortKeyReview({ object: "t", columnTypes: types, candidates: [{ kind: "clustering-key", columns: ["ts"], descending: ["ts"], source: "CLUSTERED INDEX cx" }] });
|
|
52
|
+
expect(r.finding.evidence).toContain("descending in the source, ascending here: ts");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("engineReview", () => {
|
|
57
|
+
test("an append table is MergeTree and adds no column", () => {
|
|
58
|
+
const r = engineReview({ object: "t", mode: "append", uniqueKey: ["id"] });
|
|
59
|
+
expect(r.engine).toBe("MergeTree");
|
|
60
|
+
expect(r.extraColumns).toEqual([]);
|
|
61
|
+
expect(r.finding.evidence).toContain("the source enforces uniqueness on (id); MergeTree does not");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a CDC mirror is ReplacingMergeTree(_version, _deleted) with the two columns", () => {
|
|
65
|
+
const r = engineReview({ object: "t", mode: "cdc", uniqueKey: ["id"] });
|
|
66
|
+
expect(r.engine).toBe("ReplacingMergeTree(_version, _deleted)");
|
|
67
|
+
expect(r.extraColumns).toEqual([CDC_VERSION_COLUMN, CDC_DELETED_COLUMN]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a CDC mirror takes the caller's names for the two columns, in the engine, the columns and the advice (#4399)", () => {
|
|
71
|
+
const r = engineReview({ object: "t", mode: "cdc", cdcColumns: { version: "_peerdb_version", deleted: "_peerdb_is_deleted" } });
|
|
72
|
+
expect(r.engine).toBe("ReplacingMergeTree(_peerdb_version, _peerdb_is_deleted)");
|
|
73
|
+
expect(r.extraColumns).toEqual([
|
|
74
|
+
{ name: "_peerdb_version", type: CDC_VERSION_COLUMN.type },
|
|
75
|
+
{ name: "_peerdb_is_deleted", type: CDC_DELETED_COLUMN.type },
|
|
76
|
+
]);
|
|
77
|
+
expect(r.finding.message).toContain("_peerdb_version and _peerdb_is_deleted");
|
|
78
|
+
expect(r.finding.suggestion).toContain("highest _peerdb_version");
|
|
79
|
+
expect(r.finding.suggestion).toContain("filter on _peerdb_is_deleted,");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("the engines it writes exist in the pinned catalog and take the arguments it gives", () => {
|
|
83
|
+
expect(Object.keys(TABLE_ENGINES)).toContain("MergeTree");
|
|
84
|
+
const replacing = TABLE_ENGINES.ReplacingMergeTree;
|
|
85
|
+
expect(replacing.args?.map((a) => a.name)).toEqual(["ver", "is_deleted"]);
|
|
86
|
+
expect(replacing.mergeTree).toBe(true);
|
|
87
|
+
expect(CDC_VERSION_COLUMN.type in TYPE_FAMILIES).toBe(true);
|
|
88
|
+
expect(CDC_DELETED_COLUMN.type in TYPE_FAMILIES).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("nullableReview", () => {
|
|
93
|
+
test("a NOT NULL column keeps its type and raises nothing", () => {
|
|
94
|
+
expect(nullableReview({ object: "t", column: "id", type: "Int64", sourceNullable: false })).toEqual({ type: "Int64" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a nullable column with a default says the default could replace Nullable", () => {
|
|
98
|
+
const r = nullableReview({ object: "t", column: "n", type: "Int32", sourceNullable: true, defaultExpr: "0" });
|
|
99
|
+
expect(r.type).toBe("Nullable(Int32)");
|
|
100
|
+
expect(r.finding?.evidence).toEqual(["n allows NULL in the source", "it has the default 0"]);
|
|
101
|
+
expect(r.finding?.suggestion).toContain("Int32 DEFAULT 0");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a nullable column with no default says so", () => {
|
|
105
|
+
const r = nullableReview({ object: "t", column: "n", type: "String", sourceNullable: true });
|
|
106
|
+
expect(r.type).toBe("Nullable(String)");
|
|
107
|
+
expect(r.finding?.evidence).toEqual(["n allows NULL in the source", "it has no default"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("Nullable is a family of the pinned catalog", () => {
|
|
111
|
+
expect("Nullable" in TYPE_FAMILIES && "LowCardinality" in TYPE_FAMILIES).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("lowCardinalityReview", () => {
|
|
116
|
+
const at = (column: string, type = "String", maxLength?: number) => lowCardinalityReview({ object: "t", column, type, ...(maxLength !== undefined ? { maxLength } : {}) });
|
|
117
|
+
|
|
118
|
+
test("a String column named for a small set is a candidate and keeps its type", () => {
|
|
119
|
+
for (const name of ["status", "order_status", "OrderStatus", "country_code", "currency"]) {
|
|
120
|
+
const f = at(name);
|
|
121
|
+
expect(f?.ruleId, name).toBe("SQLRV004");
|
|
122
|
+
expect(f?.message).toContain("stays String");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the declared length is evidence when it is short", () => {
|
|
127
|
+
expect(at("status", "String", 20)?.evidence).toContain("the declared length is 20");
|
|
128
|
+
expect(at("status", "String", 4000)?.evidence.join(" ")).not.toContain("declared length");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("other names, other types and words that merely end the same way are not candidates", () => {
|
|
132
|
+
expect(at("name")).toBeUndefined();
|
|
133
|
+
expect(at("birthday")).toBeUndefined();
|
|
134
|
+
expect(at("prostatus")).toBeUndefined();
|
|
135
|
+
expect(at("status", "Int32")).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a Nullable(String) column is a candidate and the suggestion names the nested form", () => {
|
|
139
|
+
expect(at("status", "Nullable(String)")?.suggestion).toContain("LowCardinality(Nullable(String))");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("constraintReview", () => {
|
|
144
|
+
test("a foreign key, a unique constraint and a check are comments and findings", () => {
|
|
145
|
+
const r = constraintReview({
|
|
146
|
+
object: "t",
|
|
147
|
+
constraints: [
|
|
148
|
+
{ kind: "foreign-key", name: "fk", columns: ["a"], references: { table: "u", columns: ["id"] } },
|
|
149
|
+
{ kind: "unique", name: "uq", columns: ["b", "c"] },
|
|
150
|
+
{ kind: "check", name: "ck", expression: "([x] > 0\n AND [x] < 10)" },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
expect(r.comments).toEqual(["-- FOREIGN KEY fk (a) REFERENCES u (id)", "-- UNIQUE uq (b, c)", "-- CHECK ck ([x] > 0 AND [x] < 10)"]);
|
|
154
|
+
expect(r.findings.map((f) => f.ruleId)).toEqual(["SQLRV005", "SQLRV005", "SQLRV005"]);
|
|
155
|
+
expect(r.findings[0]?.column).toBe("a");
|
|
156
|
+
expect(r.findings[1]?.column).toBeUndefined();
|
|
157
|
+
expect(r.comments.every((c) => !c.includes("\n"))).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("constraintReview: an exclusion constraint (#4399)", () => {
|
|
162
|
+
test("is a comment and a finding with no column", () => {
|
|
163
|
+
const r = constraintReview({ object: "t", constraints: [{ kind: "exclusion", name: "no_overlap", expression: "USING gist (during WITH &&)" }] });
|
|
164
|
+
expect(r.comments).toEqual(["-- EXCLUDE no_overlap USING gist (during WITH &&)"]);
|
|
165
|
+
expect(r.findings[0]).toMatchObject({ ruleId: "SQLRV005", severity: "warning" });
|
|
166
|
+
expect(r.findings[0]?.column).toBeUndefined();
|
|
167
|
+
expect(r.findings[0]?.message).toContain("Exclusion constraint no_overlap is not enforced");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("the rules", () => {
|
|
172
|
+
test("every rule has a unique id and a name", () => {
|
|
173
|
+
const names = Object.values(REVIEW_RULES).map((r) => r.name);
|
|
174
|
+
expect(new Set(names).size).toBe(names.length);
|
|
175
|
+
for (const [id, r] of Object.entries(REVIEW_RULES)) expect(r.id).toBe(id);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("findingText leads with the object and column, then evidence and suggestion", () => {
|
|
179
|
+
expect(findingText({ ruleId: "SQLRV003", severity: "warning", object: "t", column: "c", message: "m.", evidence: ["a", "b"], suggestion: "s." })).toBe("t.c: m. Evidence: a; b. Suggestion: s.");
|
|
180
|
+
});
|
|
181
|
+
});
|