@intentius/chant-lexicon-sql 0.124.18 → 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/import/live-export.d.ts +8 -0
- package/dist/clickhouse/import/live-export.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/live/undeclarable.d.ts +32 -0
- package/dist/clickhouse/live/undeclarable.d.ts.map +1 -0
- 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/testing/fake-server.d.ts +6 -0
- package/dist/clickhouse/testing/fake-server.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/coverage-page.d.ts +7 -0
- package/dist/codegen/coverage-page.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/core/undeclarable.d.ts +38 -0
- package/dist/core/undeclarable.d.ts.map +1 -0
- package/dist/coverage/clickhouse-commands.json +24 -0
- package/dist/coverage/clickhouse.json +25 -0
- package/dist/coverage/postgres-commands.json +245 -0
- package/dist/coverage/postgres.json +51 -0
- package/dist/coverage.d.ts +68 -0
- package/dist/coverage.d.ts.map +1 -1
- package/dist/files/clickhouse.d.ts +2 -1
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/common.d.ts +12 -0
- package/dist/files/common.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +1 -1
- package/dist/files/postgres.d.ts.map +1 -1
- package/dist/import-parser.d.ts +7 -2
- package/dist/import-parser.d.ts.map +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/import/live-export.d.ts +8 -0
- package/dist/postgres/import/live-export.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 +23 -8
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/live/undeclarable.d.ts +42 -0
- package/dist/postgres/live/undeclarable.d.ts.map +1 -0
- 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/fake-client.d.ts +6 -0
- package/dist/postgres/testing/fake-client.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/import/live-export.ts +17 -2
- 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 +65 -0
- package/src/clickhouse/live/undeclarable.ts +66 -0
- 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/testing/fake-server.ts +5 -0
- package/src/clickhouse/topology.ts +4 -2
- package/src/clickhouse.ts +6 -0
- package/src/codegen/classifier-page.test.ts +6 -0
- package/src/codegen/coverage-page.ts +77 -0
- package/src/codegen/docs.ts +5 -0
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +8 -0
- package/src/core/undeclarable.test.ts +141 -0
- package/src/core/undeclarable.ts +66 -0
- package/src/coverage/clickhouse-commands.json +24 -0
- package/src/coverage/clickhouse.json +25 -0
- package/src/coverage/postgres-commands.json +245 -0
- package/src/coverage/postgres.json +51 -0
- package/src/coverage.test.ts +78 -0
- package/src/coverage.ts +142 -0
- package/src/files/clickhouse.ts +20 -7
- package/src/files/common.ts +18 -0
- package/src/files/postgres.ts +25 -6
- package/src/generated/lexicon-sql.json +40 -0
- package/src/import-parser.ts +12 -4
- 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 +13 -2
- 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/import/live-export.ts +25 -2
- package/src/postgres/kinds.test.ts +140 -0
- package/src/postgres/live/catalog-kinds.ts +312 -0
- package/src/postgres/live/catalog.ts +33 -11
- 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 +83 -0
- package/src/postgres/live/undeclarable.ts +141 -0
- 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/fake-client.ts +5 -0
- 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
|
@@ -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
|
-
* -
|
|
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,6 +35,10 @@
|
|
|
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
|
*
|
|
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).
|
|
41
|
+
*
|
|
35
42
|
* Objects another tool owns, an ORM's or a migration runner's revision table
|
|
36
43
|
* ({@link FOREIGN_TABLES}), are read and marked with the tool, so import
|
|
37
44
|
* leaves them out with a warning and observation reports them foreign, never
|
|
@@ -46,6 +53,7 @@ import { MIGRATION_TRAILER_KEY } from "../migrate/names";
|
|
|
46
53
|
import { isProviderOwned, providerData } from "../providers";
|
|
47
54
|
import { FOREIGN_TABLES } from "../../core/foreign-tables";
|
|
48
55
|
import type { PostgresProvider } from "../providers/types";
|
|
56
|
+
import { readCollations, readRoutineKinds, readTypeKinds } from "./catalog-kinds";
|
|
49
57
|
|
|
50
58
|
/** The server's own schemas, never part of a declared schema. */
|
|
51
59
|
export const SYSTEM_SCHEMAS = ["pg_catalog", "information_schema", "pg_toast"];
|
|
@@ -69,9 +77,9 @@ export interface LivePgObject {
|
|
|
69
77
|
statement: string;
|
|
70
78
|
/** The tool whose bookkeeping this is, when it is another tool's (an ORM's revision table). */
|
|
71
79
|
foreign?: string;
|
|
72
|
-
/** 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`). */
|
|
73
81
|
signature?: string;
|
|
74
|
-
/** 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. */
|
|
75
83
|
dependents?: string[];
|
|
76
84
|
/** Why no declaration can hold this object; it is read so import can say it left it out. */
|
|
77
85
|
unsupported?: string;
|
|
@@ -112,7 +120,7 @@ const commentOn = (what: string, comment: unknown): string[] => {
|
|
|
112
120
|
};
|
|
113
121
|
|
|
114
122
|
/** The scope as a predicate on a schema name column, with its parameter. */
|
|
115
|
-
function scopeSql(column: string, scope: SchemaScope): { sql: string; params: unknown[] } {
|
|
123
|
+
export function scopeSql(column: string, scope: SchemaScope): { sql: string; params: unknown[] } {
|
|
116
124
|
if (scope.schemas) return { sql: `${column} = ANY($1::text[])`, params: [[...scope.schemas]] };
|
|
117
125
|
return {
|
|
118
126
|
sql: `${column} NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ${column} NOT LIKE 'pg\\_temp\\_%' AND ${column} NOT LIKE 'pg\\_toast\\_temp\\_%'`,
|
|
@@ -121,9 +129,13 @@ function scopeSql(column: string, scope: SchemaScope): { sql: string; params: un
|
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
/** `NOT <object is owned by an extension>`, for an object of catalog `catalog` with oid column `oid`. */
|
|
124
|
-
const notExtensionOwned = (catalog: string, oid: string): string =>
|
|
132
|
+
export const notExtensionOwned = (catalog: string, oid: string): string =>
|
|
125
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')`;
|
|
126
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
|
+
|
|
127
139
|
/** A collation name as a clause, `pg_catalog` names unqualified. */
|
|
128
140
|
const COLLATION_NAME = (oid: string) =>
|
|
129
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 ` +
|
|
@@ -188,10 +200,11 @@ const seqParams = (r: Row): SequenceParams => ({
|
|
|
188
200
|
// ── Reading ────────────────────────────────────────────────────────────
|
|
189
201
|
|
|
190
202
|
/**
|
|
191
|
-
* Every object in scope, in a stable order: schemas, extensions,
|
|
192
|
-
* domains, sequences, tables, views and
|
|
193
|
-
* functions and procedures,
|
|
194
|
-
*
|
|
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.
|
|
195
208
|
*/
|
|
196
209
|
export async function readLiveSchema(client: PostgresClient, scope: SchemaScope = {}): Promise<LivePgObject[]> {
|
|
197
210
|
const out: LivePgObject[] = [];
|
|
@@ -249,6 +262,9 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
249
262
|
}
|
|
250
263
|
}
|
|
251
264
|
|
|
265
|
+
// Collations, before the types and columns that use them (#3911).
|
|
266
|
+
out.push(...(await readCollations(client, scope)));
|
|
267
|
+
|
|
252
268
|
// Enums.
|
|
253
269
|
{
|
|
254
270
|
const s = scopeSql("n.nspname", scope);
|
|
@@ -273,6 +289,9 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
273
289
|
}
|
|
274
290
|
}
|
|
275
291
|
|
|
292
|
+
// Composite and range types (#3911).
|
|
293
|
+
out.push(...(await readTypeKinds(client, scope)));
|
|
294
|
+
|
|
276
295
|
// Domains.
|
|
277
296
|
{
|
|
278
297
|
const s = scopeSql("n.nspname", scope);
|
|
@@ -582,7 +601,7 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
582
601
|
WHERE d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND d.refobjid = p.oid AND d.deptype = 'n'
|
|
583
602
|
ORDER BY 1) AS dependents
|
|
584
603
|
FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
|
585
|
-
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")}
|
|
586
605
|
ORDER BY n.nspname ${C}, p.proname ${C}, pg_catalog.oidvectortypes(p.proargtypes) ${C}`,
|
|
587
606
|
s.params,
|
|
588
607
|
);
|
|
@@ -607,6 +626,9 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
|
|
|
607
626
|
}
|
|
608
627
|
}
|
|
609
628
|
|
|
629
|
+
// Aggregates, casts and event triggers, after the functions they call (#3911).
|
|
630
|
+
out.push(...(await readRoutineKinds(client, scope)));
|
|
631
|
+
|
|
610
632
|
// Triggers: the ones written by hand, not a foreign key's or a partition's clone of its parent's.
|
|
611
633
|
{
|
|
612
634
|
const rows = await client.query<Row>(
|
|
@@ -41,9 +41,17 @@ interface Bound {
|
|
|
41
41
|
*/
|
|
42
42
|
export function declaredAddress(entity: { type: string; props: Record<string, unknown> }, defaultSchema: string): { schema?: string; name: string; signature?: string } {
|
|
43
43
|
const name = String(entity.props.name ?? "");
|
|
44
|
-
if (entity.type === POSTGRES_ENTITY_TYPES.schema || entity.type === POSTGRES_ENTITY_TYPES.extension || entity.type === POSTGRES_ENTITY_TYPES.role) return { name };
|
|
44
|
+
if (entity.type === POSTGRES_ENTITY_TYPES.schema || entity.type === POSTGRES_ENTITY_TYPES.extension || entity.type === POSTGRES_ENTITY_TYPES.role || entity.type === POSTGRES_ENTITY_TYPES.eventTrigger) return { name };
|
|
45
|
+
// A cast is named by its two types, as the catalog prints them.
|
|
46
|
+
if (entity.type === POSTGRES_ENTITY_TYPES.cast) return { name: canonicalPgObject(entity.type, entity.props, defaultSchema).name };
|
|
45
47
|
const schema = typeof entity.props.schema === "string" ? entity.props.schema : identValue(defaultSchema);
|
|
46
|
-
if (
|
|
48
|
+
if (
|
|
49
|
+
entity.type === POSTGRES_ENTITY_TYPES.function ||
|
|
50
|
+
entity.type === POSTGRES_ENTITY_TYPES.procedure ||
|
|
51
|
+
entity.type === POSTGRES_ENTITY_TYPES.aggregate ||
|
|
52
|
+
entity.type === POSTGRES_ENTITY_TYPES.trigger ||
|
|
53
|
+
entity.type === POSTGRES_ENTITY_TYPES.policy
|
|
54
|
+
) {
|
|
47
55
|
const signature = canonicalPgObject(entity.type, entity.props, defaultSchema).signature;
|
|
48
56
|
return { schema, name, ...(signature !== undefined ? { signature } : {}) };
|
|
49
57
|
}
|
|
@@ -70,10 +78,11 @@ export function scopeFor(target: PostgresTarget, entities: ReadonlyArray<{ type:
|
|
|
70
78
|
if (target.schemas) return target.schemas;
|
|
71
79
|
if (entities.length === 0) return undefined;
|
|
72
80
|
const out = new Set<string>([target.defaultSchema]);
|
|
81
|
+
// The schema alone: the rest of an address (a routine's parameter types) needs props a caller may not have.
|
|
82
|
+
const unplaced = new Set<string>([POSTGRES_ENTITY_TYPES.extension, POSTGRES_ENTITY_TYPES.role, POSTGRES_ENTITY_TYPES.cast, POSTGRES_ENTITY_TYPES.eventTrigger]);
|
|
73
83
|
for (const e of entities) {
|
|
74
|
-
|
|
75
|
-
if (e.type ===
|
|
76
|
-
else if (a.schema) out.add(a.schema);
|
|
84
|
+
if (e.type === POSTGRES_ENTITY_TYPES.schema) out.add(String(e.props.name ?? ""));
|
|
85
|
+
else if (!unplaced.has(e.type)) out.add(typeof e.props.schema === "string" ? e.props.schema : identValue(target.defaultSchema));
|
|
77
86
|
}
|
|
78
87
|
return [...out].sort();
|
|
79
88
|
}
|