@intentius/chant-lexicon-sql 0.124.31 → 0.124.32
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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small scanners over ClickHouse expression text as the parser keeps it
|
|
3
|
+
* (sort keys, engine arguments, column types), shared by the post-synth
|
|
4
|
+
* checks (`../lint/post-synth/clickhouse-helpers.ts`) and the public read API
|
|
5
|
+
* (`../read/clickhouse.ts`). They answer narrow questions ("is this a bare
|
|
6
|
+
* column name", "what family is this type"), not what an expression
|
|
7
|
+
* evaluates to.
|
|
8
|
+
*/
|
|
9
|
+
import { TABLE_ENGINES, TYPE_FAMILIES } from "../generated/clickhouse.js";
|
|
10
|
+
/** The engine's catalog entry, when the pinned server has the engine. */
|
|
11
|
+
export function engineSpec(engine) {
|
|
12
|
+
return engine ? TABLE_ENGINES[engine.name] : undefined;
|
|
13
|
+
}
|
|
14
|
+
/** A MergeTree family engine, replicated or not. */
|
|
15
|
+
export function isMergeTree(engine) {
|
|
16
|
+
return engineSpec(engine)?.mergeTree === true;
|
|
17
|
+
}
|
|
18
|
+
/** The engine without its `Replicated` prefix: `ReplicatedReplacingMergeTree` is `ReplacingMergeTree`. */
|
|
19
|
+
export function baseEngineName(engine) {
|
|
20
|
+
const spec = engineSpec(engine);
|
|
21
|
+
return spec?.replicates ?? engine.name;
|
|
22
|
+
}
|
|
23
|
+
/** Split on commas that are not inside parentheses, brackets or quotes. */
|
|
24
|
+
export function splitTop(text, separator = ",") {
|
|
25
|
+
const parts = [];
|
|
26
|
+
let depth = 0;
|
|
27
|
+
let quote;
|
|
28
|
+
let start = 0;
|
|
29
|
+
for (let i = 0; i < text.length; i++) {
|
|
30
|
+
const c = text[i];
|
|
31
|
+
if (quote) {
|
|
32
|
+
if (c === "\\")
|
|
33
|
+
i++;
|
|
34
|
+
else if (c === quote)
|
|
35
|
+
quote = undefined;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (c === "'" || c === '"' || c === "`")
|
|
39
|
+
quote = c;
|
|
40
|
+
else if (c === "(" || c === "[")
|
|
41
|
+
depth++;
|
|
42
|
+
else if (c === ")" || c === "]")
|
|
43
|
+
depth--;
|
|
44
|
+
else if (c === separator && depth === 0) {
|
|
45
|
+
parts.push(text.slice(start, i).trim());
|
|
46
|
+
start = i + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const last = text.slice(start).trim();
|
|
50
|
+
if (last !== "" || parts.length > 0)
|
|
51
|
+
parts.push(last);
|
|
52
|
+
return parts.filter((p) => p !== "");
|
|
53
|
+
}
|
|
54
|
+
/** `(a, b)` is the list `a`, `b`; `a, b` is the same; `tuple()` and `()` are empty. */
|
|
55
|
+
export function keyElements(expr) {
|
|
56
|
+
let text = expr.trim();
|
|
57
|
+
if (/^tuple\s*\(\s*\)$/i.test(text) || text === "()")
|
|
58
|
+
return [];
|
|
59
|
+
if (text.startsWith("(") && closingParen(text, 0) === text.length - 1)
|
|
60
|
+
text = text.slice(1, -1);
|
|
61
|
+
return splitTop(text).map((e) => e.replace(/\s+(ASC|DESC)$/i, "").replace(/\s+/g, " ").trim());
|
|
62
|
+
}
|
|
63
|
+
function closingParen(text, open) {
|
|
64
|
+
let depth = 0;
|
|
65
|
+
let quote;
|
|
66
|
+
for (let i = open; i < text.length; i++) {
|
|
67
|
+
const c = text[i];
|
|
68
|
+
if (quote) {
|
|
69
|
+
if (c === "\\")
|
|
70
|
+
i++;
|
|
71
|
+
else if (c === quote)
|
|
72
|
+
quote = undefined;
|
|
73
|
+
}
|
|
74
|
+
else if (c === "'" || c === '"' || c === "`")
|
|
75
|
+
quote = c;
|
|
76
|
+
else if (c === "(")
|
|
77
|
+
depth++;
|
|
78
|
+
else if (c === ")" && --depth === 0)
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
/** The column a bare name refers to: `a` or `` `a b` ``, else undefined. */
|
|
84
|
+
export function bareName(element) {
|
|
85
|
+
const text = element.trim();
|
|
86
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(text))
|
|
87
|
+
return text;
|
|
88
|
+
const quoted = /^`((?:[^`]|``)*)`$/.exec(text) ?? /^"((?:[^"]|"")*)"$/.exec(text);
|
|
89
|
+
return quoted ? quoted[1].replace(/``|""/g, (m) => m[0]) : undefined;
|
|
90
|
+
}
|
|
91
|
+
/** A type with its `Nullable(...)` and `LowCardinality(...)` wrappers removed. */
|
|
92
|
+
export function unwrapType(type) {
|
|
93
|
+
let t = (type ?? "").trim();
|
|
94
|
+
for (;;) {
|
|
95
|
+
const m = /^(Nullable|LowCardinality)\s*\(([\s\S]*)\)$/i.exec(t);
|
|
96
|
+
if (!m)
|
|
97
|
+
return t;
|
|
98
|
+
t = m[2].trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The canonical family of a column's type (`BIGINT UNSIGNED` is `UInt64`), or its leading word when unknown. */
|
|
102
|
+
export function typeFamily(type) {
|
|
103
|
+
const head = /^[A-Za-z_][A-Za-z0-9_ ]*?(?=\s*\(|$)/.exec(unwrapType(type))?.[0]?.trim() ?? "";
|
|
104
|
+
const table = TYPE_FAMILIES;
|
|
105
|
+
return (table[head] ?? table[head.toUpperCase()])?.canonical ?? head;
|
|
106
|
+
}
|
|
107
|
+
export const isNullableType = (type) => /^Nullable\s*\(/i.test((type ?? "").trim());
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest ClickHouse client chant needs: one SQL statement over the HTTP
|
|
3
|
+
* interface, rows back as JSON objects.
|
|
4
|
+
*
|
|
5
|
+
* No driver dependency. ClickHouse's HTTP interface takes the statement as the
|
|
6
|
+
* request body and `default_format=JSONEachRow` returns one JSON object per
|
|
7
|
+
* line, which is all generation (and, later, observation and import) reads.
|
|
8
|
+
*/
|
|
9
|
+
/** A query the server refused, with the server's own message. */
|
|
10
|
+
export class ClickHouseQueryError extends Error {
|
|
11
|
+
constructor(status, serverMessage, sql) {
|
|
12
|
+
super(`ClickHouse answered ${status}: ${serverMessage.trim().split("\n")[0]}`);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.serverMessage = serverMessage;
|
|
15
|
+
this.sql = sql;
|
|
16
|
+
this.name = "ClickHouseQueryError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Run one statement and return its rows. A statement that returns nothing yields `[]`. */
|
|
20
|
+
export async function clickhouseQuery(endpoint, sql, opts = {}) {
|
|
21
|
+
const headers = { "content-type": "text/plain; charset=utf-8" };
|
|
22
|
+
if (endpoint.user !== undefined)
|
|
23
|
+
headers["X-ClickHouse-User"] = endpoint.user;
|
|
24
|
+
const password = endpoint.token ? await endpoint.token.get() : endpoint.password;
|
|
25
|
+
if (password !== undefined)
|
|
26
|
+
headers["X-ClickHouse-Key"] = password;
|
|
27
|
+
const params = new URLSearchParams({ default_format: "JSONEachRow", ...(opts.queryId !== undefined ? { query_id: opts.queryId } : {}), ...opts.settings });
|
|
28
|
+
const res = await fetch(`${endpoint.url.replace(/\/$/, "")}/?${params.toString()}`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers,
|
|
31
|
+
body: sql,
|
|
32
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
33
|
+
});
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
// A refused token is not reused: the next request mints another.
|
|
37
|
+
if (endpoint.token && (res.status === 401 || res.status === 403 || /AUTHENTICATION_FAILED|Code: 516/.test(text)))
|
|
38
|
+
endpoint.token.invalidate();
|
|
39
|
+
throw new ClickHouseQueryError(res.status, text, sql);
|
|
40
|
+
}
|
|
41
|
+
return text
|
|
42
|
+
.split("\n")
|
|
43
|
+
.filter((line) => line.length > 0)
|
|
44
|
+
.map((line) => JSON.parse(line));
|
|
45
|
+
}
|
|
46
|
+
/** True when the server answers `/ping`. */
|
|
47
|
+
export async function clickhousePing(endpoint) {
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`${endpoint.url.replace(/\/$/, "")}/ping`);
|
|
50
|
+
return res.ok;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import IR as TypeScript: one `schema.ts` holding every imported
|
|
3
|
+
* object as a `database`, `table` or `view` declaration, in dependency order,
|
|
4
|
+
* with references between them interpolated. One file because the references
|
|
5
|
+
* cross databases freely, and declarations in one file reference each other
|
|
6
|
+
* without import cycles.
|
|
7
|
+
*/
|
|
8
|
+
import { relationKey } from "../../core/typed.js";
|
|
9
|
+
import { isTrivia, tokenizeText } from "../tokens.js";
|
|
10
|
+
import { parseCreate, unquote } from "../parser.js";
|
|
11
|
+
import { CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
|
|
12
|
+
export const CLICKHOUSE_TAG_OF = {
|
|
13
|
+
[CLICKHOUSE_ENTITY_TYPES.database]: "database",
|
|
14
|
+
[CLICKHOUSE_ENTITY_TYPES.table]: "table",
|
|
15
|
+
[CLICKHOUSE_ENTITY_TYPES.view]: "view",
|
|
16
|
+
[CLICKHOUSE_ENTITY_TYPES.materializedView]: "view",
|
|
17
|
+
[CLICKHOUSE_ENTITY_TYPES.dictionary]: "dictionary",
|
|
18
|
+
[CLICKHOUSE_ENTITY_TYPES.function]: "func",
|
|
19
|
+
// Read from `.sql` files (#3711).
|
|
20
|
+
[CLICKHOUSE_ENTITY_TYPES.user]: "user",
|
|
21
|
+
[CLICKHOUSE_ENTITY_TYPES.role]: "role",
|
|
22
|
+
[CLICKHOUSE_ENTITY_TYPES.rowPolicy]: "policy",
|
|
23
|
+
[CLICKHOUSE_ENTITY_TYPES.quota]: "quota",
|
|
24
|
+
[CLICKHOUSE_ENTITY_TYPES.settingsProfile]: "settingsProfile",
|
|
25
|
+
[CLICKHOUSE_ENTITY_TYPES.grant]: "grant",
|
|
26
|
+
};
|
|
27
|
+
/** A backquoted or double-quoted identifier, written bare when it can be and double-quoted otherwise. */
|
|
28
|
+
function identifierText(t) {
|
|
29
|
+
if (t.kind !== "qident")
|
|
30
|
+
return t.text;
|
|
31
|
+
const name = unquote(t.text);
|
|
32
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `"${name.replace(/"/g, '""')}"`;
|
|
33
|
+
}
|
|
34
|
+
/** Text that sits inside a template literal: a backquote and `${` escaped. */
|
|
35
|
+
const templateSafe = (s) => s.replace(/\\(?=`|\$\{)/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
36
|
+
/** Words after which a bare name is a relation: `FROM t`, `JOIN t`, a materialized view's `TO t`. */
|
|
37
|
+
const RELATION_WORDS = new Set(["FROM", "JOIN", "TO"]);
|
|
38
|
+
/**
|
|
39
|
+
* The template body for one statement, and the exports it references. A
|
|
40
|
+
* qualified name of another object becomes `${export}`, and so does a bare
|
|
41
|
+
* name after `FROM`, `JOIN` or `TO` that an unqualified object has; the
|
|
42
|
+
* database part of the statement's own name becomes the database's export.
|
|
43
|
+
*/
|
|
44
|
+
function templateBody(item, byQualified, dbExport) {
|
|
45
|
+
const tokens = tokenizeText(item.ddl, 0);
|
|
46
|
+
let node;
|
|
47
|
+
try {
|
|
48
|
+
node = parseCreate(tokens);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
node = undefined;
|
|
52
|
+
}
|
|
53
|
+
const own = node ? { from: node.name.from, to: node.name.to } : undefined;
|
|
54
|
+
const sig = [];
|
|
55
|
+
tokens.forEach((t, i) => {
|
|
56
|
+
if (!isTrivia(t))
|
|
57
|
+
sig.push(i);
|
|
58
|
+
});
|
|
59
|
+
const isName = (t) => t !== undefined && (t.kind === "ident" || t.kind === "qident");
|
|
60
|
+
const isSelf = (target) => target === item || target.exportName === item.exportName;
|
|
61
|
+
const replace = new Map();
|
|
62
|
+
for (let k = 0; k < sig.length; k++) {
|
|
63
|
+
const a = tokens[sig[k]];
|
|
64
|
+
const dot = tokens[sig[k + 1] ?? -1];
|
|
65
|
+
const b = tokens[sig[k + 2] ?? -1];
|
|
66
|
+
const prev = tokens[sig[k - 1] ?? -1];
|
|
67
|
+
if (prev?.kind === "punct" && prev.text === ".")
|
|
68
|
+
continue;
|
|
69
|
+
if (!isName(a) || dot?.kind !== "punct" || dot.text !== "." || !isName(b)) {
|
|
70
|
+
const inOwnName = own !== undefined && sig[k] >= own.from && sig[k] < own.to;
|
|
71
|
+
const bare = isName(a) && !inOwnName && prev?.kind === "ident" && RELATION_WORDS.has(prev.text.toUpperCase());
|
|
72
|
+
const target = bare ? byQualified.get(unquote(a.text)) : undefined;
|
|
73
|
+
if (target && !isSelf(target))
|
|
74
|
+
replace.set(sig[k], { to: sig[k] + 1, ref: target.exportName });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const db = unquote(a.text);
|
|
78
|
+
const name = unquote(b.text);
|
|
79
|
+
const inOwnName = own !== undefined && sig[k] >= own.from && sig[k] < own.to;
|
|
80
|
+
const target = byQualified.get(`${db}.${name}`);
|
|
81
|
+
if (!inOwnName && target && !isSelf(target)) {
|
|
82
|
+
replace.set(sig[k], { to: sig[k + 2] + 1, ref: target.exportName });
|
|
83
|
+
k += 2;
|
|
84
|
+
}
|
|
85
|
+
else if (inOwnName && dbExport.has(db)) {
|
|
86
|
+
replace.set(sig[k], { to: sig[k] + 1, ref: dbExport.get(db) });
|
|
87
|
+
k += 2;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const parts = [""];
|
|
91
|
+
const refs = [];
|
|
92
|
+
for (let i = 0; i < tokens.length;) {
|
|
93
|
+
const r = replace.get(i);
|
|
94
|
+
if (r) {
|
|
95
|
+
refs.push(r.ref);
|
|
96
|
+
parts.push("");
|
|
97
|
+
i = r.to;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const t = tokens[i];
|
|
101
|
+
parts[parts.length - 1] += t.kind === "qident" ? templateSafe(identifierText(t)) : templateSafe(t.text);
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
return { parts, refs };
|
|
105
|
+
}
|
|
106
|
+
/** A template body as the text between the backquotes. */
|
|
107
|
+
export const bodyText = (body) => body.parts.map((p, i) => (i === 0 ? p : `\${${body.refs[i - 1]}}${p}`)).join("");
|
|
108
|
+
/**
|
|
109
|
+
* The template of each object in `items`, with the references to `targets`
|
|
110
|
+
* (the items themselves, and any object declared elsewhere that they may
|
|
111
|
+
* name) interpolated, and the items in the order a file declares them: each
|
|
112
|
+
* after what it references.
|
|
113
|
+
*/
|
|
114
|
+
/** The kinds a `FROM`, `JOIN` or `TO` never names. */
|
|
115
|
+
const NOT_RELATIONS = new Set([
|
|
116
|
+
CLICKHOUSE_ENTITY_TYPES.function,
|
|
117
|
+
CLICKHOUSE_ENTITY_TYPES.user,
|
|
118
|
+
CLICKHOUSE_ENTITY_TYPES.role,
|
|
119
|
+
CLICKHOUSE_ENTITY_TYPES.rowPolicy,
|
|
120
|
+
CLICKHOUSE_ENTITY_TYPES.quota,
|
|
121
|
+
CLICKHOUSE_ENTITY_TYPES.settingsProfile,
|
|
122
|
+
CLICKHOUSE_ENTITY_TYPES.grant,
|
|
123
|
+
]);
|
|
124
|
+
export function clickhouseTemplates(items, targets = items) {
|
|
125
|
+
const byQualified = new Map();
|
|
126
|
+
const dbExport = new Map();
|
|
127
|
+
for (const it of targets) {
|
|
128
|
+
if (it.type === CLICKHOUSE_ENTITY_TYPES.database)
|
|
129
|
+
dbExport.set(it.name, it.exportName);
|
|
130
|
+
// A function is called, never named after FROM, JOIN or TO; access objects are never named so at all.
|
|
131
|
+
else if (!NOT_RELATIONS.has(it.type))
|
|
132
|
+
byQualified.set(it.database ? `${it.database}.${it.name}` : it.name, it);
|
|
133
|
+
}
|
|
134
|
+
const bodies = new Map(items.map((it) => [it.exportName, templateBody(it, byQualified, dbExport)]));
|
|
135
|
+
// Declarations reference each other in one file, so each comes after what it references.
|
|
136
|
+
const order = [];
|
|
137
|
+
const done = new Set();
|
|
138
|
+
const visiting = new Set();
|
|
139
|
+
const byExport = new Map(items.map((it) => [it.exportName, it]));
|
|
140
|
+
const visit = (it) => {
|
|
141
|
+
if (done.has(it.exportName) || visiting.has(it.exportName))
|
|
142
|
+
return;
|
|
143
|
+
visiting.add(it.exportName);
|
|
144
|
+
for (const ref of bodies.get(it.exportName).refs) {
|
|
145
|
+
const dep = byExport.get(ref);
|
|
146
|
+
if (dep)
|
|
147
|
+
visit(dep);
|
|
148
|
+
}
|
|
149
|
+
visiting.delete(it.exportName);
|
|
150
|
+
done.add(it.exportName);
|
|
151
|
+
order.push(it);
|
|
152
|
+
};
|
|
153
|
+
for (const it of items)
|
|
154
|
+
visit(it);
|
|
155
|
+
return { order, bodies };
|
|
156
|
+
}
|
|
157
|
+
function indent(body) {
|
|
158
|
+
const lines = body.split("\n");
|
|
159
|
+
return lines.map((l, i) => (i === 0 || l.length === 0 ? l : ` ${l}`)).join("\n");
|
|
160
|
+
}
|
|
161
|
+
const RELATIONS = new Set([CLICKHOUSE_ENTITY_TYPES.table, CLICKHOUSE_ENTITY_TYPES.view, CLICKHOUSE_ENTITY_TYPES.materializedView, CLICKHOUSE_ENTITY_TYPES.dictionary]);
|
|
162
|
+
/** A table's, view's or dictionary's qualified name as its tag's type argument (#3751), so its columns are typed once `chant sql types` has run. */
|
|
163
|
+
const typeArgument = (it) => (RELATIONS.has(it.type) ? `<${JSON.stringify(relationKey(it))}>` : "");
|
|
164
|
+
export class ClickHouseGenerator {
|
|
165
|
+
constructor(header) {
|
|
166
|
+
this.header = header;
|
|
167
|
+
this.ownsLayout = true;
|
|
168
|
+
}
|
|
169
|
+
generate(ir) {
|
|
170
|
+
const items = ir.resources
|
|
171
|
+
.filter((r) => CLICKHOUSE_TAG_OF[r.type])
|
|
172
|
+
.map((r) => ({
|
|
173
|
+
exportName: r.logicalId,
|
|
174
|
+
type: r.type,
|
|
175
|
+
database: r.properties.database,
|
|
176
|
+
name: String(r.properties.name),
|
|
177
|
+
ddl: String(r.properties.ddl),
|
|
178
|
+
}));
|
|
179
|
+
if (items.length === 0)
|
|
180
|
+
return [];
|
|
181
|
+
const { order, bodies } = clickhouseTemplates(items);
|
|
182
|
+
const tags = [...new Set(order.map((it) => CLICKHOUSE_TAG_OF[it.type]))].sort();
|
|
183
|
+
const lines = [];
|
|
184
|
+
if (this.header)
|
|
185
|
+
lines.push(...this.header.split("\n").map((l) => `// ${l}`), "");
|
|
186
|
+
lines.push(`import { ${tags.join(", ")} } from "@intentius/chant-lexicon-sql/clickhouse";`, "");
|
|
187
|
+
for (const it of order) {
|
|
188
|
+
lines.push(`export const ${it.exportName} = ${CLICKHOUSE_TAG_OF[it.type]}${typeArgument(it)}\`\n ${indent(bodyText(bodies.get(it.exportName)))}\`;`, "");
|
|
189
|
+
}
|
|
190
|
+
return [{ path: "schema.ts", content: `${lines.join("\n").trimEnd()}\n` }];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding a SQL user-defined function in what ClickHouse stored (#3745).
|
|
3
|
+
*
|
|
4
|
+
* ClickHouse does not keep a call to a SQL function in a view's query or a
|
|
5
|
+
* column's DEFAULT: it writes the function's body in place of the call, with
|
|
6
|
+
* the arguments substituted, and a nested call expanded the same way.
|
|
7
|
+
* `CREATE FUNCTION tax AS x -> (x * 0.2)` used as `tax(amt)` reads back as
|
|
8
|
+
* `amt * 0.2`. So an import can't look for the name; it looks for the body.
|
|
9
|
+
*
|
|
10
|
+
* Each function's body is expanded (a call to another function becomes that
|
|
11
|
+
* function's body), its parameters become wildcards, and the result is
|
|
12
|
+
* matched against the stored statement's tokens. Parentheses that only
|
|
13
|
+
* group are dropped on both sides, since the server adds or leaves them by
|
|
14
|
+
* precedence where the body lands. A wildcard matches one balanced
|
|
15
|
+
* expression, and a parameter used twice must match the same text twice.
|
|
16
|
+
*
|
|
17
|
+
* A function whose body is just a parameter (`x -> x`) leaves nothing to
|
|
18
|
+
* look for, so it is never found this way.
|
|
19
|
+
*/
|
|
20
|
+
import { isTrivia, tokenizeText } from "../tokens.js";
|
|
21
|
+
import { unquote } from "../parser.js";
|
|
22
|
+
/** A function's parameters and body, from `CREATE FUNCTION name AS (x, y) -> body`; undefined when it doesn't read that way. */
|
|
23
|
+
export function functionLambda(statement) {
|
|
24
|
+
let tokens;
|
|
25
|
+
try {
|
|
26
|
+
tokens = tokenizeText(statement, 0).filter((t) => !isTrivia(t));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const as = tokens.findIndex((t) => t.kind === "ident" && t.text.toUpperCase() === "AS");
|
|
32
|
+
const arrow = tokens.findIndex((t, i) => i > as && t.kind === "op" && t.text === "->");
|
|
33
|
+
if (as < 0 || arrow < 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
const params = tokens
|
|
36
|
+
.slice(as + 1, arrow)
|
|
37
|
+
.filter((t) => t.kind === "ident" || t.kind === "qident")
|
|
38
|
+
.map(nameOf);
|
|
39
|
+
const body = tokens.slice(arrow + 1);
|
|
40
|
+
while (body.length > 0 && body[body.length - 1].kind === "punct" && body[body.length - 1].text === ";")
|
|
41
|
+
body.pop();
|
|
42
|
+
return { params, body };
|
|
43
|
+
}
|
|
44
|
+
const nameOf = (t) => (t.kind === "qident" ? unquote(t.text) : t.text);
|
|
45
|
+
const isPunct = (t, text) => t?.kind === "punct" && t.text === text;
|
|
46
|
+
const isName = (t) => t?.kind === "ident" || t?.kind === "qident";
|
|
47
|
+
/** Words a `(` can follow that make it a grouping, not a call. */
|
|
48
|
+
const GROUPING_AFTER = new Set(["AND", "OR", "NOT", "IS", "LIKE", "ILIKE", "BETWEEN", "WHEN", "THEN", "ELSE", "CASE", "SELECT", "WHERE", "PREWHERE", "HAVING", "FROM", "ON", "AS", "BY", "DEFAULT", "MATERIALIZED", "ALIAS", "EPHEMERAL", "TTL", "INTERVAL", "DISTINCT", "ALL"]);
|
|
49
|
+
/** Words a wildcard never spans: they start the next clause or column attribute. */
|
|
50
|
+
const CLAUSE = new Set(["SELECT", "FROM", "WHERE", "PREWHERE", "GROUP", "ORDER", "HAVING", "LIMIT", "AS", "ENGINE", "SETTINGS", "DEFAULT", "MATERIALIZED", "ALIAS", "EPHEMERAL", "CODEC", "TTL", "COMMENT", "JOIN", "ON", "USING", "UNION", "WITH"]);
|
|
51
|
+
/** The tokens with grouping parentheses taken out; a call's parentheses stay. Trivia is assumed gone. */
|
|
52
|
+
function ungroup(tokens) {
|
|
53
|
+
const out = [];
|
|
54
|
+
const grouping = [];
|
|
55
|
+
tokens.forEach((t, i) => {
|
|
56
|
+
if (isPunct(t, "(")) {
|
|
57
|
+
const prev = tokens[i - 1];
|
|
58
|
+
const call = (isName(prev) && !(prev.kind === "ident" && GROUPING_AFTER.has(prev.text.toUpperCase()))) || isPunct(prev, ")");
|
|
59
|
+
grouping.push(!call);
|
|
60
|
+
if (call)
|
|
61
|
+
out.push(t);
|
|
62
|
+
}
|
|
63
|
+
else if (isPunct(t, ")")) {
|
|
64
|
+
if (grouping.pop() === false)
|
|
65
|
+
out.push(t);
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
out.push(t);
|
|
69
|
+
});
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** The index past the `)` matching the `(` at `open`, and the arguments between, split at top-level commas. */
|
|
73
|
+
function callArgs(tokens, open) {
|
|
74
|
+
const args = [[]];
|
|
75
|
+
let depth = 0;
|
|
76
|
+
for (let i = open + 1; i < tokens.length; i++) {
|
|
77
|
+
const t = tokens[i];
|
|
78
|
+
if (isPunct(t, "("))
|
|
79
|
+
depth++;
|
|
80
|
+
else if (isPunct(t, ")")) {
|
|
81
|
+
if (depth === 0)
|
|
82
|
+
return { args: args.length === 1 && args[0].length === 0 ? [] : args, end: i + 1 };
|
|
83
|
+
depth--;
|
|
84
|
+
}
|
|
85
|
+
else if (depth === 0 && isPunct(t, ",")) {
|
|
86
|
+
args.push([]);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
args[args.length - 1].push(t);
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/** A placeholder token for parameter `n`, which can't collide with anything a statement holds. */
|
|
94
|
+
const placeholder = (n) => ({ kind: "ident", text: `\u0000${n}`, part: 0, start: 0, end: 0 });
|
|
95
|
+
/**
|
|
96
|
+
* `tokens` with `params` replaced by their arguments' tokens and each call to
|
|
97
|
+
* a function in `lambdas` replaced by that function's body, recursively.
|
|
98
|
+
*/
|
|
99
|
+
function expand(tokens, params, lambdas, seen) {
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
102
|
+
const t = tokens[i];
|
|
103
|
+
if (isName(t)) {
|
|
104
|
+
const name = nameOf(t);
|
|
105
|
+
const callee = lambdas.get(name);
|
|
106
|
+
if (callee && isPunct(tokens[i + 1], "(") && !seen.has(name)) {
|
|
107
|
+
const call = callArgs(tokens, i + 1);
|
|
108
|
+
if (call && call.args.length === callee.params.length) {
|
|
109
|
+
const bound = new Map(callee.params.map((p, n) => [p, expand(call.args[n], params, lambdas, seen)]));
|
|
110
|
+
out.push(...expand(callee.body, bound, lambdas, new Set([...seen, name])));
|
|
111
|
+
i = call.end - 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const arg = params.get(name);
|
|
116
|
+
// A parameter is a bare name: `x`, not the `x` in `t.x` or a call `x(...)`.
|
|
117
|
+
if (arg && !isPunct(tokens[i - 1], ".") && !isPunct(tokens[i + 1], "(")) {
|
|
118
|
+
out.push(...arg);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
out.push(t);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** What to look for: the function's expanded body, with its parameters as wildcards. Undefined when nothing but a parameter is left to match. */
|
|
127
|
+
export function inlinedPattern(name, lambdas) {
|
|
128
|
+
const own = lambdas.get(name);
|
|
129
|
+
if (!own)
|
|
130
|
+
return undefined;
|
|
131
|
+
const params = new Map(own.params.map((p, n) => [p, [placeholder(n)]]));
|
|
132
|
+
const tokens = ungroup(expand(own.body, params, lambdas, new Set([name])));
|
|
133
|
+
const items = tokens.map((t) => (t.text.startsWith("\u0000") ? { arg: Number(t.text.slice(1)) } : { lit: t.text }));
|
|
134
|
+
return items.some((i) => "lit" in i) ? items : undefined;
|
|
135
|
+
}
|
|
136
|
+
/** A budget on matching steps per statement, so a pathological statement can't stall an import. */
|
|
137
|
+
const STEP_LIMIT = 200000;
|
|
138
|
+
/** Whether `pattern` appears in `statement`'s tokens. */
|
|
139
|
+
export function containsInlined(statement, pattern) {
|
|
140
|
+
let raw;
|
|
141
|
+
try {
|
|
142
|
+
raw = tokenizeText(statement, 0).filter((t) => !isTrivia(t));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const text = ungroup(raw).map((t) => t.text);
|
|
148
|
+
const lits = pattern.flatMap((i) => ("lit" in i ? [i.lit] : []));
|
|
149
|
+
const present = new Set(text);
|
|
150
|
+
if (!lits.every((l) => present.has(l)))
|
|
151
|
+
return false;
|
|
152
|
+
let steps = 0;
|
|
153
|
+
const match = (p, at, bound) => {
|
|
154
|
+
if (++steps > STEP_LIMIT)
|
|
155
|
+
return false;
|
|
156
|
+
if (p === pattern.length)
|
|
157
|
+
return true;
|
|
158
|
+
const item = pattern[p];
|
|
159
|
+
if ("lit" in item)
|
|
160
|
+
return text[at] === item.lit && match(p + 1, at + 1, bound);
|
|
161
|
+
const known = bound.get(item.arg);
|
|
162
|
+
if (known)
|
|
163
|
+
return known.every((s, k) => text[at + k] === s) && match(p + 1, at + known.length, bound);
|
|
164
|
+
// One balanced expression: never past a top-level comma, an unmatched `)`, or the next clause.
|
|
165
|
+
let depth = 0;
|
|
166
|
+
for (let end = at; end < text.length; end++) {
|
|
167
|
+
const s = text[end];
|
|
168
|
+
if (depth === 0 && (s === "," || s === ";" || s === ")" || CLAUSE.has(s.toUpperCase())))
|
|
169
|
+
return false;
|
|
170
|
+
if (s === "(")
|
|
171
|
+
depth++;
|
|
172
|
+
else if (s === ")")
|
|
173
|
+
depth--;
|
|
174
|
+
if (depth > 0)
|
|
175
|
+
continue;
|
|
176
|
+
bound.set(item.arg, text.slice(at, end + 1));
|
|
177
|
+
if (match(p + 1, end + 1, bound))
|
|
178
|
+
return true;
|
|
179
|
+
bound.delete(item.arg);
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
};
|
|
183
|
+
for (let at = 0; at < text.length; at++)
|
|
184
|
+
if (match(0, at, new Map()))
|
|
185
|
+
return true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The functions among `functions` whose bodies appear inlined in any of
|
|
190
|
+
* `statements`. A function is matched with the others' bodies expanded into
|
|
191
|
+
* its own, as the server does.
|
|
192
|
+
*/
|
|
193
|
+
export function inlinedFunctions(statements, functions) {
|
|
194
|
+
const lambdas = new Map();
|
|
195
|
+
for (const f of functions) {
|
|
196
|
+
const l = functionLambda(f.statement);
|
|
197
|
+
if (l)
|
|
198
|
+
lambdas.set(f.name, l);
|
|
199
|
+
}
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const f of functions) {
|
|
202
|
+
const pattern = inlinedPattern(f.name, lambdas);
|
|
203
|
+
if (pattern && statements.some((s) => containsInlined(s, pattern)))
|
|
204
|
+
out.push(f.name);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|