@intentius/chant-lexicon-sql 0.124.45 → 0.124.48
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/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 +21 -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 +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/codegen/registry.js +5 -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/coverage/clickhouse.json +1 -1
- package/dist/coverage/postgres.json +7 -7
- 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 +684 -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.js +12 -2
- 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 +6 -5
- 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/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 +11 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/postgres.js +1 -1
- package/dist/meta.json +25 -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-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 +6 -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/PostgresOperator.md +9 -0
- package/dist/okf/types/PostgresPublication.md +9 -0
- package/dist/okf/types/PostgresStatistics.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 +36 -2
- package/dist/postgres/apply/apply.js +8 -1
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/apply/statements.js +87 -2
- 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 +110 -1
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entities.js +146 -0
- package/dist/postgres/entity-types.d.ts +5 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/entity-types.js +5 -0
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/generator.js +50 -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 +14 -0
- package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
- package/dist/postgres/live/catalog-kinds.js +206 -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 +10 -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 +41 -12
- 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/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 +72 -1
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/parser.js +194 -2
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/deep.js +29 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/diff.js +39 -1
- package/dist/postgres/plan/normalize.d.ts +17 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.js +109 -1
- package/dist/postgres/plan/rules.d.ts +1 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/rules.js +3 -2
- package/dist/postgres/plan/schema.js +1 -1
- 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 +3 -2
- package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
- package/dist/postgres/testing/roundtrip-schema.js +13 -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-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 +5 -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/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 +21 -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 +1 -1
- package/src/codegen/registry.ts +5 -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/undeclarable.test.ts +6 -5
- package/src/coverage/clickhouse.json +1 -1
- package/src/coverage/postgres.json +7 -7
- 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 +737 -0
- package/src/files/clickhouse.ts +8 -1
- package/src/files/common.ts +14 -1
- package/src/files/postgres.ts +12 -2
- package/src/generated/clickhouse.ts +3 -0
- package/src/generated/lexicon-sql.json +25 -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/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 +11 -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-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/plugin.ts +37 -2
- package/src/postgres/apply/apply.ts +9 -1
- package/src/postgres/apply/statements.ts +74 -1
- package/src/postgres/constraint-names.ts +71 -0
- package/src/postgres/database-kinds.test.ts +384 -0
- package/src/postgres/entities.ts +266 -1
- package/src/postgres/entity-types.ts +5 -0
- package/src/postgres/import/generator.ts +42 -2
- package/src/postgres/import/live-export.ts +1 -0
- package/src/postgres/live/catalog-kinds.ts +223 -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 +11 -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 +53 -14
- 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/steps.ts +157 -12
- package/src/postgres/parser.ts +234 -2
- package/src/postgres/plan/deep.ts +28 -1
- package/src/postgres/plan/diff.ts +36 -1
- package/src/postgres/plan/normalize.ts +122 -1
- package/src/postgres/plan/rules.ts +10 -3
- package/src/postgres/plan/schema.ts +1 -1
- package/src/postgres/secret-options.ts +10 -0
- package/src/postgres/testing/roundtrip-schema.ts +13 -1
- package/src/postgres.ts +16 -0
- package/src/serializer.ts +13 -2
- package/src/skills/chant-sql-postgres-migration.md +3 -3
- package/src/skills/chant-sql-postgres.md +6 -1
- package/src/skills/chant-sql.md +2 -1
- 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 +5 -0
- package/src/workspace-kinds.test.ts +11 -0
- package/workspace-kinds.json +100 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The T-SQL to ClickHouse type map.
|
|
3
|
+
*
|
|
4
|
+
* Source types and their ranges, precisions and defaults are from Microsoft's
|
|
5
|
+
* data type reference
|
|
6
|
+
* (https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql)
|
|
7
|
+
* and the page of each type under it: `float(n)` is `real` for n in 1..24 and
|
|
8
|
+
* the 8-byte type for 25..53 with a default of 53; `decimal(p,s)` defaults to
|
|
9
|
+
* (18,0) with p up to 38; `datetime2` and `time` take a scale of 0 to 7 with
|
|
10
|
+
* a default of 7; `money` is a 64-bit integer with four decimals and
|
|
11
|
+
* `smallmoney` a 32-bit one; `rowversion` (`timestamp`) is an 8-byte counter.
|
|
12
|
+
*
|
|
13
|
+
* Target types are ClickHouse types of the pinned server (see
|
|
14
|
+
* `../spec/pin.ts`). `types.test.ts` checks every family this file writes
|
|
15
|
+
* against `TYPE_FAMILIES` in the generated catalog and every precision against
|
|
16
|
+
* the `DateTime64`, `Time64` and `Decimal` ranges, so a pin move that drops a
|
|
17
|
+
* type fails there.
|
|
18
|
+
*
|
|
19
|
+
* A type either maps with no judgment (`note` absent) or carries a `note`
|
|
20
|
+
* naming what the map could not keep. A type this file does not know
|
|
21
|
+
* (`sql_variant`, a user-defined alias type) is refused by name.
|
|
22
|
+
*/
|
|
23
|
+
const num = (args, i) => (typeof args[i] === "number" ? args[i] : undefined);
|
|
24
|
+
/** Map a column type. `name` is lower-case, `args` the parenthesised arguments. */
|
|
25
|
+
export function mapType(name, args) {
|
|
26
|
+
switch (name) {
|
|
27
|
+
case "tinyint":
|
|
28
|
+
return { ok: true, type: "UInt8" };
|
|
29
|
+
case "smallint":
|
|
30
|
+
return { ok: true, type: "Int16" };
|
|
31
|
+
case "int":
|
|
32
|
+
case "integer":
|
|
33
|
+
return { ok: true, type: "Int32" };
|
|
34
|
+
case "bigint":
|
|
35
|
+
return { ok: true, type: "Int64" };
|
|
36
|
+
case "bit":
|
|
37
|
+
return { ok: true, type: "Bool" };
|
|
38
|
+
case "decimal":
|
|
39
|
+
case "numeric":
|
|
40
|
+
case "dec": {
|
|
41
|
+
const p = num(args, 0) ?? 18;
|
|
42
|
+
const s = num(args, 1) ?? 0;
|
|
43
|
+
return { ok: true, type: `Decimal(${String(p)}, ${String(s)})` };
|
|
44
|
+
}
|
|
45
|
+
case "money":
|
|
46
|
+
return { ok: true, type: "Decimal(19, 4)" };
|
|
47
|
+
case "smallmoney":
|
|
48
|
+
return { ok: true, type: "Decimal(10, 4)" };
|
|
49
|
+
case "real":
|
|
50
|
+
return { ok: true, type: "Float32" };
|
|
51
|
+
case "float": {
|
|
52
|
+
const n = num(args, 0) ?? 53;
|
|
53
|
+
return { ok: true, type: n <= 24 ? "Float32" : "Float64" };
|
|
54
|
+
}
|
|
55
|
+
case "date":
|
|
56
|
+
return { ok: true, type: "Date32" };
|
|
57
|
+
case "time":
|
|
58
|
+
return { ok: true, type: `Time64(${String(num(args, 0) ?? 7)})` };
|
|
59
|
+
case "datetime2":
|
|
60
|
+
return { ok: true, type: `DateTime64(${String(num(args, 0) ?? 7)})` };
|
|
61
|
+
case "datetime":
|
|
62
|
+
return { ok: true, type: "DateTime64(3)" };
|
|
63
|
+
case "smalldatetime":
|
|
64
|
+
return { ok: true, type: "DateTime64(0)" };
|
|
65
|
+
case "datetimeoffset": {
|
|
66
|
+
const p = num(args, 0) ?? 7;
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
type: `DateTime64(${String(p)}, 'UTC')`,
|
|
70
|
+
note: {
|
|
71
|
+
message: "datetimeoffset is read as a UTC DateTime64; the stored offset is not kept.",
|
|
72
|
+
evidence: ["datetimeoffset stores a time zone offset (-14:00 to +14:00) with each value; a ClickHouse DateTime64 holds an instant and one column time zone", "values must be converted to UTC when loaded"],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
case "char":
|
|
77
|
+
case "varchar":
|
|
78
|
+
case "nchar":
|
|
79
|
+
case "nvarchar":
|
|
80
|
+
case "text":
|
|
81
|
+
case "ntext":
|
|
82
|
+
case "sysname": {
|
|
83
|
+
const max = name === "sysname" ? 128 : typeof args[0] === "number" ? args[0] : undefined;
|
|
84
|
+
return { ok: true, type: "String", ...(max !== undefined ? { maxLength: max } : {}) };
|
|
85
|
+
}
|
|
86
|
+
case "binary":
|
|
87
|
+
case "varbinary":
|
|
88
|
+
case "image": {
|
|
89
|
+
const max = typeof args[0] === "number" ? args[0] : undefined;
|
|
90
|
+
return { ok: true, type: "String", ...(max !== undefined ? { maxLength: max } : {}) };
|
|
91
|
+
}
|
|
92
|
+
case "uniqueidentifier":
|
|
93
|
+
return { ok: true, type: "UUID" };
|
|
94
|
+
case "json":
|
|
95
|
+
return { ok: true, type: "JSON" };
|
|
96
|
+
case "xml":
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
type: "String",
|
|
100
|
+
note: { message: "xml is read as String; ClickHouse has no XML type.", evidence: ["the xml type validates and indexes documents; a String does neither"] },
|
|
101
|
+
};
|
|
102
|
+
case "rowversion":
|
|
103
|
+
case "timestamp":
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
type: "String",
|
|
107
|
+
note: {
|
|
108
|
+
message: `${name} is read as String; its 8 bytes are an opaque counter.`,
|
|
109
|
+
evidence: ["rowversion is a database-wide counter that SQL Server writes on each row change, not a date or time", "a String keeps the bytes; a UInt64 version column has to be loaded from the source"],
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
case "hierarchyid":
|
|
113
|
+
return { ok: true, type: "String", note: { message: "hierarchyid is read as String; ClickHouse has no hierarchy type.", evidence: ["the binary form is kept as bytes; hierarchy methods (GetAncestor, IsDescendantOf) have no ClickHouse equivalent"] } };
|
|
114
|
+
case "geography":
|
|
115
|
+
case "geometry":
|
|
116
|
+
return { ok: true, type: "String", note: { message: `${name} is read as String; the spatial value is not converted.`, evidence: ["a spatial value needs a conversion (well-known text or binary) at load time", "ClickHouse has Point, Ring and Polygon types, which fit only some shapes"] } };
|
|
117
|
+
case "vector": {
|
|
118
|
+
const dims = num(args, 0);
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
type: "Array(Float32)",
|
|
122
|
+
note: { message: "vector is read as Array(Float32).", evidence: [dims !== undefined ? `declared with ${String(dims)} dimensions, which an Array does not enforce` : "the dimension count is not enforced by an Array"] },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
case "sql_variant":
|
|
126
|
+
return { ok: false, reason: "sql_variant holds values of any type, and no single ClickHouse type matches it" };
|
|
127
|
+
case "cursor":
|
|
128
|
+
case "table":
|
|
129
|
+
return { ok: false, reason: `${name} is not a type a stored column can have` };
|
|
130
|
+
default:
|
|
131
|
+
return { ok: false, reason: `the type ${name} is not a system type; user-defined alias types are not read` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant import --validate` for a translated schema (#4398): create the
|
|
3
|
+
* emitted DDL on the pinned ClickHouse server, as `chant migrate --validate`
|
|
4
|
+
* runs the emitted YAML through the target's linter.
|
|
5
|
+
*
|
|
6
|
+
* `validateOnEmulator` starts its own throwaway container of the pinned image
|
|
7
|
+
* (`../clickhouse/container.ts`) and removes it afterwards. It never reads
|
|
8
|
+
* `CLICKHOUSE_URL`, so a validation cannot reach a server that holds data.
|
|
9
|
+
* `validateStatements` is the part that needs no Docker: it runs statements
|
|
10
|
+
* through any `run` function, in the order a server needs them (databases,
|
|
11
|
+
* then tables, then views), and turns each refusal into an `error`
|
|
12
|
+
* diagnostic naming the object.
|
|
13
|
+
*/
|
|
14
|
+
import type { ImportDiagnostic, TemplateIR } from "@intentius/chant/import/parser";
|
|
15
|
+
export interface ValidationStatement {
|
|
16
|
+
/** The object, as the diagnostic names it. */
|
|
17
|
+
name: string;
|
|
18
|
+
ddl: string;
|
|
19
|
+
type: string;
|
|
20
|
+
}
|
|
21
|
+
/** The IR's ClickHouse objects as statements in creation order. */
|
|
22
|
+
export declare function statementsOf(ir: TemplateIR): ValidationStatement[];
|
|
23
|
+
/** Run each statement; a refusal is an error diagnostic, and the run goes on to the next. */
|
|
24
|
+
export declare function validateStatements(statements: readonly ValidationStatement[], run: (sql: string) => Promise<void>): Promise<ImportDiagnostic[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Create the IR's objects on a throwaway server of the pinned image. Throws,
|
|
27
|
+
* naming the cause, when Docker is not available: the caller reports that the
|
|
28
|
+
* validation did not run, rather than reporting a pass.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateOnEmulator(ir: TemplateIR): Promise<ImportDiagnostic[]>;
|
|
31
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/sqlserver/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAMnF,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AASD,mEAAmE;AACnE,wBAAgB,YAAY,CAAC,EAAE,EAAE,UAAU,GAAG,mBAAmB,EAAE,CAQlE;AAED,6FAA6F;AAC7F,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAWrJ;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAWpF"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant import --validate` for a translated schema (#4398): create the
|
|
3
|
+
* emitted DDL on the pinned ClickHouse server, as `chant migrate --validate`
|
|
4
|
+
* runs the emitted YAML through the target's linter.
|
|
5
|
+
*
|
|
6
|
+
* `validateOnEmulator` starts its own throwaway container of the pinned image
|
|
7
|
+
* (`../clickhouse/container.ts`) and removes it afterwards. It never reads
|
|
8
|
+
* `CLICKHOUSE_URL`, so a validation cannot reach a server that holds data.
|
|
9
|
+
* `validateStatements` is the part that needs no Docker: it runs statements
|
|
10
|
+
* through any `run` function, in the order a server needs them (databases,
|
|
11
|
+
* then tables, then views), and turns each refusal into an `error`
|
|
12
|
+
* diagnostic naming the object.
|
|
13
|
+
*/
|
|
14
|
+
import { CLICKHOUSE_ENTITY_TYPES } from "../clickhouse/entities.js";
|
|
15
|
+
import { ClickHouseQueryError, clickhouseQuery } from "../clickhouse/http.js";
|
|
16
|
+
import { dockerAvailable, startScratchServer } from "../clickhouse/container.js";
|
|
17
|
+
import { clickhouseImage } from "../spec/pin.js";
|
|
18
|
+
const RANK = {
|
|
19
|
+
[CLICKHOUSE_ENTITY_TYPES.database]: 0,
|
|
20
|
+
[CLICKHOUSE_ENTITY_TYPES.table]: 1,
|
|
21
|
+
[CLICKHOUSE_ENTITY_TYPES.view]: 2,
|
|
22
|
+
[CLICKHOUSE_ENTITY_TYPES.materializedView]: 2,
|
|
23
|
+
};
|
|
24
|
+
/** The IR's ClickHouse objects as statements in creation order. */
|
|
25
|
+
export function statementsOf(ir) {
|
|
26
|
+
const out = ir.resources.map((r, i) => {
|
|
27
|
+
const p = r.properties;
|
|
28
|
+
return { name: p.database ? `${p.database}.${p.name}` : p.name, ddl: p.ddl, type: r.type, i };
|
|
29
|
+
});
|
|
30
|
+
return out
|
|
31
|
+
.sort((a, b) => (RANK[a.type] ?? 3) - (RANK[b.type] ?? 3) || a.i - b.i)
|
|
32
|
+
.map(({ name, ddl, type }) => ({ name, ddl, type }));
|
|
33
|
+
}
|
|
34
|
+
/** Run each statement; a refusal is an error diagnostic, and the run goes on to the next. */
|
|
35
|
+
export async function validateStatements(statements, run) {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const s of statements) {
|
|
38
|
+
try {
|
|
39
|
+
await run(s.ddl);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
const msg = e instanceof ClickHouseQueryError ? e.serverMessage.trim().split("\n")[0] : e instanceof Error ? e.message : String(e);
|
|
43
|
+
out.push({ ruleId: "SQLRV012", severity: "error", message: `${s.name}: the emulator rejected the statement. ${msg}`, entity: s.name });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create the IR's objects on a throwaway server of the pinned image. Throws,
|
|
50
|
+
* naming the cause, when Docker is not available: the caller reports that the
|
|
51
|
+
* validation did not run, rather than reporting a pass.
|
|
52
|
+
*/
|
|
53
|
+
export async function validateOnEmulator(ir) {
|
|
54
|
+
if (ir.resources.some((r) => r.type.startsWith("Postgres::")))
|
|
55
|
+
throw new Error("--validate checks ClickHouse DDL; this template holds Postgres objects");
|
|
56
|
+
if (!(await dockerAvailable()))
|
|
57
|
+
throw new Error("Docker is not available, so the ClickHouse emulator cannot start");
|
|
58
|
+
const server = await startScratchServer(clickhouseImage(), { namePrefix: "chant-sql-import-validate" });
|
|
59
|
+
try {
|
|
60
|
+
return await validateStatements(statementsOf(ir), async (sql) => {
|
|
61
|
+
await clickhouseQuery(server.endpoint, sql);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await server.stop();
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/validate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAgD,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAMtH,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAgD,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAMtH,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAuFvF,wBAAsB,QAAQ,CAAC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAoEpF"}
|
package/dist/validate.js
CHANGED
|
@@ -43,6 +43,11 @@ const REQUIRED_ENTITIES = [
|
|
|
43
43
|
"PostgresCollation",
|
|
44
44
|
"PostgresCast",
|
|
45
45
|
"PostgresEventTrigger",
|
|
46
|
+
"PostgresOperator",
|
|
47
|
+
"PostgresStatistics",
|
|
48
|
+
"PostgresForeignDataWrapper",
|
|
49
|
+
"PostgresForeignServer",
|
|
50
|
+
"PostgresPublication",
|
|
46
51
|
"PostgresExtension",
|
|
47
52
|
"PostgresFunction",
|
|
48
53
|
"PostgresProcedure",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-sql",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.48",
|
|
4
4
|
"description": "SQL lexicon for chant — database schema as typed declarations, one dialect at a time; ClickHouse first, with types read from a pinned server's catalog",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -72,6 +72,6 @@
|
|
|
72
72
|
"typescript": "^5.9.3"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@intentius/chant": "^0.124.
|
|
75
|
+
"@intentius/chant": "^0.124.48"
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/change-set.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* dialects' `CHANGE_CLASSES` and `PG_CHANGE_CLASSES` price them:
|
|
26
26
|
*
|
|
27
27
|
* - a `drop` class is a `delete` (`destroy`); a `create` class a `create`;
|
|
28
|
-
* - a `rebuild` or `expand` class, or a step a migration Op makes, is a
|
|
28
|
+
* - a `rebuild`, `replace` or `expand` class, or a step a migration Op makes, is a
|
|
29
29
|
* `replace` (`replace`): the object, or a column of it, is built again;
|
|
30
30
|
* - anything else is an `update`, its disruption the worst class's
|
|
31
31
|
* (`metadata` in-place; `validate`, `concurrently` and `rewrite` rolling).
|
|
@@ -67,6 +67,7 @@ export const SQL_CLASS_DISRUPTION: Readonly<Record<string, ChangeSetDisruption>>
|
|
|
67
67
|
validate: "rolling",
|
|
68
68
|
concurrently: "rolling",
|
|
69
69
|
rewrite: "rolling",
|
|
70
|
+
replace: "replace",
|
|
70
71
|
rebuild: "replace",
|
|
71
72
|
expand: "replace",
|
|
72
73
|
drop: "destroy",
|
|
@@ -24,6 +24,7 @@ import { CLASSIFIER_RULES, type ChangeClass, type ClassifierRuleId } from "../pl
|
|
|
24
24
|
import { stampedComment } from "../ownership";
|
|
25
25
|
import { CLICKHOUSE_ENTITY_TYPES, type ClickHouseEntityType } from "../entities";
|
|
26
26
|
import { renderFor, type Topology } from "../topology";
|
|
27
|
+
import { readsTable } from "../plan/stream";
|
|
27
28
|
import { accessClauseText, alterAccessField, alterQuotaOrProfileField, grantStatement, revokeStatement, SECRETLESS, type GrantAtom } from "../access";
|
|
28
29
|
|
|
29
30
|
/** A backquoted identifier. */
|
|
@@ -613,6 +614,24 @@ function deferredDefaultRoles(
|
|
|
613
614
|
return out;
|
|
614
615
|
}
|
|
615
616
|
|
|
617
|
+
/**
|
|
618
|
+
* The statements that replace a stream table (#4396), in the order they run:
|
|
619
|
+
* detach each materialized view that reads it (`readers`, as the server has
|
|
620
|
+
* them), drop the table, create it as declared, attach the views. The table
|
|
621
|
+
* holds no rows, so nothing is copied; a view that stays detached for the
|
|
622
|
+
* length of the replacement reads nothing, and the messages wait in the
|
|
623
|
+
* broker (or the files in the bucket) for the new table.
|
|
624
|
+
*/
|
|
625
|
+
export function replaceStreamSteps(obj: DeclaredObject, live: CanonicalObject, readers: readonly CanonicalObject[], marker: OwnershipMarker | undefined): Step[] {
|
|
626
|
+
const view = (r: CanonicalObject) => qualifiedIdent(r.database, r.name);
|
|
627
|
+
return [
|
|
628
|
+
...readers.map((r) => stepFor(`DETACH TABLE ${view(r)}`, "SQLCH225")),
|
|
629
|
+
stepFor(`DROP TABLE ${qualifiedIdent(live.database, live.name)} SYNC`, "SQLCH225"),
|
|
630
|
+
stepFor(createStatement(obj, marker), "SQLCH225"),
|
|
631
|
+
...readers.map((r) => stepFor(`ATTACH TABLE ${view(r)}`, "SQLCH225")),
|
|
632
|
+
];
|
|
633
|
+
}
|
|
634
|
+
|
|
616
635
|
function planDeclared(input: StatementPlanInput): StatementPlan {
|
|
617
636
|
const keyOf = input.keyOf ?? ((o: DeclaredObject) => o.key);
|
|
618
637
|
const byObject = new Map<string, Change[]>();
|
|
@@ -640,6 +659,12 @@ function planDeclared(input: StatementPlanInput): StatementPlan {
|
|
|
640
659
|
const renamedFrom = mine.find((c) => c.rule === "SQLCH230" || c.rule === "SQLCH231")?.before;
|
|
641
660
|
const currentKey = renamedFrom !== undefined && input.current.has(renamedFrom) ? renamedFrom : key;
|
|
642
661
|
const live = input.current.get(currentKey);
|
|
662
|
+
// A stream table (#4396): the views that read it are detached, it is dropped and created, and they are attached again.
|
|
663
|
+
if (live && mine.some((c) => c.rule === "SQLCH225")) {
|
|
664
|
+
const dropped = new Set(input.changes.filter((c) => c.rule === "SQLCH250").map((c) => c.object));
|
|
665
|
+
const readers = [...input.current].filter(([k, o]) => !dropped.has(k) && readsTable(o, live)).map(([, o]) => o);
|
|
666
|
+
return { verdict: "alter", obj, changes: mine, steps: replaceStreamSteps(obj, live, readers, input.marker) };
|
|
667
|
+
}
|
|
643
668
|
// A function has no comment, so nothing to restamp: it never carries the marker.
|
|
644
669
|
const restamp =
|
|
645
670
|
obj.type !== CLICKHOUSE_ENTITY_TYPES.function && !stepsSetComment(obj, mine) && (mine.some((c) => c.field === "comment") || !(input.carriesMarker?.(currentKey) ?? true));
|
|
@@ -23,12 +23,16 @@
|
|
|
23
23
|
* (#3745), so a use is found by that body (`./inlined.ts`) as well as by
|
|
24
24
|
* the name. A function carries no comment, so `owned` does
|
|
25
25
|
* not apply to it.
|
|
26
|
-
* - An object of a kind no declaration holds (a window view, a
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* - An object of a kind no declaration holds (a window view, a workload,
|
|
27
|
+
* an HTTP handler, ...; `../live/undeclarable.ts`) fails the import, which
|
|
28
|
+
* names every one of them (#3911).
|
|
29
29
|
* `skipUndeclarable` (`--skip-undeclarable`) imports the rest and lists
|
|
30
30
|
* them as warnings. A selector or `owned` import asks for objects chant can
|
|
31
31
|
* hold, so it does not look.
|
|
32
|
+
* - A SQL-defined named collection is the environment's (#4395): the import
|
|
33
|
+
* reads its name only, never its values, and lists it as a prerequisite
|
|
34
|
+
* (a warning, and `metadata.prerequisites` with the imported objects that
|
|
35
|
+
* name it) instead of refusing the server.
|
|
32
36
|
*/
|
|
33
37
|
|
|
34
38
|
import type { ExportedTemplate, ResourceSelector } from "@intentius/chant/lexicon";
|
|
@@ -41,8 +45,9 @@ import { isTrivia, tokenizeText } from "../tokens";
|
|
|
41
45
|
import { unquote } from "../parser";
|
|
42
46
|
import type { LiveObject } from "../live/catalog";
|
|
43
47
|
import { inlinedFunctions } from "./inlined";
|
|
44
|
-
import { readUndeclarable } from "../live/undeclarable";
|
|
48
|
+
import { readReferenced, readUndeclarable } from "../live/undeclarable";
|
|
45
49
|
import { refuseUndeclarable } from "../../core/undeclarable";
|
|
50
|
+
import { namedCollectionPrerequisites } from "../named-collections";
|
|
46
51
|
|
|
47
52
|
/** The functions among `names` a statement calls: a name followed by `(`. */
|
|
48
53
|
export function calledFunctions(ddl: string, names: ReadonlySet<string>): string[] {
|
|
@@ -137,5 +142,15 @@ export async function exportResources(options: ExportOptions): Promise<ExportedT
|
|
|
137
142
|
name: o.name,
|
|
138
143
|
ddl: options.verbatim ? o.statement : stripServerDefaults(o.statement),
|
|
139
144
|
}));
|
|
140
|
-
|
|
145
|
+
// Named collections are the environment's (#4395): listed as prerequisites, never imported.
|
|
146
|
+
const referenced = await readReferenced(target);
|
|
147
|
+
for (const u of referenced.unread) warnings.push(`the server's named collections were not read, so the import cannot list them as prerequisites: ${u}`);
|
|
148
|
+
const { prerequisites, warnings: prereq } = namedCollectionPrerequisites(referenced.objects, objects, {
|
|
149
|
+
of: options.environment,
|
|
150
|
+
found: "the server holds it",
|
|
151
|
+
unseen: "the reader did not see it on the server (it needs SHOW NAMED COLLECTIONS)",
|
|
152
|
+
});
|
|
153
|
+
warnings.push(...prereq);
|
|
154
|
+
const ir = objectsToIR(objects, warnings);
|
|
155
|
+
return (prerequisites.length > 0 ? { ...ir, metadata: { ...ir.metadata, prerequisites } } : ir) as ExportedTemplate;
|
|
141
156
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named collections against the pinned server (#4395): a plan fails naming a
|
|
3
|
+
* collection the build references and the server lacks, and passes once it
|
|
4
|
+
* exists, without reading its values; import from a server that holds one
|
|
5
|
+
* succeeds and lists it as a prerequisite.
|
|
6
|
+
*
|
|
7
|
+
* Needs Docker; skips cleanly without it. One throwaway
|
|
8
|
+
* `clickhouse/clickhouse-server` container at the pin, removed afterwards
|
|
9
|
+
* even on failure.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
16
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
17
|
+
import { dockerAvailable, startScratchServer, type ScratchServer } from "../container";
|
|
18
|
+
import { clickhouseQuery } from "../http";
|
|
19
|
+
import { clickhouseImage } from "../../spec/pin";
|
|
20
|
+
import { database, table } from "../entities";
|
|
21
|
+
import { sqlSerializer } from "../../serializer";
|
|
22
|
+
import { planAgainstServer } from "../plan/commands";
|
|
23
|
+
import { renderDiff } from "../plan/report";
|
|
24
|
+
import { exportResources } from "../import/live-export";
|
|
25
|
+
|
|
26
|
+
const enabled = await dockerAvailable();
|
|
27
|
+
let server: ScratchServer | undefined;
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-sql-named-collections-"));
|
|
29
|
+
const buildFile = join(dir, "lake.json");
|
|
30
|
+
const q = (sql: string) => clickhouseQuery(server!.endpoint, sql);
|
|
31
|
+
|
|
32
|
+
const DB = "lakehouse";
|
|
33
|
+
const db = database`CREATE DATABASE lakehouse`;
|
|
34
|
+
const lake = table`CREATE TABLE ${db}.lake (a Int32) ENGINE = S3(lake_creds, url = 'http://minio:9000/bucket/x.parquet', format = 'Parquet')`;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
if (!enabled) return;
|
|
38
|
+
server = await startScratchServer(clickhouseImage(), { namePrefix: "chant-sql-named-collections" });
|
|
39
|
+
const out = sqlSerializer.serialize(new Map<string, unknown>([["db", db], ["lake", lake]]) as never) as SerializerResult;
|
|
40
|
+
writeFileSync(buildFile, out.primary);
|
|
41
|
+
}, 600_000);
|
|
42
|
+
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
await server?.stop();
|
|
45
|
+
rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const env = (user?: string) => ({ config: {}, env: { CLICKHOUSE_URL: server!.endpoint.url, ...(user ? { CLICKHOUSE_USER: user } : {}) } });
|
|
49
|
+
|
|
50
|
+
describe.skipIf(!enabled)("named collections on the pinned server", () => {
|
|
51
|
+
test("a plan against a server without lake_creds fails, naming it", async () => {
|
|
52
|
+
const diff = await planAgainstServer("e2e", buildFile, env());
|
|
53
|
+
expect(diff.namedCollections).toEqual({ referenced: ["lake_creds"], missing: ["lake_creds"] });
|
|
54
|
+
expect(renderDiff(diff)).toMatch(/Refused: the build references a named collection the server does not have: lake_creds\./);
|
|
55
|
+
}, 120_000);
|
|
56
|
+
|
|
57
|
+
test("with it, the plan passes, and no query reads its values", async () => {
|
|
58
|
+
await q("CREATE NAMED COLLECTION lake_creds AS access_key_id = 'AKIAEXAMPLE', secret_access_key = 'not-a-real-secret', url = 'http://minio:9000/bucket/'");
|
|
59
|
+
const diff = await planAgainstServer("e2e", buildFile, env());
|
|
60
|
+
expect(diff.namedCollections).toEqual({ referenced: ["lake_creds"], missing: [] });
|
|
61
|
+
await q("SYSTEM FLUSH LOGS");
|
|
62
|
+
const reads = await clickhouseQuery<{ query: string }>(
|
|
63
|
+
server!.endpoint,
|
|
64
|
+
"SELECT query FROM system.query_log WHERE type = 'QueryFinish' AND query LIKE '%chant%' AND query LIKE '%named_collections%' AND query NOT LIKE '%query_log%'",
|
|
65
|
+
);
|
|
66
|
+
expect(reads.length).toBeGreaterThan(0);
|
|
67
|
+
for (const r of reads) expect(r.query).not.toMatch(/\bcollection\b(?! \*\/)|create_query/);
|
|
68
|
+
}, 120_000);
|
|
69
|
+
|
|
70
|
+
test("a user without SHOW NAMED COLLECTIONS sees none, and the plan says so", async () => {
|
|
71
|
+
await q("CREATE USER IF NOT EXISTS planner IDENTIFIED WITH no_password");
|
|
72
|
+
await q("GRANT SELECT, SHOW ON *.* TO planner");
|
|
73
|
+
const diff = await planAgainstServer("e2e", buildFile, env("planner"));
|
|
74
|
+
expect(diff.namedCollections?.missing).toEqual(["lake_creds"]);
|
|
75
|
+
expect(renderDiff(diff)).toMatch(/grant the plan's user SHOW NAMED COLLECTIONS/);
|
|
76
|
+
}, 120_000);
|
|
77
|
+
|
|
78
|
+
test("import from a server with a SQL-defined named collection succeeds and lists it as a prerequisite", async () => {
|
|
79
|
+
await q(`CREATE DATABASE IF NOT EXISTS ${DB}`);
|
|
80
|
+
await q(`CREATE TABLE IF NOT EXISTS ${DB}.lake (a Int32) ENGINE = S3(lake_creds, url = 'http://minio:9000/bucket/x.parquet', format = 'Parquet')`);
|
|
81
|
+
const ir = await exportResources({ environment: "e2e", ...env() });
|
|
82
|
+
expect(ir.resources.map((r) => r.properties.name)).toEqual([DB, "lake"]);
|
|
83
|
+
expect(ir.metadata?.prerequisites).toEqual([{ kind: "named collection", name: "lake_creds", referencedBy: [`${DB}.lake`] }]);
|
|
84
|
+
expect(ir.warnings).toContainEqual(expect.stringMatching(/^named collection lake_creds is a prerequisite of e2e, named by lakehouse\.lake: the server holds it/));
|
|
85
|
+
// The import carries the name, never a value.
|
|
86
|
+
expect(JSON.stringify(ir)).not.toMatch(/AKIAEXAMPLE|not-a-real-secret/);
|
|
87
|
+
}, 120_000);
|
|
88
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan's existence check for the named collections a build references
|
|
3
|
+
* (#4395): by name, in `system.named_collections`, never the `collection`
|
|
4
|
+
* or `create_query` columns, which hold the values.
|
|
5
|
+
*
|
|
6
|
+
* Checked against the pinned server (26.8.15.10): the table's columns are
|
|
7
|
+
* `name`, `collection` (a `Map(String, String)`), `source` (`SQL` or
|
|
8
|
+
* `Config`) and `create_query`. A user with `SHOW NAMED COLLECTIONS` and
|
|
9
|
+
* without `SHOW NAMED COLLECTIONS SECRETS` sees every key, with each value as
|
|
10
|
+
* `'[HIDDEN]'`, so keys could be compared without the secrets grant. A user
|
|
11
|
+
* without `SHOW NAMED COLLECTIONS` reads the table without an error and sees
|
|
12
|
+
* no rows; a collection that user cannot see is reported missing, with the
|
|
13
|
+
* grant named.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { clickhouseQuery, type ClickHouseEndpoint } from "../http";
|
|
18
|
+
import type { NamedCollectionCheck } from "../plan/diff";
|
|
19
|
+
|
|
20
|
+
const quote = (s: string) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
21
|
+
|
|
22
|
+
/** The named collections a build output references (its document's `namedCollections`). */
|
|
23
|
+
export function referencedCollections(buildJson: string): string[] {
|
|
24
|
+
const doc = JSON.parse(buildJson) as { namedCollections?: unknown };
|
|
25
|
+
return Array.isArray(doc.namedCollections) ? doc.namedCollections.filter((n): n is string => typeof n === "string") : [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** {@link referencedCollections} of a build output file. */
|
|
29
|
+
export const referencedCollectionsOf = (buildFile: string): string[] => referencedCollections(readFileSync(buildFile, "utf-8"));
|
|
30
|
+
|
|
31
|
+
/** Which of `names` the server has, by name. Undefined when nothing is referenced. */
|
|
32
|
+
export async function checkNamedCollections(endpoint: ClickHouseEndpoint, names: readonly string[]): Promise<NamedCollectionCheck | undefined> {
|
|
33
|
+
if (names.length === 0) return undefined;
|
|
34
|
+
const referenced = [...new Set(names)].sort();
|
|
35
|
+
try {
|
|
36
|
+
const rows = await clickhouseQuery<{ name: string }>(
|
|
37
|
+
endpoint,
|
|
38
|
+
`/* chant plan: named collections */ SELECT name FROM system.named_collections WHERE name IN (${referenced.map(quote).join(", ")})`,
|
|
39
|
+
);
|
|
40
|
+
const have = new Set(rows.map((r) => r.name));
|
|
41
|
+
return { referenced, missing: referenced.filter((n) => !have.has(n)) };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return { referenced, missing: referenced, unread: err instanceof Error ? err.message.split("\n")[0]! : String(err) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether the check fails the plan. */
|
|
48
|
+
export const namedCollectionsFail = (c: NamedCollectionCheck | undefined): boolean => c !== undefined && c.missing.length > 0;
|
|
49
|
+
|
|
50
|
+
/** The report's lines for a failed check; none when it passed. */
|
|
51
|
+
export function renderNamedCollections(c: NamedCollectionCheck | undefined): string[] {
|
|
52
|
+
if (!c || c.missing.length === 0) return [];
|
|
53
|
+
if (c.unread !== undefined) {
|
|
54
|
+
return ["", `Refused: the build references named collection(s) ${c.missing.join(", ")}, and the server's could not be read to check them: ${c.unread}`];
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
"",
|
|
58
|
+
`Refused: the build references ${c.missing.length === 1 ? "a named collection" : "named collections"} the server does not have: ${c.missing.join(", ")}. ` +
|
|
59
|
+
"Create each on the server (chant never declares one), or grant the plan's user SHOW NAMED COLLECTIONS if it is there and hidden from that user.",
|
|
60
|
+
];
|
|
61
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Import against a real ClickHouse server never leaves an object out without
|
|
3
|
-
* a word (#3911): a server holding a
|
|
4
|
-
*
|
|
3
|
+
* a word (#3911): a server holding a workload made with SQL, beside a
|
|
4
|
+
* table, fails the import, which names it; a named collection is the
|
|
5
|
+
* environment's (#4395), listed as a prerequisite and not refused; a quota and a
|
|
5
6
|
* settings profile are declared kinds, which an import reads only when a
|
|
6
7
|
* build declares them, so they are not refused; with `skipUndeclarable` the table is imported and
|
|
7
8
|
* they are warnings. What `users.xml` defines (the `default` quota and
|
|
@@ -43,7 +44,7 @@ afterAll(async () => {
|
|
|
43
44
|
|
|
44
45
|
const run = (extra: Record<string, unknown> = {}) => exportResources({ environment: "x", config: {}, env: { CLICKHOUSE_URL: server!.endpoint.url }, ...extra });
|
|
45
46
|
|
|
46
|
-
const NAMED = ["
|
|
47
|
+
const NAMED = ["workload all"];
|
|
47
48
|
|
|
48
49
|
describe.skipIf(!enabled)("importing a server with objects chant cannot declare", () => {
|
|
49
50
|
test("fails, naming every one of them", async () => {
|
|
@@ -61,5 +62,6 @@ describe.skipIf(!enabled)("importing a server with objects chant cannot declare"
|
|
|
61
62
|
expect(ir.resources.map((r) => r.properties.name)).toEqual(["analytics", "events"]);
|
|
62
63
|
const left = (ir.warnings ?? []).filter((w) => w.includes("is left out (--skip-undeclarable)"));
|
|
63
64
|
expect(left.map((w) => w.split(" is left out")[0]).sort()).toEqual(NAMED);
|
|
65
|
+
expect(ir.metadata?.prerequisites).toEqual([{ kind: "named collection", name: "nc_s3", referencedBy: [] }]);
|
|
64
66
|
}, 120_000);
|
|
65
67
|
});
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* (#3911), found so import can refuse them by name instead of leaving them
|
|
4
4
|
* out (`../../core/undeclarable.ts`).
|
|
5
5
|
*
|
|
6
|
-
* One `system.*` query per kind the coverage table marks refused
|
|
7
|
-
* (`../../coverage/clickhouse.json`), keyed by the row's `kind`. A
|
|
8
|
-
* row is flipped to declared is no longer asked about.
|
|
6
|
+
* One `system.*` query per kind the coverage table marks refused or
|
|
7
|
+
* referenced (`../../coverage/clickhouse.json`), keyed by the row's `kind`. A
|
|
8
|
+
* kind whose row is flipped to declared is no longer asked about. A
|
|
9
|
+
* referenced kind (a named collection, #4395) is read by name only, as a
|
|
10
|
+
* prerequisite the import lists, never as a refusal.
|
|
9
11
|
*
|
|
10
12
|
* What the server's configuration files define (`storage = 'users_xml'`, a
|
|
11
13
|
* named collection whose source is the config, an executable function) is
|
|
@@ -47,12 +49,26 @@ export const CLICKHOUSE_PROBES: Readonly<Record<string, Probe>> = {
|
|
|
47
49
|
* pass.
|
|
48
50
|
*/
|
|
49
51
|
export async function readUndeclarable(target: ClickHouseTarget): Promise<{ objects: UndeclarableObject[]; unread: string[] }> {
|
|
52
|
+
return readKinds(target, "refused");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Every object of a kind the coverage table marks referenced (#4395): the
|
|
57
|
+
* server's SQL-defined named collections, by name. Their values are never
|
|
58
|
+
* read. A reader without `SHOW NAMED COLLECTIONS` sees none, and the server
|
|
59
|
+
* says nothing about that, so an empty list is not proof there are none.
|
|
60
|
+
*/
|
|
61
|
+
export async function readReferenced(target: ClickHouseTarget): Promise<{ objects: UndeclarableObject[]; unread: string[] }> {
|
|
62
|
+
return readKinds(target, "referenced");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readKinds(target: ClickHouseTarget, status: "refused" | "referenced"): Promise<{ objects: UndeclarableObject[]; unread: string[] }> {
|
|
50
66
|
const scope = (column: string) =>
|
|
51
67
|
target.databases ? `${column} IN (${target.databases.map(quote).join(", ")})` : `${column} NOT IN (${[...SYSTEM_DATABASES].map(quote).join(", ")})`;
|
|
52
68
|
const objects: UndeclarableObject[] = [];
|
|
53
69
|
const unread: string[] = [];
|
|
54
70
|
for (const row of KIND_COVERAGE.clickhouse) {
|
|
55
|
-
if (row.status !==
|
|
71
|
+
if (row.status !== status) continue;
|
|
56
72
|
const probe = CLICKHOUSE_PROBES[row.kind];
|
|
57
73
|
if (!probe) throw new Error(`no system table query finds a ClickHouse ${row.kind} (src/clickhouse/live/undeclarable.ts)`);
|
|
58
74
|
try {
|