@intentius/chant-lexicon-sql 0.115.0 → 0.116.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/codegen/registry.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -5
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +15 -0
- package/dist/okf/index.md +3 -0
- package/dist/okf/types/PostgresFunction.md +9 -0
- package/dist/okf/types/PostgresProcedure.md +9 -0
- package/dist/okf/types/PostgresTrigger.md +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/apply.d.ts +6 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +9 -2
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +110 -3
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +3 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +16 -4
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +6 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +82 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +21 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +6 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +11 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +3 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +3 -0
- package/src/generated/lexicon-sql.json +15 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +3 -0
- package/src/lsp/postgres-template.ts +6 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/plugin.ts +3 -0
- package/src/postgres/apply/apply.ts +21 -5
- package/src/postgres/apply/statements.ts +53 -11
- package/src/postgres/entities.ts +332 -9
- package/src/postgres/entity-types.ts +3 -0
- package/src/postgres/import/generator.ts +5 -2
- package/src/postgres/import/live-export.ts +4 -0
- package/src/postgres/live/catalog.ts +78 -4
- package/src/postgres/live/describe-resources.ts +17 -7
- package/src/postgres/live/routines.e2e.test.ts +173 -0
- package/src/postgres/migrate/batches.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
- package/src/postgres/parser.ts +314 -8
- package/src/postgres/plan/commands.ts +2 -2
- package/src/postgres/plan/deep.ts +57 -17
- package/src/postgres/plan/diff.ts +46 -6
- package/src/postgres/plan/normalize.ts +158 -4
- package/src/postgres/plan/rules.ts +36 -0
- package/src/postgres/plan/schema.ts +25 -6
- package/src/postgres/plan/server-normalize.ts +24 -0
- package/src/postgres/routines.test.ts +150 -0
- package/src/postgres.ts +11 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +18 -2
- package/src/validate.ts +3 -0
|
@@ -80,9 +80,17 @@ export const pgString = (value: string): string => `'${value.replace(/'/g, "''")
|
|
|
80
80
|
/** `schema.name`, each part quoted where Postgres needs it. */
|
|
81
81
|
export const pgQualified = (schema: string | undefined, name: string): string => (schema ? `${quoteIdent(schema)}.${quoteIdent(name)}` : quoteIdent(name));
|
|
82
82
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
/**
|
|
84
|
+
* An object's name in a statement. A schema and an extension have no schema;
|
|
85
|
+
* a routine is named with its parameter types (`app.f(integer,text)`), a
|
|
86
|
+
* trigger with its table (`touch ON app.users`).
|
|
87
|
+
*/
|
|
88
|
+
export const objectIdent = (o: { kind: CanonicalKind; schema?: string; name: string; signature?: string }): string =>
|
|
89
|
+
o.kind === "schema" || o.kind === "extension"
|
|
90
|
+
? quoteIdent(o.name)
|
|
91
|
+
: o.kind === "trigger"
|
|
92
|
+
? `${quoteIdent(o.name)}${o.signature ?? ""}`
|
|
93
|
+
: `${pgQualified(o.schema, o.name)}${o.signature ?? ""}`;
|
|
86
94
|
|
|
87
95
|
/** The words a statement names the object's kind with. */
|
|
88
96
|
export const KIND_WORDS: Readonly<Record<CanonicalKind, string>> = {
|
|
@@ -95,6 +103,9 @@ export const KIND_WORDS: Readonly<Record<CanonicalKind, string>> = {
|
|
|
95
103
|
enum: "TYPE",
|
|
96
104
|
domain: "DOMAIN",
|
|
97
105
|
extension: "EXTENSION",
|
|
106
|
+
function: "FUNCTION",
|
|
107
|
+
procedure: "PROCEDURE",
|
|
108
|
+
trigger: "TRIGGER",
|
|
98
109
|
};
|
|
99
110
|
|
|
100
111
|
/** The canonical kind of an entity type. */
|
|
@@ -168,7 +179,8 @@ export function createSteps(
|
|
|
168
179
|
splitStatements(obj.ddl).forEach((s, i) => {
|
|
169
180
|
if (i > 0 && isOwnComment(s, obj.canonical.kind)) return;
|
|
170
181
|
let sql = s.text;
|
|
171
|
-
|
|
182
|
+
const replaceable = obj.canonical.kind === "view" || obj.canonical.kind === "function" || obj.canonical.kind === "procedure" || (obj.canonical.kind === "trigger" && obj.canonical.fields.constraint !== true);
|
|
183
|
+
if (i === 0 && opts.orReplace && replaceable && !/^\s*CREATE\s+OR\s+REPLACE\b/i.test(sql)) sql = sql.replace(/\bCREATE\s+/i, "CREATE OR REPLACE ");
|
|
172
184
|
const concurrently = i === 0 && obj.canonical.kind === "index" && obj.props.concurrently === true;
|
|
173
185
|
out.push({ ...step(sql, cls, opts.rule, !concurrently), ...(concurrently ? { buildsIndex: objectIdent(obj.canonical) } : {}) });
|
|
174
186
|
});
|
|
@@ -177,10 +189,10 @@ export function createSteps(
|
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
/** The `DROP` for an object a prune removes. Never `CASCADE`: what something else still uses is kept. */
|
|
180
|
-
export function dropStatement(kind: CanonicalKind, schema: string | undefined, name: string): PgStep {
|
|
181
|
-
const what = objectIdent({ kind, ...(schema !== undefined ? { schema } : {}), name });
|
|
192
|
+
export function dropStatement(kind: CanonicalKind, schema: string | undefined, name: string, signature?: string): PgStep {
|
|
193
|
+
const what = objectIdent({ kind, ...(schema !== undefined ? { schema } : {}), name, ...(signature !== undefined ? { signature } : {}) });
|
|
182
194
|
if (kind === "index") return step(`DROP INDEX CONCURRENTLY ${what}`, "concurrently", "SQLPG242", false);
|
|
183
|
-
return step(`DROP ${KIND_WORDS[kind]} ${what}`, "drop", "SQLPG270");
|
|
195
|
+
return step(`DROP ${KIND_WORDS[kind]} ${what}`, "drop", kind === "trigger" ? "SQLPG285" : "SQLPG270");
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
// ── Columns ────────────────────────────────────────────────────────────
|
|
@@ -403,7 +415,7 @@ export const isRefused = (c: PgChange): boolean => c.class === "expand";
|
|
|
403
415
|
export const isDestructive = (c: PgChange): boolean => c.rule === "SQLPG204";
|
|
404
416
|
|
|
405
417
|
/** Changes that replace the object: drop it and create it again from its declaration. */
|
|
406
|
-
export const isRecreate = (c: PgChange): boolean => c.rule === "SQLPG252" || c.rule === "SQLPG243";
|
|
418
|
+
export const isRecreate = (c: PgChange): boolean => c.rule === "SQLPG252" || c.rule === "SQLPG243" || c.rule === "SQLPG281";
|
|
407
419
|
|
|
408
420
|
/**
|
|
409
421
|
* The statements for an object that exists on the server, change by change.
|
|
@@ -538,8 +550,15 @@ export function alterSteps(
|
|
|
538
550
|
steps.push(step(`ALTER INDEX ${objectIdent(live)} RENAME TO ${quoteIdent(d.name)}`, c.class, c.rule));
|
|
539
551
|
break;
|
|
540
552
|
case "SQLPG250":
|
|
553
|
+
case "SQLPG280":
|
|
541
554
|
steps.push(...createSteps(obj, opts.marker, { orReplace: true, cls: c.class, rule: c.rule }).slice(0, 1));
|
|
542
555
|
break;
|
|
556
|
+
case "SQLPG284":
|
|
557
|
+
if (d.fields.constraint === true || live.fields.constraint === true || d.signature !== live.signature) {
|
|
558
|
+
steps.push(step(`DROP TRIGGER ${objectIdent(live)}`, c.class, c.rule));
|
|
559
|
+
steps.push(...createSteps(obj, opts.marker, { cls: c.class, rule: c.rule }));
|
|
560
|
+
} else steps.push(...createSteps(obj, opts.marker, { orReplace: true, cls: c.class, rule: c.rule }).slice(0, 1));
|
|
561
|
+
break;
|
|
543
562
|
case "SQLPG260": {
|
|
544
563
|
const before = (live.fields.labels as string[] | undefined) ?? [];
|
|
545
564
|
const after = (d.fields.labels as string[] | undefined) ?? [];
|
|
@@ -631,6 +650,8 @@ export interface PgDropStatement {
|
|
|
631
650
|
kind: CanonicalKind;
|
|
632
651
|
schema?: string;
|
|
633
652
|
name: string;
|
|
653
|
+
/** A routine's parameter types, a trigger's table. */
|
|
654
|
+
signature?: string;
|
|
634
655
|
step: PgStep;
|
|
635
656
|
}
|
|
636
657
|
|
|
@@ -663,7 +684,21 @@ export interface PgStatementPlan {
|
|
|
663
684
|
}
|
|
664
685
|
|
|
665
686
|
/** Drop order: what reads from a table before the table, types after the tables using them, a schema last. */
|
|
666
|
-
const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
687
|
+
const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
688
|
+
trigger: 0,
|
|
689
|
+
materializedView: 1,
|
|
690
|
+
view: 2,
|
|
691
|
+
index: 3,
|
|
692
|
+
table: 4,
|
|
693
|
+
sequence: 5,
|
|
694
|
+
// A routine after what calls it (a view, a trigger, a default), before the types its parameters use.
|
|
695
|
+
function: 6,
|
|
696
|
+
procedure: 6,
|
|
697
|
+
domain: 7,
|
|
698
|
+
enum: 8,
|
|
699
|
+
extension: 9,
|
|
700
|
+
schema: 10,
|
|
701
|
+
};
|
|
667
702
|
|
|
668
703
|
/**
|
|
669
704
|
* The statements that take the current schema to the declared one, from the
|
|
@@ -735,11 +770,18 @@ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
|
|
|
735
770
|
|
|
736
771
|
const drops: PgDropStatement[] = [];
|
|
737
772
|
for (const c of input.changes) {
|
|
738
|
-
if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242") && c.after === undefined)) continue;
|
|
773
|
+
if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242" || c.rule === "SQLPG285") && c.after === undefined)) continue;
|
|
739
774
|
const o = currentByKey.get(c.object);
|
|
740
775
|
if (!o || o.foreign) continue;
|
|
741
776
|
const schema = o.kind === "schema" || o.kind === "extension" ? undefined : o.schema;
|
|
742
|
-
drops.push({
|
|
777
|
+
drops.push({
|
|
778
|
+
key: c.object,
|
|
779
|
+
kind: o.kind,
|
|
780
|
+
...(schema !== undefined ? { schema } : {}),
|
|
781
|
+
name: o.name,
|
|
782
|
+
...(o.signature !== undefined ? { signature: o.signature } : {}),
|
|
783
|
+
step: dropStatement(o.kind, schema, o.name, o.signature),
|
|
784
|
+
});
|
|
743
785
|
}
|
|
744
786
|
drops.sort((a, b) => DROP_ORDER[a.kind] - DROP_ORDER[b.kind]);
|
|
745
787
|
return { objects, drops };
|
package/src/postgres/entities.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Postgres entities: `schema`, `table`, `index`, `view`, `sequence`,
|
|
3
|
-
* `type`, `domain` and `
|
|
4
|
-
* fold time into one Declarable each
|
|
3
|
+
* `type`, `domain`, `extension`, `func`, `procedure` and `trigger` tagged
|
|
4
|
+
* templates that parse their DDL at fold time into one Declarable each
|
|
5
|
+
* (chant #3278, #3279, #3680).
|
|
5
6
|
*
|
|
6
7
|
* ```ts
|
|
7
8
|
* import { schema, table, index } from "@intentius/chant-lexicon-sql/postgres";
|
|
@@ -40,6 +41,12 @@
|
|
|
40
41
|
* - `literal(value)` is a quoted string literal (`'` doubled; a backslash is an
|
|
41
42
|
* ordinary character, since standard_conforming_strings is on).
|
|
42
43
|
*
|
|
44
|
+
* A function's or procedure's body is a string constant (`AS $$ ... $$`), kept
|
|
45
|
+
* verbatim. An interpolation inside it renders as text (an object as its
|
|
46
|
+
* schema-qualified name, a column reference as the column's name) and is
|
|
47
|
+
* recorded as a reference, so the routine is created after what its body
|
|
48
|
+
* names: `` func`... AS $$ SELECT count(*) FROM ${users} $$` ``.
|
|
49
|
+
*
|
|
43
50
|
* Postgres has no inline comment clause, so a template may follow its CREATE
|
|
44
51
|
* with `COMMENT ON` statements for the same object, its columns and its
|
|
45
52
|
* constraints. They fold into the entity's `comment`, each column's and each
|
|
@@ -258,6 +265,79 @@ export interface ExtensionProps extends Omit<CommonProps, "schema"> {
|
|
|
258
265
|
ifNotExists?: boolean;
|
|
259
266
|
}
|
|
260
267
|
|
|
268
|
+
/** One parameter of a function or procedure, as written. */
|
|
269
|
+
export interface RoutineArgDef {
|
|
270
|
+
/** `in` when not written. */
|
|
271
|
+
mode: "in" | "out" | "inout" | "variadic";
|
|
272
|
+
name?: string;
|
|
273
|
+
type: string;
|
|
274
|
+
default?: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface RoutineProps extends CommonProps {
|
|
278
|
+
orReplace?: boolean;
|
|
279
|
+
args: RoutineArgDef[];
|
|
280
|
+
/** `RETURNS type`, `SETOF` included (`setof app.users`). */
|
|
281
|
+
returns?: string;
|
|
282
|
+
/** `RETURNS TABLE (...)`'s columns. */
|
|
283
|
+
returnsTable?: Array<{ name: string; type: string }>;
|
|
284
|
+
language?: string;
|
|
285
|
+
volatility?: "immutable" | "stable" | "volatile";
|
|
286
|
+
/** `STRICT` / `RETURNS NULL ON NULL INPUT` (true) or `CALLED ON NULL INPUT` (false). */
|
|
287
|
+
strict?: boolean;
|
|
288
|
+
/** `SECURITY DEFINER` (true) or `SECURITY INVOKER` (false). */
|
|
289
|
+
securityDefiner?: boolean;
|
|
290
|
+
leakproof?: boolean;
|
|
291
|
+
parallel?: "unsafe" | "restricted" | "safe";
|
|
292
|
+
cost?: string;
|
|
293
|
+
rows?: string;
|
|
294
|
+
support?: string;
|
|
295
|
+
window?: boolean;
|
|
296
|
+
/** `TRANSFORM FOR TYPE ...`, as written. */
|
|
297
|
+
transform?: string;
|
|
298
|
+
/** `SET name = value` clauses: each parameter, folded, to its value as written. */
|
|
299
|
+
set?: Record<string, string>;
|
|
300
|
+
/** The definition: the string constant's value, verbatim. */
|
|
301
|
+
body: string;
|
|
302
|
+
/** A C function's link symbol (`AS 'obj_file', 'link_symbol'`). */
|
|
303
|
+
link?: string;
|
|
304
|
+
/** The objects the body interpolates. */
|
|
305
|
+
reads: PostgresObject[];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** `func` declares a function; `procedure` a procedure, which has no result and none of a function's planner attributes. */
|
|
309
|
+
export type FunctionProps = RoutineProps;
|
|
310
|
+
export type ProcedureProps = Omit<RoutineProps, "returns" | "returnsTable" | "volatility" | "strict" | "leakproof" | "parallel" | "cost" | "rows" | "support" | "window">;
|
|
311
|
+
|
|
312
|
+
export interface TriggerProps extends CommonProps {
|
|
313
|
+
orReplace?: boolean;
|
|
314
|
+
/** `CREATE CONSTRAINT TRIGGER`. */
|
|
315
|
+
constraint?: boolean;
|
|
316
|
+
timing: "before" | "after" | "instead of";
|
|
317
|
+
/** The events, each `insert`, `update`, `delete` or `truncate`; an `update` may name its columns. */
|
|
318
|
+
events: Array<{ event: "insert" | "update" | "delete" | "truncate"; columns?: string[] }>;
|
|
319
|
+
/** The table or view: the entity when interpolated, else its name. */
|
|
320
|
+
table: PostgresObject | string;
|
|
321
|
+
/** The table's SQL name. */
|
|
322
|
+
tableName: string;
|
|
323
|
+
/** A constraint trigger's `FROM` table's SQL name. */
|
|
324
|
+
from?: string;
|
|
325
|
+
deferrable?: boolean;
|
|
326
|
+
initiallyDeferred?: boolean;
|
|
327
|
+
/** `REFERENCING OLD TABLE AS ...` / `NEW TABLE AS ...`. */
|
|
328
|
+
referencing?: { old?: string; new?: string };
|
|
329
|
+
/** `FOR EACH ROW` or `FOR EACH STATEMENT` (the default). */
|
|
330
|
+
forEach: "row" | "statement";
|
|
331
|
+
/** The `WHEN` condition, parentheses excluded. */
|
|
332
|
+
when?: string;
|
|
333
|
+
/** The function it executes: the entity when interpolated, else its name. */
|
|
334
|
+
function: PostgresObject | string;
|
|
335
|
+
/** The function's SQL name. */
|
|
336
|
+
functionName: string;
|
|
337
|
+
/** The arguments, as written. */
|
|
338
|
+
args: string[];
|
|
339
|
+
}
|
|
340
|
+
|
|
261
341
|
/**
|
|
262
342
|
* A string as a quoted Postgres string literal: `'` doubled, a backslash left
|
|
263
343
|
* as written (standard_conforming_strings is on). An interpolated plain string
|
|
@@ -324,6 +404,21 @@ export interface PostgresExtension extends PostgresObject {
|
|
|
324
404
|
readonly props: ExtensionProps;
|
|
325
405
|
}
|
|
326
406
|
|
|
407
|
+
export interface PostgresFunction extends PostgresObject {
|
|
408
|
+
readonly entityType: "Postgres::Function";
|
|
409
|
+
readonly props: FunctionProps;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export interface PostgresProcedure extends PostgresObject {
|
|
413
|
+
readonly entityType: "Postgres::Procedure";
|
|
414
|
+
readonly props: ProcedureProps;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface PostgresTrigger extends PostgresObject {
|
|
418
|
+
readonly entityType: "Postgres::Trigger";
|
|
419
|
+
readonly props: TriggerProps;
|
|
420
|
+
}
|
|
421
|
+
|
|
327
422
|
export function isPostgresObject(value: unknown): value is PostgresObject {
|
|
328
423
|
return isSqlObjectOf(value, "Postgres::");
|
|
329
424
|
}
|
|
@@ -426,7 +521,7 @@ export const POSTGRES_TEMPLATES: TemplateDialect = {
|
|
|
426
521
|
|
|
427
522
|
type Ctx = TemplateCtx;
|
|
428
523
|
|
|
429
|
-
export type PostgresTag = "schema" | "table" | "index" | "view" | "sequence" | "type" | "domain" | "extension";
|
|
524
|
+
export type PostgresTag = "schema" | "table" | "index" | "view" | "sequence" | "type" | "domain" | "extension" | "func" | "procedure" | "trigger";
|
|
430
525
|
|
|
431
526
|
const STATEMENT_OF: Record<PostgresTag, StatementNode["statement"]> = {
|
|
432
527
|
schema: "schema",
|
|
@@ -437,6 +532,9 @@ const STATEMENT_OF: Record<PostgresTag, StatementNode["statement"]> = {
|
|
|
437
532
|
type: "enum",
|
|
438
533
|
domain: "domain",
|
|
439
534
|
extension: "extension",
|
|
535
|
+
func: "function",
|
|
536
|
+
procedure: "procedure",
|
|
537
|
+
trigger: "trigger",
|
|
440
538
|
};
|
|
441
539
|
|
|
442
540
|
const TAG_OF: Record<StatementNode["statement"], PostgresTag | undefined> = {
|
|
@@ -448,6 +546,9 @@ const TAG_OF: Record<StatementNode["statement"], PostgresTag | undefined> = {
|
|
|
448
546
|
enum: "type",
|
|
449
547
|
domain: "domain",
|
|
450
548
|
extension: "extension",
|
|
549
|
+
function: "func",
|
|
550
|
+
procedure: "procedure",
|
|
551
|
+
trigger: "trigger",
|
|
451
552
|
comment: undefined,
|
|
452
553
|
};
|
|
453
554
|
|
|
@@ -461,6 +562,9 @@ export const STATEMENT_NAMES: Record<StatementNode["statement"], string> = {
|
|
|
461
562
|
enum: "CREATE TYPE",
|
|
462
563
|
domain: "CREATE DOMAIN",
|
|
463
564
|
extension: "CREATE EXTENSION",
|
|
565
|
+
function: "CREATE FUNCTION",
|
|
566
|
+
procedure: "CREATE PROCEDURE",
|
|
567
|
+
trigger: "CREATE TRIGGER",
|
|
464
568
|
comment: "COMMENT ON",
|
|
465
569
|
};
|
|
466
570
|
|
|
@@ -678,7 +782,10 @@ function applyComments(
|
|
|
678
782
|
if (value !== undefined) constraint!.comment = value;
|
|
679
783
|
} else {
|
|
680
784
|
if (!objectTypes.includes(c.objectType)) refuse(c, `does not comment on the ${tag} this template declares`);
|
|
681
|
-
if (
|
|
785
|
+
if (c.objectType === "TRIGGER") {
|
|
786
|
+
// `COMMENT ON TRIGGER name ON table`: the name is the trigger's, the table its own.
|
|
787
|
+
if (qualified(ctx, c.target).name !== self.name || !c.on || !isSelf({ ...qualified(ctx, c.on), name: self.name })) refuse(c, `names another trigger than ${self.name}`);
|
|
788
|
+
} else if (!isSelf(qualified(ctx, c.target))) refuse(c, `names another object than ${sqlNameOf(self)}`);
|
|
682
789
|
comment = value;
|
|
683
790
|
}
|
|
684
791
|
}
|
|
@@ -694,8 +801,108 @@ function lastDot(ctx: Ctx, span: Span): number {
|
|
|
694
801
|
return span.from;
|
|
695
802
|
}
|
|
696
803
|
|
|
697
|
-
|
|
698
|
-
|
|
804
|
+
/**
|
|
805
|
+
* The interpolations that sit inside a string constant (a routine's
|
|
806
|
+
* `AS $$ ... $$` body), rendered into the text around them: a string
|
|
807
|
+
* constant is one token, so the template is joined there before it is
|
|
808
|
+
* tokenized. Returns the joined parts and values, the original index of each
|
|
809
|
+
* value kept, and the objects the inlined interpolations name.
|
|
810
|
+
*/
|
|
811
|
+
function inlineQuoted(parts: readonly string[], values: readonly unknown[]): { parts: string[]; values: unknown[]; original: number[]; inlined: number[]; refs: unknown[] } {
|
|
812
|
+
// The lexical state at the end of each part: inside a '...' string, a $tag$ string, or neither.
|
|
813
|
+
type State = { kind: "none" } | { kind: "single"; escapes: boolean } | { kind: "dollar"; tag: string } | { kind: "dquote" } | { kind: "line" } | { kind: "block"; depth: number };
|
|
814
|
+
let state: State = { kind: "none" };
|
|
815
|
+
const insideAt: Array<"single" | "dollar" | undefined> = [];
|
|
816
|
+
parts.forEach((src, part) => {
|
|
817
|
+
for (let i = 0; i < src.length; i++) {
|
|
818
|
+
const c = src[i]!;
|
|
819
|
+
const rest = src.slice(i);
|
|
820
|
+
switch (state.kind) {
|
|
821
|
+
case "none": {
|
|
822
|
+
if (c === "'") state = { kind: "single", escapes: /[Ee]$/.test(src.slice(0, i)) && !/[A-Za-z0-9_$][Ee]$/.test(src.slice(0, i)) };
|
|
823
|
+
else if (c === '"') state = { kind: "dquote" };
|
|
824
|
+
else if (rest.startsWith("--")) state = { kind: "line" };
|
|
825
|
+
else if (rest.startsWith("/*")) {
|
|
826
|
+
state = { kind: "block", depth: 1 };
|
|
827
|
+
i++;
|
|
828
|
+
} else if (c === "$" && !/[A-Za-z0-9_$]$/.test(src.slice(0, i))) {
|
|
829
|
+
const m = /^\$([A-Za-z_\u0080-\uffff][A-Za-z0-9_\u0080-\uffff]*)?\$/.exec(rest);
|
|
830
|
+
if (m) {
|
|
831
|
+
state = { kind: "dollar", tag: m[0] };
|
|
832
|
+
i += m[0].length - 1;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
case "single":
|
|
838
|
+
if (state.escapes && c === "\\") i++;
|
|
839
|
+
else if (c === "'") {
|
|
840
|
+
if (src[i + 1] === "'") i++;
|
|
841
|
+
else state = { kind: "none" };
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
case "dquote":
|
|
845
|
+
if (c === '"') {
|
|
846
|
+
if (src[i + 1] === '"') i++;
|
|
847
|
+
else state = { kind: "none" };
|
|
848
|
+
}
|
|
849
|
+
break;
|
|
850
|
+
case "dollar":
|
|
851
|
+
if (rest.startsWith(state.tag)) {
|
|
852
|
+
i += state.tag.length - 1;
|
|
853
|
+
state = { kind: "none" };
|
|
854
|
+
}
|
|
855
|
+
break;
|
|
856
|
+
case "line":
|
|
857
|
+
if (c === "\n") state = { kind: "none" };
|
|
858
|
+
break;
|
|
859
|
+
case "block":
|
|
860
|
+
if (rest.startsWith("/*")) {
|
|
861
|
+
state.depth++;
|
|
862
|
+
i++;
|
|
863
|
+
} else if (rest.startsWith("*/")) {
|
|
864
|
+
i++;
|
|
865
|
+
state = state.depth === 1 ? { kind: "none" } : { kind: "block", depth: state.depth - 1 };
|
|
866
|
+
}
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (part < parts.length - 1) insideAt.push(state.kind === "single" ? "single" : state.kind === "dollar" ? "dollar" : undefined);
|
|
871
|
+
});
|
|
872
|
+
if (!insideAt.some(Boolean)) return { parts: [...parts], values: [...values], original: values.map((_, i) => i), inlined: [], refs: [] };
|
|
873
|
+
const out: string[] = [parts[0]!];
|
|
874
|
+
const outValues: unknown[] = [];
|
|
875
|
+
const original: number[] = [];
|
|
876
|
+
const inlined: number[] = [];
|
|
877
|
+
const refs: unknown[] = [];
|
|
878
|
+
values.forEach((v, i) => {
|
|
879
|
+
if (!insideAt[i]) {
|
|
880
|
+
outValues.push(v);
|
|
881
|
+
original.push(i);
|
|
882
|
+
out.push(parts[i + 1]!);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
let text: string;
|
|
886
|
+
if (isPostgresObject(v) || isColumnRef(v)) {
|
|
887
|
+
text = renderReference(v);
|
|
888
|
+
refs.push(v);
|
|
889
|
+
} else if (v instanceof SqlLiteral) text = v.sql;
|
|
890
|
+
else if (v === null) text = "NULL";
|
|
891
|
+
else if (typeof v === "string" || typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") text = String(v);
|
|
892
|
+
else throw new SqlTemplateError("func", `the interpolation inside the body is ${describe(v)}; a body takes an object, a column reference, a string or a number`, i, parts[i]!.length);
|
|
893
|
+
// Inside a '...' string a quote is doubled.
|
|
894
|
+
out[out.length - 1] += (insideAt[i] === "single" ? text.replace(/'/g, "''") : text) + parts[i + 1]!;
|
|
895
|
+
inlined.push(i);
|
|
896
|
+
});
|
|
897
|
+
return { parts: out, values: outValues, original, inlined, refs };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string[], rawValues: readonly unknown[]): PostgresObject {
|
|
901
|
+
const rawParts = templateParts(strings);
|
|
902
|
+
const routine = tag === "func" || tag === "procedure";
|
|
903
|
+
const joined = routine ? inlineQuoted(rawParts, rawValues) : undefined;
|
|
904
|
+
const parts = joined ? joined.parts : rawParts;
|
|
905
|
+
const values = joined ? joined.values : rawValues;
|
|
699
906
|
const tokens = spliceWith(POSTGRES_TEMPLATES, tag, parts, values);
|
|
700
907
|
let nodes: StatementNode[];
|
|
701
908
|
try {
|
|
@@ -723,11 +930,18 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
723
930
|
const ctx: Ctx = { d: { ...POSTGRES_TEMPLATES, renderReference: render }, tokens, values, fed: values.map(() => new Set<string>()) };
|
|
724
931
|
for (const t of tokens) if (t.splice !== undefined) ctx.fed[t.splice]!.add("ddl");
|
|
725
932
|
const ddl = untokenize(tokens, (i) => render(values[i], i)).trim().replace(/;\s*$/, "");
|
|
726
|
-
const source = { strings:
|
|
727
|
-
const dependsOn = [...new Set(values.filter((v) => isPostgresObject(v) || isColumnRef(v)))];
|
|
933
|
+
const source = { strings: rawParts };
|
|
934
|
+
const dependsOn = [...new Set([...values, ...(joined?.refs ?? [])].filter((v) => isPostgresObject(v) || isColumnRef(v)))];
|
|
728
935
|
const make = <T extends PostgresObject>(type: PostgresEntityType, q: { schema?: string; name: string }, props: object, columnNames?: readonly string[]): T => {
|
|
729
936
|
const entity = makeSqlEntity(PostgresObject.prototype as PostgresObject, type, sqlNameOf(q), props, columnNames, dependsOn) as T;
|
|
730
|
-
|
|
937
|
+
const fed = ctx.fed.map((paths) => [...paths].sort());
|
|
938
|
+
if (!joined) setInterpolationFields(entity, fed);
|
|
939
|
+
else {
|
|
940
|
+
// Back to the template's own interpolations: one inlined into the body feeds the body.
|
|
941
|
+
const byOriginal: string[][] = rawValues.map(() => ["body", "ddl"]);
|
|
942
|
+
joined.original.forEach((orig, i) => (byOriginal[orig] = fed[i]!));
|
|
943
|
+
setInterpolationFields(entity, byOriginal);
|
|
944
|
+
}
|
|
731
945
|
return entity;
|
|
732
946
|
};
|
|
733
947
|
|
|
@@ -970,6 +1184,96 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
970
1184
|
feed(ctx, node.query, "lineage");
|
|
971
1185
|
return make(node.materialized ? POSTGRES_ENTITY_TYPES.materializedView : POSTGRES_ENTITY_TYPES.view, q, props, outputs);
|
|
972
1186
|
}
|
|
1187
|
+
case "function":
|
|
1188
|
+
case "procedure": {
|
|
1189
|
+
const q = qualified(ctx, node.name);
|
|
1190
|
+
const comment = applyComments(tag, ctx, comments, q, node.statement === "function" ? ["FUNCTION", "ROUTINE"] : ["PROCEDURE", "ROUTINE"]);
|
|
1191
|
+
const flag = (o: string) => node.options.find((x) => x.option === o)?.flag;
|
|
1192
|
+
const value = (o: string, path: string) => {
|
|
1193
|
+
const x = node.options.find((y) => y.option === o);
|
|
1194
|
+
return x?.value ? text(ctx, x.value, path) : undefined;
|
|
1195
|
+
};
|
|
1196
|
+
const set: Record<string, string> = {};
|
|
1197
|
+
for (const s of node.set) set[s.name] = req(text(ctx, s.value, "set"));
|
|
1198
|
+
const [def, link] = node.body.map((b) => stringValue(text(ctx, b, "body")));
|
|
1199
|
+
const lang = text(ctx, node.language, "language");
|
|
1200
|
+
const props: RoutineProps = strip(
|
|
1201
|
+
{
|
|
1202
|
+
...q,
|
|
1203
|
+
orReplace: node.orReplace || undefined,
|
|
1204
|
+
args: node.args.map((a) =>
|
|
1205
|
+
stripUnset({
|
|
1206
|
+
mode: (a.mode?.toLowerCase() ?? "in") as RoutineArgDef["mode"],
|
|
1207
|
+
name: a.name,
|
|
1208
|
+
type: req(text(ctx, a.type, "args")),
|
|
1209
|
+
default: text(ctx, a.default, "args"),
|
|
1210
|
+
}),
|
|
1211
|
+
),
|
|
1212
|
+
returns: node.returns ? `${node.returns.setof ? "SETOF " : ""}${req(text(ctx, node.returns.type, "returns"))}` : undefined,
|
|
1213
|
+
returnsTable: node.returnsTable?.map((c) => ({ name: c.name, type: req(text(ctx, c.type, "returns")) })),
|
|
1214
|
+
language: lang === undefined ? undefined : lang.startsWith("'") ? stringValue(lang) : identValue(lang),
|
|
1215
|
+
volatility: value("VOLATILITY", "volatility")?.toLowerCase() as RoutineProps["volatility"],
|
|
1216
|
+
strict: flag("STRICT"),
|
|
1217
|
+
securityDefiner: flag("SECURITY"),
|
|
1218
|
+
leakproof: flag("LEAKPROOF"),
|
|
1219
|
+
parallel: value("PARALLEL", "parallel")?.toLowerCase() as RoutineProps["parallel"],
|
|
1220
|
+
cost: value("COST", "cost"),
|
|
1221
|
+
rows: value("ROWS", "rows"),
|
|
1222
|
+
support: value("SUPPORT", "support"),
|
|
1223
|
+
window: flag("WINDOW"),
|
|
1224
|
+
transform: value("TRANSFORM", "transform"),
|
|
1225
|
+
set: Object.keys(set).length > 0 ? set : undefined,
|
|
1226
|
+
body: req(def),
|
|
1227
|
+
link,
|
|
1228
|
+
reads: (joined?.refs ?? []).filter((v): v is PostgresObject => isPostgresObject(v)),
|
|
1229
|
+
comment,
|
|
1230
|
+
ddl,
|
|
1231
|
+
source,
|
|
1232
|
+
},
|
|
1233
|
+
["args", "reads"],
|
|
1234
|
+
);
|
|
1235
|
+
for (const k of ["strict", "securityDefiner", "leakproof"] as const) if (flag(k === "securityDefiner" ? "SECURITY" : k.toUpperCase()) === false) props[k] = false;
|
|
1236
|
+
return make(node.statement === "function" ? POSTGRES_ENTITY_TYPES.function : POSTGRES_ENTITY_TYPES.procedure, q, props);
|
|
1237
|
+
}
|
|
1238
|
+
case "trigger": {
|
|
1239
|
+
const tq = qualified(ctx, node.table);
|
|
1240
|
+
const name = node.name.pieces[0] || identValue(req(text(ctx, node.name.span)));
|
|
1241
|
+
feed(ctx, node.name.span, "name");
|
|
1242
|
+
// A trigger is its table's: it is named in the table's schema.
|
|
1243
|
+
const q = { schema: tq.schema, name };
|
|
1244
|
+
const comment = applyComments(tag, ctx, comments, q, ["TRIGGER"]);
|
|
1245
|
+
const referencing: { old?: string; new?: string } = {};
|
|
1246
|
+
for (const r of node.referencing) referencing[r.which === "OLD" ? "old" : "new"] = r.name;
|
|
1247
|
+
const props: TriggerProps = strip(
|
|
1248
|
+
{
|
|
1249
|
+
...q,
|
|
1250
|
+
orReplace: node.orReplace || undefined,
|
|
1251
|
+
constraint: node.constraint || undefined,
|
|
1252
|
+
timing: node.timing.toLowerCase() as TriggerProps["timing"],
|
|
1253
|
+
events: node.events.map((e) => {
|
|
1254
|
+
for (const c of e.columns) feed(ctx, c.span, "events");
|
|
1255
|
+
const columns = e.columns.map((c) => columnName(ctx, c));
|
|
1256
|
+
return { event: e.event.toLowerCase() as TriggerProps["events"][number]["event"], ...(columns.length > 0 ? { columns } : {}) };
|
|
1257
|
+
}),
|
|
1258
|
+
table: target(ctx, node.table),
|
|
1259
|
+
tableName: sqlNameOf(tq),
|
|
1260
|
+
from: node.from ? targetName(ctx, node.from) : undefined,
|
|
1261
|
+
deferrable: node.deferrable,
|
|
1262
|
+
initiallyDeferred: node.initiallyDeferred,
|
|
1263
|
+
referencing: Object.keys(referencing).length > 0 ? referencing : undefined,
|
|
1264
|
+
forEach: (node.forEach ?? "STATEMENT").toLowerCase() as TriggerProps["forEach"],
|
|
1265
|
+
when: text(ctx, node.when, "when"),
|
|
1266
|
+
function: target(ctx, node.function),
|
|
1267
|
+
functionName: targetName(ctx, node.function),
|
|
1268
|
+
args: node.args.map((a) => req(text(ctx, a, "args"))),
|
|
1269
|
+
comment,
|
|
1270
|
+
ddl,
|
|
1271
|
+
source,
|
|
1272
|
+
},
|
|
1273
|
+
["events", "args"],
|
|
1274
|
+
);
|
|
1275
|
+
return make(POSTGRES_ENTITY_TYPES.trigger, q, props);
|
|
1276
|
+
}
|
|
973
1277
|
case "comment":
|
|
974
1278
|
throw new SqlTemplateError(tag, "a COMMENT ON goes after the CREATE it comments on", 0, 0);
|
|
975
1279
|
}
|
|
@@ -1014,3 +1318,22 @@ export function domain(strings: TemplateStringsArray, ...values: unknown[]): Pos
|
|
|
1014
1318
|
export function extension(strings: TemplateStringsArray, ...values: unknown[]): PostgresExtension {
|
|
1015
1319
|
return build("extension", strings, values) as PostgresExtension;
|
|
1016
1320
|
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* `` func`CREATE [OR REPLACE] FUNCTION name(args) RETURNS ... LANGUAGE ... AS $$ ... $$` ``:
|
|
1324
|
+
* one function, its body a string kept verbatim. (`function` is a JavaScript
|
|
1325
|
+
* key word, hence `func`.)
|
|
1326
|
+
*/
|
|
1327
|
+
export function func(strings: TemplateStringsArray, ...values: unknown[]): PostgresFunction {
|
|
1328
|
+
return build("func", strings, values) as PostgresFunction;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** `` procedure`CREATE [OR REPLACE] PROCEDURE name(args) LANGUAGE ... AS $$ ... $$` ``: one procedure. */
|
|
1332
|
+
export function procedure(strings: TemplateStringsArray, ...values: unknown[]): PostgresProcedure {
|
|
1333
|
+
return build("procedure", strings, values) as PostgresProcedure;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** `` trigger`CREATE [OR REPLACE] [CONSTRAINT] TRIGGER name ... ON ${table} ... EXECUTE FUNCTION ${fn}()` ``: one trigger on its table. */
|
|
1337
|
+
export function trigger(strings: TemplateStringsArray, ...values: unknown[]): PostgresTrigger {
|
|
1338
|
+
return build("trigger", strings, values) as PostgresTrigger;
|
|
1339
|
+
}
|
|
@@ -13,6 +13,9 @@ export const POSTGRES_ENTITY_TYPES = {
|
|
|
13
13
|
enum: "Postgres::Enum",
|
|
14
14
|
domain: "Postgres::Domain",
|
|
15
15
|
extension: "Postgres::Extension",
|
|
16
|
+
function: "Postgres::Function",
|
|
17
|
+
procedure: "Postgres::Procedure",
|
|
18
|
+
trigger: "Postgres::Trigger",
|
|
16
19
|
} as const;
|
|
17
20
|
|
|
18
21
|
export type PostgresEntityType = (typeof POSTGRES_ENTITY_TYPES)[keyof typeof POSTGRES_ENTITY_TYPES];
|
|
@@ -42,6 +42,9 @@ const TAG: Record<string, string> = {
|
|
|
42
42
|
[POSTGRES_ENTITY_TYPES.enum]: "type",
|
|
43
43
|
[POSTGRES_ENTITY_TYPES.domain]: "domain",
|
|
44
44
|
[POSTGRES_ENTITY_TYPES.extension]: "extension",
|
|
45
|
+
[POSTGRES_ENTITY_TYPES.function]: "func",
|
|
46
|
+
[POSTGRES_ENTITY_TYPES.procedure]: "procedure",
|
|
47
|
+
[POSTGRES_ENTITY_TYPES.trigger]: "trigger",
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
/** Text that sits inside a template literal: a backquote and `${` escaped. */
|
|
@@ -144,8 +147,8 @@ export class PostgresGenerator implements TypeScriptGenerator {
|
|
|
144
147
|
const schemaExport = new Map<string, string>();
|
|
145
148
|
for (const it of items) {
|
|
146
149
|
if (it.type === POSTGRES_ENTITY_TYPES.schema) schemaExport.set(it.name, it.exportName);
|
|
147
|
-
// An index is never referenced by name in another object's DDL.
|
|
148
|
-
else if (it.schema && it.type !== POSTGRES_ENTITY_TYPES.index) byQualified.set(`${it.schema}.${it.name}`, it);
|
|
150
|
+
// An index or a trigger is never referenced by name in another object's DDL.
|
|
151
|
+
else if (it.schema && it.type !== POSTGRES_ENTITY_TYPES.index && it.type !== POSTGRES_ENTITY_TYPES.trigger) byQualified.set(`${it.schema}.${it.name}`, it);
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
const bodies = new Map(items.map((it) => [it.exportName, templateBody(it, byQualified, schemaExport)]));
|
|
@@ -43,6 +43,10 @@ export async function exportResources(options: ExportOptions): Promise<ExportedT
|
|
|
43
43
|
if (options.selector?.type && o.type !== options.selector.type) continue;
|
|
44
44
|
const name = options.selector?.name;
|
|
45
45
|
if (name !== undefined && name !== o.name && name !== `${o.schema}.${o.name}`) continue;
|
|
46
|
+
if (o.unsupported) {
|
|
47
|
+
warnings.push(`${o.schema ? `${o.schema}.` : ""}${o.name}${o.signature ?? ""} is left out: ${o.unsupported}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
46
50
|
if (o.foreign) {
|
|
47
51
|
warnings.push(`${o.schema ? `${o.schema}.` : ""}${o.name} is kept by ${o.foreign}; left out, since declaring it would have chant change what that tool owns`);
|
|
48
52
|
continue;
|