@intentius/chant-lexicon-sql 0.124.19 → 0.124.22

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 (156) 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/live/catalog.d.ts +6 -5
  9. package/dist/clickhouse/live/catalog.d.ts.map +1 -1
  10. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  11. package/dist/clickhouse/parser.d.ts +16 -0
  12. package/dist/clickhouse/parser.d.ts.map +1 -1
  13. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  14. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  15. package/dist/clickhouse/plan/normalize.d.ts +6 -3
  16. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  17. package/dist/clickhouse/plan/rules.d.ts +3 -0
  18. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  19. package/dist/clickhouse/topology.d.ts.map +1 -1
  20. package/dist/clickhouse.d.ts +1 -1
  21. package/dist/clickhouse.d.ts.map +1 -1
  22. package/dist/codegen/registry.d.ts.map +1 -1
  23. package/dist/coverage/clickhouse.json +8 -8
  24. package/dist/coverage/postgres.json +6 -6
  25. package/dist/files/clickhouse.d.ts +2 -1
  26. package/dist/files/clickhouse.d.ts.map +1 -1
  27. package/dist/files/postgres.d.ts +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/integrity.json +10 -10
  31. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  32. package/dist/lint/post-synth/sql101.d.ts.map +1 -1
  33. package/dist/lint/rules/sqlch001.d.ts +1 -1
  34. package/dist/lint/rules/sqlch001.d.ts.map +1 -1
  35. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  36. package/dist/lint/rules/templates.d.ts +1 -1
  37. package/dist/lint/rules/templates.d.ts.map +1 -1
  38. package/dist/lsp/postgres-template.d.ts.map +1 -1
  39. package/dist/manifest.json +1 -1
  40. package/dist/meta.json +40 -0
  41. package/dist/okf/index.md +9 -1
  42. package/dist/okf/rules/SQL101.md +1 -0
  43. package/dist/okf/rules/SQLCH001.md +2 -2
  44. package/dist/okf/rules/SQLPG113.md +2 -0
  45. package/dist/okf/types/PostgresAggregate.md +13 -0
  46. package/dist/okf/types/PostgresCast.md +9 -0
  47. package/dist/okf/types/PostgresCollation.md +9 -0
  48. package/dist/okf/types/PostgresCompositeType.md +13 -0
  49. package/dist/okf/types/PostgresEventTrigger.md +9 -0
  50. package/dist/okf/types/PostgresRangeType.md +13 -0
  51. package/dist/okf/types/Quota.md +9 -0
  52. package/dist/okf/types/SettingsProfile.md +9 -0
  53. package/dist/plugin.d.ts.map +1 -1
  54. package/dist/postgres/apply/apply.d.ts.map +1 -1
  55. package/dist/postgres/apply/statements.d.ts +4 -3
  56. package/dist/postgres/apply/statements.d.ts.map +1 -1
  57. package/dist/postgres/entities.d.ts +129 -6
  58. package/dist/postgres/entities.d.ts.map +1 -1
  59. package/dist/postgres/entity-types.d.ts +6 -0
  60. package/dist/postgres/entity-types.d.ts.map +1 -1
  61. package/dist/postgres/import/generator.d.ts +3 -2
  62. package/dist/postgres/import/generator.d.ts.map +1 -1
  63. package/dist/postgres/live/catalog-kinds.d.ts +28 -0
  64. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -0
  65. package/dist/postgres/live/catalog.d.ts +15 -11
  66. package/dist/postgres/live/catalog.d.ts.map +1 -1
  67. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  68. package/dist/postgres/parser.d.ts +88 -5
  69. package/dist/postgres/parser.d.ts.map +1 -1
  70. package/dist/postgres/plan/deep.d.ts.map +1 -1
  71. package/dist/postgres/plan/diff.d.ts.map +1 -1
  72. package/dist/postgres/plan/normalize.d.ts +17 -2
  73. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  74. package/dist/postgres/plan/rules.d.ts +3 -0
  75. package/dist/postgres/plan/rules.d.ts.map +1 -1
  76. package/dist/postgres/plan/schema.d.ts +3 -2
  77. package/dist/postgres/plan/schema.d.ts.map +1 -1
  78. package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
  79. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  80. package/dist/postgres.d.ts +1 -1
  81. package/dist/postgres.d.ts.map +1 -1
  82. package/dist/rules/postgres-names.ts +15 -2
  83. package/dist/rules/postgres-templates.ts +2 -2
  84. package/dist/rules/sql101.ts +1 -0
  85. package/dist/rules/sqlch001.ts +10 -3
  86. package/dist/rules/sqlpg001.ts +6 -21
  87. package/dist/rules/sqlpg113.ts +1 -1
  88. package/dist/rules/templates.ts +2 -2
  89. package/dist/validate.d.ts.map +1 -1
  90. package/package.json +2 -2
  91. package/src/clickhouse/access.ts +197 -22
  92. package/src/clickhouse/apply/statements.ts +17 -5
  93. package/src/clickhouse/entities.ts +78 -2
  94. package/src/clickhouse/import/generator.ts +16 -3
  95. package/src/clickhouse/live/catalog.ts +57 -10
  96. package/src/clickhouse/live/describe-resources.ts +3 -1
  97. package/src/clickhouse/live/kinds.e2e.test.ts +179 -0
  98. package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -4
  99. package/src/clickhouse/parser.test.ts +45 -2
  100. package/src/clickhouse/parser.ts +111 -3
  101. package/src/clickhouse/plan/deep.ts +25 -6
  102. package/src/clickhouse/plan/diff.ts +41 -1
  103. package/src/clickhouse/plan/normalize.ts +12 -4
  104. package/src/clickhouse/plan/rules.ts +22 -1
  105. package/src/clickhouse/quota-profile.test.ts +186 -0
  106. package/src/clickhouse/table-function.test.ts +54 -0
  107. package/src/clickhouse/testdata/server-ddl-baseline.json +17 -14
  108. package/src/clickhouse/topology.ts +4 -2
  109. package/src/clickhouse.ts +6 -0
  110. package/src/codegen/postgres-classifier-page.ts +1 -1
  111. package/src/codegen/registry.ts +8 -0
  112. package/src/core/undeclarable.test.ts +22 -19
  113. package/src/coverage/clickhouse.json +8 -8
  114. package/src/coverage/postgres.json +6 -6
  115. package/src/files/clickhouse.ts +14 -6
  116. package/src/files/postgres.ts +19 -5
  117. package/src/generated/lexicon-sql.json +40 -0
  118. package/src/index.ts +2 -2
  119. package/src/lint/post-synth/postgres-names.ts +15 -2
  120. package/src/lint/post-synth/sql101.ts +1 -0
  121. package/src/lint/post-synth/sqlpg113.ts +1 -1
  122. package/src/lint/rules/postgres-templates.ts +2 -2
  123. package/src/lint/rules/sqlch001.ts +10 -3
  124. package/src/lint/rules/sqlpg001.ts +6 -21
  125. package/src/lint/rules/templates.ts +2 -2
  126. package/src/lsp/postgres-template.ts +14 -0
  127. package/src/mcp/index.ts +5 -5
  128. package/src/mcp/postgres.ts +1 -1
  129. package/src/plugin.ts +7 -1
  130. package/src/postgres/apply/apply.ts +8 -2
  131. package/src/postgres/apply/statements.ts +29 -6
  132. package/src/postgres/entities.test.ts +2 -2
  133. package/src/postgres/entities.ts +316 -28
  134. package/src/postgres/entity-types.ts +6 -0
  135. package/src/postgres/import/generator.ts +31 -4
  136. package/src/postgres/kinds.test.ts +140 -0
  137. package/src/postgres/live/catalog-kinds.ts +312 -0
  138. package/src/postgres/live/catalog.ts +30 -12
  139. package/src/postgres/live/describe-resources.ts +14 -5
  140. package/src/postgres/live/kinds.e2e.test.ts +182 -0
  141. package/src/postgres/live/roundtrip.e2e.test.ts +9 -3
  142. package/src/postgres/live/undeclarable.e2e.test.ts +15 -13
  143. package/src/postgres/parser.test.ts +2 -1
  144. package/src/postgres/parser.ts +307 -17
  145. package/src/postgres/plan/deep.ts +33 -0
  146. package/src/postgres/plan/diff.ts +45 -0
  147. package/src/postgres/plan/normalize.ts +153 -2
  148. package/src/postgres/plan/rules.ts +21 -0
  149. package/src/postgres/plan/schema.ts +6 -4
  150. package/src/postgres/testing/roundtrip-schema.ts +19 -1
  151. package/src/postgres.ts +19 -0
  152. package/src/skill-defs.ts +1 -1
  153. package/src/skills/chant-sql-postgres.md +7 -3
  154. package/src/skills/chant-sql.md +3 -1
  155. package/src/unknown-clauses.test.ts +2 -1
  156. package/src/validate.ts +8 -0
@@ -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
  }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Composite and range types, aggregates, collations, casts and event
3
+ * triggers against a live server (#3911): applied as declared, they plan
4
+ * with no change and read back with no drift; a definition changed in the
5
+ * declaration is replaced (CREATE OR REPLACE AGGREGATE), dropped and created,
6
+ * or refused while something depends on it; an import writes each back as
7
+ * the declaration it came from, which plans with no change.
8
+ *
9
+ * The server: a throwaway one at the pin, skipped cleanly when Docker is not
10
+ * available. An event trigger is the database's and fires for every DDL in
11
+ * it, so this test does not run against a shared server
12
+ * (`CHANT_SQL_E2E_POSTGRES_URL`).
13
+ */
14
+
15
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
19
+ import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
20
+ import { connectPostgres, type PostgresClient, type PostgresEndpoint } from "./client";
21
+ import { readLiveSchema } from "./catalog";
22
+ import { planPgAgainstServer } from "../plan/commands";
23
+ import { observeResourcesDeep, liveProps } from "../plan/deep";
24
+ import { postgresApply } from "../../op/activities/postgres-apply";
25
+ import { objectsToIR } from "../import/ir";
26
+ import { PostgresGenerator, postgresTemplates } from "../import/generator";
27
+ import { declaredObjects } from "../apply/apply";
28
+
29
+ const SCHEMA = "chant_e2e_3911";
30
+ const enabled = await dockerAvailable();
31
+ const dir = mkdtempSync(join(tmpdir(), "chant-pg-kinds-"));
32
+ let server: TestPostgres | undefined;
33
+ let endpoint: PostgresEndpoint;
34
+ let admin: PostgresClient;
35
+
36
+ beforeAll(async () => {
37
+ if (!enabled) return;
38
+ server = await startTestPostgres();
39
+ endpoint = server.endpoint();
40
+ admin = await connectPostgres(endpoint);
41
+ }, 600_000);
42
+
43
+ afterAll(async () => {
44
+ await admin?.end();
45
+ await server?.stop();
46
+ rmSync(dir, { recursive: true, force: true });
47
+ });
48
+
49
+ interface Obj {
50
+ export: string;
51
+ type: string;
52
+ ddl: string;
53
+ dependsOn?: string[];
54
+ }
55
+
56
+ const MARKER = { stack: "e2e3911", env: "test" };
57
+ const profile = () => ({
58
+ config: { ownership: MARKER, sql: { profiles: { e2e: { url: endpoint.url, password: { env: "PG_E2E_PASSWORD" }, schemas: [SCHEMA] } } } },
59
+ env: { PG_E2E_PASSWORD: endpoint.password ?? "" },
60
+ });
61
+
62
+ function writeBuild(file: string, objects: Obj[]): string {
63
+ const path = join(dir, file);
64
+ writeFileSync(path, JSON.stringify({ dialect: "postgres", postgresMajor: 18, objects: objects.map((o) => ({ dependsOn: [], ...o })) }));
65
+ return path;
66
+ }
67
+
68
+ const apply = (path: string, prune = false) => postgresApply({ buildPath: path, environment: "e2e", ...(prune ? { prune: true } : {}) }, undefined, { ...profile(), log: () => undefined });
69
+ const plan = (path: string) => planPgAgainstServer("e2e", path, profile());
70
+
71
+ const S = SCHEMA;
72
+ interface Version {
73
+ /** The composite type's attributes. */
74
+ pair: string;
75
+ /** The aggregate's INITCOND. */
76
+ initcond: string;
77
+ /** The range's SUBTYPE_DIFF clause. */
78
+ diff: string;
79
+ /** The collation's locale. */
80
+ locale: string;
81
+ /** The cast's context. */
82
+ context: string;
83
+ /** The event trigger's tags. */
84
+ tags: string;
85
+ }
86
+
87
+ const objects = (v: Version): Obj[] => [
88
+ { export: "app", type: "Postgres::Schema", ddl: `CREATE SCHEMA ${S}` },
89
+ { export: "ci", type: "Postgres::Collation", dependsOn: ["app"], ddl: `CREATE COLLATION ${S}.ci (provider = icu, locale = '${v.locale}', deterministic = false);\nCOMMENT ON COLLATION ${S}.ci IS 'Case-insensitive'` },
90
+ { export: "pair", type: "Postgres::CompositeType", dependsOn: ["app"], ddl: `CREATE TYPE ${S}.pair AS (${v.pair});\nCOMMENT ON TYPE ${S}.pair IS 'Two values'` },
91
+ { export: "floatrange", type: "Postgres::RangeType", dependsOn: ["app"], ddl: `CREATE TYPE ${S}.floatrange AS RANGE (SUBTYPE = float8${v.diff})` },
92
+ { export: "people", type: "Postgres::Table", dependsOn: ["ci", "floatrange"], ddl: `CREATE TABLE ${S}.people (id int PRIMARY KEY, nick text COLLATE ${S}.ci, span ${S}.floatrange)` },
93
+ { export: "add2", type: "Postgres::Function", dependsOn: ["app"], ddl: `CREATE FUNCTION ${S}.add2(a integer, b integer) RETURNS integer LANGUAGE sql IMMUTABLE AS $$ SELECT a + b $$` },
94
+ {
95
+ export: "total",
96
+ type: "Postgres::Aggregate",
97
+ dependsOn: ["add2"],
98
+ ddl: `CREATE AGGREGATE ${S}.total(integer) (SFUNC = ${S}.add2, STYPE = int4, INITCOND = ${v.initcond}, PARALLEL = SAFE);\nCOMMENT ON AGGREGATE ${S}.total(int) IS 'A sum'`,
99
+ },
100
+ { export: "pairText", type: "Postgres::Function", dependsOn: ["pair"], ddl: `CREATE FUNCTION ${S}.pair_text(p ${S}.pair) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT p.a || ':' || p.b $$` },
101
+ { export: "pairCast", type: "Postgres::Cast", dependsOn: ["pair", "pairText"], ddl: `CREATE CAST (${S}.pair AS text) WITH FUNCTION ${S}.pair_text(${S}.pair)${v.context}` },
102
+ { export: "audit", type: "Postgres::Function", dependsOn: ["app"], ddl: `CREATE FUNCTION ${S}.audit() RETURNS event_trigger LANGUAGE plpgsql AS $$ BEGIN END $$` },
103
+ { export: "ddlAudit", type: "Postgres::EventTrigger", dependsOn: ["audit"], ddl: `CREATE EVENT TRIGGER ddl_audit_3911 ON ddl_command_end WHEN TAG IN (${v.tags}) EXECUTE FUNCTION ${S}.audit();\nCOMMENT ON EVENT TRIGGER ddl_audit_3911 IS 'Audits DDL'` },
104
+ ];
105
+
106
+ const V1: Version = { pair: "a integer, b text COLLATE \"C\"", initcond: "0", diff: ", SUBTYPE_DIFF = float8mi", locale: "und-u-ks-level2", context: " AS ASSIGNMENT", tags: "'CREATE TABLE'" };
107
+ // Each kind changed: the aggregate's INITCOND (OR REPLACE), the range's SUBTYPE_DIFF (a column uses the type: refused), the cast's context and the event trigger's tags (drop and create).
108
+ const V2: Version = { ...V1, initcond: "'10'", diff: "", context: " AS IMPLICIT", tags: "'CREATE TABLE', 'ALTER TABLE'" };
109
+
110
+ describe.skipIf(!enabled)("types, aggregates, collations, casts and event triggers on a live server", () => {
111
+ test("apply, plan, drift, change and import", async () => {
112
+ const v1 = writeBuild("v1.json", objects(V1));
113
+ const first = await apply(v1);
114
+ expect(first.failed).toEqual([]);
115
+ expect(first.applied.map((a) => [a.name, a.action])).toEqual([
116
+ [S, "created"],
117
+ [`${S}.ci`, "created"],
118
+ [`${S}.pair`, "created"],
119
+ [`${S}.floatrange`, "created"],
120
+ [`${S}.people`, "created"],
121
+ [`${S}.add2(integer,integer)`, "created"],
122
+ [`${S}.total(integer)`, "created"],
123
+ [`${S}.pair_text(${S}.pair)`, "created"],
124
+ [`(${S}.pair AS text)`, "created"],
125
+ [`${S}.audit()`, "created"],
126
+ ["ddl_audit_3911", "created"],
127
+ ]);
128
+ // Each works: the aggregate sums, the cast casts, the collation compares without case, the range has its multirange.
129
+ expect((await admin.query<{ t: number }>(`SELECT ${S}.total(x) AS t FROM generate_series(1, 4) x`))[0]!.t).toBe(10);
130
+ expect((await admin.query<{ t: string }>(`SELECT ROW(1, 'x')::${S}.pair::text AS t`))[0]!.t).toBe("1:x");
131
+ expect((await admin.query<{ eq: boolean }>(`SELECT 'Ab' = 'aB' COLLATE ${S}.ci AS eq`))[0]!.eq).toBe(true);
132
+ expect((await admin.query<{ m: string }>(`SELECT '{[1,2)}'::${S}.floatmultirange::text AS m`))[0]!.m).toBe("{[1,2)}");
133
+
134
+ // As declared: no change, and no drift.
135
+ expect((await plan(v1)).changes).toEqual([]);
136
+ const declared = declaredObjects(JSON.stringify({ dialect: "postgres", objects: objects(V1).map((o) => ({ dependsOn: [], ...o })) }));
137
+ const entities = new Map(declared.map((o) => [o.exportName, { entityType: o.type, props: o.props }]));
138
+ const deep = await observeResourcesDeep({ environment: "e2e", entityNames: [...entities.keys()], entities, ...profile() });
139
+ expect(deep.unobserved ?? {}).toEqual({});
140
+ const strip = (p: Record<string, unknown>) => Object.fromEntries(Object.entries(p).filter(([k]) => !["ddl", "source", "reads", "orReplace"].includes(k)));
141
+ for (const [name, e] of entities) expect(strip(deep.resources[name]!.properties), name).toEqual(strip(e.props));
142
+
143
+ // Changed by hand: drift, in the plan.
144
+ await admin.query(`CREATE OR REPLACE AGGREGATE ${S}.total(integer) (SFUNC = ${S}.add2, STYPE = int4, INITCOND = '5', PARALLEL = SAFE)`);
145
+ expect((await plan(v1)).changes.map((c) => [c.object, c.field, c.rule])).toEqual([[`total (${S}.total(integer))`, "definition", "SQLPG302"]]);
146
+
147
+ // Changed in the declaration: each with its rule; the range is used by a column, so its change is refused.
148
+ const v2 = writeBuild("v2.json", objects(V2));
149
+ const planned = await plan(v2);
150
+ expect(planned.changes.map((c) => [c.object, c.rule])).toEqual([
151
+ [`floatrange (${S}.floatrange)`, "SQLPG301"],
152
+ [`total (${S}.total(integer))`, "SQLPG302"],
153
+ [`pairCast ((${S}.pair AS text))`, "SQLPG300"],
154
+ ["ddlAudit (ddl_audit_3911)", "SQLPG300"],
155
+ ]);
156
+ expect(planned.refused.map((c) => c.object)).toEqual([`floatrange (${S}.floatrange)`]);
157
+ const v2Applied = writeBuild("v2-applied.json", objects({ ...V2, diff: V1.diff }));
158
+ const second = await apply(v2Applied);
159
+ expect(second.failed).toEqual([]);
160
+ expect(second.applied.filter((a) => a.action === "updated").map((a) => a.name)).toEqual([`${S}.total(integer)`, `(${S}.pair AS text)`, "ddl_audit_3911"]);
161
+ expect((await plan(v2Applied)).changes).toEqual([]);
162
+ expect((await admin.query<{ t: number }>(`SELECT ${S}.total(x) AS t FROM generate_series(1, 4) x`))[0]!.t).toBe(20);
163
+
164
+ // Import: the server's own printing, written back as declarations that parse and plan with no change.
165
+ const live = await readLiveSchema(admin, { schemas: [S] });
166
+ for (const o of live) liveProps(o);
167
+ const ir = objectsToIR(live.map((o) => ({ type: o.type, ...(o.schema ? { schema: o.schema } : {}), name: o.name, ddl: o.statement })));
168
+ const [file] = new PostgresGenerator().generate(ir);
169
+ expect(file!.content).toContain("export const ci = collation`");
170
+ expect(file!.content).toContain("export const pair = type`");
171
+ expect(file!.content).toContain("export const floatrange = type`");
172
+ expect(file!.content).toContain("export const total = aggregate`");
173
+ expect(file!.content).toContain("SFUNC = ${add2}");
174
+ expect(file!.content).toContain("cast`\n CREATE CAST (${pair} AS text) WITH FUNCTION ${pairText}(${pair}) AS IMPLICIT");
175
+ expect(file!.content).toContain("export const ddlAudit3911 = eventTrigger`");
176
+ // The imported statements, in the generator's order, as a build: no change against the server they came from.
177
+ const items = ir.resources.map((r) => ({ exportName: r.logicalId, type: r.type, ...(r.properties.schema ? { schema: String(r.properties.schema) } : {}), name: String(r.properties.name), ddl: String(r.properties.ddl) }));
178
+ const { order } = postgresTemplates(items);
179
+ const imported = writeBuild("imported.json", order.map((it) => ({ export: it.exportName, type: it.type, ddl: it.ddl })));
180
+ expect((await plan(imported)).changes).toEqual([]);
181
+ }, 600_000);
182
+ });
@@ -2,9 +2,9 @@
2
2
  * The live round trip (#3279): a schema on one Postgres database is imported
3
3
  * as chant declarations, built, and applied to a second, empty database;
4
4
  * both catalogs then print the same statements for every object, exporting
5
- * the second gives back the same declarations, every declared object is
6
- * present, and the deep read of the applied database reports no drift from
7
- * the declarations.
5
+ * the second gives back the same declarations, a plan of the build against
6
+ * either database has no change, every declared object is present, and the
7
+ * deep read of the applied database reports no drift from the declarations.
8
8
  *
9
9
  * Needs Docker; skips cleanly without it. One throwaway `postgres` container
10
10
  * at the pin (18.6, by tag and digest), removed afterwards even on failure.
@@ -22,6 +22,7 @@ import { PostgresGenerator } from "../import/generator";
22
22
  import { readLiveSchema } from "./catalog";
23
23
  import { describeResources } from "./describe-resources";
24
24
  import { observeResourcesDeep } from "../plan/deep";
25
+ import { planPgAgainstServer } from "../plan/commands";
25
26
  import { sqlPlugin } from "../../plugin";
26
27
  import { POSTGRES_DDL_FILE, sqlSerializer } from "../../serializer";
27
28
 
@@ -90,6 +91,11 @@ describe.skipIf(!enabled)("import, build, apply, and the catalog is unchanged",
90
91
  // The catalog is unchanged.
91
92
  expect(await catalog("target")).toEqual(await catalog("source"));
92
93
 
94
+ // A plan of the build against either database: no change (#3911).
95
+ const buildFile = join(projectDir, "schema.json");
96
+ writeFileSync(buildFile, out.primary);
97
+ for (const db of ["target", "source"]) expect((await planPgAgainstServer("rt", buildFile, profile(db))).changes, db).toEqual([]);
98
+
93
99
  // Exporting the applied database gives back the same declarations.
94
100
  const again = await exportResources({ environment: "rt", ...profile("target") });
95
101
  expect(new PostgresGenerator().generate(again)[0]!.content).toBe(file!.content);
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Import against a real Postgres server never leaves an object out without a
3
- * word (#3911): a database holding a composite type, an aggregate, a
4
- * collation, a cast and an event trigger beside a table fails the import,
5
- * which names all five; with `skipUndeclarable` the table is imported and
6
- * the five are warnings. What an extension brings (hstore's casts and
7
- * operators) is the extension's, and is not refused.
3
+ * word (#3911): a database holding a rewrite rule, an encoding conversion, an
4
+ * access method and a shell type beside a table fails the import, which
5
+ * names all four; with `skipUndeclarable` the rest is imported and the four
6
+ * are warnings. A composite type, an aggregate, a collation, a cast and an
7
+ * event trigger are declared kinds, so they are imported, not refused. What
8
+ * an extension brings (hstore's casts and operators) is the extension's, and
9
+ * is not refused.
8
10
  *
9
11
  * Needs Docker; skips cleanly without it. One throwaway `postgres` container
10
12
  * at the pin, removed afterwards even on failure.
@@ -30,6 +32,10 @@ CREATE FUNCTION app.pair_text(app.pair) RETURNS text LANGUAGE sql AS $$ SELECT (
30
32
  CREATE CAST (app.pair AS text) WITH FUNCTION app.pair_text(app.pair);
31
33
  CREATE FUNCTION app.on_ddl() RETURNS event_trigger LANGUAGE plpgsql AS $$ BEGIN END $$;
32
34
  CREATE EVENT TRIGGER audit_ddl ON ddl_command_end EXECUTE FUNCTION app.on_ddl();
35
+ CREATE RULE users_notify AS ON INSERT TO app.users DO ALSO NOTIFY users;
36
+ CREATE CONVERSION app.latin1_utf8 FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8;
37
+ CREATE ACCESS METHOD heap2 TYPE TABLE HANDLER heap_tableam_handler;
38
+ CREATE TYPE app.later;
33
39
  `;
34
40
 
35
41
  beforeAll(async () => {
@@ -52,13 +58,7 @@ const profile = () => ({
52
58
  env: { PG_UD_PASSWORD: server!.endpoint("source").password },
53
59
  });
54
60
 
55
- const NAMED = [
56
- "aggregate app.total(integer)",
57
- "cast (app.pair AS text)",
58
- "collation app.ci",
59
- "event trigger audit_ddl",
60
- "composite type app.pair",
61
- ];
61
+ const NAMED = ["rule app.users_notify ON users", "conversion app.latin1_utf8", "access method heap2", "base type app.later"];
62
62
 
63
63
  describe.skipIf(!enabled)("importing a database with objects chant cannot declare", () => {
64
64
  test("fails, naming every one of them", async () => {
@@ -74,7 +74,9 @@ describe.skipIf(!enabled)("importing a database with objects chant cannot declar
74
74
 
75
75
  test("with skipUndeclarable, imports the rest and lists them as warnings", async () => {
76
76
  const ir = await exportResources({ environment: "src", skipUndeclarable: true, ...profile() });
77
- expect(ir.resources.map((r) => r.properties.name)).toEqual(expect.arrayContaining(["users", "hstore", "add2", "pair_text", "on_ddl"]));
77
+ expect(ir.resources.map((r) => r.properties.name)).toEqual(
78
+ expect.arrayContaining(["users", "hstore", "add2", "pair_text", "on_ddl", "pair", "total", "ci", "(app.pair AS text)", "audit_ddl"]),
79
+ );
78
80
  const left = (ir.warnings ?? []).filter((w) => w.includes("is left out (--skip-undeclarable)"));
79
81
  expect(left.map((w) => w.split(" is left out")[0]).sort()).toEqual([...NAMED].sort());
80
82
  }, 120_000);
@@ -164,7 +164,8 @@ describe("the other statements", () => {
164
164
  expect(s.options.map((o) => o.option)).toEqual(["AS", "INCREMENT", "NO MINVALUE", "MAXVALUE", "START", "CACHE", "NO CYCLE", "OWNED BY"]);
165
165
  expect(parse("CREATE DOMAIN d AS text CONSTRAINT c CHECK (VALUE <> '') NOT VALID")[0]!.statement).toBe("domain");
166
166
  expect(parse("CREATE TYPE mood AS ENUM ('sad', 'ok')")[0]!.statement).toBe("enum");
167
- expect(at("CREATE TYPE mood AS (a int)")).toBe("(");
167
+ expect(parse("CREATE TYPE pair AS (a int)")[0]!.statement).toBe("composite");
168
+ expect(at("CREATE TYPE t (INPUT = t_in)")).toBe("(");
168
169
  expect(parse("CREATE SCHEMA IF NOT EXISTS app AUTHORIZATION owner")[0]!.statement).toBe("schema");
169
170
  expect(parse("CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public VERSION '1.6' CASCADE")[0]!.statement).toBe("extension");
170
171
  });