true-pg 0.7.0 → 0.8.0

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 (41) hide show
  1. package/lib/extractor/adapter.d.ts +8 -5
  2. package/lib/extractor/adapter.js +27 -12
  3. package/lib/extractor/canonicalise/composite.d.ts +7 -0
  4. package/lib/extractor/canonicalise/composite.js +53 -0
  5. package/lib/extractor/canonicalise/domain.d.ts +12 -0
  6. package/lib/extractor/canonicalise/domain.js +77 -0
  7. package/lib/extractor/canonicalise/enum.d.ts +8 -0
  8. package/lib/extractor/canonicalise/enum.js +22 -0
  9. package/lib/extractor/canonicalise/index.d.ts +11 -0
  10. package/lib/extractor/canonicalise/index.js +148 -0
  11. package/lib/extractor/canonicalise/parse.d.ts +43 -0
  12. package/lib/extractor/canonicalise/parse.js +50 -0
  13. package/lib/extractor/canonicalise/range.d.ts +11 -0
  14. package/lib/extractor/canonicalise/range.js +36 -0
  15. package/lib/extractor/canonicalise/resolve.d.ts +19 -0
  16. package/lib/extractor/canonicalise/resolve.js +59 -0
  17. package/lib/extractor/canonicalise/types.d.ts +70 -0
  18. package/lib/extractor/canonicalise/types.js +13 -0
  19. package/lib/extractor/index.d.ts +5 -2
  20. package/lib/extractor/index.js +5 -7
  21. package/lib/extractor/kinds/composite.d.ts +1 -1
  22. package/lib/extractor/kinds/composite.js +1 -1
  23. package/lib/extractor/kinds/domain.d.ts +1 -1
  24. package/lib/extractor/kinds/domain.js +1 -1
  25. package/lib/extractor/kinds/function.d.ts +1 -1
  26. package/lib/extractor/kinds/function.js +5 -7
  27. package/lib/extractor/kinds/materialized-view.d.ts +1 -1
  28. package/lib/extractor/kinds/materialized-view.js +4 -7
  29. package/lib/extractor/kinds/range.d.ts +1 -1
  30. package/lib/extractor/kinds/range.js +1 -2
  31. package/lib/extractor/kinds/table.d.ts +1 -1
  32. package/lib/extractor/kinds/table.js +2 -7
  33. package/lib/extractor/kinds/view.d.ts +1 -1
  34. package/lib/extractor/kinds/view.js +4 -7
  35. package/lib/imports.test.js +6 -14
  36. package/lib/index.js +2 -2
  37. package/lib/util.d.ts +3 -6
  38. package/lib/util.js +15 -11
  39. package/package.json +1 -1
  40. package/lib/extractor/canonicalise.d.ts +0 -110
  41. package/lib/extractor/canonicalise.js +0 -384
@@ -1,384 +0,0 @@
1
- import { Deferred, unreachable } from "../util.js";
2
- import { DbAdapter } from "./adapter.js";
3
- const removeNulls = (o) => {
4
- for (const key in o)
5
- if (o[key] == null)
6
- delete o[key];
7
- return o;
8
- };
9
- /**
10
- * Parses a PostgreSQL type name string to extract its base name,
11
- * modifiers, and dimensions from explicit '[]' brackets.
12
- *
13
- * Examples:
14
- *
15
- * - `parseTypeName("varchar(50)")`
16
- *
17
- * `⤷ { baseTypeName: "varchar", modifiers: "50", dimensions: 0, originalTypeName: "varchar(50)" }`
18
- *
19
- * - `parseTypeName("int[]")`
20
- *
21
- * `⤷ { baseTypeName: "int", modifiers: null, dimensions: 1, originalTypeName: "int[]" }`
22
- *
23
- * - `parseTypeName("public.my_table[][]")`
24
- *
25
- * `⤷ { baseTypeName: "public.my_table", modifiers: null, dimensions: 2, originalTypeName: "public.my_table[][]" }`
26
- *
27
- * - `parseTypeName("numeric(10, 2)[]")`
28
- *
29
- * `⤷ { baseTypeName: "numeric", modifiers: "10, 2", dimensions: 1, originalTypeName: "numeric(10, 2)[]" }`
30
- *
31
- * - `parseTypeName("geometry(Point, 4326)")`
32
- *
33
- * `⤷ { baseTypeName: "geometry", modifiers: "Point, 4326", dimensions: 0, originalTypeName: "geometry(Point, 4326)" }`
34
- *
35
- * - `parseTypeName("_text")`
36
- *
37
- * `⤷ { baseTypeName: "_text", modifiers: null, dimensions: 0, originalTypeName: "_text" }`
38
- *
39
- * Internal arrays aren't handled here
40
- */
41
- export function parseTypeName(type) {
42
- let base = type;
43
- let modifiers = null;
44
- let dimensions = 0;
45
- // 1. Extract modifiers (content within the last parentheses)
46
- const modifierMatch = base.match(/\(([^)]*)\)$/);
47
- if (modifierMatch) {
48
- modifiers = modifierMatch[1];
49
- base = base.substring(0, modifierMatch.index).trim();
50
- }
51
- // 2. Count and remove explicit array brackets '[]'
52
- // Repeatedly remove '[]' from the end and count dimensions
53
- while (base.endsWith("[]")) {
54
- dimensions++;
55
- base = base.slice(0, -2);
56
- }
57
- return { original: type, base, modifiers, dimensions };
58
- }
59
- export var Canonical;
60
- (function (Canonical) {
61
- let Kind;
62
- (function (Kind) {
63
- Kind["Base"] = "base";
64
- Kind["Composite"] = "composite";
65
- Kind["Domain"] = "domain";
66
- Kind["Enum"] = "enum";
67
- Kind["Range"] = "range";
68
- Kind["Pseudo"] = "pseudo";
69
- Kind["Unknown"] = "unknown";
70
- })(Kind = Canonical.Kind || (Canonical.Kind = {}));
71
- })(Canonical || (Canonical = {}));
72
- /**
73
- * Takes base type names (without modifiers/brackets), resolves them to their ultimate base type OID
74
- * and internal array dimensions, and fetches basic kind information
75
- */
76
- async function resolveBasicInfo(db, types) {
77
- const query = `
78
- WITH RECURSIVE
79
- input(base_type_name, seq) AS (
80
- SELECT * FROM unnest($1::text[], $2::int[])
81
- ),
82
- type_resolution(seq, current_oid, level) AS (
83
- -- Base case: Look up the initial base type name
84
- SELECT i.seq, t.oid, 1
85
- FROM input i JOIN pg_type t ON t.oid = i.base_type_name::regtype
86
- UNION ALL
87
- -- Recursive step: Follow typelem for standard arrays (_)
88
- SELECT r.seq, t.typelem, r.level + 1
89
- FROM type_resolution r JOIN pg_type t ON r.current_oid = t.oid
90
- WHERE t.typelem != 0 AND left(t.typname, 1) = '_'
91
- ),
92
- final_resolution AS (
93
- -- Get the OID and max level (depth) for each sequence number
94
- SELECT DISTINCT ON (seq) seq, current_oid AS base_type_oid, level
95
- FROM type_resolution ORDER BY seq, level DESC
96
- )
97
- -- Combine resolution with basic type info fetching
98
- SELECT
99
- fr.seq,
100
- fr.base_type_oid AS oid,
101
- (fr.level - 1) AS internal_dimensions,
102
- n.nspname AS schema,
103
- t.typname AS name,
104
- n.nspname || '.' || t.typname AS canonical_name,
105
- CASE t.typtype
106
- WHEN 'b' THEN 'base'::text WHEN 'c' THEN 'composite'::text WHEN 'd' THEN 'domain'::text
107
- WHEN 'e' THEN 'enum'::text WHEN 'p' THEN 'pseudo'::text WHEN 'r' THEN 'range'::text
108
- ELSE 'unknown'::text
109
- END AS kind,
110
- t.typrelid,
111
- t.typbasetype,
112
- COALESCE(r.rngsubtype, 0) AS rngsubtype
113
- FROM final_resolution fr
114
- JOIN pg_type t ON t.oid = fr.base_type_oid
115
- JOIN pg_namespace n ON t.typnamespace = n.oid
116
- LEFT JOIN pg_range r ON t.oid = r.rngtypid AND t.typtype = 'r'
117
- ORDER BY fr.seq;
118
- `;
119
- // Need to handle the string 'kind' coming back from the DB
120
- const results = await db.query(query, [
121
- types.map(t => t.parsed.base),
122
- types.map(t => t.seq),
123
- ]);
124
- return results;
125
- }
126
- async function resolveBasicInfo1(db, type) {
127
- const query = `
128
- WITH RECURSIVE
129
- input(base_type_name) AS (
130
- SELECT $1::text
131
- ),
132
- type_resolution(current_oid, level) AS (
133
- -- Base case: Look up the initial base type name
134
- SELECT t.oid, 1
135
- FROM input i JOIN pg_type t ON t.oid = i.base_type_name::regtype
136
- UNION ALL
137
- -- Recursive step: Follow typelem for standard arrays (_)
138
- SELECT t.typelem, r.level + 1
139
- FROM type_resolution r JOIN pg_type t ON r.current_oid = t.oid
140
- WHERE t.typelem != 0 AND left(t.typname, 1) = '_'
141
- ),
142
- final_resolution AS (
143
- -- Get the OID and max level (depth) for each sequence number
144
- SELECT DISTINCT ON (current_oid) current_oid AS base_type_oid, level
145
- FROM type_resolution ORDER BY current_oid, level DESC
146
- )
147
- -- Combine resolution with basic type info fetching
148
- SELECT
149
- fr.base_type_oid AS oid,
150
- (fr.level - 1) AS internal_dimensions,
151
- n.nspname AS schema,
152
- t.typname AS name,
153
- n.nspname || '.' || t.typname AS canonical_name,
154
- CASE t.typtype
155
- WHEN 'b' THEN 'base'::text WHEN 'c' THEN 'composite'::text WHEN 'd' THEN 'domain'::text
156
- WHEN 'e' THEN 'enum'::text WHEN 'p' THEN 'pseudo'::text WHEN 'r' THEN 'range'::text
157
- ELSE 'unknown'::text
158
- END AS kind,
159
- t.typrelid,
160
- t.typbasetype,
161
- COALESCE(r.rngsubtype, 0) AS rngsubtype
162
- FROM final_resolution fr
163
- JOIN pg_type t ON t.oid = fr.base_type_oid
164
- JOIN pg_namespace n ON t.typnamespace = n.oid
165
- LEFT JOIN pg_range r ON t.oid = r.rngtypid AND t.typtype = 'r';
166
- `;
167
- // Need to handle the string 'kind' coming back from the DB
168
- const results = await db.query(query, [type.base]);
169
- return results[0];
170
- }
171
- /** Fetches enum values for given enum type OIDs */
172
- async function getEnumValues(db, oid) {
173
- const query = `
174
- SELECT array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
175
- FROM pg_enum e
176
- WHERE e.enumtypid = $1::oid
177
- GROUP BY e.enumtypid;
178
- `;
179
- const results = await db.query(query, [oid]);
180
- return results[0]?.values ?? [];
181
- }
182
- /** Fetches composite attributes for given composite type OIDs (typrelid) */
183
- async function getCompositeAttributes(db, relid) {
184
- const query = `
185
- SELECT
186
- a.attrelid AS relid,
187
- jsonb_agg(
188
- jsonb_build_object(
189
- 'name', a.attname,
190
- 'index', a.attnum,
191
- 'type_oid', a.atttypid,
192
- 'type_name', format_type(a.atttypid, null),
193
- 'comment', col_description(a.attrelid, a.attnum::int),
194
- 'defaultValue', pg_get_expr(d.adbin, d.adrelid),
195
- 'isNullable', NOT a.attnotnull,
196
- 'isIdentity', a.attidentity IS NOT NULL AND a.attidentity != '',
197
- 'generated', CASE WHEN a.attidentity = 'a' THEN 'ALWAYS' WHEN a.attidentity = 'd' THEN 'BY DEFAULT' WHEN a.attgenerated = 's' THEN 'ALWAYS' ELSE 'NEVER' END
198
- ) ORDER BY a.attnum
199
- ) AS attributes
200
- FROM pg_attribute a
201
- LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
202
- WHERE a.attrelid = $1::oid AND a.attnum > 0 AND NOT a.attisdropped
203
- GROUP BY a.attrelid;
204
- `;
205
- const results = await db.query(query, [relid]);
206
- return results[0]?.attributes ?? [];
207
- }
208
- /** Recursive helper to find the ultimate base type OID for a domain */
209
- async function findUltimateDomainBaseOid(db, oid) {
210
- const query = `
211
- WITH RECURSIVE domain_chain(oid, base_oid, level) AS (
212
- SELECT $1::oid, t.typbasetype, 1
213
- FROM pg_type t WHERE t.oid = $1::oid AND t.typtype = 'd'
214
- UNION ALL
215
- SELECT t.oid, t.typbasetype, dc.level + 1
216
- FROM domain_chain dc JOIN pg_type t ON dc.base_oid = t.oid
217
- WHERE t.typtype = 'd'
218
- )
219
- SELECT base_oid FROM domain_chain ORDER BY level DESC LIMIT 1;
220
- `;
221
- const result = await db.query(query, [oid]);
222
- return result[0]?.base_oid ?? oid; // Return original if not a domain or chain ends
223
- }
224
- /** Fetches the canonical name of the ultimate base type for given domain OIDs */
225
- async function getDomainBaseTypeName(db, typbasetype) {
226
- const ultimateBaseOid = await findUltimateDomainBaseOid(db, typbasetype);
227
- const query = `
228
- SELECT t.oid, format('%I.%I', n.nspname, t.typname) AS name
229
- FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid
230
- WHERE t.oid = $1::oid
231
- `;
232
- const results = await db.query(query, [ultimateBaseOid]);
233
- return results[0]?.name ?? "";
234
- }
235
- /** Fetches the canonical name of the subtype for given range OIDs */
236
- async function getRangeSubtypeName(db, oid) {
237
- const query = `
238
- SELECT t.oid, format('%I.%I', n.nspname, t.typname) AS name
239
- FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid
240
- WHERE t.oid = $1::oid
241
- `;
242
- const results = await db.query(query, [oid]);
243
- return results[0]?.name ?? "";
244
- }
245
- async function canonicaliseType(db, basic, rawTypeCache, canonicalCache) {
246
- switch (basic.kind) {
247
- case Canonical.Kind.Base:
248
- return { kind: Canonical.Kind.Base };
249
- case Canonical.Kind.Enum:
250
- const enumValues = await getEnumValues(db, basic.oid);
251
- if (enumValues.length === 0) {
252
- throw new Error(`Enum ${basic.canonical_name} (OID: ${basic.oid}) lacks values.`);
253
- }
254
- return { kind: Canonical.Kind.Enum, enum_values: enumValues };
255
- case Canonical.Kind.Composite: {
256
- const rawAttributes = await getCompositeAttributes(db, basic.typrelid);
257
- const attributeTypes = rawAttributes.map(attr => attr.type_name);
258
- const canonicalAttributeTypes = await canonicalise(db, attributeTypes, rawTypeCache, canonicalCache); // Recursive call
259
- const attributes = await Promise.all(rawAttributes.map(async (attr, index) => {
260
- return removeNulls({
261
- name: attr.name,
262
- index: attr.index,
263
- type: canonicalAttributeTypes[index],
264
- comment: attr.comment,
265
- defaultValue: attr.defaultValue,
266
- isNullable: attr.isNullable,
267
- isIdentity: attr.isIdentity,
268
- generated: attr.generated,
269
- });
270
- }));
271
- return { kind: Canonical.Kind.Composite, attributes };
272
- }
273
- case Canonical.Kind.Domain: {
274
- const baseTypeName = await getDomainBaseTypeName(db, basic.typbasetype);
275
- if (!baseTypeName) {
276
- throw new Error(`Domain ${basic.canonical_name} (OID: ${basic.oid}) lacks a resolved base type name.`);
277
- }
278
- const canonicalBaseType = await canonicalise(db, [baseTypeName], rawTypeCache, canonicalCache); // Recursive call
279
- return { kind: Canonical.Kind.Domain, domain_base_type: canonicalBaseType[0] };
280
- }
281
- case Canonical.Kind.Range: {
282
- const subtypeName = await getRangeSubtypeName(db, basic.rngsubtype);
283
- if (!subtypeName) {
284
- throw new Error(`Range ${basic.canonical_name} (OID: ${basic.oid}) lacks a resolved subtype name.`);
285
- }
286
- const canonicalSubtype = await canonicalise(db, [subtypeName], rawTypeCache, canonicalCache); // Recursive call
287
- return { kind: Canonical.Kind.Range, range_subtype: canonicalSubtype[0] };
288
- }
289
- case Canonical.Kind.Pseudo:
290
- return { kind: Canonical.Kind.Pseudo };
291
- case Canonical.Kind.Unknown:
292
- throw new Error(`Canonicalising "${basic.original_type}" resulted in unknown kind: ${basic.canonical_name}`);
293
- default:
294
- return unreachable(basic.kind);
295
- }
296
- }
297
- export const canonicalise = async (db, types, rawTypeCache, canonicalCache) => {
298
- if (types.length === 0)
299
- return [];
300
- const withSeq = types.map((type, seq) => ({ type, seq }));
301
- // final list of resolved canonical types
302
- const results = [];
303
- // unresolved types, awaiting resolution
304
- const unresolved = [];
305
- for (const { type, seq } of withSeq) {
306
- // if the type is already resolved, add it to the results
307
- const cached = rawTypeCache.get(type);
308
- if (cached)
309
- results.push(cached);
310
- else {
311
- // if the type is not resolved, create a deferred promise and add it to the unresolved list
312
- const deferred = new Deferred();
313
- rawTypeCache.set(type, deferred.promise);
314
- results.push(deferred.promise);
315
- const parsed = parseTypeName(type);
316
- unresolved.push({ seq, parsed, deferred });
317
- }
318
- }
319
- const resolved = await resolveBasicInfo(db, unresolved);
320
- Promise.all(resolved.map(async (info, index) => {
321
- const { parsed, deferred } = unresolved[index];
322
- try {
323
- const dimensions = parsed.dimensions + info.internal_dimensions;
324
- const common = {
325
- kind: info.kind,
326
- oid: info.oid,
327
- typrelid: info.typrelid,
328
- typbasetype: info.typbasetype,
329
- rngsubtype: info.rngsubtype,
330
- canonical_name: info.canonical_name,
331
- schema: info.schema,
332
- name: info.name,
333
- original_type: parsed.original,
334
- modifiers: parsed.modifiers,
335
- dimensions,
336
- };
337
- let cached = canonicalCache.get(info.canonical_name);
338
- if (cached) {
339
- const exclusive = await cached;
340
- const result = { ...common, ...exclusive };
341
- deferred.resolve(result);
342
- }
343
- else {
344
- const deferred2 = new Deferred();
345
- canonicalCache.set(info.canonical_name, deferred2.promise);
346
- cached = deferred2.promise;
347
- const exclusive = await canonicaliseType(db, common, rawTypeCache, canonicalCache);
348
- deferred2.resolve(exclusive);
349
- const result = { ...common, ...exclusive };
350
- deferred.resolve(result);
351
- }
352
- }
353
- catch (error) {
354
- deferred.reject(error);
355
- }
356
- }));
357
- const ret = await Promise.all(results);
358
- return ret;
359
- };
360
- export const oidsToQualifiedNames = async (db, oids) => {
361
- if (oids.length === 0)
362
- return [];
363
- const query = `
364
- SELECT
365
- input.ord,
366
- format('%I.%I', n.nspname, t.typname) AS qualified_name
367
- -- Use unnest WITH ORDINALITY because SQL doesn't guarantee order of SELECT results
368
- FROM unnest($1::oid[]) WITH ORDINALITY AS input(oid, ord)
369
- JOIN pg_type t ON t.oid = input.oid
370
- JOIN pg_namespace n ON t.typnamespace = n.oid
371
- ORDER BY input.ord;
372
- `;
373
- const results = await db.query(query, [oids]);
374
- return results.map(r => r.qualified_name);
375
- };
376
- export const canonicaliseFromOids = async (db, oids) => {
377
- if (oids.length === 0)
378
- return [];
379
- const types = await oidsToQualifiedNames(db, oids);
380
- const unknown = types.filter(name => name == undefined);
381
- if (unknown.length > 0)
382
- throw new Error(`Failed to resolve OIDs to type names: ${unknown.join(", ")}`);
383
- return db.canonicalise(types);
384
- };