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,89 @@
|
|
|
1
|
+
import { diagnostic } from "../diagnostics.js";
|
|
2
|
+
import { sha256 } from "../hash.js";
|
|
3
|
+
const parseCacheLimit = 2000;
|
|
4
|
+
const parseCache = new Map();
|
|
5
|
+
let cachedParser;
|
|
6
|
+
export async function parseSql(sql, file) {
|
|
7
|
+
return (await parseSqlAst(sql, file)).diagnostics;
|
|
8
|
+
}
|
|
9
|
+
export async function parseSqlAst(sql, file) {
|
|
10
|
+
const cacheKey = sha256(sql);
|
|
11
|
+
const cached = parseCache.get(cacheKey);
|
|
12
|
+
if (cached) {
|
|
13
|
+
return withLocation(cached, file);
|
|
14
|
+
}
|
|
15
|
+
const outcome = await parseUncached(sql);
|
|
16
|
+
if (parseCache.size >= parseCacheLimit) {
|
|
17
|
+
parseCache.clear();
|
|
18
|
+
}
|
|
19
|
+
parseCache.set(cacheKey, outcome);
|
|
20
|
+
return withLocation(outcome, file);
|
|
21
|
+
}
|
|
22
|
+
async function parseUncached(sql) {
|
|
23
|
+
try {
|
|
24
|
+
const parser = await loadParser();
|
|
25
|
+
if (!parser) {
|
|
26
|
+
return {
|
|
27
|
+
diagnostics: [
|
|
28
|
+
diagnostic("SUPA_PARSE_UNAVAILABLE", "warning", "libpg-query did not expose a parser", {}),
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
ast: await parser(sql),
|
|
34
|
+
diagnostics: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
diagnostics: [
|
|
40
|
+
diagnostic("SUPA_PARSE_ERROR", "error", errorMessage(error), {
|
|
41
|
+
statement: sql,
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function loadParser() {
|
|
48
|
+
if (cachedParser !== undefined && cachedParser !== null) {
|
|
49
|
+
return cachedParser;
|
|
50
|
+
}
|
|
51
|
+
if (cachedParser === null) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const module = (await import("libpg-query"));
|
|
55
|
+
const parser = findParser(module);
|
|
56
|
+
cachedParser = parser ?? null;
|
|
57
|
+
return parser;
|
|
58
|
+
}
|
|
59
|
+
function withLocation(outcome, file) {
|
|
60
|
+
if (!file || outcome.diagnostics.length === 0) {
|
|
61
|
+
return outcome;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
...outcome,
|
|
65
|
+
diagnostics: outcome.diagnostics.map((item) => ({ ...item, file })),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function findParser(module) {
|
|
69
|
+
const candidates = [
|
|
70
|
+
module.parse,
|
|
71
|
+
module.parseQuery,
|
|
72
|
+
module.parseSync,
|
|
73
|
+
module.default?.parse,
|
|
74
|
+
module.default?.parseQuery,
|
|
75
|
+
module.default?.parseSync,
|
|
76
|
+
];
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
if (typeof candidate === "function") {
|
|
79
|
+
return candidate;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
function errorMessage(error) {
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
return error.message;
|
|
87
|
+
}
|
|
88
|
+
return String(error);
|
|
89
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SchemaObject } from "../core.js";
|
|
2
|
+
import type { AstNode } from "./ast.js";
|
|
3
|
+
export declare function builtinPublicDefault(kindPhrase: string): string[] | undefined;
|
|
4
|
+
/** A GRANT that restates the built-in default ACL is a semantic no-op. */
|
|
5
|
+
export declare function isBuiltinDefaultGrant(kindPhrase: string, grantee: string, privileges: string[]): boolean;
|
|
6
|
+
export interface GrantObjectInput {
|
|
7
|
+
grantee: string;
|
|
8
|
+
kindPhrase: string;
|
|
9
|
+
ordinal: number;
|
|
10
|
+
privileges: string[];
|
|
11
|
+
schema?: string | undefined;
|
|
12
|
+
targetIdentity: string;
|
|
13
|
+
targetRendered: string;
|
|
14
|
+
verb: "GRANT" | "REVOKE";
|
|
15
|
+
withGrantOption?: boolean;
|
|
16
|
+
file?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function buildGrantObject(input: GrantObjectInput): SchemaObject;
|
|
19
|
+
export interface DefaultPrivilegeObjectInput {
|
|
20
|
+
forRole?: string | undefined;
|
|
21
|
+
grantee: string;
|
|
22
|
+
objectType: string;
|
|
23
|
+
ordinal: number;
|
|
24
|
+
privileges: string[];
|
|
25
|
+
schema?: string | undefined;
|
|
26
|
+
verb: "GRANT" | "REVOKE";
|
|
27
|
+
file?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
export declare function buildDefaultPrivilegeObject(input: DefaultPrivilegeObjectInput): SchemaObject;
|
|
30
|
+
export declare function grantObjectsFromAst(node: AstNode, statement: string, ordinal: number, file?: string): SchemaObject[];
|
|
31
|
+
export declare function defaultPrivilegesFromAst(node: AstNode, statement: string, ordinal: number, file?: string): SchemaObject[];
|
|
32
|
+
export declare function commentObjectFromAst(node: AstNode, statement: string, ordinal: number, file?: string): SchemaObject | undefined;
|
|
33
|
+
//# sourceMappingURL=privileges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privileges.d.ts","sourceRoot":"","sources":["../../src/sql/privileges.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAmGxC,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAK7E;AAED,0EAA0E;AAC1E,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAcT;AAiBD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAqBtE;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,2BAA2B,GAAG,YAAY,CA6B5F;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,GACZ,YAAY,EAAE,CAqChB;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,GACZ,YAAY,EAAE,CAsChB;AAqBD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,GACZ,YAAY,GAAG,SAAS,CAgB1B"}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { sha256 } from "../hash.js";
|
|
2
|
+
import { asRecord, listItems, objectWithArgsIdentity, qualifiedName, rangeVarName, readArray, readBoolean, readString, roleSpecName, stringList, stringValue, typeNameToSql, } from "./ast.js";
|
|
3
|
+
import { formatQualifiedName, normalizeSql, quoteIdent } from "./identifiers.js";
|
|
4
|
+
import { makeObject } from "./statements.js";
|
|
5
|
+
const grantObjectKinds = new Map([
|
|
6
|
+
["OBJECT_DATABASE", "DATABASE"],
|
|
7
|
+
["OBJECT_DOMAIN", "DOMAIN"],
|
|
8
|
+
["OBJECT_FDW", "FOREIGN DATA WRAPPER"],
|
|
9
|
+
["OBJECT_FOREIGN_SERVER", "FOREIGN SERVER"],
|
|
10
|
+
["OBJECT_FUNCTION", "FUNCTION"],
|
|
11
|
+
["OBJECT_LANGUAGE", "LANGUAGE"],
|
|
12
|
+
["OBJECT_LARGEOBJECT", "LARGE OBJECT"],
|
|
13
|
+
["OBJECT_PROCEDURE", "PROCEDURE"],
|
|
14
|
+
["OBJECT_ROUTINE", "ROUTINE"],
|
|
15
|
+
["OBJECT_SCHEMA", "SCHEMA"],
|
|
16
|
+
["OBJECT_SEQUENCE", "SEQUENCE"],
|
|
17
|
+
["OBJECT_TABLE", "TABLE"],
|
|
18
|
+
["OBJECT_TABLESPACE", "TABLESPACE"],
|
|
19
|
+
["OBJECT_TYPE", "TYPE"],
|
|
20
|
+
]);
|
|
21
|
+
const allInSchemaKinds = new Map([
|
|
22
|
+
["OBJECT_FUNCTION", "ALL FUNCTIONS IN SCHEMA"],
|
|
23
|
+
["OBJECT_PROCEDURE", "ALL PROCEDURES IN SCHEMA"],
|
|
24
|
+
["OBJECT_ROUTINE", "ALL ROUTINES IN SCHEMA"],
|
|
25
|
+
["OBJECT_SEQUENCE", "ALL SEQUENCES IN SCHEMA"],
|
|
26
|
+
["OBJECT_TABLE", "ALL TABLES IN SCHEMA"],
|
|
27
|
+
]);
|
|
28
|
+
const defaultPrivilegeKinds = new Map([
|
|
29
|
+
["OBJECT_FUNCTION", "FUNCTIONS"],
|
|
30
|
+
["OBJECT_PROCEDURE", "ROUTINES"],
|
|
31
|
+
["OBJECT_ROUTINE", "ROUTINES"],
|
|
32
|
+
["OBJECT_SCHEMA", "SCHEMAS"],
|
|
33
|
+
["OBJECT_SEQUENCE", "SEQUENCES"],
|
|
34
|
+
["OBJECT_TABLE", "TABLES"],
|
|
35
|
+
["OBJECT_TYPE", "TYPES"],
|
|
36
|
+
]);
|
|
37
|
+
const fullPrivilegeSets = new Map([
|
|
38
|
+
["DATABASE", ["CONNECT", "CREATE", "TEMPORARY"]],
|
|
39
|
+
["DOMAIN", ["USAGE"]],
|
|
40
|
+
["FOREIGN DATA WRAPPER", ["USAGE"]],
|
|
41
|
+
["FOREIGN SERVER", ["USAGE"]],
|
|
42
|
+
["FUNCTION", ["EXECUTE"]],
|
|
43
|
+
["FUNCTIONS", ["EXECUTE"]],
|
|
44
|
+
["LANGUAGE", ["USAGE"]],
|
|
45
|
+
["LARGE OBJECT", ["SELECT", "UPDATE"]],
|
|
46
|
+
["PROCEDURE", ["EXECUTE"]],
|
|
47
|
+
["ROUTINE", ["EXECUTE"]],
|
|
48
|
+
["ROUTINES", ["EXECUTE"]],
|
|
49
|
+
["SCHEMA", ["CREATE", "USAGE"]],
|
|
50
|
+
["SCHEMAS", ["CREATE", "USAGE"]],
|
|
51
|
+
["SEQUENCE", ["SELECT", "UPDATE", "USAGE"]],
|
|
52
|
+
["SEQUENCES", ["SELECT", "UPDATE", "USAGE"]],
|
|
53
|
+
["TABLE", ["DELETE", "INSERT", "REFERENCES", "SELECT", "TRIGGER", "TRUNCATE", "UPDATE"]],
|
|
54
|
+
["TABLES", ["DELETE", "INSERT", "REFERENCES", "SELECT", "TRIGGER", "TRUNCATE", "UPDATE"]],
|
|
55
|
+
["TABLESPACE", ["CREATE"]],
|
|
56
|
+
["TYPE", ["USAGE"]],
|
|
57
|
+
["TYPES", ["USAGE"]],
|
|
58
|
+
]);
|
|
59
|
+
// PostgreSQL's built-in default ACL (acldefault): PUBLIC implicitly holds
|
|
60
|
+
// EXECUTE on routines and USAGE on types/domains/languages. pg_dump only
|
|
61
|
+
// dumps ACL deltas against these defaults; both supaschema lanes do the
|
|
62
|
+
// same so an explicit grant on one object does not surface the materialized
|
|
63
|
+
// default entries as drift.
|
|
64
|
+
const builtinPublicDefaults = new Map([
|
|
65
|
+
["DOMAIN", ["USAGE"]],
|
|
66
|
+
["FUNCTION", ["EXECUTE"]],
|
|
67
|
+
["FUNCTIONS", ["EXECUTE"]],
|
|
68
|
+
["LANGUAGE", ["USAGE"]],
|
|
69
|
+
["PROCEDURE", ["EXECUTE"]],
|
|
70
|
+
["ROUTINE", ["EXECUTE"]],
|
|
71
|
+
["ROUTINES", ["EXECUTE"]],
|
|
72
|
+
["TYPE", ["USAGE"]],
|
|
73
|
+
["TYPES", ["USAGE"]],
|
|
74
|
+
]);
|
|
75
|
+
export function builtinPublicDefault(kindPhrase) {
|
|
76
|
+
const lookupKey = kindPhrase.startsWith("ALL ")
|
|
77
|
+
? (kindPhrase.split(" ")[1] ?? kindPhrase)
|
|
78
|
+
: kindPhrase;
|
|
79
|
+
return builtinPublicDefaults.get(lookupKey);
|
|
80
|
+
}
|
|
81
|
+
/** A GRANT that restates the built-in default ACL is a semantic no-op. */
|
|
82
|
+
export function isBuiltinDefaultGrant(kindPhrase, grantee, privileges) {
|
|
83
|
+
if (grantee !== "PUBLIC") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const defaults = builtinPublicDefault(kindPhrase);
|
|
87
|
+
if (!defaults) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const normalized = normalizePrivileges([...privileges].sort(), kindPhrase);
|
|
91
|
+
return (normalized.join(",") === "ALL" ||
|
|
92
|
+
normalized.join(",") === defaults.join(",") ||
|
|
93
|
+
normalizePrivileges(defaults, kindPhrase).join(",") === normalized.join(","));
|
|
94
|
+
}
|
|
95
|
+
function normalizePrivileges(privileges, kindPhrase) {
|
|
96
|
+
const lookupKey = kindPhrase.startsWith("ALL ")
|
|
97
|
+
? (kindPhrase.split(" ")[1] ?? kindPhrase)
|
|
98
|
+
: kindPhrase;
|
|
99
|
+
const fullSet = fullPrivilegeSets.get(lookupKey);
|
|
100
|
+
if (!fullSet) {
|
|
101
|
+
return privileges;
|
|
102
|
+
}
|
|
103
|
+
const granted = new Set(privileges);
|
|
104
|
+
if (granted.has("ALL") || fullSet.every((privilege) => granted.has(privilege))) {
|
|
105
|
+
return ["ALL"];
|
|
106
|
+
}
|
|
107
|
+
return privileges;
|
|
108
|
+
}
|
|
109
|
+
export function buildGrantObject(input) {
|
|
110
|
+
const keyword = input.verb === "GRANT" ? "TO" : "FROM";
|
|
111
|
+
const suffix = input.withGrantOption ? " WITH GRANT OPTION" : "";
|
|
112
|
+
const privileges = normalizePrivileges(input.privileges, input.kindPhrase);
|
|
113
|
+
const canonicalSql = `${input.verb} ${privileges.join(", ")} ON ${input.kindPhrase} ${input.targetRendered} ${keyword} ${renderRole(input.grantee)}${suffix}`;
|
|
114
|
+
const ref = {
|
|
115
|
+
kind: "grant",
|
|
116
|
+
name: `${input.verb.toLowerCase()}:${input.kindPhrase.toLowerCase().replaceAll(" ", "-")}:${input.targetIdentity}:${input.grantee}`,
|
|
117
|
+
};
|
|
118
|
+
if (input.schema) {
|
|
119
|
+
ref.schema = input.schema;
|
|
120
|
+
}
|
|
121
|
+
return makeObject(ref, canonicalSql, input.ordinal, input.file, {
|
|
122
|
+
grantee: input.grantee,
|
|
123
|
+
kindPhrase: input.kindPhrase,
|
|
124
|
+
privileges,
|
|
125
|
+
target: input.targetRendered,
|
|
126
|
+
targetIdentity: input.targetIdentity,
|
|
127
|
+
verb: input.verb,
|
|
128
|
+
withGrantOption: input.withGrantOption === true,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function buildDefaultPrivilegeObject(input) {
|
|
132
|
+
// The identity deliberately excludes FOR ROLE: a statement without it
|
|
133
|
+
// records under whichever role executes the migration, so the executor
|
|
134
|
+
// role cannot be part of cross-lane identity. forRole stays in metadata
|
|
135
|
+
// for excludedGrantRoles filtering and SQL rendering.
|
|
136
|
+
const scope = input.schema ? `in ${input.schema}` : "";
|
|
137
|
+
const privileges = normalizePrivileges(input.privileges, input.objectType);
|
|
138
|
+
const clauses = [
|
|
139
|
+
"ALTER DEFAULT PRIVILEGES",
|
|
140
|
+
input.forRole ? `FOR ROLE ${renderRole(input.forRole)}` : "",
|
|
141
|
+
input.schema ? `IN SCHEMA ${quoteIdent(input.schema)}` : "",
|
|
142
|
+
`${input.verb} ${privileges.join(", ")} ON ${input.objectType}`,
|
|
143
|
+
`${input.verb === "GRANT" ? "TO" : "FROM"} ${renderRole(input.grantee)}`,
|
|
144
|
+
].filter(Boolean);
|
|
145
|
+
const ref = {
|
|
146
|
+
kind: "default-privilege",
|
|
147
|
+
name: `${input.verb.toLowerCase()}:${scope || "global"}:${input.objectType.toLowerCase()}:${input.grantee}`,
|
|
148
|
+
};
|
|
149
|
+
if (input.schema) {
|
|
150
|
+
ref.schema = input.schema;
|
|
151
|
+
}
|
|
152
|
+
return makeObject(ref, clauses.join(" "), input.ordinal, input.file, {
|
|
153
|
+
forRole: input.forRole,
|
|
154
|
+
grantee: input.grantee,
|
|
155
|
+
objectType: input.objectType,
|
|
156
|
+
privileges,
|
|
157
|
+
schema: input.schema,
|
|
158
|
+
verb: input.verb,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export function grantObjectsFromAst(node, statement, ordinal, file) {
|
|
162
|
+
const isGrant = readBoolean(node.is_grant);
|
|
163
|
+
const verb = isGrant ? "GRANT" : "REVOKE";
|
|
164
|
+
const objtype = readString(node.objtype) ?? "OBJECT_TABLE";
|
|
165
|
+
const allInSchema = readString(node.targtype) === "ACL_TARGET_ALL_IN_SCHEMA";
|
|
166
|
+
const kindPhrase = allInSchema ? allInSchemaKinds.get(objtype) : grantObjectKinds.get(objtype);
|
|
167
|
+
const privileges = grantPrivileges(node.privileges);
|
|
168
|
+
const grantees = readArray(node.grantees)
|
|
169
|
+
.map((item) => roleSpecName(item))
|
|
170
|
+
.filter((role) => role !== undefined);
|
|
171
|
+
const targets = readArray(node.objects)
|
|
172
|
+
.map((item) => grantTarget(item, objtype, allInSchema))
|
|
173
|
+
.filter((target) => target !== undefined);
|
|
174
|
+
if (!kindPhrase || grantees.length === 0 || targets.length === 0) {
|
|
175
|
+
return [fallbackPrivilegeObject("grant", statement, ordinal, file)];
|
|
176
|
+
}
|
|
177
|
+
const withGrantOption = isGrant && readBoolean(node.grant_option);
|
|
178
|
+
const objects = [];
|
|
179
|
+
for (const target of targets) {
|
|
180
|
+
for (const grantee of grantees) {
|
|
181
|
+
objects.push(buildGrantObject({
|
|
182
|
+
file,
|
|
183
|
+
grantee,
|
|
184
|
+
kindPhrase,
|
|
185
|
+
ordinal: ordinal + objects.length,
|
|
186
|
+
privileges,
|
|
187
|
+
schema: target.schema,
|
|
188
|
+
targetIdentity: target.identity,
|
|
189
|
+
targetRendered: target.rendered,
|
|
190
|
+
verb,
|
|
191
|
+
withGrantOption,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return objects;
|
|
196
|
+
}
|
|
197
|
+
export function defaultPrivilegesFromAst(node, statement, ordinal, file) {
|
|
198
|
+
const action = asRecord(node.action);
|
|
199
|
+
if (!action) {
|
|
200
|
+
return [fallbackPrivilegeObject("default-privilege", statement, ordinal, file)];
|
|
201
|
+
}
|
|
202
|
+
const isGrant = readBoolean(action.is_grant);
|
|
203
|
+
const verb = isGrant ? "GRANT" : "REVOKE";
|
|
204
|
+
const objectType = defaultPrivilegeKinds.get(readString(action.objtype) ?? "OBJECT_TABLE");
|
|
205
|
+
const privileges = grantPrivileges(action.privileges);
|
|
206
|
+
const grantees = readArray(action.grantees)
|
|
207
|
+
.map((item) => roleSpecName(item))
|
|
208
|
+
.filter((role) => role !== undefined);
|
|
209
|
+
const { forRoles, schemas } = defaultPrivilegeScope(node.options);
|
|
210
|
+
if (!objectType || grantees.length === 0) {
|
|
211
|
+
return [fallbackPrivilegeObject("default-privilege", statement, ordinal, file)];
|
|
212
|
+
}
|
|
213
|
+
const roleScopes = forRoles.length > 0 ? forRoles : [undefined];
|
|
214
|
+
const schemaScopes = schemas.length > 0 ? schemas : [undefined];
|
|
215
|
+
const objects = [];
|
|
216
|
+
for (const forRole of roleScopes) {
|
|
217
|
+
for (const schema of schemaScopes) {
|
|
218
|
+
for (const grantee of grantees) {
|
|
219
|
+
objects.push(buildDefaultPrivilegeObject({
|
|
220
|
+
file,
|
|
221
|
+
forRole,
|
|
222
|
+
grantee,
|
|
223
|
+
objectType,
|
|
224
|
+
ordinal: ordinal + objects.length,
|
|
225
|
+
privileges,
|
|
226
|
+
schema,
|
|
227
|
+
verb,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return objects;
|
|
233
|
+
}
|
|
234
|
+
const commentObjectKinds = new Map([
|
|
235
|
+
["OBJECT_COLUMN", "column"],
|
|
236
|
+
["OBJECT_DOMAIN", "domain"],
|
|
237
|
+
["OBJECT_EXTENSION", "extension"],
|
|
238
|
+
["OBJECT_FOREIGN_TABLE", "foreign table"],
|
|
239
|
+
["OBJECT_FUNCTION", "function"],
|
|
240
|
+
["OBJECT_INDEX", "index"],
|
|
241
|
+
["OBJECT_MATVIEW", "materialized view"],
|
|
242
|
+
["OBJECT_POLICY", "policy"],
|
|
243
|
+
["OBJECT_PROCEDURE", "procedure"],
|
|
244
|
+
["OBJECT_SCHEMA", "schema"],
|
|
245
|
+
["OBJECT_SEQUENCE", "sequence"],
|
|
246
|
+
["OBJECT_TABCONSTRAINT", "constraint"],
|
|
247
|
+
["OBJECT_TABLE", "table"],
|
|
248
|
+
["OBJECT_TRIGGER", "trigger"],
|
|
249
|
+
["OBJECT_TYPE", "type"],
|
|
250
|
+
["OBJECT_VIEW", "view"],
|
|
251
|
+
]);
|
|
252
|
+
export function commentObjectFromAst(node, statement, ordinal, file) {
|
|
253
|
+
const objtype = readString(node.objtype);
|
|
254
|
+
const kindWord = objtype ? commentObjectKinds.get(objtype) : undefined;
|
|
255
|
+
if (!kindWord) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
const target = commentTarget(node.object, objtype ?? "");
|
|
259
|
+
if (!target) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
const descriptor = `${kindWord} ${target.identity}`;
|
|
263
|
+
const ref = { kind: "comment", name: sha256(descriptor).slice(0, 16) };
|
|
264
|
+
if (target.schema) {
|
|
265
|
+
ref.schema = target.schema;
|
|
266
|
+
}
|
|
267
|
+
return makeObject(ref, statement, ordinal, file, { descriptor });
|
|
268
|
+
}
|
|
269
|
+
function commentTarget(object, objtype) {
|
|
270
|
+
if (objtype === "OBJECT_FUNCTION" || objtype === "OBJECT_PROCEDURE") {
|
|
271
|
+
const identity = objectWithArgsIdentity(object);
|
|
272
|
+
if (!identity) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
identity: `${identity.schema}.${identity.name}(${identity.signature})`,
|
|
277
|
+
schema: identity.schema,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (objtype === "OBJECT_TYPE" || objtype === "OBJECT_DOMAIN") {
|
|
281
|
+
const typeNode = asRecord(object);
|
|
282
|
+
const named = qualifiedName(asRecord(typeNode?.TypeName)?.names ?? typeNode?.names);
|
|
283
|
+
if (named) {
|
|
284
|
+
return { identity: `${named.schema}.${named.name}`, schema: named.schema };
|
|
285
|
+
}
|
|
286
|
+
return { identity: normalizeSql(typeNameToSql(object)) };
|
|
287
|
+
}
|
|
288
|
+
// Parse-tree identifier parts are already canonical (unquoted names are
|
|
289
|
+
// folded, quoted case is preserved); lowercasing here would corrupt quoted
|
|
290
|
+
// mixed-case identities and break cross-lane comment parity.
|
|
291
|
+
const parts = stringList(object);
|
|
292
|
+
if (parts.length === 0) {
|
|
293
|
+
const single = stringValue(object) ?? readString(object);
|
|
294
|
+
return single ? { identity: single } : undefined;
|
|
295
|
+
}
|
|
296
|
+
if (objtype === "OBJECT_SCHEMA") {
|
|
297
|
+
return { identity: parts.join(".") };
|
|
298
|
+
}
|
|
299
|
+
if (parts.length >= 2) {
|
|
300
|
+
return { identity: parts.join("."), schema: parts[0] ?? "" };
|
|
301
|
+
}
|
|
302
|
+
return { identity: `public.${parts[0] ?? ""}`, schema: "public" };
|
|
303
|
+
}
|
|
304
|
+
function grantPrivileges(value) {
|
|
305
|
+
const privileges = readArray(value)
|
|
306
|
+
.map((item) => readString(asRecord(asRecord(item)?.AccessPriv)?.priv_name))
|
|
307
|
+
.filter((name) => name !== undefined)
|
|
308
|
+
.map((name) => name.toUpperCase())
|
|
309
|
+
.sort((left, right) => left.localeCompare(right));
|
|
310
|
+
return privileges.length > 0 ? privileges : ["ALL"];
|
|
311
|
+
}
|
|
312
|
+
function grantTarget(value, objtype, allInSchema) {
|
|
313
|
+
if (allInSchema || objtype === "OBJECT_SCHEMA") {
|
|
314
|
+
const schema = stringValue(value) ?? readString(asRecord(value)?.sval);
|
|
315
|
+
if (!schema) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
return { identity: schema, rendered: quoteIdent(schema), schema };
|
|
319
|
+
}
|
|
320
|
+
if (objtype === "OBJECT_FUNCTION" ||
|
|
321
|
+
objtype === "OBJECT_PROCEDURE" ||
|
|
322
|
+
objtype === "OBJECT_ROUTINE") {
|
|
323
|
+
const identity = objectWithArgsIdentity(value);
|
|
324
|
+
if (!identity) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
identity: `${identity.schema}.${identity.name}(${identity.signature})`,
|
|
329
|
+
rendered: `${formatQualifiedName(identity.schema, identity.name)}(${identity.signature})`,
|
|
330
|
+
schema: identity.schema,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const range = rangeVarName(value);
|
|
334
|
+
if (range) {
|
|
335
|
+
return {
|
|
336
|
+
identity: `${range.schema}.${range.name}`,
|
|
337
|
+
rendered: formatQualifiedName(range.schema, range.name),
|
|
338
|
+
schema: range.schema,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const named = qualifiedName(value);
|
|
342
|
+
if (named) {
|
|
343
|
+
return {
|
|
344
|
+
identity: `${named.schema}.${named.name}`,
|
|
345
|
+
rendered: formatQualifiedName(named.schema, named.name),
|
|
346
|
+
schema: named.schema,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
function defaultPrivilegeScope(options) {
|
|
352
|
+
const forRoles = [];
|
|
353
|
+
const schemas = [];
|
|
354
|
+
for (const item of readArray(options)) {
|
|
355
|
+
const defElem = asRecord(asRecord(item)?.DefElem);
|
|
356
|
+
const name = readString(defElem?.defname);
|
|
357
|
+
if (name === "roles") {
|
|
358
|
+
for (const role of listItems(defElem?.arg)) {
|
|
359
|
+
const roleName = roleSpecName(role);
|
|
360
|
+
if (roleName) {
|
|
361
|
+
forRoles.push(roleName);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (name === "schemas") {
|
|
366
|
+
for (const schema of stringList(defElem?.arg)) {
|
|
367
|
+
schemas.push(schema.toLowerCase());
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { forRoles, schemas };
|
|
372
|
+
}
|
|
373
|
+
function renderRole(role) {
|
|
374
|
+
return role === "PUBLIC" ? "PUBLIC" : quoteIdent(role);
|
|
375
|
+
}
|
|
376
|
+
function fallbackPrivilegeObject(kind, statement, ordinal, file) {
|
|
377
|
+
const name = sha256(normalizeSql(statement)).slice(0, 16);
|
|
378
|
+
return makeObject({ kind, name }, statement, ordinal, file);
|
|
379
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../src/sql/split.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwFxD;AA0BD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,MAAM,EAAE,CAqEtE"}
|