@intentius/chant-lexicon-sql 0.124.19 → 0.124.21
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/clickhouse/access.d.ts +36 -7
- package/dist/clickhouse/access.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +60 -0
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts +1 -7
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/live/catalog.d.ts +6 -5
- package/dist/clickhouse/live/catalog.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/parser.d.ts +16 -0
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +6 -3
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +3 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/topology.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/coverage/clickhouse.json +8 -8
- package/dist/coverage/postgres.json +6 -6
- package/dist/files/clickhouse.d.ts +2 -1
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +10 -10
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/sql101.d.ts.map +1 -1
- package/dist/lint/rules/sqlch001.d.ts +1 -1
- package/dist/lint/rules/sqlch001.d.ts.map +1 -1
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lint/rules/templates.d.ts +1 -1
- package/dist/lint/rules/templates.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 +40 -0
- package/dist/okf/index.md +9 -1
- package/dist/okf/rules/SQL101.md +1 -0
- package/dist/okf/rules/SQLCH001.md +2 -2
- package/dist/okf/rules/SQLPG113.md +2 -0
- package/dist/okf/types/PostgresAggregate.md +13 -0
- package/dist/okf/types/PostgresCast.md +9 -0
- package/dist/okf/types/PostgresCollation.md +9 -0
- package/dist/okf/types/PostgresCompositeType.md +13 -0
- package/dist/okf/types/PostgresEventTrigger.md +9 -0
- package/dist/okf/types/PostgresRangeType.md +13 -0
- package/dist/okf/types/Quota.md +9 -0
- package/dist/okf/types/SettingsProfile.md +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +4 -3
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +129 -6
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +6 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +3 -2
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/live/catalog-kinds.d.ts +28 -0
- package/dist/postgres/live/catalog-kinds.d.ts.map +1 -0
- package/dist/postgres/live/catalog.d.ts +15 -11
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +88 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +17 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +3 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +3 -2
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
- package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-names.ts +15 -2
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sql101.ts +1 -0
- package/dist/rules/sqlch001.ts +10 -3
- package/dist/rules/sqlpg001.ts +6 -21
- package/dist/rules/sqlpg113.ts +1 -1
- package/dist/rules/templates.ts +2 -2
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/access.ts +197 -22
- package/src/clickhouse/apply/statements.ts +17 -5
- package/src/clickhouse/entities.ts +78 -2
- package/src/clickhouse/import/generator.ts +16 -3
- package/src/clickhouse/live/catalog.ts +57 -10
- package/src/clickhouse/live/describe-resources.ts +3 -1
- package/src/clickhouse/live/kinds.e2e.test.ts +179 -0
- package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -4
- package/src/clickhouse/parser.test.ts +45 -2
- package/src/clickhouse/parser.ts +111 -3
- package/src/clickhouse/plan/deep.ts +25 -6
- package/src/clickhouse/plan/diff.ts +41 -1
- package/src/clickhouse/plan/normalize.ts +12 -4
- package/src/clickhouse/plan/rules.ts +22 -1
- package/src/clickhouse/quota-profile.test.ts +186 -0
- package/src/clickhouse/table-function.test.ts +54 -0
- package/src/clickhouse/testdata/server-ddl-baseline.json +17 -14
- package/src/clickhouse/topology.ts +4 -2
- package/src/clickhouse.ts +6 -0
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +8 -0
- package/src/core/undeclarable.test.ts +22 -19
- package/src/coverage/clickhouse.json +8 -8
- package/src/coverage/postgres.json +6 -6
- package/src/files/clickhouse.ts +14 -6
- package/src/files/postgres.ts +19 -5
- package/src/generated/lexicon-sql.json +40 -0
- package/src/index.ts +2 -2
- package/src/lint/post-synth/postgres-names.ts +15 -2
- package/src/lint/post-synth/sql101.ts +1 -0
- package/src/lint/post-synth/sqlpg113.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlch001.ts +10 -3
- package/src/lint/rules/sqlpg001.ts +6 -21
- package/src/lint/rules/templates.ts +2 -2
- package/src/lsp/postgres-template.ts +14 -0
- package/src/mcp/index.ts +5 -5
- package/src/mcp/postgres.ts +1 -1
- package/src/plugin.ts +7 -1
- package/src/postgres/apply/apply.ts +8 -2
- package/src/postgres/apply/statements.ts +29 -6
- package/src/postgres/entities.test.ts +2 -2
- package/src/postgres/entities.ts +316 -28
- package/src/postgres/entity-types.ts +6 -0
- package/src/postgres/import/generator.ts +31 -4
- package/src/postgres/kinds.test.ts +140 -0
- package/src/postgres/live/catalog-kinds.ts +312 -0
- package/src/postgres/live/catalog.ts +30 -12
- package/src/postgres/live/describe-resources.ts +14 -5
- package/src/postgres/live/kinds.e2e.test.ts +182 -0
- package/src/postgres/live/roundtrip.e2e.test.ts +9 -3
- package/src/postgres/live/undeclarable.e2e.test.ts +15 -13
- package/src/postgres/parser.test.ts +2 -1
- package/src/postgres/parser.ts +307 -17
- package/src/postgres/plan/deep.ts +33 -0
- package/src/postgres/plan/diff.ts +45 -0
- package/src/postgres/plan/normalize.ts +153 -2
- package/src/postgres/plan/rules.ts +21 -0
- package/src/postgres/plan/schema.ts +6 -4
- package/src/postgres/testing/roundtrip-schema.ts +19 -1
- package/src/postgres.ts +19 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +7 -3
- package/src/skills/chant-sql.md +3 -1
- package/src/unknown-clauses.test.ts +2 -1
- package/src/validate.ts +8 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The import IR as TypeScript: one `schema.ts` holding every imported object
|
|
3
|
-
* as a `schema`, `table`, `index`, `view`, `sequence`, `type`, `domain
|
|
4
|
-
* `extension`
|
|
3
|
+
* as a `schema`, `table`, `index`, `view`, `sequence`, `type`, `domain`,
|
|
4
|
+
* `extension`, `func`, `aggregate`, `collation`, `cast`, `eventTrigger` (and
|
|
5
|
+
* the rest of the tags) declaration, in dependency order, with the references between
|
|
5
6
|
* them interpolated, so the build orders them as the server did:
|
|
6
7
|
*
|
|
7
8
|
* - a qualified name of another imported object becomes `${export}`
|
|
@@ -52,7 +53,13 @@ const TAG: Record<string, string> = {
|
|
|
52
53
|
[POSTGRES_ENTITY_TYPES.materializedView]: "view",
|
|
53
54
|
[POSTGRES_ENTITY_TYPES.sequence]: "sequence",
|
|
54
55
|
[POSTGRES_ENTITY_TYPES.enum]: "type",
|
|
56
|
+
[POSTGRES_ENTITY_TYPES.composite]: "type",
|
|
57
|
+
[POSTGRES_ENTITY_TYPES.range]: "type",
|
|
55
58
|
[POSTGRES_ENTITY_TYPES.domain]: "domain",
|
|
59
|
+
[POSTGRES_ENTITY_TYPES.aggregate]: "aggregate",
|
|
60
|
+
[POSTGRES_ENTITY_TYPES.collation]: "collation",
|
|
61
|
+
[POSTGRES_ENTITY_TYPES.cast]: "cast",
|
|
62
|
+
[POSTGRES_ENTITY_TYPES.eventTrigger]: "eventTrigger",
|
|
56
63
|
[POSTGRES_ENTITY_TYPES.extension]: "extension",
|
|
57
64
|
[POSTGRES_ENTITY_TYPES.function]: "func",
|
|
58
65
|
[POSTGRES_ENTITY_TYPES.procedure]: "procedure",
|
|
@@ -63,6 +70,19 @@ const TAG: Record<string, string> = {
|
|
|
63
70
|
[POSTGRES_ENTITY_TYPES.defaultPrivileges]: "grant",
|
|
64
71
|
};
|
|
65
72
|
|
|
73
|
+
/** The kinds no other object's DDL names. */
|
|
74
|
+
const UNREFERENCED: ReadonlySet<string> = new Set([
|
|
75
|
+
POSTGRES_ENTITY_TYPES.index,
|
|
76
|
+
POSTGRES_ENTITY_TYPES.trigger,
|
|
77
|
+
POSTGRES_ENTITY_TYPES.policy,
|
|
78
|
+
POSTGRES_ENTITY_TYPES.grant,
|
|
79
|
+
POSTGRES_ENTITY_TYPES.defaultPrivileges,
|
|
80
|
+
POSTGRES_ENTITY_TYPES.role,
|
|
81
|
+
POSTGRES_ENTITY_TYPES.extension,
|
|
82
|
+
POSTGRES_ENTITY_TYPES.cast,
|
|
83
|
+
POSTGRES_ENTITY_TYPES.eventTrigger,
|
|
84
|
+
]);
|
|
85
|
+
|
|
66
86
|
/** The tag each entity type is declared with (`table`, `func`, ...). */
|
|
67
87
|
export const POSTGRES_TAG_OF: Readonly<Record<string, string>> = TAG;
|
|
68
88
|
|
|
@@ -104,6 +124,13 @@ function bareSpots(tokens: Token[]): Set<number> {
|
|
|
104
124
|
for (const node of nodes) {
|
|
105
125
|
if (node.statement === "table") for (const c of node.columns) if (c.type) first(c.type.from, c.type.to);
|
|
106
126
|
if (node.statement === "domain") first(node.type.from, node.type.to);
|
|
127
|
+
if (node.statement === "composite") for (const a of node.attributes) first(a.type.from, a.type.to);
|
|
128
|
+
if (node.statement === "cast") {
|
|
129
|
+
first(node.source.from, node.source.to);
|
|
130
|
+
first(node.target.from, node.target.to);
|
|
131
|
+
name(node.function);
|
|
132
|
+
}
|
|
133
|
+
if (node.statement === "eventTrigger") name(node.function);
|
|
107
134
|
if (node.statement === "index" || node.statement === "policy") name(node.table);
|
|
108
135
|
if (node.statement === "trigger") {
|
|
109
136
|
name(node.table);
|
|
@@ -202,8 +229,8 @@ export function postgresTemplates(items: readonly Item[], targets: readonly Item
|
|
|
202
229
|
const schemaExport = new Map<string, string>();
|
|
203
230
|
for (const it of targets) {
|
|
204
231
|
if (it.type === POSTGRES_ENTITY_TYPES.schema) schemaExport.set(it.name, it.exportName);
|
|
205
|
-
// An index
|
|
206
|
-
else if (
|
|
232
|
+
// An index, a trigger, a cast or an event trigger is never referenced by name in another object's DDL.
|
|
233
|
+
else if (!UNREFERENCED.has(it.type)) {
|
|
207
234
|
byQualified.set(it.schema ? `${it.schema}.${it.name}` : it.name, it);
|
|
208
235
|
}
|
|
209
236
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite and range types, aggregates, collations, casts and event triggers
|
|
3
|
+
* (#3911): what the tags parse and refuse, how a change to each is
|
|
4
|
+
* classified and made, and how an import writes them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { aggregate, cast, collation, eventTrigger, func, schema, SqlTemplateError, type } from "./entities";
|
|
9
|
+
import { diffObject, keyedByQualifiedName, pgSchemaFromLive, type PgSchemaObject } from "./plan/schema";
|
|
10
|
+
import { diffPgSchemas, matchPgObjects } from "./plan/diff";
|
|
11
|
+
import { alterSteps, dropStatement } from "./apply/statements";
|
|
12
|
+
import { declaredObjects } from "./apply/apply";
|
|
13
|
+
import type { LivePgObject } from "./live/catalog";
|
|
14
|
+
import { objectsToIR } from "./import/ir";
|
|
15
|
+
import { PostgresGenerator } from "./import/generator";
|
|
16
|
+
import { canonicalPgObject } from "./plan/normalize";
|
|
17
|
+
|
|
18
|
+
const app = schema`CREATE SCHEMA app`;
|
|
19
|
+
|
|
20
|
+
describe("the tags", () => {
|
|
21
|
+
test("type declares enum, composite and range types", () => {
|
|
22
|
+
const pair = type`CREATE TYPE ${app}.pair AS (a int, b text COLLATE "C"); COMMENT ON TYPE ${app}.pair IS 'Two'`;
|
|
23
|
+
expect(pair.entityType).toBe("Postgres::CompositeType");
|
|
24
|
+
expect(pair.props).toMatchObject({ schema: "app", name: "pair", attributes: [{ name: "a", type: "int" }, { name: "b", type: "text", collate: '"C"' }], comment: "Two" });
|
|
25
|
+
const range = type`CREATE TYPE ${app}.floatrange AS RANGE (SUBTYPE = float8, SUBTYPE_DIFF = float8mi)`;
|
|
26
|
+
expect(range.entityType).toBe("Postgres::RangeType");
|
|
27
|
+
expect(canonicalPgObject(range.entityType, range.props as unknown as Record<string, unknown>).fields).toMatchObject({ subtype: "double precision", subtypeDiff: "float8mi", multirange: "app.floatmultirange" });
|
|
28
|
+
expect(type`CREATE TYPE ${app}.mood AS ENUM ('ok')`.entityType).toBe("Postgres::Enum");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("an aggregate is identified by its argument types; its options compare canonical", () => {
|
|
32
|
+
const add = func`CREATE FUNCTION ${app}.add2(a int, b int) RETURNS int LANGUAGE sql AS $$ SELECT a + b $$`;
|
|
33
|
+
const total = aggregate`CREATE AGGREGATE ${app}.total(int) (SFUNC = ${add}, STYPE = int4, INITCOND = 0, PARALLEL = UNSAFE)`;
|
|
34
|
+
expect(total.dependsOn).toContain(add);
|
|
35
|
+
const c = canonicalPgObject(total.entityType, total.props as unknown as Record<string, unknown>);
|
|
36
|
+
expect(c.signature).toBe("(integer)");
|
|
37
|
+
expect(c.fields.definition).toEqual({ initcond: "0", sfunc: "app.add2", stype: "integer" });
|
|
38
|
+
const median = aggregate`CREATE AGGREGATE app.med(ORDER BY float8) (SFUNC = pg_catalog.array_append, STYPE = float8[])`;
|
|
39
|
+
expect(canonicalPgObject(median.entityType, median.props as unknown as Record<string, unknown>).signature).toBe("(ORDER BY double precision)");
|
|
40
|
+
expect(canonicalPgObject("Postgres::Aggregate", aggregate`CREATE AGGREGATE app.n(*) (SFUNC = int8inc, STYPE = int8)`.props as unknown as Record<string, unknown>).signature).toBe("(*)");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a collation, a cast and an event trigger", () => {
|
|
44
|
+
const ci = collation`CREATE COLLATION ${app}.ci (provider = icu, locale = 'und-u-ks-level2', deterministic = false)`;
|
|
45
|
+
expect(canonicalPgObject(ci.entityType, ci.props as unknown as Record<string, unknown>).fields).toMatchObject({ provider: "icu", locale: "und-u-ks-level2", nondeterministic: true });
|
|
46
|
+
const c2 = collation`CREATE COLLATION app.c2 (LOCALE = 'C')`;
|
|
47
|
+
expect(canonicalPgObject(c2.entityType, c2.props as unknown as Record<string, unknown>).fields).toMatchObject({ provider: "libc", lcCollate: "C", lcCtype: "C" });
|
|
48
|
+
|
|
49
|
+
const pair = type`CREATE TYPE ${app}.pair AS (a int)`;
|
|
50
|
+
const k = cast`CREATE CAST (${pair} AS text) WITH INOUT AS ASSIGNMENT; COMMENT ON CAST (${pair} AS text) IS 'x'`;
|
|
51
|
+
expect(k.sqlName).toBe("(app.pair AS text)");
|
|
52
|
+
expect(k.props).toMatchObject({ sourceType: "app.pair", targetType: "text", method: "inout", context: "assignment", comment: "x" });
|
|
53
|
+
expect(k.dependsOn).toEqual([pair]);
|
|
54
|
+
|
|
55
|
+
const fn = func`CREATE FUNCTION ${app}.audit() RETURNS event_trigger LANGUAGE plpgsql AS $$ BEGIN END $$`;
|
|
56
|
+
const et = eventTrigger`CREATE EVENT TRIGGER audit ON ddl_command_end WHEN TAG IN ('CREATE TABLE', 'alter table') EXECUTE FUNCTION ${fn}()`;
|
|
57
|
+
expect(canonicalPgObject(et.entityType, et.props as unknown as Record<string, unknown>).fields).toMatchObject({ event: "ddl_command_end", tags: ["ALTER TABLE", "CREATE TABLE"], function: "app.audit" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("what is refused", () => {
|
|
61
|
+
expect(() => type`CREATE TYPE app.later`).toThrow(/shell type/);
|
|
62
|
+
expect(() => type`CREATE TYPE app.t (INPUT = t_in, OUTPUT = t_out)`).toThrow(/base type/);
|
|
63
|
+
expect(() => aggregate`CREATE AGGREGATE app.a (BASETYPE = int, SFUNC = f, STYPE = int)`).toThrow(/old form/);
|
|
64
|
+
expect(() => aggregate`CREATE AGGREGATE app.a(int) (SFUNC = f)`).toThrow(/expected STYPE/);
|
|
65
|
+
expect(() => aggregate`CREATE AGGREGATE app.a(OUT int) (SFUNC = f, STYPE = int)`).toThrow(/IN or VARIADIC/);
|
|
66
|
+
expect(() => collation`CREATE COLLATION app.c FROM "C"`).toThrow(/FROM copies/);
|
|
67
|
+
expect(() => collation`CREATE COLLATION app.c (lokale = 'C')`).toThrow(/an option of a collation/);
|
|
68
|
+
expect(() => type`CREATE TYPE app.r AS RANGE (SUBTYPE_DIFF = f)`).toThrow(/expected SUBTYPE/);
|
|
69
|
+
expect(() => eventTrigger`CREATE EVENT TRIGGER app.e ON ddl_command_end EXECUTE FUNCTION f()`).toThrow(/not schema-qualified/);
|
|
70
|
+
expect(() => cast`CREATE CAST (int AS text) WITH INOUT; COMMENT ON CAST (int AS bigint) IS 'x'`).toThrow(/another cast/);
|
|
71
|
+
expect(() => collation`CREATE TABLE app.t (a int)`).toThrow(SqlTemplateError);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── Classifying and making changes ─────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const live = (type: string, statement: string, extra: Partial<LivePgObject> = {}) => pgSchemaFromLive([{ type, statement, name: "", oid: "1", ...extra } as LivePgObject], "public");
|
|
78
|
+
const declared = (type: string, ddl: string): PgSchemaObject[] => keyedByQualifiedName([{ key: "x", canonical: { ...diffObject(type, ddl, "public"), exportName: "x" } }]);
|
|
79
|
+
const rules = (type: string, before: string, after: string, extra: Partial<LivePgObject> = {}) =>
|
|
80
|
+
diffPgSchemas(live(type, before, extra), declared(type, after)).changes.map((c) => [c.field, c.rule]);
|
|
81
|
+
|
|
82
|
+
const AGG = "Postgres::Aggregate";
|
|
83
|
+
const AGG1 = "CREATE AGGREGATE app.total(integer) (SFUNC = app.add2, STYPE = integer, INITCOND = '0')";
|
|
84
|
+
|
|
85
|
+
function steps(type: string, liveDdl: string, declaredDdl: string, extra: Partial<LivePgObject> = {}): string[] {
|
|
86
|
+
const [obj] = declaredObjects(JSON.stringify({ dialect: "postgres", objects: [{ export: "x", type, ddl: declaredDdl, dependsOn: [] }] }));
|
|
87
|
+
const after = keyedByQualifiedName([{ key: "x", canonical: obj!.canonical }]);
|
|
88
|
+
const before = live(type, liveDdl, extra);
|
|
89
|
+
const m = matchPgObjects(before, after).find((x) => x.kind === "matched")!;
|
|
90
|
+
return alterSteps(obj!, m.before!.canonical, diffPgSchemas(before, after).changes, { major: 18 }).steps.map((s) => s.sql);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("the classifier and the statements", () => {
|
|
94
|
+
test("the server's printing is the declaration's", () => {
|
|
95
|
+
expect(rules(AGG, AGG1, "CREATE AGGREGATE app.total(int) (sfunc = app.add2, stype = int4, initcond = 0, parallel = unsafe)")).toEqual([]);
|
|
96
|
+
expect(rules("Postgres::CompositeType", "CREATE TYPE app.pair AS (\n a integer,\n b text COLLATE \"C\"\n)", "CREATE TYPE app.pair AS (a int, b text COLLATE pg_catalog.\"C\")")).toEqual([]);
|
|
97
|
+
expect(rules("Postgres::Cast", "CREATE CAST (app.pair AS text) WITH FUNCTION app.f(app.pair)", "CREATE CAST (app.pair AS text) WITH FUNCTION app.f")).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("an aggregate's options are replaced in place; its state type is a drop and a create, refused while something depends on it", () => {
|
|
101
|
+
expect(rules(AGG, AGG1, AGG1.replace("'0'", "'1'"))).toEqual([["definition", "SQLPG302"]]);
|
|
102
|
+
expect(steps(AGG, AGG1, AGG1.replace("'0'", "'1'"))).toEqual([AGG1.replace("CREATE ", "CREATE OR REPLACE ").replace("'0'", "'1'")]);
|
|
103
|
+
expect(rules(AGG, AGG1, AGG1.replace("STYPE = integer", "STYPE = bigint"))).toEqual([["definition", "SQLPG300"]]);
|
|
104
|
+
expect(rules(AGG, AGG1, AGG1.replace("STYPE = integer", "STYPE = bigint"), { dependents: ["view app.v"] })).toEqual([["definition", "SQLPG301"]]);
|
|
105
|
+
const recreate = steps(AGG, AGG1, AGG1.replace("STYPE = integer", "STYPE = bigint"));
|
|
106
|
+
expect(recreate[0]).toBe("DROP AGGREGATE app.total(integer)");
|
|
107
|
+
expect(recreate[2]).toMatch(/^COMMENT ON AGGREGATE app\.total\(integer\) IS /);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a cast, an event trigger, a collation and a type are dropped and created", () => {
|
|
111
|
+
const C1 = "CREATE CAST (app.pair AS text) WITH INOUT";
|
|
112
|
+
expect(steps("Postgres::Cast", C1, `${C1} AS IMPLICIT`).slice(0, 2)).toEqual(["DROP CAST (app.pair AS text)", `${C1} AS IMPLICIT`]);
|
|
113
|
+
const E1 = "CREATE EVENT TRIGGER e ON ddl_command_end EXECUTE FUNCTION app.f()";
|
|
114
|
+
expect(steps("Postgres::EventTrigger", E1, E1.replace("ddl_command_end", "sql_drop"))[0]).toBe("DROP EVENT TRIGGER e");
|
|
115
|
+
expect(rules("Postgres::Collation", "CREATE COLLATION app.c (LOCALE = 'C')", "CREATE COLLATION app.c (LOCALE = 'POSIX')", { dependents: ["column nick of table app.people"] })).toEqual([["lcCollate, lcCtype", "SQLPG301"]]);
|
|
116
|
+
expect(rules("Postgres::RangeType", "CREATE TYPE app.r AS RANGE (SUBTYPE = integer)", "CREATE TYPE app.r AS RANGE (SUBTYPE = bigint)")).toEqual([["subtype", "SQLPG300"]]);
|
|
117
|
+
expect(dropStatement("cast", undefined, "(a AS b)").sql).toBe("DROP CAST (a AS b)");
|
|
118
|
+
expect(dropStatement("composite", "app", "pair").sql).toBe("DROP TYPE app.pair");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a comment changes in place", () => {
|
|
122
|
+
expect(steps("Postgres::Collation", "CREATE COLLATION app.c (LOCALE = 'C')", "CREATE COLLATION app.c (LOCALE = 'C'); COMMENT ON COLLATION app.c IS 'c'")).toEqual([expect.stringMatching(/^COMMENT ON COLLATION app\.c IS 'c/)]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("importing", () => {
|
|
127
|
+
test("each kind is written with its tag, its references interpolated", () => {
|
|
128
|
+
const content = new PostgresGenerator().generate(
|
|
129
|
+
objectsToIR([
|
|
130
|
+
{ type: "Postgres::CompositeType", schema: "app", name: "pair", ddl: "CREATE TYPE app.pair AS (a integer)" },
|
|
131
|
+
{ type: "Postgres::Function", schema: "app", name: "pair_text", ddl: "CREATE OR REPLACE FUNCTION app.pair_text(p app.pair)\n RETURNS text\n LANGUAGE sql\nAS $function$ SELECT '' $function$" },
|
|
132
|
+
{ type: "Postgres::Cast", name: "(app.pair AS text)", ddl: "CREATE CAST (app.pair AS text) WITH FUNCTION app.pair_text(app.pair)" },
|
|
133
|
+
{ type: "Postgres::Aggregate", schema: "app", name: "n", ddl: "CREATE AGGREGATE app.n(*) (SFUNC = int8inc, STYPE = bigint)" },
|
|
134
|
+
]),
|
|
135
|
+
)[0]!.content;
|
|
136
|
+
expect(content).toContain('import { aggregate, cast, func, type } from "@intentius/chant-lexicon-sql/postgres";');
|
|
137
|
+
expect(content).toContain("CREATE CAST (${pair} AS text) WITH FUNCTION ${pairText}(${pair})");
|
|
138
|
+
expect(content).toContain("export const n = aggregate`");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalog read of the kinds #3911 adds: composite and range types,
|
|
3
|
+
* collations, aggregates, casts and event triggers. Postgres has no printer
|
|
4
|
+
* for any of them (`pg_dump` assembles each from its catalog row), so the
|
|
5
|
+
* statement is assembled here the same way: `format_type()` for every type,
|
|
6
|
+
* `regproc` for every function (bare in `pg_catalog`, qualified elsewhere,
|
|
7
|
+
* since the session's search_path is empty), and an option only where it is
|
|
8
|
+
* not what Postgres would choose by itself, so the printed form is the
|
|
9
|
+
* canonical one a declaration is compared with (`../plan/normalize.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Left out, each for its reason: a table's row type (the table is its
|
|
12
|
+
* declaration), a multirange type (its range type creates it), an object an
|
|
13
|
+
* extension owns, and a cast the server defines (`oid` below 16384, the first
|
|
14
|
+
* normal object id).
|
|
15
|
+
*
|
|
16
|
+
* Each object carries what depends on it (`pg_depend`, a normal dependency),
|
|
17
|
+
* which a DROP refuses over, so a change the plan would make by dropping and
|
|
18
|
+
* creating it is refused instead (SQLPG301).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { PostgresClient } from "./client";
|
|
22
|
+
import { notExtensionOwned, scopeSql, type LivePgObject, type SchemaScope } from "./catalog";
|
|
23
|
+
import { POSTGRES_ENTITY_TYPES } from "../entity-types";
|
|
24
|
+
import { quoteIdent } from "../keywords";
|
|
25
|
+
import { stripMarker } from "../../core/ownership";
|
|
26
|
+
|
|
27
|
+
type Row = Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
const C = 'COLLATE "C"';
|
|
30
|
+
const str = (v: unknown): string | undefined => (v === null || v === undefined ? undefined : String(v));
|
|
31
|
+
const literal = (s: string): string => `'${s.replace(/'/g, "''")}'`;
|
|
32
|
+
const qname = (schema: string, name: string): string => `${quoteIdent(schema)}.${quoteIdent(name)}`;
|
|
33
|
+
|
|
34
|
+
function commentOn(what: string, comment: unknown): string[] {
|
|
35
|
+
const c = str(comment);
|
|
36
|
+
if (c === undefined) return [];
|
|
37
|
+
const own = stripMarker(c);
|
|
38
|
+
return own === "" ? [] : [`COMMENT ON ${what} IS ${literal(own)}`];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const notInternal = (catalog: string, oid: string): string =>
|
|
42
|
+
`NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_catalog.${catalog}'::pg_catalog.regclass AND d.objid = ${oid} AND d.deptype = 'i')`;
|
|
43
|
+
|
|
44
|
+
/** What depends on an object of catalog `catalog` (a normal dependency), as the server describes each. */
|
|
45
|
+
const dependentsOf = (catalog: string, oid: string): string =>
|
|
46
|
+
`ARRAY(SELECT pg_catalog.pg_describe_object(d.classid, d.objid, d.objsubid) FROM pg_catalog.pg_depend d
|
|
47
|
+
WHERE d.refclassid = 'pg_catalog.${catalog}'::pg_catalog.regclass AND d.refobjid = ${oid} AND d.deptype = 'n' ORDER BY 1)`;
|
|
48
|
+
|
|
49
|
+
/** A collation's name as a clause: a `pg_catalog` one bare. */
|
|
50
|
+
const collationName = (oid: string): string =>
|
|
51
|
+
`(SELECT CASE WHEN cn.nspname = 'pg_catalog' THEN pg_catalog.quote_ident(co.collname) ELSE pg_catalog.quote_ident(cn.nspname) || '.' || pg_catalog.quote_ident(co.collname) END ` +
|
|
52
|
+
`FROM pg_catalog.pg_collation co JOIN pg_catalog.pg_namespace cn ON cn.oid = co.collnamespace WHERE co.oid = ${oid})`;
|
|
53
|
+
|
|
54
|
+
/** A function named by a `regproc` column, or nothing for 0 (which `regproc` prints as `-`). */
|
|
55
|
+
const proc = (column: string): string => `NULLIF(${column}::text, '-')`;
|
|
56
|
+
|
|
57
|
+
const dependents = (r: Row): { dependents?: string[] } => {
|
|
58
|
+
const d = (r.dependents as string[] | null) ?? [];
|
|
59
|
+
return d.length > 0 ? { dependents: d } : {};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const withComment = (r: Row): { comment?: string } => (str(r.comment) ? { comment: str(r.comment)! } : {});
|
|
63
|
+
|
|
64
|
+
/** Composite and range types: read where the enums are, before the domains and tables that may use them. */
|
|
65
|
+
export async function readTypeKinds(client: PostgresClient, scope: SchemaScope): Promise<LivePgObject[]> {
|
|
66
|
+
const out: LivePgObject[] = [];
|
|
67
|
+
const s = scopeSql("n.nspname", scope);
|
|
68
|
+
|
|
69
|
+
// Composite types: the standalone ones, not a table's or a view's row type.
|
|
70
|
+
const composites = await client.query<Row>(
|
|
71
|
+
`SELECT t.oid::text AS oid, n.nspname AS schema, t.typname AS name, pg_catalog.obj_description(t.oid, 'pg_type') AS comment,
|
|
72
|
+
ARRAY(SELECT pg_catalog.quote_ident(a.attname) || ' ' || pg_catalog.format_type(a.atttypid, a.atttypmod) ||
|
|
73
|
+
CASE WHEN a.attcollation <> 0 AND a.attcollation <> at.typcollation THEN ' COLLATE ' || ${collationName("a.attcollation")} ELSE '' END
|
|
74
|
+
FROM pg_catalog.pg_attribute AS a JOIN pg_catalog.pg_type at ON at.oid = a.atttypid
|
|
75
|
+
WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum) AS attributes,
|
|
76
|
+
${dependentsOf("pg_type", "t.oid")} AS dependents
|
|
77
|
+
FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace JOIN pg_catalog.pg_class c ON c.oid = t.typrelid
|
|
78
|
+
WHERE t.typtype = 'c' AND c.relkind = 'c' AND ${s.sql} AND ${notExtensionOwned("pg_type", "t.oid")}
|
|
79
|
+
ORDER BY n.nspname ${C}, t.typname ${C}`,
|
|
80
|
+
s.params,
|
|
81
|
+
);
|
|
82
|
+
for (const r of composites) {
|
|
83
|
+
const q = qname(String(r.schema), String(r.name));
|
|
84
|
+
const attributes = (r.attributes as string[] | null) ?? [];
|
|
85
|
+
out.push({
|
|
86
|
+
type: POSTGRES_ENTITY_TYPES.composite,
|
|
87
|
+
schema: String(r.schema),
|
|
88
|
+
name: String(r.name),
|
|
89
|
+
oid: String(r.oid),
|
|
90
|
+
...withComment(r),
|
|
91
|
+
...dependents(r),
|
|
92
|
+
statement: [`CREATE TYPE ${q} AS (${attributes.length > 0 ? `\n ${attributes.join(",\n ")}\n` : ""})`, ...commentOn(`TYPE ${q}`, r.comment)].join(";\n"),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Range types, each with the options that differ from what CREATE TYPE ... AS RANGE would choose.
|
|
97
|
+
const ranges = await client.query<Row>(
|
|
98
|
+
`SELECT t.oid::text AS oid, n.nspname AS schema, t.typname AS name, pg_catalog.obj_description(t.oid, 'pg_type') AS comment,
|
|
99
|
+
pg_catalog.format_type(r.rngsubtype, NULL) AS subtype,
|
|
100
|
+
CASE WHEN r.rngsubopc <> (SELECT o.oid FROM pg_catalog.pg_opclass o JOIN pg_catalog.pg_am am ON am.oid = o.opcmethod
|
|
101
|
+
WHERE am.amname = 'btree' AND o.opcintype = r.rngsubtype AND o.opcdefault LIMIT 1)
|
|
102
|
+
THEN (SELECT CASE WHEN on2.nspname = 'pg_catalog' THEN pg_catalog.quote_ident(o.opcname) ELSE pg_catalog.quote_ident(on2.nspname) || '.' || pg_catalog.quote_ident(o.opcname) END
|
|
103
|
+
FROM pg_catalog.pg_opclass o JOIN pg_catalog.pg_namespace on2 ON on2.oid = o.opcnamespace WHERE o.oid = r.rngsubopc) END AS opclass,
|
|
104
|
+
CASE WHEN r.rngcollation <> 0 AND r.rngcollation <> st.typcollation THEN ${collationName("r.rngcollation")} END AS collation,
|
|
105
|
+
${proc("r.rngcanonical")} AS canonical, ${proc("r.rngsubdiff")} AS subdiff,
|
|
106
|
+
mn.nspname AS multi_schema, mt.typname AS multi_name,
|
|
107
|
+
${dependentsOf("pg_type", "t.oid")} AS dependents
|
|
108
|
+
FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace JOIN pg_catalog.pg_range r ON r.rngtypid = t.oid
|
|
109
|
+
JOIN pg_catalog.pg_type st ON st.oid = r.rngsubtype
|
|
110
|
+
JOIN pg_catalog.pg_type mt ON mt.oid = r.rngmultitypid JOIN pg_catalog.pg_namespace mn ON mn.oid = mt.typnamespace
|
|
111
|
+
WHERE t.typtype = 'r' AND ${s.sql} AND ${notExtensionOwned("pg_type", "t.oid")}
|
|
112
|
+
ORDER BY n.nspname ${C}, t.typname ${C}`,
|
|
113
|
+
s.params,
|
|
114
|
+
);
|
|
115
|
+
for (const r of ranges) {
|
|
116
|
+
const schema = String(r.schema);
|
|
117
|
+
const name = String(r.name);
|
|
118
|
+
const q = qname(schema, name);
|
|
119
|
+
const opts = [`SUBTYPE = ${r.subtype}`];
|
|
120
|
+
if (str(r.opclass)) opts.push(`SUBTYPE_OPCLASS = ${r.opclass}`);
|
|
121
|
+
if (str(r.collation)) opts.push(`COLLATION = ${r.collation}`);
|
|
122
|
+
if (str(r.canonical)) opts.push(`CANONICAL = ${r.canonical}`);
|
|
123
|
+
if (str(r.subdiff)) opts.push(`SUBTYPE_DIFF = ${r.subdiff}`);
|
|
124
|
+
// The multirange type's name, when it is not the one Postgres derives in the range's schema.
|
|
125
|
+
const at = name.indexOf("range");
|
|
126
|
+
const derived = at >= 0 ? `${name.slice(0, at)}multi${name.slice(at)}` : `${name}_multirange`;
|
|
127
|
+
if (String(r.multi_schema) !== schema || String(r.multi_name) !== derived) opts.push(`MULTIRANGE_TYPE_NAME = ${qname(String(r.multi_schema), String(r.multi_name))}`);
|
|
128
|
+
out.push({
|
|
129
|
+
type: POSTGRES_ENTITY_TYPES.range,
|
|
130
|
+
schema,
|
|
131
|
+
name,
|
|
132
|
+
oid: String(r.oid),
|
|
133
|
+
...withComment(r),
|
|
134
|
+
...dependents(r),
|
|
135
|
+
statement: [`CREATE TYPE ${q} AS RANGE (${opts.join(", ")})`, ...commentOn(`TYPE ${q}`, r.comment)].join(";\n"),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Collations: read first, before the types and tables whose columns use them. */
|
|
142
|
+
export async function readCollations(client: PostgresClient, scope: SchemaScope): Promise<LivePgObject[]> {
|
|
143
|
+
const s = scopeSql("n.nspname", scope);
|
|
144
|
+
const rows = await client.query<Row>(
|
|
145
|
+
`SELECT c.oid::text AS oid, n.nspname AS schema, c.collname AS name, pg_catalog.to_jsonb(c) AS def,
|
|
146
|
+
pg_catalog.obj_description(c.oid, 'pg_collation') AS comment, ${dependentsOf("pg_collation", "c.oid")} AS dependents
|
|
147
|
+
FROM pg_catalog.pg_collation c JOIN pg_catalog.pg_namespace n ON n.oid = c.collnamespace
|
|
148
|
+
WHERE ${s.sql} AND ${notExtensionOwned("pg_collation", "c.oid")}
|
|
149
|
+
ORDER BY n.nspname ${C}, c.collname ${C}`,
|
|
150
|
+
s.params,
|
|
151
|
+
);
|
|
152
|
+
const out: LivePgObject[] = [];
|
|
153
|
+
for (const r of rows) {
|
|
154
|
+
// The row as JSON: its columns differ across the majors (`colliculocale` in 15 and 16, `colllocale` from 17, `collicurules` from 16).
|
|
155
|
+
const d = (typeof r.def === "string" ? JSON.parse(r.def) : r.def) as Record<string, unknown>;
|
|
156
|
+
const provider = ({ c: "libc", i: "icu", b: "builtin", d: "default" } as Record<string, string>)[String(d.collprovider)] ?? "libc";
|
|
157
|
+
const locale = str(d.colllocale) ?? str(d.colliculocale);
|
|
158
|
+
const opts: string[] = [];
|
|
159
|
+
if (provider !== "libc") opts.push(`PROVIDER = ${provider}`);
|
|
160
|
+
if (provider === "libc" || locale === undefined) {
|
|
161
|
+
const collate = str(d.collcollate) ?? "";
|
|
162
|
+
const ctype = str(d.collctype) ?? "";
|
|
163
|
+
if (provider !== "libc") opts.push(`LOCALE = ${literal(collate)}`);
|
|
164
|
+
else if (collate === ctype) opts.push(`LOCALE = ${literal(collate)}`);
|
|
165
|
+
else opts.push(`LC_COLLATE = ${literal(collate)}`, `LC_CTYPE = ${literal(ctype)}`);
|
|
166
|
+
} else opts.push(`LOCALE = ${literal(locale)}`);
|
|
167
|
+
if (d.collisdeterministic === false) opts.push("DETERMINISTIC = false");
|
|
168
|
+
if (str(d.collicurules)) opts.push(`RULES = ${literal(String(d.collicurules))}`);
|
|
169
|
+
const q = qname(String(r.schema), String(r.name));
|
|
170
|
+
out.push({
|
|
171
|
+
type: POSTGRES_ENTITY_TYPES.collation,
|
|
172
|
+
schema: String(r.schema),
|
|
173
|
+
name: String(r.name),
|
|
174
|
+
oid: String(r.oid),
|
|
175
|
+
...withComment(r),
|
|
176
|
+
...dependents(r),
|
|
177
|
+
statement: [`CREATE COLLATION ${q} (${opts.join(", ")})`, ...commentOn(`COLLATION ${q}`, r.comment)].join(";\n"),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const MODIFY: Record<string, string> = { r: "READ_ONLY", s: "SHAREABLE", w: "READ_WRITE" };
|
|
184
|
+
|
|
185
|
+
/** Aggregates, casts and event triggers: read after the functions they call. */
|
|
186
|
+
export async function readRoutineKinds(client: PostgresClient, scope: SchemaScope): Promise<LivePgObject[]> {
|
|
187
|
+
const out: LivePgObject[] = [];
|
|
188
|
+
const s = scopeSql("n.nspname", scope);
|
|
189
|
+
|
|
190
|
+
const aggregates = await client.query<Row>(
|
|
191
|
+
`SELECT p.oid::text AS oid, n.nspname AS schema, p.proname AS name, pg_catalog.pg_get_function_arguments(p.oid) AS args,
|
|
192
|
+
ARRAY(SELECT pg_catalog.format_type(u.x, NULL) FROM pg_catalog.unnest(p.proargtypes::pg_catalog.oid[]) WITH ORDINALITY AS u(x, i) ORDER BY u.i) AS argtypes,
|
|
193
|
+
a.aggkind AS kind, a.aggnumdirectargs AS ndirect,
|
|
194
|
+
${proc("a.aggtransfn")} AS sfunc, pg_catalog.format_type(a.aggtranstype, NULL) AS stype, a.aggtransspace AS sspace,
|
|
195
|
+
${proc("a.aggfinalfn")} AS finalfunc, a.aggfinalextra AS finalextra, a.aggfinalmodify::text AS finalmodify,
|
|
196
|
+
${proc("a.aggcombinefn")} AS combinefunc, ${proc("a.aggserialfn")} AS serialfunc, ${proc("a.aggdeserialfn")} AS deserialfunc,
|
|
197
|
+
a.agginitval AS initcond,
|
|
198
|
+
${proc("a.aggmtransfn")} AS msfunc, ${proc("a.aggminvtransfn")} AS minvfunc,
|
|
199
|
+
CASE WHEN a.aggmtranstype <> 0 THEN pg_catalog.format_type(a.aggmtranstype, NULL) END AS mstype, a.aggmtransspace AS msspace,
|
|
200
|
+
${proc("a.aggmfinalfn")} AS mfinalfunc, a.aggmfinalextra AS mfinalextra, a.aggmfinalmodify::text AS mfinalmodify, a.aggminitval AS minitcond,
|
|
201
|
+
CASE WHEN a.aggsortop <> 0 THEN (SELECT CASE WHEN opn.nspname = 'pg_catalog' THEN o.oprname ELSE 'OPERATOR(' || pg_catalog.quote_ident(opn.nspname) || '.' || o.oprname || ')' END
|
|
202
|
+
FROM pg_catalog.pg_operator o JOIN pg_catalog.pg_namespace opn ON opn.oid = o.oprnamespace WHERE o.oid = a.aggsortop) END AS sortop,
|
|
203
|
+
p.proparallel AS parallel, pg_catalog.obj_description(p.oid, 'pg_proc') AS comment,
|
|
204
|
+
${dependentsOf("pg_proc", "p.oid")} AS dependents
|
|
205
|
+
FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace JOIN pg_catalog.pg_aggregate a ON a.aggfnoid = p.oid
|
|
206
|
+
WHERE p.prokind = 'a' AND ${s.sql} AND ${notExtensionOwned("pg_proc", "p.oid")}
|
|
207
|
+
ORDER BY n.nspname ${C}, p.proname ${C}, pg_catalog.oidvectortypes(p.proargtypes) ${C}`,
|
|
208
|
+
s.params,
|
|
209
|
+
);
|
|
210
|
+
for (const r of aggregates) {
|
|
211
|
+
const kind = String(r.kind);
|
|
212
|
+
const types = (r.argtypes as string[] | null) ?? [];
|
|
213
|
+
const ndirect = Number(r.ndirect ?? 0);
|
|
214
|
+
const signature =
|
|
215
|
+
kind === "n"
|
|
216
|
+
? `(${types.length === 0 ? "*" : types.join(",")})`
|
|
217
|
+
: `(${ndirect > 0 ? `${types.slice(0, ndirect).join(",")} ` : ""}ORDER BY ${types.slice(ndirect).join(",")})`;
|
|
218
|
+
const args = str(r.args) || "*";
|
|
219
|
+
const modifyDefault = kind === "n" ? "r" : "w";
|
|
220
|
+
const opts = [`SFUNC = ${r.sfunc}`, `STYPE = ${r.stype}`];
|
|
221
|
+
if (Number(r.sspace) !== 0) opts.push(`SSPACE = ${Number(r.sspace)}`);
|
|
222
|
+
if (str(r.finalfunc)) opts.push(`FINALFUNC = ${r.finalfunc}`);
|
|
223
|
+
if (r.finalextra === true) opts.push("FINALFUNC_EXTRA");
|
|
224
|
+
if (str(r.finalmodify) && r.finalmodify !== modifyDefault) opts.push(`FINALFUNC_MODIFY = ${MODIFY[String(r.finalmodify)]}`);
|
|
225
|
+
if (str(r.combinefunc)) opts.push(`COMBINEFUNC = ${r.combinefunc}`);
|
|
226
|
+
if (str(r.serialfunc)) opts.push(`SERIALFUNC = ${r.serialfunc}`);
|
|
227
|
+
if (str(r.deserialfunc)) opts.push(`DESERIALFUNC = ${r.deserialfunc}`);
|
|
228
|
+
if (str(r.initcond) !== undefined) opts.push(`INITCOND = ${literal(String(r.initcond))}`);
|
|
229
|
+
if (str(r.msfunc)) {
|
|
230
|
+
opts.push(`MSFUNC = ${r.msfunc}`);
|
|
231
|
+
if (str(r.minvfunc)) opts.push(`MINVFUNC = ${r.minvfunc}`);
|
|
232
|
+
if (str(r.mstype)) opts.push(`MSTYPE = ${r.mstype}`);
|
|
233
|
+
if (Number(r.msspace) !== 0) opts.push(`MSSPACE = ${Number(r.msspace)}`);
|
|
234
|
+
if (str(r.mfinalfunc)) opts.push(`MFINALFUNC = ${r.mfinalfunc}`);
|
|
235
|
+
if (r.mfinalextra === true) opts.push("MFINALFUNC_EXTRA");
|
|
236
|
+
if (str(r.mfinalmodify) && r.mfinalmodify !== modifyDefault) opts.push(`MFINALFUNC_MODIFY = ${MODIFY[String(r.mfinalmodify)]}`);
|
|
237
|
+
if (str(r.minitcond) !== undefined) opts.push(`MINITCOND = ${literal(String(r.minitcond))}`);
|
|
238
|
+
}
|
|
239
|
+
if (str(r.sortop)) opts.push(`SORTOP = ${r.sortop}`);
|
|
240
|
+
if (r.parallel === "s") opts.push("PARALLEL = SAFE");
|
|
241
|
+
else if (r.parallel === "r") opts.push("PARALLEL = RESTRICTED");
|
|
242
|
+
if (kind === "h") opts.push("HYPOTHETICAL");
|
|
243
|
+
const q = qname(String(r.schema), String(r.name));
|
|
244
|
+
out.push({
|
|
245
|
+
type: POSTGRES_ENTITY_TYPES.aggregate,
|
|
246
|
+
schema: String(r.schema),
|
|
247
|
+
name: String(r.name),
|
|
248
|
+
oid: String(r.oid),
|
|
249
|
+
signature,
|
|
250
|
+
...withComment(r),
|
|
251
|
+
...dependents(r),
|
|
252
|
+
statement: [`CREATE AGGREGATE ${q}(${args}) (${opts.join(", ")})`, ...commentOn(`AGGREGATE ${q}(${args})`, r.comment)].join(";\n"),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Casts the database defines: the server's own have oids below the first normal object id. In scope when a type or the function is.
|
|
257
|
+
const scoped = (column: string) =>
|
|
258
|
+
scope.schemas
|
|
259
|
+
? `(SELECT xn.nspname FROM pg_catalog.pg_type x JOIN pg_catalog.pg_namespace xn ON xn.oid = x.typnamespace WHERE x.oid = ${column}) = ANY($1::text[])`
|
|
260
|
+
: "true";
|
|
261
|
+
const casts = await client.query<Row>(
|
|
262
|
+
`SELECT c.oid::text AS oid, pg_catalog.format_type(c.castsource, NULL) AS source, pg_catalog.format_type(c.casttarget, NULL) AS target,
|
|
263
|
+
c.castmethod::text AS method, c.castcontext::text AS context,
|
|
264
|
+
CASE WHEN c.castfunc <> 0 THEN c.castfunc::pg_catalog.regprocedure::text END AS func,
|
|
265
|
+
pg_catalog.obj_description(c.oid, 'pg_cast') AS comment, ${dependentsOf("pg_cast", "c.oid")} AS dependents
|
|
266
|
+
FROM pg_catalog.pg_cast c
|
|
267
|
+
WHERE c.oid >= 16384 AND ${notExtensionOwned("pg_cast", "c.oid")} AND ${notInternal("pg_cast", "c.oid")}
|
|
268
|
+
AND (${scoped("c.castsource")} OR ${scoped("c.casttarget")}
|
|
269
|
+
OR ${scope.schemas ? `(SELECT pn.nspname FROM pg_catalog.pg_proc pp JOIN pg_catalog.pg_namespace pn ON pn.oid = pp.pronamespace WHERE pp.oid = c.castfunc) = ANY($1::text[])` : "true"})
|
|
270
|
+
ORDER BY pg_catalog.format_type(c.castsource, NULL) ${C}, pg_catalog.format_type(c.casttarget, NULL) ${C}`,
|
|
271
|
+
scope.schemas ? [[...scope.schemas]] : [],
|
|
272
|
+
);
|
|
273
|
+
for (const r of casts) {
|
|
274
|
+
const name = `(${r.source} AS ${r.target})`;
|
|
275
|
+
const how = r.method === "b" ? "WITHOUT FUNCTION" : r.method === "i" ? "WITH INOUT" : `WITH FUNCTION ${r.func}`;
|
|
276
|
+
const context = r.context === "a" ? " AS ASSIGNMENT" : r.context === "i" ? " AS IMPLICIT" : "";
|
|
277
|
+
out.push({
|
|
278
|
+
type: POSTGRES_ENTITY_TYPES.cast,
|
|
279
|
+
name,
|
|
280
|
+
oid: String(r.oid),
|
|
281
|
+
...withComment(r),
|
|
282
|
+
...dependents(r),
|
|
283
|
+
statement: [`CREATE CAST ${name} ${how}${context}`, ...commentOn(`CAST ${name}`, r.comment)].join(";\n"),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Event triggers: the database's, in scope when their function is.
|
|
288
|
+
const fs = scopeSql("fn.nspname", scope);
|
|
289
|
+
const events = await client.query<Row>(
|
|
290
|
+
`SELECT e.oid::text AS oid, e.evtname AS name, e.evtevent AS event, ${proc("e.evtfoid::pg_catalog.regproc")} AS func, e.evttags AS tags,
|
|
291
|
+
pg_catalog.obj_description(e.oid, 'pg_event_trigger') AS comment
|
|
292
|
+
FROM pg_catalog.pg_event_trigger e JOIN pg_catalog.pg_proc f ON f.oid = e.evtfoid JOIN pg_catalog.pg_namespace fn ON fn.oid = f.pronamespace
|
|
293
|
+
WHERE ${fs.sql} AND ${notExtensionOwned("pg_event_trigger", "e.oid")}
|
|
294
|
+
ORDER BY e.evtname ${C}`,
|
|
295
|
+
fs.params,
|
|
296
|
+
);
|
|
297
|
+
for (const r of events) {
|
|
298
|
+
const name = quoteIdent(String(r.name));
|
|
299
|
+
const tags = (r.tags as string[] | null) ?? [];
|
|
300
|
+
out.push({
|
|
301
|
+
type: POSTGRES_ENTITY_TYPES.eventTrigger,
|
|
302
|
+
name: String(r.name),
|
|
303
|
+
oid: String(r.oid),
|
|
304
|
+
...withComment(r),
|
|
305
|
+
statement: [
|
|
306
|
+
`CREATE EVENT TRIGGER ${name} ON ${quoteIdent(String(r.event))}${tags.length > 0 ? ` WHEN TAG IN (${tags.map(literal).join(", ")})` : ""} EXECUTE FUNCTION ${r.func}()`,
|
|
307
|
+
...commentOn(`EVENT TRIGGER ${name}`, r.comment),
|
|
308
|
+
].join(";\n"),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
* - objects an extension owns (`pg_depend.deptype = 'e'`): the extension
|
|
19
19
|
* declares them, so the extension is what is imported;
|
|
20
20
|
* - `plpgsql`, installed in every database;
|
|
21
|
-
* - a trigger the server makes for itself (a foreign key's)
|
|
22
|
-
*
|
|
21
|
+
* - a trigger the server makes for itself (a foreign key's) or clones onto a
|
|
22
|
+
* partition from its parent's, and a function or cast that is part of
|
|
23
|
+
* another object (a range type's constructors and its multirange cast);
|
|
24
|
+
* - a table's row type, a multirange type (its range type creates it), and a
|
|
25
|
+
* cast the server defines (`./catalog-kinds.ts`);
|
|
23
26
|
* - a routine with a SQL-standard body (`RETURN`, `BEGIN ATOMIC`): read, but
|
|
24
27
|
* marked `unsupported`, since the server prints it rewritten and a
|
|
25
28
|
* declaration cannot hold it;
|
|
@@ -32,9 +35,9 @@
|
|
|
32
35
|
* progress (`../migrate/names.ts`, `../migrate/carry.ts`), so a table reads
|
|
33
36
|
* as it was before the migration until its switch, and as declared after it.
|
|
34
37
|
*
|
|
35
|
-
* Objects of the kinds no declaration holds (
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
+
* Objects of the kinds no declaration holds (operators, publications, rules,
|
|
39
|
+
* ...) are not read here: import finds them with `./undeclarable.ts` and
|
|
40
|
+
* refuses them by name (#3911).
|
|
38
41
|
*
|
|
39
42
|
* Objects another tool owns, an ORM's or a migration runner's revision table
|
|
40
43
|
* ({@link FOREIGN_TABLES}), are read and marked with the tool, so import
|
|
@@ -50,6 +53,7 @@ import { MIGRATION_TRAILER_KEY } from "../migrate/names";
|
|
|
50
53
|
import { isProviderOwned, providerData } from "../providers";
|
|
51
54
|
import { FOREIGN_TABLES } from "../../core/foreign-tables";
|
|
52
55
|
import type { PostgresProvider } from "../providers/types";
|
|
56
|
+
import { readCollations, readRoutineKinds, readTypeKinds } from "./catalog-kinds";
|
|
53
57
|
|
|
54
58
|
/** The server's own schemas, never part of a declared schema. */
|
|
55
59
|
export const SYSTEM_SCHEMAS = ["pg_catalog", "information_schema", "pg_toast"];
|
|
@@ -73,9 +77,9 @@ export interface LivePgObject {
|
|
|
73
77
|
statement: string;
|
|
74
78
|
/** The tool whose bookkeeping this is, when it is another tool's (an ORM's revision table). */
|
|
75
79
|
foreign?: string;
|
|
76
|
-
/** A routine's input parameter types, `(integer,text)`; a trigger's table, ` ON app.users` (`../plan/normalize.ts`). */
|
|
80
|
+
/** A routine's or aggregate's input parameter types, `(integer,text)`; a trigger's table, ` ON app.users` (`../plan/normalize.ts`). */
|
|
77
81
|
signature?: string;
|
|
78
|
-
/** What depends on a routine (a view, a trigger, a column
|
|
82
|
+
/** What depends on a routine, type, aggregate, collation or cast (a view, a trigger, a column), as the server describes each. */
|
|
79
83
|
dependents?: string[];
|
|
80
84
|
/** Why no declaration can hold this object; it is read so import can say it left it out. */
|
|
81
85
|
unsupported?: string;
|
|
@@ -128,6 +132,10 @@ export function scopeSql(column: string, scope: SchemaScope): { sql: string; par
|
|
|
128
132
|
export const notExtensionOwned = (catalog: string, oid: string): string =>
|
|
129
133
|
`NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_catalog.${catalog}'::pg_catalog.regclass AND d.objid = ${oid} AND d.deptype = 'e')`;
|
|
130
134
|
|
|
135
|
+
/** `NOT <object is part of another>` (`pg_depend.deptype = 'i'`): a range type's constructor functions, its multirange cast. */
|
|
136
|
+
const notInternal = (catalog: string, oid: string): string =>
|
|
137
|
+
`NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_catalog.${catalog}'::pg_catalog.regclass AND d.objid = ${oid} AND d.deptype = 'i')`;
|
|
138
|
+
|
|
131
139
|
/** A collation name as a clause, `pg_catalog` names unqualified. */
|
|
132
140
|
const COLLATION_NAME = (oid: string) =>
|
|
133
141
|
`(SELECT CASE WHEN cn.nspname = 'pg_catalog' THEN pg_catalog.quote_ident(co.collname) ELSE pg_catalog.quote_ident(cn.nspname) || '.' || pg_catalog.quote_ident(co.collname) END ` +
|
|
@@ -192,10 +200,11 @@ const seqParams = (r: Row): SequenceParams => ({
|
|
|
192
200
|
// ── Reading ────────────────────────────────────────────────────────────
|
|
193
201
|
|
|
194
202
|
/**
|
|
195
|
-
* Every object in scope, in a stable order: schemas, extensions,
|
|
196
|
-
* domains, sequences, tables, views and
|
|
197
|
-
* functions and procedures,
|
|
198
|
-
*
|
|
203
|
+
* Every object in scope, in a stable order: schemas, extensions, collations,
|
|
204
|
+
* enums, composite and range types, domains, sequences, tables, views and
|
|
205
|
+
* materialized views, indexes, functions and procedures, aggregates, casts,
|
|
206
|
+
* event triggers, triggers, and where access is read, policies and the
|
|
207
|
+
* declared roles; each kind by schema then name, in byte order.
|
|
199
208
|
*/
|
|
200
209
|
export async function readLiveSchema(client: PostgresClient, scope: SchemaScope = {}): Promise<LivePgObject[]> {
|
|
201
210
|
const out: LivePgObject[] = [];
|
|
@@ -253,6 +262,9 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
253
262
|
}
|
|
254
263
|
}
|
|
255
264
|
|
|
265
|
+
// Collations, before the types and columns that use them (#3911).
|
|
266
|
+
out.push(...(await readCollations(client, scope)));
|
|
267
|
+
|
|
256
268
|
// Enums.
|
|
257
269
|
{
|
|
258
270
|
const s = scopeSql("n.nspname", scope);
|
|
@@ -277,6 +289,9 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
277
289
|
}
|
|
278
290
|
}
|
|
279
291
|
|
|
292
|
+
// Composite and range types (#3911).
|
|
293
|
+
out.push(...(await readTypeKinds(client, scope)));
|
|
294
|
+
|
|
280
295
|
// Domains.
|
|
281
296
|
{
|
|
282
297
|
const s = scopeSql("n.nspname", scope);
|
|
@@ -586,7 +601,7 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
586
601
|
WHERE d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND d.refobjid = p.oid AND d.deptype = 'n'
|
|
587
602
|
ORDER BY 1) AS dependents
|
|
588
603
|
FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
|
589
|
-
WHERE p.prokind IN ('f', 'p', 'w') AND ${s.sql} AND ${notExtensionOwned("pg_proc", "p.oid")}
|
|
604
|
+
WHERE p.prokind IN ('f', 'p', 'w') AND ${s.sql} AND ${notExtensionOwned("pg_proc", "p.oid")} AND ${notInternal("pg_proc", "p.oid")}
|
|
590
605
|
ORDER BY n.nspname ${C}, p.proname ${C}, pg_catalog.oidvectortypes(p.proargtypes) ${C}`,
|
|
591
606
|
s.params,
|
|
592
607
|
);
|
|
@@ -611,6 +626,9 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
611
626
|
}
|
|
612
627
|
}
|
|
613
628
|
|
|
629
|
+
// Aggregates, casts and event triggers, after the functions they call (#3911).
|
|
630
|
+
out.push(...(await readRoutineKinds(client, scope)));
|
|
631
|
+
|
|
614
632
|
// Triggers: the ones written by hand, not a foreign key's or a partition's clone of its parent's.
|
|
615
633
|
{
|
|
616
634
|
const rows = await client.query<Row>(
|