@intentius/chant-lexicon-sql 0.114.1 → 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 (142) hide show
  1. package/dist/clickhouse/apply/mutations.d.ts +7 -1
  2. package/dist/clickhouse/apply/mutations.d.ts.map +1 -1
  3. package/dist/clickhouse/container.d.ts +22 -7
  4. package/dist/clickhouse/container.d.ts.map +1 -1
  5. package/dist/clickhouse/plan/deep.d.ts +5 -0
  6. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  7. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  8. package/dist/clickhouse/rebuild/backfill.d.ts +11 -5
  9. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  10. package/dist/clickhouse/rebuild/op.d.ts +29 -0
  11. package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
  12. package/dist/clickhouse/rebuild/partitions.d.ts +8 -0
  13. package/dist/clickhouse/rebuild/partitions.d.ts.map +1 -1
  14. package/dist/clickhouse/rebuild/receipts.d.ts +4 -6
  15. package/dist/clickhouse/rebuild/receipts.d.ts.map +1 -1
  16. package/dist/clickhouse/rebuild/shards.d.ts +59 -0
  17. package/dist/clickhouse/rebuild/shards.d.ts.map +1 -0
  18. package/dist/clickhouse/rebuild/steps.d.ts +3 -0
  19. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  20. package/dist/clickhouse/rebuild/verify.d.ts +4 -0
  21. package/dist/clickhouse/rebuild/verify.d.ts.map +1 -1
  22. package/dist/codegen/registry.d.ts.map +1 -1
  23. package/dist/config.d.ts +14 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/core/receipts.d.ts +5 -3
  26. package/dist/core/receipts.d.ts.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/integrity.json +5 -5
  30. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  31. package/dist/lsp/postgres-template.d.ts.map +1 -1
  32. package/dist/manifest.json +1 -1
  33. package/dist/meta.json +15 -0
  34. package/dist/okf/index.md +3 -0
  35. package/dist/okf/types/PostgresFunction.md +9 -0
  36. package/dist/okf/types/PostgresProcedure.md +9 -0
  37. package/dist/okf/types/PostgresTrigger.md +9 -0
  38. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  39. package/dist/op/activities/index.d.ts +13 -1
  40. package/dist/op/activities/index.d.ts.map +1 -1
  41. package/dist/op/activities/sql-exec.d.ts +41 -0
  42. package/dist/op/activities/sql-exec.d.ts.map +1 -0
  43. package/dist/op/activity-contracts.d.ts +20 -0
  44. package/dist/op/activity-contracts.d.ts.map +1 -1
  45. package/dist/plugin.d.ts.map +1 -1
  46. package/dist/postgres/apply/apply.d.ts +6 -1
  47. package/dist/postgres/apply/apply.d.ts.map +1 -1
  48. package/dist/postgres/apply/statements.d.ts +9 -2
  49. package/dist/postgres/apply/statements.d.ts.map +1 -1
  50. package/dist/postgres/entities.d.ts +110 -3
  51. package/dist/postgres/entities.d.ts.map +1 -1
  52. package/dist/postgres/entity-types.d.ts +3 -0
  53. package/dist/postgres/entity-types.d.ts.map +1 -1
  54. package/dist/postgres/import/generator.d.ts.map +1 -1
  55. package/dist/postgres/import/live-export.d.ts.map +1 -1
  56. package/dist/postgres/live/catalog.d.ts +17 -5
  57. package/dist/postgres/live/catalog.d.ts.map +1 -1
  58. package/dist/postgres/live/describe-resources.d.ts +6 -1
  59. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  60. package/dist/postgres/migrate/receipts.d.ts +8 -5
  61. package/dist/postgres/migrate/receipts.d.ts.map +1 -1
  62. package/dist/postgres/parser.d.ts +82 -5
  63. package/dist/postgres/parser.d.ts.map +1 -1
  64. package/dist/postgres/plan/deep.d.ts.map +1 -1
  65. package/dist/postgres/plan/diff.d.ts +1 -1
  66. package/dist/postgres/plan/diff.d.ts.map +1 -1
  67. package/dist/postgres/plan/normalize.d.ts +21 -2
  68. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  69. package/dist/postgres/plan/rules.d.ts +6 -0
  70. package/dist/postgres/plan/rules.d.ts.map +1 -1
  71. package/dist/postgres/plan/schema.d.ts +11 -1
  72. package/dist/postgres/plan/schema.d.ts.map +1 -1
  73. package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
  74. package/dist/postgres.d.ts +1 -1
  75. package/dist/postgres.d.ts.map +1 -1
  76. package/dist/receipts.d.ts +102 -0
  77. package/dist/receipts.d.ts.map +1 -0
  78. package/dist/rules/postgres-templates.ts +2 -2
  79. package/dist/rules/sqlpg001.ts +3 -0
  80. package/dist/validate.d.ts.map +1 -1
  81. package/package.json +2 -2
  82. package/src/clickhouse/apply/mutations.ts +9 -3
  83. package/src/clickhouse/container.ts +39 -11
  84. package/src/clickhouse/plan/cluster-drift.e2e.test.ts +117 -0
  85. package/src/clickhouse/plan/deep.ts +102 -3
  86. package/src/clickhouse/plan/normalize.ts +10 -0
  87. package/src/clickhouse/plan/plan.test.ts +10 -0
  88. package/src/clickhouse/rebuild/backfill.ts +85 -42
  89. package/src/clickhouse/rebuild/op.ts +80 -35
  90. package/src/clickhouse/rebuild/partitions.ts +11 -0
  91. package/src/clickhouse/rebuild/rebuild-sharded.e2e.test.ts +161 -0
  92. package/src/clickhouse/rebuild/rebuild.e2e.test.ts +110 -4
  93. package/src/clickhouse/rebuild/rebuild.test.ts +56 -4
  94. package/src/clickhouse/rebuild/receipts.ts +4 -6
  95. package/src/clickhouse/rebuild/shards.ts +104 -0
  96. package/src/clickhouse/rebuild/steps.ts +4 -0
  97. package/src/clickhouse/rebuild/verify.ts +14 -5
  98. package/src/codegen/postgres-classifier-page.ts +1 -1
  99. package/src/codegen/registry.ts +3 -0
  100. package/src/config.ts +7 -0
  101. package/src/core/receipts.ts +5 -3
  102. package/src/generated/lexicon-sql.json +15 -0
  103. package/src/index.ts +1 -1
  104. package/src/lint/rules/postgres-templates.ts +2 -2
  105. package/src/lint/rules/sqlpg001.ts +3 -0
  106. package/src/lsp/postgres-template.ts +6 -0
  107. package/src/mcp/postgres.ts +1 -1
  108. package/src/op/activities/clickhouse-rebuild.ts +32 -7
  109. package/src/op/activities/index.ts +21 -4
  110. package/src/op/activities/sql-exec.ts +84 -0
  111. package/src/op/activity-contracts.ts +15 -0
  112. package/src/op/effect-receipts.e2e.test.ts +277 -0
  113. package/src/plugin.ts +3 -0
  114. package/src/postgres/apply/apply.ts +21 -5
  115. package/src/postgres/apply/statements.ts +53 -11
  116. package/src/postgres/entities.ts +332 -9
  117. package/src/postgres/entity-types.ts +3 -0
  118. package/src/postgres/import/generator.ts +5 -2
  119. package/src/postgres/import/live-export.ts +4 -0
  120. package/src/postgres/live/catalog.ts +81 -5
  121. package/src/postgres/live/describe-resources.ts +17 -7
  122. package/src/postgres/live/routines.e2e.test.ts +173 -0
  123. package/src/postgres/migrate/batches.e2e.test.ts +2 -1
  124. package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
  125. package/src/postgres/migrate/migrate.test.ts +3 -2
  126. package/src/postgres/migrate/receipts.ts +18 -10
  127. package/src/postgres/parser.ts +314 -8
  128. package/src/postgres/plan/commands.ts +2 -2
  129. package/src/postgres/plan/deep.ts +57 -17
  130. package/src/postgres/plan/diff.ts +46 -6
  131. package/src/postgres/plan/normalize.ts +158 -4
  132. package/src/postgres/plan/rules.ts +36 -0
  133. package/src/postgres/plan/schema.ts +25 -6
  134. package/src/postgres/plan/server-normalize.ts +24 -0
  135. package/src/postgres/routines.test.ts +150 -0
  136. package/src/postgres.ts +11 -0
  137. package/src/receipts.test.ts +91 -0
  138. package/src/receipts.ts +259 -0
  139. package/src/skill-defs.ts +1 -1
  140. package/src/skills/chant-sql-postgres.md +18 -2
  141. package/src/skills/chant-sql-rebuild.md +5 -1
  142. package/src/validate.ts +3 -0
@@ -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;
@@ -18,11 +18,16 @@
18
18
  * - objects an extension owns (`pg_depend.deptype = 'e'`): the extension
19
19
  * declares them, so the extension is what is imported;
20
20
  * - `plpgsql`, installed in every database;
21
+ * - aggregates, and a trigger the server makes for itself (a foreign key's)
22
+ * or clones onto a partition from its parent's;
23
+ * - a routine with a SQL-standard body (`RETURN`, `BEGIN ATOMIC`): read, but
24
+ * marked `unsupported`, since the server prints it rewritten and a
25
+ * declaration cannot hold it;
21
26
  * - a sequence an identity column owns (it is the column's), and a `serial`
22
27
  * column's sequence, since the column prints as `serial`;
23
28
  * - an index that backs a constraint (the constraint creates it), and an
24
29
  * index partition (the partitioned index creates it);
25
- * - chant's own receipts tables (`../../core/ownership.ts`), and the working
30
+ * - chant's own receipts tables and schema (`../../core/ownership.ts`), and the working
26
31
  * columns, constraints and indexes of an expand-and-contract migration in
27
32
  * progress (`../migrate/names.ts`, `../migrate/carry.ts`), so a table reads
28
33
  * as it was before the migration until its switch, and as declared after it.
@@ -76,6 +81,12 @@ export interface LivePgObject {
76
81
  statement: string;
77
82
  /** The tool whose bookkeeping this is, when it is another tool's (an ORM's revision table). */
78
83
  foreign?: string;
84
+ /** A routine's input parameter types, `(integer,text)`; a trigger's table, ` ON app.users` (`../plan/normalize.ts`). */
85
+ signature?: string;
86
+ /** What depends on a routine (a view, a trigger, a column default), as the server describes each. */
87
+ dependents?: string[];
88
+ /** Why no declaration can hold this object; it is read so import can say it left it out. */
89
+ unsupported?: string;
79
90
  }
80
91
 
81
92
  /** What {@link readLiveSchema} reads. */
@@ -181,8 +192,9 @@ const seqParams = (r: Row): SequenceParams => ({
181
192
 
182
193
  /**
183
194
  * Every object in scope, in a stable order: schemas, extensions, enums,
184
- * domains, sequences, tables, views and materialized views, indexes; each
185
- * kind by schema then name, in byte order.
195
+ * domains, sequences, tables, views and materialized views, indexes,
196
+ * functions and procedures, triggers; each kind by schema then name, in byte
197
+ * order.
186
198
  */
187
199
  export async function readLiveSchema(client: PostgresClient, scope: SchemaScope = {}): Promise<LivePgObject[]> {
188
200
  const out: LivePgObject[] = [];
@@ -199,6 +211,8 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
199
211
  );
200
212
  for (const r of rows) {
201
213
  const name = String(r.name);
214
+ // chant's own receipts schema (`../../receipts.ts`), like the receipts tables.
215
+ if (hasChantTrailerKey(str(r.comment), [RECEIPTS_TRAILER_KEY])) continue;
202
216
  out.push({
203
217
  type: POSTGRES_ENTITY_TYPES.schema,
204
218
  name,
@@ -554,11 +568,73 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
554
568
  }
555
569
  }
556
570
 
571
+ // Functions and procedures (not aggregates).
572
+ {
573
+ const rows = await client.query<Row>(
574
+ `SELECT p.oid::text AS oid, n.nspname AS schema, p.proname AS name, p.prokind AS kind, pg_catalog.pg_get_functiondef(p.oid) AS def,
575
+ '(' || pg_catalog.replace(pg_catalog.oidvectortypes(p.proargtypes), ', ', ',') || ')' AS signature,
576
+ p.prosqlbody IS NOT NULL AS sqlbody, pg_catalog.obj_description(p.oid, 'pg_proc') AS comment,
577
+ ARRAY(SELECT pg_catalog.pg_describe_object(d.classid, d.objid, d.objsubid) FROM pg_catalog.pg_depend d
578
+ WHERE d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND d.refobjid = p.oid AND d.deptype = 'n'
579
+ ORDER BY 1) AS dependents
580
+ FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
581
+ WHERE p.prokind IN ('f', 'p', 'w') AND ${s.sql} AND ${notExtensionOwned("pg_proc", "p.oid")}
582
+ ORDER BY n.nspname ${C}, p.proname ${C}, pg_catalog.oidvectortypes(p.proargtypes) ${C}`,
583
+ s.params,
584
+ );
585
+ for (const r of rows) {
586
+ // A migration's dual-write function, until its contract.
587
+ if (hasChantTrailerKey(str(r.comment), [MIGRATION_TRAILER_KEY])) continue;
588
+ const signature = String(r.signature);
589
+ const q = `${qname(String(r.schema), String(r.name))}${signature}`;
590
+ const procedure = r.kind === "p";
591
+ const dependents = (r.dependents as string[] | null) ?? [];
592
+ out.push({
593
+ type: procedure ? POSTGRES_ENTITY_TYPES.procedure : POSTGRES_ENTITY_TYPES.function,
594
+ schema: String(r.schema),
595
+ name: String(r.name),
596
+ oid: String(r.oid),
597
+ signature,
598
+ ...(dependents.length > 0 ? { dependents } : {}),
599
+ ...(str(r.comment) ? { comment: str(r.comment) } : {}),
600
+ ...(r.sqlbody === true ? { unsupported: "its body is SQL-standard (RETURN or BEGIN ATOMIC), which the server prints back rewritten; declare it with a string body (AS $$ ... $$)" } : {}),
601
+ statement: [String(r.def).trim(), ...commentOn(`${procedure ? "PROCEDURE" : "FUNCTION"} ${q}`, r.comment)].join(";\n"),
602
+ });
603
+ }
604
+ }
605
+
606
+ // Triggers: the ones written by hand, not a foreign key's or a partition's clone of its parent's.
607
+ {
608
+ const rows = await client.query<Row>(
609
+ `SELECT t.oid::text AS oid, n.nspname AS schema, c.relname AS table_name, t.tgname AS name, pg_catalog.pg_get_triggerdef(t.oid, true) AS def,
610
+ pg_catalog.obj_description(t.oid, 'pg_trigger') AS comment
611
+ FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
612
+ WHERE NOT t.tgisinternal AND t.tgparentid = 0 AND ${s.sql} AND ${notExtensionOwned("pg_class", "c.oid")}
613
+ ORDER BY n.nspname ${C}, c.relname ${C}, t.tgname ${C}`,
614
+ s.params,
615
+ );
616
+ for (const r of rows) {
617
+ // A migration's dual-write trigger, until its contract.
618
+ if (hasChantTrailerKey(str(r.comment), [MIGRATION_TRAILER_KEY])) continue;
619
+ const table = qname(String(r.schema), String(r.table_name));
620
+ out.push({
621
+ type: POSTGRES_ENTITY_TYPES.trigger,
622
+ schema: String(r.schema),
623
+ name: String(r.name),
624
+ oid: String(r.oid),
625
+ signature: ` ON ${table}`,
626
+ ...(str(r.comment) ? { comment: str(r.comment) } : {}),
627
+ ...(FOREIGN_TABLES[String(r.table_name)] ? { foreign: FOREIGN_TABLES[String(r.table_name)] } : {}),
628
+ statement: [String(r.def), ...commentOn(`TRIGGER ${quoteIdent(String(r.name))} ON ${table}`, r.comment)].join(";\n"),
629
+ });
630
+ }
631
+ }
632
+
557
633
  return out;
558
634
  }
559
635
 
560
- /** The key an object is found by: its type and schema-qualified name. */
561
- export const liveKey = (type: string, schema: string | undefined, name: string): string => `${type} ${schema ?? ""}.${name}`;
636
+ /** The key an object is found by: its type and schema-qualified name, and a routine's parameter types or a trigger's table. */
637
+ export const liveKey = (type: string, schema: string | undefined, name: string, signature?: string): string => `${type} ${schema ?? ""}.${name}${signature ?? ""}`;
562
638
 
563
639
  /**
564
640
  * Mark what the managed service owns (#3282) as foreign: its reserved
@@ -27,17 +27,27 @@ import { liveKey, markProviderOwned, readLiveSchema, type LivePgObject } from ".
27
27
  import { POSTGRES_ENTITY_TYPES } from "../entity-types";
28
28
  import { isChantManaged, readMarker, stripMarker } from "../../core/ownership";
29
29
  import { identValue } from "../parser";
30
+ import { canonicalPgObject } from "../plan/normalize";
30
31
 
31
32
  interface Bound {
32
33
  target: PostgresTarget;
33
34
  byKey: Map<string, LivePgObject>;
34
35
  }
35
36
 
36
- /** The schema and name a declared entity is created as; a bare name is in the default schema. */
37
- export function declaredAddress(entity: { type: string; props: Record<string, unknown> }, defaultSchema: string): { schema?: string; name: string } {
37
+ /**
38
+ * The schema and name a declared entity is created as; a bare name is in the
39
+ * default schema. A routine's parameter types and a trigger's table tell it
40
+ * from another of the same name (`signature`).
41
+ */
42
+ export function declaredAddress(entity: { type: string; props: Record<string, unknown> }, defaultSchema: string): { schema?: string; name: string; signature?: string } {
38
43
  const name = String(entity.props.name ?? "");
39
44
  if (entity.type === POSTGRES_ENTITY_TYPES.schema || entity.type === POSTGRES_ENTITY_TYPES.extension) return { name };
40
- return { schema: typeof entity.props.schema === "string" ? entity.props.schema : identValue(defaultSchema), name };
45
+ const schema = typeof entity.props.schema === "string" ? entity.props.schema : identValue(defaultSchema);
46
+ if (entity.type === POSTGRES_ENTITY_TYPES.function || entity.type === POSTGRES_ENTITY_TYPES.procedure || entity.type === POSTGRES_ENTITY_TYPES.trigger) {
47
+ const signature = canonicalPgObject(entity.type, entity.props, defaultSchema).signature;
48
+ return { schema, name, ...(signature !== undefined ? { signature } : {}) };
49
+ }
50
+ return { schema, name };
41
51
  }
42
52
 
43
53
  /** A schema scope for the catalog read: the profile's schemas, else every schema the declarations name and the default one. */
@@ -59,7 +69,7 @@ function adapter(options: BindOptions & { owned?: boolean; declared: DeclaredEnt
59
69
  const { target, client } = await bindPostgres(options);
60
70
  try {
61
71
  const live = markProviderOwned(await readLiveSchema(client, { schemas: scopeFor(target, options.declared.filter((d) => d.type.startsWith("Postgres::"))) }), target.provider);
62
- return { target, byKey: new Map(live.map((o) => [liveKey(o.type, o.schema, o.name), o])) };
72
+ return { target, byKey: new Map(live.map((o) => [liveKey(o.type, o.schema, o.name, o.signature), o])) };
63
73
  } finally {
64
74
  await client.end();
65
75
  }
@@ -67,9 +77,9 @@ function adapter(options: BindOptions & { owned?: boolean; declared: DeclaredEnt
67
77
  classifyBindFailure: (err) => classifyPostgresFailure(err),
68
78
  async read({ target, byKey }, entity): Promise<EntityObservation> {
69
79
  if (!entity.type.startsWith("Postgres::")) return { unobserved: { reason: "unsupported-kind", detail: entity.type } };
70
- const { schema, name } = declaredAddress(entity, target.defaultSchema);
71
- const queried = `${redactUrl(target.endpoint.url)} ${schema ? `${schema}.` : ""}${name}`;
72
- const live = byKey.get(liveKey(entity.type, schema, name));
80
+ const { schema, name, signature } = declaredAddress(entity, target.defaultSchema);
81
+ const queried = `${redactUrl(target.endpoint.url)} ${schema ? `${schema}.` : ""}${name}${signature ?? ""}`;
82
+ const live = byKey.get(liveKey(entity.type, schema, name, signature));
73
83
  if (!live) return { absent: true, queried };
74
84
  const owned = !live.foreign && isChantManaged(live.comment);
75
85
  if (options.owned && !owned) {