@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.
- package/dist/clickhouse/apply/mutations.d.ts +7 -1
- package/dist/clickhouse/apply/mutations.d.ts.map +1 -1
- package/dist/clickhouse/container.d.ts +22 -7
- package/dist/clickhouse/container.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts +5 -0
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/backfill.d.ts +11 -5
- package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/op.d.ts +29 -0
- package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/partitions.d.ts +8 -0
- package/dist/clickhouse/rebuild/partitions.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/receipts.d.ts +4 -6
- package/dist/clickhouse/rebuild/receipts.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/shards.d.ts +59 -0
- package/dist/clickhouse/rebuild/shards.d.ts.map +1 -0
- package/dist/clickhouse/rebuild/steps.d.ts +3 -0
- package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/verify.d.ts +4 -0
- package/dist/clickhouse/rebuild/verify.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/core/receipts.d.ts +5 -3
- package/dist/core/receipts.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/op/activities/clickhouse-rebuild.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +13 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/sql-exec.d.ts +41 -0
- package/dist/op/activities/sql-exec.d.ts.map +1 -0
- package/dist/op/activity-contracts.d.ts +20 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- 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 +17 -5
- 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/migrate/receipts.d.ts +8 -5
- package/dist/postgres/migrate/receipts.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/receipts.d.ts +102 -0
- package/dist/receipts.d.ts.map +1 -0
- 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/clickhouse/apply/mutations.ts +9 -3
- package/src/clickhouse/container.ts +39 -11
- package/src/clickhouse/plan/cluster-drift.e2e.test.ts +117 -0
- package/src/clickhouse/plan/deep.ts +102 -3
- package/src/clickhouse/plan/normalize.ts +10 -0
- package/src/clickhouse/plan/plan.test.ts +10 -0
- package/src/clickhouse/rebuild/backfill.ts +85 -42
- package/src/clickhouse/rebuild/op.ts +80 -35
- package/src/clickhouse/rebuild/partitions.ts +11 -0
- package/src/clickhouse/rebuild/rebuild-sharded.e2e.test.ts +161 -0
- package/src/clickhouse/rebuild/rebuild.e2e.test.ts +110 -4
- package/src/clickhouse/rebuild/rebuild.test.ts +56 -4
- package/src/clickhouse/rebuild/receipts.ts +4 -6
- package/src/clickhouse/rebuild/shards.ts +104 -0
- package/src/clickhouse/rebuild/steps.ts +4 -0
- package/src/clickhouse/rebuild/verify.ts +14 -5
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +3 -0
- package/src/config.ts +7 -0
- package/src/core/receipts.ts +5 -3
- 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/op/activities/clickhouse-rebuild.ts +32 -7
- package/src/op/activities/index.ts +21 -4
- package/src/op/activities/sql-exec.ts +84 -0
- package/src/op/activity-contracts.ts +15 -0
- package/src/op/effect-receipts.e2e.test.ts +277 -0
- 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 +81 -5
- 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/migrate/migrate.test.ts +3 -2
- package/src/postgres/migrate/receipts.ts +18 -10
- 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/receipts.test.ts +91 -0
- package/src/receipts.ts +259 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +18 -2
- package/src/skills/chant-sql-rebuild.md +5 -1
- package/src/validate.ts +3 -0
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;
|
|
@@ -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
|
|
185
|
-
* kind by schema then name, in byte
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
-
|
|
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) {
|