@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.
Files changed (78) hide show
  1. package/dist/codegen/registry.d.ts.map +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/integrity.json +5 -5
  5. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  6. package/dist/lsp/postgres-template.d.ts.map +1 -1
  7. package/dist/manifest.json +1 -1
  8. package/dist/meta.json +15 -0
  9. package/dist/okf/index.md +3 -0
  10. package/dist/okf/types/PostgresFunction.md +9 -0
  11. package/dist/okf/types/PostgresProcedure.md +9 -0
  12. package/dist/okf/types/PostgresTrigger.md +9 -0
  13. package/dist/plugin.d.ts.map +1 -1
  14. package/dist/postgres/apply/apply.d.ts +6 -1
  15. package/dist/postgres/apply/apply.d.ts.map +1 -1
  16. package/dist/postgres/apply/statements.d.ts +9 -2
  17. package/dist/postgres/apply/statements.d.ts.map +1 -1
  18. package/dist/postgres/entities.d.ts +110 -3
  19. package/dist/postgres/entities.d.ts.map +1 -1
  20. package/dist/postgres/entity-types.d.ts +3 -0
  21. package/dist/postgres/entity-types.d.ts.map +1 -1
  22. package/dist/postgres/import/generator.d.ts.map +1 -1
  23. package/dist/postgres/import/live-export.d.ts.map +1 -1
  24. package/dist/postgres/live/catalog.d.ts +16 -4
  25. package/dist/postgres/live/catalog.d.ts.map +1 -1
  26. package/dist/postgres/live/describe-resources.d.ts +6 -1
  27. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  28. package/dist/postgres/parser.d.ts +82 -5
  29. package/dist/postgres/parser.d.ts.map +1 -1
  30. package/dist/postgres/plan/deep.d.ts.map +1 -1
  31. package/dist/postgres/plan/diff.d.ts +1 -1
  32. package/dist/postgres/plan/diff.d.ts.map +1 -1
  33. package/dist/postgres/plan/normalize.d.ts +21 -2
  34. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  35. package/dist/postgres/plan/rules.d.ts +6 -0
  36. package/dist/postgres/plan/rules.d.ts.map +1 -1
  37. package/dist/postgres/plan/schema.d.ts +11 -1
  38. package/dist/postgres/plan/schema.d.ts.map +1 -1
  39. package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
  40. package/dist/postgres.d.ts +1 -1
  41. package/dist/postgres.d.ts.map +1 -1
  42. package/dist/rules/postgres-templates.ts +2 -2
  43. package/dist/rules/sqlpg001.ts +3 -0
  44. package/dist/validate.d.ts.map +1 -1
  45. package/package.json +2 -2
  46. package/src/codegen/postgres-classifier-page.ts +1 -1
  47. package/src/codegen/registry.ts +3 -0
  48. package/src/generated/lexicon-sql.json +15 -0
  49. package/src/index.ts +1 -1
  50. package/src/lint/rules/postgres-templates.ts +2 -2
  51. package/src/lint/rules/sqlpg001.ts +3 -0
  52. package/src/lsp/postgres-template.ts +6 -0
  53. package/src/mcp/postgres.ts +1 -1
  54. package/src/plugin.ts +3 -0
  55. package/src/postgres/apply/apply.ts +21 -5
  56. package/src/postgres/apply/statements.ts +53 -11
  57. package/src/postgres/entities.ts +332 -9
  58. package/src/postgres/entity-types.ts +3 -0
  59. package/src/postgres/import/generator.ts +5 -2
  60. package/src/postgres/import/live-export.ts +4 -0
  61. package/src/postgres/live/catalog.ts +78 -4
  62. package/src/postgres/live/describe-resources.ts +17 -7
  63. package/src/postgres/live/routines.e2e.test.ts +173 -0
  64. package/src/postgres/migrate/batches.e2e.test.ts +2 -1
  65. package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
  66. package/src/postgres/parser.ts +314 -8
  67. package/src/postgres/plan/commands.ts +2 -2
  68. package/src/postgres/plan/deep.ts +57 -17
  69. package/src/postgres/plan/diff.ts +46 -6
  70. package/src/postgres/plan/normalize.ts +158 -4
  71. package/src/postgres/plan/rules.ts +36 -0
  72. package/src/postgres/plan/schema.ts +25 -6
  73. package/src/postgres/plan/server-normalize.ts +24 -0
  74. package/src/postgres/routines.test.ts +150 -0
  75. package/src/postgres.ts +11 -0
  76. package/src/skill-defs.ts +1 -1
  77. package/src/skills/chant-sql-postgres.md +18 -2
  78. 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
- /** An object's name in a statement. A schema and an extension have no schema. */
84
- export const objectIdent = (o: { kind: CanonicalKind; schema?: string; name: string }): string =>
85
- o.kind === "schema" || o.kind === "extension" ? quoteIdent(o.name) : pgQualified(o.schema, o.name);
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
- if (i === 0 && opts.orReplace && obj.canonical.kind === "view" && !/\bOR\s+REPLACE\b/i.test(sql)) sql = sql.replace(/\bCREATE\s+/i, "CREATE OR REPLACE ");
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> = { materializedView: 0, view: 1, index: 2, table: 3, sequence: 4, domain: 5, enum: 6, extension: 7, schema: 8 };
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({ key: c.object, kind: o.kind, ...(schema !== undefined ? { schema } : {}), name: o.name, step: dropStatement(o.kind, schema, o.name) });
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 };
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The Postgres entities: `schema`, `table`, `index`, `view`, `sequence`,
3
- * `type`, `domain` and `extension` tagged templates that parse their DDL at
4
- * fold time into one Declarable each (chant #3278, #3279).
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 (!isSelf(qualified(ctx, c.target))) refuse(c, `names another object than ${sqlNameOf(self)}`);
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
- function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string[], values: readonly unknown[]): PostgresObject {
698
- const parts = templateParts(strings);
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: parts };
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
- setInterpolationFields(entity, ctx.fed.map((paths) => [...paths].sort()));
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;