@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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickHouse DDL into the import IR, and back out as `database`, `table` and
|
|
3
|
+
* `view` declarations.
|
|
4
|
+
*
|
|
5
|
+
* `chant import --from <env>` reads each object's `SHOW CREATE` and `chant
|
|
6
|
+
* import schema.sql` reads a file of CREATE statements; both arrive here as
|
|
7
|
+
* statements. The generated declaration is the statement itself, the
|
|
8
|
+
* server's canonical form when it came from a server (#3046 question 5), with
|
|
9
|
+
* two changes:
|
|
10
|
+
*
|
|
11
|
+
* - a qualified name of another imported object becomes a reference to its
|
|
12
|
+
* export (`FROM analytics.events` is `FROM ${events}`), and the database
|
|
13
|
+
* part of an object's own name becomes a reference to the database's export,
|
|
14
|
+
* so dependency order survives the import;
|
|
15
|
+
* - a backquoted identifier that needs no quoting is written bare, and one that
|
|
16
|
+
* does is double-quoted, so the template has no backquotes to escape.
|
|
17
|
+
*
|
|
18
|
+
* Columns written by name inside a view's SELECT stay text: which table a
|
|
19
|
+
* bare name belongs to is the server's to resolve, and a guess would be
|
|
20
|
+
* wrong lineage. An imported view therefore records its reads, not its column
|
|
21
|
+
* lineage, until its column references are written as `${t.columns.c}`.
|
|
22
|
+
*/
|
|
23
|
+
import { isTrivia, tokenizeText } from "../tokens.js";
|
|
24
|
+
import { parseCreate, unquote } from "../parser.js";
|
|
25
|
+
import { CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
|
|
26
|
+
import { MERGE_TREE_SETTINGS } from "../../generated/clickhouse.js";
|
|
27
|
+
const JS_RESERVED = new Set("break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "));
|
|
28
|
+
function camel(name) {
|
|
29
|
+
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
30
|
+
if (words.length === 0)
|
|
31
|
+
return "object";
|
|
32
|
+
const s = words.map((w, i) => (i === 0 ? w[0].toLowerCase() + w.slice(1) : w[0].toUpperCase() + w.slice(1))).join("");
|
|
33
|
+
return /^[0-9]/.test(s) ? `t${s}` : s;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Export names for a set of objects. A table or view is its name in camel
|
|
37
|
+
* case, prefixed with its database when two databases share a name; a
|
|
38
|
+
* database is its name with `Db` after it, so `analytics` the database and
|
|
39
|
+
* `analytics` a table do not collide.
|
|
40
|
+
*/
|
|
41
|
+
export function exportNames(objects) {
|
|
42
|
+
const relations = objects.filter((o) => o.type !== CLICKHOUSE_ENTITY_TYPES.database);
|
|
43
|
+
const count = new Map();
|
|
44
|
+
for (const o of relations)
|
|
45
|
+
count.set(o.name, (count.get(o.name) ?? 0) + 1);
|
|
46
|
+
const used = new Set();
|
|
47
|
+
return objects.map((o) => {
|
|
48
|
+
let base = o.type === CLICKHOUSE_ENTITY_TYPES.database
|
|
49
|
+
? `${camel(o.name)}Db`
|
|
50
|
+
: (count.get(o.name) ?? 0) > 1 && o.database
|
|
51
|
+
? camel(`${o.database}_${o.name}`)
|
|
52
|
+
: camel(o.name);
|
|
53
|
+
if (JS_RESERVED.has(base))
|
|
54
|
+
base = `${base}Table`;
|
|
55
|
+
let name = base;
|
|
56
|
+
for (let i = 2; used.has(name); i++)
|
|
57
|
+
name = `${base}${i}`;
|
|
58
|
+
used.add(name);
|
|
59
|
+
return name;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Split a file of statements on top-level `;`. */
|
|
63
|
+
export function splitStatements(text) {
|
|
64
|
+
const out = [];
|
|
65
|
+
let current = [];
|
|
66
|
+
const flush = () => {
|
|
67
|
+
const stmt = current.map((t) => t.text).join("").trim();
|
|
68
|
+
if (current.some((t) => !isTrivia(t)))
|
|
69
|
+
out.push(stmt);
|
|
70
|
+
current = [];
|
|
71
|
+
};
|
|
72
|
+
for (const t of tokenizeText(text, 0)) {
|
|
73
|
+
if (t.kind === "punct" && t.text === ";")
|
|
74
|
+
flush();
|
|
75
|
+
else
|
|
76
|
+
current.push(t);
|
|
77
|
+
}
|
|
78
|
+
flush();
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
const statementType = (node) => node.statement === "database"
|
|
82
|
+
? CLICKHOUSE_ENTITY_TYPES.database
|
|
83
|
+
: node.statement === "table"
|
|
84
|
+
? CLICKHOUSE_ENTITY_TYPES.table
|
|
85
|
+
: node.statement === "dictionary"
|
|
86
|
+
? CLICKHOUSE_ENTITY_TYPES.dictionary
|
|
87
|
+
: node.statement === "function"
|
|
88
|
+
? CLICKHOUSE_ENTITY_TYPES.function
|
|
89
|
+
: node.materialized
|
|
90
|
+
? CLICKHOUSE_ENTITY_TYPES.materializedView
|
|
91
|
+
: CLICKHOUSE_ENTITY_TYPES.view;
|
|
92
|
+
function nameOf(tokens, span) {
|
|
93
|
+
const parts = tokens
|
|
94
|
+
.slice(span.from, span.to)
|
|
95
|
+
.filter((t) => !isTrivia(t) && !(t.kind === "punct" && t.text === "."))
|
|
96
|
+
.map((t) => unquote(t.text));
|
|
97
|
+
return parts.length >= 2 ? { database: parts[parts.length - 2], name: parts[parts.length - 1] } : { name: parts[0] ?? "" };
|
|
98
|
+
}
|
|
99
|
+
/** Read a statement's kind and name. Throws the parser's error when it does not parse. */
|
|
100
|
+
export function describeStatement(ddl) {
|
|
101
|
+
const tokens = tokenizeText(ddl, 0);
|
|
102
|
+
const node = parseCreate(tokens);
|
|
103
|
+
return { type: statementType(node), ...nameOf(tokens, node.name), ddl };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Leave out what the server adds and the declaration would not say: settings
|
|
107
|
+
* at the pinned server's default (`index_granularity = 8192`), and a view's
|
|
108
|
+
* column list, which the server infers from the SELECT. A server re-adds both
|
|
109
|
+
* on create, so the stripped statement creates the same object.
|
|
110
|
+
*/
|
|
111
|
+
export function stripServerDefaults(ddl) {
|
|
112
|
+
const tokens = tokenizeText(ddl, 0);
|
|
113
|
+
let node;
|
|
114
|
+
try {
|
|
115
|
+
node = parseCreate(tokens);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return ddl;
|
|
119
|
+
}
|
|
120
|
+
const edits = [];
|
|
121
|
+
if ((node.statement === "table" || node.statement === "view") && node.settings && node.settingsClause) {
|
|
122
|
+
const text = (s) => tokens.slice(s.from, s.to).map((t) => t.text).join("").trim();
|
|
123
|
+
const kept = node.settings.filter((s) => {
|
|
124
|
+
const def = MERGE_TREE_SETTINGS[s.key];
|
|
125
|
+
return !def || text(s.value).replace(/^'|'$/g, "") !== def.default;
|
|
126
|
+
});
|
|
127
|
+
if (kept.length !== node.settings.length) {
|
|
128
|
+
edits.push({
|
|
129
|
+
span: node.settingsClause,
|
|
130
|
+
text: kept.length === 0 ? "" : `SETTINGS ${kept.map((s) => `${s.key} = ${text(s.value)}`).join(", ")}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (node.statement === "view" && node.columnsSpan)
|
|
135
|
+
edits.push({ span: node.columnsSpan, text: "" });
|
|
136
|
+
if (edits.length === 0)
|
|
137
|
+
return ddl;
|
|
138
|
+
edits.sort((a, b) => b.span.from - a.span.from);
|
|
139
|
+
const out = [...tokens];
|
|
140
|
+
for (const e of edits) {
|
|
141
|
+
out.splice(e.span.from, e.span.to - e.span.from, { kind: "ws", text: e.text, part: 0, start: 0, end: 0 });
|
|
142
|
+
}
|
|
143
|
+
return out
|
|
144
|
+
.map((t) => t.text)
|
|
145
|
+
.join("")
|
|
146
|
+
.split("\n")
|
|
147
|
+
.filter((line) => line.trim().length > 0)
|
|
148
|
+
.join("\n")
|
|
149
|
+
.replace(/[ \t]+$/gm, "");
|
|
150
|
+
}
|
|
151
|
+
/** The IR for a set of objects. */
|
|
152
|
+
export function objectsToIR(objects, warnings = []) {
|
|
153
|
+
const names = exportNames(objects);
|
|
154
|
+
const resources = objects.map((o, i) => ({
|
|
155
|
+
logicalId: names[i],
|
|
156
|
+
type: o.type,
|
|
157
|
+
properties: { ...(o.database ? { database: o.database } : {}), name: o.name, ddl: o.ddl },
|
|
158
|
+
}));
|
|
159
|
+
return { resources, parameters: [], ...(warnings.length ? { warnings } : {}) };
|
|
160
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live export: `chant import --from <env>` writes the server's schema as
|
|
3
|
+
* `database`, `table` and `view` declarations.
|
|
4
|
+
*
|
|
5
|
+
* Every object in the environment's databases is read with `SHOW CREATE`, the
|
|
6
|
+
* server's canonical form, which is the baseline #3046 question 5 asks for.
|
|
7
|
+
* By default what the server adds on its own is left out (settings at their
|
|
8
|
+
* default, a view's inferred column list), so the declaration says what an
|
|
9
|
+
* author would; `verbatim` keeps the statement as the server printed it.
|
|
10
|
+
*
|
|
11
|
+
* - `selector.type` is an entity type (`ClickHouse::Table`); `selector.name` is
|
|
12
|
+
* an object name, `db.name` or `name`.
|
|
13
|
+
* - `owned` keeps the objects whose comment carries chant's ownership marker
|
|
14
|
+
* (`../ownership.ts`), the trailer the applier stamps. The trailer itself is
|
|
15
|
+
* never written into a declaration: `readLiveSchema` takes it off.
|
|
16
|
+
* - a migration runner's history table (`schema_migrations`,
|
|
17
|
+
* `goose_db_version`) is left out with a warning (#3676), as on Postgres.
|
|
18
|
+
* - A SQL user-defined function belongs to no database, and every project's
|
|
19
|
+
* functions sit side by side on a server (#3718). Without a selector, an
|
|
20
|
+
* import adopts only the functions the imported objects use (and those
|
|
21
|
+
* functions call), and the ones `sql.profiles.<env>.importFunctions` names;
|
|
22
|
+
* a warning names the rest. ClickHouse stores a call as the function's body
|
|
23
|
+
* (#3745), so a use is found by that body (`./inlined.ts`) as well as by
|
|
24
|
+
* the name. A function carries no comment, so `owned` does
|
|
25
|
+
* not apply to it.
|
|
26
|
+
* - An object of a kind no declaration holds (a window view, a quota, a
|
|
27
|
+
* settings profile, a named collection, ...; `../live/undeclarable.ts`)
|
|
28
|
+
* fails the import, which names every one of them (#3911).
|
|
29
|
+
* `skipUndeclarable` (`--skip-undeclarable`) imports the rest and lists
|
|
30
|
+
* them as warnings. A selector or `owned` import asks for objects chant can
|
|
31
|
+
* hold, so it does not look.
|
|
32
|
+
*/
|
|
33
|
+
import { bindClickHouse } from "../live/bind.js";
|
|
34
|
+
import { readLiveSchema } from "../live/catalog.js";
|
|
35
|
+
import { objectsToIR, stripServerDefaults } from "./ir.js";
|
|
36
|
+
import { isChantManaged } from "../ownership.js";
|
|
37
|
+
import { CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
|
|
38
|
+
import { isTrivia, tokenizeText } from "../tokens.js";
|
|
39
|
+
import { unquote } from "../parser.js";
|
|
40
|
+
import { inlinedFunctions } from "./inlined.js";
|
|
41
|
+
import { readUndeclarable } from "../live/undeclarable.js";
|
|
42
|
+
import { refuseUndeclarable } from "../../core/undeclarable.js";
|
|
43
|
+
/** The functions among `names` a statement calls: a name followed by `(`. */
|
|
44
|
+
export function calledFunctions(ddl, names) {
|
|
45
|
+
const tokens = tokenizeText(ddl, 0).filter((t) => !isTrivia(t));
|
|
46
|
+
const out = new Set();
|
|
47
|
+
tokens.forEach((t, i) => {
|
|
48
|
+
if (t.kind !== "ident" && t.kind !== "qident")
|
|
49
|
+
return;
|
|
50
|
+
const next = tokens[i + 1];
|
|
51
|
+
if (next?.kind !== "punct" || next.text !== "(")
|
|
52
|
+
return;
|
|
53
|
+
const name = t.kind === "qident" ? unquote(t.text) : t.text;
|
|
54
|
+
if (names.has(name))
|
|
55
|
+
out.add(name);
|
|
56
|
+
});
|
|
57
|
+
return [...out];
|
|
58
|
+
}
|
|
59
|
+
/** Whether `patterns` (names, or prefixes ending in `*`) name a function. */
|
|
60
|
+
const named = (patterns, name) => patterns.some((p) => (p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : p === name));
|
|
61
|
+
/**
|
|
62
|
+
* The functions an import adopts (#3718): those the objects call, those
|
|
63
|
+
* whose bodies the objects hold in place of a call (#3745, `./inlined.ts`),
|
|
64
|
+
* those `patterns` name, and the functions those call in turn; and the rest.
|
|
65
|
+
*/
|
|
66
|
+
export function scopedFunctions(objects, functions, patterns = []) {
|
|
67
|
+
const byName = new Map(functions.map((f) => [f.name, f]));
|
|
68
|
+
const names = new Set(byName.keys());
|
|
69
|
+
const adopted = new Set(functions.filter((f) => named(patterns, f.name)).map((f) => f.name));
|
|
70
|
+
for (const o of objects)
|
|
71
|
+
for (const n of calledFunctions(o.statement, names))
|
|
72
|
+
adopted.add(n);
|
|
73
|
+
for (const n of inlinedFunctions(objects.map((o) => o.statement), functions))
|
|
74
|
+
adopted.add(n);
|
|
75
|
+
const queue = [...adopted];
|
|
76
|
+
while (queue.length > 0) {
|
|
77
|
+
for (const n of calledFunctions(byName.get(queue.pop()).statement, names)) {
|
|
78
|
+
if (!adopted.has(n)) {
|
|
79
|
+
adopted.add(n);
|
|
80
|
+
queue.push(n);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { adopted: functions.filter((f) => adopted.has(f.name)), skipped: functions.filter((f) => !adopted.has(f.name)).map((f) => f.name) };
|
|
85
|
+
}
|
|
86
|
+
export async function exportResources(options) {
|
|
87
|
+
const target = await bindClickHouse(options);
|
|
88
|
+
// A selector or an owned import asks only for what chant can hold.
|
|
89
|
+
const undeclarable = !options.selector && !options.owned ? await readUndeclarable(target) : { objects: [], unread: [] };
|
|
90
|
+
const warnings = undeclarable.unread.map((u) => `the server was not checked for objects of one kind chant cannot declare, so some may be left out: ${u}`);
|
|
91
|
+
refuseUndeclarable("clickhouse", undeclarable.objects, options.skipUndeclarable, warnings);
|
|
92
|
+
// What is refused, and with --skip-undeclarable left out, is not also read as a table (a window view is in system.tables).
|
|
93
|
+
const refused = new Set(undeclarable.objects.filter((o) => o.schema !== undefined).map((o) => `${o.schema}.${o.name}`));
|
|
94
|
+
const all = (await readLiveSchema(target)).filter((o) => !(o.database !== undefined && refused.has(`${o.database}.${o.name}`)));
|
|
95
|
+
const isFunction = (o) => o.type === CLICKHOUSE_ENTITY_TYPES.function;
|
|
96
|
+
// A selector picks what it names, functions included; without one, functions go with what calls them (#3718).
|
|
97
|
+
const live = options.selector ? all : all.filter((o) => !isFunction(o));
|
|
98
|
+
const selected = live.filter((o) => {
|
|
99
|
+
if (options.owned && !isChantManaged(o.comment))
|
|
100
|
+
return false;
|
|
101
|
+
if (options.selector?.type && o.type !== options.selector.type)
|
|
102
|
+
return false;
|
|
103
|
+
const name = options.selector?.name;
|
|
104
|
+
if (name !== undefined && name !== o.name && name !== `${o.database}.${o.name}`)
|
|
105
|
+
return false;
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
if (!options.selector) {
|
|
109
|
+
const { adopted, skipped } = scopedFunctions(selected.filter((o) => !o.foreign), all.filter(isFunction), target.importFunctions);
|
|
110
|
+
selected.push(...adopted);
|
|
111
|
+
if (skipped.length > 0) {
|
|
112
|
+
warnings.push(`${skipped.length} SQL function${skipped.length === 1 ? "" : "s"} on the server ${skipped.length === 1 ? "is" : "are"} not imported, since no imported object uses ${skipped.length === 1 ? "it" : "them"}: ${skipped.join(", ")}. Name ${skipped.length === 1 ? "it" : "them"} in sql.profiles.${options.environment}.importFunctions to import ${skipped.length === 1 ? "it" : "them"}.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const objects = selected
|
|
116
|
+
// The `default` database exists on every server; declaring it would make apply create what is there.
|
|
117
|
+
.filter((o) => !(o.type === "ClickHouse::Database" && o.name === "default"))
|
|
118
|
+
.filter((o) => {
|
|
119
|
+
if (!o.foreign)
|
|
120
|
+
return true;
|
|
121
|
+
warnings.push(`${o.database ? `${o.database}.` : ""}${o.name} is kept by ${o.foreign}; left out, since declaring it would have chant change what that tool owns`);
|
|
122
|
+
return false;
|
|
123
|
+
})
|
|
124
|
+
.map((o) => ({
|
|
125
|
+
type: o.type,
|
|
126
|
+
...(o.database ? { database: o.database } : {}),
|
|
127
|
+
name: o.name,
|
|
128
|
+
ddl: options.verbatim ? o.statement : stripServerDefaults(o.statement),
|
|
129
|
+
}));
|
|
130
|
+
return objectsToIR(objects, warnings);
|
|
131
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant import schema.sql` for ClickHouse: a file of CREATE statements into
|
|
3
|
+
* the import IR. A statement that is not a CREATE, or does not parse, is an
|
|
4
|
+
* error naming it (`../../files/clickhouse.ts`); none is left out.
|
|
5
|
+
*/
|
|
6
|
+
import { objectsToIR } from "./ir.js";
|
|
7
|
+
import { readClickHouse } from "../../files/clickhouse.js";
|
|
8
|
+
import { SqlFileError } from "../../files/common.js";
|
|
9
|
+
export class ClickHouseSqlParser {
|
|
10
|
+
parse(content) {
|
|
11
|
+
const problems = [];
|
|
12
|
+
const objects = readClickHouse(content, { origin: "the DDL" }, problems);
|
|
13
|
+
if (problems.length > 0)
|
|
14
|
+
throw new SqlFileError("the DDL", problems);
|
|
15
|
+
return objectsToIR(objects);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which ClickHouse server a chant environment reads: the config lookup and
|
|
3
|
+
* the binding, shared by observation and live export so both read the same
|
|
4
|
+
* server.
|
|
5
|
+
*/
|
|
6
|
+
import { ClickHouseQueryError, clickhouseQuery } from "../http.js";
|
|
7
|
+
import { parseTopology, toTopology } from "../topology.js";
|
|
8
|
+
import { credentialSource, POSTGRES_ONLY_TOKEN_SOURCES, TokenSourceError } from "../../token-source.js";
|
|
9
|
+
export const isUnresolvedTarget = (v) => "reason" in v;
|
|
10
|
+
/**
|
|
11
|
+
* The server an environment reads. Pure: `config` and `env` are passed in.
|
|
12
|
+
*
|
|
13
|
+
* 1. `sql.profiles.<environment>`. A profile naming a credential variable that
|
|
14
|
+
* is not set is `no-credentials`: the profile said how to authenticate.
|
|
15
|
+
* 2. `CLICKHOUSE_URL`, with `CLICKHOUSE_USER` and `CLICKHOUSE_PASSWORD` when set,
|
|
16
|
+
* and `CLICKHOUSE_TOPOLOGY` (`single`, `cluster:<name>`, `replicated`,
|
|
17
|
+
* `cloud`).
|
|
18
|
+
* 3. Otherwise `no-binding`.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveClickHouseTarget(input) {
|
|
21
|
+
const env = input.env ?? process.env;
|
|
22
|
+
const profile = input.environment !== undefined ? input.config?.sql?.profiles?.[input.environment] : undefined;
|
|
23
|
+
if (profile) {
|
|
24
|
+
const source = `sql.profiles.${input.environment}`;
|
|
25
|
+
const endpoint = { url: profile.url.replace(/\/+$/, "") };
|
|
26
|
+
if (profile.user) {
|
|
27
|
+
const value = env[profile.user.env];
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return { reason: "no-credentials", detail: `${source}.user names ${profile.user.env}, which is not set` };
|
|
30
|
+
endpoint.user = value;
|
|
31
|
+
}
|
|
32
|
+
const password = profile.password;
|
|
33
|
+
if (password && "env" in password) {
|
|
34
|
+
const value = env[password.env];
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return { reason: "no-credentials", detail: `${source}.password names ${password.env}, which is not set` };
|
|
37
|
+
endpoint.password = value;
|
|
38
|
+
}
|
|
39
|
+
else if (password) {
|
|
40
|
+
if (POSTGRES_ONLY_TOKEN_SOURCES.has(password.token)) {
|
|
41
|
+
return { reason: "no-credentials", detail: `${source}.password is a ${password.token} token, which only a Postgres server takes; use a command source for ClickHouse` };
|
|
42
|
+
}
|
|
43
|
+
endpoint.token = credentialSource(password, { url: profile.url, ...(endpoint.user !== undefined ? { user: endpoint.user } : {}), env, source: `${source}.password` });
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
endpoint,
|
|
47
|
+
source,
|
|
48
|
+
...(profile.databases ? { databases: profile.databases } : {}),
|
|
49
|
+
defaultDatabase: profile.defaultDatabase ?? "default",
|
|
50
|
+
...(profile.topology !== undefined ? { topology: toTopology(profile.topology) } : {}),
|
|
51
|
+
...(profile.access === true ? { access: true } : {}),
|
|
52
|
+
...(profile.importFunctions ? { importFunctions: profile.importFunctions } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const url = env.CLICKHOUSE_URL;
|
|
56
|
+
if (!url) {
|
|
57
|
+
const where = input.environment !== undefined ? `sql.profiles.${input.environment} is not declared and ` : "";
|
|
58
|
+
return { reason: "no-binding", detail: `${where}CLICKHOUSE_URL is not set, so there is no ClickHouse server to read` };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
endpoint: {
|
|
62
|
+
url: url.replace(/\/+$/, ""),
|
|
63
|
+
...(env.CLICKHOUSE_USER !== undefined ? { user: env.CLICKHOUSE_USER } : {}),
|
|
64
|
+
...(env.CLICKHOUSE_PASSWORD !== undefined ? { password: env.CLICKHOUSE_PASSWORD } : {}),
|
|
65
|
+
},
|
|
66
|
+
source: "env CLICKHOUSE_URL",
|
|
67
|
+
defaultDatabase: "default",
|
|
68
|
+
...(env.CLICKHOUSE_TOPOLOGY ? { topology: parseTopology(env.CLICKHOUSE_TOPOLOGY) } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A target that could not be resolved. */
|
|
72
|
+
export class ClickHouseBindingError extends Error {
|
|
73
|
+
constructor(unresolved) {
|
|
74
|
+
super(unresolved.detail);
|
|
75
|
+
this.unresolved = unresolved;
|
|
76
|
+
this.name = "ClickHouseBindingError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function loadConfig(cwd) {
|
|
80
|
+
try {
|
|
81
|
+
const { loadChantConfig } = await import("@intentius/chant/config");
|
|
82
|
+
return (await loadChantConfig(cwd)).config;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// No project config: the environment variables still bind.
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Resolve the environment's server. Throws {@link ClickHouseBindingError} when there is none. */
|
|
90
|
+
export async function bindClickHouse(options = {}) {
|
|
91
|
+
const config = options.config ?? (await loadConfig(options.cwd ?? process.cwd()));
|
|
92
|
+
const target = resolveClickHouseTarget({ environment: options.environment, config, env: options.env });
|
|
93
|
+
if (isUnresolvedTarget(target))
|
|
94
|
+
throw new ClickHouseBindingError(target);
|
|
95
|
+
return target;
|
|
96
|
+
}
|
|
97
|
+
/** What a failed bind or read means, in the observation vocabulary. */
|
|
98
|
+
export function classifyClickHouseFailure(err) {
|
|
99
|
+
if (err instanceof ClickHouseBindingError)
|
|
100
|
+
return { reason: err.unresolved.reason, detail: err.unresolved.detail };
|
|
101
|
+
if (err instanceof TokenSourceError)
|
|
102
|
+
return { reason: "no-credentials", detail: err.message };
|
|
103
|
+
if (err instanceof ClickHouseQueryError) {
|
|
104
|
+
// 516 AUTHENTICATION_FAILED and 497 ACCESS_DENIED arrive as 401/403 or in the message.
|
|
105
|
+
if (err.status === 401 || err.status === 403 || /AUTHENTICATION_FAILED|ACCESS_DENIED|Code: 516|Code: 497/.test(err.serverMessage)) {
|
|
106
|
+
return { reason: "no-credentials", detail: `${err.message} (the server refused the credentials)` };
|
|
107
|
+
}
|
|
108
|
+
return { reason: "read-failed", detail: err.message };
|
|
109
|
+
}
|
|
110
|
+
return { reason: "read-failed", detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
|
|
111
|
+
}
|
|
112
|
+
/** `SELECT version()` on the target. */
|
|
113
|
+
export async function serverVersion(target) {
|
|
114
|
+
const [row] = await clickhouseQuery(target.endpoint, "SELECT version() AS v");
|
|
115
|
+
return row?.v ?? "";
|
|
116
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a live server's schema: the databases and the tables, views,
|
|
3
|
+
* materialized views and dictionaries in them, from `system.databases`,
|
|
4
|
+
* `system.tables` and `SHOW CREATE`, and the SQL user-defined functions,
|
|
5
|
+
* from `system.functions`.
|
|
6
|
+
*/
|
|
7
|
+
import { clickhouseQuery } from "../http.js";
|
|
8
|
+
import { CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
|
|
9
|
+
import { isChantWorkingObject, stripMarkerFromStatement } from "../ownership.js";
|
|
10
|
+
import { foreignTool } from "../../core/foreign-tables.js";
|
|
11
|
+
import { parseCreate, unquote } from "../parser.js";
|
|
12
|
+
import { tokenizeText } from "../tokens.js";
|
|
13
|
+
/** The server's own databases, never part of a declared schema. */
|
|
14
|
+
export const SYSTEM_DATABASES = new Set(["system", "information_schema", "INFORMATION_SCHEMA"]);
|
|
15
|
+
const quote = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
16
|
+
const ident = (s) => `\`${s.replace(/`/g, "``")}\``;
|
|
17
|
+
/** The entity type a `system.tables` engine stands for. */
|
|
18
|
+
export function entityTypeOfEngine(engine) {
|
|
19
|
+
if (engine === "View")
|
|
20
|
+
return CLICKHOUSE_ENTITY_TYPES.view;
|
|
21
|
+
if (engine === "MaterializedView")
|
|
22
|
+
return CLICKHOUSE_ENTITY_TYPES.materializedView;
|
|
23
|
+
return CLICKHOUSE_ENTITY_TYPES.table;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Every object in the target's databases, sorted by database then name.
|
|
27
|
+
* A materialized view's inner table (`.inner_id.<uuid>`, `.inner.<name>`)
|
|
28
|
+
* belongs to its view and is left out, as are temporary tables. A
|
|
29
|
+
* dictionary is read (#3682) from `system.tables`, where a reader granted
|
|
30
|
+
* `SELECT` on the database sees it without a grant on `system.dictionaries`;
|
|
31
|
+
* a table with the `Dictionary` engine is a table.
|
|
32
|
+
*
|
|
33
|
+
* A table another tool keeps its migration history in (`schema_migrations`,
|
|
34
|
+
* `goose_db_version`, `../../core/foreign-tables.ts`) is read and marked with
|
|
35
|
+
* the tool, as the Postgres reader marks it (#3676): import leaves it out
|
|
36
|
+
* with a warning, and a plan names it in a hint instead of dropping it.
|
|
37
|
+
*
|
|
38
|
+
* So are chant's own working objects (`../ownership.ts`
|
|
39
|
+
* `isChantWorkingObject`): a rebuild migration's new, dual-write and retained
|
|
40
|
+
* tables, and the effect receipts database. None of them is a declaration, so
|
|
41
|
+
* a plan, an import or a prune that saw them would propose dropping, importing
|
|
42
|
+
* or pruning the very objects a rebuild in progress depends on.
|
|
43
|
+
*/
|
|
44
|
+
export async function readLiveSchema(target, opts = {}) {
|
|
45
|
+
const q = (sql) => clickhouseQuery(target.endpoint, sql);
|
|
46
|
+
const scope = target.databases
|
|
47
|
+
? `IN (${target.databases.map(quote).join(", ")})`
|
|
48
|
+
: `NOT IN (${[...SYSTEM_DATABASES].map(quote).join(", ")})`;
|
|
49
|
+
const databases = await q(`SELECT name, engine, toString(uuid) AS uuid, comment FROM system.databases WHERE name ${scope} ORDER BY name`);
|
|
50
|
+
// `system.tables` shows no comment for a table over a table function, whatever it was created with: its statement has it (#3911).
|
|
51
|
+
const tables = await q(`SELECT database, name, engine, toString(uuid) AS uuid, comment, startsWith(create_table_query, 'CREATE DICTIONARY') AS dictionary, ` +
|
|
52
|
+
`if(comment = '' AND position(create_table_query, ' COMMENT ') > 0, create_table_query, '') AS proxy FROM system.tables ` +
|
|
53
|
+
`WHERE database ${scope} AND NOT is_temporary AND name NOT LIKE '.inner%' ` +
|
|
54
|
+
`ORDER BY database, name`);
|
|
55
|
+
const working = new Set(databases.filter((d) => isChantWorkingObject(d.comment)).map((d) => d.name));
|
|
56
|
+
const out = [];
|
|
57
|
+
const statement = async (kind, what) => opts.withStatements === false
|
|
58
|
+
? ""
|
|
59
|
+
: stripMarkerFromStatement((await q(`SHOW CREATE ${kind} ${what}`))[0]?.statement ?? "");
|
|
60
|
+
for (const d of databases) {
|
|
61
|
+
if (working.has(d.name))
|
|
62
|
+
continue;
|
|
63
|
+
out.push({
|
|
64
|
+
type: CLICKHOUSE_ENTITY_TYPES.database,
|
|
65
|
+
name: d.name,
|
|
66
|
+
engine: d.engine,
|
|
67
|
+
...(d.uuid && !/^0{8}-/.test(d.uuid) ? { uuid: d.uuid } : {}),
|
|
68
|
+
...(d.comment ? { comment: d.comment } : {}),
|
|
69
|
+
statement: await statement("DATABASE", ident(d.name)),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
for (const t of tables) {
|
|
73
|
+
if (t.proxy)
|
|
74
|
+
t.comment = commentOf(t.proxy) ?? t.comment;
|
|
75
|
+
if (working.has(t.database) || isChantWorkingObject(t.comment))
|
|
76
|
+
continue;
|
|
77
|
+
const type = Number(t.dictionary) === 1 ? CLICKHOUSE_ENTITY_TYPES.dictionary : entityTypeOfEngine(t.engine);
|
|
78
|
+
const foreign = type === CLICKHOUSE_ENTITY_TYPES.table ? foreignTool(t.name) : undefined;
|
|
79
|
+
out.push({
|
|
80
|
+
type,
|
|
81
|
+
database: t.database,
|
|
82
|
+
name: t.name,
|
|
83
|
+
engine: t.engine,
|
|
84
|
+
...(t.uuid && !/^0{8}-/.test(t.uuid) ? { uuid: t.uuid } : {}),
|
|
85
|
+
...(t.comment ? { comment: t.comment } : {}),
|
|
86
|
+
...(foreign ? { foreign } : {}),
|
|
87
|
+
statement: await statement("TABLE", `${ident(t.database)}.${ident(t.name)}`),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// SQL user-defined functions belong to no database: every one is read, and a caller keeps those it declares.
|
|
91
|
+
const functions = await q(`SELECT name, create_query AS statement FROM system.functions WHERE origin = 'SQLUserDefined' ORDER BY name`);
|
|
92
|
+
for (const f of functions) {
|
|
93
|
+
out.push({ type: CLICKHOUSE_ENTITY_TYPES.function, name: f.name, engine: "", statement: opts.withStatements === false ? "" : f.statement });
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** A `CREATE` statement's own `COMMENT`, its value unquoted; undefined when it has none or does not parse. */
|
|
98
|
+
function commentOf(statement) {
|
|
99
|
+
try {
|
|
100
|
+
const tokens = tokenizeText(statement, 0);
|
|
101
|
+
const node = parseCreate(tokens);
|
|
102
|
+
if (!node.comment)
|
|
103
|
+
return undefined;
|
|
104
|
+
const literal = tokens
|
|
105
|
+
.slice(node.comment.from, node.comment.to)
|
|
106
|
+
.map((t) => t.text)
|
|
107
|
+
.join("")
|
|
108
|
+
.trim();
|
|
109
|
+
return /^'.*'$/s.test(literal) ? literal.slice(1, -1).replace(/\\(.)/g, "$1").replace(/''/g, "'") : unquote(literal);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The users, roles, row policies, quotas, settings profiles and grantees'
|
|
117
|
+
* grants the build declares, as the server holds them (#3682, #3911):
|
|
118
|
+
* `SHOW CREATE USER`, `SHOW CREATE ROLE`, `SHOW CREATE ROW POLICY`,
|
|
119
|
+
* `SHOW CREATE QUOTA`, `SHOW CREATE SETTINGS PROFILE`, and for a grantee every line of
|
|
120
|
+
* `SHOW GRANTS FOR`, one per line. Nothing else is read, so a project that
|
|
121
|
+
* declares none needs no access to `system.users` and the like, and an
|
|
122
|
+
* object made by hand is never seen. One that does not exist is left out.
|
|
123
|
+
*/
|
|
124
|
+
export async function readLiveAccess(target, declared, opts = {}) {
|
|
125
|
+
if (declared.length === 0)
|
|
126
|
+
return [];
|
|
127
|
+
const q = (sql) => clickhouseQuery(target.endpoint, sql);
|
|
128
|
+
const first = async (sql) => (opts.withStatements === false ? "" : Object.values((await q(sql))[0] ?? {})[0] ?? "");
|
|
129
|
+
const all = async (sql) => (await q(sql)).map((r) => Object.values(r)[0] ?? "");
|
|
130
|
+
const names = (kind) => [...new Set(declared.filter((d) => d.kind === kind).map((d) => d.name))];
|
|
131
|
+
const users = names("user");
|
|
132
|
+
const roles = names("role");
|
|
133
|
+
const grantees = names("grants");
|
|
134
|
+
const principals = [...new Set([...users, ...roles, ...grantees])];
|
|
135
|
+
const list = (xs) => xs.map(quote).join(", ");
|
|
136
|
+
const existingUsers = new Set(principals.length ? (await q(`SELECT name FROM system.users WHERE name IN (${list(principals)})`)).map((r) => r.name) : []);
|
|
137
|
+
const existingRoles = new Set(principals.length ? (await q(`SELECT name FROM system.roles WHERE name IN (${list(principals)})`)).map((r) => r.name) : []);
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const name of users.filter((u) => existingUsers.has(u))) {
|
|
140
|
+
out.push({ type: CLICKHOUSE_ENTITY_TYPES.user, name, engine: "", statement: await first(`SHOW CREATE USER ${ident(name)}`) });
|
|
141
|
+
}
|
|
142
|
+
for (const name of roles.filter((r) => existingRoles.has(r))) {
|
|
143
|
+
out.push({ type: CLICKHOUSE_ENTITY_TYPES.role, name, engine: "", statement: await first(`SHOW CREATE ROLE ${ident(name)}`) });
|
|
144
|
+
}
|
|
145
|
+
for (const [kind, type, table, show] of [
|
|
146
|
+
["quota", CLICKHOUSE_ENTITY_TYPES.quota, "system.quotas", "QUOTA"],
|
|
147
|
+
["settingsProfile", CLICKHOUSE_ENTITY_TYPES.settingsProfile, "system.settings_profiles", "SETTINGS PROFILE"],
|
|
148
|
+
]) {
|
|
149
|
+
const wanted = names(kind);
|
|
150
|
+
if (wanted.length === 0)
|
|
151
|
+
continue;
|
|
152
|
+
const existing = new Set((await q(`SELECT name FROM ${table} WHERE name IN (${list(wanted)})`)).map((r) => r.name));
|
|
153
|
+
for (const name of wanted.filter((n) => existing.has(n))) {
|
|
154
|
+
out.push({ type, name, engine: "", statement: await first(`SHOW CREATE ${show} ${ident(name)}`) });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const policies = declared.filter((d) => d.kind === "rowPolicy");
|
|
158
|
+
if (policies.length > 0) {
|
|
159
|
+
const live = await q(`SELECT short_name AS name, database, table FROM system.row_policies WHERE short_name IN (${list(policies.map((p) => p.name))})`);
|
|
160
|
+
for (const p of policies) {
|
|
161
|
+
if (!live.some((l) => l.name === p.name && l.database === p.database && l.table === p.table))
|
|
162
|
+
continue;
|
|
163
|
+
out.push({
|
|
164
|
+
type: CLICKHOUSE_ENTITY_TYPES.rowPolicy,
|
|
165
|
+
name: p.name,
|
|
166
|
+
database: p.database,
|
|
167
|
+
table: p.table,
|
|
168
|
+
engine: "",
|
|
169
|
+
statement: await first(`SHOW CREATE ROW POLICY ${ident(p.name)} ON ${ident(p.database)}.${ident(p.table)}`),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const name of grantees.filter((g) => existingUsers.has(g) || existingRoles.has(g))) {
|
|
174
|
+
const lines = opts.withStatements === false ? [] : await all(`SHOW GRANTS FOR ${ident(name)}`);
|
|
175
|
+
out.push({ type: CLICKHOUSE_ENTITY_TYPES.grant, name, engine: "", statement: lines.join("\n") });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/** The canonical kinds a profile's `access` switch decides (#3716). */
|
|
180
|
+
export const ACCESS_KINDS = new Set(["user", "role", "rowPolicy", "quota", "settingsProfile", "grants"]);
|
|
181
|
+
/** The entity types a profile's `access` switch decides (#3716). */
|
|
182
|
+
export const ACCESS_ENTITY_TYPES = new Set([
|
|
183
|
+
CLICKHOUSE_ENTITY_TYPES.user,
|
|
184
|
+
CLICKHOUSE_ENTITY_TYPES.role,
|
|
185
|
+
CLICKHOUSE_ENTITY_TYPES.rowPolicy,
|
|
186
|
+
CLICKHOUSE_ENTITY_TYPES.quota,
|
|
187
|
+
CLICKHOUSE_ENTITY_TYPES.settingsProfile,
|
|
188
|
+
CLICKHOUSE_ENTITY_TYPES.grant,
|
|
189
|
+
]);
|
|
190
|
+
/** Why an access declaration is not read, planned or applied where the profile does not manage access. */
|
|
191
|
+
export const ACCESS_UNMANAGED_DETAIL = "access is not managed in this environment: set sql.profiles.<env>.access to plan and apply users, roles, row policies, quotas, settings profiles and grants";
|
|
192
|
+
/** The hint a plan gives for the access declarations it left out. */
|
|
193
|
+
export function accessUnmanagedHint(n, environment) {
|
|
194
|
+
return `${n} access declaration${n === 1 ? " is" : "s are"} not planned: ${environment}'s profile does not manage access (sql.profiles.<env>.access)`;
|
|
195
|
+
}
|
|
196
|
+
/** The access objects among declared canonical objects, for `readLiveAccess`. */
|
|
197
|
+
export function declaredAccess(objects) {
|
|
198
|
+
return objects
|
|
199
|
+
.filter((o) => ACCESS_KINDS.has(o.kind))
|
|
200
|
+
.map((o) => ({ kind: o.kind, name: o.name, ...(o.database !== undefined ? { database: o.database } : {}), ...(o.table !== undefined ? { table: o.table } : {}) }));
|
|
201
|
+
}
|
|
202
|
+
/** The users, roles, row policies, quotas and settings profiles among declared entities, for `readLiveAccess`. */
|
|
203
|
+
export function accessOf(entities, defaultDatabase) {
|
|
204
|
+
const kinds = {
|
|
205
|
+
[CLICKHOUSE_ENTITY_TYPES.user]: "user",
|
|
206
|
+
[CLICKHOUSE_ENTITY_TYPES.role]: "role",
|
|
207
|
+
[CLICKHOUSE_ENTITY_TYPES.rowPolicy]: "rowPolicy",
|
|
208
|
+
[CLICKHOUSE_ENTITY_TYPES.quota]: "quota",
|
|
209
|
+
[CLICKHOUSE_ENTITY_TYPES.settingsProfile]: "settingsProfile",
|
|
210
|
+
};
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const e of entities.values()) {
|
|
213
|
+
const kind = kinds[e.entityType];
|
|
214
|
+
if (!kind)
|
|
215
|
+
continue;
|
|
216
|
+
const name = String(e.props.name ?? "");
|
|
217
|
+
if (kind === "rowPolicy")
|
|
218
|
+
out.push({ kind, name, database: typeof e.props.database === "string" ? e.props.database : defaultDatabase, table: String(e.props.table ?? "") });
|
|
219
|
+
else
|
|
220
|
+
out.push({ kind, name });
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|