@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
package/src/postgres/entities.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Postgres entities: `schema`, `table`, `index`, `view`, `sequence`,
|
|
3
|
-
* `type`, `domain`, `extension`, `func`, `procedure
|
|
4
|
-
* templates that parse their DDL at fold time into
|
|
5
|
-
* (chant #3278, #3279, #3680).
|
|
3
|
+
* `type`, `domain`, `extension`, `func`, `procedure`, `trigger`, `policy`,
|
|
4
|
+
* `role` and `grant` tagged templates that parse their DDL at fold time into
|
|
5
|
+
* one Declarable each (chant #3278, #3279, #3680, #3681).
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
8
|
* import { schema, table, index } from "@intentius/chant-lexicon-sql/postgres";
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
import type { AttrRef } from "@intentius/chant/attrref";
|
|
57
57
|
import { setInterpolationFields } from "@intentius/chant/provenance";
|
|
58
58
|
import { isTrivia, POSTGRES_LEXICAL, SqlSyntaxError, untokenize, type Token } from "./tokens";
|
|
59
|
-
import { identValue, parseStatements, type ColumnNode, type CommentNode, type ConstraintNode, type NameNode, type Span, type StatementNode } from "./parser";
|
|
59
|
+
import { identValue, parseStatements, type ColumnNode, type CommentNode, type ConstraintNode, type GrantNode, type NameNode, type RowSecurityNode, type Span, type StatementNode } from "./parser";
|
|
60
60
|
import { keywordCategory, quoteIdent } from "./keywords";
|
|
61
61
|
import { feed, lineage, spanText as text, splice as spliceWith, templateSyntaxError, type TemplateCtx, type TemplateDialect } from "../core/interpolation";
|
|
62
62
|
import { SqlObject, isColumnRefOf, isSqlObjectOf, makeSqlEntity } from "../core/entity";
|
|
@@ -183,6 +183,10 @@ export interface TableProps extends CommonProps {
|
|
|
183
183
|
with?: string;
|
|
184
184
|
onCommit?: string;
|
|
185
185
|
tablespace?: string;
|
|
186
|
+
/** `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` after the CREATE (true), or `DISABLE` (false). */
|
|
187
|
+
rowSecurity?: boolean;
|
|
188
|
+
/** `ALTER TABLE ... FORCE ROW LEVEL SECURITY` after the CREATE (true), or `NO FORCE` (false): the owner is subject to the policies too. */
|
|
189
|
+
forceRowSecurity?: boolean;
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
export interface IndexProps extends CommonProps {
|
|
@@ -338,6 +342,70 @@ export interface TriggerProps extends CommonProps {
|
|
|
338
342
|
args: string[];
|
|
339
343
|
}
|
|
340
344
|
|
|
345
|
+
export interface PolicyProps extends CommonProps {
|
|
346
|
+
/** The table: the entity when interpolated, else its name. */
|
|
347
|
+
table: PostgresObject | string;
|
|
348
|
+
tableName: string;
|
|
349
|
+
/** `AS PERMISSIVE` (the default) or `AS RESTRICTIVE`. */
|
|
350
|
+
permissive: boolean;
|
|
351
|
+
command: "all" | "select" | "insert" | "update" | "delete";
|
|
352
|
+
/** The roles it applies to; `public` when it names none. */
|
|
353
|
+
roles: string[];
|
|
354
|
+
/** `USING (...)`, parentheses excluded. */
|
|
355
|
+
using?: string;
|
|
356
|
+
/** `WITH CHECK (...)`, parentheses excluded. */
|
|
357
|
+
check?: string;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface RoleProps extends Omit<CommonProps, "schema"> {
|
|
361
|
+
/** The options written, each by its key word, lower case (`login`, `noinherit`). */
|
|
362
|
+
options: string[];
|
|
363
|
+
/** `CONNECTION LIMIT n`. */
|
|
364
|
+
connectionLimit?: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** A privilege as written: `select`, `update` on some columns, `all`. */
|
|
368
|
+
export interface PrivilegeDef {
|
|
369
|
+
privilege: string;
|
|
370
|
+
columns?: string[];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface GrantProps {
|
|
374
|
+
/** A grant's or revoke's name, which no catalog holds: what it grants on what to whom. */
|
|
375
|
+
name: string;
|
|
376
|
+
action: "grant" | "revoke";
|
|
377
|
+
/** `REVOKE GRANT OPTION FOR`: only the right to grant on is revoked. */
|
|
378
|
+
grantOptionFor?: boolean;
|
|
379
|
+
privileges: PrivilegeDef[];
|
|
380
|
+
/** The kind of object. */
|
|
381
|
+
on: "table" | "sequence" | "schema" | "function" | "procedure" | "routine";
|
|
382
|
+
/** The objects: each the entity when interpolated, else its name. */
|
|
383
|
+
objects: Array<PostgresObject | string>;
|
|
384
|
+
/** The objects' SQL names; a routine's with its parameter types when they are written or interpolated. */
|
|
385
|
+
objectNames: string[];
|
|
386
|
+
/** Role names; `public` for PUBLIC. */
|
|
387
|
+
grantees: string[];
|
|
388
|
+
withGrantOption?: boolean;
|
|
389
|
+
ddl: string;
|
|
390
|
+
source: { strings: string[] };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface DefaultPrivilegesProps {
|
|
394
|
+
name: string;
|
|
395
|
+
action: "grant" | "revoke";
|
|
396
|
+
grantOptionFor?: boolean;
|
|
397
|
+
/** `FOR ROLE`: the roles whose new objects it applies to; the role that applies it when not written. */
|
|
398
|
+
forRoles?: string[];
|
|
399
|
+
/** `IN SCHEMA`: the schemas; every schema when not written. */
|
|
400
|
+
inSchemas?: string[];
|
|
401
|
+
privileges: string[];
|
|
402
|
+
on: "tables" | "sequences" | "functions" | "types" | "schemas";
|
|
403
|
+
grantees: string[];
|
|
404
|
+
withGrantOption?: boolean;
|
|
405
|
+
ddl: string;
|
|
406
|
+
source: { strings: string[] };
|
|
407
|
+
}
|
|
408
|
+
|
|
341
409
|
/**
|
|
342
410
|
* A string as a quoted Postgres string literal: `'` doubled, a backslash left
|
|
343
411
|
* as written (standard_conforming_strings is on). An interpolated plain string
|
|
@@ -419,6 +487,21 @@ export interface PostgresTrigger extends PostgresObject {
|
|
|
419
487
|
readonly props: TriggerProps;
|
|
420
488
|
}
|
|
421
489
|
|
|
490
|
+
export interface PostgresPolicy extends PostgresObject {
|
|
491
|
+
readonly entityType: "Postgres::Policy";
|
|
492
|
+
readonly props: PolicyProps;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export interface PostgresRole extends PostgresObject {
|
|
496
|
+
readonly entityType: "Postgres::Role";
|
|
497
|
+
readonly props: RoleProps;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export interface PostgresGrant extends PostgresObject {
|
|
501
|
+
readonly entityType: "Postgres::Grant" | "Postgres::DefaultPrivileges";
|
|
502
|
+
readonly props: GrantProps | DefaultPrivilegesProps;
|
|
503
|
+
}
|
|
504
|
+
|
|
422
505
|
export function isPostgresObject(value: unknown): value is PostgresObject {
|
|
423
506
|
return isSqlObjectOf(value, "Postgres::");
|
|
424
507
|
}
|
|
@@ -521,7 +604,7 @@ export const POSTGRES_TEMPLATES: TemplateDialect = {
|
|
|
521
604
|
|
|
522
605
|
type Ctx = TemplateCtx;
|
|
523
606
|
|
|
524
|
-
export type PostgresTag = "schema" | "table" | "index" | "view" | "sequence" | "type" | "domain" | "extension" | "func" | "procedure" | "trigger";
|
|
607
|
+
export type PostgresTag = "schema" | "table" | "index" | "view" | "sequence" | "type" | "domain" | "extension" | "func" | "procedure" | "trigger" | "policy" | "role" | "grant";
|
|
525
608
|
|
|
526
609
|
const STATEMENT_OF: Record<PostgresTag, StatementNode["statement"]> = {
|
|
527
610
|
schema: "schema",
|
|
@@ -535,6 +618,9 @@ const STATEMENT_OF: Record<PostgresTag, StatementNode["statement"]> = {
|
|
|
535
618
|
func: "function",
|
|
536
619
|
procedure: "procedure",
|
|
537
620
|
trigger: "trigger",
|
|
621
|
+
policy: "policy",
|
|
622
|
+
role: "role",
|
|
623
|
+
grant: "grant",
|
|
538
624
|
};
|
|
539
625
|
|
|
540
626
|
const TAG_OF: Record<StatementNode["statement"], PostgresTag | undefined> = {
|
|
@@ -549,6 +635,10 @@ const TAG_OF: Record<StatementNode["statement"], PostgresTag | undefined> = {
|
|
|
549
635
|
function: "func",
|
|
550
636
|
procedure: "procedure",
|
|
551
637
|
trigger: "trigger",
|
|
638
|
+
policy: "policy",
|
|
639
|
+
role: "role",
|
|
640
|
+
grant: "grant",
|
|
641
|
+
rowSecurity: undefined,
|
|
552
642
|
comment: undefined,
|
|
553
643
|
};
|
|
554
644
|
|
|
@@ -565,6 +655,10 @@ export const STATEMENT_NAMES: Record<StatementNode["statement"], string> = {
|
|
|
565
655
|
function: "CREATE FUNCTION",
|
|
566
656
|
procedure: "CREATE PROCEDURE",
|
|
567
657
|
trigger: "CREATE TRIGGER",
|
|
658
|
+
policy: "CREATE POLICY",
|
|
659
|
+
role: "CREATE ROLE",
|
|
660
|
+
grant: "GRANT",
|
|
661
|
+
rowSecurity: "ALTER TABLE",
|
|
568
662
|
comment: "COMMENT ON",
|
|
569
663
|
};
|
|
570
664
|
|
|
@@ -782,9 +876,9 @@ function applyComments(
|
|
|
782
876
|
if (value !== undefined) constraint!.comment = value;
|
|
783
877
|
} else {
|
|
784
878
|
if (!objectTypes.includes(c.objectType)) refuse(c, `does not comment on the ${tag} this template declares`);
|
|
785
|
-
if (c.objectType === "TRIGGER") {
|
|
879
|
+
if (c.objectType === "TRIGGER" || c.objectType === "POLICY") {
|
|
786
880
|
// `COMMENT ON TRIGGER name ON table`: the name is the trigger's, the table its own.
|
|
787
|
-
if (qualified(ctx, c.target).name !== self.name || !c.on || !isSelf({ ...qualified(ctx, c.on), name: self.name })) refuse(c, `names another
|
|
881
|
+
if (qualified(ctx, c.target).name !== self.name || !c.on || !isSelf({ ...qualified(ctx, c.on), name: self.name })) refuse(c, `names another ${c.objectType.toLowerCase()} than ${self.name}`);
|
|
788
882
|
} else if (!isSelf(qualified(ctx, c.target))) refuse(c, `names another object than ${sqlNameOf(self)}`);
|
|
789
883
|
comment = value;
|
|
790
884
|
}
|
|
@@ -801,6 +895,17 @@ function lastDot(ctx: Ctx, span: Span): number {
|
|
|
801
895
|
return span.from;
|
|
802
896
|
}
|
|
803
897
|
|
|
898
|
+
/**
|
|
899
|
+
* The first statement after the CREATE that its template may not hold: a
|
|
900
|
+
* template is one object, then COMMENT ON statements for it; a table's may
|
|
901
|
+
* also turn row-level security on or off (`ALTER TABLE ... ENABLE ROW LEVEL
|
|
902
|
+
* SECURITY`), as `pg_dump` prints it. A grant is one statement.
|
|
903
|
+
*/
|
|
904
|
+
export function followOnStray(tag: PostgresTag, rest: readonly StatementNode[]): StatementNode | undefined {
|
|
905
|
+
if (tag === "grant") return rest[0];
|
|
906
|
+
return rest.find((n) => n.statement !== "comment" && !(tag === "table" && n.statement === "rowSecurity"));
|
|
907
|
+
}
|
|
908
|
+
|
|
804
909
|
/**
|
|
805
910
|
* The interpolations that sit inside a string constant (a routine's
|
|
806
911
|
* `AS $$ ... $$` body), rendered into the text around them: a string
|
|
@@ -897,6 +1002,75 @@ function inlineQuoted(parts: readonly string[], values: readonly unknown[]): { p
|
|
|
897
1002
|
return { parts: out, values: outValues, original, inlined, refs };
|
|
898
1003
|
}
|
|
899
1004
|
|
|
1005
|
+
/** A role named where one is granted to: an interpolated role entity gives its name. */
|
|
1006
|
+
function roleName(ctx: Ctx, r: { name: string; span: Span }): string {
|
|
1007
|
+
if (r.name) return r.name;
|
|
1008
|
+
const t = ctx.tokens[r.span.from];
|
|
1009
|
+
const v = t?.kind === "ref" ? ctx.values[t.part] : undefined;
|
|
1010
|
+
if (isPostgresObject(v)) return (v.props as { name: string }).name;
|
|
1011
|
+
return identValue(req(text(ctx, r.span)));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** A GRANT's, REVOKE's or ALTER DEFAULT PRIVILEGES's props. */
|
|
1015
|
+
function grantProps(ctx: Ctx, node: GrantNode, ddl: string, source: { strings: string[] }): GrantProps | DefaultPrivilegesProps {
|
|
1016
|
+
const action = node.revoke ? "revoke" : "grant";
|
|
1017
|
+
const grantees = node.grantees.map((g) => roleName(ctx, g));
|
|
1018
|
+
if (node.defaults) {
|
|
1019
|
+
if (node.privileges.some((p) => p.columns.length > 0)) throw new SqlTemplateError("grant", "default privileges are on whole objects; a column list is for a grant on one table", 0, 0);
|
|
1020
|
+
const on = (node.on === "ROUTINES" ? "functions" : node.on.toLowerCase()) as DefaultPrivilegesProps["on"];
|
|
1021
|
+
const forRoles = node.forRoles?.map((r) => roleName(ctx, r));
|
|
1022
|
+
const inSchemas = node.inSchemas?.map((s) => columnName(ctx, s));
|
|
1023
|
+
if (on === "schemas" && inSchemas) throw new SqlTemplateError("grant", "default privileges on schemas cannot be limited to a schema (IN SCHEMA)", 0, 0);
|
|
1024
|
+
const privileges = node.privileges.map((p) => p.privilege.toLowerCase());
|
|
1025
|
+
return stripUnset({
|
|
1026
|
+
name: `default privileges ${action === "grant" ? "grant" : "revoke"} ${privileges.join(", ")} on ${on}${inSchemas ? ` in ${inSchemas.join(", ")}` : ""}${forRoles ? ` for ${forRoles.join(", ")}` : ""} ${action === "grant" ? "to" : "from"} ${grantees.join(", ")}`,
|
|
1027
|
+
action,
|
|
1028
|
+
grantOptionFor: node.grantOptionFor || undefined,
|
|
1029
|
+
forRoles,
|
|
1030
|
+
inSchemas,
|
|
1031
|
+
privileges,
|
|
1032
|
+
on,
|
|
1033
|
+
grantees,
|
|
1034
|
+
withGrantOption: node.withGrantOption || undefined,
|
|
1035
|
+
ddl,
|
|
1036
|
+
source,
|
|
1037
|
+
}) as DefaultPrivilegesProps;
|
|
1038
|
+
}
|
|
1039
|
+
const on = (node.on === "TABLE" ? "table" : node.on.toLowerCase()) as GrantProps["on"];
|
|
1040
|
+
const routine = on === "function" || on === "procedure" || on === "routine";
|
|
1041
|
+
const objects: Array<PostgresObject | string> = [];
|
|
1042
|
+
const objectNames: string[] = [];
|
|
1043
|
+
for (const o of node.objects) {
|
|
1044
|
+
const t = target(ctx, o.name);
|
|
1045
|
+
objects.push(t);
|
|
1046
|
+
let sqlName = typeof t === "string" ? t : on === "schema" ? (t.props as { name: string }).name : t.sqlName;
|
|
1047
|
+
if (routine) {
|
|
1048
|
+
if (o.args) sqlName += `(${req(text(ctx, o.args, "objects")) ?? ""})`;
|
|
1049
|
+
else if (typeof t !== "string" && (t.entityType === POSTGRES_ENTITY_TYPES.function || t.entityType === POSTGRES_ENTITY_TYPES.procedure)) {
|
|
1050
|
+
// An interpolated routine is named by its own parameter types.
|
|
1051
|
+
sqlName += `(${(t.props as RoutineProps).args.filter((a) => a.mode !== "out").map((a) => a.type).join(", ")})`;
|
|
1052
|
+
} else throw new SqlTemplateError("grant", `${sqlName}: name the parameter types (${sqlName}(...)), or interpolate the routine, since overloads share a name`, 0, 0);
|
|
1053
|
+
}
|
|
1054
|
+
objectNames.push(sqlName);
|
|
1055
|
+
}
|
|
1056
|
+
const privileges = node.privileges.map((p) => stripUnset({ privilege: p.privilege.toLowerCase(), columns: p.columns.length > 0 ? p.columns.map((c) => columnName(ctx, c)) : undefined }) as PrivilegeDef);
|
|
1057
|
+
if (on !== "table" && privileges.some((p) => p.columns)) throw new SqlTemplateError("grant", "a column list is for privileges on a table", 0, 0);
|
|
1058
|
+
const verb = action === "grant" ? "to" : "from";
|
|
1059
|
+
return stripUnset({
|
|
1060
|
+
name: `${action} ${privileges.map((p) => `${p.privilege}${p.columns ? `(${p.columns.join(", ")})` : ""}`).join(", ")} on ${on} ${objectNames.join(", ")} ${verb} ${grantees.join(", ")}`,
|
|
1061
|
+
action,
|
|
1062
|
+
grantOptionFor: node.grantOptionFor || undefined,
|
|
1063
|
+
privileges,
|
|
1064
|
+
on,
|
|
1065
|
+
objects,
|
|
1066
|
+
objectNames,
|
|
1067
|
+
grantees,
|
|
1068
|
+
withGrantOption: node.withGrantOption || undefined,
|
|
1069
|
+
ddl,
|
|
1070
|
+
source,
|
|
1071
|
+
}) as GrantProps;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
900
1074
|
function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string[], rawValues: readonly unknown[]): PostgresObject {
|
|
901
1075
|
const rawParts = templateParts(strings);
|
|
902
1076
|
const routine = tag === "func" || tag === "procedure";
|
|
@@ -915,11 +1089,28 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
915
1089
|
if (node.statement !== STATEMENT_OF[tag]) {
|
|
916
1090
|
const holds = STATEMENT_NAMES[node.statement];
|
|
917
1091
|
const use = TAG_OF[node.statement];
|
|
918
|
-
throw new SqlTemplateError(
|
|
1092
|
+
throw new SqlTemplateError(
|
|
1093
|
+
tag,
|
|
1094
|
+
use
|
|
1095
|
+
? `holds a ${holds}; use the ${use} tag`
|
|
1096
|
+
: node.statement === "rowSecurity"
|
|
1097
|
+
? "holds an ALTER TABLE ... ROW LEVEL SECURITY before any CREATE; it goes after the table's CREATE TABLE, in the table's template"
|
|
1098
|
+
: `holds a ${holds} before any CREATE; a COMMENT ON goes after the CREATE it comments on`,
|
|
1099
|
+
0,
|
|
1100
|
+
0,
|
|
1101
|
+
);
|
|
919
1102
|
}
|
|
920
|
-
const stray =
|
|
921
|
-
if (stray)
|
|
922
|
-
|
|
1103
|
+
const stray = followOnStray(tag, rest);
|
|
1104
|
+
if (stray) {
|
|
1105
|
+
throw new SqlTemplateError(
|
|
1106
|
+
tag,
|
|
1107
|
+
`holds a second statement (${STATEMENT_NAMES[stray.statement]}); a template declares one object, followed only by COMMENT ON statements for it${tag === "table" ? " and ALTER TABLE ... ROW LEVEL SECURITY" : ""}`,
|
|
1108
|
+
0,
|
|
1109
|
+
0,
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
const comments = rest.filter((n): n is CommentNode => n.statement === "comment");
|
|
1113
|
+
const rowSecurity = rest.filter((n): n is RowSecurityNode => n.statement === "rowSecurity");
|
|
923
1114
|
|
|
924
1115
|
const positions = regclassPositions(tokens, values);
|
|
925
1116
|
const render = (v: unknown, i?: number): string => {
|
|
@@ -1078,6 +1269,16 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
1078
1269
|
const named = new Map<string, Commentable>();
|
|
1079
1270
|
for (const c of [primaryKey, ...uniques, ...checks, ...foreignKeys, ...exclusions]) if (c?.name) named.set(c.name, c);
|
|
1080
1271
|
const comment = applyComments(tag, ctx, comments, q, ["TABLE"], { columns: new Map(columns.map((c) => [c.name, c])), constraints: named });
|
|
1272
|
+
let rls: boolean | undefined;
|
|
1273
|
+
let force: boolean | undefined;
|
|
1274
|
+
for (const r of rowSecurity) {
|
|
1275
|
+
const t = qualified(ctx, r.table);
|
|
1276
|
+
if (t.name !== q.name || (t.schema !== undefined && q.schema !== undefined && t.schema !== q.schema)) {
|
|
1277
|
+
throw new SqlTemplateError(tag, `ALTER TABLE ${sqlNameOf(t)} ... ROW LEVEL SECURITY names another table than ${sqlNameOf(q)}`, 0, 0);
|
|
1278
|
+
}
|
|
1279
|
+
if (r.action === "ENABLE" || r.action === "DISABLE") rls = r.action === "ENABLE";
|
|
1280
|
+
else force = r.action === "FORCE";
|
|
1281
|
+
}
|
|
1081
1282
|
const props: TableProps = strip(
|
|
1082
1283
|
{
|
|
1083
1284
|
...q,
|
|
@@ -1099,12 +1300,16 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
1099
1300
|
with: text(ctx, node.with, "with"),
|
|
1100
1301
|
onCommit: text(ctx, node.onCommit),
|
|
1101
1302
|
tablespace: text(ctx, node.tablespace, "tablespace"),
|
|
1303
|
+
rowSecurity: rls,
|
|
1304
|
+
forceRowSecurity: force,
|
|
1102
1305
|
comment,
|
|
1103
1306
|
ddl,
|
|
1104
1307
|
source,
|
|
1105
1308
|
},
|
|
1106
1309
|
["columns", "uniques", "checks", "foreignKeys", "exclusions", "like", "inherits"],
|
|
1107
1310
|
);
|
|
1311
|
+
if (rls === false) props.rowSecurity = false;
|
|
1312
|
+
if (force === false) props.forceRowSecurity = false;
|
|
1108
1313
|
return make(POSTGRES_ENTITY_TYPES.table, q, props, columns.map((c) => c.name));
|
|
1109
1314
|
}
|
|
1110
1315
|
case "index": {
|
|
@@ -1274,6 +1479,52 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
1274
1479
|
);
|
|
1275
1480
|
return make(POSTGRES_ENTITY_TYPES.trigger, q, props);
|
|
1276
1481
|
}
|
|
1482
|
+
case "policy": {
|
|
1483
|
+
const tq = qualified(ctx, node.table);
|
|
1484
|
+
const name = node.name.pieces[0] || identValue(req(text(ctx, node.name.span)));
|
|
1485
|
+
feed(ctx, node.name.span, "name");
|
|
1486
|
+
// A policy is its table's, named in the table's schema.
|
|
1487
|
+
const q = { schema: tq.schema, name };
|
|
1488
|
+
const comment = applyComments(tag, ctx, comments, q, ["POLICY"]);
|
|
1489
|
+
const props: PolicyProps = stripUnset({
|
|
1490
|
+
...q,
|
|
1491
|
+
table: target(ctx, node.table),
|
|
1492
|
+
tableName: sqlNameOf(tq),
|
|
1493
|
+
permissive: node.permissive !== false,
|
|
1494
|
+
command: (node.command ?? "ALL").toLowerCase() as PolicyProps["command"],
|
|
1495
|
+
roles: node.roles.length > 0 ? node.roles.map((r) => roleName(ctx, r)) : ["public"],
|
|
1496
|
+
using: text(ctx, node.using, "using"),
|
|
1497
|
+
check: text(ctx, node.check, "check"),
|
|
1498
|
+
comment,
|
|
1499
|
+
ddl,
|
|
1500
|
+
source,
|
|
1501
|
+
});
|
|
1502
|
+
// AS RESTRICTIVE is a false that means something.
|
|
1503
|
+
props.permissive = node.permissive !== false;
|
|
1504
|
+
return make(POSTGRES_ENTITY_TYPES.policy, q, props);
|
|
1505
|
+
}
|
|
1506
|
+
case "role": {
|
|
1507
|
+
const name = node.name.pieces[0] || identValue(req(text(ctx, node.name.span)));
|
|
1508
|
+
feed(ctx, node.name.span, "name");
|
|
1509
|
+
const comment = applyComments(tag, ctx, comments, { name }, ["ROLE"]);
|
|
1510
|
+
const value = (o: string) => {
|
|
1511
|
+
const x = node.options.find((y) => y.option === o);
|
|
1512
|
+
return x?.value ? text(ctx, x.value, "options") : undefined;
|
|
1513
|
+
};
|
|
1514
|
+
const props: RoleProps = stripUnset({
|
|
1515
|
+
name,
|
|
1516
|
+
options: node.options.filter((o) => o.value === undefined).map((o) => o.option.toLowerCase()),
|
|
1517
|
+
connectionLimit: value("CONNECTION LIMIT"),
|
|
1518
|
+
comment,
|
|
1519
|
+
ddl,
|
|
1520
|
+
source,
|
|
1521
|
+
});
|
|
1522
|
+
return make(POSTGRES_ENTITY_TYPES.role, { name }, props);
|
|
1523
|
+
}
|
|
1524
|
+
case "grant":
|
|
1525
|
+
return make(node.defaults ? POSTGRES_ENTITY_TYPES.defaultPrivileges : POSTGRES_ENTITY_TYPES.grant, { name: "" }, grantProps(ctx, node, ddl, source));
|
|
1526
|
+
case "rowSecurity":
|
|
1527
|
+
throw new SqlTemplateError(tag, "an ALTER TABLE ... ROW LEVEL SECURITY goes after the table's CREATE TABLE, in the table's template", 0, 0);
|
|
1277
1528
|
case "comment":
|
|
1278
1529
|
throw new SqlTemplateError(tag, "a COMMENT ON goes after the CREATE it comments on", 0, 0);
|
|
1279
1530
|
}
|
|
@@ -1337,3 +1588,28 @@ export function procedure(strings: TemplateStringsArray, ...values: unknown[]):
|
|
|
1337
1588
|
export function trigger(strings: TemplateStringsArray, ...values: unknown[]): PostgresTrigger {
|
|
1338
1589
|
return build("trigger", strings, values) as PostgresTrigger;
|
|
1339
1590
|
}
|
|
1591
|
+
|
|
1592
|
+
/** `` policy`CREATE POLICY name ON ${table} ... USING (...)` ``: one row-level security policy on its table. */
|
|
1593
|
+
export function policy(strings: TemplateStringsArray, ...values: unknown[]): PostgresPolicy {
|
|
1594
|
+
return build("policy", strings, values) as PostgresPolicy;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* `` role`CREATE ROLE name [WITH] options` ``: a role the schema needs, created
|
|
1599
|
+
* when it is missing. A password and memberships are the environment's and are
|
|
1600
|
+
* refused; a role the environment provisions is named as text where it is
|
|
1601
|
+
* granted to, never declared.
|
|
1602
|
+
*/
|
|
1603
|
+
export function role(strings: TemplateStringsArray, ...values: unknown[]): PostgresRole {
|
|
1604
|
+
return build("role", strings, values) as PostgresRole;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* `` grant`GRANT ... ON ... TO ...` ``, `REVOKE ... FROM ...`, or
|
|
1609
|
+
* `ALTER DEFAULT PRIVILEGES ... GRANT|REVOKE ...`: privileges on schemas,
|
|
1610
|
+
* tables, columns, sequences and routines, applied where the profile manages
|
|
1611
|
+
* access (`sql.profiles.<env>.access`).
|
|
1612
|
+
*/
|
|
1613
|
+
export function grant(strings: TemplateStringsArray, ...values: unknown[]): PostgresGrant {
|
|
1614
|
+
return build("grant", strings, values) as PostgresGrant;
|
|
1615
|
+
}
|
|
@@ -16,6 +16,10 @@ export const POSTGRES_ENTITY_TYPES = {
|
|
|
16
16
|
function: "Postgres::Function",
|
|
17
17
|
procedure: "Postgres::Procedure",
|
|
18
18
|
trigger: "Postgres::Trigger",
|
|
19
|
+
policy: "Postgres::Policy",
|
|
20
|
+
role: "Postgres::Role",
|
|
21
|
+
grant: "Postgres::Grant",
|
|
22
|
+
defaultPrivileges: "Postgres::DefaultPrivileges",
|
|
19
23
|
} as const;
|
|
20
24
|
|
|
21
25
|
export type PostgresEntityType = (typeof POSTGRES_ENTITY_TYPES)[keyof typeof POSTGRES_ENTITY_TYPES];
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* (`nextval('app.invoice_seq'::regclass)` is `nextval(${invoiceSeq})`);
|
|
12
12
|
* - the schema part of the object's own name, wherever the name appears,
|
|
13
13
|
* becomes the schema's export (`CREATE TABLE ${appSchema}.users`);
|
|
14
|
-
* - an extension's `WITH SCHEMA
|
|
14
|
+
* - an extension's `WITH SCHEMA`, a grant's `ON SCHEMA` and default
|
|
15
|
+
* privileges' `IN SCHEMA` name the schema's export;
|
|
16
|
+
* - a grant's object becomes the object's export (`GRANT SELECT ON TABLE
|
|
17
|
+
* ${orders} TO reader`), a policy's table its table's.
|
|
15
18
|
*
|
|
16
19
|
* Columns stay text, as they do for ClickHouse (#3236): which relation a bare
|
|
17
20
|
* column belongs to is the server's to resolve. One file, because the
|
|
@@ -45,6 +48,10 @@ const TAG: Record<string, string> = {
|
|
|
45
48
|
[POSTGRES_ENTITY_TYPES.function]: "func",
|
|
46
49
|
[POSTGRES_ENTITY_TYPES.procedure]: "procedure",
|
|
47
50
|
[POSTGRES_ENTITY_TYPES.trigger]: "trigger",
|
|
51
|
+
[POSTGRES_ENTITY_TYPES.policy]: "policy",
|
|
52
|
+
[POSTGRES_ENTITY_TYPES.role]: "role",
|
|
53
|
+
[POSTGRES_ENTITY_TYPES.grant]: "grant",
|
|
54
|
+
[POSTGRES_ENTITY_TYPES.defaultPrivileges]: "grant",
|
|
48
55
|
};
|
|
49
56
|
|
|
50
57
|
/** Text that sits inside a template literal: a backquote and `${` escaped. */
|
|
@@ -85,8 +92,9 @@ function templateBody(item: Item, byQualified: Map<string, Item>, schemaExport:
|
|
|
85
92
|
const dot = tokens[sig[k + 1] ?? -1];
|
|
86
93
|
const b = tokens[sig[k + 2] ?? -1];
|
|
87
94
|
if (!isName(a) || dot?.kind !== "punct" || dot.text !== "." || !isName(b)) {
|
|
88
|
-
// An extension's `WITH SCHEMA app`.
|
|
89
|
-
|
|
95
|
+
// An extension's `WITH SCHEMA app`, a grant's `ON SCHEMA app`, default privileges' `IN SCHEMA app`.
|
|
96
|
+
const bySchemaWord = item.type === POSTGRES_ENTITY_TYPES.extension || item.type === POSTGRES_ENTITY_TYPES.grant || item.type === POSTGRES_ENTITY_TYPES.defaultPrivileges;
|
|
97
|
+
if (bySchemaWord && isName(a) && prev?.kind === "ident" && prev.text.toUpperCase() === "SCHEMA") {
|
|
90
98
|
const s = schemaExport.get(identValue(a));
|
|
91
99
|
if (s) replace.set(sig[k]!, { to: sig[k]! + 1, text: `\${${s}}`, ref: s });
|
|
92
100
|
}
|
|
@@ -148,7 +156,9 @@ export class PostgresGenerator implements TypeScriptGenerator {
|
|
|
148
156
|
for (const it of items) {
|
|
149
157
|
if (it.type === POSTGRES_ENTITY_TYPES.schema) schemaExport.set(it.name, it.exportName);
|
|
150
158
|
// An index or a trigger is never referenced by name in another object's DDL.
|
|
151
|
-
else if (it.schema && it.type !== POSTGRES_ENTITY_TYPES.index && it.type !== POSTGRES_ENTITY_TYPES.trigger
|
|
159
|
+
else if (it.schema && it.type !== POSTGRES_ENTITY_TYPES.index && it.type !== POSTGRES_ENTITY_TYPES.trigger && it.type !== POSTGRES_ENTITY_TYPES.policy) {
|
|
160
|
+
byQualified.set(`${it.schema}.${it.name}`, it);
|
|
161
|
+
}
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
const bodies = new Map(items.map((it) => [it.exportName, templateBody(it, byQualified, schemaExport)]));
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* a declaration of them would have chant fight it.
|
|
12
12
|
* - `verbatim` changes nothing yet: the printers already leave out what the
|
|
13
13
|
* server would add by itself.
|
|
14
|
+
* - Where the profile manages access (`sql.profiles.<env>.access`), the
|
|
15
|
+
* policies and each table's row-level security come with the objects, and
|
|
16
|
+
* the privileges on them and the default privileges are written as `grant`
|
|
17
|
+
* declarations: the GRANT and REVOKE statements that take a new object's
|
|
18
|
+
* privileges to what the server holds. Roles are the environment's and are
|
|
19
|
+
* not imported.
|
|
14
20
|
*/
|
|
15
21
|
|
|
16
22
|
import type { ExportedTemplate, ResourceSelector } from "@intentius/chant/lexicon";
|
|
@@ -18,6 +24,7 @@ import { bindPostgres, type BindOptions } from "../live/bind";
|
|
|
18
24
|
import { markProviderOwned, readLiveSchema } from "../live/catalog";
|
|
19
25
|
import { objectsToIR, type ImportedPgObject } from "./ir";
|
|
20
26
|
import { isChantManaged } from "../../core/ownership";
|
|
27
|
+
import { importedAccess } from "../access/import";
|
|
21
28
|
|
|
22
29
|
export interface ExportOptions extends Omit<BindOptions, "environment"> {
|
|
23
30
|
environment: string;
|
|
@@ -31,8 +38,11 @@ export interface ExportOptions extends Omit<BindOptions, "environment"> {
|
|
|
31
38
|
export async function exportResources(options: ExportOptions): Promise<ExportedTemplate> {
|
|
32
39
|
const { target, client } = await bindPostgres(options);
|
|
33
40
|
let live;
|
|
41
|
+
let access: ImportedPgObject[] = [];
|
|
34
42
|
try {
|
|
35
|
-
live = markProviderOwned(await readLiveSchema(client, { schemas: target.schemas }), target.provider);
|
|
43
|
+
live = markProviderOwned(await readLiveSchema(client, { schemas: target.schemas, access: target.access === true }), target.provider);
|
|
44
|
+
// Where the profile manages access, the privileges on what is imported, and the default privileges, as grant declarations.
|
|
45
|
+
if (target.access === true && !options.selector) access = await importedAccess(client, live.filter((o) => !o.foreign && !o.unsupported && (!options.owned || isChantManaged(o.comment))), target.schemas);
|
|
36
46
|
} finally {
|
|
37
47
|
await client.end();
|
|
38
48
|
}
|
|
@@ -53,5 +63,5 @@ export async function exportResources(options: ExportOptions): Promise<ExportedT
|
|
|
53
63
|
}
|
|
54
64
|
objects.push({ type: o.type, ...(o.schema ? { schema: o.schema } : {}), name: o.name, ddl: o.statement });
|
|
55
65
|
}
|
|
56
|
-
return objectsToIR(objects, warnings) as ExportedTemplate;
|
|
66
|
+
return objectsToIR([...objects, ...access], warnings) as ExportedTemplate;
|
|
57
67
|
}
|
|
@@ -21,6 +21,8 @@ export interface PostgresTarget {
|
|
|
21
21
|
provider?: PostgresProvider;
|
|
22
22
|
/** The apply's timeouts the profile sets (`lockTimeoutMs`, `statementTimeoutMs`, `scanTimeoutMs`); the applier's defaults otherwise. */
|
|
23
23
|
timeouts?: { lockTimeoutMs?: number; statementTimeoutMs?: number; scanTimeoutMs?: number };
|
|
24
|
+
/** Whether chant manages access here (`sql.profiles.<env>.access`): policies, row-level security, the declared roles, grants and default privileges. */
|
|
25
|
+
access?: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface UnresolvedTarget {
|
|
@@ -72,6 +74,7 @@ export function resolvePostgresTarget(input: {
|
|
|
72
74
|
...(profile.schemas ? { schemas: profile.schemas } : {}),
|
|
73
75
|
defaultSchema: profile.defaultSchema ?? "public",
|
|
74
76
|
...(Object.keys(timeouts).length > 0 ? { timeouts } : {}),
|
|
77
|
+
...(profile.access === true ? { access: true } : {}),
|
|
75
78
|
});
|
|
76
79
|
}
|
|
77
80
|
const url = env.POSTGRES_URL;
|
|
@@ -93,6 +93,15 @@ export interface LivePgObject {
|
|
|
93
93
|
export interface SchemaScope {
|
|
94
94
|
/** The schemas in scope; undefined is every schema but the server's own and `public`'s own objects included. */
|
|
95
95
|
schemas?: readonly string[];
|
|
96
|
+
/**
|
|
97
|
+
* Whether access is read (`sql.profiles.<env>.access`): the policies on the
|
|
98
|
+
* tables in scope, each table's row-level security, and the roles named in
|
|
99
|
+
* `roles`. Off, none of it is read, so a project that manages access
|
|
100
|
+
* elsewhere sees none of it.
|
|
101
|
+
*/
|
|
102
|
+
access?: boolean;
|
|
103
|
+
/** The roles to read, when access is: the declared ones. A role is the cluster's, never read otherwise. */
|
|
104
|
+
roles?: readonly string[];
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
type Row = Record<string, unknown>;
|
|
@@ -193,8 +202,8 @@ const seqParams = (r: Row): SequenceParams => ({
|
|
|
193
202
|
/**
|
|
194
203
|
* Every object in scope, in a stable order: schemas, extensions, enums,
|
|
195
204
|
* domains, sequences, tables, views and materialized views, indexes,
|
|
196
|
-
* functions and procedures, triggers
|
|
197
|
-
* order.
|
|
205
|
+
* functions and procedures, triggers, and where access is read, policies and
|
|
206
|
+
* the declared roles; each kind by schema then name, in byte order.
|
|
198
207
|
*/
|
|
199
208
|
export async function readLiveSchema(client: PostgresClient, scope: SchemaScope = {}): Promise<LivePgObject[]> {
|
|
200
209
|
const out: LivePgObject[] = [];
|
|
@@ -326,6 +335,7 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
326
335
|
FROM pg_catalog.pg_inherits i JOIN pg_catalog.pg_class pc ON pc.oid = i.inhparent JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace
|
|
327
336
|
WHERE i.inhrelid = c.oid ORDER BY i.inhseqno) AS parents,
|
|
328
337
|
c.reloptions AS reloptions, (SELECT a.amname FROM pg_catalog.pg_am a WHERE a.oid = c.relam) AS am,
|
|
338
|
+
c.relrowsecurity AS rowsecurity, c.relforcerowsecurity AS forcerowsecurity,
|
|
329
339
|
(SELECT ts.spcname FROM pg_catalog.pg_tablespace ts WHERE ts.oid = c.reltablespace) AS tablespace,
|
|
330
340
|
pg_catalog.obj_description(c.oid, 'pg_class') AS comment
|
|
331
341
|
FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
@@ -492,7 +502,13 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
492
502
|
oid: rel,
|
|
493
503
|
...(str(t.comment) ? { comment: str(t.comment) } : {}),
|
|
494
504
|
...(FOREIGN_TABLES[String(t.name)] ? { foreign: FOREIGN_TABLES[String(t.name)] } : {}),
|
|
495
|
-
statement: [
|
|
505
|
+
statement: [
|
|
506
|
+
create,
|
|
507
|
+
...(scope.access && t.rowsecurity === true ? [`ALTER TABLE ${q} ENABLE ROW LEVEL SECURITY`] : []),
|
|
508
|
+
...(scope.access && t.forcerowsecurity === true ? [`ALTER TABLE ${q} FORCE ROW LEVEL SECURITY`] : []),
|
|
509
|
+
...commentOn(`TABLE ${q}`, t.comment),
|
|
510
|
+
...commentLines,
|
|
511
|
+
].join(";\n"),
|
|
496
512
|
});
|
|
497
513
|
}
|
|
498
514
|
|
|
@@ -630,6 +646,66 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
630
646
|
}
|
|
631
647
|
}
|
|
632
648
|
|
|
649
|
+
if (scope.access) {
|
|
650
|
+
// Policies, on the tables in scope.
|
|
651
|
+
const policies = await client.query<Row>(
|
|
652
|
+
`SELECT p.oid::text AS oid, n.nspname AS schema, c.relname AS table_name, p.polname AS name, p.polpermissive AS permissive, p.polcmd AS cmd,
|
|
653
|
+
ARRAY(SELECT CASE WHEN r = 0 THEN 'PUBLIC' ELSE pg_catalog.quote_ident(pg_catalog.pg_get_userbyid(r)) END FROM pg_catalog.unnest(p.polroles) r ORDER BY 1) AS roles,
|
|
654
|
+
pg_catalog.pg_get_expr(p.polqual, p.polrelid, true) AS qual, pg_catalog.pg_get_expr(p.polwithcheck, p.polrelid, true) AS withcheck,
|
|
655
|
+
pg_catalog.obj_description(p.oid, 'pg_policy') AS comment
|
|
656
|
+
FROM pg_catalog.pg_policy p JOIN pg_catalog.pg_class c ON c.oid = p.polrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
657
|
+
WHERE ${s.sql} AND ${notExtensionOwned("pg_class", "c.oid")}
|
|
658
|
+
ORDER BY n.nspname ${C}, c.relname ${C}, p.polname ${C}`,
|
|
659
|
+
s.params,
|
|
660
|
+
);
|
|
661
|
+
const COMMAND: Record<string, string> = { r: "SELECT", a: "INSERT", w: "UPDATE", d: "DELETE" };
|
|
662
|
+
for (const r of policies) {
|
|
663
|
+
const table = qname(String(r.schema), String(r.table_name));
|
|
664
|
+
const roles = (r.roles as string[] | null) ?? [];
|
|
665
|
+
const parts = [`CREATE POLICY ${quoteIdent(String(r.name))} ON ${table}`];
|
|
666
|
+
if (r.permissive === false) parts.push("AS RESTRICTIVE");
|
|
667
|
+
if (COMMAND[String(r.cmd)]) parts.push(`FOR ${COMMAND[String(r.cmd)]}`);
|
|
668
|
+
if (!(roles.length === 1 && roles[0] === "PUBLIC")) parts.push(`TO ${roles.join(", ")}`);
|
|
669
|
+
if (str(r.qual)) parts.push(`USING (${r.qual})`);
|
|
670
|
+
if (str(r.withcheck)) parts.push(`WITH CHECK (${r.withcheck})`);
|
|
671
|
+
out.push({
|
|
672
|
+
type: POSTGRES_ENTITY_TYPES.policy,
|
|
673
|
+
schema: String(r.schema),
|
|
674
|
+
name: String(r.name),
|
|
675
|
+
oid: String(r.oid),
|
|
676
|
+
signature: ` ON ${table}`,
|
|
677
|
+
...(str(r.comment) ? { comment: str(r.comment) } : {}),
|
|
678
|
+
statement: [parts.join(" "), ...commentOn(`POLICY ${quoteIdent(String(r.name))} ON ${table}`, r.comment)].join(";\n"),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// The declared roles, by name: a role is the cluster's, and the ones the declarations do not name are the environment's.
|
|
683
|
+
if (scope.roles && scope.roles.length > 0) {
|
|
684
|
+
const roles = await client.query<Row>(
|
|
685
|
+
`SELECT r.oid::text AS oid, r.rolname AS name, r.rolsuper AS superuser, r.rolinherit AS inherit, r.rolcreaterole AS createrole, r.rolcreatedb AS createdb,
|
|
686
|
+
r.rolcanlogin AS login, r.rolreplication AS replication, r.rolbypassrls AS bypassrls, r.rolconnlimit AS connlimit,
|
|
687
|
+
pg_catalog.shobj_description(r.oid, 'pg_authid') AS comment
|
|
688
|
+
FROM pg_catalog.pg_roles r WHERE r.rolname = ANY($1::text[]) ORDER BY r.rolname ${C}`,
|
|
689
|
+
[[...scope.roles]],
|
|
690
|
+
);
|
|
691
|
+
for (const r of roles) {
|
|
692
|
+
const words: string[] = [];
|
|
693
|
+
for (const [k, dflt] of [["superuser", false], ["createdb", false], ["createrole", false], ["inherit", true], ["login", false], ["replication", false], ["bypassrls", false]] as const) {
|
|
694
|
+
if ((r[k] === true) !== dflt) words.push(r[k] === true ? k.toUpperCase() : `NO${k.toUpperCase()}`);
|
|
695
|
+
}
|
|
696
|
+
if (Number(r.connlimit) !== -1) words.push(`CONNECTION LIMIT ${Number(r.connlimit)}`);
|
|
697
|
+
const name = quoteIdent(String(r.name));
|
|
698
|
+
out.push({
|
|
699
|
+
type: POSTGRES_ENTITY_TYPES.role,
|
|
700
|
+
name: String(r.name),
|
|
701
|
+
oid: String(r.oid),
|
|
702
|
+
...(str(r.comment) ? { comment: str(r.comment) } : {}),
|
|
703
|
+
statement: [`CREATE ROLE ${name}${words.length > 0 ? ` WITH ${words.join(" ")}` : ""}`, ...commentOn(`ROLE ${name}`, r.comment)].join(";\n"),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
633
709
|
return out;
|
|
634
710
|
}
|
|
635
711
|
|