@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
|
@@ -84,6 +84,9 @@ import type { PgChangeClass } from "../plan/rules";
|
|
|
84
84
|
import type { CanonicalKind } from "../plan/normalize";
|
|
85
85
|
import { quoteIdent } from "../keywords";
|
|
86
86
|
import { planPgStatements, scansRows, type DeclaredPgObject, type PgDropStatement, type PgStep } from "./statements";
|
|
87
|
+
import { ACCESS_KINDS, ACCESS_MANAGED_KINDS } from "../plan/normalize";
|
|
88
|
+
import { declaredAccess, diffAccess, targetKey } from "../access/acl";
|
|
89
|
+
import { readLiveAccess } from "../access/live";
|
|
87
90
|
|
|
88
91
|
/** The timeouts a statement runs with, in milliseconds; 0 is no limit. */
|
|
89
92
|
export interface PostgresApplyTimeouts {
|
|
@@ -144,9 +147,9 @@ export class PostgresApplyError extends SqlApplyError<PostgresApplyOutcome> {
|
|
|
144
147
|
* (`app.touch()`), a trigger's with its table (`touch ON app.users`).
|
|
145
148
|
*/
|
|
146
149
|
export const refName = (o: { kind: CanonicalKind; schema?: string; name: string; signature?: string }): string =>
|
|
147
|
-
o.kind === "trigger"
|
|
150
|
+
o.kind === "trigger" || o.kind === "policy"
|
|
148
151
|
? `${o.name}${o.signature ?? ""}`
|
|
149
|
-
: o.kind === "schema" || o.kind === "extension" || o.schema === undefined
|
|
152
|
+
: o.kind === "schema" || o.kind === "extension" || o.kind === "role" || o.schema === undefined
|
|
150
153
|
? o.name
|
|
151
154
|
: `${o.schema}.${o.name}${o.kind === "function" || o.kind === "procedure" ? (o.signature ?? "") : ""}`;
|
|
152
155
|
|
|
@@ -259,7 +262,21 @@ export async function applyPostgres(
|
|
|
259
262
|
});
|
|
260
263
|
const normalizedByKey = new Map(plan.declared.map((o) => [o.key, o.canonical]));
|
|
261
264
|
const liveRawByKey = new Map(plan.liveObjects.map((o) => [qualifiedKey(liveCanonicalAddress(o)), o]));
|
|
262
|
-
const
|
|
265
|
+
const managed = target.access === true;
|
|
266
|
+
// Access declarations a profile does not manage are not applied; grants and default privileges run after every object, as access.
|
|
267
|
+
for (const o of declared) {
|
|
268
|
+
if (!managed && ACCESS_MANAGED_KINDS.has(o.canonical.kind)) {
|
|
269
|
+
outcome.notAttempted.push({
|
|
270
|
+
kind: o.type,
|
|
271
|
+
name: o.name,
|
|
272
|
+
reason: "filtered",
|
|
273
|
+
detail: `access is not managed in this environment: set sql.profiles.<env>.access to plan and apply policies, roles, grants and row-level security`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const objects = declared
|
|
278
|
+
.filter((o) => !ACCESS_KINDS.has(o.canonical.kind) && (managed || !ACCESS_MANAGED_KINDS.has(o.canonical.kind)))
|
|
279
|
+
.map((original) => ({ ...original, canonical: { ...original.canonical, ...normalizedByKey.get(original.key) } }));
|
|
263
280
|
const matchedLive = new Set<string>();
|
|
264
281
|
for (const m of matchPgObjects(plan.live, plan.declared)) if (m.kind === "matched") matchedLive.add(m.after.key);
|
|
265
282
|
|
|
@@ -275,6 +292,7 @@ export async function applyPostgres(
|
|
|
275
292
|
allowDestructive: opts.prune === true,
|
|
276
293
|
carriesMarker: (_live, key) => carriesMarker(liveRawByKey.get(key)?.comment, opts.marker),
|
|
277
294
|
extensionComment: (name) => extensionComments.get(name),
|
|
295
|
+
access: managed,
|
|
278
296
|
...(opts.marker ? { marker: opts.marker } : {}),
|
|
279
297
|
});
|
|
280
298
|
|
|
@@ -436,11 +454,102 @@ export async function applyPostgres(
|
|
|
436
454
|
}
|
|
437
455
|
await commit();
|
|
438
456
|
|
|
457
|
+
if (managed) await applyAccess(client, declared, onServer, exportNames, { ...opts, major: plan.major ?? opts.major ?? POSTGRES_LATEST_MAJOR, ...(plan.scope ? { schemas: plan.scope } : {}) }, timeouts, outcome, log);
|
|
458
|
+
|
|
439
459
|
if (opts.prune) await prune(client, statements.drops, liveRawByKey, opts, timeouts, outcome, log);
|
|
440
460
|
if (outcome.failed.length > 0) throw new PostgresApplyError(outcome);
|
|
441
461
|
return outcome;
|
|
442
462
|
}
|
|
443
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Access, after every object (#3681): the privileges and default privileges
|
|
466
|
+
* the declarations add up to, against what the server holds now that the
|
|
467
|
+
* objects exist, made in one transaction. A grant or default-privileges
|
|
468
|
+
* declaration is `updated` when a statement ran for it and `unchanged`
|
|
469
|
+
* otherwise; a privilege nothing declares is revoked under the object's name.
|
|
470
|
+
*/
|
|
471
|
+
async function applyAccess(
|
|
472
|
+
client: PostgresClient,
|
|
473
|
+
declared: readonly DeclaredPgObject[],
|
|
474
|
+
onServer: ReadonlySet<string>,
|
|
475
|
+
exportNames: ReadonlySet<string>,
|
|
476
|
+
opts: PostgresApplyOptions & { major: number; schemas?: string[] },
|
|
477
|
+
timeouts: PostgresApplyTimeouts,
|
|
478
|
+
outcome: PostgresApplyOutcome,
|
|
479
|
+
log: (line: string) => void,
|
|
480
|
+
): Promise<void> {
|
|
481
|
+
const decls = declared.filter((o) => ACCESS_KINDS.has(o.canonical.kind));
|
|
482
|
+
const ref = (o: DeclaredPgObject) => ({ kind: o.type, name: o.canonical.name });
|
|
483
|
+
const ready: DeclaredPgObject[] = [];
|
|
484
|
+
for (const o of decls) {
|
|
485
|
+
const missing = missingDependencies(o.exportName, o.dependsOn, exportNames, onServer);
|
|
486
|
+
if (missing.length > 0) outcome.notAttempted.push({ ...ref(o), reason: "dependency-failed", detail: dependencyFailedDetail(missing) });
|
|
487
|
+
else ready.push(o);
|
|
488
|
+
}
|
|
489
|
+
const skipped = new Set(decls.filter((o) => !ready.includes(o)).map((o) => o.exportName));
|
|
490
|
+
const [me] = await client.query<{ self: string }>("SELECT current_user AS self");
|
|
491
|
+
const want = declaredAccess(
|
|
492
|
+
declared.filter((o) => !skipped.has(o.exportName)).map((o) => ({ key: o.exportName, canonical: o.canonical })),
|
|
493
|
+
{ major: opts.major, ...(me?.self ? { self: me.self } : {}) },
|
|
494
|
+
);
|
|
495
|
+
const have = await readLiveAccess(client, want.targets, {
|
|
496
|
+
...(opts.schemas ? { schemas: opts.schemas } : {}),
|
|
497
|
+
forRoles: want.forRoles,
|
|
498
|
+
declaredDefaults: want.targets.filter((t) => t.kind === "default"),
|
|
499
|
+
});
|
|
500
|
+
// A target that is not on the server (its object failed) is left for the next apply.
|
|
501
|
+
const changes = diffAccess(have.state, want.state, want.exportsOf).filter(
|
|
502
|
+
(c) => c.target.kind === "default" || have.present.has(targetKey(c.target.kind === "column" ? { kind: "table", name: c.target.name } : c.target)),
|
|
503
|
+
);
|
|
504
|
+
const ranFor = new Map<string, string[]>();
|
|
505
|
+
if (changes.length > 0) {
|
|
506
|
+
const t = { lockTimeoutMs: timeouts.lockTimeoutMs, statementTimeoutMs: timeouts.statementTimeoutMs };
|
|
507
|
+
const number = outcome.transactions.length;
|
|
508
|
+
const members = new Set<string>();
|
|
509
|
+
log("BEGIN");
|
|
510
|
+
await client.query("BEGIN");
|
|
511
|
+
try {
|
|
512
|
+
for (const set of [`SET LOCAL lock_timeout = ${ms(t.lockTimeoutMs)}`, `SET LOCAL statement_timeout = ${ms(t.statementTimeoutMs)}`]) {
|
|
513
|
+
log(set);
|
|
514
|
+
await client.query(set);
|
|
515
|
+
}
|
|
516
|
+
for (const c of changes) {
|
|
517
|
+
const object = c.exports[0] !== undefined ? (declared.find((o) => o.exportName === c.exports[0])?.canonical.name ?? c.change.object) : c.change.object.slice(4);
|
|
518
|
+
members.add(object);
|
|
519
|
+
for (const sql of c.sql) {
|
|
520
|
+
opts.signal?.throwIfAborted();
|
|
521
|
+
log(sql);
|
|
522
|
+
outcome.statements.push({ object, sql, class: c.change.class, ...t, transaction: number });
|
|
523
|
+
await client.query(sql);
|
|
524
|
+
for (const e of c.exports) ranFor.set(e, [...(ranFor.get(e) ?? []), sql]);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
log("COMMIT");
|
|
528
|
+
await client.query("COMMIT");
|
|
529
|
+
outcome.transactions.push({ objects: [...members], result: "committed" });
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (opts.signal?.aborted) {
|
|
532
|
+
await client.query("ROLLBACK").catch(() => undefined);
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
535
|
+
const error = errorText(err, t);
|
|
536
|
+
const failedAt = outcome.statements[outcome.statements.length - 1]?.sql ?? "BEGIN";
|
|
537
|
+
log("ROLLBACK");
|
|
538
|
+
await client.query("ROLLBACK").catch(() => undefined);
|
|
539
|
+
outcome.transactions.push({ objects: [...members], result: "rolled-back", failedAt: { sql: failedAt, error } });
|
|
540
|
+
for (const o of ready) {
|
|
541
|
+
if (changes.some((c) => c.exports.includes(o.exportName))) outcome.failed.push({ ...ref(o), error: `${error}, in the access transaction at ${failedAt}`, statements: [] });
|
|
542
|
+
else outcome.applied.push({ ...ref(o), action: "unchanged", statements: [] });
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const o of ready) {
|
|
548
|
+
const ran = ranFor.get(o.exportName) ?? [];
|
|
549
|
+
outcome.applied.push({ ...ref(o), action: ran.length > 0 ? "updated" : "unchanged", statements: ran });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
444
553
|
/** The address a live object is keyed by in the diff. */
|
|
445
554
|
function liveCanonicalAddress(o: LivePgObject): { kind: CanonicalKind; schema?: string; name: string; signature?: string } {
|
|
446
555
|
const kind = (
|
|
@@ -457,11 +566,15 @@ function liveCanonicalAddress(o: LivePgObject): { kind: CanonicalKind; schema?:
|
|
|
457
566
|
"Postgres::Function": "function",
|
|
458
567
|
"Postgres::Procedure": "procedure",
|
|
459
568
|
"Postgres::Trigger": "trigger",
|
|
569
|
+
"Postgres::Policy": "policy",
|
|
570
|
+
"Postgres::Role": "role",
|
|
571
|
+
"Postgres::Grant": "grant",
|
|
572
|
+
"Postgres::DefaultPrivileges": "defaultPrivileges",
|
|
460
573
|
} as const
|
|
461
574
|
)[o.type];
|
|
462
575
|
return {
|
|
463
576
|
kind,
|
|
464
|
-
...(o.schema !== undefined && kind !== "schema" && kind !== "extension" ? { schema: o.schema } : {}),
|
|
577
|
+
...(o.schema !== undefined && kind !== "schema" && kind !== "extension" && kind !== "role" ? { schema: o.schema } : {}),
|
|
465
578
|
name: o.name,
|
|
466
579
|
...(o.signature !== undefined ? { signature: o.signature } : {}),
|
|
467
580
|
};
|
|
@@ -29,10 +29,12 @@ import { POSTGRES_ENTITY_TYPES, type PostgresEntityType } from "../entity-types"
|
|
|
29
29
|
import { addConstraintRule, matchPgObjects } from "../plan/diff";
|
|
30
30
|
import type { PgChange } from "../plan/diff";
|
|
31
31
|
import { PG_CLASSIFIER_RULES, type PgChangeClass, type PgClassifierRuleId } from "../plan/rules";
|
|
32
|
-
import { canonicalOptions, sameConstraint, type CanonicalConstraint, type CanonicalKind } from "../plan/normalize";
|
|
32
|
+
import { ACCESS_KINDS, canonicalOptions, sameConstraint, type CanonicalConstraint, type CanonicalKind } from "../plan/normalize";
|
|
33
33
|
import type { PgDiffObject, PgSchemaObject } from "../plan/schema";
|
|
34
34
|
import { migrationTarget } from "../migrate/handoff";
|
|
35
|
-
import type { CheckDef, ColumnDef, DomainProps, ExclusionDef, ForeignKeyDef, KeyDef, SequenceProps, TableProps } from "../entities";
|
|
35
|
+
import type { CheckDef, ColumnDef, DomainProps, ExclusionDef, ForeignKeyDef, KeyDef, PolicyProps, SequenceProps, TableProps } from "../entities";
|
|
36
|
+
|
|
37
|
+
const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
|
|
36
38
|
|
|
37
39
|
/** One object as a build declares it, in the shape the applier works with. */
|
|
38
40
|
export interface DeclaredPgObject {
|
|
@@ -86,9 +88,9 @@ export const pgQualified = (schema: string | undefined, name: string): string =>
|
|
|
86
88
|
* trigger with its table (`touch ON app.users`).
|
|
87
89
|
*/
|
|
88
90
|
export const objectIdent = (o: { kind: CanonicalKind; schema?: string; name: string; signature?: string }): string =>
|
|
89
|
-
o.kind === "schema" || o.kind === "extension"
|
|
91
|
+
o.kind === "schema" || o.kind === "extension" || o.kind === "role"
|
|
90
92
|
? quoteIdent(o.name)
|
|
91
|
-
: o.kind === "trigger"
|
|
93
|
+
: o.kind === "trigger" || o.kind === "policy"
|
|
92
94
|
? `${quoteIdent(o.name)}${o.signature ?? ""}`
|
|
93
95
|
: `${pgQualified(o.schema, o.name)}${o.signature ?? ""}`;
|
|
94
96
|
|
|
@@ -106,6 +108,10 @@ export const KIND_WORDS: Readonly<Record<CanonicalKind, string>> = {
|
|
|
106
108
|
function: "FUNCTION",
|
|
107
109
|
procedure: "PROCEDURE",
|
|
108
110
|
trigger: "TRIGGER",
|
|
111
|
+
policy: "POLICY",
|
|
112
|
+
role: "ROLE",
|
|
113
|
+
grant: "GRANT",
|
|
114
|
+
defaultPrivileges: "DEFAULT PRIVILEGES",
|
|
109
115
|
};
|
|
110
116
|
|
|
111
117
|
/** The canonical kind of an entity type. */
|
|
@@ -172,12 +178,14 @@ export function commentStatement(obj: Pick<DeclaredPgObject, "canonical">, marke
|
|
|
172
178
|
export function createSteps(
|
|
173
179
|
obj: Pick<DeclaredPgObject, "ddl" | "canonical" | "props">,
|
|
174
180
|
marker: OwnershipMarker | undefined,
|
|
175
|
-
opts: { base?: string; orReplace?: boolean; cls?: PgChangeClass; rule?: PgClassifierRuleId } = {},
|
|
181
|
+
opts: { base?: string; orReplace?: boolean; cls?: PgChangeClass; rule?: PgClassifierRuleId; access?: boolean } = {},
|
|
176
182
|
): PgStep[] {
|
|
177
183
|
const cls = opts.cls ?? "create";
|
|
178
184
|
const out: PgStep[] = [];
|
|
179
185
|
splitStatements(obj.ddl).forEach((s, i) => {
|
|
180
186
|
if (i > 0 && isOwnComment(s, obj.canonical.kind)) return;
|
|
187
|
+
// A table's row-level security is access, which only a profile that manages access applies.
|
|
188
|
+
if (i > 0 && opts.access === false && firstNode(s)?.statement === "rowSecurity") return;
|
|
181
189
|
let sql = s.text;
|
|
182
190
|
const replaceable = obj.canonical.kind === "view" || obj.canonical.kind === "function" || obj.canonical.kind === "procedure" || (obj.canonical.kind === "trigger" && obj.canonical.fields.constraint !== true);
|
|
183
191
|
if (i === 0 && opts.orReplace && replaceable && !/^\s*CREATE\s+OR\s+REPLACE\b/i.test(sql)) sql = sql.replace(/\bCREATE\s+/i, "CREATE OR REPLACE ");
|
|
@@ -192,7 +200,7 @@ export function createSteps(
|
|
|
192
200
|
export function dropStatement(kind: CanonicalKind, schema: string | undefined, name: string, signature?: string): PgStep {
|
|
193
201
|
const what = objectIdent({ kind, ...(schema !== undefined ? { schema } : {}), name, ...(signature !== undefined ? { signature } : {}) });
|
|
194
202
|
if (kind === "index") return step(`DROP INDEX CONCURRENTLY ${what}`, "concurrently", "SQLPG242", false);
|
|
195
|
-
return step(`DROP ${KIND_WORDS[kind]} ${what}`, "drop", kind === "trigger" ? "SQLPG285" : "SQLPG270");
|
|
203
|
+
return step(`DROP ${KIND_WORDS[kind]} ${what}`, "drop", kind === "trigger" ? "SQLPG285" : kind === "policy" ? "SQLPG292" : "SQLPG270");
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
// ── Columns ────────────────────────────────────────────────────────────
|
|
@@ -553,6 +561,36 @@ export function alterSteps(
|
|
|
553
561
|
case "SQLPG280":
|
|
554
562
|
steps.push(...createSteps(obj, opts.marker, { orReplace: true, cls: c.class, rule: c.rule }).slice(0, 1));
|
|
555
563
|
break;
|
|
564
|
+
case "SQLPG291": {
|
|
565
|
+
const p = obj.props as unknown as PolicyProps;
|
|
566
|
+
const recreate =
|
|
567
|
+
["table", "restrictive", "command"].some((k) => !same(d.fields[k], live.fields[k])) ||
|
|
568
|
+
(live.fields.using !== undefined && d.fields.using === undefined) ||
|
|
569
|
+
(live.fields.check !== undefined && d.fields.check === undefined);
|
|
570
|
+
if (recreate) {
|
|
571
|
+
steps.push(step(`DROP POLICY ${objectIdent(live)}`, c.class, c.rule));
|
|
572
|
+
steps.push(...createSteps(obj, opts.marker, { cls: c.class, rule: c.rule }));
|
|
573
|
+
} else {
|
|
574
|
+
const roles = p.roles.map((r) => (r === "public" ? "PUBLIC" : quoteIdent(r))).join(", ");
|
|
575
|
+
steps.push(step([`ALTER POLICY ${objectIdent(d)} TO ${roles}`, p.using ? `USING (${p.using})` : "", p.check ? `WITH CHECK (${p.check})` : ""].filter(Boolean).join(" "), c.class, c.rule));
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case "SQLPG293":
|
|
580
|
+
if (!same(d.fields.rowSecurity, live.fields.rowSecurity)) steps.push(step(`${alterKind} ${d.fields.rowSecurity ? "ENABLE" : "DISABLE"} ROW LEVEL SECURITY`, c.class, c.rule));
|
|
581
|
+
if (!same(d.fields.forceRowSecurity, live.fields.forceRowSecurity)) steps.push(step(`${alterKind} ${d.fields.forceRowSecurity ? "FORCE" : "NO FORCE"} ROW LEVEL SECURITY`, c.class, c.rule));
|
|
582
|
+
break;
|
|
583
|
+
case "SQLPG294": {
|
|
584
|
+
if (steps.some((x) => x.rule === "SQLPG294")) break;
|
|
585
|
+
const want = new Set((d.fields.attributes as string[] | undefined) ?? []);
|
|
586
|
+
const have = new Set((live.fields.attributes as string[] | undefined) ?? []);
|
|
587
|
+
const flip = (a: string) => (a.startsWith("no") ? a.slice(2) : `no${a}`);
|
|
588
|
+
// An attribute at its default is in neither set: write the side that changes.
|
|
589
|
+
const words = [...[...want].filter((a) => !have.has(a)), ...[...have].filter((a) => !want.has(a)).map(flip)];
|
|
590
|
+
const limit = d.fields.connectionLimit !== live.fields.connectionLimit ? [`CONNECTION LIMIT ${String(d.fields.connectionLimit ?? -1)}`] : [];
|
|
591
|
+
steps.push(step(`ALTER ROLE ${objectIdent(d)} WITH ${[...words.map((w) => w.toUpperCase()), ...limit].join(" ")}`, c.class, c.rule));
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
556
594
|
case "SQLPG284":
|
|
557
595
|
if (d.fields.constraint === true || live.fields.constraint === true || d.signature !== live.signature) {
|
|
558
596
|
steps.push(step(`DROP TRIGGER ${objectIdent(live)}`, c.class, c.rule));
|
|
@@ -674,6 +712,8 @@ export interface PgStatementPlanInput {
|
|
|
674
712
|
carriesMarker?: (live: PgDiffObject, currentKey: string) => boolean;
|
|
675
713
|
/** The comment an extension's control file sets, kept under the trailer when the declaration sets none. */
|
|
676
714
|
extensionComment?: (name: string) => string | undefined;
|
|
715
|
+
/** Whether access is managed: off, a table is created without its row-level security statements. Default: on. */
|
|
716
|
+
access?: boolean;
|
|
677
717
|
}
|
|
678
718
|
|
|
679
719
|
export interface PgStatementPlan {
|
|
@@ -685,6 +725,9 @@ export interface PgStatementPlan {
|
|
|
685
725
|
|
|
686
726
|
/** Drop order: what reads from a table before the table, types after the tables using them, a schema last. */
|
|
687
727
|
const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
728
|
+
grant: 0,
|
|
729
|
+
defaultPrivileges: 0,
|
|
730
|
+
policy: 0,
|
|
688
731
|
trigger: 0,
|
|
689
732
|
materializedView: 1,
|
|
690
733
|
view: 2,
|
|
@@ -698,6 +741,8 @@ const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
|
698
741
|
enum: 8,
|
|
699
742
|
extension: 9,
|
|
700
743
|
schema: 10,
|
|
744
|
+
// A role is the cluster's: never dropped by a prune (`../live/catalog.ts` reads only the declared ones).
|
|
745
|
+
role: 11,
|
|
701
746
|
};
|
|
702
747
|
|
|
703
748
|
/**
|
|
@@ -718,7 +763,8 @@ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
|
|
|
718
763
|
}
|
|
719
764
|
|
|
720
765
|
const recreatedRelations = new Set<string>();
|
|
721
|
-
|
|
766
|
+
// Grants and default privileges are access, made by their own statements (`../access/acl.ts`).
|
|
767
|
+
const objects = input.declared.filter((obj) => !ACCESS_KINDS.has(obj.canonical.kind)).map((obj): PgObjectStatements => {
|
|
722
768
|
const key = keyOf(obj);
|
|
723
769
|
const currentKey = matched.get(key);
|
|
724
770
|
const live = currentKey !== undefined ? currentByKey.get(currentKey) : undefined;
|
|
@@ -746,7 +792,7 @@ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
|
|
|
746
792
|
const base = obj.canonical.kind === "extension" && !live ? input.extensionComment?.(obj.canonical.name) : undefined;
|
|
747
793
|
// An index on a table that exists is built under the scan timeout (SQLPG240, SQLPG241); one on a new table is part of the create.
|
|
748
794
|
const how = mine.find((c) => c.rule === "SQLPG200" || c.rule === "SQLPG240" || c.rule === "SQLPG241");
|
|
749
|
-
const steps = createSteps(obj, input.marker, { ...(base !== undefined ? { base } : {}), ...(how ? { cls: how.class, rule: how.rule } : {}) });
|
|
795
|
+
const steps = createSteps(obj, input.marker, { ...(base !== undefined ? { base } : {}), ...(how ? { cls: how.class, rule: how.rule } : {}), ...(input.access === false ? { access: false } : {}) });
|
|
750
796
|
return { verdict: "create", obj, changes: mine, steps, ...withLive };
|
|
751
797
|
}
|
|
752
798
|
|
|
@@ -770,7 +816,7 @@ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
|
|
|
770
816
|
|
|
771
817
|
const drops: PgDropStatement[] = [];
|
|
772
818
|
for (const c of input.changes) {
|
|
773
|
-
if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242" || c.rule === "SQLPG285") && c.after === undefined)) continue;
|
|
819
|
+
if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242" || c.rule === "SQLPG285" || c.rule === "SQLPG292") && c.after === undefined)) continue;
|
|
774
820
|
const o = currentByKey.get(c.object);
|
|
775
821
|
if (!o || o.foreign) continue;
|
|
776
822
|
const schema = o.kind === "schema" || o.kind === "extension" ? undefined : o.schema;
|