supaschema 0.1.0-rc.1
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/.agents/skills/supaschema/SKILL.md +61 -0
- package/.claude/hooks/block-generated-migration-edits.mjs +32 -0
- package/.claude/rules/supaschema.md +22 -0
- package/.claude/settings.json +16 -0
- package/.claude/skills/supaschema/SKILL.md +61 -0
- package/.codex/hooks/supaschema-tool-gate.mjs +73 -0
- package/.codex/hooks.json +16 -0
- package/.codex/rules/supaschema.rules +22 -0
- package/AGENTS.md +40 -0
- package/LICENSE +661 -0
- package/LICENSE-COMMERCIAL.md +35 -0
- package/README.md +249 -0
- package/benchmarks/README.md +104 -0
- package/benchmarks/compare.js +489 -0
- package/benchmarks/fixtures/additive/from.sql +8 -0
- package/benchmarks/fixtures/additive/manifest.json +1 -0
- package/benchmarks/fixtures/additive/to.sql +9 -0
- package/benchmarks/fixtures/functions-policies/from.sql +24 -0
- package/benchmarks/fixtures/functions-policies/manifest.json +5 -0
- package/benchmarks/fixtures/functions-policies/to.sql +24 -0
- package/benchmarks/plot-lib.js +234 -0
- package/benchmarks/plot-svg.js +339 -0
- package/benchmarks/plot.js +154 -0
- package/benchmarks/tools/bench-all.sh +49 -0
- package/benchmarks/tools/build-project-fixture.mjs +245 -0
- package/benchmarks/tools/compare-db.mjs +101 -0
- package/benchmarks/tools/compare-fixtures.mjs +84 -0
- package/benchmarks/tools/compare-report.mjs +90 -0
- package/benchmarks/tools/compare-supabase.mjs +67 -0
- package/benchmarks/tools/registry.js +266 -0
- package/benchmarks/tools/run-workflow.mjs +77 -0
- package/bin/postinstall.mjs +26 -0
- package/bin/supaschema +2 -0
- package/config-schema.json +208 -0
- package/corpus/supabase-style/corpus.json +6 -0
- package/corpus/supabase-style/migrations/20260101000000_init.sql +28 -0
- package/corpus/supabase-style/migrations/20260102000000_noise.sql +13 -0
- package/corpus/supabase-style/migrations/20260103000000_churn.sql +4 -0
- package/corpus/supabase-style/migrations/20260104000000_triggers.sql +17 -0
- package/corpus/supabase-style/roles.sql +13 -0
- package/corpus/supabase-style/tree/functions.sql +26 -0
- package/corpus/supabase-style/tree/policies.sql +4 -0
- package/corpus/supabase-style/tree/schema.sql +4 -0
- package/corpus/supabase-style/tree/tables.sql +20 -0
- package/corpus/supabase-style/tree/triggers.sql +3 -0
- package/corpus/supabase-style/tree/types.sql +2 -0
- package/corpus/supabase-style/tree/views.sql +6 -0
- package/dist/audit.d.ts +20 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +68 -0
- package/dist/benchmark-db.d.ts +5 -0
- package/dist/benchmark-db.d.ts.map +1 -0
- package/dist/benchmark-db.js +71 -0
- package/dist/benchmark-fixtures.d.ts +10 -0
- package/dist/benchmark-fixtures.d.ts.map +1 -0
- package/dist/benchmark-fixtures.js +201 -0
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +308 -0
- package/dist/catalog-comments.d.ts +9 -0
- package/dist/catalog-comments.d.ts.map +1 -0
- package/dist/catalog-comments.js +194 -0
- package/dist/catalog-extras.d.ts +12 -0
- package/dist/catalog-extras.d.ts.map +1 -0
- package/dist/catalog-extras.js +408 -0
- package/dist/catalog-foreign.d.ts +15 -0
- package/dist/catalog-foreign.d.ts.map +1 -0
- package/dist/catalog-foreign.js +114 -0
- package/dist/catalog-tables.d.ts +9 -0
- package/dist/catalog-tables.d.ts.map +1 -0
- package/dist/catalog-tables.js +114 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +351 -0
- package/dist/check-hazards.d.ts +7 -0
- package/dist/check-hazards.d.ts.map +1 -0
- package/dist/check-hazards.js +83 -0
- package/dist/check-reporters.d.ts +8 -0
- package/dist/check-reporters.d.ts.map +1 -0
- package/dist/check-reporters.js +76 -0
- package/dist/check.d.ts +3 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +229 -0
- package/dist/cli-defaults.d.ts +24 -0
- package/dist/cli-defaults.d.ts.map +1 -0
- package/dist/cli-defaults.js +65 -0
- package/dist/cli-diff.d.ts +13 -0
- package/dist/cli-diff.d.ts.map +1 -0
- package/dist/cli-diff.js +348 -0
- package/dist/cli-reports.d.ts +9 -0
- package/dist/cli-reports.d.ts.map +1 -0
- package/dist/cli-reports.js +90 -0
- package/dist/cli-tools.d.ts +17 -0
- package/dist/cli-tools.d.ts.map +1 -0
- package/dist/cli-tools.js +136 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +239 -0
- package/dist/config-schema-gen.d.ts +2 -0
- package/dist/config-schema-gen.d.ts.map +1 -0
- package/dist/config-schema-gen.js +11 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +132 -0
- package/dist/core.d.ts +115 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1 -0
- package/dist/corpus.d.ts +26 -0
- package/dist/corpus.d.ts.map +1 -0
- package/dist/corpus.js +112 -0
- package/dist/database-url.d.ts +8 -0
- package/dist/database-url.d.ts.map +1 -0
- package/dist/database-url.js +74 -0
- package/dist/db-admin.d.ts +23 -0
- package/dist/db-admin.d.ts.map +1 -0
- package/dist/db-admin.js +147 -0
- package/dist/diagnostics.d.ts +16 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +155 -0
- package/dist/diff-score.d.ts +12 -0
- package/dist/diff-score.d.ts.map +1 -0
- package/dist/diff-score.js +339 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +110 -0
- package/dist/hash.d.ts +7 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +34 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/lineage.d.ts +23 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +61 -0
- package/dist/migrations-status.d.ts +35 -0
- package/dist/migrations-status.d.ts.map +1 -0
- package/dist/migrations-status.js +131 -0
- package/dist/plan-order.d.ts +4 -0
- package/dist/plan-order.d.ts.map +1 -0
- package/dist/plan-order.js +178 -0
- package/dist/planner-replace.d.ts +4 -0
- package/dist/planner-replace.d.ts.map +1 -0
- package/dist/planner-replace.js +76 -0
- package/dist/planner-table.d.ts +3 -0
- package/dist/planner-table.d.ts.map +1 -0
- package/dist/planner-table.js +165 -0
- package/dist/planner.d.ts +5 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +385 -0
- package/dist/render-guards.d.ts +12 -0
- package/dist/render-guards.d.ts.map +1 -0
- package/dist/render-guards.js +159 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +325 -0
- package/dist/selfcheck.d.ts +11 -0
- package/dist/selfcheck.d.ts.map +1 -0
- package/dist/selfcheck.js +43 -0
- package/dist/source-normalize.d.ts +14 -0
- package/dist/source-normalize.d.ts.map +1 -0
- package/dist/source-normalize.js +420 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +233 -0
- package/dist/sql/ast.d.ts +42 -0
- package/dist/sql/ast.d.ts.map +1 -0
- package/dist/sql/ast.js +241 -0
- package/dist/sql/canonical-nodes.d.ts +5 -0
- package/dist/sql/canonical-nodes.d.ts.map +1 -0
- package/dist/sql/canonical-nodes.js +101 -0
- package/dist/sql/extract-helpers.d.ts +18 -0
- package/dist/sql/extract-helpers.d.ts.map +1 -0
- package/dist/sql/extract-helpers.js +127 -0
- package/dist/sql/extract.d.ts +13 -0
- package/dist/sql/extract.d.ts.map +1 -0
- package/dist/sql/extract.js +323 -0
- package/dist/sql/facts.d.ts +34 -0
- package/dist/sql/facts.d.ts.map +1 -0
- package/dist/sql/facts.js +392 -0
- package/dist/sql/identifiers.d.ts +13 -0
- package/dist/sql/identifiers.d.ts.map +1 -0
- package/dist/sql/identifiers.js +83 -0
- package/dist/sql/normalize-deparse.d.ts +25 -0
- package/dist/sql/normalize-deparse.d.ts.map +1 -0
- package/dist/sql/normalize-deparse.js +96 -0
- package/dist/sql/object-hash.d.ts +5 -0
- package/dist/sql/object-hash.d.ts.map +1 -0
- package/dist/sql/object-hash.js +24 -0
- package/dist/sql/parser.d.ts +8 -0
- package/dist/sql/parser.d.ts.map +1 -0
- package/dist/sql/parser.js +89 -0
- package/dist/sql/privileges.d.ts +33 -0
- package/dist/sql/privileges.d.ts.map +1 -0
- package/dist/sql/privileges.js +379 -0
- package/dist/sql/split.d.ts +3 -0
- package/dist/sql/split.d.ts.map +1 -0
- package/dist/sql/split.js +182 -0
- package/dist/sql/statements.d.ts +17 -0
- package/dist/sql/statements.d.ts.map +1 -0
- package/dist/sql/statements.js +284 -0
- package/dist/sql/table-constraints.d.ts +15 -0
- package/dist/sql/table-constraints.d.ts.map +1 -0
- package/dist/sql/table-constraints.js +304 -0
- package/dist/sql/table-shape.d.ts +38 -0
- package/dist/sql/table-shape.d.ts.map +1 -0
- package/dist/sql/table-shape.js +287 -0
- package/dist/sync.d.ts +27 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +86 -0
- package/dist/typegen-model.d.ts +78 -0
- package/dist/typegen-model.d.ts.map +1 -0
- package/dist/typegen-model.js +338 -0
- package/dist/typegen-views.d.ts +7 -0
- package/dist/typegen-views.d.ts.map +1 -0
- package/dist/typegen-views.js +92 -0
- package/dist/typegen-zod.d.ts +3 -0
- package/dist/typegen-zod.d.ts.map +1 -0
- package/dist/typegen-zod.js +149 -0
- package/dist/typegen.d.ts +4 -0
- package/dist/typegen.d.ts.map +1 -0
- package/dist/typegen.js +184 -0
- package/dist/validators.d.ts +3 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +104 -0
- package/dist/verify-environment.d.ts +5 -0
- package/dist/verify-environment.d.ts.map +1 -0
- package/dist/verify-environment.js +92 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +261 -0
- package/docs/benchmarks/additive-correctness.svg +86 -0
- package/docs/benchmarks/additive-latency.svg +60 -0
- package/docs/benchmarks/functions-policies-correctness.svg +86 -0
- package/docs/benchmarks/functions-policies-latency.svg +60 -0
- package/docs/benchmarks/realistic-correctness.svg +86 -0
- package/docs/benchmarks/realistic-latency.svg +60 -0
- package/docs/benchmarks/scaling-latency.svg +106 -0
- package/docs/benchmarks/workflow-latency.svg +98 -0
- package/docs/benchmarks/xl-correctness.svg +86 -0
- package/docs/benchmarks/xl-latency.svg +60 -0
- package/docs/benchmarks/xxl-correctness.svg +86 -0
- package/docs/benchmarks/xxl-latency.svg +66 -0
- package/docs/case-study-anilize.md +51 -0
- package/docs/ci-gate.md +44 -0
- package/docs/ci.md +68 -0
- package/docs/commands.md +35 -0
- package/docs/config.md +72 -0
- package/docs/corpus.md +33 -0
- package/docs/diagnostics.md +77 -0
- package/docs/hints.md +92 -0
- package/docs/release.md +19 -0
- package/docs/support-matrix.md +57 -0
- package/examples/postgres/schemas/001_app.sql +17 -0
- package/examples/supabase/schemas/001_app.sql +13 -0
- package/examples/supabase/schemas-next/001_app.sql +21 -0
- package/examples/supabase/supaschema.config.json +15 -0
- package/package.json +99 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { sha256 } from "./hash.js";
|
|
2
|
+
import { formatQualifiedName, quoteIdent } from "./sql/identifiers.js";
|
|
3
|
+
import { makeObject } from "./sql/statements.js";
|
|
4
|
+
const managedSchemaFilter = `
|
|
5
|
+
n.nspname !~ '^pg_'
|
|
6
|
+
and n.nspname <> 'information_schema'
|
|
7
|
+
`;
|
|
8
|
+
const relationCommentWords = new Map([
|
|
9
|
+
["S", "sequence"],
|
|
10
|
+
["i", "index"],
|
|
11
|
+
["m", "materialized view"],
|
|
12
|
+
["p", "table"],
|
|
13
|
+
["r", "table"],
|
|
14
|
+
["v", "view"],
|
|
15
|
+
]);
|
|
16
|
+
export async function collectComments(pool) {
|
|
17
|
+
const sections = await Promise.all([
|
|
18
|
+
collectRelationComments(pool),
|
|
19
|
+
collectFunctionComments(pool),
|
|
20
|
+
collectSchemaComments(pool),
|
|
21
|
+
collectTypeComments(pool),
|
|
22
|
+
collectPolicyComments(pool),
|
|
23
|
+
collectTriggerComments(pool),
|
|
24
|
+
collectConstraintComments(pool),
|
|
25
|
+
collectExtensionComments(pool),
|
|
26
|
+
]);
|
|
27
|
+
return sections.flat();
|
|
28
|
+
}
|
|
29
|
+
async function collectRelationComments(pool) {
|
|
30
|
+
const objects = [];
|
|
31
|
+
const rows = await pool.query(`
|
|
32
|
+
select n.nspname as schema, c.relname as name, c.relkind as relkind,
|
|
33
|
+
d.objsubid as column_number, a.attname as column_name, d.description as description
|
|
34
|
+
from pg_description d
|
|
35
|
+
join pg_class c on c.oid = d.objoid
|
|
36
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
37
|
+
left join pg_attribute a on a.attrelid = c.oid and a.attnum = d.objsubid and d.objsubid > 0
|
|
38
|
+
where d.classoid = 'pg_class'::regclass and ${managedSchemaFilter}
|
|
39
|
+
order by n.nspname, c.relname, d.objsubid
|
|
40
|
+
`);
|
|
41
|
+
for (const row of rows.rows) {
|
|
42
|
+
const schema = text(row.schema);
|
|
43
|
+
const name = text(row.name);
|
|
44
|
+
const word = relationCommentWords.get(text(row.relkind));
|
|
45
|
+
if (!word) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const isColumn = typeof row.column_number === "number" && row.column_number > 0;
|
|
49
|
+
if (isColumn && !row.column_name) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const descriptor = isColumn
|
|
53
|
+
? `column ${schema}.${name}.${text(row.column_name)}`
|
|
54
|
+
: `${word} ${schema}.${name}`;
|
|
55
|
+
const targetSql = isColumn
|
|
56
|
+
? `COLUMN ${formatQualifiedName(schema, name)}.${quoteIdent(text(row.column_name))}`
|
|
57
|
+
: `${word.toUpperCase()} ${formatQualifiedName(schema, name)}`;
|
|
58
|
+
objects.push(commentObject(descriptor, targetSql, schema, text(row.description)));
|
|
59
|
+
}
|
|
60
|
+
return objects;
|
|
61
|
+
}
|
|
62
|
+
async function collectFunctionComments(pool) {
|
|
63
|
+
const rows = await pool.query(`
|
|
64
|
+
select n.nspname as schema, p.proname as name,
|
|
65
|
+
oidvectortypes(p.proargtypes) as args, d.description as description
|
|
66
|
+
from pg_description d
|
|
67
|
+
join pg_proc p on p.oid = d.objoid
|
|
68
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
69
|
+
where d.classoid = 'pg_proc'::regclass and ${managedSchemaFilter}
|
|
70
|
+
order by n.nspname, p.proname
|
|
71
|
+
`);
|
|
72
|
+
return rows.rows.map((row) => {
|
|
73
|
+
const schema = text(row.schema);
|
|
74
|
+
const name = text(row.name);
|
|
75
|
+
const args = text(row.args);
|
|
76
|
+
return commentObject(`function ${schema}.${name}(${args})`, `FUNCTION ${formatQualifiedName(schema, name)}(${args})`, schema, text(row.description));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function collectSchemaComments(pool) {
|
|
80
|
+
const rows = await pool.query(`
|
|
81
|
+
select n.nspname as name, d.description as description
|
|
82
|
+
from pg_description d
|
|
83
|
+
join pg_namespace n on n.oid = d.objoid
|
|
84
|
+
where d.classoid = 'pg_namespace'::regclass
|
|
85
|
+
and ${managedSchemaFilter}
|
|
86
|
+
and n.nspname <> 'public'
|
|
87
|
+
order by n.nspname
|
|
88
|
+
`);
|
|
89
|
+
return rows.rows.map((row) => {
|
|
90
|
+
const name = text(row.name);
|
|
91
|
+
return commentObject(`schema ${name}`, `SCHEMA ${name}`, undefined, text(row.description));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function collectTypeComments(pool) {
|
|
95
|
+
const rows = await pool.query(`
|
|
96
|
+
select n.nspname as schema, t.typname as name, t.typtype as typtype,
|
|
97
|
+
d.description as description
|
|
98
|
+
from pg_description d
|
|
99
|
+
join pg_type t on t.oid = d.objoid
|
|
100
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
101
|
+
where d.classoid = 'pg_type'::regclass and ${managedSchemaFilter}
|
|
102
|
+
order by n.nspname, t.typname
|
|
103
|
+
`);
|
|
104
|
+
return rows.rows.map((row) => {
|
|
105
|
+
const schema = text(row.schema);
|
|
106
|
+
const name = text(row.name);
|
|
107
|
+
const word = text(row.typtype) === "d" ? "domain" : "type";
|
|
108
|
+
return commentObject(`${word} ${schema}.${name}`, `${word.toUpperCase()} ${formatQualifiedName(schema, name)}`, schema, text(row.description));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function collectPolicyComments(pool) {
|
|
112
|
+
const rows = await pool.query(`
|
|
113
|
+
select n.nspname as schema, c.relname as table_name, p.polname as name,
|
|
114
|
+
d.description as description
|
|
115
|
+
from pg_description d
|
|
116
|
+
join pg_policy p on p.oid = d.objoid
|
|
117
|
+
join pg_class c on c.oid = p.polrelid
|
|
118
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
119
|
+
where d.classoid = 'pg_policy'::regclass and ${managedSchemaFilter}
|
|
120
|
+
order by n.nspname, c.relname, p.polname
|
|
121
|
+
`);
|
|
122
|
+
return rows.rows.map((row) => {
|
|
123
|
+
const schema = text(row.schema);
|
|
124
|
+
const table = text(row.table_name);
|
|
125
|
+
const name = text(row.name);
|
|
126
|
+
return commentObject(`policy ${schema}.${table}.${name}`, `POLICY ${quoteIdent(name)} ON ${formatQualifiedName(schema, table)}`, schema, text(row.description));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async function collectTriggerComments(pool) {
|
|
130
|
+
const rows = await pool.query(`
|
|
131
|
+
select n.nspname as schema, c.relname as table_name, t.tgname as name,
|
|
132
|
+
d.description as description
|
|
133
|
+
from pg_description d
|
|
134
|
+
join pg_trigger t on t.oid = d.objoid
|
|
135
|
+
join pg_class c on c.oid = t.tgrelid
|
|
136
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
137
|
+
where d.classoid = 'pg_trigger'::regclass
|
|
138
|
+
and not t.tgisinternal
|
|
139
|
+
and ${managedSchemaFilter}
|
|
140
|
+
order by n.nspname, c.relname, t.tgname
|
|
141
|
+
`);
|
|
142
|
+
return rows.rows.map((row) => {
|
|
143
|
+
const schema = text(row.schema);
|
|
144
|
+
const table = text(row.table_name);
|
|
145
|
+
const name = text(row.name);
|
|
146
|
+
return commentObject(`trigger ${schema}.${table}.${name}`, `TRIGGER ${quoteIdent(name)} ON ${formatQualifiedName(schema, table)}`, schema, text(row.description));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async function collectConstraintComments(pool) {
|
|
150
|
+
const rows = await pool.query(`
|
|
151
|
+
select n.nspname as schema, r.relname as table_name, c.conname as name,
|
|
152
|
+
d.description as description
|
|
153
|
+
from pg_description d
|
|
154
|
+
join pg_constraint c on c.oid = d.objoid
|
|
155
|
+
join pg_class r on r.oid = c.conrelid
|
|
156
|
+
join pg_namespace n on n.oid = r.relnamespace
|
|
157
|
+
where d.classoid = 'pg_constraint'::regclass and ${managedSchemaFilter}
|
|
158
|
+
order by n.nspname, r.relname, c.conname
|
|
159
|
+
`);
|
|
160
|
+
return rows.rows.map((row) => {
|
|
161
|
+
const schema = text(row.schema);
|
|
162
|
+
const table = text(row.table_name);
|
|
163
|
+
const name = text(row.name);
|
|
164
|
+
return commentObject(`constraint ${schema}.${table}.${name}`, `CONSTRAINT ${quoteIdent(name)} ON ${formatQualifiedName(schema, table)}`, schema, text(row.description));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async function collectExtensionComments(pool) {
|
|
168
|
+
const rows = await pool.query(`
|
|
169
|
+
select e.extname as name, d.description as description
|
|
170
|
+
from pg_description d
|
|
171
|
+
join pg_extension e on e.oid = d.objoid
|
|
172
|
+
where d.classoid = 'pg_extension'::regclass
|
|
173
|
+
and e.extname <> 'plpgsql'
|
|
174
|
+
order by e.extname
|
|
175
|
+
`);
|
|
176
|
+
return rows.rows.map((row) => {
|
|
177
|
+
const name = text(row.name);
|
|
178
|
+
return commentObject(`extension ${name}`, `EXTENSION ${quoteIdent(name)}`, undefined, text(row.description));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function commentObject(descriptor, targetSql, schema, description) {
|
|
182
|
+
const sql = `COMMENT ON ${targetSql} IS '${description.replaceAll("'", "''")}'`;
|
|
183
|
+
const ref = {
|
|
184
|
+
kind: "comment",
|
|
185
|
+
name: sha256(descriptor).slice(0, 16),
|
|
186
|
+
};
|
|
187
|
+
if (schema) {
|
|
188
|
+
ref.schema = schema;
|
|
189
|
+
}
|
|
190
|
+
return makeObject(ref, sql, 0, undefined, { descriptor });
|
|
191
|
+
}
|
|
192
|
+
function text(value) {
|
|
193
|
+
return typeof value === "string" ? value : String(value);
|
|
194
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SchemaObject } from "./core.js";
|
|
2
|
+
type CatalogQuery = {
|
|
3
|
+
query: <Row extends Record<string, unknown>>(text: string, values?: unknown[]) => Promise<{
|
|
4
|
+
rows: Row[];
|
|
5
|
+
}>;
|
|
6
|
+
};
|
|
7
|
+
export declare function collectTypes(pool: CatalogQuery): Promise<SchemaObject[]>;
|
|
8
|
+
export declare function collectSequences(pool: CatalogQuery): Promise<SchemaObject[]>;
|
|
9
|
+
export declare function collectGrants(pool: CatalogQuery): Promise<SchemaObject[]>;
|
|
10
|
+
export declare function collectDefaultPrivileges(pool: CatalogQuery): Promise<SchemaObject[]>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=catalog-extras.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-extras.d.ts","sourceRoot":"","sources":["../src/catalog-extras.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAS9C,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;CAC/B,CAAC;AAOF,wBAAsB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA+E9E;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAsElF;AAQD,wBAAsB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA2L/E;AAUD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAuE1F"}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { formatQualifiedName, quoteIdent, stripOuterDoubleQuotes } from "./sql/identifiers.js";
|
|
2
|
+
import { buildDefaultPrivilegeObject, buildGrantObject, isBuiltinDefaultGrant, } from "./sql/privileges.js";
|
|
3
|
+
import { makeObject } from "./sql/statements.js";
|
|
4
|
+
const managedSchemaFilter = `
|
|
5
|
+
n.nspname !~ '^pg_'
|
|
6
|
+
and n.nspname <> 'information_schema'
|
|
7
|
+
`;
|
|
8
|
+
export async function collectTypes(pool) {
|
|
9
|
+
const objects = [];
|
|
10
|
+
const enums = await pool.query(`
|
|
11
|
+
select n.nspname as schema, t.typname as name,
|
|
12
|
+
array_agg(e.enumlabel order by e.enumsortorder) as values
|
|
13
|
+
from pg_type t
|
|
14
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
15
|
+
join pg_enum e on e.enumtypid = t.oid
|
|
16
|
+
where t.typtype = 'e' and ${managedSchemaFilter}
|
|
17
|
+
group by n.nspname, t.typname
|
|
18
|
+
order by n.nspname, t.typname
|
|
19
|
+
`);
|
|
20
|
+
for (const row of enums.rows) {
|
|
21
|
+
const schema = text(row.schema);
|
|
22
|
+
const name = text(row.name);
|
|
23
|
+
const values = textArray(row.values);
|
|
24
|
+
const rendered = values.map((value) => `'${value.replaceAll("'", "''")}'`).join(", ");
|
|
25
|
+
objects.push(makeObject({ kind: "enum", name, schema }, `CREATE TYPE ${formatQualifiedName(schema, name)} AS ENUM (${rendered})`, 0, undefined, { values }));
|
|
26
|
+
}
|
|
27
|
+
const domains = await pool.query(`
|
|
28
|
+
select n.nspname as schema, t.typname as name,
|
|
29
|
+
format_type(t.typbasetype, t.typtypmod) as base,
|
|
30
|
+
t.typnotnull as not_null,
|
|
31
|
+
coalesce(
|
|
32
|
+
(select string_agg(pg_get_constraintdef(c.oid, true), ' ' order by c.conname)
|
|
33
|
+
from pg_constraint c where c.contypid = t.oid and c.contype = 'c'),
|
|
34
|
+
''
|
|
35
|
+
) as constraints
|
|
36
|
+
from pg_type t
|
|
37
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
38
|
+
where t.typtype = 'd' and ${managedSchemaFilter}
|
|
39
|
+
order by n.nspname, t.typname
|
|
40
|
+
`);
|
|
41
|
+
for (const row of domains.rows) {
|
|
42
|
+
const schema = text(row.schema);
|
|
43
|
+
const name = text(row.name);
|
|
44
|
+
const parts = [`CREATE DOMAIN ${formatQualifiedName(schema, name)} AS ${text(row.base)}`];
|
|
45
|
+
if (row.not_null === true) {
|
|
46
|
+
parts.push("NOT NULL");
|
|
47
|
+
}
|
|
48
|
+
const constraints = text(row.constraints);
|
|
49
|
+
if (constraints.length > 0) {
|
|
50
|
+
parts.push(constraints);
|
|
51
|
+
}
|
|
52
|
+
objects.push(makeObject({ kind: "domain", name, schema }, parts.join(" "), 0));
|
|
53
|
+
}
|
|
54
|
+
const composites = await pool.query(`
|
|
55
|
+
select n.nspname as schema, c.relname as name,
|
|
56
|
+
string_agg(
|
|
57
|
+
quote_ident(a.attname) || ' ' || format_type(a.atttypid, a.atttypmod),
|
|
58
|
+
', ' order by a.attnum
|
|
59
|
+
) as columns
|
|
60
|
+
from pg_class c
|
|
61
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
62
|
+
join pg_attribute a on a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped
|
|
63
|
+
where c.relkind = 'c' and ${managedSchemaFilter}
|
|
64
|
+
group by n.nspname, c.relname
|
|
65
|
+
order by n.nspname, c.relname
|
|
66
|
+
`);
|
|
67
|
+
for (const row of composites.rows) {
|
|
68
|
+
const schema = text(row.schema);
|
|
69
|
+
const name = text(row.name);
|
|
70
|
+
objects.push(makeObject({ kind: "type", name, schema }, `CREATE TYPE ${formatQualifiedName(schema, name)} AS (${text(row.columns)})`, 0));
|
|
71
|
+
}
|
|
72
|
+
return objects;
|
|
73
|
+
}
|
|
74
|
+
export async function collectSequences(pool) {
|
|
75
|
+
const objects = [];
|
|
76
|
+
const sequences = await pool.query(`
|
|
77
|
+
select n.nspname as schema, c.relname as name,
|
|
78
|
+
format_type(s.seqtypid, null) as data_type,
|
|
79
|
+
s.seqstart::text as start_value,
|
|
80
|
+
s.seqincrement::text as increment_by,
|
|
81
|
+
s.seqmin::text as min_value,
|
|
82
|
+
s.seqmax::text as max_value,
|
|
83
|
+
s.seqcache::text as cache_size,
|
|
84
|
+
s.seqcycle as cycle,
|
|
85
|
+
dn.nspname as owned_schema, dc.relname as owned_table, a.attname as owned_column
|
|
86
|
+
from pg_class c
|
|
87
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
88
|
+
join pg_sequence s on s.seqrelid = c.oid
|
|
89
|
+
left join pg_depend d
|
|
90
|
+
on d.objid = c.oid and d.classid = 'pg_class'::regclass and d.deptype = 'a'
|
|
91
|
+
left join pg_class dc on dc.oid = d.refobjid
|
|
92
|
+
left join pg_namespace dn on dn.oid = dc.relnamespace
|
|
93
|
+
left join pg_attribute a on a.attrelid = d.refobjid and a.attnum = d.refobjsubid
|
|
94
|
+
where c.relkind = 'S'
|
|
95
|
+
and ${managedSchemaFilter}
|
|
96
|
+
and not exists (
|
|
97
|
+
select 1 from pg_depend i
|
|
98
|
+
where i.objid = c.oid and i.classid = 'pg_class'::regclass and i.deptype = 'i'
|
|
99
|
+
)
|
|
100
|
+
order by n.nspname, c.relname
|
|
101
|
+
`);
|
|
102
|
+
for (const row of sequences.rows) {
|
|
103
|
+
const schema = text(row.schema);
|
|
104
|
+
const name = text(row.name);
|
|
105
|
+
const ownedBy = row.owned_table && row.owned_column
|
|
106
|
+
? `${text(row.owned_schema)}.${text(row.owned_table)}.${text(row.owned_column)}`
|
|
107
|
+
: undefined;
|
|
108
|
+
const clauses = [`CREATE SEQUENCE ${formatQualifiedName(schema, name)}`];
|
|
109
|
+
const dataType = text(row.data_type);
|
|
110
|
+
if (dataType !== "bigint") {
|
|
111
|
+
clauses.push(`AS ${dataType}`);
|
|
112
|
+
}
|
|
113
|
+
for (const [keyword, value, fallback] of [
|
|
114
|
+
["INCREMENT BY", text(row.increment_by), "1"],
|
|
115
|
+
["MINVALUE", text(row.min_value), "1"],
|
|
116
|
+
["MAXVALUE", text(row.max_value), sequenceTypeMax.get(dataType) ?? ""],
|
|
117
|
+
["START WITH", text(row.start_value), "1"],
|
|
118
|
+
["CACHE", text(row.cache_size), "1"],
|
|
119
|
+
]) {
|
|
120
|
+
if (value !== fallback) {
|
|
121
|
+
clauses.push(`${keyword} ${value}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (row.cycle === true) {
|
|
125
|
+
clauses.push("CYCLE");
|
|
126
|
+
}
|
|
127
|
+
if (ownedBy) {
|
|
128
|
+
clauses.push(`OWNED BY ${formatQualifiedName(text(row.owned_schema), text(row.owned_table))}.${quoteIdent(text(row.owned_column))}`);
|
|
129
|
+
}
|
|
130
|
+
objects.push(makeObject({ kind: "sequence", name, schema }, clauses.join(" "), 0, undefined, ownedBy ? { ownedBy } : {}));
|
|
131
|
+
}
|
|
132
|
+
return objects;
|
|
133
|
+
}
|
|
134
|
+
const sequenceTypeMax = new Map([
|
|
135
|
+
["bigint", "9223372036854775807"],
|
|
136
|
+
["integer", "2147483647"],
|
|
137
|
+
["smallint", "32767"],
|
|
138
|
+
]);
|
|
139
|
+
export async function collectGrants(pool) {
|
|
140
|
+
const objects = [];
|
|
141
|
+
const relationGrants = await pool.query(`
|
|
142
|
+
select n.nspname as schema, c.relname as name, c.relkind as relkind,
|
|
143
|
+
case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
|
|
144
|
+
array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
|
|
145
|
+
from pg_class c
|
|
146
|
+
join pg_namespace n on n.oid = c.relnamespace,
|
|
147
|
+
lateral aclexplode(c.relacl) as acl
|
|
148
|
+
where c.relacl is not null
|
|
149
|
+
and c.relkind in ('r', 'p', 'v', 'm', 'S')
|
|
150
|
+
and ${managedSchemaFilter}
|
|
151
|
+
and acl.grantee <> c.relowner
|
|
152
|
+
and not exists (
|
|
153
|
+
select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
|
|
154
|
+
where i.objoid = c.oid and i.classoid = 'pg_class'::regclass
|
|
155
|
+
and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
|
|
156
|
+
)
|
|
157
|
+
group by n.nspname, c.relname, c.relkind, acl.grantee
|
|
158
|
+
order by n.nspname, c.relname, grantee
|
|
159
|
+
`);
|
|
160
|
+
for (const row of relationGrants.rows) {
|
|
161
|
+
const schema = text(row.schema);
|
|
162
|
+
const name = text(row.name);
|
|
163
|
+
const kindPhrase = text(row.relkind) === "S" ? "SEQUENCE" : "TABLE";
|
|
164
|
+
objects.push(buildGrantObject({
|
|
165
|
+
grantee: text(row.grantee),
|
|
166
|
+
kindPhrase,
|
|
167
|
+
ordinal: 0,
|
|
168
|
+
privileges: textArray(row.privileges),
|
|
169
|
+
schema,
|
|
170
|
+
targetIdentity: `${schema}.${name}`,
|
|
171
|
+
targetRendered: formatQualifiedName(schema, name),
|
|
172
|
+
verb: "GRANT",
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
// pg_init_privs records initdb/extension-time ACLs; like pg_dump, only the
|
|
176
|
+
// delta against them is declared state (initdb grants USAGE on schema
|
|
177
|
+
// public to PUBLIC in every database).
|
|
178
|
+
const schemaGrants = await pool.query(`
|
|
179
|
+
select n.nspname as name,
|
|
180
|
+
case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
|
|
181
|
+
array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
|
|
182
|
+
from pg_namespace n,
|
|
183
|
+
lateral aclexplode(n.nspacl) as acl
|
|
184
|
+
where n.nspacl is not null
|
|
185
|
+
and ${managedSchemaFilter}
|
|
186
|
+
and acl.grantee <> n.nspowner
|
|
187
|
+
and not exists (
|
|
188
|
+
select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
|
|
189
|
+
where i.objoid = n.oid and i.classoid = 'pg_namespace'::regclass
|
|
190
|
+
and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
|
|
191
|
+
)
|
|
192
|
+
group by n.nspname, acl.grantee
|
|
193
|
+
order by n.nspname, grantee
|
|
194
|
+
`);
|
|
195
|
+
for (const row of schemaGrants.rows) {
|
|
196
|
+
const name = text(row.name);
|
|
197
|
+
objects.push(buildGrantObject({
|
|
198
|
+
grantee: text(row.grantee),
|
|
199
|
+
kindPhrase: "SCHEMA",
|
|
200
|
+
ordinal: 0,
|
|
201
|
+
privileges: textArray(row.privileges),
|
|
202
|
+
schema: name,
|
|
203
|
+
targetIdentity: name,
|
|
204
|
+
targetRendered: `"${name}"`,
|
|
205
|
+
verb: "GRANT",
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
const functionGrants = await pool.query(`
|
|
209
|
+
select n.nspname as schema, p.proname as name,
|
|
210
|
+
oidvectortypes(p.proargtypes) as args,
|
|
211
|
+
case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
|
|
212
|
+
array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
|
|
213
|
+
from pg_proc p
|
|
214
|
+
join pg_namespace n on n.oid = p.pronamespace,
|
|
215
|
+
lateral aclexplode(p.proacl) as acl
|
|
216
|
+
where p.proacl is not null
|
|
217
|
+
and ${managedSchemaFilter}
|
|
218
|
+
and acl.grantee <> p.proowner
|
|
219
|
+
and not exists (
|
|
220
|
+
select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
|
|
221
|
+
where i.objoid = p.oid and i.classoid = 'pg_proc'::regclass
|
|
222
|
+
and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
|
|
223
|
+
)
|
|
224
|
+
group by n.nspname, p.proname, args, acl.grantee
|
|
225
|
+
order by n.nspname, p.proname, grantee
|
|
226
|
+
`);
|
|
227
|
+
for (const row of functionGrants.rows) {
|
|
228
|
+
const schema = text(row.schema);
|
|
229
|
+
const name = text(row.name);
|
|
230
|
+
const args = text(row.args);
|
|
231
|
+
const grantee = text(row.grantee);
|
|
232
|
+
const privileges = textArray(row.privileges);
|
|
233
|
+
if (isBuiltinDefaultGrant("FUNCTION", grantee, privileges)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
objects.push(buildGrantObject({
|
|
237
|
+
grantee,
|
|
238
|
+
kindPhrase: "FUNCTION",
|
|
239
|
+
ordinal: 0,
|
|
240
|
+
privileges,
|
|
241
|
+
schema,
|
|
242
|
+
targetIdentity: `${schema}.${name}(${args})`,
|
|
243
|
+
targetRendered: `${formatQualifiedName(schema, name)}(${args})`,
|
|
244
|
+
verb: "GRANT",
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
// A non-null routine ACL lacking PUBLIC's built-in EXECUTE means it was
|
|
248
|
+
// explicitly revoked; emit that revoke so trees declaring it hash-match.
|
|
249
|
+
const revokedFunctionDefaults = await pool.query(`
|
|
250
|
+
select n.nspname as schema, p.proname as name, oidvectortypes(p.proargtypes) as args
|
|
251
|
+
from pg_proc p
|
|
252
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
253
|
+
where p.proacl is not null
|
|
254
|
+
and ${managedSchemaFilter}
|
|
255
|
+
and not exists (
|
|
256
|
+
select 1 from aclexplode(p.proacl) acl
|
|
257
|
+
where acl.grantee = 0 and acl.privilege_type = 'EXECUTE'
|
|
258
|
+
)
|
|
259
|
+
order by n.nspname, p.proname
|
|
260
|
+
`);
|
|
261
|
+
for (const row of revokedFunctionDefaults.rows) {
|
|
262
|
+
const schema = text(row.schema);
|
|
263
|
+
const name = text(row.name);
|
|
264
|
+
const args = text(row.args);
|
|
265
|
+
objects.push(buildGrantObject({
|
|
266
|
+
grantee: "PUBLIC",
|
|
267
|
+
kindPhrase: "FUNCTION",
|
|
268
|
+
ordinal: 0,
|
|
269
|
+
privileges: ["ALL"],
|
|
270
|
+
schema,
|
|
271
|
+
targetIdentity: `${schema}.${name}(${args})`,
|
|
272
|
+
targetRendered: `${formatQualifiedName(schema, name)}(${args})`,
|
|
273
|
+
verb: "REVOKE",
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
const typeGrants = await pool.query(`
|
|
277
|
+
select n.nspname as schema, t.typname as name, t.typtype as typtype,
|
|
278
|
+
case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
|
|
279
|
+
array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
|
|
280
|
+
from pg_type t
|
|
281
|
+
join pg_namespace n on n.oid = t.typnamespace,
|
|
282
|
+
lateral aclexplode(t.typacl) as acl
|
|
283
|
+
where t.typacl is not null
|
|
284
|
+
and t.typtype in ('e', 'd', 'c', 'r')
|
|
285
|
+
and ${managedSchemaFilter}
|
|
286
|
+
and acl.grantee <> t.typowner
|
|
287
|
+
and not exists (
|
|
288
|
+
select 1 from pg_init_privs i, lateral aclexplode(i.initprivs) ia
|
|
289
|
+
where i.objoid = t.oid and i.classoid = 'pg_type'::regclass
|
|
290
|
+
and ia.grantee = acl.grantee and ia.privilege_type = acl.privilege_type
|
|
291
|
+
)
|
|
292
|
+
group by n.nspname, t.typname, t.typtype, acl.grantee
|
|
293
|
+
order by n.nspname, t.typname, grantee
|
|
294
|
+
`);
|
|
295
|
+
for (const row of typeGrants.rows) {
|
|
296
|
+
const schema = text(row.schema);
|
|
297
|
+
const name = text(row.name);
|
|
298
|
+
const grantee = text(row.grantee);
|
|
299
|
+
const privileges = textArray(row.privileges);
|
|
300
|
+
const kindPhrase = text(row.typtype) === "d" ? "DOMAIN" : "TYPE";
|
|
301
|
+
if (isBuiltinDefaultGrant(kindPhrase, grantee, privileges)) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
objects.push(buildGrantObject({
|
|
305
|
+
grantee,
|
|
306
|
+
kindPhrase,
|
|
307
|
+
ordinal: 0,
|
|
308
|
+
privileges,
|
|
309
|
+
schema,
|
|
310
|
+
targetIdentity: `${schema}.${name}`,
|
|
311
|
+
targetRendered: formatQualifiedName(schema, name),
|
|
312
|
+
verb: "GRANT",
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
return objects;
|
|
316
|
+
}
|
|
317
|
+
const defaultPrivilegeObjectTypes = new Map([
|
|
318
|
+
["S", "SEQUENCES"],
|
|
319
|
+
["T", "TYPES"],
|
|
320
|
+
["f", "FUNCTIONS"],
|
|
321
|
+
["n", "SCHEMAS"],
|
|
322
|
+
["r", "TABLES"],
|
|
323
|
+
]);
|
|
324
|
+
export async function collectDefaultPrivileges(pool) {
|
|
325
|
+
const objects = [];
|
|
326
|
+
const rows = await pool.query(`
|
|
327
|
+
select pg_get_userbyid(d.defaclrole) as for_role,
|
|
328
|
+
case when d.defaclnamespace = 0 then null else n.nspname end as schema,
|
|
329
|
+
d.defaclobjtype as objtype,
|
|
330
|
+
case when acl.grantee = 0 then 'PUBLIC' else pg_get_userbyid(acl.grantee) end as grantee,
|
|
331
|
+
array_agg(distinct acl.privilege_type order by acl.privilege_type) as privileges
|
|
332
|
+
from pg_default_acl d
|
|
333
|
+
left join pg_namespace n on n.oid = d.defaclnamespace,
|
|
334
|
+
lateral aclexplode(d.defaclacl) as acl
|
|
335
|
+
group by d.defaclrole, schema, d.defaclobjtype, acl.grantee
|
|
336
|
+
order by for_role, schema, d.defaclobjtype, grantee
|
|
337
|
+
`);
|
|
338
|
+
for (const row of rows.rows) {
|
|
339
|
+
const objectType = defaultPrivilegeObjectTypes.get(text(row.objtype));
|
|
340
|
+
if (!objectType) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const forRole = text(row.for_role);
|
|
344
|
+
const grantee = text(row.grantee);
|
|
345
|
+
const privileges = textArray(row.privileges);
|
|
346
|
+
// The owner's self-entry and PUBLIC's built-in routine/type defaults are
|
|
347
|
+
// acldefault noise, not declared state.
|
|
348
|
+
if (grantee === forRole || isBuiltinDefaultGrant(objectType, grantee, privileges)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
objects.push(buildDefaultPrivilegeObject({
|
|
352
|
+
forRole,
|
|
353
|
+
grantee,
|
|
354
|
+
objectType,
|
|
355
|
+
ordinal: 0,
|
|
356
|
+
privileges,
|
|
357
|
+
schema: row.schema === null ? undefined : text(row.schema),
|
|
358
|
+
verb: "GRANT",
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
// A default-ACL row for routines whose entries lack PUBLIC EXECUTE records
|
|
362
|
+
// an ALTER DEFAULT PRIVILEGES ... REVOKE ... FROM PUBLIC.
|
|
363
|
+
const revokedDefaults = await pool.query(`
|
|
364
|
+
select pg_get_userbyid(d.defaclrole) as for_role,
|
|
365
|
+
case when d.defaclnamespace = 0 then null else n.nspname end as schema,
|
|
366
|
+
d.defaclobjtype as objtype
|
|
367
|
+
from pg_default_acl d
|
|
368
|
+
left join pg_namespace n on n.oid = d.defaclnamespace
|
|
369
|
+
where d.defaclobjtype in ('f', 'T')
|
|
370
|
+
and not exists (
|
|
371
|
+
select 1 from aclexplode(d.defaclacl) acl where acl.grantee = 0
|
|
372
|
+
)
|
|
373
|
+
order by for_role, schema, objtype
|
|
374
|
+
`);
|
|
375
|
+
for (const row of revokedDefaults.rows) {
|
|
376
|
+
const objectType = defaultPrivilegeObjectTypes.get(text(row.objtype));
|
|
377
|
+
if (!objectType) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
objects.push(buildDefaultPrivilegeObject({
|
|
381
|
+
forRole: text(row.for_role),
|
|
382
|
+
grantee: "PUBLIC",
|
|
383
|
+
objectType,
|
|
384
|
+
ordinal: 0,
|
|
385
|
+
privileges: ["ALL"],
|
|
386
|
+
schema: row.schema === null ? undefined : text(row.schema),
|
|
387
|
+
verb: "REVOKE",
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
return objects;
|
|
391
|
+
}
|
|
392
|
+
function text(value) {
|
|
393
|
+
return typeof value === "string" ? value : String(value);
|
|
394
|
+
}
|
|
395
|
+
function textArray(value) {
|
|
396
|
+
if (Array.isArray(value)) {
|
|
397
|
+
return value.map(text);
|
|
398
|
+
}
|
|
399
|
+
const raw = text(value).trim();
|
|
400
|
+
if (raw.startsWith("{") && raw.endsWith("}")) {
|
|
401
|
+
return raw
|
|
402
|
+
.slice(1, -1)
|
|
403
|
+
.split(",")
|
|
404
|
+
.map((item) => stripOuterDoubleQuotes(item.trim()))
|
|
405
|
+
.filter(Boolean);
|
|
406
|
+
}
|
|
407
|
+
return raw.length > 0 ? [raw] : [];
|
|
408
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SchemaObject } from "./core.js";
|
|
2
|
+
type CatalogQuery = {
|
|
3
|
+
query: <Row extends Record<string, unknown>>(text: string, values?: unknown[]) => Promise<{
|
|
4
|
+
rows: Row[];
|
|
5
|
+
}>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Whole-object FDW tier: servers and foreign tables are modeled by their
|
|
9
|
+
* complete definition (no column-level diffing). Server and column options
|
|
10
|
+
* are reconstructed from catalog option arrays; user mappings are excluded
|
|
11
|
+
* because they carry credentials.
|
|
12
|
+
*/
|
|
13
|
+
export declare function collectForeignObjects(pool: CatalogQuery): Promise<SchemaObject[]>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=catalog-foreign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-foreign.d.ts","sourceRoot":"","sources":["../src/catalog-foreign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAI9C,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;CAC/B,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAuFvF"}
|