true-pg 0.6.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 (46) hide show
  1. package/lib/extractor/adapter.d.ts +9 -1
  2. package/lib/extractor/adapter.js +30 -1
  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.d.ts → canonicalise/types.d.ts} +17 -11
  18. package/lib/extractor/canonicalise/types.js +13 -0
  19. package/lib/extractor/index.d.ts +7 -4
  20. package/lib/extractor/index.js +5 -10
  21. package/lib/extractor/kinds/composite.d.ts +1 -1
  22. package/lib/extractor/kinds/composite.js +1 -2
  23. package/lib/extractor/kinds/domain.d.ts +1 -1
  24. package/lib/extractor/kinds/domain.js +1 -2
  25. package/lib/extractor/kinds/function.d.ts +1 -1
  26. package/lib/extractor/kinds/function.js +5 -8
  27. package/lib/extractor/kinds/materialized-view.d.ts +1 -1
  28. package/lib/extractor/kinds/materialized-view.js +4 -8
  29. package/lib/extractor/kinds/range.d.ts +1 -1
  30. package/lib/extractor/kinds/range.js +1 -3
  31. package/lib/extractor/kinds/table.d.ts +1 -1
  32. package/lib/extractor/kinds/table.js +2 -8
  33. package/lib/extractor/kinds/view.d.ts +1 -1
  34. package/lib/extractor/kinds/view.js +4 -8
  35. package/lib/extractor/pgtype.d.ts +1 -1
  36. package/lib/imports.js +6 -3
  37. package/lib/imports.test.d.ts +1 -0
  38. package/lib/imports.test.js +180 -0
  39. package/lib/index.js +19 -3
  40. package/lib/kysely/index.js +2 -0
  41. package/lib/types.d.ts +1 -1
  42. package/lib/util.d.ts +4 -0
  43. package/lib/util.js +18 -0
  44. package/lib/zod/index.js +2 -10
  45. package/package.json +1 -1
  46. package/lib/extractor/canonicalise.js +0 -245
@@ -1,16 +1,24 @@
1
1
  import Pg from "pg";
2
2
  import { PGlite as Pglite } from "@electric-sql/pglite";
3
+ import type { Canonical, QueueMember } from "./canonicalise/index.ts";
3
4
  export declare class DbAdapter {
4
5
  private client;
5
6
  private external?;
7
+ resolveQueue: QueueMember[];
8
+ queryCount: number;
6
9
  constructor(client: Pg.Client | Pg.Pool | Pglite, external?: boolean | undefined);
10
+ resetQueue(): void;
11
+ resetQueryCount(): void;
12
+ reset(): void;
13
+ enqueue(type: string): Canonical;
14
+ resolve(): Promise<Canonical[]>;
7
15
  connect(): Promise<void>;
8
16
  /**
9
17
  * Execute a read query and return just the rows
10
18
  */
11
19
  query<R, I extends any[] = []>(text: string, params?: I): Promise<R[]>;
12
20
  /**
13
- * Close the connection if needed
21
+ * Close the connection and clear the cache
14
22
  */
15
23
  close(): Promise<void>;
16
24
  }
@@ -1,13 +1,40 @@
1
1
  import Pg from "pg";
2
2
  import { PGlite as Pglite } from "@electric-sql/pglite";
3
+ import { canonicaliseQueue } from "./canonicalise/index.js";
3
4
  export class DbAdapter {
4
5
  client;
5
6
  external;
7
+ resolveQueue = [];
8
+ queryCount = 0;
6
9
  constructor(client, external) {
7
10
  this.client = client;
8
11
  this.external = external;
9
12
  }
13
+ resetQueue() {
14
+ this.resolveQueue = [];
15
+ }
16
+ resetQueryCount() {
17
+ this.queryCount = 0;
18
+ }
19
+ reset() {
20
+ this.resetQueue();
21
+ this.resetQueryCount();
22
+ }
23
+ enqueue(type) {
24
+ const member = { type, out: {} };
25
+ this.resolveQueue.push(member);
26
+ return member.out;
27
+ }
28
+ async resolve() {
29
+ const results = await canonicaliseQueue(this, this.resolveQueue);
30
+ for (let i = 0; i < this.resolveQueue.length; i++) {
31
+ this.resolveQueue[i].out = results[i];
32
+ }
33
+ this.resetQueue();
34
+ return results;
35
+ }
10
36
  async connect() {
37
+ this.reset();
11
38
  if (this.external)
12
39
  return;
13
40
  if (this.client instanceof Pg.Pool) {
@@ -24,6 +51,7 @@ export class DbAdapter {
24
51
  * Execute a read query and return just the rows
25
52
  */
26
53
  async query(text, params) {
54
+ this.queryCount++;
27
55
  let stack;
28
56
  try {
29
57
  stack = new Error().stack;
@@ -47,9 +75,10 @@ export class DbAdapter {
47
75
  }
48
76
  }
49
77
  /**
50
- * Close the connection if needed
78
+ * Close the connection and clear the cache
51
79
  */
52
80
  async close() {
81
+ this.reset();
53
82
  if (this.external)
54
83
  return;
55
84
  if (this.client instanceof Pg.Pool) {
@@ -0,0 +1,7 @@
1
+ import { Canonical, type ExclusiveComposite } from "./types.ts";
2
+ import type { DbAdapter } from "../adapter.ts";
3
+ export type { ExclusiveComposite };
4
+ export declare function getCompositeDetails(db: DbAdapter, enqueue: (types: string) => Canonical, entries: {
5
+ typrelid: number;
6
+ canonical_name: string;
7
+ }[]): Promise<ExclusiveComposite[]>;
@@ -0,0 +1,53 @@
1
+ import { Canonical } from "./types.js";
2
+ import { minifyQuery, removeNulls } from "../../util.js";
3
+ const query = minifyQuery(`
4
+ SELECT
5
+ input.seq,
6
+ input.relid,
7
+ jsonb_agg(
8
+ jsonb_build_object(
9
+ 'name', a.attname,
10
+ 'index', a.attnum,
11
+ 'type_oid', a.atttypid,
12
+ 'type_name', format_type(a.atttypid, null),
13
+ 'comment', col_description(a.attrelid, a.attnum::int),
14
+ 'defaultValue', pg_get_expr(d.adbin, d.adrelid),
15
+ 'isNullable', NOT a.attnotnull,
16
+ 'isIdentity', a.attidentity IS NOT NULL AND a.attidentity != '',
17
+ 'generated', CASE
18
+ WHEN a.attidentity = 'a' THEN 'ALWAYS'
19
+ WHEN a.attidentity = 'd' THEN 'BY DEFAULT'
20
+ WHEN a.attgenerated = 's' THEN 'ALWAYS'
21
+ ELSE 'NEVER'
22
+ END
23
+ ) ORDER BY a.attnum
24
+ ) AS attributes
25
+ FROM unnest($1::oid[]) WITH ORDINALITY AS input(relid, seq)
26
+ JOIN pg_attribute a ON a.attrelid = input.relid
27
+ LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
28
+ WHERE a.attnum > 0 AND NOT a.attisdropped
29
+ GROUP BY input.relid, input.seq
30
+ ORDER BY input.seq;
31
+ `);
32
+ // TODO: combine all recursive canonicalise calls into a single call then unnest the results
33
+ export async function getCompositeDetails(db, enqueue, entries) {
34
+ if (entries.length === 0)
35
+ return [];
36
+ const results = await db.query(query, [entries.map(r => r.typrelid)]);
37
+ return results.map((result, index) => {
38
+ const attributes = result.attributes.map((attr, index) => {
39
+ const canonical = enqueue(attr.type_name);
40
+ return removeNulls({
41
+ name: attr.name,
42
+ index: attr.index,
43
+ type: canonical,
44
+ comment: attr.comment,
45
+ defaultValue: attr.defaultValue,
46
+ isNullable: attr.isNullable,
47
+ isIdentity: attr.isIdentity,
48
+ generated: attr.generated,
49
+ });
50
+ });
51
+ return { kind: Canonical.Kind.Composite, canonical_name: entries[index].canonical_name, attributes };
52
+ });
53
+ }
@@ -0,0 +1,12 @@
1
+ import { Canonical, type ExclusiveDomain } from "./types.ts";
2
+ import type { DbAdapter } from "../adapter.ts";
3
+ export type { ExclusiveDomain };
4
+ /**
5
+ * Fetches the canonical name of the ultimate base type for a given domain OID
6
+ * in a single query. If the OID is not a domain, it returns the name of the
7
+ * type corresponding to the original OID.
8
+ */
9
+ export declare function getDomainDetails(db: DbAdapter, enqueue: (types: string) => Canonical, entries: {
10
+ oid: number;
11
+ canonical_name: string;
12
+ }[]): Promise<ExclusiveDomain[]>;
@@ -0,0 +1,77 @@
1
+ import { Canonical } from "./types.js";
2
+ import { minifyQuery } from "../../util.js";
3
+ const query = minifyQuery(`
4
+ WITH RECURSIVE
5
+ -- 1. Unnest input OIDs and sequences
6
+ input_data AS (
7
+ SELECT oid, seq
8
+ FROM unnest($1::oid[]) WITH ORDINALITY AS u(oid, seq)
9
+ ),
10
+ -- 2. Recursively find the base type for each input OID
11
+ domain_chain(input_seq, current_oid, base_oid, level) AS (
12
+ -- Base case: Start with the input OIDs from unnested data
13
+ SELECT
14
+ i.seq,
15
+ i.oid,
16
+ t.typbasetype,
17
+ 1
18
+ FROM input_data i
19
+ JOIN pg_type t ON t.oid = i.oid
20
+ WHERE t.typtype = 'd' -- Ensure it's actually a domain
21
+
22
+ UNION ALL
23
+
24
+ -- Recursive step: Follow the domain chain for each sequence
25
+ SELECT
26
+ dc.input_seq,
27
+ dc.base_oid, -- The current OID becomes the base OID from the previous step
28
+ t.typbasetype,
29
+ dc.level + 1
30
+ FROM domain_chain dc
31
+ JOIN pg_type t ON dc.base_oid = t.oid
32
+ WHERE t.typtype = 'd' -- Continue only if the next type is also a domain
33
+ ),
34
+ -- 3. Determine the final base OID for each sequence
35
+ final_base AS (
36
+ SELECT DISTINCT ON (i.seq)
37
+ i.seq,
38
+ -- Use the ultimate base_oid from the chain if it exists for this seq,
39
+ -- otherwise fallback to the original input OID for this seq
40
+ COALESCE(
41
+ (SELECT dc.base_oid FROM domain_chain dc WHERE dc.input_seq = i.seq ORDER BY dc.level DESC LIMIT 1),
42
+ i.oid
43
+ ) AS oid
44
+ FROM input_data i
45
+ )
46
+ -- 4. Fetch the formatted name for the final OID for each sequence
47
+ SELECT
48
+ fb.seq,
49
+ format('%I.%I', n.nspname, t.typname) AS name
50
+ FROM final_base fb
51
+ JOIN pg_type t ON t.oid = fb.oid
52
+ JOIN pg_namespace n ON t.typnamespace = n.oid
53
+ ORDER BY fb.seq;
54
+ `);
55
+ /**
56
+ * Fetches the canonical name of the ultimate base type for a given domain OID
57
+ * in a single query. If the OID is not a domain, it returns the name of the
58
+ * type corresponding to the original OID.
59
+ */
60
+ export async function getDomainDetails(db, enqueue, entries) {
61
+ if (entries.length === 0)
62
+ return [];
63
+ const results = await db.query(query, [entries.map(i => i.oid)]);
64
+ // Basic check for result length mismatch
65
+ if (results.length !== entries.length) {
66
+ throw new Error("Mismatch between input domain count and domain detail results count.");
67
+ }
68
+ return results.map((result, index) => {
69
+ const { canonical_name, oid } = entries[index];
70
+ const name = result.name;
71
+ if (!name) {
72
+ throw new Error(`Could not resolve base type for domain ${canonical_name} (OID: ${oid}).`);
73
+ }
74
+ const canonicalBaseType = enqueue(name);
75
+ return { kind: Canonical.Kind.Domain, canonical_name, domain_base_type: canonicalBaseType };
76
+ });
77
+ }
@@ -0,0 +1,8 @@
1
+ import { type ExclusiveEnum } from "./types.ts";
2
+ import type { DbAdapter } from "../adapter.ts";
3
+ export type { ExclusiveEnum };
4
+ /** Fetches enum values for given enum type OIDs */
5
+ export declare function getEnumDetails(db: DbAdapter, entries: {
6
+ oid: number;
7
+ canonical_name: string;
8
+ }[]): Promise<ExclusiveEnum[]>;
@@ -0,0 +1,22 @@
1
+ import { Canonical } from "./types.js";
2
+ import { minifyQuery } from "../../util.js";
3
+ const query = minifyQuery(`
4
+ SELECT
5
+ input.seq,
6
+ json_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
7
+ FROM unnest($1::oid[]) WITH ORDINALITY AS input(oid, seq)
8
+ JOIN pg_enum e ON e.enumtypid = input.oid
9
+ GROUP BY input.seq, e.enumtypid
10
+ ORDER BY input.seq;
11
+ `);
12
+ /** Fetches enum values for given enum type OIDs */
13
+ export async function getEnumDetails(db, entries) {
14
+ if (entries.length === 0)
15
+ return [];
16
+ const results = await db.query(query, [entries.map(o => o.oid)]);
17
+ return results.map((r, i) => ({
18
+ kind: Canonical.Kind.Enum,
19
+ canonical_name: entries[i].canonical_name,
20
+ enum_values: r.values,
21
+ }));
22
+ }
@@ -0,0 +1,11 @@
1
+ import { Canonical, type ExclusiveCanonProps } from "./types.ts";
2
+ import { DbAdapter } from "../adapter.ts";
3
+ import { type ResolvedBasicInfo } from "./resolve.ts";
4
+ export { Canonical, type ExclusiveCanonProps };
5
+ export interface QueueMember {
6
+ type: string;
7
+ out: Canonical;
8
+ }
9
+ export declare const canonicaliseQueue: (db: DbAdapter, queue: QueueMember[], resolveCache?: Map<string, ResolvedBasicInfo>, canonCache?: Map<string, ExclusiveCanonProps>) => Promise<Canonical[]>;
10
+ export declare const oidsToQualifiedNames: (db: DbAdapter, oids: number[]) => Promise<string[]>;
11
+ export declare const canonicaliseFromOids: (db: DbAdapter, oids: number[]) => Promise<Canonical[]>;
@@ -0,0 +1,148 @@
1
+ import { Canonical } from "./types.js";
2
+ import { DbAdapter } from "../adapter.js";
3
+ import { resolveBasicInfo } from "./resolve.js";
4
+ import { parseRawType } from "./parse.js";
5
+ import { getEnumDetails } from "./enum.js";
6
+ import { getCompositeDetails } from "./composite.js";
7
+ import { getDomainDetails } from "./domain.js";
8
+ import { getRangeDetails } from "./range.js";
9
+ export { Canonical };
10
+ // The Final Strategy
11
+ //
12
+ // Insert placeholder objects wherever a Canonical is needed
13
+ // After extracting all tables, views, functions, etc. resolve all Canonicals
14
+ // Then map over the input types and wait for all cached promises to resolve
15
+ // Finally patch all Canonicals with resolved values
16
+ export const canonicaliseQueue = async (db, queue, resolveCache = new Map(), canonCache = new Map()) => {
17
+ if (queue.length === 0)
18
+ return [];
19
+ const parsed = queue.map(q => parseRawType(q.type));
20
+ const plain = [...new Set(parsed.filter(p => !resolveCache.has(p.plain)).map(p => p.plain))];
21
+ const resolved = await resolveBasicInfo(db, plain);
22
+ plain.forEach((p, i) => {
23
+ const r = resolved[i];
24
+ if (!r)
25
+ throw new Error(`(Unreachable) Could not find resolved basic info for ${p}`);
26
+ resolveCache.set(p, r);
27
+ });
28
+ const unknown = resolved.filter(r => r.kind === Canonical.Kind.Unknown);
29
+ if (unknown.length > 0) {
30
+ const types = unknown.map(u => u.canonical_name).join(", ");
31
+ throw new Error(`Received kind 'unknown', could not resolve ${unknown.length} types: ${types}`);
32
+ }
33
+ const internalQueue = [];
34
+ const q = (types) => {
35
+ const member = { type: types, out: {} };
36
+ internalQueue.push(member);
37
+ return member.out;
38
+ };
39
+ const batches = {
40
+ enums: [],
41
+ composites: [],
42
+ domains: [],
43
+ ranges: [],
44
+ };
45
+ let seen = new Set();
46
+ const Kind = Canonical.Kind;
47
+ // split in one loop instead of 4 filters
48
+ for (const r of resolved) {
49
+ if (canonCache.has(r.canonical_name))
50
+ continue;
51
+ // deduplicate
52
+ if (seen.has(r.canonical_name))
53
+ continue;
54
+ seen.add(r.canonical_name);
55
+ if (r.kind === Kind.Enum)
56
+ batches.enums.push(r);
57
+ if (r.kind === Kind.Composite)
58
+ batches.composites.push(r);
59
+ if (r.kind === Kind.Domain)
60
+ batches.domains.push(r);
61
+ if (r.kind === Kind.Range)
62
+ batches.ranges.push(r);
63
+ // special cases because these are not further extracted
64
+ if (r.kind === Kind.Base || r.kind === Kind.Pseudo)
65
+ canonCache.set(r.canonical_name, { kind: r.kind, canonical_name: r.canonical_name });
66
+ }
67
+ // @ts-expect-error allow GC
68
+ seen = null;
69
+ {
70
+ // extract all in parallel
71
+ const [enums, composites, domains, ranges] = await Promise.all([
72
+ getEnumDetails(db, batches.enums),
73
+ getCompositeDetails(db, q, batches.composites),
74
+ getDomainDetails(db, q, batches.domains),
75
+ getRangeDetails(db, q, batches.ranges),
76
+ ]);
77
+ for (const e of enums)
78
+ canonCache.set(e.canonical_name, e);
79
+ for (const c of composites)
80
+ canonCache.set(c.canonical_name, c);
81
+ for (const d of domains)
82
+ canonCache.set(d.canonical_name, d);
83
+ for (const r of ranges)
84
+ canonCache.set(r.canonical_name, r);
85
+ }
86
+ await Promise.all(parsed.map(async (p, index) => {
87
+ const info = resolveCache.get(p.plain);
88
+ if (!info)
89
+ throw new Error(`(Unreachable) Could not find resolved basic info for ${p.plain}`);
90
+ const m = queue[index];
91
+ try {
92
+ const dimensions = p.dimensions + info.internal_dimensions;
93
+ const common = {
94
+ kind: info.kind,
95
+ oid: info.oid,
96
+ typrelid: info.typrelid,
97
+ typbasetype: info.typbasetype,
98
+ rngsubtype: info.rngsubtype,
99
+ canonical_name: info.canonical_name,
100
+ schema: info.schema,
101
+ name: info.name,
102
+ original_type: p.original,
103
+ modifiers: p.modifiers,
104
+ dimensions,
105
+ };
106
+ const kind = info.kind;
107
+ if (kind === Canonical.Kind.Unknown)
108
+ throw new Error(`Could not find canonical type for "${info.schema}.${info.canonical_name}"`);
109
+ const exclusive = canonCache.get(info.canonical_name);
110
+ if (!exclusive)
111
+ throw new Error(`(Unreachable) Could not find canonical type for ${info.canonical_name}`);
112
+ const result = { ...common, ...exclusive };
113
+ Object.assign(m.out, result);
114
+ }
115
+ catch (error) {
116
+ throw error;
117
+ }
118
+ }));
119
+ if (internalQueue.length > 0) {
120
+ await canonicaliseQueue(db, internalQueue, resolveCache, canonCache);
121
+ }
122
+ return queue.map(m => m.out);
123
+ };
124
+ export const oidsToQualifiedNames = async (db, oids) => {
125
+ if (oids.length === 0)
126
+ return [];
127
+ const query = `
128
+ SELECT
129
+ input.ord,
130
+ format('%I.%I', n.nspname, t.typname) AS qualified_name
131
+ -- Use unnest WITH ORDINALITY because SQL doesn't guarantee order of SELECT results
132
+ FROM unnest($1::oid[]) WITH ORDINALITY AS input(oid, ord)
133
+ JOIN pg_type t ON t.oid = input.oid
134
+ JOIN pg_namespace n ON t.typnamespace = n.oid
135
+ ORDER BY input.ord;
136
+ `;
137
+ const results = await db.query(query, [oids]);
138
+ return results.map(r => r.qualified_name);
139
+ };
140
+ export const canonicaliseFromOids = async (db, oids) => {
141
+ if (oids.length === 0)
142
+ return [];
143
+ const types = await oidsToQualifiedNames(db, oids);
144
+ const unknown = types.filter(name => name == undefined);
145
+ if (unknown.length > 0)
146
+ throw new Error(`Failed to resolve OIDs to type names: ${unknown.join(", ")}`);
147
+ return canonicaliseQueue(db, types.map(t => ({ type: t, out: {} })));
148
+ };
@@ -0,0 +1,43 @@
1
+ export interface ParsedType {
2
+ /** Name after removing modifiers and brackets, e.g. "varchar" in "varchar(50)" */
3
+ plain: string;
4
+ /** Modifiers, e.g. "50" in "varchar(50)" */
5
+ modifiers: string | null;
6
+ /** Number of dimensions from explicit brackets, e.g. 1 in "int[]" */
7
+ dimensions: number;
8
+ /** Original type name, e.g. "varchar(50)" */
9
+ original: string;
10
+ }
11
+ /**
12
+ * Parses a PostgreSQL type name string to extract its base name,
13
+ * modifiers, and dimensions from explicit '[]' brackets.
14
+ *
15
+ * Examples:
16
+ *
17
+ * - `parseTypeName("varchar(50)")`
18
+ *
19
+ * `⤷ { plain: "varchar", modifiers: "50", dimensions: 0, original: "varchar(50)" }`
20
+ *
21
+ * - `parseTypeName("int[]")`
22
+ *
23
+ * `⤷ { plain: "int", modifiers: null, dimensions: 1, original: "int[]" }`
24
+ *
25
+ * - `parseTypeName("public.my_table[][]")`
26
+ *
27
+ * `⤷ { plain: "public.my_table", modifiers: null, dimensions: 2, original: "public.my_table[][]" }`
28
+ *
29
+ * - `parseTypeName("numeric(10, 2)[]")`
30
+ *
31
+ * `⤷ { plain: "numeric", modifiers: "10, 2", dimensions: 1, original: "numeric(10, 2)[]" }`
32
+ *
33
+ * - `parseTypeName("geometry(Point, 4326)")`
34
+ *
35
+ * `⤷ { plain: "geometry", modifiers: "Point, 4326", dimensions: 0, original: "geometry(Point, 4326)" }`
36
+ *
37
+ * - `parseTypeName("_text")`
38
+ *
39
+ * `⤷ { plain: "_text", modifiers: null, dimensions: 0, original: "_text" }`
40
+ *
41
+ * Internal arrays aren't handled here
42
+ */
43
+ export declare function parseRawType(type: string): ParsedType;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Parses a PostgreSQL type name string to extract its base name,
3
+ * modifiers, and dimensions from explicit '[]' brackets.
4
+ *
5
+ * Examples:
6
+ *
7
+ * - `parseTypeName("varchar(50)")`
8
+ *
9
+ * `⤷ { plain: "varchar", modifiers: "50", dimensions: 0, original: "varchar(50)" }`
10
+ *
11
+ * - `parseTypeName("int[]")`
12
+ *
13
+ * `⤷ { plain: "int", modifiers: null, dimensions: 1, original: "int[]" }`
14
+ *
15
+ * - `parseTypeName("public.my_table[][]")`
16
+ *
17
+ * `⤷ { plain: "public.my_table", modifiers: null, dimensions: 2, original: "public.my_table[][]" }`
18
+ *
19
+ * - `parseTypeName("numeric(10, 2)[]")`
20
+ *
21
+ * `⤷ { plain: "numeric", modifiers: "10, 2", dimensions: 1, original: "numeric(10, 2)[]" }`
22
+ *
23
+ * - `parseTypeName("geometry(Point, 4326)")`
24
+ *
25
+ * `⤷ { plain: "geometry", modifiers: "Point, 4326", dimensions: 0, original: "geometry(Point, 4326)" }`
26
+ *
27
+ * - `parseTypeName("_text")`
28
+ *
29
+ * `⤷ { plain: "_text", modifiers: null, dimensions: 0, original: "_text" }`
30
+ *
31
+ * Internal arrays aren't handled here
32
+ */
33
+ export function parseRawType(type) {
34
+ let base = type;
35
+ let modifiers = null;
36
+ let dimensions = 0;
37
+ // 1. Extract modifiers (content within the last parentheses)
38
+ const modifierMatch = base.match(/\(([^)]*)\)$/);
39
+ if (modifierMatch) {
40
+ modifiers = modifierMatch[1];
41
+ base = base.substring(0, modifierMatch.index).trim();
42
+ }
43
+ // 2. Count and remove explicit array brackets '[]'
44
+ // Repeatedly remove '[]' from the end and count dimensions
45
+ while (base.endsWith("[]")) {
46
+ dimensions++;
47
+ base = base.slice(0, -2);
48
+ }
49
+ return { original: type, plain: base, modifiers, dimensions };
50
+ }
@@ -0,0 +1,11 @@
1
+ import type { DbAdapter } from "../adapter.ts";
2
+ import type { ExclusiveRange } from "./types.ts";
3
+ import { Canonical } from "./types.ts";
4
+ export type { ExclusiveRange };
5
+ /**
6
+ * Fetches the canonical name of the subtype for given range OIDs, ordered by sequence.
7
+ */
8
+ export declare function getRangeDetails(db: DbAdapter, enqueue: (types: string) => Canonical, entries: {
9
+ oid: number;
10
+ canonical_name: string;
11
+ }[]): Promise<ExclusiveRange[]>;
@@ -0,0 +1,36 @@
1
+ import { minifyQuery } from "../../util.js";
2
+ import { Canonical } from "./types.js";
3
+ const query = minifyQuery(`
4
+ -- Fetch the formatted name for each input OID, ordered by sequence
5
+ SELECT
6
+ u.seq,
7
+ format('%I.%I', n.nspname, t.typname) AS name
8
+ FROM unnest($1::oid[]) WITH ORDINALITY AS u(oid, seq)
9
+ JOIN pg_type t ON t.oid = u.oid
10
+ JOIN pg_namespace n ON t.typnamespace = n.oid
11
+ ORDER BY u.seq;
12
+ `);
13
+ /**
14
+ * Fetches the canonical name of the subtype for given range OIDs, ordered by sequence.
15
+ */
16
+ export async function getRangeDetails(db, enqueue, entries) {
17
+ if (entries.length === 0)
18
+ return [];
19
+ const oids = entries.map(i => i.oid);
20
+ const results = await db.query(query, [oids]);
21
+ if (results.length !== entries.length) {
22
+ throw new Error("Mismatch between input range count and range detail results count.");
23
+ }
24
+ return results.map((result, index) => {
25
+ const { canonical_name } = entries[index];
26
+ const subtypeName = result.name;
27
+ if (!subtypeName) {
28
+ throw new Error(`Range ${canonical_name} (Subtype OID: ${oids[index]}) lacks a resolved subtype name.`);
29
+ }
30
+ const canonicalSubtype = enqueue(subtypeName);
31
+ if (!canonicalSubtype) {
32
+ throw new Error(`Failed to canonicalise subtype "${subtypeName}" for Range ${canonical_name} (Subtype OID: ${oids[index]}).`);
33
+ }
34
+ return { kind: Canonical.Kind.Range, canonical_name, range_subtype: canonicalSubtype };
35
+ });
36
+ }
@@ -0,0 +1,19 @@
1
+ import type { DbAdapter } from "../adapter.ts";
2
+ import type { Canonical } from "./types.ts";
3
+ export interface ResolvedBasicInfo {
4
+ original_name: string;
5
+ oid: number;
6
+ internal_dimensions: number;
7
+ schema: string;
8
+ name: string;
9
+ canonical_name: string;
10
+ kind: Canonical.Kind;
11
+ typrelid: number;
12
+ typbasetype: number;
13
+ rngsubtype: number;
14
+ }
15
+ /**
16
+ * Takes base type names (without modifiers/brackets), resolves them to their ultimate base type OID
17
+ * and internal array dimensions, and fetches basic kind information
18
+ */
19
+ export declare function resolveBasicInfo(db: DbAdapter, types: string[]): Promise<ResolvedBasicInfo[]>;
@@ -0,0 +1,59 @@
1
+ import { minifyQuery } from "../../util.js";
2
+ const query = minifyQuery(`
3
+ WITH RECURSIVE
4
+ input(base_type_name, seq) AS (
5
+ SELECT * FROM unnest($1::text[]) WITH ORDINALITY
6
+ ),
7
+ type_resolution(seq, current_oid, level) AS (
8
+ -- Base case: Look up the initial base type name
9
+ SELECT i.seq, t.oid, 1, i.base_type_name
10
+ FROM input i JOIN pg_type t ON t.oid = i.base_type_name::regtype
11
+ UNION ALL
12
+ -- Recursive step: Follow typelem for standard arrays (_)
13
+ SELECT r.seq, t.typelem, r.level + 1, r.base_type_name
14
+ FROM type_resolution r JOIN pg_type t ON r.current_oid = t.oid
15
+ -- // TODO: do a more robust check for array types than 'left(t.typname, 1) = '_'
16
+ WHERE t.typelem != 0 AND left(t.typname, 1) = '_'
17
+ ),
18
+ final_resolution AS (
19
+ -- Get the OID and max level (depth) for each sequence number
20
+ SELECT DISTINCT ON (seq) seq, current_oid AS base_type_oid, level, base_type_name
21
+ FROM type_resolution ORDER BY seq, level DESC
22
+ )
23
+ -- Combine resolution with basic type info fetching
24
+ SELECT
25
+ fr.seq,
26
+ fr.base_type_name as original_name,
27
+ fr.base_type_oid AS oid,
28
+ (fr.level - 1) AS internal_dimensions,
29
+ n.nspname AS schema,
30
+ t.typname AS name,
31
+ n.nspname || '.' || t.typname AS canonical_name,
32
+ CASE t.typtype
33
+ WHEN 'b' THEN 'base'::text
34
+ WHEN 'c' THEN 'composite'::text
35
+ WHEN 'd' THEN 'domain'::text
36
+ WHEN 'e' THEN 'enum'::text
37
+ WHEN 'p' THEN 'pseudo'::text
38
+ WHEN 'r' THEN 'range'::text
39
+ ELSE 'unknown'::text
40
+ END AS kind,
41
+ t.typrelid, -- needed for composite details
42
+ t.typbasetype, -- needed for domain details
43
+ COALESCE(r.rngsubtype, 0) AS rngsubtype -- needed for range details
44
+ FROM final_resolution fr
45
+ JOIN pg_type t ON t.oid = fr.base_type_oid
46
+ JOIN pg_namespace n ON t.typnamespace = n.oid
47
+ LEFT JOIN pg_range r ON t.oid = r.rngtypid AND t.typtype = 'r'
48
+ ORDER BY fr.seq;
49
+ `);
50
+ /**
51
+ * Takes base type names (without modifiers/brackets), resolves them to their ultimate base type OID
52
+ * and internal array dimensions, and fetches basic kind information
53
+ */
54
+ export async function resolveBasicInfo(db, types) {
55
+ if (types.length === 0)
56
+ return [];
57
+ const results = await db.query(query, [types]);
58
+ return results;
59
+ }