@intentius/chant-lexicon-sql 0.124.31 → 0.124.33
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.js +250 -0
- package/dist/clickhouse/access.js +722 -0
- package/dist/clickhouse/apply/apply.js +243 -0
- package/dist/clickhouse/apply/mutations.js +66 -0
- package/dist/clickhouse/apply/statements.js +545 -0
- package/dist/clickhouse/catalog-types.js +7 -0
- package/dist/clickhouse/container.js +152 -0
- package/dist/clickhouse/engine-syntax.js +159 -0
- package/dist/clickhouse/entities.js +593 -0
- package/dist/clickhouse/expressions.js +107 -0
- package/dist/clickhouse/http.js +55 -0
- package/dist/clickhouse/import/generator.js +192 -0
- package/dist/clickhouse/import/inlined.js +207 -0
- package/dist/clickhouse/import/ir.js +160 -0
- package/dist/clickhouse/import/live-export.js +131 -0
- package/dist/clickhouse/import/parser.js +17 -0
- package/dist/clickhouse/live/bind.js +116 -0
- package/dist/clickhouse/live/catalog.js +223 -0
- package/dist/clickhouse/live/describe-resources.js +91 -0
- package/dist/clickhouse/live/undeclarable.js +59 -0
- package/dist/clickhouse/overlays/codecs.js +57 -0
- package/dist/clickhouse/overlays/engines.js +101 -0
- package/dist/clickhouse/overlays/kinds.js +1 -0
- package/dist/clickhouse/overlays/projections.js +26 -0
- package/dist/clickhouse/overlays/skip-indexes.js +40 -0
- package/dist/clickhouse/overlays/ttl.js +34 -0
- package/dist/clickhouse/overlays/types.js +59 -0
- package/dist/clickhouse/ownership.js +83 -0
- package/dist/clickhouse/parser.js +674 -0
- package/dist/clickhouse/plan/commands.js +159 -0
- package/dist/clickhouse/plan/deep.js +345 -0
- package/dist/clickhouse/plan/diff.js +312 -0
- package/dist/clickhouse/plan/disruption.js +73 -0
- package/dist/clickhouse/plan/normalize.js +545 -0
- package/dist/clickhouse/plan/rebuild-handoff.js +57 -0
- package/dist/clickhouse/plan/report.js +26 -0
- package/dist/clickhouse/plan/rules.js +72 -0
- package/dist/clickhouse/plan/schema.js +67 -0
- package/dist/clickhouse/plan/server-format.js +35 -0
- package/dist/clickhouse/rebuild/backfill.js +265 -0
- package/dist/clickhouse/rebuild/engines.js +146 -0
- package/dist/clickhouse/rebuild/observe.js +260 -0
- package/dist/clickhouse/rebuild/op.js +152 -0
- package/dist/clickhouse/rebuild/partitions.js +65 -0
- package/dist/clickhouse/rebuild/receipts.js +120 -0
- package/dist/clickhouse/rebuild/replicas.js +121 -0
- package/dist/clickhouse/rebuild/shards.js +76 -0
- package/dist/clickhouse/rebuild/steps.js +383 -0
- package/dist/clickhouse/rebuild/verify.js +279 -0
- package/dist/clickhouse/row-types.js +151 -0
- package/dist/clickhouse/testing/fake-server.js +47 -0
- package/dist/clickhouse/testing/roundtrip-schema.js +25 -0
- package/dist/clickhouse/testing/writable-server.js +123 -0
- package/dist/clickhouse/tokens.js +23 -0
- package/dist/clickhouse/topology.js +463 -0
- package/dist/clickhouse/type-syntax.js +73 -0
- package/dist/clickhouse.js +25 -0
- package/dist/codegen/classifier-page.js +93 -0
- package/dist/codegen/clickhouse-module.js +249 -0
- package/dist/codegen/coverage-page.js +70 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +222 -0
- package/dist/codegen/generate-cli.js +13 -0
- package/dist/codegen/generate.js +97 -0
- package/dist/codegen/package.js +28 -0
- package/dist/codegen/postgres-classifier-page.js +95 -0
- package/dist/codegen/postgres-module.js +418 -0
- package/dist/codegen/registry.js +52 -0
- package/dist/composites/audit-log-table.js +35 -0
- package/dist/composites/catalog.js +524 -0
- package/dist/composites/cdc-mirror.js +32 -0
- package/dist/composites/clickhouse.js +6 -0
- package/dist/composites/events-table.js +26 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/join-table.js +25 -0
- package/dist/composites/postgres.js +6 -0
- package/dist/composites/refreshed-view.js +24 -0
- package/dist/composites/replacing-table.js +28 -0
- package/dist/composites/rollup-view.js +29 -0
- package/dist/composites/sharded-table.js +27 -0
- package/dist/composites/soft-delete-table.js +34 -0
- package/dist/composites/tenant-table.js +26 -0
- package/dist/composites/typed.js +8 -0
- package/dist/config.js +206 -0
- package/dist/core/apply.js +93 -0
- package/dist/core/classifier.js +115 -0
- package/dist/core/container.js +69 -0
- package/dist/core/cursor.js +261 -0
- package/dist/core/diff.js +48 -0
- package/dist/core/entity.js +58 -0
- package/dist/core/find-templates.js +118 -0
- package/dist/core/foreign-tables.js +27 -0
- package/dist/core/handoff.js +32 -0
- package/dist/core/index.js +64 -0
- package/dist/core/interpolation.js +191 -0
- package/dist/core/normalize.js +43 -0
- package/dist/core/ownership.js +119 -0
- package/dist/core/receipts.js +22 -0
- package/dist/core/references.js +61 -0
- package/dist/core/statements.js +42 -0
- package/dist/core/template.js +79 -0
- package/dist/core/testing/http-stub.js +21 -0
- package/dist/core/tokens.js +202 -0
- package/dist/core/typed.js +40 -0
- package/dist/core/undeclarable.js +46 -0
- package/dist/coverage.js +161 -0
- package/dist/detect.js +32 -0
- package/dist/dialects.js +18 -0
- package/dist/files/build-root.js +72 -0
- package/dist/files/clickhouse.js +138 -0
- package/dist/files/common.js +58 -0
- package/dist/files/postgres.js +389 -0
- package/dist/generated/clickhouse-types.js +5 -0
- package/dist/generated/clickhouse.js +4361 -0
- package/dist/generated/postgres-types.js +227 -0
- package/dist/generated/postgres.js +3667 -0
- package/dist/import-generator.js +14 -0
- package/dist/import-parser.js +71 -0
- package/dist/index.js +24 -0
- package/dist/init-templates.js +290 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +31 -0
- package/dist/lint/audit-lineage.js +22 -0
- package/dist/lint/post-synth/audit-entries.js +34 -0
- package/dist/lint/post-synth/clickhouse-helpers.js +79 -0
- package/dist/lint/post-synth/clickhouse-type-check.js +162 -0
- package/dist/lint/post-synth/index.js +112 -0
- package/dist/lint/post-synth/postgres-audit-entries.js +35 -0
- package/dist/lint/post-synth/postgres-helpers.js +65 -0
- package/dist/lint/post-synth/postgres-names.js +507 -0
- package/dist/lint/post-synth/sql-helpers.js +23 -0
- package/dist/lint/post-synth/sql101.js +59 -0
- package/dist/lint/post-synth/sql102.js +34 -0
- package/dist/lint/post-synth/sqlch101.js +34 -0
- package/dist/lint/post-synth/sqlch102.js +23 -0
- package/dist/lint/post-synth/sqlch103.js +45 -0
- package/dist/lint/post-synth/sqlch104.js +41 -0
- package/dist/lint/post-synth/sqlch105.js +36 -0
- package/dist/lint/post-synth/sqlch106.js +27 -0
- package/dist/lint/post-synth/sqlch107.js +39 -0
- package/dist/lint/post-synth/sqlch108.js +28 -0
- package/dist/lint/post-synth/sqlch109.js +21 -0
- package/dist/lint/post-synth/sqlch110.js +31 -0
- package/dist/lint/post-synth/sqlch111.js +41 -0
- package/dist/lint/post-synth/sqlch112.js +28 -0
- package/dist/lint/post-synth/sqlch113.js +19 -0
- package/dist/lint/post-synth/sqlch114.js +26 -0
- package/dist/lint/post-synth/sqlch115.js +30 -0
- package/dist/lint/post-synth/sqlch116.js +22 -0
- package/dist/lint/post-synth/sqlch117.js +26 -0
- package/dist/lint/post-synth/sqlch118.js +27 -0
- package/dist/lint/post-synth/sqlch119.js +32 -0
- package/dist/lint/post-synth/sqlch120.js +20 -0
- package/dist/lint/post-synth/sqlch121.js +11 -0
- package/dist/lint/post-synth/sqlch122.js +11 -0
- package/dist/lint/post-synth/sqlch123.js +10 -0
- package/dist/lint/post-synth/sqlch124.js +21 -0
- package/dist/lint/post-synth/sqlch125.js +55 -0
- package/dist/lint/post-synth/sqlch126.js +62 -0
- package/dist/lint/post-synth/sqlch127.js +132 -0
- package/dist/lint/post-synth/sqlpg101.js +25 -0
- package/dist/lint/post-synth/sqlpg102.js +35 -0
- package/dist/lint/post-synth/sqlpg103.js +23 -0
- package/dist/lint/post-synth/sqlpg104.js +23 -0
- package/dist/lint/post-synth/sqlpg105.js +22 -0
- package/dist/lint/post-synth/sqlpg106.js +22 -0
- package/dist/lint/post-synth/sqlpg107.js +22 -0
- package/dist/lint/post-synth/sqlpg108.js +28 -0
- package/dist/lint/post-synth/sqlpg109.js +39 -0
- package/dist/lint/post-synth/sqlpg110.js +38 -0
- package/dist/lint/post-synth/sqlpg111.js +23 -0
- package/dist/lint/post-synth/sqlpg112.js +25 -0
- package/dist/lint/post-synth/sqlpg113.js +29 -0
- package/dist/lint/post-synth/sqlpg114.js +39 -0
- package/dist/lint/post-synth/sqlpg115.js +48 -0
- package/dist/lint/post-synth/sqlpg116.js +26 -0
- package/dist/lint/post-synth/sqlpg117.js +24 -0
- package/dist/lint/post-synth/sqlpg118.js +22 -0
- package/dist/lint/post-synth/sqlpg119.js +41 -0
- package/dist/lint/post-synth/sqlpg120.js +83 -0
- package/dist/lint/post-synth/sqlpg121.js +33 -0
- package/dist/lint/post-synth/sqlpg122.js +27 -0
- package/dist/lint/post-synth/sqlpg123.js +69 -0
- package/dist/lint/post-synth/sqlpg124.js +50 -0
- package/dist/lint/post-synth/sqlpg125.js +41 -0
- package/dist/lint/post-synth/sqlpg126.js +38 -0
- package/dist/lint/rules/index.js +12 -0
- package/dist/lint/rules/postgres-templates.js +27 -0
- package/dist/lint/rules/sqlch001.js +79 -0
- package/dist/lint/rules/sqlch002.js +124 -0
- package/dist/lint/rules/sqlch003.js +69 -0
- package/dist/lint/rules/sqlch004.js +83 -0
- package/dist/lint/rules/sqlpg001.js +68 -0
- package/dist/lint/rules/sqlpg002.js +69 -0
- package/dist/lint/rules/sqlpg003.js +58 -0
- package/dist/lint/rules/sqlpg004.js +77 -0
- package/dist/lint/rules/sqlpg005.js +86 -0
- package/dist/lint/rules/templates.js +25 -0
- package/dist/lint/rules/typed-names.js +132 -0
- package/dist/live-dialect.js +32 -0
- package/dist/lsp/catalog.js +44 -0
- package/dist/lsp/completions.js +136 -0
- package/dist/lsp/hover.js +115 -0
- package/dist/lsp/postgres-catalog.js +176 -0
- package/dist/lsp/postgres-template.js +171 -0
- package/dist/lsp/postgres.js +266 -0
- package/dist/lsp/refs.js +60 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +19 -0
- package/dist/lsp/template.js +143 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +293 -0
- package/dist/mcp/postgres.js +117 -0
- package/dist/migration-statements.js +243 -0
- package/dist/op/activities/clickhouse-apply.js +83 -0
- package/dist/op/activities/clickhouse-emulator.js +53 -0
- package/dist/op/activities/clickhouse-rebuild.js +119 -0
- package/dist/op/activities/index.js +26 -0
- package/dist/op/activities/postgres-apply.js +126 -0
- package/dist/op/activities/postgres-emulator.js +70 -0
- package/dist/op/activities/postgres-migration.js +156 -0
- package/dist/op/activities/sql-exec.js +57 -0
- package/dist/op/activity-contracts.js +103 -0
- package/dist/package-cli.js +14 -0
- package/dist/plugin.js +259 -0
- package/dist/postgres/access/acl.js +344 -0
- package/dist/postgres/access/changes.js +30 -0
- package/dist/postgres/access/import.js +61 -0
- package/dist/postgres/access/live.js +96 -0
- package/dist/postgres/apply/apply.js +557 -0
- package/dist/postgres/apply/statements.js +882 -0
- package/dist/postgres/catalog-types.js +18 -0
- package/dist/postgres/container.js +58 -0
- package/dist/postgres/entities.js +1390 -0
- package/dist/postgres/entity-types.js +28 -0
- package/dist/postgres/import/generator.js +283 -0
- package/dist/postgres/import/ir.js +53 -0
- package/dist/postgres/import/live-export.js +83 -0
- package/dist/postgres/keywords.js +32 -0
- package/dist/postgres/live/bind.js +130 -0
- package/dist/postgres/live/catalog-kinds.js +301 -0
- package/dist/postgres/live/catalog.js +644 -0
- package/dist/postgres/live/client.js +151 -0
- package/dist/postgres/live/describe-resources.js +135 -0
- package/dist/postgres/live/undeclarable.js +111 -0
- package/dist/postgres/migrate/backfill.js +170 -0
- package/dist/postgres/migrate/batches.js +122 -0
- package/dist/postgres/migrate/carry.js +518 -0
- package/dist/postgres/migrate/handoff.js +59 -0
- package/dist/postgres/migrate/names.js +61 -0
- package/dist/postgres/migrate/observe.js +360 -0
- package/dist/postgres/migrate/op.js +178 -0
- package/dist/postgres/migrate/receipts.js +117 -0
- package/dist/postgres/migrate/replication.js +106 -0
- package/dist/postgres/migrate/steps.js +514 -0
- package/dist/postgres/overlays/columns.js +31 -0
- package/dist/postgres/overlays/constraints.js +52 -0
- package/dist/postgres/overlays/extension-objects.js +88 -0
- package/dist/postgres/overlays/indexes.js +25 -0
- package/dist/postgres/overlays/kinds.js +2 -0
- package/dist/postgres/overlays/partitioning.js +23 -0
- package/dist/postgres/overlays/relations.js +25 -0
- package/dist/postgres/overlays/storage.js +65 -0
- package/dist/postgres/overlays/type-ddl.js +36 -0
- package/dist/postgres/overlays/types.js +74 -0
- package/dist/postgres/parser.js +1824 -0
- package/dist/postgres/plan/commands.js +137 -0
- package/dist/postgres/plan/deep.js +324 -0
- package/dist/postgres/plan/diff.js +499 -0
- package/dist/postgres/plan/disruption.js +64 -0
- package/dist/postgres/plan/normalize.js +889 -0
- package/dist/postgres/plan/report.js +21 -0
- package/dist/postgres/plan/rules.js +115 -0
- package/dist/postgres/plan/schema.js +105 -0
- package/dist/postgres/plan/server-normalize.js +166 -0
- package/dist/postgres/providers/aurora.js +44 -0
- package/dist/postgres/providers/azure.js +53 -0
- package/dist/postgres/providers/cloud-sql.js +41 -0
- package/dist/postgres/providers/index.js +73 -0
- package/dist/postgres/providers/neon.js +44 -0
- package/dist/postgres/providers/rds.js +47 -0
- package/dist/postgres/providers/supabase.js +45 -0
- package/dist/postgres/providers/types.js +21 -0
- package/dist/postgres/row-types.js +107 -0
- package/dist/postgres/testing/fake-client.js +34 -0
- package/dist/postgres/testing/migration.js +48 -0
- package/dist/postgres/testing/roundtrip-schema.js +64 -0
- package/dist/postgres/testing/server.js +47 -0
- package/dist/postgres/testing/writable-server.js +160 -0
- package/dist/postgres/tokens.js +41 -0
- package/dist/postgres.js +15 -0
- package/dist/read/clickhouse.js +82 -0
- package/dist/read/objects.js +44 -0
- package/dist/read/postgres.js +104 -0
- package/dist/read.js +52 -0
- package/dist/receipts.js +207 -0
- package/dist/serializer.js +172 -0
- package/dist/skill-defs.js +93 -0
- package/dist/spec/catalog.js +191 -0
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/fetch.js +90 -0
- package/dist/spec/pin.js +42 -0
- package/dist/spec/postgres-catalog.js +171 -0
- package/dist/spec/postgres-fetch.js +91 -0
- package/dist/spec/postgres-pin.js +82 -0
- package/dist/sql-files.js +206 -0
- package/dist/sql-types-command.js +97 -0
- package/dist/sql-types.js +323 -0
- package/dist/token-source.js +156 -0
- package/dist/validate-cli.js +5 -0
- package/dist/validate.js +151 -0
- package/dist/workspace-outputs.js +191 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/lint/rules/typescript-7.test.ts +2 -1
- package/src/lsp/catalog.ts +2 -2
- package/src/lsp/postgres-catalog.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/mcp/index.ts +4 -2
- package/src/spec/fetch.ts +2 -2
- package/src/spec/postgres-fetch.ts +2 -2
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tag does with its interpolations, in every dialect (chant #3196,
|
|
3
|
+
* #3212): splice plain values into the token list as SQL text while
|
|
4
|
+
* references stay `ref` tokens, render a span with its references written as
|
|
5
|
+
* names, record which props each spliced value fed, locate a parse error on
|
|
6
|
+
* its template line, and read column lineage off a view's select list.
|
|
7
|
+
*
|
|
8
|
+
* The dialect supplies its tokenizer rules, which values are its objects and
|
|
9
|
+
* column references, how a reference renders, and how a select-list name
|
|
10
|
+
* reads ({@link TemplateDialect}).
|
|
11
|
+
*/
|
|
12
|
+
import { isTrivia, tokenize, tokenizeText, untokenize } from "./tokens.js";
|
|
13
|
+
import { interpolationLine, spliceText, SqlTemplateError } from "./template.js";
|
|
14
|
+
/** Record that the spliced text inside `span` fed `path`. */
|
|
15
|
+
export function feed(ctx, span, path) {
|
|
16
|
+
if (!span)
|
|
17
|
+
return;
|
|
18
|
+
for (let i = span.from; i < span.to; i++) {
|
|
19
|
+
const splice = ctx.tokens[i].splice;
|
|
20
|
+
if (splice !== undefined)
|
|
21
|
+
ctx.fed[splice].add(path);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Splice every interpolation that is not a reference into the token list as
|
|
26
|
+
* SQL text. References stay `ref` tokens for the parser and the entity to see.
|
|
27
|
+
*/
|
|
28
|
+
export function splice(d, tag, parts, values) {
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const t of tokenize(parts, d.lexical)) {
|
|
31
|
+
if (t.kind !== "ref") {
|
|
32
|
+
out.push(t);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const value = values[t.part];
|
|
36
|
+
if (d.isObject(value) || d.isColumnRef(value)) {
|
|
37
|
+
out.push(t);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const text = spliceText(value);
|
|
41
|
+
if (typeof text !== "string") {
|
|
42
|
+
throw new SqlTemplateError(tag, `the interpolation on template line ${interpolationLine(parts, t.part)} is ${text.error}`, t.part, parts[t.part].length);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
for (const s of tokenizeText(text, t.part, d.lexical))
|
|
46
|
+
out.push({ ...s, splice: t.part });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
throw new SqlTemplateError(tag, `the text interpolated on template line ${interpolationLine(parts, t.part)} does not tokenize: ${message}`, t.part, parts[t.part].length);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** A span's text, interpolations rendered, trimmed. With `path`, the span's spliced text is recorded as feeding it. */
|
|
56
|
+
export function spanText(ctx, span, path) {
|
|
57
|
+
if (!span || span.to <= span.from)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (path)
|
|
60
|
+
feed(ctx, span, path);
|
|
61
|
+
return untokenize(ctx.tokens.slice(span.from, span.to), (i) => ctx.d.renderReference(ctx.values[i], i)).trim();
|
|
62
|
+
}
|
|
63
|
+
/** A parsed statement's unknown clauses as props carry them; undefined when there are none. */
|
|
64
|
+
export function unparsedClauses(ctx, unknown) {
|
|
65
|
+
if (!unknown || unknown.length === 0)
|
|
66
|
+
return undefined;
|
|
67
|
+
const render = (i) => ctx.d.renderReference(ctx.values[i], i);
|
|
68
|
+
return unknown.map((u) => {
|
|
69
|
+
const before = untokenize(ctx.tokens.slice(0, u.span.from), render).replace(/^\s+/, "");
|
|
70
|
+
return {
|
|
71
|
+
clause: u.clause,
|
|
72
|
+
text: spanText(ctx, u.span, "unparsed") ?? u.clause,
|
|
73
|
+
...(u.column !== undefined ? { column: u.column } : {}),
|
|
74
|
+
line: before.split("\n").length,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** A parse error as the tag reports it: on the template line, or in the interpolated text it came from. */
|
|
79
|
+
export function templateSyntaxError(tag, parts, err) {
|
|
80
|
+
const where = err.token?.splice !== undefined
|
|
81
|
+
? `in the text interpolated on template line ${interpolationLine(parts, err.token.splice)}`
|
|
82
|
+
: `on template line ${parts.slice(0, err.part).join("${}").split("\n").length + parts[err.part].slice(0, err.offset).split("\n").length - 1}`;
|
|
83
|
+
return new SqlTemplateError(tag, `${err.message} (${where})`, err.part, err.offset);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Column-level lineage of a view's SELECT: one edge per item of the top-level
|
|
87
|
+
* select list. FROM, WHERE and GROUP BY stay text; the objects they
|
|
88
|
+
* interpolate land in `reads`. A column written by name inside the SQL is not
|
|
89
|
+
* lineage: only references are (#3047, "no parsing of SQL for names").
|
|
90
|
+
*/
|
|
91
|
+
export function lineage(ctx, select) {
|
|
92
|
+
const { d } = ctx;
|
|
93
|
+
const sig = [];
|
|
94
|
+
for (let i = select.from; i < select.to; i++)
|
|
95
|
+
if (!isTrivia(ctx.tokens[i]))
|
|
96
|
+
sig.push(i);
|
|
97
|
+
const tok = (i) => ctx.tokens[i];
|
|
98
|
+
const isKw = (i, w) => tok(i).kind === "ident" && tok(i).text.toUpperCase() === w;
|
|
99
|
+
let depth = 0;
|
|
100
|
+
let selectAt = -1;
|
|
101
|
+
let fromAt = sig.length;
|
|
102
|
+
const commas = [];
|
|
103
|
+
for (let k = 0; k < sig.length; k++) {
|
|
104
|
+
const t = tok(sig[k]);
|
|
105
|
+
if (t.kind === "punct" && t.text === "(")
|
|
106
|
+
depth++;
|
|
107
|
+
else if (t.kind === "punct" && t.text === ")")
|
|
108
|
+
depth--;
|
|
109
|
+
else if (depth === 0 && selectAt < 0 && isKw(sig[k], "SELECT")) {
|
|
110
|
+
selectAt = k;
|
|
111
|
+
if (sig[k + 1] !== undefined && isKw(sig[k + 1], "DISTINCT")) {
|
|
112
|
+
selectAt = k + 1;
|
|
113
|
+
// `DISTINCT ON (a, b)`: the parenthesized list is not a select-list item.
|
|
114
|
+
if (d.lineage?.distinctOn && sig[k + 2] !== undefined && isKw(sig[k + 2], "ON") && tok(sig[k + 3] ?? -1)?.text === "(") {
|
|
115
|
+
let nest = 0;
|
|
116
|
+
let j = k + 3;
|
|
117
|
+
for (; j < sig.length; j++) {
|
|
118
|
+
const x = tok(sig[j]);
|
|
119
|
+
if (x.kind === "punct" && x.text === "(")
|
|
120
|
+
nest++;
|
|
121
|
+
else if (x.kind === "punct" && x.text === ")" && --nest === 0)
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
selectAt = j;
|
|
125
|
+
k = j;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (sig[k + 1] !== undefined && d.lineage?.distinctOn && isKw(sig[k + 1], "ALL"))
|
|
129
|
+
selectAt = k + 1;
|
|
130
|
+
}
|
|
131
|
+
else if (depth === 0 && selectAt >= 0 && fromAt === sig.length && isKw(sig[k], "FROM"))
|
|
132
|
+
fromAt = k;
|
|
133
|
+
else if (depth === 0 && selectAt >= 0 && fromAt === sig.length && t.kind === "punct" && t.text === ",")
|
|
134
|
+
commas.push(k);
|
|
135
|
+
}
|
|
136
|
+
const edges = [];
|
|
137
|
+
if (selectAt >= 0) {
|
|
138
|
+
const bounds = [selectAt, ...commas, fromAt];
|
|
139
|
+
for (let b = 0; b + 1 < bounds.length; b++) {
|
|
140
|
+
const items = sig.slice(bounds[b] + 1, bounds[b + 1]);
|
|
141
|
+
if (items.length === 0)
|
|
142
|
+
continue;
|
|
143
|
+
let output;
|
|
144
|
+
let exprItems = items;
|
|
145
|
+
const asAt = items.findIndex((i) => isKw(i, "AS"));
|
|
146
|
+
if (asAt >= 0 && asAt === items.length - 2) {
|
|
147
|
+
output = d.identValue(tok(items[asAt + 1]).text);
|
|
148
|
+
exprItems = items.slice(0, asAt);
|
|
149
|
+
}
|
|
150
|
+
else if (items.length === 1) {
|
|
151
|
+
const t = tok(items[0]);
|
|
152
|
+
if (t.kind === "ref" && d.isColumnRef(ctx.values[t.part]))
|
|
153
|
+
output = ctx.values[t.part].attribute;
|
|
154
|
+
else if (t.kind === "ident" || t.kind === "qident")
|
|
155
|
+
output = d.identValue(t.text);
|
|
156
|
+
}
|
|
157
|
+
else if (items.length === 3 && tok(items[1]).text === "." && (tok(items[2]).kind === "ident" || tok(items[2]).kind === "qident")) {
|
|
158
|
+
output = d.identValue(tok(items[2]).text);
|
|
159
|
+
}
|
|
160
|
+
else if (d.lineage?.qualifiedRefs && items.length === 3 && tok(items[1]).text === "." && tok(items[2]).kind === "ref" && d.isColumnRef(ctx.values[tok(items[2]).part])) {
|
|
161
|
+
// `alias.${t.columns.c}` names its output after the column.
|
|
162
|
+
output = ctx.values[tok(items[2]).part].attribute;
|
|
163
|
+
}
|
|
164
|
+
else if (d.lineage?.aliasWithoutAs && items.length >= 2) {
|
|
165
|
+
// `expr name`: an alias without AS. The name follows an operand, never an operator or `.`.
|
|
166
|
+
const last = tok(items[items.length - 1]);
|
|
167
|
+
const before = tok(items[items.length - 2]);
|
|
168
|
+
const keyword = (t) => (t.kind === "ident" ? d.lineage?.keyword?.(t) : undefined);
|
|
169
|
+
const operandEnd = before.kind === "ref" || before.kind === "string" || before.kind === "number" || before.kind === "qident" || before.kind === "param" ||
|
|
170
|
+
(before.kind === "ident" && keyword(before) === undefined) ||
|
|
171
|
+
(before.kind === "punct" && (before.text === ")" || before.text === "]"));
|
|
172
|
+
const label = last.kind === "qident" || (last.kind === "ident" && keyword(last) !== "reserved");
|
|
173
|
+
if (label && operandEnd) {
|
|
174
|
+
output = d.identValue(last.text);
|
|
175
|
+
exprItems = items.slice(0, -1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (output === undefined && d.lineage?.outputOf)
|
|
179
|
+
output = d.lineage.outputOf(exprItems.map(tok), (t) => (t.kind === "ref" ? ctx.values[t.part] : undefined));
|
|
180
|
+
const refs = exprItems.filter((i) => tok(i).kind === "ref").map((i) => tok(i).part);
|
|
181
|
+
const exprSpan = { from: exprItems[0], to: exprItems[exprItems.length - 1] + 1, refs };
|
|
182
|
+
edges.push({
|
|
183
|
+
output: output ?? d.unnamedOutput(b + 1),
|
|
184
|
+
expr: spanText(ctx, exprSpan) ?? "",
|
|
185
|
+
from: [...new Set(refs.map((i) => ctx.values[i]).filter((v) => d.isColumnRef(v)))],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const reads = [...new Set(select.refs.map((i) => ctx.values[i]).filter((v) => d.isObject(v)))];
|
|
190
|
+
return { edges, reads };
|
|
191
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalization helpers that hold in every dialect. A dialect's normalization
|
|
3
|
+
* undoes what its own server rewrites (quoting, type aliases, default
|
|
4
|
+
* levels), by rule over its own parse; these are the pieces that are not any
|
|
5
|
+
* one database's grammar.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The name in a `-- previously: <name>` comment, when the text holds one: the
|
|
9
|
+
* rename hint, before a `CREATE` for an object or on a column's line for a
|
|
10
|
+
* column. The name may be quoted with backquotes or double quotes.
|
|
11
|
+
*/
|
|
12
|
+
export function previouslyIn(comments) {
|
|
13
|
+
for (const c of comments) {
|
|
14
|
+
const m = /^--\s*previously\s*:\s*([`"]?)([^\s`"]+)\1\s*$/i.exec(c.trim());
|
|
15
|
+
if (m)
|
|
16
|
+
return m[2];
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Split canonical text (tokens joined by single spaces) on its top-level
|
|
22
|
+
* commas, outside parentheses and brackets.
|
|
23
|
+
*/
|
|
24
|
+
export function splitTopLevel(text) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
let depth = 0;
|
|
27
|
+
let cur = [];
|
|
28
|
+
for (const tok of text.split(" ")) {
|
|
29
|
+
if (tok === "(" || tok === "[")
|
|
30
|
+
depth++;
|
|
31
|
+
if (tok === ")" || tok === "]")
|
|
32
|
+
depth--;
|
|
33
|
+
if (tok === "," && depth === 0) {
|
|
34
|
+
parts.push(cur.join(" "));
|
|
35
|
+
cur = [];
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
cur.push(tok);
|
|
39
|
+
}
|
|
40
|
+
if (cur.length)
|
|
41
|
+
parts.push(cur.join(" "));
|
|
42
|
+
return parts.filter((p) => p.length > 0);
|
|
43
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant's ownership marker as a trailer on an object's own comment, in every
|
|
3
|
+
* dialect that can comment on its objects (#3208).
|
|
4
|
+
*
|
|
5
|
+
* The marker never replaces the user's comment, which is a declared property.
|
|
6
|
+
* It is appended as a bracketed trailer,
|
|
7
|
+
*
|
|
8
|
+
* Raw events [chant managed-by=chant stack=shop env=prod]
|
|
9
|
+
*
|
|
10
|
+
* and taken off again wherever chant reads a definition, so planning, the deep
|
|
11
|
+
* diff and import compare and write the declared comment alone. Someone who
|
|
12
|
+
* edits the comment by hand and drops the trailer makes the object read as
|
|
13
|
+
* foreign, which is the safe direction: it is never pruned, and the next apply
|
|
14
|
+
* of its declaration stamps it again.
|
|
15
|
+
*
|
|
16
|
+
* Values are percent-encoded outside `[A-Za-z0-9._-]`, so the trailer never
|
|
17
|
+
* holds a quote, a backslash, a space or a bracket of its own.
|
|
18
|
+
*
|
|
19
|
+
* Where the comment lives in a statement, and so how a trailer comes off a
|
|
20
|
+
* printed `CREATE`, is the dialect's grammar and stays with the dialect.
|
|
21
|
+
*/
|
|
22
|
+
import { OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
23
|
+
/** The keys inside the trailer. */
|
|
24
|
+
export const COMMENT_OWNERSHIP_KEYS = {
|
|
25
|
+
managedBy: "managed-by",
|
|
26
|
+
stack: "stack",
|
|
27
|
+
env: "env",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The trailer key on the objects that hold effect receipts. Like a
|
|
31
|
+
* migration's working objects, they are chant's own bookkeeping and never
|
|
32
|
+
* part of a declared schema.
|
|
33
|
+
*/
|
|
34
|
+
export const RECEIPTS_TRAILER_KEY = "receipts";
|
|
35
|
+
const VALUE = "[A-Za-z0-9._%-]*";
|
|
36
|
+
/** One `key=value` pair of a trailer, as a regular expression source. */
|
|
37
|
+
export const TRAILER_PAIR = `[A-Za-z0-9._-]+=${VALUE}`;
|
|
38
|
+
/** The trailer at the end of a comment's text. */
|
|
39
|
+
const TRAILER = new RegExp(`(?:^|\\s+)\\[chant((?: ${TRAILER_PAIR})+)\\]$`);
|
|
40
|
+
const encode = (v) => v.replace(/[^A-Za-z0-9._-]/g, (c) => [...new TextEncoder().encode(c)].map((b) => `%${b.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
41
|
+
const decode = (v) => {
|
|
42
|
+
try {
|
|
43
|
+
return decodeURIComponent(v);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return v;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The trailer for a marker: `[chant managed-by=chant stack=<stack> env=<env>]`.
|
|
51
|
+
* `extra` appends more pairs after the marker's own; a migration Op uses them
|
|
52
|
+
* to say which migration an object it made belongs to.
|
|
53
|
+
* Keys are `[A-Za-z0-9._-]+`; values are encoded like the rest.
|
|
54
|
+
*/
|
|
55
|
+
export function markerTrailer(marker, extra = {}) {
|
|
56
|
+
const k = COMMENT_OWNERSHIP_KEYS;
|
|
57
|
+
const pairs = [`${k.managedBy}=${OWNERSHIP_MANAGED_BY_VALUE}`];
|
|
58
|
+
if (marker?.stack)
|
|
59
|
+
pairs.push(`${k.stack}=${encode(marker.stack)}`);
|
|
60
|
+
if (marker?.env)
|
|
61
|
+
pairs.push(`${k.env}=${encode(marker.env)}`);
|
|
62
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
63
|
+
if (!/^[A-Za-z0-9._-]+$/.test(key))
|
|
64
|
+
throw new Error(`ownership trailer key ${JSON.stringify(key)} is not [A-Za-z0-9._-]+`);
|
|
65
|
+
pairs.push(`${key}=${encode(value)}`);
|
|
66
|
+
}
|
|
67
|
+
return `[chant ${pairs.join(" ")}]`;
|
|
68
|
+
}
|
|
69
|
+
/** A comment's text with the trailer taken off. */
|
|
70
|
+
export function stripMarker(comment) {
|
|
71
|
+
return comment.replace(TRAILER, "");
|
|
72
|
+
}
|
|
73
|
+
/** The comment to set: the declared comment, then the trailer (with `extra` pairs, see {@link markerTrailer}). */
|
|
74
|
+
export function stampedComment(declared, marker, extra) {
|
|
75
|
+
const own = stripMarker(declared ?? "");
|
|
76
|
+
return own ? `${own} ${markerTrailer(marker, extra)}` : markerTrailer(marker, extra);
|
|
77
|
+
}
|
|
78
|
+
/** Every pair in a comment's trailer, decoded, or undefined when it has no trailer. */
|
|
79
|
+
export function readTrailerPairs(comment) {
|
|
80
|
+
const m = TRAILER.exec(comment ?? "");
|
|
81
|
+
if (!m)
|
|
82
|
+
return undefined;
|
|
83
|
+
return new Map(m[1]
|
|
84
|
+
.trim()
|
|
85
|
+
.split(" ")
|
|
86
|
+
.map((p) => {
|
|
87
|
+
const at = p.indexOf("=");
|
|
88
|
+
return [p.slice(0, at), decode(p.slice(at + 1))];
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
/** Whether a comment carries chant's managed-by marker and any of `keys` in its trailer. */
|
|
92
|
+
export function hasChantTrailerKey(comment, keys) {
|
|
93
|
+
const pairs = readTrailerPairs(comment);
|
|
94
|
+
return pairs?.get(COMMENT_OWNERSHIP_KEYS.managedBy) === OWNERSHIP_MANAGED_BY_VALUE && keys.some((k) => pairs.has(k));
|
|
95
|
+
}
|
|
96
|
+
/** The marker a comment carries, or undefined when it carries none. */
|
|
97
|
+
export function readMarker(comment) {
|
|
98
|
+
const pairs = readTrailerPairs(comment);
|
|
99
|
+
if (!pairs)
|
|
100
|
+
return undefined;
|
|
101
|
+
const k = COMMENT_OWNERSHIP_KEYS;
|
|
102
|
+
const managedBy = pairs.get(k.managedBy);
|
|
103
|
+
if (managedBy === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
const stack = pairs.get(k.stack);
|
|
106
|
+
const env = pairs.get(k.env);
|
|
107
|
+
return { managedBy, ...(stack ? { stack } : {}), ...(env ? { env } : {}) };
|
|
108
|
+
}
|
|
109
|
+
/** Whether a comment carries chant's managed-by marker. */
|
|
110
|
+
export function isChantManaged(comment) {
|
|
111
|
+
return readMarker(comment)?.managedBy === OWNERSHIP_MANAGED_BY_VALUE;
|
|
112
|
+
}
|
|
113
|
+
/** Whether the comment's marker is exactly this project's: managed by chant, same stack, same env. */
|
|
114
|
+
export function carriesMarker(comment, marker) {
|
|
115
|
+
const m = readMarker(comment);
|
|
116
|
+
if (m?.managedBy !== OWNERSHIP_MANAGED_BY_VALUE)
|
|
117
|
+
return false;
|
|
118
|
+
return (m.stack ?? undefined) === (marker?.stack || undefined) && (m.env ?? undefined) === (marker?.env || undefined);
|
|
119
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect receipts kept on the database server itself, in every dialect
|
|
3
|
+
* (concepts/effect-receipts.mdx).
|
|
4
|
+
*
|
|
5
|
+
* A receipt is the proof that one effect ran, kept where the effect happened.
|
|
6
|
+
* A migration Op's backfill copies data on the server, so its receipts are
|
|
7
|
+
* rows on the same server, bound to the object they witness, and read and
|
|
8
|
+
* written over the connection the copy already uses. They die with the data
|
|
9
|
+
* when the Op's onFailure drops it, which a receipt kept in git would not.
|
|
10
|
+
*
|
|
11
|
+
* A dialect's store implements this interface over its own client and keeps
|
|
12
|
+
* the receipts in a table whose comment carries the ownership trailer with
|
|
13
|
+
* the `receipts` key (`./ownership.ts`), so schema reads leave it out. A
|
|
14
|
+
* migration's store is reached by its backfill directly. The store `effect()`
|
|
15
|
+
* steps use (`../receipts.ts`) is registered as the `receiptRead` /
|
|
16
|
+
* `receiptWrite` activities only as a fallback, so it never takes over
|
|
17
|
+
* another configured lexicon's receipt row.
|
|
18
|
+
*/
|
|
19
|
+
/** The receipt's address: `<stack>/<env>/<effect>`, the same fields an ownership marker carries. */
|
|
20
|
+
export function receiptAddress(identity, effect) {
|
|
21
|
+
return `${identity.stack || "-"}/${identity.env || "-"}/${effect}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* References between schema objects, in every dialect: column lineage, the
|
|
3
|
+
* name a reference is written under in a build's output, and the order
|
|
4
|
+
* objects are created in.
|
|
5
|
+
*/
|
|
6
|
+
import { isAttrRefLike } from "@intentius/chant/utils";
|
|
7
|
+
/**
|
|
8
|
+
* A reference as a build's output writes it: an entity's export name, or
|
|
9
|
+
* `export.column` for a column. Undefined when the target is not exported.
|
|
10
|
+
*/
|
|
11
|
+
export function referenceName(value, names) {
|
|
12
|
+
if (isAttrRefLike(value)) {
|
|
13
|
+
const parent = value.parent.deref();
|
|
14
|
+
const owner = parent ? names.get(parent) : undefined;
|
|
15
|
+
return owner ? `${owner}.${value.attribute}` : undefined;
|
|
16
|
+
}
|
|
17
|
+
return names.get(value);
|
|
18
|
+
}
|
|
19
|
+
/** Lineage edges with their column references written as `export.column`. */
|
|
20
|
+
export function lineageJson(edges, names) {
|
|
21
|
+
return edges.map((e) => ({
|
|
22
|
+
output: e.output,
|
|
23
|
+
expr: e.expr,
|
|
24
|
+
from: e.from.map((r) => referenceName(r, names) ?? null),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Export names in creation order: an object after everything it references,
|
|
29
|
+
* and after the names in `extra` (an object read from a `.sql` file that a
|
|
30
|
+
* template names as text, `../sql-files.ts`).
|
|
31
|
+
* Ties break by export name, so the order is the same however the files were
|
|
32
|
+
* discovered. A reference cycle is an error naming the cycle.
|
|
33
|
+
*/
|
|
34
|
+
export function applyOrder(objects, names, extra) {
|
|
35
|
+
const deps = new Map();
|
|
36
|
+
for (const [name, obj] of objects) {
|
|
37
|
+
const on = new Set(extra?.get(name)?.filter((d) => d !== name && objects.has(d)) ?? []);
|
|
38
|
+
for (const ref of obj.dependsOn) {
|
|
39
|
+
const parent = isAttrRefLike(ref) ? ref.parent.deref() : ref;
|
|
40
|
+
const dep = parent ? names.get(parent) : undefined;
|
|
41
|
+
if (dep !== undefined && dep !== name && objects.has(dep))
|
|
42
|
+
on.add(dep);
|
|
43
|
+
}
|
|
44
|
+
deps.set(name, [...on].sort());
|
|
45
|
+
}
|
|
46
|
+
const order = [];
|
|
47
|
+
const done = new Set();
|
|
48
|
+
const visit = (n, stack) => {
|
|
49
|
+
if (done.has(n))
|
|
50
|
+
return;
|
|
51
|
+
if (stack.includes(n))
|
|
52
|
+
throw new Error(`sql: a reference cycle between schema objects: ${[...stack, n].join(" -> ")}`);
|
|
53
|
+
for (const d of deps.get(n) ?? [])
|
|
54
|
+
visit(d, [...stack, n]);
|
|
55
|
+
done.add(n);
|
|
56
|
+
order.push(n);
|
|
57
|
+
};
|
|
58
|
+
for (const n of [...objects.keys()].sort())
|
|
59
|
+
visit(n, []);
|
|
60
|
+
return order;
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The statements a schema change takes, rendered offline (#3644), the same
|
|
3
|
+
* shape in every dialect: what `diffStatements`
|
|
4
|
+
* (`../migration-statements.ts`) returns and `chant sql diff --statements
|
|
5
|
+
* --json` prints, for a migration file to hold.
|
|
6
|
+
*
|
|
7
|
+
* Every value is plain JSON: strings, booleans, arrays and objects, no
|
|
8
|
+
* `undefined` left in, so the document reads back as it was written. Steps
|
|
9
|
+
* are in the order they run.
|
|
10
|
+
*/
|
|
11
|
+
/** A classified change as a step names it, with no `undefined` fields. */
|
|
12
|
+
export function statementChange(c) {
|
|
13
|
+
return {
|
|
14
|
+
rule: c.rule,
|
|
15
|
+
class: c.class,
|
|
16
|
+
field: c.field,
|
|
17
|
+
...(c.before !== undefined ? { before: c.before } : {}),
|
|
18
|
+
...(c.after !== undefined ? { after: c.after } : {}),
|
|
19
|
+
...(c.destructive ? { destructive: true } : {}),
|
|
20
|
+
...(c.note !== undefined ? { note: c.note } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** The steps as a migration file's SQL: each statement after a comment naming its object, rule and class; an Op or manual step as a comment. */
|
|
24
|
+
export function renderStatements(doc) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const s of doc.steps) {
|
|
27
|
+
if (s.kind === "statement") {
|
|
28
|
+
const notes = [s.transactional || doc.dialect !== "postgres" ? "" : "outside a transaction", s.waitsForMutation ? "waits for its mutation" : "", s.destructive ? "destructive" : ""].filter(Boolean);
|
|
29
|
+
out.push(`-- ${s.object} (${s.name}): ${s.rule} ${s.class}${notes.length ? `, ${notes.join(", ")}` : ""}`);
|
|
30
|
+
if (s.precheck)
|
|
31
|
+
out.push(`-- pre-check, must return 0 (${s.precheck.detail}): ${s.precheck.sql}`);
|
|
32
|
+
out.push(`${s.sql};`, "");
|
|
33
|
+
}
|
|
34
|
+
else if (s.kind === "op") {
|
|
35
|
+
out.push(`-- ${s.object} (${s.name}): ${s.changes.map((c) => c.rule).join(", ")} made by ${s.op}, not a statement:`, `-- ${s.declaration}`, "");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
out.push(`-- ${s.object} (${s.name}): ${s.changes.map((c) => c.rule).join(", ")} has no statement and no Op: ${s.detail}`, "");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out.join("\n");
|
|
42
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tagged-template plumbing every dialect's tags share: reading a
|
|
3
|
+
* template's raw parts, what a non-reference interpolation splices in, and
|
|
4
|
+
* the error a tag throws with the template line it happened on.
|
|
5
|
+
*
|
|
6
|
+
* Tokenizing and parsing the spliced statement is the dialect's own.
|
|
7
|
+
*/
|
|
8
|
+
/** A spliced value meant as a string literal. A dialect's `literal()` makes one with its own quoting. */
|
|
9
|
+
export class SqlLiteral {
|
|
10
|
+
constructor(sql) {
|
|
11
|
+
this.sql = sql;
|
|
12
|
+
Object.freeze(this);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** An error from a tag: the statement kind, the template line, and what went wrong. */
|
|
16
|
+
export class SqlTemplateError extends Error {
|
|
17
|
+
constructor(tag, message, part, offset) {
|
|
18
|
+
super(`${tag}\`...\`: ${message}`);
|
|
19
|
+
this.part = part;
|
|
20
|
+
this.offset = offset;
|
|
21
|
+
this.name = "SqlTemplateError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A template's parts as the SQL reads them: the raw text, so a backslash in a
|
|
26
|
+
* regex or an escape stays as written, with the two escapes a template
|
|
27
|
+
* literal forces undone. `` \` `` is a backquote and `\${` is `${`; neither
|
|
28
|
+
* can be written in a template any other way.
|
|
29
|
+
*/
|
|
30
|
+
export function templateParts(strings) {
|
|
31
|
+
const raw = strings.raw ?? strings;
|
|
32
|
+
return raw.map(unescapeTemplateDelimiters);
|
|
33
|
+
}
|
|
34
|
+
export function unescapeTemplateDelimiters(part) {
|
|
35
|
+
return part.replace(/\\(`|\$\{)/g, "$1");
|
|
36
|
+
}
|
|
37
|
+
/** Where interpolation `index` sits in the template, for a message: the line of the template it is on. */
|
|
38
|
+
export function interpolationLine(parts, index) {
|
|
39
|
+
return parts.slice(0, index + 1).join("${}").split("\n").length;
|
|
40
|
+
}
|
|
41
|
+
/** A value's kind, for a message. */
|
|
42
|
+
export function describeValue(value) {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return "undefined";
|
|
45
|
+
if (value === null)
|
|
46
|
+
return "null";
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return "an array";
|
|
49
|
+
return typeof value === "object" ? `an object (${Object.prototype.toString.call(value)})` : `a ${typeof value}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The SQL text a non-reference value splices in, or an error message: a
|
|
53
|
+
* string is SQL text, a number, bigint or boolean its literal, `null` is
|
|
54
|
+
* `NULL`, a {@link SqlLiteral} its quoted form. Anything else has no SQL form.
|
|
55
|
+
*/
|
|
56
|
+
export function spliceText(value) {
|
|
57
|
+
if (typeof value === "string")
|
|
58
|
+
return value;
|
|
59
|
+
if (typeof value === "number") {
|
|
60
|
+
return Number.isFinite(value) ? String(value) : { error: `the number ${value} has no SQL form` };
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "bigint")
|
|
63
|
+
return value.toString();
|
|
64
|
+
if (typeof value === "boolean")
|
|
65
|
+
return value ? "true" : "false";
|
|
66
|
+
if (value === null)
|
|
67
|
+
return "NULL";
|
|
68
|
+
if (value instanceof SqlLiteral)
|
|
69
|
+
return value.sql;
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
return {
|
|
72
|
+
error: "undefined. A column is referenced through `.columns` (`${events.columns.user_id}`, not " +
|
|
73
|
+
"`${events.user_id}`), and a column name that does not exist is undefined too",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { error: `${describeValue(value)}, which has no SQL form` };
|
|
77
|
+
}
|
|
78
|
+
/** `o` without its `undefined` and `false` fields, so props hold only what the DDL says. */
|
|
79
|
+
export const stripUnset = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined && v !== false));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stand-in HTTP server for tests, in every dialect: listens on a random
|
|
3
|
+
* loopback port and answers each request's body with what `answer` returns.
|
|
4
|
+
* A dialect's fake server (the ClickHouse HTTP interface answering its
|
|
5
|
+
* catalog reads) is built on it. Not a SQL engine.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
export async function startHttpStub(answer) {
|
|
9
|
+
const server = createServer((req, res) => {
|
|
10
|
+
let body = "";
|
|
11
|
+
req.on("data", (c) => (body += c));
|
|
12
|
+
req.on("end", () => {
|
|
13
|
+
const { status, text } = answer(body, req);
|
|
14
|
+
res.statusCode = status;
|
|
15
|
+
res.end(text);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", () => r()));
|
|
19
|
+
const { port } = server.address();
|
|
20
|
+
return { url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) };
|
|
21
|
+
}
|