@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,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,
|
|
6
|
-
*
|
|
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);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import against a real Postgres server never leaves an object out without a
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* Needs Docker; skips cleanly without it. One throwaway `postgres` container
|
|
12
|
+
* at the pin, removed afterwards even on failure.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
16
|
+
import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
|
|
17
|
+
import { exportResources } from "../import/live-export";
|
|
18
|
+
import { UndeclarableObjectsError } from "../../core/undeclarable";
|
|
19
|
+
|
|
20
|
+
const enabled = await dockerAvailable();
|
|
21
|
+
let server: TestPostgres | undefined;
|
|
22
|
+
|
|
23
|
+
const SEED = `
|
|
24
|
+
CREATE EXTENSION hstore WITH SCHEMA public;
|
|
25
|
+
CREATE SCHEMA app;
|
|
26
|
+
CREATE TABLE app.users (id bigint PRIMARY KEY, name text);
|
|
27
|
+
CREATE TYPE app.pair AS (a integer, b text);
|
|
28
|
+
CREATE FUNCTION app.add2(integer, integer) RETURNS integer LANGUAGE sql AS $$ SELECT $1 + $2 $$;
|
|
29
|
+
CREATE AGGREGATE app.total(integer) (sfunc = app.add2, stype = integer, initcond = '0');
|
|
30
|
+
CREATE COLLATION app.ci (provider = icu, locale = 'und-u-ks-level2', deterministic = false);
|
|
31
|
+
CREATE FUNCTION app.pair_text(app.pair) RETURNS text LANGUAGE sql AS $$ SELECT ($1).b $$;
|
|
32
|
+
CREATE CAST (app.pair AS text) WITH FUNCTION app.pair_text(app.pair);
|
|
33
|
+
CREATE FUNCTION app.on_ddl() RETURNS event_trigger LANGUAGE plpgsql AS $$ BEGIN END $$;
|
|
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;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
if (!enabled) return;
|
|
43
|
+
server = await startTestPostgres();
|
|
44
|
+
const admin = await server.connect();
|
|
45
|
+
await admin.query("CREATE DATABASE source");
|
|
46
|
+
await admin.end();
|
|
47
|
+
const source = await server.connect("source");
|
|
48
|
+
await source.query(SEED);
|
|
49
|
+
await source.end();
|
|
50
|
+
}, 600_000);
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await server?.stop();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const profile = () => ({
|
|
57
|
+
config: { sql: { profiles: { src: { url: server!.endpoint("source").url, password: { env: "PG_UD_PASSWORD" } } } } },
|
|
58
|
+
env: { PG_UD_PASSWORD: server!.endpoint("source").password },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const NAMED = ["rule app.users_notify ON users", "conversion app.latin1_utf8", "access method heap2", "base type app.later"];
|
|
62
|
+
|
|
63
|
+
describe.skipIf(!enabled)("importing a database with objects chant cannot declare", () => {
|
|
64
|
+
test("fails, naming every one of them", async () => {
|
|
65
|
+
const err = await exportResources({ environment: "src", ...profile() }).then(
|
|
66
|
+
() => undefined,
|
|
67
|
+
(e: unknown) => e,
|
|
68
|
+
);
|
|
69
|
+
expect(err).toBeInstanceOf(UndeclarableObjectsError);
|
|
70
|
+
const objects = (err as UndeclarableObjectsError).objects.map((o) => `${o.kind} ${o.schema ? `${o.schema}.` : ""}${o.name}`);
|
|
71
|
+
expect([...objects].sort()).toEqual([...NAMED].sort());
|
|
72
|
+
for (const line of NAMED) expect((err as Error).message).toContain(` - ${line}\n`);
|
|
73
|
+
}, 120_000);
|
|
74
|
+
|
|
75
|
+
test("with skipUndeclarable, imports the rest and lists them as warnings", async () => {
|
|
76
|
+
const ir = await exportResources({ environment: "src", skipUndeclarable: true, ...profile() });
|
|
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
|
+
);
|
|
80
|
+
const left = (ir.warnings ?? []).filter((w) => w.includes("is left out (--skip-undeclarable)"));
|
|
81
|
+
expect(left.map((w) => w.split(" is left out")[0]).sort()).toEqual([...NAMED].sort());
|
|
82
|
+
}, 120_000);
|
|
83
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The objects in a live Postgres database of the kinds no declaration holds
|
|
3
|
+
* (#3911), found so import can refuse them by name instead of leaving them
|
|
4
|
+
* out (`../../core/undeclarable.ts`).
|
|
5
|
+
*
|
|
6
|
+
* One catalog query per kind the coverage table marks refused
|
|
7
|
+
* (`../../coverage/postgres.json`), keyed by the row's `kind`. A kind whose
|
|
8
|
+
* row is flipped to declared is no longer asked about: its objects are then
|
|
9
|
+
* the catalog reader's (`./catalog.ts`) to read.
|
|
10
|
+
*
|
|
11
|
+
* Left out, as the reader leaves them out: the server's own schemas, objects
|
|
12
|
+
* an extension owns (`pg_depend.deptype = 'e'`; the extension is declared),
|
|
13
|
+
* the objects `initdb` makes, whose oids are below 16384
|
|
14
|
+
* (`FirstNormalObjectId`), and an object that is part of another
|
|
15
|
+
* (`deptype = 'i'`: the casts a range type makes for itself). An object in a
|
|
16
|
+
* schema is looked for in the schemas in scope; one that belongs to the whole
|
|
17
|
+
* database (a cast, an event trigger, a publication, a foreign data wrapper)
|
|
18
|
+
* is always looked for.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { PostgresClient } from "./client";
|
|
22
|
+
import { notExtensionOwned, scopeSql, type SchemaScope } from "./catalog";
|
|
23
|
+
import { KIND_COVERAGE } from "../../coverage";
|
|
24
|
+
import type { UndeclarableObject } from "../../core/undeclarable";
|
|
25
|
+
|
|
26
|
+
/** `FirstNormalObjectId`: every oid below it is the server's own. */
|
|
27
|
+
const FIRST_USER_OID = 16384;
|
|
28
|
+
|
|
29
|
+
interface Probe {
|
|
30
|
+
/**
|
|
31
|
+
* A query returning `schema` (NULL for a database-wide object), `name`, and
|
|
32
|
+
* for a database-wide object the role that owns it (so a managed
|
|
33
|
+
* provider's own can be told apart); `scope` is the predicate on `n.nspname`.
|
|
34
|
+
*/
|
|
35
|
+
sql(scope: string): string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const C = 'COLLATE "C"';
|
|
39
|
+
|
|
40
|
+
/** Not part of another object (`pg_depend.deptype = 'i'`): the multirange cast a range type makes for itself is the range type's. */
|
|
41
|
+
const notInternal = (catalog: string, oid: 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
|
+
const ns = (alias: string, column: string) => `JOIN pg_catalog.pg_namespace ${alias} ON ${alias}.oid = ${column}`;
|
|
44
|
+
const args = (oid: string) => `'(' || pg_catalog.replace(pg_catalog.oidvectortypes(${oid}), ', ', ',') || ')'`;
|
|
45
|
+
|
|
46
|
+
/** A probe for objects of a catalog in a schema, the catalog aliased `x`; `name` is the name expression. */
|
|
47
|
+
const inSchema = (catalog: string, nsColumn: string, name: string, where = "TRUE"): Probe => ({
|
|
48
|
+
sql: (scope) =>
|
|
49
|
+
`SELECT n.nspname AS schema, ${name} AS name, NULL::text AS owner FROM pg_catalog.${catalog} x ${ns("n", `x.${nsColumn}`)}
|
|
50
|
+
WHERE ${where} AND ${scope} AND x.oid >= ${FIRST_USER_OID} AND ${notExtensionOwned(catalog, "x.oid")} AND ${notInternal(catalog, "x.oid")}`,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** A probe for objects that belong to the database, not a schema. */
|
|
54
|
+
const databaseWide = (catalog: string, name: string, owner?: string): Probe => ({
|
|
55
|
+
sql: () =>
|
|
56
|
+
`SELECT NULL::text AS schema, ${name} AS name, ${owner ? `pg_catalog.pg_get_userbyid(x.${owner})` : "NULL::text"} AS owner FROM pg_catalog.${catalog} x
|
|
57
|
+
WHERE x.oid >= ${FIRST_USER_OID} AND ${notExtensionOwned(catalog, "x.oid")} AND ${notInternal(catalog, "x.oid")}`,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** The pg_type rows of one `typtype`. */
|
|
61
|
+
const type = (where: string) => inSchema("pg_type", "typnamespace", "x.typname", where);
|
|
62
|
+
|
|
63
|
+
export const POSTGRES_PROBES: Readonly<Record<string, Probe>> = {
|
|
64
|
+
"access method": databaseWide("pg_am", "x.amname"),
|
|
65
|
+
aggregate: inSchema("pg_proc", "pronamespace", `x.proname || ${args("x.proargtypes")}`, "x.prokind = 'a'"),
|
|
66
|
+
"routine with a SQL-standard body": inSchema("pg_proc", "pronamespace", `x.proname || ${args("x.proargtypes")}`, "x.prokind IN ('f', 'p', 'w') AND x.prosqlbody IS NOT NULL"),
|
|
67
|
+
cast: databaseWide("pg_cast", "'(' || pg_catalog.format_type(x.castsource, NULL) || ' AS ' || pg_catalog.format_type(x.casttarget, NULL) || ')'"),
|
|
68
|
+
collation: inSchema("pg_collation", "collnamespace", "x.collname"),
|
|
69
|
+
conversion: inSchema("pg_conversion", "connamespace", "x.conname"),
|
|
70
|
+
"event trigger": databaseWide("pg_event_trigger", "x.evtname", "evtowner"),
|
|
71
|
+
"foreign data wrapper": databaseWide("pg_foreign_data_wrapper", "x.fdwname", "fdwowner"),
|
|
72
|
+
"foreign server": databaseWide("pg_foreign_server", "x.srvname", "srvowner"),
|
|
73
|
+
"user mapping": {
|
|
74
|
+
// pg_user_mapping is the superuser's to read; the view shows every mapping without its options.
|
|
75
|
+
sql: () =>
|
|
76
|
+
`SELECT NULL::text AS schema, 'for ' || x.usename || ' on server ' || x.srvname AS name, pg_catalog.pg_get_userbyid(s.srvowner) AS owner FROM pg_catalog.pg_user_mappings x
|
|
77
|
+
JOIN pg_catalog.pg_foreign_server s ON s.oid = x.srvid
|
|
78
|
+
WHERE ${notExtensionOwned("pg_foreign_server", "s.oid")}`,
|
|
79
|
+
},
|
|
80
|
+
"foreign table": inSchema("pg_class", "relnamespace", "x.relname", "x.relkind = 'f'"),
|
|
81
|
+
"procedural language": databaseWide("pg_language", "x.lanname", "lanowner"),
|
|
82
|
+
operator: inSchema("pg_operator", "oprnamespace", "x.oprname || '(' || pg_catalog.format_type(x.oprleft, NULL) || ',' || pg_catalog.format_type(x.oprright, NULL) || ')'"),
|
|
83
|
+
"operator class": inSchema("pg_opclass", "opcnamespace", "x.opcname || ' USING ' || (SELECT a.amname FROM pg_catalog.pg_am a WHERE a.oid = x.opcmethod)"),
|
|
84
|
+
// A family CREATE OPERATOR CLASS made for itself, named as its class, is the class's.
|
|
85
|
+
"operator family": inSchema(
|
|
86
|
+
"pg_opfamily",
|
|
87
|
+
"opfnamespace",
|
|
88
|
+
"x.opfname || ' USING ' || (SELECT a.amname FROM pg_catalog.pg_am a WHERE a.oid = x.opfmethod)",
|
|
89
|
+
"NOT EXISTS (SELECT 1 FROM pg_catalog.pg_opclass c WHERE c.opcfamily = x.oid AND c.opcname = x.opfname AND c.opcnamespace = x.opfnamespace)",
|
|
90
|
+
),
|
|
91
|
+
publication: databaseWide("pg_publication", "x.pubname", "pubowner"),
|
|
92
|
+
subscription: {
|
|
93
|
+
sql: () =>
|
|
94
|
+
`SELECT NULL::text AS schema, x.subname AS name, pg_catalog.pg_get_userbyid(x.subowner) AS owner FROM pg_catalog.pg_subscription x
|
|
95
|
+
WHERE x.subdbid = (SELECT d.oid FROM pg_catalog.pg_database d WHERE d.datname = pg_catalog.current_database())`,
|
|
96
|
+
},
|
|
97
|
+
rule: {
|
|
98
|
+
sql: (scope) =>
|
|
99
|
+
`SELECT n.nspname AS schema, r.rulename || ' ON ' || c.relname AS name, NULL::text AS owner
|
|
100
|
+
FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class ${ns("n", "c.relnamespace")}
|
|
101
|
+
WHERE r.rulename <> '_RETURN' AND ${scope} AND ${notExtensionOwned("pg_class", "c.oid")}`,
|
|
102
|
+
},
|
|
103
|
+
"extended statistics": inSchema("pg_statistic_ext", "stxnamespace", "x.stxname"),
|
|
104
|
+
"text search configuration": inSchema("pg_ts_config", "cfgnamespace", "x.cfgname"),
|
|
105
|
+
"text search dictionary": inSchema("pg_ts_dict", "dictnamespace", "x.dictname"),
|
|
106
|
+
"text search parser": inSchema("pg_ts_parser", "prsnamespace", "x.prsname"),
|
|
107
|
+
"text search template": inSchema("pg_ts_template", "tmplnamespace", "x.tmplname"),
|
|
108
|
+
transform: databaseWide(
|
|
109
|
+
"pg_transform",
|
|
110
|
+
"'FOR ' || pg_catalog.format_type(x.trftype, NULL) || ' LANGUAGE ' || (SELECT l.lanname FROM pg_catalog.pg_language l WHERE l.oid = x.trflang)",
|
|
111
|
+
),
|
|
112
|
+
"composite type": type("x.typtype = 'c' AND EXISTS (SELECT 1 FROM pg_catalog.pg_class c WHERE c.oid = x.typrelid AND c.relkind = 'c')"),
|
|
113
|
+
"range type": type("x.typtype = 'r'"),
|
|
114
|
+
// A base type, or a shell; not the array type the server makes beside every type.
|
|
115
|
+
"base type": type("x.typtype IN ('b', 'p') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_type e WHERE e.typarray = x.oid)"),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Every object in scope of a kind the coverage table refuses, kind by kind
|
|
120
|
+
* in the table's order, each by schema then name. A kind the session may not
|
|
121
|
+
* read (a role without the privilege) is a problem in `unread`, not a pass.
|
|
122
|
+
*/
|
|
123
|
+
export async function readUndeclarable(client: PostgresClient, scope: SchemaScope = {}): Promise<{ objects: UndeclarableObject[]; unread: string[] }> {
|
|
124
|
+
const s = scopeSql("n.nspname", scope);
|
|
125
|
+
const objects: UndeclarableObject[] = [];
|
|
126
|
+
const unread: string[] = [];
|
|
127
|
+
for (const row of KIND_COVERAGE.postgres) {
|
|
128
|
+
if (row.status !== "refused") continue;
|
|
129
|
+
const probe = POSTGRES_PROBES[row.kind];
|
|
130
|
+
if (!probe) throw new Error(`no catalog query finds a Postgres ${row.kind} (src/postgres/live/undeclarable.ts)`);
|
|
131
|
+
// The comment names the kind, in the server's log and to a test's fake client.
|
|
132
|
+
const sql = `/* chant undeclarable: ${row.kind} */ SELECT q.schema, q.name, q.owner FROM (${probe.sql(s.sql)}) q ORDER BY q.schema ${C} NULLS FIRST, q.name ${C}`;
|
|
133
|
+
try {
|
|
134
|
+
const rows = await client.query<{ schema: string | null; name: string; owner: string | null }>(sql, sql.includes("$1") ? s.params : []);
|
|
135
|
+
for (const r of rows) objects.push({ kind: row.kind, ...(r.schema ? { schema: r.schema } : {}), name: r.name, ...(r.owner ? { owner: r.owner } : {}) });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
unread.push(`${row.kind}: ${err instanceof Error ? err.message : String(err)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { objects, unread };
|
|
141
|
+
}
|
|
@@ -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(
|
|
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
|
});
|