@intentius/chant-lexicon-sql 0.124.46 → 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
package/src/files/clickhouse.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { isAccessStatement, parseAccess, type AccessNode } from "../clickhouse/a
|
|
|
21
21
|
import type { Token } from "../clickhouse/tokens";
|
|
22
22
|
import { CLICKHOUSE_TAG_OF } from "../clickhouse/import/generator";
|
|
23
23
|
import { describeStatement, type ImportedObject } from "../clickhouse/import/ir";
|
|
24
|
-
import { fileUnparsed, firstLine, lineAt, quoteBare, splitTokens, tokensText, undeclarableStatement, type FileLine, type ReadOptions } from "./common";
|
|
24
|
+
import { fileUnparsed, firstLine, lineAt, quoteBare, referencedStatement, splitTokens, tokensText, undeclarableStatement, type FileLine, type ReadOptions } from "./common";
|
|
25
|
+
import { createdCollectionName } from "../clickhouse/named-collections";
|
|
25
26
|
import { templateSafe, templateStrings } from "./postgres";
|
|
26
27
|
|
|
27
28
|
type Tag = (strings: TemplateStringsArray, ...values: unknown[]) => Declarable;
|
|
@@ -95,6 +96,12 @@ export function readClickHouse(ddl: string, options: ReadOptions, problems: stri
|
|
|
95
96
|
for (const stmt of splitTokens(tokenizeText(ddl, 0), isTrivia)) {
|
|
96
97
|
let sql = tokensText(stmt);
|
|
97
98
|
const line = lineAt(ddl, stmt[0]!.start);
|
|
99
|
+
const referenced = options.prerequisites ? referencedStatement("clickhouse", sql) : undefined;
|
|
100
|
+
const collection = referenced ? createdCollectionName(sql) : undefined;
|
|
101
|
+
if (referenced && collection !== undefined) {
|
|
102
|
+
options.prerequisites!.push({ kind: referenced.kind, name: collection });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
98
105
|
const undeclarable = undeclarableStatement("clickhouse", sql);
|
|
99
106
|
if (undeclarable) {
|
|
100
107
|
(options.undeclarable ?? problems).push(undeclarable);
|
package/src/files/common.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Token } from "../core/tokens";
|
|
4
4
|
import type { UnknownClause } from "../core/cursor";
|
|
5
5
|
import type { SqlDialect } from "../dialects";
|
|
6
|
-
import { statementKind } from "../coverage";
|
|
6
|
+
import { statementKind, type KindRow } from "../coverage";
|
|
7
7
|
|
|
8
8
|
export interface ReadOptions {
|
|
9
9
|
/** Names the DDL in errors: a file's path, a command. */
|
|
@@ -19,6 +19,19 @@ export interface ReadOptions {
|
|
|
19
19
|
* --parser-option skipUndeclarable` lists them as warnings.
|
|
20
20
|
*/
|
|
21
21
|
undeclarable?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Where a statement of a referenced kind goes (#4395), as the name it
|
|
24
|
+
* creates: `chant import` lists a named collection's `CREATE` as an
|
|
25
|
+
* environment prerequisite. Without it, such a statement is a problem, as
|
|
26
|
+
* in a build's `.sql` file, which never declares one.
|
|
27
|
+
*/
|
|
28
|
+
prerequisites?: Array<{ kind: string; name: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The coverage row of a `CREATE` of a referenced kind (#4395), or undefined for any other statement. */
|
|
32
|
+
export function referencedStatement(dialect: SqlDialect, sql: string): KindRow | undefined {
|
|
33
|
+
const row = statementKind(dialect, sql);
|
|
34
|
+
return row?.status === "referenced" ? row : undefined;
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
/**
|
package/src/files/postgres.ts
CHANGED
|
@@ -138,7 +138,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
|
|
|
138
138
|
};
|
|
139
139
|
switch (node.statement) {
|
|
140
140
|
case "comment":
|
|
141
|
-
if (!["SCHEMA", "EXTENSION", "ROLE", "TRIGGER", "POLICY", "CONSTRAINT", "COLUMN", "CAST", "EVENT TRIGGER"].includes(node.objectType)) mark(node.target);
|
|
141
|
+
if (!["SCHEMA", "EXTENSION", "ROLE", "TRIGGER", "POLICY", "CONSTRAINT", "COLUMN", "CAST", "EVENT TRIGGER", "FOREIGN DATA WRAPPER", "SERVER", "PUBLICATION"].includes(node.objectType)) mark(node.target);
|
|
142
142
|
if (node.objectType === "COLUMN" && nameOf(tokens, node.target).length === 2 && options.schema) {
|
|
143
143
|
const first = tokens.slice(node.target.span.from, node.target.span.to).find((t) => !isTrivia(t));
|
|
144
144
|
if (first) inserts.add(tokens.indexOf(first));
|
|
@@ -177,7 +177,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
|
|
|
177
177
|
problems.push(`an index with no name (its declaration needs one): ${firstLine(tokensText(tokens))}`);
|
|
178
178
|
continue;
|
|
179
179
|
}
|
|
180
|
-
const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant", "cast", "eventTrigger"].includes(node.statement);
|
|
180
|
+
const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant", "cast", "eventTrigger", "foreignDataWrapper", "foreignServer", "publication"].includes(node.statement);
|
|
181
181
|
if (ownName && "name" in node) mark(node.name);
|
|
182
182
|
const object: PgFileObject = { node, statements: [rewrite(tokens, inserts, options.schema)], constraints: [], line, unparsed: fileUnparsed("unknown" in node ? node.unknown : undefined, tokens, tokensText(tokens), line) };
|
|
183
183
|
objects.push(object);
|
|
@@ -279,6 +279,10 @@ function ownerKey(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify:
|
|
|
279
279
|
return `${node.statement} ${nameOf(tokens, node.name).at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.table)))}`;
|
|
280
280
|
case "eventTrigger":
|
|
281
281
|
return `eventTrigger ${nameOf(tokens, node.name).at(-1) ?? ""}`;
|
|
282
|
+
case "foreignDataWrapper":
|
|
283
|
+
case "foreignServer":
|
|
284
|
+
case "publication":
|
|
285
|
+
return `${node.statement} ${nameOf(tokens, node.name).at(-1) ?? ""}`;
|
|
282
286
|
case "cast":
|
|
283
287
|
return `cast ${castKey(tokens, node.source, node.target)}`;
|
|
284
288
|
case "grant":
|
|
@@ -311,6 +315,12 @@ function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qua
|
|
|
311
315
|
return node.on ? `${node.objectType.toLowerCase()} ${target.at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
|
|
312
316
|
case "EVENT TRIGGER":
|
|
313
317
|
return `eventTrigger ${target.at(-1) ?? ""}`;
|
|
318
|
+
case "FOREIGN DATA WRAPPER":
|
|
319
|
+
return `foreignDataWrapper ${target.at(-1) ?? ""}`;
|
|
320
|
+
case "SERVER":
|
|
321
|
+
return `foreignServer ${target.at(-1) ?? ""}`;
|
|
322
|
+
case "PUBLICATION":
|
|
323
|
+
return `publication ${target.at(-1) ?? ""}`;
|
|
314
324
|
case "CAST":
|
|
315
325
|
return node.cast ? `cast ${castKey(tokens, node.cast.source, node.cast.target)}` : undefined;
|
|
316
326
|
case "CONSTRAINT":
|
|
@@ -2177,6 +2177,9 @@ export const AGGREGATE_COMBINATORS: readonly string[] = ["ArgMax","ArgMin","Arra
|
|
|
2177
2177
|
/** The table functions (`system.table_functions`): `numbers`, `remote`, `s3`. */
|
|
2178
2178
|
export const TABLE_FUNCTIONS: readonly string[] = ["SQLStandardValues","arrowFlight","arrowflight","azureBlobStorage","azureBlobStorageCluster","bigquery","cluster","clusterAllReplicas","cosn","deltaLake","deltaLakeAzure","deltaLakeAzureCluster","deltaLakeCluster","deltaLakeLocal","deltaLakeS3","deltaLakeS3Cluster","dictionary","eval","executable","file","fileCluster","filesystem","format","fuzzJSON","fuzzQuery","gcs","generateRandom","generateSeries","generate_series","hdfs","hdfsCluster","hive","hudi","hudiCluster","iceberg","icebergAzure","icebergAzureCluster","icebergCluster","icebergHDFS","icebergHDFSCluster","icebergLocal","icebergLocalCluster","icebergS3","icebergS3Cluster","input","jdbc","loop","merge","mergeTreeAnalyzeIndexes","mergeTreeAnalyzeIndexesUUID","mergeTreeCodecBlockCounts","mergeTreeIndex","mergeTreeProjection","mergeTreeTextIndex","mongodb","mysql","null","numbers","numbers_mt","odbc","oss","paimon","paimonAzure","paimonAzureCluster","paimonCluster","paimonHDFS","paimonHDFSCluster","paimonLocal","paimonS3","paimonS3Cluster","postgresql","primes","prometheusQuery","prometheusQueryRange","redis","remote","remoteSecure","s3","s3Cluster","sqlite","timeSeriesData","timeSeriesMetrics","timeSeriesSamples","timeSeriesSelector","timeSeriesTags","url","urlCluster","values","view","viewExplain","viewIfPermitted","ytsaurus","zeros","zeros_mt"];
|
|
2179
2179
|
|
|
2180
|
+
/** The input and output formats (`system.formats`): `Parquet`, `JSONEachRow`. */
|
|
2181
|
+
export const FORMATS: readonly string[] = ["Arrow","ArrowStream","Avro","AvroConfluent","BSONEachRow","Buffers","CSV","CSVWithNames","CSVWithNamesAndTypes","CapnProto","CustomSeparated","CustomSeparatedIgnoreSpaces","CustomSeparatedIgnoreSpacesWithNames","CustomSeparatedIgnoreSpacesWithNamesAndTypes","CustomSeparatedWithNames","CustomSeparatedWithNamesAndTypes","DWARF","Form","GeoJSON","Hash","HiveText","JSON","JSONAsObject","JSONAsString","JSONColumns","JSONColumnsWithMetadata","JSONCompact","JSONCompactColumns","JSONCompactEachRow","JSONCompactEachRowWithNames","JSONCompactEachRowWithNamesAndTypes","JSONCompactEachRowWithProgress","JSONCompactStrings","JSONCompactStringsEachRow","JSONCompactStringsEachRowWithNames","JSONCompactStringsEachRowWithNamesAndTypes","JSONCompactStringsEachRowWithProgress","JSONEachRow","JSONEachRowWithProgress","JSONL","JSONLines","JSONObjectEachRow","JSONStrings","JSONStringsEachRow","JSONStringsEachRowWithProgress","LineAsString","LineAsStringWithNames","LineAsStringWithNamesAndTypes","MD","Markdown","MsgPack","MySQLDump","MySQLWire","NDJSON","Native","Npy","Null","ODBCDriver2","ORC","One","PNG","Parquet","ParquetMetadata","PostgreSQLWire","Pretty","PrettyCompact","PrettyCompactMonoBlock","PrettyCompactNoEscapes","PrettyCompactNoEscapesMonoBlock","PrettyJSONEachRow","PrettyJSONLines","PrettyMonoBlock","PrettyNDJSON","PrettyNoEscapes","PrettyNoEscapesMonoBlock","PrettySpace","PrettySpaceMonoBlock","PrettySpaceNoEscapes","PrettySpaceNoEscapesMonoBlock","Prometheus","Protobuf","ProtobufList","ProtobufSingle","Puffin","PuffinMetadata","Raw","RawBLOB","RawWithNames","RawWithNamesAndTypes","Regexp","RowBinary","RowBinaryWithDefaults","RowBinaryWithNames","RowBinaryWithNamesAndTypes","RowBinaryWithNamesAndTypesAndDefaults","SQLInsert","TSKV","TSV","TSVRaw","TSVRawWithNames","TSVRawWithNamesAndTypes","TSVWithNames","TSVWithNamesAndTypes","TabSeparated","TabSeparatedRaw","TabSeparatedRawWithNames","TabSeparatedRawWithNamesAndTypes","TabSeparatedWithNames","TabSeparatedWithNamesAndTypes","Template","TemplateIgnoreSpaces","Values","Vertical","XML"];
|
|
2182
|
+
|
|
2180
2183
|
export const SKIP_INDEX_TYPES: Readonly<Record<SkipIndexType, SkipIndexSpec>> = {
|
|
2181
2184
|
bloom_filter: {"syntax":"INDEX name expr TYPE bloom_filter([false_positive_rate]) GRANULARITY g","args":[{"name":"false_positive_rate","position":0,"optional":true,"repeated":false,"named":false,"kind":"number","range":[0,1],"note":"Exclusive of 0 and 1; 0.025 when omitted."}],"summary":"A Bloom filter over the values of the index expression, for speeding up equality and IN conditions on columns, including Array and Map elements."},
|
|
2182
2185
|
hypothesis: {"syntax":"INDEX name expr TYPE hypothesis GRANULARITY g","args":[],"summary":"Deprecated and no longer supported. It is retained only so that tables which still reference it can be attached."},
|
|
@@ -129,6 +129,31 @@
|
|
|
129
129
|
"kind": "resource",
|
|
130
130
|
"lexicon": "sql"
|
|
131
131
|
},
|
|
132
|
+
"PostgresOperator": {
|
|
133
|
+
"resourceType": "Postgres::Operator",
|
|
134
|
+
"kind": "resource",
|
|
135
|
+
"lexicon": "sql"
|
|
136
|
+
},
|
|
137
|
+
"PostgresStatistics": {
|
|
138
|
+
"resourceType": "Postgres::Statistics",
|
|
139
|
+
"kind": "resource",
|
|
140
|
+
"lexicon": "sql"
|
|
141
|
+
},
|
|
142
|
+
"PostgresForeignDataWrapper": {
|
|
143
|
+
"resourceType": "Postgres::ForeignDataWrapper",
|
|
144
|
+
"kind": "resource",
|
|
145
|
+
"lexicon": "sql"
|
|
146
|
+
},
|
|
147
|
+
"PostgresForeignServer": {
|
|
148
|
+
"resourceType": "Postgres::ForeignServer",
|
|
149
|
+
"kind": "resource",
|
|
150
|
+
"lexicon": "sql"
|
|
151
|
+
},
|
|
152
|
+
"PostgresPublication": {
|
|
153
|
+
"resourceType": "Postgres::Publication",
|
|
154
|
+
"kind": "resource",
|
|
155
|
+
"lexicon": "sql"
|
|
156
|
+
},
|
|
132
157
|
"PostgresExtension": {
|
|
133
158
|
"resourceType": "Postgres::Extension",
|
|
134
159
|
"kind": "resource",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { resolveParserOptions } from "@intentius/chant/import/parser-options";
|
|
3
|
+
import { guessDialect, looksLikeTSql, sourceOption } from "./import-parser";
|
|
4
|
+
import { sqlPlugin } from "./plugin";
|
|
5
|
+
|
|
6
|
+
const TSQL = `SET ANSI_NULLS ON
|
|
7
|
+
GO
|
|
8
|
+
CREATE TABLE [dbo].[Orders] (
|
|
9
|
+
[OrderID] INT IDENTITY (1, 1) NOT NULL,
|
|
10
|
+
[Note] NVARCHAR (200) NULL,
|
|
11
|
+
CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED ([OrderID] ASC)
|
|
12
|
+
);
|
|
13
|
+
GO`;
|
|
14
|
+
|
|
15
|
+
const parse = (text: string, entries: string[] = []) => {
|
|
16
|
+
const resolved = resolveParserOptions(sqlPlugin, entries);
|
|
17
|
+
if ("error" in resolved) throw new Error(resolved.error);
|
|
18
|
+
return sqlPlugin.templateParser!(resolved.options).parse(text);
|
|
19
|
+
};
|
|
20
|
+
const types = (ir: { resources: Array<{ type: string }> }) => [...new Set(ir.resources.map((r) => r.type))];
|
|
21
|
+
|
|
22
|
+
describe("a T-SQL file is not read as Postgres", () => {
|
|
23
|
+
test("the dialect guess alone would have said Postgres, which is why the markers exist", () => {
|
|
24
|
+
expect(guessDialect(TSQL)).toBe("postgres");
|
|
25
|
+
expect(looksLikeTSql(TSQL)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("with no option, a T-SQL file is read as SQL Server and gives ClickHouse objects and diagnostics", () => {
|
|
29
|
+
const ir = parse(TSQL);
|
|
30
|
+
expect(types(ir)).toEqual(["ClickHouse::Table"]);
|
|
31
|
+
expect(ir.diagnostics?.length).toBeGreaterThan(0);
|
|
32
|
+
expect(ir.metadata).toMatchObject({ source: "sqlserver" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test.each([
|
|
36
|
+
["a GO line", "CREATE TABLE a (x int)\nGO\n"],
|
|
37
|
+
["CLUSTERED", "CREATE CLUSTERED INDEX ix ON a (x)"],
|
|
38
|
+
["COLUMNSTORE", "CREATE CLUSTERED COLUMNSTORE INDEX cci ON a"],
|
|
39
|
+
["a bracketed name", "CREATE TABLE [a] (x int)"],
|
|
40
|
+
["nvarchar", "CREATE TABLE a (x nvarchar(5))"],
|
|
41
|
+
["IDENTITY(1,1)", "CREATE TABLE a (x int IDENTITY(1,1))"],
|
|
42
|
+
["an SSMS preamble", "SET QUOTED_IDENTIFIER ON\nCREATE TABLE a (x int)"],
|
|
43
|
+
["sp_addextendedproperty", "EXEC sp_addextendedproperty @name = N'a'"],
|
|
44
|
+
])("%s marks a file as T-SQL", (_, text) => {
|
|
45
|
+
expect(looksLikeTSql(text)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test.each([
|
|
49
|
+
["Postgres with arrays and a quoted name", 'CREATE TABLE "a" (x int[], y text[3], z int GENERATED ALWAYS AS IDENTITY, PRIMARY KEY (x))'],
|
|
50
|
+
["Postgres with a subscript", "CREATE FUNCTION f(a int[]) RETURNS int AS $$ SELECT a[1] $$ LANGUAGE sql"],
|
|
51
|
+
["ClickHouse", "CREATE TABLE a (x UInt8) ENGINE = MergeTree ORDER BY x"],
|
|
52
|
+
["ClickHouse naming a clustered column", "CREATE TABLE clustered (x UInt8) ENGINE = Memory"],
|
|
53
|
+
["a plain index", "CREATE INDEX ix ON a (x)"],
|
|
54
|
+
])("%s is not", (_, text) => {
|
|
55
|
+
expect(looksLikeTSql(text)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("Postgres and ClickHouse files import as before", () => {
|
|
59
|
+
expect(types(parse("CREATE TABLE a (x int PRIMARY KEY, y text)"))).toEqual(["Postgres::Table"]);
|
|
60
|
+
expect(types(parse("CREATE TABLE a (x UInt8) ENGINE = MergeTree ORDER BY x"))).toEqual(["ClickHouse::Table"]);
|
|
61
|
+
expect(parse("CREATE TABLE a (x int PRIMARY KEY)").diagnostics).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a dialect named by the author is kept: dialect=postgres on T-SQL fails as Postgres, it is not rerouted", () => {
|
|
65
|
+
expect(() => parse(TSQL, ["dialect=postgres"])).toThrow(/Postgres DDL/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("--parser-option from=sqlserver", () => {
|
|
70
|
+
test("is declared with the other options", () => {
|
|
71
|
+
expect(sqlPlugin.parserOptions!().map((o) => o.name)).toEqual(["dialect", "from", "skipUndeclarable"]);
|
|
72
|
+
expect(resolveParserOptions(sqlPlugin, ["from=sqlserver"])).toEqual({ options: { from: "sqlserver" } });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("forces the T-SQL source on a file without markers", () => {
|
|
76
|
+
const ir = parse("CREATE TABLE a (x int NOT NULL)", ["from=sqlserver"]);
|
|
77
|
+
expect(types(ir)).toEqual(["ClickHouse::Table"]);
|
|
78
|
+
expect(ir.diagnostics!.map((d) => d.ruleId)).toEqual(["SQLRV001", "SQLRV002"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a source other than sqlserver is refused, naming what is accepted", () => {
|
|
82
|
+
expect(() => sourceOption({ from: "mysql" })).toThrow("expected sqlserver, got mysql");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("cannot be combined with dialect", () => {
|
|
86
|
+
expect(() => sourceOption({ from: "sqlserver", dialect: "clickhouse" })).toThrow(/cannot be combined with dialect/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a lexing error is reported as an import error naming the position", () => {
|
|
90
|
+
expect(() => parse("CREATE TABLE [a (x int)", ["from=sqlserver"])).toThrow(/unterminated \[identifier\] \(line 1, column 14\)/);
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/import-parser.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `chant import schema.sql`: a file of DDL into the import IR, in either
|
|
3
|
-
* dialect
|
|
3
|
+
* dialect, or a SQL Server (T-SQL) schema into ClickHouse declarations.
|
|
4
|
+
*
|
|
5
|
+
* `--parser-option from=sqlserver` (#4398) reads the file as T-SQL and
|
|
6
|
+
* translates it to ClickHouse (`./sqlserver/translate.ts`). SQL Server is an
|
|
7
|
+
* import source, not a dialect: nothing is declared in it, and a build never
|
|
8
|
+
* holds one. A file with T-SQL's own markers (`[bracketed]` names, a `GO`
|
|
9
|
+
* line, `CLUSTERED` or `COLUMNSTORE`, `nvarchar`, `IDENTITY(1,1)`) is read
|
|
10
|
+
* the same way without the option, so it is not taken for Postgres.
|
|
11
|
+
* `from=sqlserver` with `dialect=...` is refused: the target is ClickHouse.
|
|
12
|
+
*
|
|
13
|
+
* `--parser-option dialect=postgres` (or `clickhouse`) names the dialect;
|
|
4
14
|
* without one it is read off the statements: an `ENGINE =` clause, a
|
|
5
15
|
* `CREATE DATABASE`, a `CREATE DICTIONARY` or a lambda `CREATE FUNCTION f AS
|
|
6
16
|
* (x) -> ...` is ClickHouse, a statement
|
|
@@ -22,6 +32,9 @@ import { readPostgres } from "./files/postgres";
|
|
|
22
32
|
import { SqlFileError } from "./files/common";
|
|
23
33
|
import { objectsToIR as chObjectsToIR } from "./clickhouse/import/ir";
|
|
24
34
|
import { objectsToIR as pgObjectsToIR } from "./postgres/import/ir";
|
|
35
|
+
import { namedCollectionPrerequisites } from "./clickhouse/named-collections";
|
|
36
|
+
import { translateSqlServer } from "./sqlserver/translate";
|
|
37
|
+
import { TsqlLexError } from "./sqlserver/lexer";
|
|
25
38
|
|
|
26
39
|
const CLICKHOUSE_ONLY = [
|
|
27
40
|
/\bENGINE\s*=/i,
|
|
@@ -37,6 +50,31 @@ const POSTGRES_ONLY = [
|
|
|
37
50
|
/^\s*CREATE\s+TABLE\b/im,
|
|
38
51
|
];
|
|
39
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Markers only T-SQL has. Each is rare enough in Postgres and ClickHouse DDL
|
|
55
|
+
* that one is enough: a bracketed name after CREATE TABLE or ON, a `GO`
|
|
56
|
+
* batch line, `CLUSTERED`/`COLUMNSTORE`, T-SQL type names, `IDENTITY(n, n)`,
|
|
57
|
+
* the `SET ANSI_NULLS` preamble of an SSMS script.
|
|
58
|
+
*/
|
|
59
|
+
const TSQL_MARKERS = [
|
|
60
|
+
/^\s*GO\s*$/im,
|
|
61
|
+
/\bCREATE\s+(?:UNIQUE\s+)?(?:NONCLUSTERED\s+|CLUSTERED\s+)(?:COLUMNSTORE\s+)?INDEX\b/i,
|
|
62
|
+
/\bCREATE\s+(?:NONCLUSTERED\s+|CLUSTERED\s+)?COLUMNSTORE\s+INDEX\b/i,
|
|
63
|
+
/\bPRIMARY\s+KEY\s+(?:NON)?CLUSTERED\b/i,
|
|
64
|
+
/\b(?:CREATE\s+TABLE|ALTER\s+TABLE|\bON|REFERENCES)\s+\[[^\]\n]+\]/i,
|
|
65
|
+
/\b(?:NVARCHAR|NCHAR|UNIQUEIDENTIFIER|DATETIME2|DATETIMEOFFSET|SMALLDATETIME|SMALLMONEY|ROWVERSION)\b/i,
|
|
66
|
+
/\bIDENTITY\s*\(\s*\d+\s*,\s*\d+\s*\)/i,
|
|
67
|
+
/^\s*SET\s+(?:ANSI_NULLS|QUOTED_IDENTIFIER|NOCOUNT|ANSI_PADDING)\b/im,
|
|
68
|
+
/\bEXEC(?:UTE)?\s+sp_addextendedproperty\b/i,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** True when the DDL has a marker only T-SQL has and no ClickHouse-only clause. */
|
|
72
|
+
export function looksLikeTSql(content: string): boolean {
|
|
73
|
+
const text = content.replace(/--[^\n]*/g, "");
|
|
74
|
+
if (CLICKHOUSE_ONLY.some((re) => re.test(text))) return false;
|
|
75
|
+
return TSQL_MARKERS.some((re) => re.test(text));
|
|
76
|
+
}
|
|
77
|
+
|
|
40
78
|
/** The dialect a file of DDL is written in, read off its statements. */
|
|
41
79
|
export function guessDialect(content: string): SqlDialect {
|
|
42
80
|
const text = content.replace(/--[^\n]*/g, "");
|
|
@@ -53,20 +91,56 @@ export function dialectOption(options: ParserOptions | undefined): SqlDialect |
|
|
|
53
91
|
return d as SqlDialect;
|
|
54
92
|
}
|
|
55
93
|
|
|
94
|
+
/** Sources other than a dialect that `--parser-option from=` names. */
|
|
95
|
+
export const IMPORT_SOURCES = ["sqlserver"] as const;
|
|
96
|
+
export type ImportSource = (typeof IMPORT_SOURCES)[number];
|
|
97
|
+
|
|
98
|
+
/** The `from` parser option, checked. */
|
|
99
|
+
export function sourceOption(options: ParserOptions | undefined): ImportSource | undefined {
|
|
100
|
+
const f = options?.from;
|
|
101
|
+
if (f === undefined) return undefined;
|
|
102
|
+
if (typeof f !== "string" || !(IMPORT_SOURCES as readonly string[]).includes(f)) throw new Error(`--parser-option from: expected ${IMPORT_SOURCES.join(" or ")}, got ${String(f)}`);
|
|
103
|
+
if (options?.dialect !== undefined) throw new Error(`--parser-option from=${f} cannot be combined with dialect: a ${f} schema is translated to ClickHouse`);
|
|
104
|
+
return f as ImportSource;
|
|
105
|
+
}
|
|
106
|
+
|
|
56
107
|
export class SqlFileParser implements TemplateParser {
|
|
57
108
|
constructor(
|
|
58
109
|
private readonly dialect?: SqlDialect,
|
|
59
|
-
private readonly options: { skipUndeclarable?: boolean } = {},
|
|
110
|
+
private readonly options: { skipUndeclarable?: boolean; from?: ImportSource } = {},
|
|
60
111
|
) {}
|
|
61
112
|
|
|
62
113
|
parse(content: string): TemplateIR {
|
|
114
|
+
if (this.options.from === "sqlserver" || (this.dialect === undefined && looksLikeTSql(content))) return this.parseSqlServer(content);
|
|
63
115
|
const dialect = this.dialect ?? guessDialect(content);
|
|
64
116
|
const problems: string[] = [];
|
|
65
117
|
const skipped: string[] = [];
|
|
66
|
-
const
|
|
118
|
+
const prerequisites: Array<{ kind: string; name: string }> = [];
|
|
119
|
+
const read = { origin: "the DDL", prerequisites, ...(this.options.skipUndeclarable ? { undeclarable: skipped } : {}) };
|
|
67
120
|
const ir = dialect === "postgres" ? pgObjectsToIR(readPostgres(content, read, problems)) : chObjectsToIR(readClickHouse(content, read, problems));
|
|
68
121
|
if (problems.length > 0) throw new SqlFileError(`the ${dialect === "postgres" ? "Postgres" : "ClickHouse"} DDL`, problems);
|
|
69
|
-
|
|
70
|
-
|
|
122
|
+
// A named collection is the environment's (#4395): its CREATE, or a declaration naming one, is a prerequisite, its values never imported.
|
|
123
|
+
const objects = ir.resources.map((r) => r.properties as { database?: string; name: string; ddl: string });
|
|
124
|
+
const prereq =
|
|
125
|
+
dialect === "clickhouse"
|
|
126
|
+
? namedCollectionPrerequisites(prerequisites, objects, { found: "the DDL creates it", unseen: "the DDL does not create it" })
|
|
127
|
+
: { prerequisites: [], warnings: [] };
|
|
128
|
+
const warnings = [...(ir.warnings ?? []), ...prereq.warnings, ...skipped.map((s) => `left out (skipUndeclarable): ${s}`)];
|
|
129
|
+
const out = prereq.prerequisites.length > 0 ? { ...ir, metadata: { ...ir.metadata, prerequisites: prereq.prerequisites } } : ir;
|
|
130
|
+
return warnings.length > 0 ? { ...out, warnings } : out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A T-SQL schema as ClickHouse objects, with a finding for each judgment call (#4398). */
|
|
134
|
+
private parseSqlServer(content: string): TemplateIR {
|
|
135
|
+
let t;
|
|
136
|
+
try {
|
|
137
|
+
t = translateSqlServer(content, { skipUnreadable: this.options.skipUndeclarable === true });
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e instanceof TsqlLexError) throw new SqlFileError("the SQL Server DDL", [e.message]);
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
if (t.problems.length > 0) throw new SqlFileError("the SQL Server DDL", t.problems);
|
|
143
|
+
const ir = chObjectsToIR(t.objects);
|
|
144
|
+
return { ...ir, diagnostics: t.diagnostics, metadata: { ...ir.metadata, source: "sqlserver" } };
|
|
71
145
|
}
|
|
72
146
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { database, table, view, dictionary, user, role, policy, quota, settingsP
|
|
|
18
18
|
// A tag folds as the function its file imports, so `table` imported from
|
|
19
19
|
// `@intentius/chant-lexicon-sql/postgres` is Postgres's. Declarations import
|
|
20
20
|
// them from the dialect subpath.
|
|
21
|
-
export { schema, index, sequence, type, domain, extension, func, procedure, aggregate, trigger, eventTrigger, collation, cast } from "./postgres/entities";
|
|
21
|
+
export { schema, index, sequence, type, domain, extension, func, procedure, aggregate, trigger, eventTrigger, operator, statistics, foreignDataWrapper, foreignServer, publication, collation, cast } from "./postgres/entities";
|
|
22
22
|
|
|
23
23
|
// A schema change's statements between two builds, rendered offline for a
|
|
24
24
|
// migration file (#3644); also at `@intentius/chant-lexicon-sql/migration-statements`.
|
|
@@ -33,4 +33,5 @@ export const sqlPostSynthAuditEntries: Record<string, RuleMeta> = {
|
|
|
33
33
|
SQLCH125: auditRule("SQLCH125", "merge-worthy", "guidance", "A codec parameter is outside what the codec takes", "Give the codec a parameter within its range or set, or drop the parameter for the default.", { category: "correctness" }),
|
|
34
34
|
SQLCH126: auditRule("SQLCH126", "merge-worthy", "guidance", "A GRANT column list names a column the table does not declare", "Fix the column name, or declare the column on the table.", { category: "correctness" }),
|
|
35
35
|
SQLCH127: auditRule("SQLCH127", "merge-worthy", "guidance", "An expression calls a function the pinned server does not have", "Fix the function name, use one from the pinned server's system.functions, or declare it with func.", { category: "correctness" }),
|
|
36
|
+
SQLCH128: auditRule("SQLCH128", "merge-worthy", "guidance", "An integration engine or table function argument holds a literal credential", "Create a named collection with the credentials on the server and name it in place of the arguments: ENGINE = S3(lake_creds, format = 'Parquet').", { category: "security" }),
|
|
36
37
|
};
|
|
@@ -29,6 +29,7 @@ import { sqlch124 } from "./sqlch124";
|
|
|
29
29
|
import { sqlch125 } from "./sqlch125";
|
|
30
30
|
import { sqlch126 } from "./sqlch126";
|
|
31
31
|
import { sqlch127 } from "./sqlch127";
|
|
32
|
+
import { sqlch128 } from "./sqlch128";
|
|
32
33
|
import { sqlpg101 } from "./sqlpg101";
|
|
33
34
|
import { sqlpg102 } from "./sqlpg102";
|
|
34
35
|
import { sqlpg103 } from "./sqlpg103";
|
|
@@ -86,6 +87,7 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
86
87
|
sqlch125,
|
|
87
88
|
sqlch126,
|
|
88
89
|
sqlch127,
|
|
90
|
+
sqlch128,
|
|
89
91
|
sqlpg101,
|
|
90
92
|
sqlpg102,
|
|
91
93
|
sqlpg103,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLCH128 (#4395): a literal credential in an integration engine's or table
|
|
3
|
+
* function's arguments. Each flagged case builds today and writes its secret
|
|
4
|
+
* into the build output and `clickhouse.sql`; each clean case keeps it out.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { makePostSynthCtx } from "@intentius/chant-test-utils";
|
|
8
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
9
|
+
import { sqlSerializer } from "../../serializer";
|
|
10
|
+
import { database, dictionary, table, view } from "../../clickhouse/entities";
|
|
11
|
+
import { sqlAuditCatalog } from "../audit-catalog";
|
|
12
|
+
import { postSynthChecks } from "./index";
|
|
13
|
+
|
|
14
|
+
function run(entities: Record<string, unknown>) {
|
|
15
|
+
const check = postSynthChecks.find((c) => c.id === "SQLCH128")!;
|
|
16
|
+
const out = sqlSerializer.serialize(new Map(Object.entries(entities)) as never) as SerializerResult;
|
|
17
|
+
return check.check(makePostSynthCtx("sql", out.primary));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The findings' messages, in the build's apply order (by export name here). */
|
|
21
|
+
const messages = (entities: Record<string, unknown>) => {
|
|
22
|
+
const diags = run(entities);
|
|
23
|
+
for (const d of diags) expect(d).toMatchObject({ checkId: "SQLCH128", lexicon: "sql", severity: "error" });
|
|
24
|
+
return diags.map((d) => d.message);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test("SQLCH128 is registered with an audit entry", () => {
|
|
28
|
+
expect(postSynthChecks.map((c) => c.id)).toContain("SQLCH128");
|
|
29
|
+
expect(sqlAuditCatalog.SQLCH128).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("an inline secret fails", () => {
|
|
33
|
+
test("S3: the secret access key", () => {
|
|
34
|
+
const t = table`CREATE TABLE lake (a Int32) ENGINE = S3('https://bucket.s3.amazonaws.com/x.parquet', 'AKIAEXAMPLE', 'wJalrXUtnFEMI', 'Parquet')`;
|
|
35
|
+
expect(messages({ t })).toEqual([
|
|
36
|
+
expect.stringMatching(
|
|
37
|
+
/^t \(lake\): ENGINE = S3 argument secret_access_key holds a literal credential, which is the environment's\. Create a named collection .*ENGINE = S3\(lake_creds, format = 'Parquet'\)/,
|
|
38
|
+
),
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("Kafka: kafka_sasl_password, a setting", () => {
|
|
43
|
+
const t = table`CREATE TABLE k (a String) ENGINE = Kafka() SETTINGS kafka_broker_list = 'kafka:9092', kafka_topic_list = 'events', kafka_group_name = 'g', kafka_format = 'JSONEachRow', kafka_sasl_username = 'app', kafka_sasl_password = 'hunter2'`;
|
|
44
|
+
expect(messages({ t })).toEqual([expect.stringMatching(/^t \(k\): Kafka setting kafka_sasl_password holds a literal credential/)]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("PostgreSQL and MySQL: the password", () => {
|
|
48
|
+
const pg = table`CREATE TABLE pg (id Int32) ENGINE = PostgreSQL('db:5432', 'shop', 'orders', 'app', 'hunter2')`;
|
|
49
|
+
const my = table`CREATE TABLE my (id Int32) ENGINE = MySQL('db:3306', 'shop', 'orders', 'app', 'hunter2')`;
|
|
50
|
+
expect(messages({ pg, my })).toEqual([
|
|
51
|
+
expect.stringMatching(/^my \(my\): ENGINE = MySQL argument password holds a literal credential/),
|
|
52
|
+
expect.stringMatching(/^pg \(pg\): ENGINE = PostgreSQL argument password holds a literal credential/),
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a database engine, a key = value override after a collection, a URL with a password", () => {
|
|
57
|
+
const db = database`CREATE DATABASE mirror ENGINE = MySQL('db:3306', 'shop', 'app', 'hunter2')`;
|
|
58
|
+
const s3 = table`CREATE TABLE s (a Int32) ENGINE = S3(lake_creds, secret_access_key = 'override', format = 'Parquet')`;
|
|
59
|
+
const u = table`CREATE TABLE u (a Int32) ENGINE = URL('https://app:hunter2@example.com/data.csv', 'CSV')`;
|
|
60
|
+
expect(messages({ db, s3, u })).toEqual([
|
|
61
|
+
expect.stringMatching(/^db \(mirror\): ENGINE = MySQL argument password holds a literal credential/),
|
|
62
|
+
expect.stringMatching(/^s3 \(s\): ENGINE = S3 argument secret_access_key holds a literal credential/),
|
|
63
|
+
expect.stringMatching(/^u \(u\): ENGINE = URL argument 1 holds a URL with a password in it/),
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a table function, in a table over it and in a view's SELECT", () => {
|
|
68
|
+
const over = table`CREATE TABLE over AS s3('https://bucket/x.csv', 'AKIAEXAMPLE', 'wJalrXUtnFEMI', 'CSV')`;
|
|
69
|
+
const v = view`CREATE VIEW v AS SELECT * FROM mysql('db:3306', 'shop', 'orders', 'app', 'hunter2')`;
|
|
70
|
+
const r = view`CREATE VIEW r AS SELECT * FROM remote('h:9000', 'shop', 'orders', 'app', 'hunter2')`;
|
|
71
|
+
expect(messages({ over, v, r })).toEqual([
|
|
72
|
+
expect.stringMatching(/^over \(over\): s3\(\) argument secret_access_key holds a literal credential/),
|
|
73
|
+
expect.stringMatching(/^r \(r\): remote\(\) argument password holds a literal credential/),
|
|
74
|
+
expect.stringMatching(/^v \(v\): mysql\(\) argument password holds a literal credential/),
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a dictionary source's PASSWORD", () => {
|
|
79
|
+
const d = dictionary`CREATE DICTIONARY d (id UInt64, name String) PRIMARY KEY id SOURCE(MYSQL(HOST 'db' PORT 3306 USER 'app' PASSWORD 'hunter2' DB 'shop' TABLE 'names')) LAYOUT(HASHED()) LIFETIME(300)`;
|
|
80
|
+
expect(messages({ d })).toEqual([expect.stringMatching(/^d \(d\): SOURCE PASSWORD holds a literal credential/)]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("the server's '[HIDDEN]' mask, as an import of SHOW CREATE writes it, is named as such", () => {
|
|
84
|
+
const t = table`CREATE TABLE lake (a Int32) ENGINE = S3('https://bucket/x.parquet', 'AKIAEXAMPLE', '[HIDDEN]')`;
|
|
85
|
+
expect(messages({ t })).toEqual([expect.stringMatching(/secret_access_key holds '\[HIDDEN\]', the server's mask for a credential, in place of one/)]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("no secret, no finding", () => {
|
|
90
|
+
test("a named collection in place of the arguments", () => {
|
|
91
|
+
const s3 = table`CREATE TABLE lake (a Int32) ENGINE = S3(lake_creds, url = 'https://bucket/x.parquet', format = 'Parquet')`;
|
|
92
|
+
const k = table`CREATE TABLE k (a String) ENGINE = Kafka(kafka_creds, kafka_format = 'JSONEachRow')`;
|
|
93
|
+
const pg = table`CREATE TABLE pg (id Int32) ENGINE = PostgreSQL(pg_creds, table = 'orders')`;
|
|
94
|
+
const v = view`CREATE VIEW v AS SELECT * FROM mysql(mysql_creds, table = 'orders')`;
|
|
95
|
+
const d = dictionary`CREATE DICTIONARY d (id UInt64, name String) PRIMARY KEY id SOURCE(MYSQL(NAME mysql_creds)) LAYOUT(HASHED()) LIFETIME(300)`;
|
|
96
|
+
expect(messages({ s3, k, pg, v, d })).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("an S3 table with a format and a compression where the keys would be, NOSIGN, or an empty password", () => {
|
|
100
|
+
const fmt = table`CREATE TABLE f (a Int32) ENGINE = S3('https://bucket/x.csv.gz', 'CSV', 'gzip')`;
|
|
101
|
+
const nosign = table`CREATE TABLE n (a Int32) ENGINE = S3('https://bucket/x.csv', NOSIGN, 'CSV')`;
|
|
102
|
+
const az = table`CREATE TABLE az (a Int32) ENGINE = AzureBlobStorage('https://acct.blob.core.windows.net', 'c', 'p.csv', 'CSV', 'gzip')`;
|
|
103
|
+
const d = dictionary`CREATE DICTIONARY d (id UInt64, name String) PRIMARY KEY id SOURCE(CLICKHOUSE(TABLE 'names' USER 'default' PASSWORD '')) LAYOUT(HASHED()) LIFETIME(300)`;
|
|
104
|
+
const mt = table`CREATE TABLE m (password String) ENGINE = MergeTree ORDER BY tuple()`;
|
|
105
|
+
expect(messages({ fmt, nosign, az, d, mt })).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
});
|