@intentius/chant-lexicon-sql 0.115.0 → 0.117.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.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 +35 -0
- package/dist/okf/index.md +7 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
- package/dist/okf/types/PostgresFunction.md +9 -0
- package/dist/okf/types/PostgresGrant.md +9 -0
- package/dist/okf/types/PostgresPolicy.md +9 -0
- package/dist/okf/types/PostgresProcedure.md +9 -0
- package/dist/okf/types/PostgresRole.md +9 -0
- package/dist/okf/types/PostgresTrigger.md +9 -0
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +10 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/access/acl.d.ts +119 -0
- package/dist/postgres/access/acl.d.ts.map +1 -0
- package/dist/postgres/access/import.d.ts +13 -0
- package/dist/postgres/access/import.d.ts.map +1 -0
- package/dist/postgres/access/live.d.ts +24 -0
- package/dist/postgres/access/live.d.ts.map +1 -0
- 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 +12 -2
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +212 -3
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +7 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +4 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts +6 -0
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/bind.d.ts +2 -0
- package/dist/postgres/live/bind.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +25 -4
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +15 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/migrate/op.d.ts +29 -0
- package/dist/postgres/migrate/op.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +155 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/commands.d.ts +12 -0
- package/dist/postgres/plan/commands.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +11 -2
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +28 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +14 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +12 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts +4 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +10 -4
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +7 -0
- package/src/config.ts +10 -0
- package/src/generated/lexicon-sql.json +35 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +10 -4
- package/src/lsp/postgres-template.ts +8 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/migration-statements.ts +7 -0
- package/src/op/activities/postgres-migration.ts +27 -2
- package/src/op/activity-contracts.ts +1 -0
- package/src/plugin.ts +6 -0
- package/src/postgres/access/access.e2e.test.ts +191 -0
- package/src/postgres/access/access.test.ts +147 -0
- package/src/postgres/access/acl.ts +400 -0
- package/src/postgres/access/import.ts +64 -0
- package/src/postgres/access/live.ts +123 -0
- package/src/postgres/apply/apply.ts +135 -6
- package/src/postgres/apply/statements.ts +104 -16
- package/src/postgres/entities.ts +613 -14
- package/src/postgres/entity-types.ts +7 -0
- package/src/postgres/import/generator.ts +18 -5
- package/src/postgres/import/live-export.ts +16 -2
- package/src/postgres/live/bind.ts +3 -0
- package/src/postgres/live/catalog.ts +155 -5
- package/src/postgres/live/describe-resources.ts +43 -9
- 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 +24 -1
- package/src/postgres/migrate/op.ts +80 -35
- package/src/postgres/migrate/outer.e2e.test.ts +163 -0
- package/src/postgres/parser.ts +554 -9
- package/src/postgres/plan/commands.ts +61 -8
- package/src/postgres/plan/deep.ts +79 -18
- package/src/postgres/plan/diff.ts +114 -9
- package/src/postgres/plan/normalize.ts +272 -5
- package/src/postgres/plan/rules.ts +80 -0
- package/src/postgres/plan/schema.ts +27 -6
- package/src/postgres/plan/server-normalize.ts +53 -1
- package/src/postgres/routines.test.ts +150 -0
- package/src/postgres.ts +22 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres-migration.md +3 -1
- package/src/skills/chant-sql-postgres.md +25 -2
- package/src/validate.ts +7 -0
|
@@ -27,7 +27,15 @@
|
|
|
27
27
|
* column's columns are `NOT NULL` whether or not that is written;
|
|
28
28
|
* - an identity column's or a sequence's options at their defaults are left
|
|
29
29
|
* out; storage parameters compare as a map;
|
|
30
|
-
* - an index's access method defaults to `btree
|
|
30
|
+
* - an index's access method defaults to `btree`;
|
|
31
|
+
* - a function's or procedure's parameter types drop their modifiers (the
|
|
32
|
+
* catalog keeps none: `varchar(20)` is `character varying`), a parameter's
|
|
33
|
+
* mode is `in` unless written, its attributes at their defaults (VOLATILE,
|
|
34
|
+
* CALLED ON NULL INPUT, SECURITY INVOKER, PARALLEL UNSAFE, COST 100 or 1,
|
|
35
|
+
* ROWS 1000 or 0) are left out, and its body is compared as written, since
|
|
36
|
+
* the server keeps it verbatim;
|
|
37
|
+
* - a trigger's events compare as a set, `UPDATE OF` columns sorted, and its
|
|
38
|
+
* arguments as the strings the catalog stores.
|
|
31
39
|
*
|
|
32
40
|
* What the rules cannot see (a view's `SELECT *`, which the server expands, or
|
|
33
41
|
* an expression it parenthesizes) is asked of the live server itself when a
|
|
@@ -52,10 +60,19 @@ import type {
|
|
|
52
60
|
IndexProps,
|
|
53
61
|
KeyDef,
|
|
54
62
|
SchemaProps,
|
|
63
|
+
DefaultPrivilegesProps,
|
|
64
|
+
GrantProps,
|
|
65
|
+
PolicyProps,
|
|
66
|
+
RoleProps,
|
|
67
|
+
RoutineProps,
|
|
55
68
|
SequenceProps,
|
|
56
69
|
TableProps,
|
|
70
|
+
TriggerProps,
|
|
57
71
|
ViewProps,
|
|
58
72
|
} from "../entities";
|
|
73
|
+
import { stringValue } from "../entities";
|
|
74
|
+
import { parseStatements } from "../parser";
|
|
75
|
+
import type { RoutineNode } from "../parser";
|
|
59
76
|
|
|
60
77
|
// ── Types ──────────────────────────────────────────────────────────────
|
|
61
78
|
|
|
@@ -178,7 +195,7 @@ function castEnd(sig: readonly Token[], i: number): { end: number; type: string
|
|
|
178
195
|
return { end: j, type: words.join("").toLowerCase() };
|
|
179
196
|
}
|
|
180
197
|
|
|
181
|
-
const isLiteral = (t: Token | undefined): boolean => t !== undefined && (t.kind === "string" || t.kind === "number");
|
|
198
|
+
const isLiteral = (t: Token | undefined): boolean => t !== undefined && (t.kind === "string" || t.kind === "number" || (t.kind === "ident" && t.text.toUpperCase() === "NULL"));
|
|
182
199
|
|
|
183
200
|
/** One token's canonical text. */
|
|
184
201
|
function tokenText(t: Token): string {
|
|
@@ -308,12 +325,40 @@ export interface CanonicalConstraint {
|
|
|
308
325
|
comment?: string;
|
|
309
326
|
}
|
|
310
327
|
|
|
311
|
-
export type CanonicalKind =
|
|
328
|
+
export type CanonicalKind =
|
|
329
|
+
| "schema"
|
|
330
|
+
| "table"
|
|
331
|
+
| "index"
|
|
332
|
+
| "view"
|
|
333
|
+
| "materializedView"
|
|
334
|
+
| "sequence"
|
|
335
|
+
| "enum"
|
|
336
|
+
| "domain"
|
|
337
|
+
| "extension"
|
|
338
|
+
| "function"
|
|
339
|
+
| "procedure"
|
|
340
|
+
| "trigger"
|
|
341
|
+
| "policy"
|
|
342
|
+
| "role"
|
|
343
|
+
| "grant"
|
|
344
|
+
| "defaultPrivileges";
|
|
345
|
+
|
|
346
|
+
/** The kinds whose declarations are privileges, compared as the access they add up to (`../access/`), not one by one. */
|
|
347
|
+
export const ACCESS_KINDS: ReadonlySet<CanonicalKind> = new Set(["grant", "defaultPrivileges"]);
|
|
348
|
+
|
|
349
|
+
/** The kinds a profile manages only when it says so (`sql.profiles.<env>.access`). */
|
|
350
|
+
export const ACCESS_MANAGED_KINDS: ReadonlySet<CanonicalKind> = new Set(["policy", "role", "grant", "defaultPrivileges"]);
|
|
312
351
|
|
|
313
352
|
export interface CanonicalPgObject {
|
|
314
353
|
kind: CanonicalKind;
|
|
315
354
|
schema?: string;
|
|
316
355
|
name: string;
|
|
356
|
+
/**
|
|
357
|
+
* What tells the object from another of the same name: a routine's input
|
|
358
|
+
* parameter types, `(integer,text)`; a trigger's table, ` ON app.users`.
|
|
359
|
+
* Written as SQL names the object after its name.
|
|
360
|
+
*/
|
|
361
|
+
signature?: string;
|
|
317
362
|
/** Everything else that is compared, by field, each field canonical. */
|
|
318
363
|
fields: Record<string, unknown>;
|
|
319
364
|
columns: CanonicalColumn[];
|
|
@@ -330,6 +375,13 @@ const kindOf: Record<string, CanonicalKind> = {
|
|
|
330
375
|
"Postgres::Enum": "enum",
|
|
331
376
|
"Postgres::Domain": "domain",
|
|
332
377
|
"Postgres::Extension": "extension",
|
|
378
|
+
"Postgres::Function": "function",
|
|
379
|
+
"Postgres::Procedure": "procedure",
|
|
380
|
+
"Postgres::Trigger": "trigger",
|
|
381
|
+
"Postgres::Policy": "policy",
|
|
382
|
+
"Postgres::Role": "role",
|
|
383
|
+
"Postgres::Grant": "grant",
|
|
384
|
+
"Postgres::DefaultPrivileges": "defaultPrivileges",
|
|
333
385
|
};
|
|
334
386
|
|
|
335
387
|
const comment = (c: string | undefined): string | undefined => {
|
|
@@ -390,6 +442,159 @@ const keyBody = (kind: string, k: KeyDef) =>
|
|
|
390
442
|
.filter(Boolean)
|
|
391
443
|
.join(" ");
|
|
392
444
|
|
|
445
|
+
/** The pseudo-types (`pg_type.typtype = 'p'`), in `pg_catalog`: a routine's parameter or result, never a column's. */
|
|
446
|
+
const PSEUDO_TYPES = new Set([
|
|
447
|
+
"any", "anyarray", "anycompatible", "anycompatiblearray", "anycompatiblemultirange", "anycompatiblenonarray", "anycompatiblerange",
|
|
448
|
+
"anyelement", "anyenum", "anymultirange", "anynonarray", "anyrange", "cstring", "event_trigger", "fdw_handler", "index_am_handler",
|
|
449
|
+
"internal", "language_handler", "pg_ddl_command", "record", "table_am_handler", "trigger", "tsm_handler", "unknown", "void",
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
/** A parameter's or result's type as the catalog keeps it: canonical, its modifiers dropped (`varchar(20)` is `character varying`). */
|
|
453
|
+
export function canonicalArgType(text: string, defaultSchema: string): string {
|
|
454
|
+
const m = /^\s*(?:pg_catalog\s*\.\s*)?([A-Za-z_]+)\s*((?:\[\s*\]\s*)*)$/.exec(text);
|
|
455
|
+
if (m && PSEUDO_TYPES.has(m[1]!.toLowerCase())) return `${m[1]!.toLowerCase()}${"[]".repeat((m[2]!.match(/\[/g) ?? []).length)}`;
|
|
456
|
+
return canonicalType(text, defaultSchema)!.replace(/\([^()]*\)/g, "");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** A routine's input parameters: those whose mode is `in`, `inout` or `variadic`. */
|
|
460
|
+
const isInput = (a: { mode: string }) => a.mode !== "out";
|
|
461
|
+
|
|
462
|
+
/** A routine's identity among its overloads: its input parameter types, `(integer,text)`. */
|
|
463
|
+
export function routineSignature(p: Pick<RoutineProps, "args">, defaultSchema: string): string {
|
|
464
|
+
return `(${p.args.filter(isInput).map((a) => canonicalArgType(a.type, defaultSchema)).join(",")})`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** The result a function declares, or the one its OUT parameters give it: one OUT is its type, several are `record`. */
|
|
468
|
+
function routineResult(p: RoutineProps, defaultSchema: string): string | undefined {
|
|
469
|
+
if (p.returnsTable) return `table(${p.returnsTable.map((c) => `${quoteIdent(c.name)} ${canonicalArgType(c.type, defaultSchema)}`).join(",")})`;
|
|
470
|
+
if (p.returns !== undefined) {
|
|
471
|
+
const m = /^setof\s+(.*)$/is.exec(p.returns.trim());
|
|
472
|
+
return m ? `setof ${canonicalArgType(m[1]!, defaultSchema)}` : canonicalArgType(p.returns, defaultSchema);
|
|
473
|
+
}
|
|
474
|
+
const outs = p.args.filter((a) => a.mode === "out" || a.mode === "inout");
|
|
475
|
+
if (outs.length === 1) return canonicalArgType(outs[0]!.type, defaultSchema);
|
|
476
|
+
if (outs.length > 1) return "record";
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** A `SET` value as compared: each item unquoted, a bare word folded, `TO` and `=` alike. */
|
|
481
|
+
function canonicalSetting(value: string): string {
|
|
482
|
+
const items: string[] = [];
|
|
483
|
+
let tokens: Token[];
|
|
484
|
+
try {
|
|
485
|
+
tokens = tokenizeText(value, 0).filter((t) => !isTrivia(t));
|
|
486
|
+
} catch {
|
|
487
|
+
return value.trim();
|
|
488
|
+
}
|
|
489
|
+
let sign = "";
|
|
490
|
+
for (const t of tokens) {
|
|
491
|
+
if (t.kind === "punct" && t.text === ",") continue;
|
|
492
|
+
if (t.kind === "op" && (t.text === "-" || t.text === "+")) {
|
|
493
|
+
sign = t.text === "-" ? "-" : "";
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (t.kind === "string") items.push(stringValue(t.text) ?? t.text);
|
|
497
|
+
else if (t.kind === "qident") items.push(identValue(t));
|
|
498
|
+
else items.push(`${sign}${t.kind === "ident" ? t.text.toLowerCase() : t.text}`);
|
|
499
|
+
sign = "";
|
|
500
|
+
}
|
|
501
|
+
return items.join(", ");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** The canonical fields of a function or procedure. */
|
|
505
|
+
function routineFields(kind: "function" | "procedure", p: RoutineProps, defaultSchema: string, fields: Record<string, unknown>): void {
|
|
506
|
+
const lang = (p.language ?? "sql").toLowerCase();
|
|
507
|
+
fields.args = p.args.map((a) =>
|
|
508
|
+
[a.mode, a.name ? quoteIdent(a.name) : "", canonicalArgType(a.type, defaultSchema), a.default !== undefined ? `default ${canonicalExpr(a.default)}` : ""].filter(Boolean).join(" "),
|
|
509
|
+
);
|
|
510
|
+
fields.language = lang;
|
|
511
|
+
// Line endings aside, the body is what the server keeps and prints back.
|
|
512
|
+
fields.body = p.body.replace(/\r\n/g, "\n");
|
|
513
|
+
fields.link = p.link;
|
|
514
|
+
fields.securityDefiner = p.securityDefiner === true;
|
|
515
|
+
fields.set = p.set && Object.keys(p.set).length > 0
|
|
516
|
+
? Object.fromEntries(Object.entries(p.set).map(([k, v]) => [k.toLowerCase(), canonicalSetting(v)]).sort(([a], [b]) => (a! < b! ? -1 : 1)))
|
|
517
|
+
: undefined;
|
|
518
|
+
fields.transform = p.transform ? canonicalExpr(p.transform) : undefined;
|
|
519
|
+
if (kind === "procedure") return;
|
|
520
|
+
const result = routineResult(p, defaultSchema);
|
|
521
|
+
const setReturning = result !== undefined && /^(setof |table\()/.test(result);
|
|
522
|
+
fields.returns = result;
|
|
523
|
+
fields.volatility = p.volatility && p.volatility !== "volatile" ? p.volatility : undefined;
|
|
524
|
+
fields.strict = p.strict === true;
|
|
525
|
+
fields.leakproof = p.leakproof === true;
|
|
526
|
+
fields.parallel = p.parallel && p.parallel !== "unsafe" ? p.parallel : undefined;
|
|
527
|
+
const defaultCost = lang === "c" || lang === "internal" ? 1 : 100;
|
|
528
|
+
fields.cost = p.cost !== undefined && Number(p.cost) !== defaultCost ? String(Number(p.cost)) : undefined;
|
|
529
|
+
const defaultRows = setReturning ? 1000 : 0;
|
|
530
|
+
fields.rows = p.rows !== undefined && Number(p.rows) !== defaultRows ? String(Number(p.rows)) : undefined;
|
|
531
|
+
fields.support = p.support ? canonicalName(p.support, "pg_catalog").replace(/^pg_catalog\./, "") : undefined;
|
|
532
|
+
fields.window = p.window === true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** A routine named with its parameters as a GRANT writes it (`app.f(int, text)`), as the target the catalog names: `app.f(integer,text)`. */
|
|
536
|
+
export function canonicalRoutineTarget(text: string, defaultSchema: string): string {
|
|
537
|
+
const open = text.indexOf("(");
|
|
538
|
+
const name = canonicalName(open < 0 ? text : text.slice(0, open), defaultSchema);
|
|
539
|
+
if (open < 0) return name;
|
|
540
|
+
try {
|
|
541
|
+
// The parameter list read the way a CREATE FUNCTION reads it: modes, names and types.
|
|
542
|
+
const tokens = tokenizeText(`CREATE FUNCTION x${text.slice(open)} RETURNS void LANGUAGE sql AS ''`, 0);
|
|
543
|
+
const [node] = parseStatements(tokens);
|
|
544
|
+
const args = (node as RoutineNode).args.map((a) => ({ mode: (a.mode?.toLowerCase() ?? "in") as RoutineProps["args"][number]["mode"], type: tokens.slice(a.type.from, a.type.to).map((t) => t.text).join("") }));
|
|
545
|
+
return `${name}${routineSignature({ args }, defaultSchema)}`;
|
|
546
|
+
} catch {
|
|
547
|
+
return `${name}${text.slice(open).replace(/\s+/g, "")}`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** A role's attributes that differ from `CREATE ROLE`'s defaults (NOLOGIN, INHERIT and every other NO...), sorted. */
|
|
552
|
+
const ROLE_DEFAULTS: Readonly<Record<string, boolean>> = { superuser: false, createdb: false, createrole: false, inherit: true, login: false, replication: false, bypassrls: false };
|
|
553
|
+
|
|
554
|
+
export function canonicalRoleAttributes(options: readonly string[]): string[] {
|
|
555
|
+
const set: Record<string, boolean> = { ...ROLE_DEFAULTS };
|
|
556
|
+
for (const o of options) {
|
|
557
|
+
const word = o.toLowerCase();
|
|
558
|
+
if (word.startsWith("no") && word.slice(2) in ROLE_DEFAULTS) set[word.slice(2)] = false;
|
|
559
|
+
else if (word in ROLE_DEFAULTS) set[word] = true;
|
|
560
|
+
}
|
|
561
|
+
return Object.entries(set)
|
|
562
|
+
.filter(([k, v]) => ROLE_DEFAULTS[k] !== v)
|
|
563
|
+
.map(([k, v]) => (v ? k : `no${k}`))
|
|
564
|
+
.sort();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** The order the catalog prints a trigger's events in. */
|
|
568
|
+
const EVENT_ORDER = ["insert", "delete", "update", "truncate"];
|
|
569
|
+
|
|
570
|
+
/** A trigger argument as the catalog stores it: a string. */
|
|
571
|
+
function triggerArg(a: string): string {
|
|
572
|
+
const t = a.trim();
|
|
573
|
+
if (/^'/.test(t) || /^[Ee]'/.test(t) || /^\$/.test(t)) return stringValue(t) ?? t;
|
|
574
|
+
if (/^"/.test(t)) return identValue(t);
|
|
575
|
+
if (/^-?[0-9.]/.test(t)) return t.replace(/\s+/g, "");
|
|
576
|
+
return t.toLowerCase();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function triggerFields(p: TriggerProps, defaultSchema: string, fields: Record<string, unknown>): void {
|
|
580
|
+
fields.table = canonicalName(p.tableName, defaultSchema);
|
|
581
|
+
fields.constraint = p.constraint === true;
|
|
582
|
+
fields.timing = p.timing;
|
|
583
|
+
fields.events = [...p.events]
|
|
584
|
+
.sort((a, b) => EVENT_ORDER.indexOf(a.event) - EVENT_ORDER.indexOf(b.event))
|
|
585
|
+
.map((e) => (e.columns && e.columns.length > 0 ? `${e.event} of ${[...e.columns].sort().map(quoteIdent).join(",")}` : e.event));
|
|
586
|
+
fields.forEach = p.forEach;
|
|
587
|
+
fields.when = canonicalExpr(p.when);
|
|
588
|
+
fields.function = canonicalName(p.functionName, defaultSchema);
|
|
589
|
+
fields.args = p.args.length > 0 ? p.args.map(triggerArg) : undefined;
|
|
590
|
+
fields.from = p.from ? canonicalName(p.from, defaultSchema) : undefined;
|
|
591
|
+
fields.deferrable = p.deferrable === true;
|
|
592
|
+
fields.initiallyDeferred = p.initiallyDeferred === true;
|
|
593
|
+
fields.referencing = p.referencing && (p.referencing.old || p.referencing.new)
|
|
594
|
+
? [p.referencing.old ? `old table as ${quoteIdent(p.referencing.old)}` : "", p.referencing.new ? `new table as ${quoteIdent(p.referencing.new)}` : ""].filter(Boolean).join(" ")
|
|
595
|
+
: undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
393
598
|
/**
|
|
394
599
|
* A definition in canonical form. `props` is an entity's props, declared or
|
|
395
600
|
* parsed from what the server printed; `defaultSchema` qualifies a bare name.
|
|
@@ -397,8 +602,8 @@ const keyBody = (kind: string, k: KeyDef) =>
|
|
|
397
602
|
export function canonicalPgObject(entityType: string, props: Record<string, unknown>, defaultSchema = "public"): CanonicalPgObject {
|
|
398
603
|
const kind = kindOf[entityType];
|
|
399
604
|
if (!kind) throw new Error(`not a Postgres entity type: ${entityType}`);
|
|
400
|
-
const schemaOf = (p: { schema?: string }) => (kind === "schema" || kind === "extension" ? undefined : (p.schema ?? defaultSchema));
|
|
401
|
-
const base = { kind, schema: schemaOf(props as { schema?: string }), name: String(props.name) };
|
|
605
|
+
const schemaOf = (p: { schema?: string }) => (kind === "schema" || kind === "extension" || kind === "role" || ACCESS_KINDS.has(kind) ? undefined : (p.schema ?? defaultSchema));
|
|
606
|
+
const base: Pick<CanonicalPgObject, "kind" | "schema" | "name" | "signature"> = { kind, schema: schemaOf(props as { schema?: string }), name: String(props.name) };
|
|
402
607
|
const fields: Record<string, unknown> = { comment: comment(props.comment as string | undefined) };
|
|
403
608
|
const columns: CanonicalColumn[] = [];
|
|
404
609
|
const constraints: CanonicalConstraint[] = [];
|
|
@@ -418,6 +623,66 @@ export function canonicalPgObject(entityType: string, props: Record<string, unkn
|
|
|
418
623
|
case "enum":
|
|
419
624
|
fields.labels = (props as unknown as EnumProps).labels;
|
|
420
625
|
break;
|
|
626
|
+
case "function":
|
|
627
|
+
case "procedure": {
|
|
628
|
+
const p = props as unknown as RoutineProps;
|
|
629
|
+
base.signature = routineSignature(p, defaultSchema);
|
|
630
|
+
routineFields(kind, p, defaultSchema, fields);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "trigger": {
|
|
634
|
+
const p = props as unknown as TriggerProps;
|
|
635
|
+
triggerFields(p, defaultSchema, fields);
|
|
636
|
+
base.signature = ` ON ${String(fields.table)}`;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "policy": {
|
|
640
|
+
const p = props as unknown as PolicyProps;
|
|
641
|
+
fields.table = canonicalName(p.tableName, defaultSchema);
|
|
642
|
+
base.signature = ` ON ${String(fields.table)}`;
|
|
643
|
+
fields.restrictive = p.permissive === false;
|
|
644
|
+
fields.command = p.command && p.command !== "all" ? p.command : undefined;
|
|
645
|
+
const roles = [...new Set(p.roles.map((r) => r.toLowerCase() === "public" ? "public" : quoteIdent(r)))].sort();
|
|
646
|
+
fields.roles = roles.length === 1 && roles[0] === "public" ? undefined : roles;
|
|
647
|
+
fields.using = canonicalExpr(p.using);
|
|
648
|
+
fields.check = canonicalExpr(p.check);
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case "role": {
|
|
652
|
+
const p = props as unknown as RoleProps;
|
|
653
|
+
const attributes = canonicalRoleAttributes(p.options);
|
|
654
|
+
fields.attributes = attributes.length > 0 ? attributes : undefined;
|
|
655
|
+
fields.connectionLimit = p.connectionLimit !== undefined && Number(p.connectionLimit) !== -1 ? String(Number(p.connectionLimit)) : undefined;
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case "grant": {
|
|
659
|
+
const p = props as unknown as GrantProps;
|
|
660
|
+
const routine = p.on === "function" || p.on === "procedure" || p.on === "routine";
|
|
661
|
+
fields.grant = {
|
|
662
|
+
action: p.action,
|
|
663
|
+
on: p.on,
|
|
664
|
+
objects: p.objectNames.map((n) => (routine ? canonicalRoutineTarget(n, defaultSchema) : p.on === "schema" ? canonicalName(n) : canonicalName(n, defaultSchema))),
|
|
665
|
+
privileges: p.privileges.map((x) => ({ privilege: x.privilege.toLowerCase(), ...(x.columns ? { columns: x.columns } : {}) })),
|
|
666
|
+
grantees: p.grantees,
|
|
667
|
+
...(p.withGrantOption ? { withGrantOption: true } : {}),
|
|
668
|
+
...(p.grantOptionFor ? { grantOptionFor: true } : {}),
|
|
669
|
+
};
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
case "defaultPrivileges": {
|
|
673
|
+
const p = props as unknown as DefaultPrivilegesProps;
|
|
674
|
+
fields.grant = {
|
|
675
|
+
action: p.action,
|
|
676
|
+
on: p.on,
|
|
677
|
+
privileges: p.privileges.map((x) => ({ privilege: x.toLowerCase() })),
|
|
678
|
+
grantees: p.grantees,
|
|
679
|
+
...(p.forRoles ? { forRoles: p.forRoles } : {}),
|
|
680
|
+
...(p.inSchemas ? { inSchemas: p.inSchemas } : {}),
|
|
681
|
+
...(p.withGrantOption ? { withGrantOption: true } : {}),
|
|
682
|
+
...(p.grantOptionFor ? { grantOptionFor: true } : {}),
|
|
683
|
+
};
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
421
686
|
case "domain": {
|
|
422
687
|
const p = props as unknown as DomainProps;
|
|
423
688
|
fields.dataType = canonicalType(p.dataType, defaultSchema);
|
|
@@ -527,6 +792,8 @@ export function canonicalPgObject(entityType: string, props: Record<string, unkn
|
|
|
527
792
|
fields.using = p.using && p.using.toLowerCase() !== "heap" ? p.using.toLowerCase() : undefined;
|
|
528
793
|
fields.with = canonicalOptions(p.with);
|
|
529
794
|
fields.tablespace = p.tablespace;
|
|
795
|
+
fields.rowSecurity = p.rowSecurity === true;
|
|
796
|
+
fields.forceRowSecurity = p.forceRowSecurity === true;
|
|
530
797
|
break;
|
|
531
798
|
}
|
|
532
799
|
}
|
|
@@ -270,6 +270,86 @@ export const PG_CLASSIFIER_RULES = {
|
|
|
270
270
|
),
|
|
271
271
|
SQLPG267: rule("SQLPG267", "metadata", "Change a schema's owner", "ALTER SCHEMA ... OWNER TO changes only the catalog.", `${DOCS}/sql-alterschema.html`),
|
|
272
272
|
SQLPG268: rule("SQLPG268", "expand", "Change an object's kind", "A view and a materialized view, or a table and either, are different kinds of object: the old one is dropped and the new one created.", `${DOCS}/sql-createview.html`),
|
|
273
|
+
SQLPG280: rule(
|
|
274
|
+
"SQLPG280",
|
|
275
|
+
"metadata",
|
|
276
|
+
"Replace a function's or procedure's definition",
|
|
277
|
+
"CREATE OR REPLACE FUNCTION (or PROCEDURE) replaces the body and attributes in the catalog; it locks no table, and a call already running finishes with the old definition. The parameter types, the result and the input parameters' names have to stay the same.",
|
|
278
|
+
`${DOCS}/sql-createfunction.html`,
|
|
279
|
+
),
|
|
280
|
+
SQLPG281: rule(
|
|
281
|
+
"SQLPG281",
|
|
282
|
+
"metadata",
|
|
283
|
+
"Drop and create a function or procedure",
|
|
284
|
+
"CREATE OR REPLACE refuses a change of the result type, the output parameters, an input parameter's name, a removed parameter default or the routine's kind: the routine is dropped and created in one transaction. Nothing on the server depends on it (a view, a trigger, a column default would make the DROP fail).",
|
|
285
|
+
`${DOCS}/sql-createfunction.html`,
|
|
286
|
+
),
|
|
287
|
+
SQLPG282: rule(
|
|
288
|
+
"SQLPG282",
|
|
289
|
+
"expand",
|
|
290
|
+
"Change a function's result or parameters while other objects depend on it",
|
|
291
|
+
"DROP FUNCTION refuses while a view, a trigger, a column default or another routine depends on the function, and CREATE OR REPLACE cannot make this change. Create the new definition under a new name (or signature), move what depends on it, then drop the old.",
|
|
292
|
+
`${DOCS}/sql-dropfunction.html`,
|
|
293
|
+
),
|
|
294
|
+
SQLPG283: rule(
|
|
295
|
+
"SQLPG283",
|
|
296
|
+
"metadata",
|
|
297
|
+
"Create a trigger on an existing table",
|
|
298
|
+
"CREATE TRIGGER takes SHARE ROW EXCLUSIVE on its table: writes and other schema changes to the table wait until the transaction commits, but no row is read. The applier's lock_timeout bounds the wait behind long-running transactions.",
|
|
299
|
+
`${DOCS}/sql-createtrigger.html`,
|
|
300
|
+
),
|
|
301
|
+
SQLPG284: rule(
|
|
302
|
+
"SQLPG284",
|
|
303
|
+
"metadata",
|
|
304
|
+
"Change a trigger",
|
|
305
|
+
"CREATE OR REPLACE TRIGGER (14 and later) replaces the trigger under SHARE ROW EXCLUSIVE on its table. A constraint trigger has no OR REPLACE, and a trigger moved to another table is another trigger: each is dropped (ACCESS EXCLUSIVE, briefly) and created in one transaction.",
|
|
306
|
+
`${DOCS}/sql-createtrigger.html`,
|
|
307
|
+
),
|
|
308
|
+
SQLPG285: rule("SQLPG285", "drop", "Drop a trigger", "DROP TRIGGER takes ACCESS EXCLUSIVE on its table, briefly; no row is read or lost, and the table's writes no longer fire it.", `${DOCS}/sql-droptrigger.html`),
|
|
309
|
+
SQLPG290: rule(
|
|
310
|
+
"SQLPG290",
|
|
311
|
+
"metadata",
|
|
312
|
+
"Create a policy on an existing table",
|
|
313
|
+
"CREATE POLICY takes ACCESS EXCLUSIVE on its table, briefly; no row is read. Where row-level security is on, the rows each role sees change when the transaction commits.",
|
|
314
|
+
`${DOCS}/sql-createpolicy.html`,
|
|
315
|
+
),
|
|
316
|
+
SQLPG291: rule(
|
|
317
|
+
"SQLPG291",
|
|
318
|
+
"metadata",
|
|
319
|
+
"Change a policy",
|
|
320
|
+
"ALTER POLICY changes its roles, USING and WITH CHECK under ACCESS EXCLUSIVE on the table, briefly. Its command, AS RESTRICTIVE and table cannot be altered, nor an expression taken away: the policy is dropped and created in one transaction, so no session sees the table without it.",
|
|
321
|
+
`${DOCS}/sql-alterpolicy.html`,
|
|
322
|
+
),
|
|
323
|
+
SQLPG292: rule("SQLPG292", "drop", "Drop a policy", "DROP POLICY takes ACCESS EXCLUSIVE on its table, briefly; the rows it let a role see are hidden from that role at once, or, if it was the last policy, every non-owner sees none.", `${DOCS}/sql-droppolicy.html`),
|
|
324
|
+
SQLPG293: rule(
|
|
325
|
+
"SQLPG293",
|
|
326
|
+
"metadata",
|
|
327
|
+
"Turn row-level security on or off, or force it on the owner",
|
|
328
|
+
"ALTER TABLE ... ENABLE, DISABLE, FORCE or NO FORCE ROW LEVEL SECURITY changes only the catalog, under ACCESS EXCLUSIVE, briefly. With it on and no policy that lets a role in, that role sees no rows; FORCE subjects the table's owner too.",
|
|
329
|
+
`${DOCS}/ddl-rowsecurity.html`,
|
|
330
|
+
),
|
|
331
|
+
SQLPG294: rule("SQLPG294", "metadata", "Change a role's attributes", "ALTER ROLE changes the role in the cluster's catalog, for every database on the server, at once; sessions already open keep the attributes they started with until they reconnect.", `${DOCS}/sql-alterrole.html`),
|
|
332
|
+
SQLPG296: rule(
|
|
333
|
+
"SQLPG296",
|
|
334
|
+
"metadata",
|
|
335
|
+
"Grant a privilege",
|
|
336
|
+
"GRANT adds to the object's privileges in the catalog; no row is read and the role can use it from its next statement.",
|
|
337
|
+
`${DOCS}/sql-grant.html`,
|
|
338
|
+
),
|
|
339
|
+
SQLPG297: rule(
|
|
340
|
+
"SQLPG297",
|
|
341
|
+
"metadata",
|
|
342
|
+
"Revoke a privilege",
|
|
343
|
+
"REVOKE removes from the object's privileges in the catalog. The role loses it at once: its next statement that needs it fails, so move every reader off it first.",
|
|
344
|
+
`${DOCS}/sql-revoke.html`,
|
|
345
|
+
),
|
|
346
|
+
SQLPG298: rule(
|
|
347
|
+
"SQLPG298",
|
|
348
|
+
"metadata",
|
|
349
|
+
"Change default privileges",
|
|
350
|
+
"ALTER DEFAULT PRIVILEGES changes what objects created afterwards are given; objects that exist keep their privileges.",
|
|
351
|
+
`${DOCS}/sql-alterdefaultprivileges.html`,
|
|
352
|
+
),
|
|
273
353
|
SQLPG270: rule("SQLPG270", "drop", "Drop an object", "The object and, for a table, a materialized view or a sequence, its data are gone.", `${DOCS}/sql-droptable.html`),
|
|
274
354
|
} as const satisfies Record<string, PgClassifierRule>;
|
|
275
355
|
|
|
@@ -28,6 +28,8 @@ export interface PgDiffObject extends CanonicalPgObject {
|
|
|
28
28
|
foreign?: string;
|
|
29
29
|
/** The export name, against a server. */
|
|
30
30
|
exportName?: string;
|
|
31
|
+
/** What depends on a routine on the server (a view, a trigger, a default), which a DROP FUNCTION would refuse. */
|
|
32
|
+
dependents?: string[];
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export type PgSchemaObject = Keyed<PgDiffObject>;
|
|
@@ -94,10 +96,24 @@ export function pgSchemaFromBuildFile(path: string, defaultSchema = "public"): P
|
|
|
94
96
|
return pgSchemaFromBuildOutput(readFileSync(path, "utf-8"), defaultSchema);
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The namespace an object's name is unique in: relations (tables, views,
|
|
101
|
+
* sequences, indexes) share one per schema, types and domains another,
|
|
102
|
+
* functions and procedures a third (told apart by their parameter types),
|
|
103
|
+
* a trigger's or a policy's name is unique on its table, and a role's in the
|
|
104
|
+
* cluster.
|
|
105
|
+
*/
|
|
106
|
+
export function pgNamespace(kind: string): string {
|
|
107
|
+
if (kind === "schema" || kind === "extension" || kind === "trigger" || kind === "policy" || kind === "role") return kind;
|
|
108
|
+
if (kind === "grant" || kind === "defaultPrivileges") return "grant";
|
|
109
|
+
if (kind === "enum" || kind === "domain") return "type";
|
|
110
|
+
if (kind === "function" || kind === "procedure") return "routine";
|
|
111
|
+
return "relation";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The identity an object has on a server: its kind's namespace, its qualified name, and its signature where it has one. */
|
|
115
|
+
export function qualifiedKey(o: { kind: string; schema?: string; name: string; signature?: string }): string {
|
|
116
|
+
return `${pgNamespace(o.kind)} ${o.schema ? `${o.schema}.` : ""}${o.name}${o.signature ?? ""}`;
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
/** The declared objects re-keyed by qualified name, the only identity a server has. */
|
|
@@ -107,8 +123,13 @@ export function keyedByQualifiedName(objects: readonly PgSchemaObject[]): PgSche
|
|
|
107
123
|
|
|
108
124
|
/** What a server holds, in the diff's form, keyed by qualified name. */
|
|
109
125
|
export function pgSchemaFromLive(live: readonly LivePgObject[], defaultSchema: string): PgSchemaObject[] {
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
// A routine with a SQL-standard body is not one a declaration can hold (`../parser.ts`): never chant's, never compared.
|
|
127
|
+
return live.filter((o) => !o.unsupported).map((o) => {
|
|
128
|
+
const canonical = {
|
|
129
|
+
...diffObject(o.type, o.statement, defaultSchema),
|
|
130
|
+
...(o.foreign ? { foreign: o.foreign } : {}),
|
|
131
|
+
...(o.dependents && o.dependents.length > 0 ? { dependents: o.dependents } : {}),
|
|
132
|
+
};
|
|
112
133
|
return { key: qualifiedKey(canonical), canonical };
|
|
113
134
|
});
|
|
114
135
|
}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* (`CREATE TEMP VIEW`, or a `CREATE TEMP TABLE` holding the columns' types,
|
|
9
9
|
* defaults and generated expressions and the checks) inside a transaction
|
|
10
10
|
* that is always rolled back, with a short `lock_timeout`, and read back with
|
|
11
|
-
* the same printers the catalog read uses.
|
|
11
|
+
* the same printers the catalog read uses. A trigger's `WHEN` and a policy's
|
|
12
|
+
* `USING` and `WITH CHECK`, which the server prints with the casts it
|
|
13
|
+
* resolved, are read back from a trigger or policy on a temporary copy of the
|
|
14
|
+
* table (`CREATE TEMP TABLE ... (LIKE table)`). Nothing is left behind: Postgres
|
|
12
15
|
* DDL is transactional and a temporary object lives in the session's own
|
|
13
16
|
* schema. A server that refuses (no TEMP privilege, a lock not granted in
|
|
14
17
|
* time, an expression it rejects) leaves the rules' answer standing.
|
|
@@ -88,6 +91,47 @@ async function tableExpressions(client: PostgresClient, o: CanonicalPgObject, de
|
|
|
88
91
|
});
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/** A trigger's WHEN condition as the server prints it, canonical. */
|
|
95
|
+
async function triggerWhen(client: PostgresClient, o: CanonicalPgObject, defaultSchema: string): Promise<string | undefined> {
|
|
96
|
+
const when = o.fields.when;
|
|
97
|
+
// An INSTEAD OF trigger, on a view, takes no WHEN.
|
|
98
|
+
if (typeof when !== "string" || o.fields.timing === "instead of") return undefined;
|
|
99
|
+
const events = ((o.fields.events as string[] | undefined) ?? []).map((e) => e.replace(/^(\w+)( of )?/, (_m, ev: string, of?: string) => `${ev.toUpperCase()}${of ? " OF " : ""}`));
|
|
100
|
+
return rolledBack(client, defaultSchema, async () => {
|
|
101
|
+
await client.query(`CREATE TEMP TABLE chant_normalize (LIKE ${String(o.fields.table)})`);
|
|
102
|
+
await client.query(`CREATE FUNCTION pg_temp.chant_normalize() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RETURN NULL; END'`);
|
|
103
|
+
await client.query(
|
|
104
|
+
`CREATE TRIGGER chant_normalize ${String(o.fields.timing).toUpperCase()} ${events.join(" OR ")} ON pg_temp.chant_normalize ` +
|
|
105
|
+
`FOR EACH ${o.fields.forEach === "row" ? "ROW" : "STATEMENT"} WHEN (${when}) EXECUTE FUNCTION pg_temp.chant_normalize()`,
|
|
106
|
+
);
|
|
107
|
+
await emptyPath(client);
|
|
108
|
+
const [row] = await client.query<Row>(`SELECT pg_catalog.pg_get_triggerdef(t.oid, true) AS def FROM pg_catalog.pg_trigger t WHERE t.tgrelid = 'pg_temp.chant_normalize'::pg_catalog.regclass`);
|
|
109
|
+
const m = / WHEN \((.*)\) EXECUTE (?:FUNCTION|PROCEDURE) /s.exec(String(row?.def ?? ""));
|
|
110
|
+
return m ? canonicalExpr(m[1]!) : undefined;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A policy's USING and WITH CHECK as the server prints them, canonical, from a policy on a temporary copy of its table. */
|
|
115
|
+
async function policyExpressions(client: PostgresClient, o: CanonicalPgObject, defaultSchema: string): Promise<{ using?: string; check?: string } | undefined> {
|
|
116
|
+
const using = o.fields.using as string | undefined;
|
|
117
|
+
const check = o.fields.check as string | undefined;
|
|
118
|
+
if (using === undefined && check === undefined) return undefined;
|
|
119
|
+
return rolledBack(client, defaultSchema, async () => {
|
|
120
|
+
await client.query(`CREATE TEMP TABLE chant_normalize (LIKE ${String(o.fields.table)})`);
|
|
121
|
+
await client.query(
|
|
122
|
+
[`CREATE POLICY chant_normalize ON pg_temp.chant_normalize FOR ${String(o.fields.command ?? "all").toUpperCase()}`, using !== undefined ? `USING (${using})` : "", check !== undefined ? `WITH CHECK (${check})` : ""]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join(" "),
|
|
125
|
+
);
|
|
126
|
+
await emptyPath(client);
|
|
127
|
+
const [row] = await client.query<Row>(
|
|
128
|
+
`SELECT pg_catalog.pg_get_expr(p.polqual, p.polrelid, true) AS qual, pg_catalog.pg_get_expr(p.polwithcheck, p.polrelid, true) AS withcheck
|
|
129
|
+
FROM pg_catalog.pg_policy p WHERE p.polrelid = 'pg_temp.chant_normalize'::pg_catalog.regclass`,
|
|
130
|
+
);
|
|
131
|
+
return { ...(row?.qual ? { using: canonicalExpr(String(row.qual)) } : {}), ...(row?.withcheck ? { check: canonicalExpr(String(row.withcheck)) } : {}) };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
91
135
|
/**
|
|
92
136
|
* The declared object with its expressions in the server's own printing,
|
|
93
137
|
* when the server answers; the object unchanged otherwise. Only the
|
|
@@ -99,6 +143,14 @@ export async function serverNormalized<T extends CanonicalPgObject & { outputs?:
|
|
|
99
143
|
// A declared column list names the outputs; otherwise the server's names do (`SELECT *` expanded).
|
|
100
144
|
return q === undefined ? o : { ...o, fields: { ...o.fields, query: q.query }, ...(o.fields.columns === undefined ? { outputs: q.outputs } : {}) };
|
|
101
145
|
}
|
|
146
|
+
if (o.kind === "policy") {
|
|
147
|
+
const e = await policyExpressions(client, o, defaultSchema);
|
|
148
|
+
return e === undefined ? o : { ...o, fields: { ...o.fields, ...e } };
|
|
149
|
+
}
|
|
150
|
+
if (o.kind === "trigger") {
|
|
151
|
+
const when = await triggerWhen(client, o, defaultSchema);
|
|
152
|
+
return when === undefined ? o : { ...o, fields: { ...o.fields, when } };
|
|
153
|
+
}
|
|
102
154
|
if (o.kind === "table") {
|
|
103
155
|
const e = await tableExpressions(client, o, defaultSchema);
|
|
104
156
|
if (!e) return o;
|