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.
- package/lib/extractor/adapter.d.ts +9 -1
- package/lib/extractor/adapter.js +30 -1
- package/lib/extractor/canonicalise/composite.d.ts +7 -0
- package/lib/extractor/canonicalise/composite.js +53 -0
- package/lib/extractor/canonicalise/domain.d.ts +12 -0
- package/lib/extractor/canonicalise/domain.js +77 -0
- package/lib/extractor/canonicalise/enum.d.ts +8 -0
- package/lib/extractor/canonicalise/enum.js +22 -0
- package/lib/extractor/canonicalise/index.d.ts +11 -0
- package/lib/extractor/canonicalise/index.js +148 -0
- package/lib/extractor/canonicalise/parse.d.ts +43 -0
- package/lib/extractor/canonicalise/parse.js +50 -0
- package/lib/extractor/canonicalise/range.d.ts +11 -0
- package/lib/extractor/canonicalise/range.js +36 -0
- package/lib/extractor/canonicalise/resolve.d.ts +19 -0
- package/lib/extractor/canonicalise/resolve.js +59 -0
- package/lib/extractor/{canonicalise.d.ts → canonicalise/types.d.ts} +17 -11
- package/lib/extractor/canonicalise/types.js +13 -0
- package/lib/extractor/index.d.ts +7 -4
- package/lib/extractor/index.js +5 -10
- package/lib/extractor/kinds/composite.d.ts +1 -1
- package/lib/extractor/kinds/composite.js +1 -2
- package/lib/extractor/kinds/domain.d.ts +1 -1
- package/lib/extractor/kinds/domain.js +1 -2
- package/lib/extractor/kinds/function.d.ts +1 -1
- package/lib/extractor/kinds/function.js +5 -8
- package/lib/extractor/kinds/materialized-view.d.ts +1 -1
- package/lib/extractor/kinds/materialized-view.js +4 -8
- package/lib/extractor/kinds/range.d.ts +1 -1
- package/lib/extractor/kinds/range.js +1 -3
- package/lib/extractor/kinds/table.d.ts +1 -1
- package/lib/extractor/kinds/table.js +2 -8
- package/lib/extractor/kinds/view.d.ts +1 -1
- package/lib/extractor/kinds/view.js +4 -8
- package/lib/extractor/pgtype.d.ts +1 -1
- package/lib/imports.js +6 -3
- package/lib/imports.test.d.ts +1 -0
- package/lib/imports.test.js +180 -0
- package/lib/index.js +19 -3
- package/lib/kysely/index.js +2 -0
- package/lib/types.d.ts +1 -1
- package/lib/util.d.ts +4 -0
- package/lib/util.js +18 -0
- package/lib/zod/index.js +2 -10
- package/package.json +1 -1
- 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
|
21
|
+
* Close the connection and clear the cache
|
14
22
|
*/
|
15
23
|
close(): Promise<void>;
|
16
24
|
}
|
package/lib/extractor/adapter.js
CHANGED
@@ -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
|
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
|
+
}
|