@typesql/pg 0.1.5 → 0.2.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/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -3
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/introspect/cli.d.ts.map +1 -1
- package/dist/introspect/cli.js +26 -18
- package/dist/introspect/cli.js.map +1 -1
- package/dist/introspect/emit.d.ts.map +1 -1
- package/dist/introspect/emit.js +48 -11
- package/dist/introspect/emit.js.map +1 -1
- package/dist/introspect/index.d.ts +3 -4
- package/dist/introspect/index.d.ts.map +1 -1
- package/dist/introspect/index.js +1 -3
- package/dist/introspect/index.js.map +1 -1
- package/dist/introspect/introspect.d.ts.map +1 -1
- package/dist/introspect/introspect.js +40 -8
- package/dist/introspect/introspect.js.map +1 -1
- package/dist/introspect/nullability.d.ts +2 -1
- package/dist/introspect/nullability.d.ts.map +1 -1
- package/dist/introspect/nullability.js +101 -29
- package/dist/introspect/nullability.js.map +1 -1
- package/dist/type-map.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +63 -39
- package/src/index.ts +32 -15
- package/src/introspect/cli.ts +124 -100
- package/src/introspect/emit.ts +224 -139
- package/src/introspect/index.ts +9 -10
- package/src/introspect/introspect.ts +239 -155
- package/src/introspect/nullability.ts +399 -257
- package/src/type-map.ts +25 -24
package/src/introspect/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
export type { IntrospectConfig } from "./cli.js";
|
|
2
|
+
export { type GenerateOptions, generateSource } from "./emit.js";
|
|
1
3
|
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
type IntrospectedColumn,
|
|
5
|
+
type IntrospectedDatabase,
|
|
6
|
+
type IntrospectedEnum,
|
|
7
|
+
type IntrospectedRelation,
|
|
8
|
+
type IntrospectOptions,
|
|
9
|
+
introspect,
|
|
10
|
+
type RelationKind,
|
|
9
11
|
} from "./introspect.js";
|
|
10
|
-
export { generateSource, type GenerateOptions } from "./emit.js";
|
|
11
|
-
export { ViewNullability, parseNodeTree, type NullabilityOptions, type QueryFn } from "./nullability.js";
|
|
12
|
-
export { main, type IntrospectConfig } from "./cli.js";
|
|
@@ -1,119 +1,144 @@
|
|
|
1
1
|
import type { PgQueryable } from "../client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { type NullabilityOptions, ViewNullability } from "./nullability.js";
|
|
3
3
|
|
|
4
|
-
export type RelationKind =
|
|
4
|
+
export type RelationKind =
|
|
5
|
+
| "table"
|
|
6
|
+
| "partitioned table"
|
|
7
|
+
| "view"
|
|
8
|
+
| "materialized view"
|
|
9
|
+
| "foreign table";
|
|
5
10
|
|
|
6
11
|
export interface IntrospectedColumn {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/**
|
|
14
|
+
* Canonical Postgres type name as TypeSQL spells it: `int4`, `timestamptz`, `varchar`, an enum
|
|
15
|
+
* name, `${T}[]` for arrays. Domains are replaced by their base type; `citext` is `text`.
|
|
16
|
+
*/
|
|
17
|
+
readonly type: string;
|
|
18
|
+
/** The column's type as Postgres prints it (`character varying(40)`, a domain name, ...). */
|
|
19
|
+
readonly declaredType: string;
|
|
20
|
+
/** The domain the column is declared with, if any (`public.positive`). */
|
|
21
|
+
readonly domain: string | null;
|
|
22
|
+
readonly nullable: boolean;
|
|
23
|
+
/** Optional on INSERT: a DEFAULT, serial, identity or generated column. Always false for views. */
|
|
24
|
+
readonly hasDefault: boolean;
|
|
25
|
+
/** `GENERATED ALWAYS AS IDENTITY` / `GENERATED BY DEFAULT AS IDENTITY`. */
|
|
26
|
+
readonly identity: "always" | "by default" | null;
|
|
27
|
+
/** The expression of a `GENERATED ALWAYS AS (…) STORED` column. */
|
|
28
|
+
readonly generated: string | null;
|
|
29
|
+
/** The DEFAULT expression. */
|
|
30
|
+
readonly default: string | null;
|
|
31
|
+
readonly comment: string | null;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export interface IntrospectedRelation {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
readonly schema: string;
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly kind: RelationKind;
|
|
38
|
+
readonly comment: string | null;
|
|
39
|
+
readonly columns: readonly IntrospectedColumn[];
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export interface IntrospectedEnum {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
readonly schema: string;
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** Labels in sort order. */
|
|
46
|
+
readonly values: readonly string[];
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export interface IntrospectedDatabase {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/** `server_version`, e.g. `17.2`. */
|
|
51
|
+
readonly version: string;
|
|
52
|
+
/** Schemas that were read, in order. */
|
|
53
|
+
readonly schemas: readonly string[];
|
|
54
|
+
readonly relations: readonly IntrospectedRelation[];
|
|
55
|
+
/** Enums of the read schemas and enums their columns use. */
|
|
56
|
+
readonly enums: readonly IntrospectedEnum[];
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
export interface IntrospectOptions {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
/** Schemas to read. Default: every schema except `pg_catalog`, `information_schema` and `pg_*`. */
|
|
61
|
+
readonly schemas?: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* Relations to leave out: names (`kysely_migration`) or `schema.name`, with `*` matching any
|
|
64
|
+
* run of characters (`kysely_migration*`, `audit.*`).
|
|
65
|
+
*/
|
|
66
|
+
readonly exclude?: readonly string[];
|
|
67
|
+
/**
|
|
68
|
+
* View columns to treat as NOT NULL when the analysis cannot prove it, called with the view's
|
|
69
|
+
* name and the column's name.
|
|
70
|
+
*/
|
|
71
|
+
readonly viewNotNull?: NullabilityOptions["notNull"];
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
const globToRegExp = (glob: string): RegExp =>
|
|
70
|
-
|
|
75
|
+
new RegExp(
|
|
76
|
+
`^${glob
|
|
77
|
+
.split("*")
|
|
78
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
79
|
+
.join(".*")}$`,
|
|
80
|
+
);
|
|
71
81
|
|
|
72
82
|
const KINDS: Readonly<Record<string, RelationKind>> = {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
r: "table",
|
|
84
|
+
p: "partitioned table",
|
|
85
|
+
v: "view",
|
|
86
|
+
m: "materialized view",
|
|
87
|
+
f: "foreign table",
|
|
78
88
|
};
|
|
79
89
|
|
|
80
90
|
/** Postgres type names TypeSQL spells differently. */
|
|
81
91
|
const TYPE_ALIASES: Readonly<Record<string, string>> = { citext: "text" };
|
|
82
92
|
|
|
83
93
|
interface TypeRow {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
readonly oid: string;
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly schema: string;
|
|
97
|
+
readonly typtype: string;
|
|
98
|
+
readonly basetype: string;
|
|
99
|
+
readonly elem: string;
|
|
100
|
+
readonly array: string;
|
|
101
|
+
readonly notnull: boolean;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
interface ResolvedType {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
readonly type: string;
|
|
106
|
+
readonly notnull: boolean;
|
|
107
|
+
readonly enumOid: string | null;
|
|
108
|
+
readonly domain: string | null;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
function resolver(types: ReadonlyMap<string, TypeRow>) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
const resolve = (oid: string): ResolvedType => {
|
|
113
|
+
const t = types.get(oid);
|
|
114
|
+
if (!t)
|
|
115
|
+
return { type: "unknown", notnull: false, enumOid: null, domain: null };
|
|
116
|
+
if (t.typtype === "d") {
|
|
117
|
+
const base = resolve(t.basetype);
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
notnull: base.notnull || t.notnull,
|
|
121
|
+
domain: `${t.schema}.${t.name}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const elem = t.elem !== "0" ? types.get(t.elem) : undefined;
|
|
125
|
+
if (elem && elem.array === t.oid) {
|
|
126
|
+
const inner = resolve(elem.oid);
|
|
127
|
+
return {
|
|
128
|
+
type: inner.type.endsWith("[]") ? inner.type : `${inner.type}[]`,
|
|
129
|
+
notnull: false,
|
|
130
|
+
enumOid: inner.enumOid,
|
|
131
|
+
domain: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
type: TYPE_ALIASES[t.name] ?? t.name,
|
|
136
|
+
notnull: false,
|
|
137
|
+
enumOid: t.typtype === "e" ? t.oid : null,
|
|
138
|
+
domain: null,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
return resolve;
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
/** Default schema filter: everything but the system schemas. */
|
|
@@ -123,48 +148,79 @@ const USER_SCHEMAS = `n.nspname NOT IN ('pg_catalog', 'information_schema') AND
|
|
|
123
148
|
* Read tables, views, materialized views, foreign tables and enums of a live database through
|
|
124
149
|
* any node-postgres-style client (`Client`, `Pool`, `PoolClient`).
|
|
125
150
|
*/
|
|
126
|
-
export async function introspect(
|
|
127
|
-
|
|
151
|
+
export async function introspect(
|
|
152
|
+
client: PgQueryable,
|
|
153
|
+
options: IntrospectOptions = {},
|
|
154
|
+
): Promise<IntrospectedDatabase> {
|
|
155
|
+
const q = async <R>(text: string, values?: unknown[]): Promise<R[]> =>
|
|
156
|
+
(await client.query(text, values)).rows as R[];
|
|
128
157
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
const [{ version }] = (await q<{ version: string }>(
|
|
159
|
+
`SELECT current_setting('server_version') AS version`,
|
|
160
|
+
)) as [{ version: string }];
|
|
161
|
+
const schemaRows = options.schemas
|
|
162
|
+
? await q<{ name: string }>(
|
|
163
|
+
`SELECT n.nspname AS name FROM unnest($1::text[]) WITH ORDINALITY AS s(name, i)
|
|
133
164
|
JOIN pg_namespace n ON n.nspname = s.name ORDER BY s.i`,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
165
|
+
[[...options.schemas]],
|
|
166
|
+
)
|
|
167
|
+
: await q<{ name: string }>(
|
|
168
|
+
`SELECT n.nspname AS name FROM pg_namespace n WHERE ${USER_SCHEMAS}
|
|
138
169
|
ORDER BY n.nspname <> 'public', n.nspname`,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
170
|
+
);
|
|
171
|
+
const schemas = schemaRows.map((r) => r.name);
|
|
172
|
+
const missing = (options.schemas ?? []).filter((s) => !schemas.includes(s));
|
|
173
|
+
if (missing.length > 0)
|
|
174
|
+
throw new Error(
|
|
175
|
+
`schema ${missing.map((s) => JSON.stringify(s)).join(", ")} does not exist`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const types = new Map(
|
|
179
|
+
(
|
|
180
|
+
await q<TypeRow>(
|
|
181
|
+
`SELECT t.oid::int8::text AS oid, t.typname AS name, n.nspname AS schema, t.typtype,
|
|
147
182
|
t.typbasetype::int8::text AS basetype, t.typelem::int8::text AS elem,
|
|
148
183
|
t.typarray::int8::text AS array, t.typnotnull AS notnull
|
|
149
184
|
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace`,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
)
|
|
186
|
+
).map((t) => [t.oid, t]),
|
|
187
|
+
);
|
|
188
|
+
const resolve = resolver(types);
|
|
153
189
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
190
|
+
const excluded = (options.exclude ?? []).map(globToRegExp);
|
|
191
|
+
const relRows = (
|
|
192
|
+
await q<{
|
|
193
|
+
oid: string;
|
|
194
|
+
schema: string;
|
|
195
|
+
name: string;
|
|
196
|
+
relkind: string;
|
|
197
|
+
comment: string | null;
|
|
198
|
+
}>(
|
|
199
|
+
`SELECT c.oid::int8::text AS oid, n.nspname AS schema, c.relname AS name, c.relkind,
|
|
157
200
|
obj_description(c.oid, 'pg_class') AS comment
|
|
158
201
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
159
202
|
WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f') AND n.nspname = ANY($1::text[])
|
|
160
203
|
ORDER BY array_position($1::text[], n.nspname::text), c.relname`,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
[schemas],
|
|
205
|
+
)
|
|
206
|
+
).filter(
|
|
207
|
+
(r) =>
|
|
208
|
+
!excluded.some(
|
|
209
|
+
(re) => re.test(r.name) || re.test(`${r.schema}.${r.name}`),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
const colRows = await q<{
|
|
213
|
+
relid: string;
|
|
214
|
+
name: string;
|
|
215
|
+
typid: string;
|
|
216
|
+
declared: string;
|
|
217
|
+
notnull: boolean;
|
|
218
|
+
identity: string;
|
|
219
|
+
generated: string;
|
|
220
|
+
default: string | null;
|
|
221
|
+
comment: string | null;
|
|
222
|
+
}>(
|
|
223
|
+
`SELECT a.attrelid::int8::text AS relid, a.attname AS name, a.atttypid::int8::text AS typid,
|
|
168
224
|
format_type(a.atttypid, a.atttypmod) AS declared, a.attnotnull AS notnull,
|
|
169
225
|
a.attidentity AS identity, a.attgenerated AS generated,
|
|
170
226
|
pg_get_expr(d.adbin, d.adrelid) AS default, col_description(a.attrelid, a.attnum) AS comment
|
|
@@ -172,50 +228,78 @@ export async function introspect(client: PgQueryable, options: IntrospectOptions
|
|
|
172
228
|
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
|
|
173
229
|
WHERE a.attrelid = ANY($1::oid[]) AND a.attnum > 0 AND NOT a.attisdropped
|
|
174
230
|
ORDER BY a.attrelid, a.attnum`,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
[relRows.map((r) => r.oid)],
|
|
232
|
+
);
|
|
233
|
+
const colsByRel = new Map<string, typeof colRows>();
|
|
234
|
+
for (const c of colRows)
|
|
235
|
+
colsByRel.set(c.relid, [...(colsByRel.get(c.relid) ?? []), c]);
|
|
236
|
+
|
|
237
|
+
const views = new ViewNullability(
|
|
238
|
+
(text, values) => client.query(text, values),
|
|
239
|
+
{
|
|
240
|
+
...(options.viewNotNull && { notNull: options.viewNotNull }),
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
const usedEnums = new Set<string>();
|
|
244
|
+
const relations: IntrospectedRelation[] = [];
|
|
245
|
+
for (const rel of relRows) {
|
|
246
|
+
const kind = KINDS[rel.relkind]!;
|
|
247
|
+
const isView = kind === "view" || kind === "materialized view";
|
|
248
|
+
const viewNotNull = isView ? await views.notNull(rel.oid) : [];
|
|
249
|
+
const columns = (colsByRel.get(rel.oid) ?? []).map(
|
|
250
|
+
(c, i): IntrospectedColumn => {
|
|
251
|
+
const t = resolve(c.typid);
|
|
252
|
+
if (t.enumOid) usedEnums.add(t.enumOid);
|
|
253
|
+
const generated = c.generated !== "" ? c.default : null;
|
|
254
|
+
const identity =
|
|
255
|
+
c.identity === "a"
|
|
256
|
+
? "always"
|
|
257
|
+
: c.identity === "d"
|
|
258
|
+
? "by default"
|
|
259
|
+
: null;
|
|
260
|
+
return {
|
|
261
|
+
name: c.name,
|
|
262
|
+
type: t.type,
|
|
263
|
+
declaredType: c.declared,
|
|
264
|
+
domain: t.domain,
|
|
265
|
+
nullable: isView ? !viewNotNull[i] : !(c.notnull || t.notnull),
|
|
266
|
+
hasDefault:
|
|
267
|
+
!isView &&
|
|
268
|
+
(c.default !== null || identity !== null || generated !== null),
|
|
269
|
+
identity,
|
|
270
|
+
generated,
|
|
271
|
+
default: generated === null ? c.default : null,
|
|
272
|
+
comment: c.comment,
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
relations.push({
|
|
277
|
+
schema: rel.schema,
|
|
278
|
+
name: rel.name,
|
|
279
|
+
kind,
|
|
280
|
+
comment: rel.comment,
|
|
281
|
+
columns,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const enumRows = await q<{
|
|
286
|
+
oid: string;
|
|
287
|
+
schema: string;
|
|
288
|
+
name: string;
|
|
289
|
+
values: string[];
|
|
290
|
+
}>(
|
|
291
|
+
`SELECT t.oid::int8::text AS oid, n.nspname AS schema, t.typname AS name,
|
|
212
292
|
array_agg(e.enumlabel::text ORDER BY e.enumsortorder) AS values
|
|
213
293
|
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace JOIN pg_enum e ON e.enumtypid = t.oid
|
|
214
294
|
WHERE n.nspname = ANY($1::text[]) OR t.oid = ANY($2::oid[])
|
|
215
295
|
GROUP BY t.oid, n.nspname, t.typname
|
|
216
296
|
ORDER BY t.typname, n.nspname`,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
297
|
+
[schemas, [...usedEnums]],
|
|
298
|
+
);
|
|
299
|
+
const enums = enumRows.map(({ schema, name, values }) => ({
|
|
300
|
+
schema,
|
|
301
|
+
name,
|
|
302
|
+
values,
|
|
303
|
+
}));
|
|
304
|
+
return { version, schemas, relations, enums };
|
|
221
305
|
}
|