@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.
Files changed (198) hide show
  1. package/dist/clickhouse/access.d.ts +36 -7
  2. package/dist/clickhouse/access.d.ts.map +1 -1
  3. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  4. package/dist/clickhouse/entities.d.ts +60 -0
  5. package/dist/clickhouse/entities.d.ts.map +1 -1
  6. package/dist/clickhouse/import/generator.d.ts +1 -7
  7. package/dist/clickhouse/import/generator.d.ts.map +1 -1
  8. package/dist/clickhouse/import/live-export.d.ts +8 -0
  9. package/dist/clickhouse/import/live-export.d.ts.map +1 -1
  10. package/dist/clickhouse/live/catalog.d.ts +6 -5
  11. package/dist/clickhouse/live/catalog.d.ts.map +1 -1
  12. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  13. package/dist/clickhouse/live/undeclarable.d.ts +32 -0
  14. package/dist/clickhouse/live/undeclarable.d.ts.map +1 -0
  15. package/dist/clickhouse/parser.d.ts +16 -0
  16. package/dist/clickhouse/parser.d.ts.map +1 -1
  17. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  18. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  19. package/dist/clickhouse/plan/normalize.d.ts +6 -3
  20. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  21. package/dist/clickhouse/plan/rules.d.ts +3 -0
  22. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  23. package/dist/clickhouse/testing/fake-server.d.ts +6 -0
  24. package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
  25. package/dist/clickhouse/topology.d.ts.map +1 -1
  26. package/dist/clickhouse.d.ts +1 -1
  27. package/dist/clickhouse.d.ts.map +1 -1
  28. package/dist/codegen/coverage-page.d.ts +7 -0
  29. package/dist/codegen/coverage-page.d.ts.map +1 -0
  30. package/dist/codegen/docs.d.ts.map +1 -1
  31. package/dist/codegen/registry.d.ts.map +1 -1
  32. package/dist/core/undeclarable.d.ts +38 -0
  33. package/dist/core/undeclarable.d.ts.map +1 -0
  34. package/dist/coverage/clickhouse-commands.json +24 -0
  35. package/dist/coverage/clickhouse.json +25 -0
  36. package/dist/coverage/postgres-commands.json +245 -0
  37. package/dist/coverage/postgres.json +51 -0
  38. package/dist/coverage.d.ts +68 -0
  39. package/dist/coverage.d.ts.map +1 -1
  40. package/dist/files/clickhouse.d.ts +2 -1
  41. package/dist/files/clickhouse.d.ts.map +1 -1
  42. package/dist/files/common.d.ts +12 -0
  43. package/dist/files/common.d.ts.map +1 -1
  44. package/dist/files/postgres.d.ts +1 -1
  45. package/dist/files/postgres.d.ts.map +1 -1
  46. package/dist/import-parser.d.ts +7 -2
  47. package/dist/import-parser.d.ts.map +1 -1
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/integrity.json +10 -10
  51. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  52. package/dist/lint/post-synth/sql101.d.ts.map +1 -1
  53. package/dist/lint/rules/sqlch001.d.ts +1 -1
  54. package/dist/lint/rules/sqlch001.d.ts.map +1 -1
  55. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  56. package/dist/lint/rules/templates.d.ts +1 -1
  57. package/dist/lint/rules/templates.d.ts.map +1 -1
  58. package/dist/lsp/postgres-template.d.ts.map +1 -1
  59. package/dist/manifest.json +1 -1
  60. package/dist/meta.json +40 -0
  61. package/dist/okf/index.md +9 -1
  62. package/dist/okf/rules/SQL101.md +1 -0
  63. package/dist/okf/rules/SQLCH001.md +2 -2
  64. package/dist/okf/rules/SQLPG113.md +2 -0
  65. package/dist/okf/types/PostgresAggregate.md +13 -0
  66. package/dist/okf/types/PostgresCast.md +9 -0
  67. package/dist/okf/types/PostgresCollation.md +9 -0
  68. package/dist/okf/types/PostgresCompositeType.md +13 -0
  69. package/dist/okf/types/PostgresEventTrigger.md +9 -0
  70. package/dist/okf/types/PostgresRangeType.md +13 -0
  71. package/dist/okf/types/Quota.md +9 -0
  72. package/dist/okf/types/SettingsProfile.md +9 -0
  73. package/dist/plugin.d.ts.map +1 -1
  74. package/dist/postgres/apply/apply.d.ts.map +1 -1
  75. package/dist/postgres/apply/statements.d.ts +4 -3
  76. package/dist/postgres/apply/statements.d.ts.map +1 -1
  77. package/dist/postgres/entities.d.ts +129 -6
  78. package/dist/postgres/entities.d.ts.map +1 -1
  79. package/dist/postgres/entity-types.d.ts +6 -0
  80. package/dist/postgres/entity-types.d.ts.map +1 -1
  81. package/dist/postgres/import/generator.d.ts +3 -2
  82. package/dist/postgres/import/generator.d.ts.map +1 -1
  83. package/dist/postgres/import/live-export.d.ts +8 -0
  84. package/dist/postgres/import/live-export.d.ts.map +1 -1
  85. package/dist/postgres/live/catalog-kinds.d.ts +28 -0
  86. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -0
  87. package/dist/postgres/live/catalog.d.ts +23 -8
  88. package/dist/postgres/live/catalog.d.ts.map +1 -1
  89. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  90. package/dist/postgres/live/undeclarable.d.ts +42 -0
  91. package/dist/postgres/live/undeclarable.d.ts.map +1 -0
  92. package/dist/postgres/parser.d.ts +88 -5
  93. package/dist/postgres/parser.d.ts.map +1 -1
  94. package/dist/postgres/plan/deep.d.ts.map +1 -1
  95. package/dist/postgres/plan/diff.d.ts.map +1 -1
  96. package/dist/postgres/plan/normalize.d.ts +17 -2
  97. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  98. package/dist/postgres/plan/rules.d.ts +3 -0
  99. package/dist/postgres/plan/rules.d.ts.map +1 -1
  100. package/dist/postgres/plan/schema.d.ts +3 -2
  101. package/dist/postgres/plan/schema.d.ts.map +1 -1
  102. package/dist/postgres/testing/fake-client.d.ts +6 -0
  103. package/dist/postgres/testing/fake-client.d.ts.map +1 -1
  104. package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
  105. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  106. package/dist/postgres.d.ts +1 -1
  107. package/dist/postgres.d.ts.map +1 -1
  108. package/dist/rules/postgres-names.ts +15 -2
  109. package/dist/rules/postgres-templates.ts +2 -2
  110. package/dist/rules/sql101.ts +1 -0
  111. package/dist/rules/sqlch001.ts +10 -3
  112. package/dist/rules/sqlpg001.ts +6 -21
  113. package/dist/rules/sqlpg113.ts +1 -1
  114. package/dist/rules/templates.ts +2 -2
  115. package/dist/validate.d.ts.map +1 -1
  116. package/package.json +2 -2
  117. package/src/clickhouse/access.ts +197 -22
  118. package/src/clickhouse/apply/statements.ts +17 -5
  119. package/src/clickhouse/entities.ts +78 -2
  120. package/src/clickhouse/import/generator.ts +16 -3
  121. package/src/clickhouse/import/live-export.ts +17 -2
  122. package/src/clickhouse/live/catalog.ts +57 -10
  123. package/src/clickhouse/live/describe-resources.ts +3 -1
  124. package/src/clickhouse/live/kinds.e2e.test.ts +179 -0
  125. package/src/clickhouse/live/undeclarable.e2e.test.ts +65 -0
  126. package/src/clickhouse/live/undeclarable.ts +66 -0
  127. package/src/clickhouse/parser.test.ts +45 -2
  128. package/src/clickhouse/parser.ts +111 -3
  129. package/src/clickhouse/plan/deep.ts +25 -6
  130. package/src/clickhouse/plan/diff.ts +41 -1
  131. package/src/clickhouse/plan/normalize.ts +12 -4
  132. package/src/clickhouse/plan/rules.ts +22 -1
  133. package/src/clickhouse/quota-profile.test.ts +186 -0
  134. package/src/clickhouse/table-function.test.ts +54 -0
  135. package/src/clickhouse/testdata/server-ddl-baseline.json +17 -14
  136. package/src/clickhouse/testing/fake-server.ts +5 -0
  137. package/src/clickhouse/topology.ts +4 -2
  138. package/src/clickhouse.ts +6 -0
  139. package/src/codegen/classifier-page.test.ts +6 -0
  140. package/src/codegen/coverage-page.ts +77 -0
  141. package/src/codegen/docs.ts +5 -0
  142. package/src/codegen/postgres-classifier-page.ts +1 -1
  143. package/src/codegen/registry.ts +8 -0
  144. package/src/core/undeclarable.test.ts +141 -0
  145. package/src/core/undeclarable.ts +66 -0
  146. package/src/coverage/clickhouse-commands.json +24 -0
  147. package/src/coverage/clickhouse.json +25 -0
  148. package/src/coverage/postgres-commands.json +245 -0
  149. package/src/coverage/postgres.json +51 -0
  150. package/src/coverage.test.ts +78 -0
  151. package/src/coverage.ts +142 -0
  152. package/src/files/clickhouse.ts +20 -7
  153. package/src/files/common.ts +18 -0
  154. package/src/files/postgres.ts +25 -6
  155. package/src/generated/lexicon-sql.json +40 -0
  156. package/src/import-parser.ts +12 -4
  157. package/src/index.ts +2 -2
  158. package/src/lint/post-synth/postgres-names.ts +15 -2
  159. package/src/lint/post-synth/sql101.ts +1 -0
  160. package/src/lint/post-synth/sqlpg113.ts +1 -1
  161. package/src/lint/rules/postgres-templates.ts +2 -2
  162. package/src/lint/rules/sqlch001.ts +10 -3
  163. package/src/lint/rules/sqlpg001.ts +6 -21
  164. package/src/lint/rules/templates.ts +2 -2
  165. package/src/lsp/postgres-template.ts +14 -0
  166. package/src/mcp/index.ts +5 -5
  167. package/src/mcp/postgres.ts +1 -1
  168. package/src/plugin.ts +13 -2
  169. package/src/postgres/apply/apply.ts +8 -2
  170. package/src/postgres/apply/statements.ts +29 -6
  171. package/src/postgres/entities.test.ts +2 -2
  172. package/src/postgres/entities.ts +316 -28
  173. package/src/postgres/entity-types.ts +6 -0
  174. package/src/postgres/import/generator.ts +31 -4
  175. package/src/postgres/import/live-export.ts +25 -2
  176. package/src/postgres/kinds.test.ts +140 -0
  177. package/src/postgres/live/catalog-kinds.ts +312 -0
  178. package/src/postgres/live/catalog.ts +33 -11
  179. package/src/postgres/live/describe-resources.ts +14 -5
  180. package/src/postgres/live/kinds.e2e.test.ts +182 -0
  181. package/src/postgres/live/roundtrip.e2e.test.ts +9 -3
  182. package/src/postgres/live/undeclarable.e2e.test.ts +83 -0
  183. package/src/postgres/live/undeclarable.ts +141 -0
  184. package/src/postgres/parser.test.ts +2 -1
  185. package/src/postgres/parser.ts +307 -17
  186. package/src/postgres/plan/deep.ts +33 -0
  187. package/src/postgres/plan/diff.ts +45 -0
  188. package/src/postgres/plan/normalize.ts +153 -2
  189. package/src/postgres/plan/rules.ts +21 -0
  190. package/src/postgres/plan/schema.ts +6 -4
  191. package/src/postgres/testing/fake-client.ts +5 -0
  192. package/src/postgres/testing/roundtrip-schema.ts +19 -1
  193. package/src/postgres.ts +19 -0
  194. package/src/skill-defs.ts +1 -1
  195. package/src/skills/chant-sql-postgres.md +7 -3
  196. package/src/skills/chant-sql.md +3 -1
  197. package/src/unknown-clauses.test.ts +2 -1
  198. 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
- * - aggregates, and a trigger the server makes for itself (a foreign key's)
22
- * or clones onto a partition from its parent's;
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 default), as the server describes each. */
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, enums,
192
- * domains, sequences, tables, views and materialized views, indexes,
193
- * functions and procedures, triggers, and where access is read, policies and
194
- * the declared roles; each kind by schema then name, in byte order.
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 (entity.type === POSTGRES_ENTITY_TYPES.function || entity.type === POSTGRES_ENTITY_TYPES.procedure || entity.type === POSTGRES_ENTITY_TYPES.trigger || entity.type === POSTGRES_ENTITY_TYPES.policy) {
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
- const a = declaredAddress(e, target.defaultSchema);
75
- if (e.type === POSTGRES_ENTITY_TYPES.schema) out.add(a.name);
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
  }