@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
|
@@ -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 {
|
|
@@ -138,9 +141,17 @@ export class PostgresApplyError extends SqlApplyError<PostgresApplyOutcome> {
|
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
/**
|
|
145
|
+
* The name an apply result gives an object: `schema.name`, or a schema's or
|
|
146
|
+
* extension's own name; a routine's with its parameter types
|
|
147
|
+
* (`app.touch()`), a trigger's with its table (`touch ON app.users`).
|
|
148
|
+
*/
|
|
149
|
+
export const refName = (o: { kind: CanonicalKind; schema?: string; name: string; signature?: string }): string =>
|
|
150
|
+
o.kind === "trigger" || o.kind === "policy"
|
|
151
|
+
? `${o.name}${o.signature ?? ""}`
|
|
152
|
+
: o.kind === "schema" || o.kind === "extension" || o.kind === "role" || o.schema === undefined
|
|
153
|
+
? o.name
|
|
154
|
+
: `${o.schema}.${o.name}${o.kind === "function" || o.kind === "procedure" ? (o.signature ?? "") : ""}`;
|
|
144
155
|
|
|
145
156
|
/**
|
|
146
157
|
* The objects a Postgres `chant build` output declares, in creation order,
|
|
@@ -251,7 +262,21 @@ export async function applyPostgres(
|
|
|
251
262
|
});
|
|
252
263
|
const normalizedByKey = new Map(plan.declared.map((o) => [o.key, o.canonical]));
|
|
253
264
|
const liveRawByKey = new Map(plan.liveObjects.map((o) => [qualifiedKey(liveCanonicalAddress(o)), o]));
|
|
254
|
-
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) } }));
|
|
255
280
|
const matchedLive = new Set<string>();
|
|
256
281
|
for (const m of matchPgObjects(plan.live, plan.declared)) if (m.kind === "matched") matchedLive.add(m.after.key);
|
|
257
282
|
|
|
@@ -267,6 +292,7 @@ export async function applyPostgres(
|
|
|
267
292
|
allowDestructive: opts.prune === true,
|
|
268
293
|
carriesMarker: (_live, key) => carriesMarker(liveRawByKey.get(key)?.comment, opts.marker),
|
|
269
294
|
extensionComment: (name) => extensionComments.get(name),
|
|
295
|
+
access: managed,
|
|
270
296
|
...(opts.marker ? { marker: opts.marker } : {}),
|
|
271
297
|
});
|
|
272
298
|
|
|
@@ -428,13 +454,104 @@ export async function applyPostgres(
|
|
|
428
454
|
}
|
|
429
455
|
await commit();
|
|
430
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
|
+
|
|
431
459
|
if (opts.prune) await prune(client, statements.drops, liveRawByKey, opts, timeouts, outcome, log);
|
|
432
460
|
if (outcome.failed.length > 0) throw new PostgresApplyError(outcome);
|
|
433
461
|
return outcome;
|
|
434
462
|
}
|
|
435
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
|
+
|
|
436
553
|
/** The address a live object is keyed by in the diff. */
|
|
437
|
-
function liveCanonicalAddress(o: LivePgObject): { kind: CanonicalKind; schema?: string; name: string } {
|
|
554
|
+
function liveCanonicalAddress(o: LivePgObject): { kind: CanonicalKind; schema?: string; name: string; signature?: string } {
|
|
438
555
|
const kind = (
|
|
439
556
|
{
|
|
440
557
|
"Postgres::Schema": "schema",
|
|
@@ -446,9 +563,21 @@ function liveCanonicalAddress(o: LivePgObject): { kind: CanonicalKind; schema?:
|
|
|
446
563
|
"Postgres::Enum": "enum",
|
|
447
564
|
"Postgres::Domain": "domain",
|
|
448
565
|
"Postgres::Extension": "extension",
|
|
566
|
+
"Postgres::Function": "function",
|
|
567
|
+
"Postgres::Procedure": "procedure",
|
|
568
|
+
"Postgres::Trigger": "trigger",
|
|
569
|
+
"Postgres::Policy": "policy",
|
|
570
|
+
"Postgres::Role": "role",
|
|
571
|
+
"Postgres::Grant": "grant",
|
|
572
|
+
"Postgres::DefaultPrivileges": "defaultPrivileges",
|
|
449
573
|
} as const
|
|
450
574
|
)[o.type];
|
|
451
|
-
return {
|
|
575
|
+
return {
|
|
576
|
+
kind,
|
|
577
|
+
...(o.schema !== undefined && kind !== "schema" && kind !== "extension" && kind !== "role" ? { schema: o.schema } : {}),
|
|
578
|
+
name: o.name,
|
|
579
|
+
...(o.signature !== undefined ? { signature: o.signature } : {}),
|
|
580
|
+
};
|
|
452
581
|
}
|
|
453
582
|
|
|
454
583
|
/** The comments the extensions' control files set, by extension name. */
|
|
@@ -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 {
|
|
@@ -80,9 +82,17 @@ export const pgString = (value: string): string => `'${value.replace(/'/g, "''")
|
|
|
80
82
|
/** `schema.name`, each part quoted where Postgres needs it. */
|
|
81
83
|
export const pgQualified = (schema: string | undefined, name: string): string => (schema ? `${quoteIdent(schema)}.${quoteIdent(name)}` : quoteIdent(name));
|
|
82
84
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* An object's name in a statement. A schema and an extension have no schema;
|
|
87
|
+
* a routine is named with its parameter types (`app.f(integer,text)`), a
|
|
88
|
+
* trigger with its table (`touch ON app.users`).
|
|
89
|
+
*/
|
|
90
|
+
export const objectIdent = (o: { kind: CanonicalKind; schema?: string; name: string; signature?: string }): string =>
|
|
91
|
+
o.kind === "schema" || o.kind === "extension" || o.kind === "role"
|
|
92
|
+
? quoteIdent(o.name)
|
|
93
|
+
: o.kind === "trigger" || o.kind === "policy"
|
|
94
|
+
? `${quoteIdent(o.name)}${o.signature ?? ""}`
|
|
95
|
+
: `${pgQualified(o.schema, o.name)}${o.signature ?? ""}`;
|
|
86
96
|
|
|
87
97
|
/** The words a statement names the object's kind with. */
|
|
88
98
|
export const KIND_WORDS: Readonly<Record<CanonicalKind, string>> = {
|
|
@@ -95,6 +105,13 @@ export const KIND_WORDS: Readonly<Record<CanonicalKind, string>> = {
|
|
|
95
105
|
enum: "TYPE",
|
|
96
106
|
domain: "DOMAIN",
|
|
97
107
|
extension: "EXTENSION",
|
|
108
|
+
function: "FUNCTION",
|
|
109
|
+
procedure: "PROCEDURE",
|
|
110
|
+
trigger: "TRIGGER",
|
|
111
|
+
policy: "POLICY",
|
|
112
|
+
role: "ROLE",
|
|
113
|
+
grant: "GRANT",
|
|
114
|
+
defaultPrivileges: "DEFAULT PRIVILEGES",
|
|
98
115
|
};
|
|
99
116
|
|
|
100
117
|
/** The canonical kind of an entity type. */
|
|
@@ -161,14 +178,17 @@ export function commentStatement(obj: Pick<DeclaredPgObject, "canonical">, marke
|
|
|
161
178
|
export function createSteps(
|
|
162
179
|
obj: Pick<DeclaredPgObject, "ddl" | "canonical" | "props">,
|
|
163
180
|
marker: OwnershipMarker | undefined,
|
|
164
|
-
opts: { base?: string; orReplace?: boolean; cls?: PgChangeClass; rule?: PgClassifierRuleId } = {},
|
|
181
|
+
opts: { base?: string; orReplace?: boolean; cls?: PgChangeClass; rule?: PgClassifierRuleId; access?: boolean } = {},
|
|
165
182
|
): PgStep[] {
|
|
166
183
|
const cls = opts.cls ?? "create";
|
|
167
184
|
const out: PgStep[] = [];
|
|
168
185
|
splitStatements(obj.ddl).forEach((s, i) => {
|
|
169
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;
|
|
170
189
|
let sql = s.text;
|
|
171
|
-
|
|
190
|
+
const replaceable = obj.canonical.kind === "view" || obj.canonical.kind === "function" || obj.canonical.kind === "procedure" || (obj.canonical.kind === "trigger" && obj.canonical.fields.constraint !== true);
|
|
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 ");
|
|
172
192
|
const concurrently = i === 0 && obj.canonical.kind === "index" && obj.props.concurrently === true;
|
|
173
193
|
out.push({ ...step(sql, cls, opts.rule, !concurrently), ...(concurrently ? { buildsIndex: objectIdent(obj.canonical) } : {}) });
|
|
174
194
|
});
|
|
@@ -177,10 +197,10 @@ export function createSteps(
|
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
/** The `DROP` for an object a prune removes. Never `CASCADE`: what something else still uses is kept. */
|
|
180
|
-
export function dropStatement(kind: CanonicalKind, schema: string | undefined, name: string): PgStep {
|
|
181
|
-
const what = objectIdent({ kind, ...(schema !== undefined ? { schema } : {}), name });
|
|
200
|
+
export function dropStatement(kind: CanonicalKind, schema: string | undefined, name: string, signature?: string): PgStep {
|
|
201
|
+
const what = objectIdent({ kind, ...(schema !== undefined ? { schema } : {}), name, ...(signature !== undefined ? { signature } : {}) });
|
|
182
202
|
if (kind === "index") return step(`DROP INDEX CONCURRENTLY ${what}`, "concurrently", "SQLPG242", false);
|
|
183
|
-
return step(`DROP ${KIND_WORDS[kind]} ${what}`, "drop", "SQLPG270");
|
|
203
|
+
return step(`DROP ${KIND_WORDS[kind]} ${what}`, "drop", kind === "trigger" ? "SQLPG285" : kind === "policy" ? "SQLPG292" : "SQLPG270");
|
|
184
204
|
}
|
|
185
205
|
|
|
186
206
|
// ── Columns ────────────────────────────────────────────────────────────
|
|
@@ -403,7 +423,7 @@ export const isRefused = (c: PgChange): boolean => c.class === "expand";
|
|
|
403
423
|
export const isDestructive = (c: PgChange): boolean => c.rule === "SQLPG204";
|
|
404
424
|
|
|
405
425
|
/** Changes that replace the object: drop it and create it again from its declaration. */
|
|
406
|
-
export const isRecreate = (c: PgChange): boolean => c.rule === "SQLPG252" || c.rule === "SQLPG243";
|
|
426
|
+
export const isRecreate = (c: PgChange): boolean => c.rule === "SQLPG252" || c.rule === "SQLPG243" || c.rule === "SQLPG281";
|
|
407
427
|
|
|
408
428
|
/**
|
|
409
429
|
* The statements for an object that exists on the server, change by change.
|
|
@@ -538,8 +558,45 @@ export function alterSteps(
|
|
|
538
558
|
steps.push(step(`ALTER INDEX ${objectIdent(live)} RENAME TO ${quoteIdent(d.name)}`, c.class, c.rule));
|
|
539
559
|
break;
|
|
540
560
|
case "SQLPG250":
|
|
561
|
+
case "SQLPG280":
|
|
541
562
|
steps.push(...createSteps(obj, opts.marker, { orReplace: true, cls: c.class, rule: c.rule }).slice(0, 1));
|
|
542
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
|
+
}
|
|
594
|
+
case "SQLPG284":
|
|
595
|
+
if (d.fields.constraint === true || live.fields.constraint === true || d.signature !== live.signature) {
|
|
596
|
+
steps.push(step(`DROP TRIGGER ${objectIdent(live)}`, c.class, c.rule));
|
|
597
|
+
steps.push(...createSteps(obj, opts.marker, { cls: c.class, rule: c.rule }));
|
|
598
|
+
} else steps.push(...createSteps(obj, opts.marker, { orReplace: true, cls: c.class, rule: c.rule }).slice(0, 1));
|
|
599
|
+
break;
|
|
543
600
|
case "SQLPG260": {
|
|
544
601
|
const before = (live.fields.labels as string[] | undefined) ?? [];
|
|
545
602
|
const after = (d.fields.labels as string[] | undefined) ?? [];
|
|
@@ -631,6 +688,8 @@ export interface PgDropStatement {
|
|
|
631
688
|
kind: CanonicalKind;
|
|
632
689
|
schema?: string;
|
|
633
690
|
name: string;
|
|
691
|
+
/** A routine's parameter types, a trigger's table. */
|
|
692
|
+
signature?: string;
|
|
634
693
|
step: PgStep;
|
|
635
694
|
}
|
|
636
695
|
|
|
@@ -653,6 +712,8 @@ export interface PgStatementPlanInput {
|
|
|
653
712
|
carriesMarker?: (live: PgDiffObject, currentKey: string) => boolean;
|
|
654
713
|
/** The comment an extension's control file sets, kept under the trailer when the declaration sets none. */
|
|
655
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;
|
|
656
717
|
}
|
|
657
718
|
|
|
658
719
|
export interface PgStatementPlan {
|
|
@@ -663,7 +724,26 @@ export interface PgStatementPlan {
|
|
|
663
724
|
}
|
|
664
725
|
|
|
665
726
|
/** Drop order: what reads from a table before the table, types after the tables using them, a schema last. */
|
|
666
|
-
const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
727
|
+
const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
728
|
+
grant: 0,
|
|
729
|
+
defaultPrivileges: 0,
|
|
730
|
+
policy: 0,
|
|
731
|
+
trigger: 0,
|
|
732
|
+
materializedView: 1,
|
|
733
|
+
view: 2,
|
|
734
|
+
index: 3,
|
|
735
|
+
table: 4,
|
|
736
|
+
sequence: 5,
|
|
737
|
+
// A routine after what calls it (a view, a trigger, a default), before the types its parameters use.
|
|
738
|
+
function: 6,
|
|
739
|
+
procedure: 6,
|
|
740
|
+
domain: 7,
|
|
741
|
+
enum: 8,
|
|
742
|
+
extension: 9,
|
|
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,
|
|
746
|
+
};
|
|
667
747
|
|
|
668
748
|
/**
|
|
669
749
|
* The statements that take the current schema to the declared one, from the
|
|
@@ -683,7 +763,8 @@ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
|
|
|
683
763
|
}
|
|
684
764
|
|
|
685
765
|
const recreatedRelations = new Set<string>();
|
|
686
|
-
|
|
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 => {
|
|
687
768
|
const key = keyOf(obj);
|
|
688
769
|
const currentKey = matched.get(key);
|
|
689
770
|
const live = currentKey !== undefined ? currentByKey.get(currentKey) : undefined;
|
|
@@ -711,7 +792,7 @@ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
|
|
|
711
792
|
const base = obj.canonical.kind === "extension" && !live ? input.extensionComment?.(obj.canonical.name) : undefined;
|
|
712
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.
|
|
713
794
|
const how = mine.find((c) => c.rule === "SQLPG200" || c.rule === "SQLPG240" || c.rule === "SQLPG241");
|
|
714
|
-
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 } : {}) });
|
|
715
796
|
return { verdict: "create", obj, changes: mine, steps, ...withLive };
|
|
716
797
|
}
|
|
717
798
|
|
|
@@ -735,11 +816,18 @@ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
|
|
|
735
816
|
|
|
736
817
|
const drops: PgDropStatement[] = [];
|
|
737
818
|
for (const c of input.changes) {
|
|
738
|
-
if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242") && c.after === undefined)) continue;
|
|
819
|
+
if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242" || c.rule === "SQLPG285" || c.rule === "SQLPG292") && c.after === undefined)) continue;
|
|
739
820
|
const o = currentByKey.get(c.object);
|
|
740
821
|
if (!o || o.foreign) continue;
|
|
741
822
|
const schema = o.kind === "schema" || o.kind === "extension" ? undefined : o.schema;
|
|
742
|
-
drops.push({
|
|
823
|
+
drops.push({
|
|
824
|
+
key: c.object,
|
|
825
|
+
kind: o.kind,
|
|
826
|
+
...(schema !== undefined ? { schema } : {}),
|
|
827
|
+
name: o.name,
|
|
828
|
+
...(o.signature !== undefined ? { signature: o.signature } : {}),
|
|
829
|
+
step: dropStatement(o.kind, schema, o.name, o.signature),
|
|
830
|
+
});
|
|
743
831
|
}
|
|
744
832
|
drops.sort((a, b) => DROP_ORDER[a.kind] - DROP_ORDER[b.kind]);
|
|
745
833
|
return { objects, drops };
|