@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.
Files changed (116) hide show
  1. package/dist/codegen/registry.d.ts.map +1 -1
  2. package/dist/config.d.ts +20 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/integrity.json +5 -5
  7. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  8. package/dist/lsp/postgres-template.d.ts.map +1 -1
  9. package/dist/manifest.json +1 -1
  10. package/dist/meta.json +35 -0
  11. package/dist/okf/index.md +7 -0
  12. package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
  13. package/dist/okf/types/PostgresFunction.md +9 -0
  14. package/dist/okf/types/PostgresGrant.md +9 -0
  15. package/dist/okf/types/PostgresPolicy.md +9 -0
  16. package/dist/okf/types/PostgresProcedure.md +9 -0
  17. package/dist/okf/types/PostgresRole.md +9 -0
  18. package/dist/okf/types/PostgresTrigger.md +9 -0
  19. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  20. package/dist/op/activity-contracts.d.ts +10 -0
  21. package/dist/op/activity-contracts.d.ts.map +1 -1
  22. package/dist/plugin.d.ts.map +1 -1
  23. package/dist/postgres/access/acl.d.ts +119 -0
  24. package/dist/postgres/access/acl.d.ts.map +1 -0
  25. package/dist/postgres/access/import.d.ts +13 -0
  26. package/dist/postgres/access/import.d.ts.map +1 -0
  27. package/dist/postgres/access/live.d.ts +24 -0
  28. package/dist/postgres/access/live.d.ts.map +1 -0
  29. package/dist/postgres/apply/apply.d.ts +6 -1
  30. package/dist/postgres/apply/apply.d.ts.map +1 -1
  31. package/dist/postgres/apply/statements.d.ts +12 -2
  32. package/dist/postgres/apply/statements.d.ts.map +1 -1
  33. package/dist/postgres/entities.d.ts +212 -3
  34. package/dist/postgres/entities.d.ts.map +1 -1
  35. package/dist/postgres/entity-types.d.ts +7 -0
  36. package/dist/postgres/entity-types.d.ts.map +1 -1
  37. package/dist/postgres/import/generator.d.ts +4 -1
  38. package/dist/postgres/import/generator.d.ts.map +1 -1
  39. package/dist/postgres/import/live-export.d.ts +6 -0
  40. package/dist/postgres/import/live-export.d.ts.map +1 -1
  41. package/dist/postgres/live/bind.d.ts +2 -0
  42. package/dist/postgres/live/bind.d.ts.map +1 -1
  43. package/dist/postgres/live/catalog.d.ts +25 -4
  44. package/dist/postgres/live/catalog.d.ts.map +1 -1
  45. package/dist/postgres/live/describe-resources.d.ts +15 -1
  46. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  47. package/dist/postgres/migrate/op.d.ts +29 -0
  48. package/dist/postgres/migrate/op.d.ts.map +1 -1
  49. package/dist/postgres/parser.d.ts +155 -5
  50. package/dist/postgres/parser.d.ts.map +1 -1
  51. package/dist/postgres/plan/commands.d.ts +12 -0
  52. package/dist/postgres/plan/commands.d.ts.map +1 -1
  53. package/dist/postgres/plan/deep.d.ts.map +1 -1
  54. package/dist/postgres/plan/diff.d.ts +11 -2
  55. package/dist/postgres/plan/diff.d.ts.map +1 -1
  56. package/dist/postgres/plan/normalize.d.ts +28 -2
  57. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  58. package/dist/postgres/plan/rules.d.ts +14 -0
  59. package/dist/postgres/plan/rules.d.ts.map +1 -1
  60. package/dist/postgres/plan/schema.d.ts +12 -1
  61. package/dist/postgres/plan/schema.d.ts.map +1 -1
  62. package/dist/postgres/plan/server-normalize.d.ts +4 -1
  63. package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
  64. package/dist/postgres.d.ts +1 -1
  65. package/dist/postgres.d.ts.map +1 -1
  66. package/dist/rules/postgres-templates.ts +2 -2
  67. package/dist/rules/sqlpg001.ts +10 -4
  68. package/dist/validate.d.ts.map +1 -1
  69. package/package.json +2 -2
  70. package/src/codegen/postgres-classifier-page.ts +1 -1
  71. package/src/codegen/registry.ts +7 -0
  72. package/src/config.ts +10 -0
  73. package/src/generated/lexicon-sql.json +35 -0
  74. package/src/index.ts +1 -1
  75. package/src/lint/rules/postgres-templates.ts +2 -2
  76. package/src/lint/rules/sqlpg001.ts +10 -4
  77. package/src/lsp/postgres-template.ts +8 -0
  78. package/src/mcp/postgres.ts +1 -1
  79. package/src/migration-statements.ts +7 -0
  80. package/src/op/activities/postgres-migration.ts +27 -2
  81. package/src/op/activity-contracts.ts +1 -0
  82. package/src/plugin.ts +6 -0
  83. package/src/postgres/access/access.e2e.test.ts +191 -0
  84. package/src/postgres/access/access.test.ts +147 -0
  85. package/src/postgres/access/acl.ts +400 -0
  86. package/src/postgres/access/import.ts +64 -0
  87. package/src/postgres/access/live.ts +123 -0
  88. package/src/postgres/apply/apply.ts +135 -6
  89. package/src/postgres/apply/statements.ts +104 -16
  90. package/src/postgres/entities.ts +613 -14
  91. package/src/postgres/entity-types.ts +7 -0
  92. package/src/postgres/import/generator.ts +18 -5
  93. package/src/postgres/import/live-export.ts +16 -2
  94. package/src/postgres/live/bind.ts +3 -0
  95. package/src/postgres/live/catalog.ts +155 -5
  96. package/src/postgres/live/describe-resources.ts +43 -9
  97. package/src/postgres/live/routines.e2e.test.ts +173 -0
  98. package/src/postgres/migrate/batches.e2e.test.ts +2 -1
  99. package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
  100. package/src/postgres/migrate/migrate.test.ts +24 -1
  101. package/src/postgres/migrate/op.ts +80 -35
  102. package/src/postgres/migrate/outer.e2e.test.ts +163 -0
  103. package/src/postgres/parser.ts +554 -9
  104. package/src/postgres/plan/commands.ts +61 -8
  105. package/src/postgres/plan/deep.ts +79 -18
  106. package/src/postgres/plan/diff.ts +114 -9
  107. package/src/postgres/plan/normalize.ts +272 -5
  108. package/src/postgres/plan/rules.ts +80 -0
  109. package/src/postgres/plan/schema.ts +27 -6
  110. package/src/postgres/plan/server-normalize.ts +53 -1
  111. package/src/postgres/routines.test.ts +150 -0
  112. package/src/postgres.ts +22 -0
  113. package/src/skill-defs.ts +1 -1
  114. package/src/skills/chant-sql-postgres-migration.md +3 -1
  115. package/src/skills/chant-sql-postgres.md +25 -2
  116. 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
- /** The name an apply result gives an object: `schema.name`, or a schema's or extension's own name. */
142
- export const refName = (o: { kind: CanonicalKind; schema?: string; name: string }): string =>
143
- o.kind === "schema" || o.kind === "extension" || o.schema === undefined ? o.name : `${o.schema}.${o.name}`;
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 objects = declared.map((original) => ({ ...original, canonical: { ...original.canonical, ...normalizedByKey.get(original.key) } }));
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 { kind, ...(o.schema !== undefined && kind !== "schema" && kind !== "extension" ? { schema: o.schema } : {}), name: o.name };
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
- /** An object's name in a statement. A schema and an extension have no schema. */
84
- export const objectIdent = (o: { kind: CanonicalKind; schema?: string; name: string }): string =>
85
- o.kind === "schema" || o.kind === "extension" ? quoteIdent(o.name) : pgQualified(o.schema, o.name);
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
- if (i === 0 && opts.orReplace && obj.canonical.kind === "view" && !/\bOR\s+REPLACE\b/i.test(sql)) sql = sql.replace(/\bCREATE\s+/i, "CREATE OR REPLACE ");
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> = { materializedView: 0, view: 1, index: 2, table: 3, sequence: 4, domain: 5, enum: 6, extension: 7, schema: 8 };
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
- const objects = input.declared.map((obj): PgObjectStatements => {
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({ key: c.object, kind: o.kind, ...(schema !== undefined ? { schema } : {}), name: o.name, step: dropStatement(o.kind, schema, o.name) });
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 };