@intentius/chant-lexicon-sql 0.116.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 +20 -0
- package/dist/okf/index.md +4 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
- package/dist/okf/types/PostgresGrant.md +9 -0
- package/dist/okf/types/PostgresPolicy.md +9 -0
- package/dist/okf/types/PostgresRole.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.map +1 -1
- package/dist/postgres/apply/statements.d.ts +3 -0
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +106 -4
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +4 -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 +11 -2
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +9 -0
- 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 +76 -3
- 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 +10 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +8 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +8 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +2 -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 +7 -4
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/registry.ts +4 -0
- package/src/config.ts +10 -0
- package/src/generated/lexicon-sql.json +20 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +7 -4
- package/src/lsp/postgres-template.ts +2 -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 +3 -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 +117 -4
- package/src/postgres/apply/statements.ts +55 -9
- package/src/postgres/entities.ts +287 -11
- package/src/postgres/entity-types.ts +4 -0
- package/src/postgres/import/generator.ts +14 -4
- package/src/postgres/import/live-export.ts +12 -2
- package/src/postgres/live/bind.ts +3 -0
- package/src/postgres/live/catalog.ts +79 -3
- package/src/postgres/live/describe-resources.ts +27 -3
- 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 +247 -8
- package/src/postgres/plan/commands.ts +60 -7
- package/src/postgres/plan/deep.ts +24 -3
- package/src/postgres/plan/diff.ts +71 -6
- package/src/postgres/plan/normalize.ts +115 -2
- package/src/postgres/plan/rules.ts +44 -0
- package/src/postgres/plan/schema.ts +4 -2
- package/src/postgres/plan/server-normalize.ts +29 -1
- package/src/postgres.ts +11 -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 +9 -2
- package/src/validate.ts +4 -0
|
@@ -60,6 +60,10 @@ import type {
|
|
|
60
60
|
IndexProps,
|
|
61
61
|
KeyDef,
|
|
62
62
|
SchemaProps,
|
|
63
|
+
DefaultPrivilegesProps,
|
|
64
|
+
GrantProps,
|
|
65
|
+
PolicyProps,
|
|
66
|
+
RoleProps,
|
|
63
67
|
RoutineProps,
|
|
64
68
|
SequenceProps,
|
|
65
69
|
TableProps,
|
|
@@ -67,6 +71,8 @@ import type {
|
|
|
67
71
|
ViewProps,
|
|
68
72
|
} from "../entities";
|
|
69
73
|
import { stringValue } from "../entities";
|
|
74
|
+
import { parseStatements } from "../parser";
|
|
75
|
+
import type { RoutineNode } from "../parser";
|
|
70
76
|
|
|
71
77
|
// ── Types ──────────────────────────────────────────────────────────────
|
|
72
78
|
|
|
@@ -319,7 +325,29 @@ export interface CanonicalConstraint {
|
|
|
319
325
|
comment?: string;
|
|
320
326
|
}
|
|
321
327
|
|
|
322
|
-
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"]);
|
|
323
351
|
|
|
324
352
|
export interface CanonicalPgObject {
|
|
325
353
|
kind: CanonicalKind;
|
|
@@ -350,6 +378,10 @@ const kindOf: Record<string, CanonicalKind> = {
|
|
|
350
378
|
"Postgres::Function": "function",
|
|
351
379
|
"Postgres::Procedure": "procedure",
|
|
352
380
|
"Postgres::Trigger": "trigger",
|
|
381
|
+
"Postgres::Policy": "policy",
|
|
382
|
+
"Postgres::Role": "role",
|
|
383
|
+
"Postgres::Grant": "grant",
|
|
384
|
+
"Postgres::DefaultPrivileges": "defaultPrivileges",
|
|
353
385
|
};
|
|
354
386
|
|
|
355
387
|
const comment = (c: string | undefined): string | undefined => {
|
|
@@ -500,6 +532,38 @@ function routineFields(kind: "function" | "procedure", p: RoutineProps, defaultS
|
|
|
500
532
|
fields.window = p.window === true;
|
|
501
533
|
}
|
|
502
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
|
+
|
|
503
567
|
/** The order the catalog prints a trigger's events in. */
|
|
504
568
|
const EVENT_ORDER = ["insert", "delete", "update", "truncate"];
|
|
505
569
|
|
|
@@ -538,7 +602,7 @@ function triggerFields(p: TriggerProps, defaultSchema: string, fields: Record<st
|
|
|
538
602
|
export function canonicalPgObject(entityType: string, props: Record<string, unknown>, defaultSchema = "public"): CanonicalPgObject {
|
|
539
603
|
const kind = kindOf[entityType];
|
|
540
604
|
if (!kind) throw new Error(`not a Postgres entity type: ${entityType}`);
|
|
541
|
-
const schemaOf = (p: { schema?: string }) => (kind === "schema" || kind === "extension" ? undefined : (p.schema ?? defaultSchema));
|
|
605
|
+
const schemaOf = (p: { schema?: string }) => (kind === "schema" || kind === "extension" || kind === "role" || ACCESS_KINDS.has(kind) ? undefined : (p.schema ?? defaultSchema));
|
|
542
606
|
const base: Pick<CanonicalPgObject, "kind" | "schema" | "name" | "signature"> = { kind, schema: schemaOf(props as { schema?: string }), name: String(props.name) };
|
|
543
607
|
const fields: Record<string, unknown> = { comment: comment(props.comment as string | undefined) };
|
|
544
608
|
const columns: CanonicalColumn[] = [];
|
|
@@ -572,6 +636,53 @@ export function canonicalPgObject(entityType: string, props: Record<string, unkn
|
|
|
572
636
|
base.signature = ` ON ${String(fields.table)}`;
|
|
573
637
|
break;
|
|
574
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
|
+
}
|
|
575
686
|
case "domain": {
|
|
576
687
|
const p = props as unknown as DomainProps;
|
|
577
688
|
fields.dataType = canonicalType(p.dataType, defaultSchema);
|
|
@@ -681,6 +792,8 @@ export function canonicalPgObject(entityType: string, props: Record<string, unkn
|
|
|
681
792
|
fields.using = p.using && p.using.toLowerCase() !== "heap" ? p.using.toLowerCase() : undefined;
|
|
682
793
|
fields.with = canonicalOptions(p.with);
|
|
683
794
|
fields.tablespace = p.tablespace;
|
|
795
|
+
fields.rowSecurity = p.rowSecurity === true;
|
|
796
|
+
fields.forceRowSecurity = p.forceRowSecurity === true;
|
|
684
797
|
break;
|
|
685
798
|
}
|
|
686
799
|
}
|
|
@@ -306,6 +306,50 @@ export const PG_CLASSIFIER_RULES = {
|
|
|
306
306
|
`${DOCS}/sql-createtrigger.html`,
|
|
307
307
|
),
|
|
308
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
|
+
),
|
|
309
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`),
|
|
310
354
|
} as const satisfies Record<string, PgClassifierRule>;
|
|
311
355
|
|
|
@@ -100,10 +100,12 @@ export function pgSchemaFromBuildFile(path: string, defaultSchema = "public"): P
|
|
|
100
100
|
* The namespace an object's name is unique in: relations (tables, views,
|
|
101
101
|
* sequences, indexes) share one per schema, types and domains another,
|
|
102
102
|
* functions and procedures a third (told apart by their parameter types),
|
|
103
|
-
*
|
|
103
|
+
* a trigger's or a policy's name is unique on its table, and a role's in the
|
|
104
|
+
* cluster.
|
|
104
105
|
*/
|
|
105
106
|
export function pgNamespace(kind: string): string {
|
|
106
|
-
if (kind === "schema" || kind === "extension" || kind === "trigger") return kind;
|
|
107
|
+
if (kind === "schema" || kind === "extension" || kind === "trigger" || kind === "policy" || kind === "role") return kind;
|
|
108
|
+
if (kind === "grant" || kind === "defaultPrivileges") return "grant";
|
|
107
109
|
if (kind === "enum" || kind === "domain") return "type";
|
|
108
110
|
if (kind === "function" || kind === "procedure") return "routine";
|
|
109
111
|
return "relation";
|
|
@@ -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.
|
|
@@ -108,6 +111,27 @@ async function triggerWhen(client: PostgresClient, o: CanonicalPgObject, default
|
|
|
108
111
|
});
|
|
109
112
|
}
|
|
110
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
|
+
|
|
111
135
|
/**
|
|
112
136
|
* The declared object with its expressions in the server's own printing,
|
|
113
137
|
* when the server answers; the object unchanged otherwise. Only the
|
|
@@ -119,6 +143,10 @@ export async function serverNormalized<T extends CanonicalPgObject & { outputs?:
|
|
|
119
143
|
// A declared column list names the outputs; otherwise the server's names do (`SELECT *` expanded).
|
|
120
144
|
return q === undefined ? o : { ...o, fields: { ...o.fields, query: q.query }, ...(o.fields.columns === undefined ? { outputs: q.outputs } : {}) };
|
|
121
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
|
+
}
|
|
122
150
|
if (o.kind === "trigger") {
|
|
123
151
|
const when = await triggerWhen(client, o, defaultSchema);
|
|
124
152
|
return when === undefined ? o : { ...o, fields: { ...o.fields, when } };
|
package/src/postgres.ts
CHANGED
|
@@ -18,6 +18,9 @@ export {
|
|
|
18
18
|
func,
|
|
19
19
|
procedure,
|
|
20
20
|
trigger,
|
|
21
|
+
policy,
|
|
22
|
+
role,
|
|
23
|
+
grant,
|
|
21
24
|
literal,
|
|
22
25
|
SqlLiteral,
|
|
23
26
|
SqlTemplateError,
|
|
@@ -39,6 +42,9 @@ export {
|
|
|
39
42
|
type PostgresFunction,
|
|
40
43
|
type PostgresProcedure,
|
|
41
44
|
type PostgresTrigger,
|
|
45
|
+
type PostgresPolicy,
|
|
46
|
+
type PostgresRole,
|
|
47
|
+
type PostgresGrant,
|
|
42
48
|
type ColumnDef,
|
|
43
49
|
type KeyDef,
|
|
44
50
|
type CheckDef,
|
|
@@ -57,6 +63,11 @@ export {
|
|
|
57
63
|
type FunctionProps,
|
|
58
64
|
type ProcedureProps,
|
|
59
65
|
type TriggerProps,
|
|
66
|
+
type PolicyProps,
|
|
67
|
+
type RoleProps,
|
|
68
|
+
type GrantProps,
|
|
69
|
+
type DefaultPrivilegesProps,
|
|
70
|
+
type PrivilegeDef,
|
|
60
71
|
type LineageEdge,
|
|
61
72
|
} from "./postgres/entities";
|
|
62
73
|
|
package/src/skill-defs.ts
CHANGED
|
@@ -54,7 +54,7 @@ export const sqlSkills = createSkillsLoader(import.meta.url, [
|
|
|
54
54
|
file: "chant-sql-postgres.md",
|
|
55
55
|
name: "chant-sql-postgres",
|
|
56
56
|
description:
|
|
57
|
-
"Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures and
|
|
57
|
+
"Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures, triggers, policies, roles and grants as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules",
|
|
58
58
|
triggers: [
|
|
59
59
|
{ type: "context" as const, value: "postgres" },
|
|
60
60
|
{ type: "context" as const, value: "postgres schema" },
|
|
@@ -40,6 +40,8 @@ Options besides `name`, `env`, `table`, `column`, `using`:
|
|
|
40
40
|
- `gate` and `contractGate`: the two approvals, each with `gate` (name), `timeout` and `description`; `gate` also takes `approval` for quorum, roles and a policy, and a policy gets `verifiedRows` and `mismatched` as context;
|
|
41
41
|
- `backfillTimeout` (default `6h`), `output` (default `dist/schema.json`), `path`, `build` (default true runs `chant build` first), `stack`, `ownershipEnv`.
|
|
42
42
|
|
|
43
|
+
Run as one step of a change approved elsewhere: `gates: "outer"` leaves out the switch gate, the contract gate and the Contract phase (verification still runs; the old column is kept until its retention date), and `onFailure: "keep"` leaves out onFailure, so a failed run's next run resumes the backfill from its receipts. Set them rather than editing the Op's phases.
|
|
44
|
+
|
|
43
45
|
## What it does
|
|
44
46
|
|
|
45
47
|
| Phase | Step |
|
|
@@ -103,7 +105,7 @@ or set `replicationLag: false` to run without the check.
|
|
|
103
105
|
|
|
104
106
|
## When something goes wrong
|
|
105
107
|
|
|
106
|
-
- A failed step (a value the `using` expression cannot convert, a verification mismatch, a refused declaration, an index that cannot be built) runs onFailure: the trigger, its function, the check, the carried indexes and constraints, the new column and the receipts table are dropped, and the plan shows the original change again. The old column was never touched. Fix the declaration or the data and run again. A failed gate or an aborted run does not run onFailure.
|
|
108
|
+
- A failed step (a value the `using` expression cannot convert, a verification mismatch, a refused declaration, an index that cannot be built) runs onFailure, unless the Op sets `onFailure: "keep"`: the trigger, its function, the check, the carried indexes and constraints, the new column and the receipts table are dropped, and the plan shows the original change again. The old column was never touched. Fix the declaration or the data and run again. A failed gate or an aborted run does not run onFailure.
|
|
107
109
|
- A lock timeout means another session holds a lock the step needs (SQLSTATE 55P03). The step retries a few times, then fails. Run again when the blocker has finished.
|
|
108
110
|
- The working objects (new column, trigger, function, check, receipts table) carry chant's comment marker with a `migration=` key, and plans, imports and prunes leave them out, so an `ApplyOp` for the rest of the schema can run beside a migration. An object under one of the working names that is not this migration's stops the run; rename or drop it by hand.
|
|
109
111
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
skill: chant-sql-postgres
|
|
3
|
-
description: Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures and
|
|
3
|
+
description: Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures, triggers, policies, roles and grants as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules
|
|
4
4
|
user-invocable: true
|
|
5
5
|
---
|
|
6
6
|
# Declaring a Postgres schema with chant
|
|
@@ -40,6 +40,9 @@ The export name (`users`) is the object's identity in chant. The name in the SQL
|
|
|
40
40
|
| `func` | `CREATE FUNCTION` | `Postgres::Function` |
|
|
41
41
|
| `procedure` | `CREATE PROCEDURE` | `Postgres::Procedure` |
|
|
42
42
|
| `trigger` | `CREATE TRIGGER` | `Postgres::Trigger` |
|
|
43
|
+
| `policy` | `CREATE POLICY` | `Postgres::Policy` |
|
|
44
|
+
| `role` | `CREATE ROLE` (no password, no memberships) | `Postgres::Role` |
|
|
45
|
+
| `grant` | `GRANT`, `REVOKE`, `ALTER DEFAULT PRIVILEGES` | `Postgres::Grant`, `Postgres::DefaultPrivileges` |
|
|
43
46
|
|
|
44
47
|
`table` and `view` imported from the package root are ClickHouse's. Import every tag from `@intentius/chant-lexicon-sql/postgres`.
|
|
45
48
|
|
|
@@ -140,6 +143,10 @@ export const ordersTouch = trigger`CREATE TRIGGER orders_touch BEFORE UPDATE ON
|
|
|
140
143
|
|
|
141
144
|
The tag is `func` (`function` is a JavaScript key word). Write the body as a string, `AS $$ ... $$`: it is kept and compared verbatim. `${users}` inside the body renders as `app.users` and makes the routine depend on the table. A SQL-standard body (`RETURN ...`, `BEGIN ATOMIC`) and `SET ... FROM CURRENT` are SQLPG001 errors. Overloads are separate objects, identified by their input parameter types. A trigger's name is unqualified; it lives on its table.
|
|
142
145
|
|
|
146
|
+
## Access
|
|
147
|
+
|
|
148
|
+
A table's template may end with `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`. `policy`, `role` and `grant` declare policies, the roles the schema needs (never a password or a membership: those are the environment's), privileges on schemas, tables, columns, sequences and routines, and default privileges. They are planned and applied only where `sql.profiles.<env>.access` is true; there, the declared privileges are the whole of them, and one granted by hand is revoked. A role the environment provisions is named as text (`TO support`), never declared. `GRANT ... ON ALL TABLES IN SCHEMA` is refused: grant per table and declare default privileges.
|
|
149
|
+
|
|
143
150
|
## Comments
|
|
144
151
|
|
|
145
152
|
Postgres has no inline comment clause. Follow the `CREATE` with `COMMENT ON` for the object, its columns and its named constraints, separated by `;`:
|
|
@@ -178,4 +185,4 @@ chant build src --lexicon sql -o dist/schema.json
|
|
|
178
185
|
|
|
179
186
|
## Not covered
|
|
180
187
|
|
|
181
|
-
`ALTER
|
|
188
|
+
`ALTER` (other than a table's row-level security), role memberships, rules, aggregates, event triggers and the composite and range forms of `CREATE TYPE` are not declared with the tags. Planning a schema change (`chant sql diff`, `chant sql plan`, the lock classes) is the `chant-sql-postgres-plan` skill. Apply and the expand-and-contract migration Op have their own skills, which arrive with the code they describe.
|
package/src/validate.ts
CHANGED
|
@@ -37,6 +37,10 @@ const REQUIRED_ENTITIES = [
|
|
|
37
37
|
"PostgresFunction",
|
|
38
38
|
"PostgresProcedure",
|
|
39
39
|
"PostgresTrigger",
|
|
40
|
+
"PostgresPolicy",
|
|
41
|
+
"PostgresRole",
|
|
42
|
+
"PostgresGrant",
|
|
43
|
+
"PostgresDefaultPrivileges",
|
|
40
44
|
];
|
|
41
45
|
|
|
42
46
|
/** Engines a ClickHouse schema declares most: the MergeTree family and the engines beside it. */
|