@intentius/chant-lexicon-sql 0.117.0 → 0.118.0
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/clickhouse/access.d.ts +132 -0
- package/dist/clickhouse/access.d.ts.map +1 -0
- package/dist/clickhouse/apply/apply.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.d.ts +2 -0
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +171 -2
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/http.d.ts +3 -0
- package/dist/clickhouse/http.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts +28 -0
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/import/ir.d.ts.map +1 -1
- package/dist/clickhouse/import/parser.d.ts +3 -3
- package/dist/clickhouse/import/parser.d.ts.map +1 -1
- package/dist/clickhouse/live/bind.d.ts.map +1 -1
- package/dist/clickhouse/live/catalog.d.ts +41 -6
- package/dist/clickhouse/live/catalog.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.d.ts +3 -2
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/parser.d.ts +54 -3
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/commands.d.ts +3 -1
- package/dist/clickhouse/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts +12 -0
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +61 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/report.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +7 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/plan/schema.d.ts +8 -4
- package/dist/clickhouse/plan/schema.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
- package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/observe.d.ts +7 -3
- package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/op.d.ts +8 -0
- package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/steps.d.ts +48 -0
- package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
- package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
- package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
- package/dist/clickhouse/topology.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +50 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/core/references.d.ts +4 -2
- package/dist/core/references.d.ts.map +1 -1
- package/dist/core/statements.d.ts +11 -0
- package/dist/core/statements.d.ts.map +1 -1
- package/dist/files/build-root.d.ts +26 -0
- package/dist/files/build-root.d.ts.map +1 -0
- package/dist/files/clickhouse.d.ts +19 -0
- package/dist/files/clickhouse.d.ts.map +1 -0
- package/dist/files/common.d.ts +25 -0
- package/dist/files/common.d.ts.map +1 -0
- package/dist/files/postgres.d.ts +36 -0
- package/dist/files/postgres.d.ts.map +1 -0
- package/dist/import-parser.d.ts +25 -0
- package/dist/import-parser.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +6 -6
- package/dist/lint/rules/sqlch001.d.ts +1 -1
- package/dist/lint/rules/sqlch001.d.ts.map +1 -1
- package/dist/lint/rules/sqlch002.d.ts.map +1 -1
- package/dist/lint/rules/templates.d.ts +1 -1
- package/dist/lint/rules/templates.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +30 -0
- package/dist/okf/index.md +7 -1
- package/dist/okf/rules/SQLCH001.md +2 -2
- package/dist/okf/types/Dictionary.md +9 -0
- package/dist/okf/types/Function.md +9 -0
- package/dist/okf/types/Grant.md +9 -0
- package/dist/okf/types/Role.md +9 -0
- package/dist/okf/types/RowPolicy.md +9 -0
- package/dist/okf/types/User.md +9 -0
- package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +9 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +68 -0
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +33 -0
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/live/bind.d.ts.map +1 -1
- package/dist/postgres/live/client.d.ts +3 -0
- package/dist/postgres/live/client.d.ts.map +1 -1
- package/dist/rules/sqlch001.ts +26 -4
- package/dist/rules/sqlch002.ts +3 -2
- package/dist/rules/templates.ts +2 -2
- package/dist/serializer.d.ts +2 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sql-files.d.ts +79 -0
- package/dist/sql-files.d.ts.map +1 -0
- package/dist/token-source.d.ts +88 -0
- package/dist/token-source.d.ts.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/access.test.ts +203 -0
- package/src/clickhouse/access.ts +595 -0
- package/src/clickhouse/apply/apply.ts +29 -20
- package/src/clickhouse/apply/statements.ts +105 -8
- package/src/clickhouse/dictionary.test.ts +177 -0
- package/src/clickhouse/entities.ts +356 -6
- package/src/clickhouse/function.test.ts +91 -0
- package/src/clickhouse/http.ts +11 -2
- package/src/clickhouse/import/generator.ts +81 -46
- package/src/clickhouse/import/import.test.ts +9 -3
- package/src/clickhouse/import/ir.ts +6 -2
- package/src/clickhouse/import/parser.ts +10 -18
- package/src/clickhouse/live/access.e2e.test.ts +218 -0
- package/src/clickhouse/live/bind.ts +17 -6
- package/src/clickhouse/live/catalog.ts +106 -10
- package/src/clickhouse/live/describe-resources.ts +23 -10
- package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
- package/src/clickhouse/live/functions.e2e.test.ts +155 -0
- package/src/clickhouse/live/live.test.ts +2 -2
- package/src/clickhouse/parser.ts +136 -4
- package/src/clickhouse/plan/commands.ts +18 -7
- package/src/clickhouse/plan/deep.ts +71 -11
- package/src/clickhouse/plan/diff.ts +65 -2
- package/src/clickhouse/plan/normalize.ts +200 -6
- package/src/clickhouse/plan/report.ts +14 -3
- package/src/clickhouse/plan/rules.ts +44 -1
- package/src/clickhouse/plan/schema.ts +40 -17
- package/src/clickhouse/plan/server-format.ts +1 -1
- package/src/clickhouse/rebuild/backfill.ts +5 -25
- package/src/clickhouse/rebuild/observe.ts +7 -3
- package/src/clickhouse/rebuild/op.ts +9 -0
- package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
- package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
- package/src/clickhouse/rebuild/steps.ts +136 -1
- package/src/clickhouse/testing/fake-server.ts +2 -0
- package/src/clickhouse/testing/writable-server.ts +2 -0
- package/src/clickhouse/topology.ts +17 -2
- package/src/clickhouse.ts +19 -0
- package/src/codegen/classifier-page.ts +2 -0
- package/src/codegen/registry.ts +6 -0
- package/src/config.ts +12 -3
- package/src/core/references.ts +5 -3
- package/src/core/statements.ts +11 -1
- package/src/files/build-root.ts +62 -0
- package/src/files/clickhouse.ts +52 -0
- package/src/files/common.ts +54 -0
- package/src/files/postgres.ts +371 -0
- package/src/generated/lexicon-sql.json +30 -0
- package/src/import-parser.ts +61 -0
- package/src/index.ts +15 -2
- package/src/lint/rules/sqlch001.ts +26 -4
- package/src/lint/rules/sqlch002.ts +3 -2
- package/src/lint/rules/templates.ts +2 -2
- package/src/lsp/template.ts +3 -3
- package/src/mcp/index.ts +6 -6
- package/src/migration-statements.test.ts +25 -0
- package/src/migration-statements.ts +1 -0
- package/src/op/activities/clickhouse-rebuild.ts +1 -0
- package/src/op/activity-contracts.ts +1 -0
- package/src/plugin.ts +29 -8
- package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
- package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
- package/src/postgres/apply/statements.test.ts +138 -6
- package/src/postgres/apply/statements.ts +156 -10
- package/src/postgres/import/generator.ts +118 -45
- package/src/postgres/live/bind.ts +17 -6
- package/src/postgres/live/client.ts +10 -2
- package/src/postgres/plan/plan.test.ts +2 -2
- package/src/postgres/plan/rules.ts +5 -5
- package/src/serializer.ts +14 -9
- package/src/skills/chant-sql-postgres-plan.md +3 -3
- package/src/skills/chant-sql-rebuild.md +3 -2
- package/src/skills/chant-sql.md +7 -1
- package/src/sql-files.test.ts +188 -0
- package/src/sql-files.ts +218 -0
- package/src/token-source.e2e.test.ts +142 -0
- package/src/token-source.test.ts +119 -0
- package/src/token-source.ts +189 -0
- package/src/validate.ts +6 -0
|
@@ -24,16 +24,24 @@
|
|
|
24
24
|
import type { GeneratedFile, TypeScriptGenerator } from "@intentius/chant/import/generator";
|
|
25
25
|
import type { TemplateIR } from "@intentius/chant/import/parser";
|
|
26
26
|
import { isTrivia, tokenizeText, type Token } from "../tokens";
|
|
27
|
-
import { identValue } from "../parser";
|
|
27
|
+
import { identValue, parseStatements, type NameNode, type StatementNode } from "../parser";
|
|
28
28
|
import { POSTGRES_ENTITY_TYPES } from "../entity-types";
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
/** One object a template is written for, or (with no `ddl` read) one a template may reference. */
|
|
31
|
+
export interface PgTemplateItem {
|
|
31
32
|
exportName: string;
|
|
32
33
|
type: string;
|
|
33
34
|
schema?: string;
|
|
34
35
|
name: string;
|
|
35
36
|
ddl: string;
|
|
36
37
|
}
|
|
38
|
+
type Item = PgTemplateItem;
|
|
39
|
+
|
|
40
|
+
/** A template's text: `parts` (template-literal safe) around one interpolation per entry of `refs`, an export name. */
|
|
41
|
+
export interface TemplateBody {
|
|
42
|
+
parts: string[];
|
|
43
|
+
refs: string[];
|
|
44
|
+
}
|
|
37
45
|
|
|
38
46
|
const TAG: Record<string, string> = {
|
|
39
47
|
[POSTGRES_ENTITY_TYPES.schema]: "schema",
|
|
@@ -54,37 +62,79 @@ const TAG: Record<string, string> = {
|
|
|
54
62
|
[POSTGRES_ENTITY_TYPES.defaultPrivileges]: "grant",
|
|
55
63
|
};
|
|
56
64
|
|
|
65
|
+
/** The tag each entity type is declared with (`table`, `func`, ...). */
|
|
66
|
+
export const POSTGRES_TAG_OF: Readonly<Record<string, string>> = TAG;
|
|
67
|
+
|
|
57
68
|
/** Text that sits inside a template literal: a backquote and `${` escaped. */
|
|
58
69
|
const templateSafe = (s: string) => s.replace(/\\(?=`|\$\{)/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
59
70
|
|
|
60
71
|
const isName = (t: Token | undefined) => t !== undefined && (t.kind === "ident" || t.kind === "qident");
|
|
61
72
|
|
|
62
|
-
/** `'app.invoice_seq'` as the qualified name it holds. */
|
|
73
|
+
/** `'app.invoice_seq'` as the qualified name it holds, or the bare name when unqualified. */
|
|
63
74
|
function regclassName(t: Token): string | undefined {
|
|
64
75
|
if (t.kind !== "string" || !/^'.*'$/s.test(t.text)) return undefined;
|
|
65
76
|
const inner = t.text.slice(1, -1).replace(/''/g, "'");
|
|
66
77
|
const pieces = inner.match(/"(?:[^"]|"")*"|[^.]+/g)?.map((p) => identValue(p)) ?? [];
|
|
67
|
-
return pieces.length === 2 ? `${pieces[0]}.${pieces[1]}` : pieces.length === 1 ?
|
|
78
|
+
return pieces.length === 2 ? `${pieces[0]}.${pieces[1]}` : pieces.length === 1 ? pieces[0] : undefined;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
|
|
81
|
+
/** Words after which a bare name is a relation: `FROM t`, `JOIN t`, `REFERENCES t`. */
|
|
82
|
+
const RELATION_WORDS = new Set(["FROM", "JOIN", "REFERENCES"]);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The tokens where a bare (unqualified) name is another object's: a column's
|
|
86
|
+
* or a domain's type, the table of an index, trigger or policy, a trigger's
|
|
87
|
+
* function, a grant's objects. A `FROM`, `JOIN` or `REFERENCES` before a
|
|
88
|
+
* name is read off the tokens directly.
|
|
89
|
+
*/
|
|
90
|
+
function bareSpots(tokens: Token[]): Set<number> {
|
|
91
|
+
const out = new Set<number>();
|
|
92
|
+
let nodes: StatementNode[];
|
|
93
|
+
try {
|
|
94
|
+
nodes = parseStatements(tokens);
|
|
95
|
+
} catch {
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
const first = (from: number, to: number) => {
|
|
99
|
+
for (let i = from; i < to; i++) if (!isTrivia(tokens[i]!)) return out.add(i);
|
|
100
|
+
return out;
|
|
101
|
+
};
|
|
102
|
+
const name = (n: NameNode | undefined) => n && n.span.to - n.span.from > 0 && first(n.span.from, n.span.to);
|
|
103
|
+
for (const node of nodes) {
|
|
104
|
+
if (node.statement === "table") for (const c of node.columns) if (c.type) first(c.type.from, c.type.to);
|
|
105
|
+
if (node.statement === "domain") first(node.type.from, node.type.to);
|
|
106
|
+
if (node.statement === "index" || node.statement === "policy") name(node.table);
|
|
107
|
+
if (node.statement === "trigger") {
|
|
108
|
+
name(node.table);
|
|
109
|
+
name(node.function);
|
|
110
|
+
}
|
|
111
|
+
if (node.statement === "grant") for (const o of node.objects) name(o.name);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function templateBody(item: Item, byQualified: Map<string, Item>, schemaExport: Map<string, string>): TemplateBody {
|
|
71
117
|
const tokens = tokenizeText(item.ddl, 0);
|
|
72
118
|
const sig: number[] = [];
|
|
73
119
|
tokens.forEach((t, i) => {
|
|
74
120
|
if (!isTrivia(t)) sig.push(i);
|
|
75
121
|
});
|
|
76
|
-
const
|
|
122
|
+
const spots = bareSpots(tokens);
|
|
123
|
+
const replace = new Map<number, { to: number; ref: string }>();
|
|
124
|
+
const isSelf = (target: Item) => target === item || (target.exportName === item.exportName && target.type === item.type);
|
|
77
125
|
for (let k = 0; k < sig.length; k++) {
|
|
78
126
|
const a = tokens[sig[k]!]!;
|
|
79
127
|
const prev = tokens[sig[k - 1] ?? -1];
|
|
80
128
|
if (prev?.kind === "punct" && prev.text === ".") continue;
|
|
81
129
|
// A regclass literal: nextval('app.seq'::regclass) or 'app.t'::regclass.
|
|
82
130
|
if (a.kind === "string" && tokens[sig[k + 1] ?? -1]?.text === "::" && tokens[sig[k + 2] ?? -1]?.text.toLowerCase() === "regclass") {
|
|
83
|
-
const
|
|
84
|
-
|
|
131
|
+
const held = regclassName(a) ?? "";
|
|
132
|
+
// An unqualified name is the object in `public`, as the server's own printing has it, else the unqualified object of that name.
|
|
133
|
+
const target = (!held.includes(".") ? byQualified.get(`public.${held}`) : undefined) ?? byQualified.get(held);
|
|
134
|
+
if (target && !isSelf(target)) {
|
|
85
135
|
const fn = tokens[sig[k - 2] ?? -1];
|
|
86
136
|
const inCall = prev?.text === "(" && fn?.kind === "ident" && /^(nextval|currval|setval)$/i.test(fn.text);
|
|
87
|
-
replace.set(sig[k]!, { to: inCall ? sig[k + 2]! + 1 : sig[k]! + 1,
|
|
137
|
+
replace.set(sig[k]!, { to: inCall ? sig[k + 2]! + 1 : sig[k]! + 1, ref: target.exportName });
|
|
88
138
|
k += inCall ? 2 : 0;
|
|
89
139
|
continue;
|
|
90
140
|
}
|
|
@@ -96,36 +146,85 @@ function templateBody(item: Item, byQualified: Map<string, Item>, schemaExport:
|
|
|
96
146
|
const bySchemaWord = item.type === POSTGRES_ENTITY_TYPES.extension || item.type === POSTGRES_ENTITY_TYPES.grant || item.type === POSTGRES_ENTITY_TYPES.defaultPrivileges;
|
|
97
147
|
if (bySchemaWord && isName(a) && prev?.kind === "ident" && prev.text.toUpperCase() === "SCHEMA") {
|
|
98
148
|
const s = schemaExport.get(identValue(a));
|
|
99
|
-
if (s) replace.set(sig[k]!, { to: sig[k]! + 1,
|
|
149
|
+
if (s) replace.set(sig[k]!, { to: sig[k]! + 1, ref: s });
|
|
150
|
+
continue;
|
|
100
151
|
}
|
|
152
|
+
// A bare name where an object is named: `REFERENCES users`, `ON users`, a column of type `status`.
|
|
153
|
+
const bare = isName(a) && (spots.has(sig[k]!) || (prev?.kind === "ident" && RELATION_WORDS.has(prev.text.toUpperCase())));
|
|
154
|
+
const target = bare ? byQualified.get(identValue(a)) : undefined;
|
|
155
|
+
if (target && !isSelf(target)) replace.set(sig[k]!, { to: sig[k]! + 1, ref: target.exportName });
|
|
101
156
|
continue;
|
|
102
157
|
}
|
|
103
158
|
const schema = identValue(a);
|
|
104
159
|
const name = identValue(b);
|
|
105
160
|
const self = schema === item.schema && name === item.name;
|
|
106
161
|
const target = byQualified.get(`${schema}.${name}`);
|
|
107
|
-
if (!self && target && target
|
|
108
|
-
replace.set(sig[k]!, { to: sig[k + 2]! + 1,
|
|
162
|
+
if (!self && target && !isSelf(target)) {
|
|
163
|
+
replace.set(sig[k]!, { to: sig[k + 2]! + 1, ref: target.exportName });
|
|
109
164
|
k += 2;
|
|
110
165
|
} else if (self && schemaExport.has(schema)) {
|
|
111
|
-
replace.set(sig[k]!, { to: sig[k]! + 1,
|
|
166
|
+
replace.set(sig[k]!, { to: sig[k]! + 1, ref: schemaExport.get(schema)! });
|
|
112
167
|
k += 2;
|
|
113
168
|
}
|
|
114
169
|
}
|
|
115
|
-
|
|
170
|
+
const parts: string[] = [""];
|
|
116
171
|
const refs: string[] = [];
|
|
117
172
|
for (let i = 0; i < tokens.length; ) {
|
|
118
173
|
const r = replace.get(i);
|
|
119
174
|
if (r) {
|
|
120
|
-
body += r.text;
|
|
121
175
|
refs.push(r.ref);
|
|
176
|
+
parts.push("");
|
|
122
177
|
i = r.to;
|
|
123
178
|
continue;
|
|
124
179
|
}
|
|
125
|
-
|
|
180
|
+
parts[parts.length - 1] += templateSafe(tokens[i]!.text);
|
|
126
181
|
i++;
|
|
127
182
|
}
|
|
128
|
-
return {
|
|
183
|
+
return { parts, refs };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A template body as the text between the backquotes. */
|
|
187
|
+
export const bodyText = (body: TemplateBody): string => body.parts.map((p, i) => (i === 0 ? p : `\${${body.refs[i - 1]}}${p}`)).join("");
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The template of each object in `items`, with the references to `targets`
|
|
191
|
+
* (the items themselves, and any object declared elsewhere that they may
|
|
192
|
+
* name) interpolated, and the items in the order a file declares them: each
|
|
193
|
+
* after what it references.
|
|
194
|
+
*
|
|
195
|
+
* A name is a reference where it is qualified (`app.users`), or bare where an
|
|
196
|
+
* object is named (`REFERENCES users`, `FROM users`, an index's table, a
|
|
197
|
+
* column's type) and an unqualified object has that name.
|
|
198
|
+
*/
|
|
199
|
+
export function postgresTemplates(items: readonly Item[], targets: readonly Item[] = items): { order: Item[]; bodies: Map<string, TemplateBody> } {
|
|
200
|
+
const byQualified = new Map<string, Item>();
|
|
201
|
+
const schemaExport = new Map<string, string>();
|
|
202
|
+
for (const it of targets) {
|
|
203
|
+
if (it.type === POSTGRES_ENTITY_TYPES.schema) schemaExport.set(it.name, it.exportName);
|
|
204
|
+
// An index or a trigger is never referenced by name in another object's DDL.
|
|
205
|
+
else if (it.type !== POSTGRES_ENTITY_TYPES.index && it.type !== POSTGRES_ENTITY_TYPES.trigger && it.type !== POSTGRES_ENTITY_TYPES.policy && it.type !== POSTGRES_ENTITY_TYPES.grant && it.type !== POSTGRES_ENTITY_TYPES.defaultPrivileges && it.type !== POSTGRES_ENTITY_TYPES.role && it.type !== POSTGRES_ENTITY_TYPES.extension) {
|
|
206
|
+
byQualified.set(it.schema ? `${it.schema}.${it.name}` : it.name, it);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const bodies = new Map(items.map((it) => [it.exportName, templateBody(it, byQualified, schemaExport)]));
|
|
211
|
+
const order: Item[] = [];
|
|
212
|
+
const done = new Set<string>();
|
|
213
|
+
const visiting = new Set<string>();
|
|
214
|
+
const byExport = new Map(items.map((it) => [it.exportName, it]));
|
|
215
|
+
const visit = (it: Item) => {
|
|
216
|
+
if (done.has(it.exportName) || visiting.has(it.exportName)) return;
|
|
217
|
+
visiting.add(it.exportName);
|
|
218
|
+
for (const ref of bodies.get(it.exportName)!.refs) {
|
|
219
|
+
const dep = byExport.get(ref);
|
|
220
|
+
if (dep) visit(dep);
|
|
221
|
+
}
|
|
222
|
+
visiting.delete(it.exportName);
|
|
223
|
+
done.add(it.exportName);
|
|
224
|
+
order.push(it);
|
|
225
|
+
};
|
|
226
|
+
for (const it of items) visit(it);
|
|
227
|
+
return { order, bodies };
|
|
129
228
|
}
|
|
130
229
|
|
|
131
230
|
const indent = (body: string) =>
|
|
@@ -151,39 +250,13 @@ export class PostgresGenerator implements TypeScriptGenerator {
|
|
|
151
250
|
}));
|
|
152
251
|
if (items.length === 0) return [];
|
|
153
252
|
|
|
154
|
-
const
|
|
155
|
-
const schemaExport = new Map<string, string>();
|
|
156
|
-
for (const it of items) {
|
|
157
|
-
if (it.type === POSTGRES_ENTITY_TYPES.schema) schemaExport.set(it.name, it.exportName);
|
|
158
|
-
// An index or a trigger is never referenced by name in another object's DDL.
|
|
159
|
-
else if (it.schema && it.type !== POSTGRES_ENTITY_TYPES.index && it.type !== POSTGRES_ENTITY_TYPES.trigger && it.type !== POSTGRES_ENTITY_TYPES.policy) {
|
|
160
|
-
byQualified.set(`${it.schema}.${it.name}`, it);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const bodies = new Map(items.map((it) => [it.exportName, templateBody(it, byQualified, schemaExport)]));
|
|
165
|
-
const order: Item[] = [];
|
|
166
|
-
const done = new Set<string>();
|
|
167
|
-
const visiting = new Set<string>();
|
|
168
|
-
const byExport = new Map(items.map((it) => [it.exportName, it]));
|
|
169
|
-
const visit = (it: Item) => {
|
|
170
|
-
if (done.has(it.exportName) || visiting.has(it.exportName)) return;
|
|
171
|
-
visiting.add(it.exportName);
|
|
172
|
-
for (const ref of bodies.get(it.exportName)!.refs) {
|
|
173
|
-
const dep = byExport.get(ref);
|
|
174
|
-
if (dep) visit(dep);
|
|
175
|
-
}
|
|
176
|
-
visiting.delete(it.exportName);
|
|
177
|
-
done.add(it.exportName);
|
|
178
|
-
order.push(it);
|
|
179
|
-
};
|
|
180
|
-
for (const it of items) visit(it);
|
|
253
|
+
const { order, bodies } = postgresTemplates(items);
|
|
181
254
|
|
|
182
255
|
const tags = [...new Set(order.map((it) => TAG[it.type]!))].sort();
|
|
183
256
|
const lines: string[] = [];
|
|
184
257
|
if (this.header) lines.push(...this.header.split("\n").map((l) => `// ${l}`), "");
|
|
185
258
|
lines.push(`import { ${tags.join(", ")} } from "@intentius/chant-lexicon-sql/postgres";`, "");
|
|
186
|
-
for (const it of order) lines.push(`export const ${it.exportName} = ${TAG[it.type]}\`\n ${indent(bodies.get(it.exportName)
|
|
259
|
+
for (const it of order) lines.push(`export const ${it.exportName} = ${TAG[it.type]}\`\n ${indent(bodyText(bodies.get(it.exportName)!))}\`;`, "");
|
|
187
260
|
return [{ path: "schema.ts", content: `${lines.join("\n").trimEnd()}\n` }];
|
|
188
261
|
}
|
|
189
262
|
}
|
|
@@ -8,6 +8,7 @@ import type { ChantConfig } from "@intentius/chant/config";
|
|
|
8
8
|
import type { UnobservedReason } from "@intentius/chant/observation";
|
|
9
9
|
import { connectPostgres, PostgresQueryError, type PostgresClient, type PostgresEndpoint } from "./client";
|
|
10
10
|
import type { PostgresProvider } from "../providers/types";
|
|
11
|
+
import { credentialSource, TokenSourceError } from "../../token-source";
|
|
11
12
|
|
|
12
13
|
export interface PostgresTarget {
|
|
13
14
|
endpoint: PostgresEndpoint;
|
|
@@ -58,12 +59,21 @@ export function resolvePostgresTarget(input: {
|
|
|
58
59
|
if (profile && isPostgresUrl(profile.url)) {
|
|
59
60
|
const source = `sql.profiles.${input.environment}`;
|
|
60
61
|
const endpoint: PostgresEndpoint = { url: profile.url };
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
if (profile.user) {
|
|
63
|
+
const value = env[profile.user.env];
|
|
64
|
+
if (value === undefined) return { reason: "no-credentials", detail: `${source}.user names ${profile.user.env}, which is not set` };
|
|
65
|
+
endpoint.user = value;
|
|
66
|
+
}
|
|
67
|
+
const password = profile.password;
|
|
68
|
+
if (password && "env" in password) {
|
|
69
|
+
const value = env[password.env];
|
|
70
|
+
if (value === undefined) return { reason: "no-credentials", detail: `${source}.password names ${password.env}, which is not set` };
|
|
71
|
+
endpoint.password = value;
|
|
72
|
+
} else if (password) {
|
|
73
|
+
if (password.token === "rds-iam" && endpoint.user === undefined) {
|
|
74
|
+
return { reason: "no-credentials", detail: `${source}.password is an rds-iam token, which is minted for one user, and ${source}.user is not set` };
|
|
75
|
+
}
|
|
76
|
+
endpoint.token = credentialSource(password, { url: profile.url, ...(endpoint.user !== undefined ? { user: endpoint.user } : {}), env, source: `${source}.password` });
|
|
67
77
|
}
|
|
68
78
|
const timeouts = Object.fromEntries(
|
|
69
79
|
(["lockTimeoutMs", "statementTimeoutMs", "scanTimeoutMs"] as const).filter((k) => profile[k] !== undefined).map((k) => [k, profile[k]]),
|
|
@@ -142,6 +152,7 @@ const CREDENTIAL_STATES = new Set(["28000", "28P01", "42501"]);
|
|
|
142
152
|
/** What a failed bind or read means, in the observation vocabulary. */
|
|
143
153
|
export function classifyPostgresFailure(err: unknown): { reason: UnobservedReason; detail: string } {
|
|
144
154
|
if (err instanceof PostgresBindingError) return { reason: err.unresolved.reason, detail: err.unresolved.detail };
|
|
155
|
+
if (err instanceof TokenSourceError) return { reason: "no-credentials", detail: err.message };
|
|
145
156
|
if (err instanceof PostgresQueryError && err.code && CREDENTIAL_STATES.has(err.code)) {
|
|
146
157
|
return { reason: "no-credentials", detail: `${err.message} (the server refused the credentials, SQLSTATE ${err.code})` };
|
|
147
158
|
}
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
* search path.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { CredentialSource } from "../../token-source";
|
|
12
|
+
|
|
11
13
|
export interface PostgresEndpoint {
|
|
12
14
|
/** A `postgres://` connection URL, without a password. */
|
|
13
15
|
url: string;
|
|
14
16
|
user?: string;
|
|
15
17
|
password?: string;
|
|
18
|
+
/** Mints the password at each connect, in place of `password` (a profile's token source, #3685). */
|
|
19
|
+
token?: CredentialSource;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/** What chant reads and writes through: one connection. */
|
|
@@ -54,7 +58,8 @@ export async function connectPostgres(endpoint: PostgresEndpoint, options: { app
|
|
|
54
58
|
// node-postgres lets the connection string's fields win over the config's, so the credentials go into the URL.
|
|
55
59
|
const url = new URL(endpoint.url);
|
|
56
60
|
if (endpoint.user !== undefined) url.username = encodeURIComponent(endpoint.user);
|
|
57
|
-
|
|
61
|
+
const password = endpoint.token ? await endpoint.token.get() : endpoint.password;
|
|
62
|
+
if (password !== undefined) url.password = encodeURIComponent(password);
|
|
58
63
|
const client = new pg.Client({ connectionString: url.toString(), application_name: options.applicationName ?? "chant" });
|
|
59
64
|
// A server that goes away emits an error event; the next query fails with it instead of the process crashing.
|
|
60
65
|
client.on("error", () => undefined);
|
|
@@ -63,7 +68,10 @@ export async function connectPostgres(endpoint: PostgresEndpoint, options: { app
|
|
|
63
68
|
await client.query("SELECT pg_catalog.set_config('search_path', '', false)");
|
|
64
69
|
} catch (err) {
|
|
65
70
|
await client.end().catch(() => undefined);
|
|
66
|
-
|
|
71
|
+
const wrapped = wrap(err);
|
|
72
|
+
// A refused token is not reused: the next connect mints another.
|
|
73
|
+
if (endpoint.token && (wrapped.code === "28P01" || wrapped.code === "28000")) endpoint.token.invalidate();
|
|
74
|
+
throw wrapped;
|
|
67
75
|
}
|
|
68
76
|
let ended = false;
|
|
69
77
|
return {
|
|
@@ -82,7 +82,7 @@ describe("columns", () => {
|
|
|
82
82
|
expect(rules(diff([table(ORDERS)], [table("CREATE TABLE app.orders (id bigint PRIMARY KEY, amount numeric(12,2) DEFAULT 0, note varchar(20) NOT NULL, status text)")]))).toEqual([
|
|
83
83
|
["columns.amount.default", "SQLPG209", "metadata"],
|
|
84
84
|
["columns.amount.notNull", "SQLPG211", "metadata"],
|
|
85
|
-
["columns.note.notNull", "SQLPG210", "
|
|
85
|
+
["columns.note.notNull", "SQLPG210", "validate"],
|
|
86
86
|
]);
|
|
87
87
|
const renamed = diff([table(ORDERS)], [table(`CREATE TABLE app.orders (id bigint PRIMARY KEY, amount numeric(12,2) NOT NULL, note varchar(20),
|
|
88
88
|
state text -- previously: status
|
|
@@ -113,7 +113,7 @@ describe("constraints", () => {
|
|
|
113
113
|
);
|
|
114
114
|
expect(rules(d)).toEqual([
|
|
115
115
|
["constraints.unique", "SQLPG221", "concurrently"],
|
|
116
|
-
["constraints.a", "SQLPG218", "
|
|
116
|
+
["constraints.a", "SQLPG218", "validate"],
|
|
117
117
|
["constraints.b", "SQLPG217", "metadata"],
|
|
118
118
|
["constraints.c", "SQLPG219", "validate"],
|
|
119
119
|
["constraints.exclude", "SQLPG222", "rewrite"],
|
|
@@ -109,9 +109,9 @@ export const PG_CLASSIFIER_RULES = {
|
|
|
109
109
|
SQLPG209: rule("SQLPG209", "metadata", "Change a column's default", "SET DEFAULT and DROP DEFAULT change only the catalog; existing rows keep their values.", ALTER_TABLE),
|
|
110
110
|
SQLPG210: rule(
|
|
111
111
|
"SQLPG210",
|
|
112
|
-
"
|
|
112
|
+
"validate",
|
|
113
113
|
"Set NOT NULL",
|
|
114
|
-
"SET NOT NULL reads the whole table under ACCESS EXCLUSIVE, unless a valid CHECK (column IS NOT NULL) constraint already proves it (since 12).
|
|
114
|
+
"SET NOT NULL alone reads the whole table under ACCESS EXCLUSIVE, unless a valid CHECK (column IS NOT NULL) constraint already proves it (since 12). So it is made as four statements: that check added NOT VALID, validated under SHARE UPDATE EXCLUSIVE while writes go on, SET NOT NULL, which the check proves without a scan, and the check dropped. Rows where the column is NULL fail the validation; the first statement's pre-check counts them.",
|
|
115
115
|
ALTER_TABLE,
|
|
116
116
|
),
|
|
117
117
|
SQLPG211: rule("SQLPG211", "metadata", "Drop NOT NULL", "DROP NOT NULL changes only the catalog.", ALTER_TABLE),
|
|
@@ -135,16 +135,16 @@ export const PG_CLASSIFIER_RULES = {
|
|
|
135
135
|
),
|
|
136
136
|
SQLPG218: rule(
|
|
137
137
|
"SQLPG218",
|
|
138
|
-
"
|
|
138
|
+
"validate",
|
|
139
139
|
"Add a CHECK constraint",
|
|
140
|
-
"ADD CONSTRAINT ... CHECK reads every row under ACCESS EXCLUSIVE.
|
|
140
|
+
"ADD CONSTRAINT ... CHECK alone reads every row under ACCESS EXCLUSIVE. On a table that exists it is made as two statements: the check added NOT VALID (SQLPG217), which reads no rows, then VALIDATE CONSTRAINT (SQLPG220) in a transaction of its own, which reads them under SHARE UPDATE EXCLUSIVE while writes go on. Rows the check refuses fail the validation; the first statement's pre-check counts them.",
|
|
141
141
|
ALTER_TABLE,
|
|
142
142
|
),
|
|
143
143
|
SQLPG219: rule(
|
|
144
144
|
"SQLPG219",
|
|
145
145
|
"validate",
|
|
146
146
|
"Add a foreign key",
|
|
147
|
-
"ADD FOREIGN KEY reads every row under SHARE ROW EXCLUSIVE on both tables, which blocks writes to them for the scan.
|
|
147
|
+
"ADD FOREIGN KEY alone reads every row under SHARE ROW EXCLUSIVE on both tables, which blocks writes to them for the scan. On a table that exists it is made as two statements: the key added NOT VALID (SQLPG217), which takes that lock briefly and reads no rows, then VALIDATE CONSTRAINT (SQLPG220) in a transaction of its own, which reads them under SHARE UPDATE EXCLUSIVE while writes go on. Orphan rows fail the validation; the first statement's pre-check counts them.",
|
|
148
148
|
ALTER_TABLE,
|
|
149
149
|
),
|
|
150
150
|
SQLPG220: rule(
|
package/src/serializer.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* The order is the dependency order of the references: a view after the
|
|
14
14
|
* tables it reads, a materialized view after its target, a table after its
|
|
15
|
-
* database.
|
|
15
|
+
* database. An object read from a `.sql` file (`./sql-files.ts`) that a
|
|
16
|
+
* template names in its text counts as referenced too. Ties break by export name, so the output is the same however the
|
|
16
17
|
* files were discovered. A reference cycle is an error naming the cycle.
|
|
17
18
|
*
|
|
18
19
|
* One build holds one dialect: the dialect is the document's, and a project
|
|
@@ -35,6 +36,7 @@ import { isClickHouseObject, type ClickHouseObject } from "./clickhouse/entities
|
|
|
35
36
|
import { isPostgresObject, type PostgresObject } from "./postgres/entities";
|
|
36
37
|
import { POSTGRES_LATEST_MAJOR } from "./spec/postgres-pin";
|
|
37
38
|
import { applyOrder, lineageJson, referenceName, type LineageEdge } from "./core/references";
|
|
39
|
+
import { sqlFileReferences } from "./sql-files";
|
|
38
40
|
|
|
39
41
|
export { applyOrder } from "./core/references";
|
|
40
42
|
|
|
@@ -44,7 +46,7 @@ export const CLICKHOUSE_DDL_FILE = "clickhouse.sql";
|
|
|
44
46
|
/** The Postgres statements' file, beside the primary output. */
|
|
45
47
|
export const POSTGRES_DDL_FILE = "postgres.sql";
|
|
46
48
|
|
|
47
|
-
function objectJson(name: string, obj: ClickHouseObject, names: Map<Declarable, string
|
|
49
|
+
function objectJson(name: string, obj: ClickHouseObject, names: Map<Declarable, string>, extra: readonly string[] = []): Record<string, unknown> {
|
|
48
50
|
const props = { ...(obj.props as Record<string, unknown>) };
|
|
49
51
|
delete props.source;
|
|
50
52
|
const ddl = props.ddl;
|
|
@@ -54,7 +56,7 @@ function objectJson(name: string, obj: ClickHouseObject, names: Map<Declarable,
|
|
|
54
56
|
if (props.lineage) {
|
|
55
57
|
props.lineage = lineageJson(props.lineage as LineageEdge[], names);
|
|
56
58
|
}
|
|
57
|
-
const dependsOn = [...new Set(obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined))];
|
|
59
|
+
const dependsOn = [...new Set([...obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined), ...extra])];
|
|
58
60
|
return JSON.parse(
|
|
59
61
|
JSON.stringify({ export: name, type: obj.entityType, sqlName: obj.sqlName, ...props, dependsOn, ddl }),
|
|
60
62
|
) as Record<string, unknown>;
|
|
@@ -68,12 +70,12 @@ function postgresJsonValue(v: unknown, names: Map<Declarable, string>): unknown
|
|
|
68
70
|
return v;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function postgresObjectJson(name: string, obj: PostgresObject, names: Map<Declarable, string
|
|
73
|
+
function postgresObjectJson(name: string, obj: PostgresObject, names: Map<Declarable, string>, extra: readonly string[] = []): Record<string, unknown> {
|
|
72
74
|
const props = { ...(obj.props as Record<string, unknown>) };
|
|
73
75
|
delete props.source;
|
|
74
76
|
const ddl = props.ddl;
|
|
75
77
|
delete props.ddl;
|
|
76
|
-
const dependsOn = [...new Set(obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined))];
|
|
78
|
+
const dependsOn = [...new Set([...obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined), ...extra])];
|
|
77
79
|
return JSON.parse(
|
|
78
80
|
JSON.stringify({ export: name, type: obj.entityType, sqlName: obj.sqlName, ...(postgresJsonValue(props, names) as object), dependsOn, ddl }),
|
|
79
81
|
) as Record<string, unknown>;
|
|
@@ -92,12 +94,14 @@ function serializePostgres(
|
|
|
92
94
|
): SerializerResult {
|
|
93
95
|
const names = new Map<Declarable, string>();
|
|
94
96
|
for (const [name, entity] of entities) names.set(entity, name);
|
|
95
|
-
|
|
97
|
+
// A template names an object read from a `.sql` file as text: that is an edge too.
|
|
98
|
+
const extra = sqlFileReferences("postgres", objects);
|
|
99
|
+
const order = applyOrder(objects, names, extra);
|
|
96
100
|
const doc = {
|
|
97
101
|
dialect: "postgres",
|
|
98
102
|
postgresMajor: targetMajor(config),
|
|
99
103
|
applyOrder: order,
|
|
100
|
-
objects: order.map((n) => postgresObjectJson(n, objects.get(n)!, names)),
|
|
104
|
+
objects: order.map((n) => postgresObjectJson(n, objects.get(n)!, names, extra.get(n))),
|
|
101
105
|
};
|
|
102
106
|
const ddl = order.map((n) => `${(objects.get(n)!.props as { ddl: string }).ddl};`).join("\n\n");
|
|
103
107
|
return {
|
|
@@ -130,11 +134,12 @@ export const sqlSerializer: Serializer = {
|
|
|
130
134
|
const names = new Map<Declarable, string>();
|
|
131
135
|
for (const [name, entity] of entities) names.set(entity, name);
|
|
132
136
|
|
|
133
|
-
const
|
|
137
|
+
const extra = sqlFileReferences("clickhouse", objects);
|
|
138
|
+
const order = applyOrder(objects, names, extra);
|
|
134
139
|
const doc = {
|
|
135
140
|
dialect: "clickhouse",
|
|
136
141
|
applyOrder: order,
|
|
137
|
-
objects: order.map((n) => objectJson(n, objects.get(n)!, names)),
|
|
142
|
+
objects: order.map((n) => objectJson(n, objects.get(n)!, names, extra.get(n))),
|
|
138
143
|
};
|
|
139
144
|
const ddl = order
|
|
140
145
|
.map((n) => `${(objects.get(n)!.props as { ddl: string }).ddl};`)
|
|
@@ -83,9 +83,9 @@ Every rule has an id in the SQLPG2xx range, names its class and cites the Postgr
|
|
|
83
83
|
| column type to a binary-coercible type (longer varchar, text, wider numeric) | SQLPG206 | metadata | indexes on the column may still be rebuilt |
|
|
84
84
|
| column type with a rewrite (integer to bigint, a shorter varchar) | SQLPG207 | rewrite | |
|
|
85
85
|
| column type across kinds (text to integer) | SQLPG208 | expand | needs USING and breaks readers |
|
|
86
|
-
| set NOT NULL | SQLPG210 |
|
|
87
|
-
| add a CHECK | SQLPG218 |
|
|
88
|
-
| add a foreign key | SQLPG219 | validate |
|
|
86
|
+
| set NOT NULL | SQLPG210 | validate | made as a `NOT VALID` check, `VALIDATE`, `SET NOT NULL`, the check dropped; a valid `CHECK (col IS NOT NULL)` already in place makes it metadata |
|
|
87
|
+
| add a CHECK | SQLPG218 | validate | made as `ADD CONSTRAINT ... NOT VALID`, then `VALIDATE` in its own transaction; writes go on during the scan |
|
|
88
|
+
| add a foreign key | SQLPG219 | validate | made as `ADD CONSTRAINT ... NOT VALID`, then `VALIDATE` in its own transaction; writes go on during the scan |
|
|
89
89
|
| add a constraint `NOT VALID` | SQLPG217 | metadata | checks new rows only |
|
|
90
90
|
| validate a constraint | SQLPG220 | validate | `SHARE UPDATE EXCLUSIVE`, so reads and writes go on |
|
|
91
91
|
| add a primary key or unique constraint | SQLPG221 | concurrently | build the unique index CONCURRENTLY, then `ADD CONSTRAINT ... USING INDEX` |
|
|
@@ -26,7 +26,7 @@ export const { op } = ClickHouseRebuildOp({
|
|
|
26
26
|
});
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Options: `name`, `env`, `table`, `dualWrite` (required); `output` (default `dist/schema.json`), `path`, `build` (default true runs `chant build` first), `retain` (default `7d`), `gate` (the swap gate: `gate`, `timeout`, `description`, `approval`), `dropGate`, `writesGate` (app mode), `backfillTimeout` (default `6h`), `mutationTimeout` (default `10m`), `replicaTimeout` (default `2m`, Replicated databases only), `stack`, `ownershipEnv`.
|
|
29
|
+
Options: `name`, `env`, `table`, `dualWrite` (required); `output` (default `dist/schema.json`), `path`, `build` (default true runs `chant build` first), `retain` (default `7d`), `gate` (the swap gate: `gate`, `timeout`, `description`, `approval`), `dropGate`, `writesGate` (app mode), `backfillTimeout` (default `6h`), `mutationTimeout` (default `10m`), `replicaTimeout` (default `2m`, Replicated databases only), `cutoverTimeout` (default `10m`, materialized-view mode only), `stack`, `ownershipEnv`.
|
|
30
30
|
|
|
31
31
|
On a `cluster:<name>` topology with more than one shard, the backfill and the verification work per shard through the profile's server, with a receipt per shard and partition; each shard needs its own `{shard}` macro.
|
|
32
32
|
|
|
@@ -34,7 +34,7 @@ Run as one step of a change approved elsewhere: `gates: "outer"` leaves out the
|
|
|
34
34
|
|
|
35
35
|
Pick the dual-write mode:
|
|
36
36
|
|
|
37
|
-
- `{ mode: "materialized-view", cutoverColumn: "ts", cutoverDelay?: "
|
|
37
|
+
- `{ mode: "materialized-view", cutoverColumn: "ts", cutoverDelay?: "5s" }` when the table has a time column rows arrive in order of. Writes keep flowing: a materialized view sends rows at or after the cut-over to the new table, the backfill copies the rows before it. The backfill starts once the server's clock has passed the cut-over and every INSERT into the old table begun before it has finished, asynchronous inserts still in their buffer included; `cutoverTimeout` bounds the second wait. Set `cutoverDelay` longer than the batches of a writer that holds rows before inserting them.
|
|
38
38
|
- `{ mode: "app" }` otherwise. A gate, `<name>-writes-stopped`, waits for someone to confirm the application stopped writing to the table; writes resume after the swap.
|
|
39
39
|
|
|
40
40
|
## Run it
|
|
@@ -57,6 +57,7 @@ The swap gate is bound to a digest of the plan and the verification (row counts
|
|
|
57
57
|
- A run stopped during the backfill (Ctrl-C, a killed job) is not a failure. The next run resumes from the receipts in `chant_receipts.receipts` on the same server (in `<db>.__chant_receipts` on a Replicated database), and a partition whose copy was cut off before its receipt is cleared and copied again, so no row is copied twice.
|
|
58
58
|
- A failed step (a failed copy after its retries, a verification mismatch, a refused declaration) runs onFailure: the new table `<t>__chant_new` and the dual-write view `<t>__chant_dual` are dropped, and the next run starts from a new table. With `onFailure: "keep"` they stay, and the next run resumes; drop them by hand to start over. The original table is untouched until the swap.
|
|
59
59
|
- A verification mismatch in materialized-view mode usually means rows arrived with a time before the cut-over later than `cutoverDelay`. Raise the delay and run again.
|
|
60
|
+
- A backfill that stops naming writes "begun before the cut-over" waited `cutoverTimeout` for INSERTs into the old table that were still running. Let them finish, or stop them, and run again.
|
|
60
61
|
- The Op refuses a database that is neither Atomic nor Replicated (`EXCHANGE TABLES` needs one of them), a table whose change is not a rebuild (use the applier), and an object already under one of its working names that is not its own. In a Replicated database it also refuses a table, or a declaration, whose engine is not `Replicated*MergeTree`: a plain MergeTree keeps different rows on each replica and one replica's copy would lose the others'.
|
|
61
62
|
|
|
62
63
|
## On a Replicated database
|
package/src/skills/chant-sql.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
skill: chant-sql
|
|
3
|
-
description: Declare ClickHouse databases, tables, views
|
|
3
|
+
description: Declare ClickHouse databases, tables, views, materialized views, dictionaries and functions as SQL-shaped tagged templates, with references, lineage and the checks that run on them
|
|
4
4
|
user-invocable: true
|
|
5
5
|
---
|
|
6
6
|
# Declaring a ClickHouse schema with chant
|
|
@@ -33,6 +33,12 @@ export const byKind = view`
|
|
|
33
33
|
|
|
34
34
|
The export name (`events`) is the object's identity in chant. The name in the SQL (`analytics.events`) is its name in the database. A materialized view is a `view` template holding `CREATE MATERIALIZED VIEW`.
|
|
35
35
|
|
|
36
|
+
A dictionary is a `dictionary` template holding `CREATE DICTIONARY`, with its attributes, `PRIMARY KEY`, `SOURCE`, `LAYOUT` and `LIFETIME`. Its attributes are columns (`${ratesDict.columns.rate}`). Any change but its comment is SQLCH245, and the applier replaces it with `CREATE OR REPLACE DICTIONARY`. Keep passwords out of `SOURCE`: name a server-side named collection instead.
|
|
37
|
+
|
|
38
|
+
A SQL user-defined function is a `func` template holding `CREATE FUNCTION name AS (x) -> expr`, with no database in its name. A changed expression is SQLCH260 (`CREATE OR REPLACE FUNCTION`). It carries no ownership marker, so chant never drops one.
|
|
39
|
+
|
|
40
|
+
Users, roles, row policies and grants are `user`, `role`, `policy` and `grant` templates. Never write a password: `IDENTIFIED BY` is refused, a user declared without `IDENTIFIED` is created by the environment with its password, and chant manages the rest of it. Grants are compared per grantee: every `grant` naming a grantee adds up to its complete list, and anything else it holds is revoked (SQLCH274). None of these is ever dropped by chant.
|
|
41
|
+
|
|
36
42
|
## Interpolations
|
|
37
43
|
|
|
38
44
|
| Value | Means | Renders as |
|