@typesql/pg 0.2.7 → 0.2.9
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 +18 -0
- package/README.md +1 -2
- package/dist/bin.js +0 -1
- package/dist/bin.js.map +1 -1
- package/dist/client.d.ts +0 -19
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +0 -22
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/introspect/cli.d.ts +0 -5
- package/dist/introspect/cli.d.ts.map +1 -1
- package/dist/introspect/cli.js +0 -1
- package/dist/introspect/cli.js.map +1 -1
- package/dist/introspect/emit.d.ts +0 -19
- package/dist/introspect/emit.d.ts.map +1 -1
- package/dist/introspect/emit.js +0 -11
- package/dist/introspect/emit.js.map +1 -1
- package/dist/introspect/introspect.d.ts +0 -27
- package/dist/introspect/introspect.d.ts.map +1 -1
- package/dist/introspect/introspect.js +0 -11
- package/dist/introspect/introspect.js.map +1 -1
- package/dist/introspect/nullability.d.ts +0 -27
- package/dist/introspect/nullability.d.ts.map +1 -1
- package/dist/introspect/nullability.js +9 -61
- package/dist/introspect/nullability.js.map +1 -1
- package/dist/type-map.d.ts +0 -65
- package/dist/type-map.d.ts.map +1 -1
- package/package.json +4 -6
- package/src/bin.ts +0 -1
- package/src/client.ts +0 -31
- package/src/index.ts +0 -7
- package/src/introspect/cli.ts +0 -5
- package/src/introspect/emit.ts +0 -25
- package/src/introspect/introspect.ts +0 -35
- package/src/introspect/nullability.ts +8 -70
- package/src/type-map.ts +0 -65
package/src/introspect/emit.ts
CHANGED
|
@@ -5,33 +5,19 @@ import type {
|
|
|
5
5
|
} from "./introspect.js";
|
|
6
6
|
|
|
7
7
|
export interface GenerateOptions {
|
|
8
|
-
/** Name of the exported schema interface. Default `"DB"`. */
|
|
9
8
|
readonly typeName?: string;
|
|
10
|
-
/** Module the column helpers are imported from. Default `"@typesql/core"`. */
|
|
11
9
|
readonly importFrom?: string;
|
|
12
|
-
/** Extra lines after the helper import, e.g. `import type { HistoryItem } from "./json.js";`. */
|
|
13
10
|
readonly imports?: readonly string[];
|
|
14
|
-
/**
|
|
15
|
-
* TypeScript types of `json` / `jsonb` columns: a map keyed `"table.column"` (public) or
|
|
16
|
-
* `"schema.table.column"`, or a function returning the TS type source for a column (or
|
|
17
|
-
* `undefined`). Such a column is declared as `Json<T>`: still jsonb to the SQL, read as `T`,
|
|
18
|
-
* written with `sql.json(value)`. Other columns are an error (use `column` for those).
|
|
19
|
-
*/
|
|
20
11
|
readonly columnTypes?:
|
|
21
12
|
| Readonly<Record<string, string>>
|
|
22
13
|
| ((
|
|
23
14
|
column: IntrospectedColumn,
|
|
24
15
|
relation: IntrospectedRelation,
|
|
25
16
|
) => string | undefined);
|
|
26
|
-
/**
|
|
27
|
-
* Full control over one column: return the TypeScript source of its descriptor
|
|
28
|
-
* (`Col<"jsonb">`, a helper of your own, ...) or `undefined` to keep the default.
|
|
29
|
-
*/
|
|
30
17
|
readonly column?: (
|
|
31
18
|
column: IntrospectedColumn,
|
|
32
19
|
relation: IntrospectedRelation,
|
|
33
20
|
) => string | undefined;
|
|
34
|
-
/** Emit table and column comments and notes as JSDoc. Default `true`. */
|
|
35
21
|
readonly comments?: boolean;
|
|
36
22
|
}
|
|
37
23
|
|
|
@@ -76,11 +62,6 @@ function columnNotes(c: IntrospectedColumn): string[] {
|
|
|
76
62
|
return notes;
|
|
77
63
|
}
|
|
78
64
|
|
|
79
|
-
/**
|
|
80
|
-
* TypeScript source declaring the TypeSQL schema of an introspected database: `public`
|
|
81
|
-
* relations at the top level, other schemas nested (`DB["auth"]["sessions"]`), views and
|
|
82
|
-
* materialized views as `View<…>`, enums under `$types`.
|
|
83
|
-
*/
|
|
84
65
|
export function generateSource(
|
|
85
66
|
db: IntrospectedDatabase,
|
|
86
67
|
options: GenerateOptions = {},
|
|
@@ -91,7 +72,6 @@ export function generateSource(
|
|
|
91
72
|
const used = new Set<string>();
|
|
92
73
|
const usedKeys = new Set<string>();
|
|
93
74
|
|
|
94
|
-
/** The TS type `columnTypes` gives a column, if any. */
|
|
95
75
|
const customType = (
|
|
96
76
|
rel: IntrospectedRelation,
|
|
97
77
|
col: IntrospectedColumn,
|
|
@@ -101,7 +81,6 @@ export function generateSource(
|
|
|
101
81
|
let ts: string | undefined;
|
|
102
82
|
if (typeof columnTypes === "function") ts = columnTypes(col, rel);
|
|
103
83
|
else {
|
|
104
|
-
// The keys contain a dot, so no key is a property every object has.
|
|
105
84
|
const k =
|
|
106
85
|
rel.schema === "public" && !(qualified in columnTypes)
|
|
107
86
|
? short
|
|
@@ -117,7 +96,6 @@ export function generateSource(
|
|
|
117
96
|
return ts;
|
|
118
97
|
};
|
|
119
98
|
|
|
120
|
-
/** `Json<T>`, `Json<T, { nullable: true }>`, `Json<T, {}, "json">`, … */
|
|
121
99
|
const jsonDescriptor = (col: IntrospectedColumn, ts: string): string => {
|
|
122
100
|
const flags = [
|
|
123
101
|
...(col.nullable ? ["nullable: true"] : []),
|
|
@@ -185,8 +163,6 @@ export function generateSource(
|
|
|
185
163
|
}
|
|
186
164
|
|
|
187
165
|
if (db.enums.length > 0) {
|
|
188
|
-
// TypeSQL's enum registry is flat (`Col<"mood">`, schema qualifiers are dropped), so enums
|
|
189
|
-
// of the same name in different schemas share an entry.
|
|
190
166
|
const byName = new Map<
|
|
191
167
|
string,
|
|
192
168
|
{ values: Set<string>; schemas: string[] }
|
|
@@ -237,7 +213,6 @@ export function generateSource(
|
|
|
237
213
|
"}",
|
|
238
214
|
"",
|
|
239
215
|
];
|
|
240
|
-
// A function has no keys of its own.
|
|
241
216
|
const unknownKeys = Object.keys(columnTypes).filter((k) => !usedKeys.has(k));
|
|
242
217
|
if (unknownKeys.length > 0 && options.column === undefined) {
|
|
243
218
|
throw new Error(
|
|
@@ -10,23 +10,13 @@ export type RelationKind =
|
|
|
10
10
|
|
|
11
11
|
export interface IntrospectedColumn {
|
|
12
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
13
|
readonly type: string;
|
|
18
|
-
/** The column's type as Postgres prints it (`character varying(40)`, a domain name, ...). */
|
|
19
14
|
readonly declaredType: string;
|
|
20
|
-
/** The domain the column is declared with, if any (`public.positive`). */
|
|
21
15
|
readonly domain: string | null;
|
|
22
16
|
readonly nullable: boolean;
|
|
23
|
-
/** Optional on INSERT: a DEFAULT, serial, identity or generated column. Always false for views. */
|
|
24
17
|
readonly hasDefault: boolean;
|
|
25
|
-
/** `GENERATED ALWAYS AS IDENTITY` / `GENERATED BY DEFAULT AS IDENTITY`. */
|
|
26
18
|
readonly identity: "always" | "by default" | null;
|
|
27
|
-
/** The expression of a `GENERATED ALWAYS AS (…) STORED` column. */
|
|
28
19
|
readonly generated: string | null;
|
|
29
|
-
/** The DEFAULT expression. */
|
|
30
20
|
readonly default: string | null;
|
|
31
21
|
readonly comment: string | null;
|
|
32
22
|
}
|
|
@@ -42,32 +32,19 @@ export interface IntrospectedRelation {
|
|
|
42
32
|
export interface IntrospectedEnum {
|
|
43
33
|
readonly schema: string;
|
|
44
34
|
readonly name: string;
|
|
45
|
-
/** Labels in sort order. */
|
|
46
35
|
readonly values: readonly string[];
|
|
47
36
|
}
|
|
48
37
|
|
|
49
38
|
export interface IntrospectedDatabase {
|
|
50
|
-
/** `server_version`, e.g. `17.2`. */
|
|
51
39
|
readonly version: string;
|
|
52
|
-
/** Schemas that were read, in order. */
|
|
53
40
|
readonly schemas: readonly string[];
|
|
54
41
|
readonly relations: readonly IntrospectedRelation[];
|
|
55
|
-
/** Enums of the read schemas and enums their columns use. */
|
|
56
42
|
readonly enums: readonly IntrospectedEnum[];
|
|
57
43
|
}
|
|
58
44
|
|
|
59
45
|
export interface IntrospectOptions {
|
|
60
|
-
/** Schemas to read. Default: every schema except `pg_catalog`, `information_schema` and `pg_*`. */
|
|
61
46
|
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
47
|
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
48
|
readonly viewNotNull?: NullabilityOptions["notNull"];
|
|
72
49
|
}
|
|
73
50
|
|
|
@@ -87,7 +64,6 @@ const KINDS: Readonly<Record<string, RelationKind>> = {
|
|
|
87
64
|
f: "foreign table",
|
|
88
65
|
};
|
|
89
66
|
|
|
90
|
-
/** Postgres type names TypeSQL spells differently. */
|
|
91
67
|
const TYPE_ALIASES: Readonly<Record<string, string>> = { citext: "text" };
|
|
92
68
|
|
|
93
69
|
interface TypeRow {
|
|
@@ -104,7 +80,6 @@ interface TypeRow {
|
|
|
104
80
|
interface ResolvedType {
|
|
105
81
|
readonly type: string;
|
|
106
82
|
readonly notnull: boolean;
|
|
107
|
-
/** The type under the domains and arrays: the enum the column uses, if it is one. */
|
|
108
83
|
readonly scalarOid: string;
|
|
109
84
|
readonly domain: string | null;
|
|
110
85
|
}
|
|
@@ -112,7 +87,6 @@ interface ResolvedType {
|
|
|
112
87
|
function resolver(types: ReadonlyMap<string, TypeRow>) {
|
|
113
88
|
const resolve = (oid: string): ResolvedType => {
|
|
114
89
|
const t = types.get(oid);
|
|
115
|
-
// A type created after the types were read (a concurrent CREATE TYPE and CREATE TABLE)
|
|
116
90
|
if (!t)
|
|
117
91
|
return { type: "unknown", notnull: false, scalarOid: oid, domain: null };
|
|
118
92
|
if (t.typtype === "d") {
|
|
@@ -123,8 +97,6 @@ function resolver(types: ReadonlyMap<string, TypeRow>) {
|
|
|
123
97
|
domain: `${t.schema}.${t.name}`,
|
|
124
98
|
};
|
|
125
99
|
}
|
|
126
|
-
// `typelem` is "0" (no type) for scalars; fixed-length types like `point` have one but are
|
|
127
|
-
// not its array type.
|
|
128
100
|
const elem = types.get(t.elem);
|
|
129
101
|
if (elem && elem.array === t.oid) {
|
|
130
102
|
const inner = resolve(elem.oid);
|
|
@@ -145,13 +117,8 @@ function resolver(types: ReadonlyMap<string, TypeRow>) {
|
|
|
145
117
|
return resolve;
|
|
146
118
|
}
|
|
147
119
|
|
|
148
|
-
/** Default schema filter: everything but the system schemas. */
|
|
149
120
|
const USER_SCHEMAS = `n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg\\_%'`;
|
|
150
121
|
|
|
151
|
-
/**
|
|
152
|
-
* Read tables, views, materialized views, foreign tables and enums of a live database through
|
|
153
|
-
* any node-postgres-style client (`Client`, `Pool`, `PoolClient`).
|
|
154
|
-
*/
|
|
155
122
|
export async function introspect(
|
|
156
123
|
client: PgQueryable,
|
|
157
124
|
options: IntrospectOptions = {},
|
|
@@ -244,7 +211,6 @@ export async function introspect(
|
|
|
244
211
|
...(options.viewNotNull && { notNull: options.viewNotNull }),
|
|
245
212
|
},
|
|
246
213
|
);
|
|
247
|
-
/** Types the columns use: their enums are read even outside `schemas`. */
|
|
248
214
|
const usedTypes = new Set<string>();
|
|
249
215
|
const relations: IntrospectedRelation[] = [];
|
|
250
216
|
for (const rel of relRows) {
|
|
@@ -268,7 +234,6 @@ export async function introspect(
|
|
|
268
234
|
declaredType: c.declared,
|
|
269
235
|
domain: t.domain,
|
|
270
236
|
nullable: viewNotNull ? !viewNotNull[i] : !(c.notnull || t.notnull),
|
|
271
|
-
// A generated column's expression is its `default` row in pg_attrdef.
|
|
272
237
|
hasDefault: !isView && (c.default !== null || identity !== null),
|
|
273
238
|
identity,
|
|
274
239
|
generated,
|
|
@@ -1,45 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Nullability of view columns, derived from the view's query tree.
|
|
3
|
-
*
|
|
4
|
-
* Postgres records no NOT NULL information for view (or materialized view) columns. Its rewrite
|
|
5
|
-
* rule (`pg_rewrite.ev_action`, the `_RETURN` rule) holds the parsed query, though, and from
|
|
6
|
-
* PostgreSQL 16 every column reference in it carries `varnullingrels`, the outer joins that can
|
|
7
|
-
* null it. A view column is NOT NULL here only when its expression provably cannot be NULL: a
|
|
8
|
-
* NOT NULL table column that no outer join nulls, a non-NULL constant, a cast or a strict
|
|
9
|
-
* operator over such values, a CASE whose every branch is NOT NULL, `count(*)`, ... Anything
|
|
10
|
-
* else (function calls outside a short list, scalar subqueries, set-returning function columns)
|
|
11
|
-
* is nullable. On PostgreSQL 15 and older every column reference counts as nullable.
|
|
12
|
-
*
|
|
13
|
-
* This module has no imports so that scripts/pg-catalog-from-pg.ts can load it with Node's
|
|
14
|
-
* type stripping.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** Runs a query: node-postgres' `client.query(text, values)` fits. */
|
|
18
1
|
export type QueryFn = (
|
|
19
2
|
text: string,
|
|
20
3
|
values?: unknown[],
|
|
21
4
|
) => Promise<{ readonly rows: readonly any[] }>;
|
|
22
5
|
|
|
23
6
|
export interface NullabilityOptions {
|
|
24
|
-
/**
|
|
25
|
-
* Columns known never to be NULL that the analysis cannot prove (typically columns of a
|
|
26
|
-
* set-returning function). Called with the view's name and the column's name.
|
|
27
|
-
*/
|
|
28
7
|
readonly notNull?: (view: string, column: string) => boolean;
|
|
29
8
|
}
|
|
30
9
|
|
|
31
|
-
/** A node of a parsed `pg_node_tree`: `{ _: "VAR", varno: "1", … }`. */
|
|
32
10
|
export interface Node {
|
|
33
11
|
readonly _: string;
|
|
34
12
|
readonly [field: string]: NodeValue;
|
|
35
13
|
}
|
|
36
14
|
export type NodeValue = Node | readonly NodeValue[] | string | null;
|
|
37
15
|
|
|
38
|
-
// ───────────────────────────── pg_node_tree reader ─────────────────────────────
|
|
39
|
-
|
|
40
16
|
type Token = string | { readonly str: string };
|
|
41
17
|
|
|
42
|
-
/** Tokens of the text form of a node tree (`outfuncs.c`). `[ … ]` datum bytes are skipped. */
|
|
43
18
|
function tokenize(s: string): Token[] {
|
|
44
19
|
const out: Token[] = [];
|
|
45
20
|
let i = 0;
|
|
@@ -79,7 +54,6 @@ function tokenize(s: string): Token[] {
|
|
|
79
54
|
return out;
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
/** Parse a node tree into plain values: nodes as objects, lists as arrays, `<>` as null. */
|
|
83
57
|
export function parseNodeTree(text: string): NodeValue {
|
|
84
58
|
const toks = tokenize(text);
|
|
85
59
|
let p = 0;
|
|
@@ -91,7 +65,7 @@ export function parseNodeTree(text: string): NodeValue {
|
|
|
91
65
|
const node: Record<string, NodeValue> = { _: toks[p++] as string };
|
|
92
66
|
while (p < toks.length && toks[p] !== "}") {
|
|
93
67
|
const label = toks[p++];
|
|
94
|
-
if (!isLabel(label)) continue;
|
|
68
|
+
if (!isLabel(label)) continue;
|
|
95
69
|
if (toks[p] === "}" || isLabel(toks[p])) {
|
|
96
70
|
node[(label as string).slice(1)] = null;
|
|
97
71
|
continue;
|
|
@@ -120,9 +94,6 @@ const node = (x: NodeValue | undefined): Node | null =>
|
|
|
120
94
|
const str = (x: NodeValue | undefined): string | null =>
|
|
121
95
|
typeof x === "string" ? x : null;
|
|
122
96
|
|
|
123
|
-
// ───────────────────────────── function knowledge ─────────────────────────────
|
|
124
|
-
|
|
125
|
-
/** `pg_catalog` functions that return a non-NULL value for non-NULL arguments. */
|
|
126
97
|
const NON_NULL_FUNCTIONS = new Set([
|
|
127
98
|
"current_database",
|
|
128
99
|
"current_user",
|
|
@@ -167,7 +138,6 @@ const NON_NULL_FUNCTIONS = new Set([
|
|
|
167
138
|
"age",
|
|
168
139
|
]);
|
|
169
140
|
|
|
170
|
-
/** `pg_stat_get_*` accessors returning a number report 0 when no statistics exist, except these. */
|
|
171
141
|
const NULLABLE_STAT_FUNCTIONS = new Set([
|
|
172
142
|
"pg_stat_get_db_checksum_failures",
|
|
173
143
|
"pg_stat_get_backend_pid",
|
|
@@ -179,9 +149,7 @@ const isNonNullStatFunction = (name: string, ret: string): boolean =>
|
|
|
179
149
|
(ret === "int8" || ret === "int4" || ret === "float8") &&
|
|
180
150
|
!NULLABLE_STAT_FUNCTIONS.has(name);
|
|
181
151
|
|
|
182
|
-
/** Aggregates that never return NULL. */
|
|
183
152
|
const NON_NULL_AGGREGATES = new Set(["count"]);
|
|
184
|
-
/** Window functions that never return NULL. */
|
|
185
153
|
const NON_NULL_WINDOW_FUNCTIONS = new Set([
|
|
186
154
|
"count",
|
|
187
155
|
"row_number",
|
|
@@ -191,12 +159,6 @@ const NON_NULL_WINDOW_FUNCTIONS = new Set([
|
|
|
191
159
|
"cume_dist",
|
|
192
160
|
]);
|
|
193
161
|
|
|
194
|
-
/**
|
|
195
|
-
* Set-returning functions in FROM and their output columns (1-based) that are never NULL. A
|
|
196
|
-
* strict set-returning function returns no rows for NULL arguments, so the arguments do not
|
|
197
|
-
* matter. `jsonb_each` values are `jsonb` (a JSON null is `'null'::jsonb`, not SQL NULL);
|
|
198
|
-
* the `_text` variants turn a JSON null into SQL NULL.
|
|
199
|
-
*/
|
|
200
162
|
const NON_NULL_SRF_COLUMNS: Readonly<Record<string, readonly number[]>> = {
|
|
201
163
|
generate_series: [1],
|
|
202
164
|
generate_subscripts: [1],
|
|
@@ -212,11 +174,6 @@ const NON_NULL_SRF_COLUMNS: Readonly<Record<string, readonly number[]>> = {
|
|
|
212
174
|
regexp_split_to_table: [1],
|
|
213
175
|
};
|
|
214
176
|
|
|
215
|
-
/**
|
|
216
|
-
* `pg_catalog` operators that can return NULL for non-NULL operands: JSON field and path access
|
|
217
|
-
* (a missing key) and jsonpath matches (an unknown result). Other built-in operators are strict
|
|
218
|
-
* and return a value for non-NULL operands; operators outside `pg_catalog` count as nullable.
|
|
219
|
-
*/
|
|
220
177
|
const NULLABLE_OPERATORS = new Set(["->", "->>", "#>", "#>>", "@@", "@?"]);
|
|
221
178
|
|
|
222
179
|
interface Proc {
|
|
@@ -230,9 +187,6 @@ interface Att {
|
|
|
230
187
|
readonly relkind: string;
|
|
231
188
|
}
|
|
232
189
|
|
|
233
|
-
// ───────────────────────────── analysis ─────────────────────────────
|
|
234
|
-
|
|
235
|
-
/** Per-column NOT NULL of views and materialized views; caches what it reads. */
|
|
236
190
|
export class ViewNullability {
|
|
237
191
|
private readonly query: QueryFn;
|
|
238
192
|
private readonly options: NullabilityOptions;
|
|
@@ -247,11 +201,8 @@ export class ViewNullability {
|
|
|
247
201
|
this.options = options;
|
|
248
202
|
}
|
|
249
203
|
|
|
250
|
-
/** NOT NULL flags of the view's columns, in column order. `relid` is the view's `pg_class.oid`. */
|
|
251
204
|
notNull(relid: string | number): Promise<boolean[]> {
|
|
252
205
|
const key = String(relid);
|
|
253
|
-
// CREATE OR REPLACE VIEW allows a view that reads itself through others: waiting for its own
|
|
254
|
-
// analysis would never end
|
|
255
206
|
if (this.analyzing.has(key)) return Promise.resolve([]);
|
|
256
207
|
let cached = this.views.get(key);
|
|
257
208
|
if (!cached) {
|
|
@@ -307,7 +258,6 @@ export class ViewNullability {
|
|
|
307
258
|
return this.operators;
|
|
308
259
|
}
|
|
309
260
|
|
|
310
|
-
/** A built-in operator that returns a value for non-NULL operands. */
|
|
311
261
|
private async strictOperator(e: Node): Promise<boolean> {
|
|
312
262
|
const op = (await this.loadOperators()).get(String(e["opno"]));
|
|
313
263
|
return op?.system === true && !NULLABLE_OPERATORS.has(op.name);
|
|
@@ -317,7 +267,6 @@ export class ViewNullability {
|
|
|
317
267
|
let cached = this.atts.get(relid);
|
|
318
268
|
if (!cached) {
|
|
319
269
|
cached = this.query(
|
|
320
|
-
// NOT NULL: the column's constraint, or a NOT NULL domain anywhere in its domain chain.
|
|
321
270
|
`SELECT a.attnum, c.relkind,
|
|
322
271
|
a.attnotnull OR coalesce((
|
|
323
272
|
WITH RECURSIVE chain(oid, nn) AS (
|
|
@@ -335,7 +284,6 @@ export class ViewNullability {
|
|
|
335
284
|
return cached;
|
|
336
285
|
}
|
|
337
286
|
|
|
338
|
-
/** Is output column `attno` (1-based) of a (sub)query NOT NULL? */
|
|
339
287
|
private async queryColumn(
|
|
340
288
|
value: NodeValue | undefined,
|
|
341
289
|
attno: number,
|
|
@@ -343,8 +291,6 @@ export class ViewNullability {
|
|
|
343
291
|
const query = node(value);
|
|
344
292
|
if (!query) return false;
|
|
345
293
|
if (query["setOperations"]) {
|
|
346
|
-
// UNION / INTERSECT / EXCEPT: every arm must be NOT NULL. The range table of a set
|
|
347
|
-
// operation holds its arms, as subqueries, and nothing else.
|
|
348
294
|
for (const r of list(query["rtable"]))
|
|
349
295
|
if (!(await this.queryColumn(node(r)?.["subquery"], attno)))
|
|
350
296
|
return false;
|
|
@@ -358,17 +304,14 @@ export class ViewNullability {
|
|
|
358
304
|
}
|
|
359
305
|
|
|
360
306
|
private async variable(v: Node, query: Node): Promise<boolean> {
|
|
361
|
-
// No varnullingrels (PostgreSQL ≤ 15): outer joins are not visible, so nothing is proven.
|
|
362
307
|
if (!("varnullingrels" in v) || v["varlevelsup"] !== "0") return false;
|
|
363
|
-
if (list(v["varnullingrels"]).length > 1) return false;
|
|
364
|
-
// ROLLUP / CUBE / GROUPING SETS null the grouped columns (PostgreSQL ≤ 17 does not mark them).
|
|
308
|
+
if (list(v["varnullingrels"]).length > 1) return false;
|
|
365
309
|
if (list(query["groupingSets"]).length > 0) return false;
|
|
366
310
|
const rte = node(list(query["rtable"])[Number(v["varno"]) - 1]);
|
|
367
311
|
const attno = Number(v["varattno"]);
|
|
368
312
|
if (!rte || !(attno > 0)) return false;
|
|
369
313
|
switch (rte["rtekind"]) {
|
|
370
314
|
case "0": {
|
|
371
|
-
// relation
|
|
372
315
|
const relid = str(rte["relid"])!;
|
|
373
316
|
const att = (await this.columns(relid)).find((a) => a.attnum === attno);
|
|
374
317
|
if (!att) return false;
|
|
@@ -376,19 +319,18 @@ export class ViewNullability {
|
|
|
376
319
|
return (await this.notNull(relid))[attno - 1] ?? false;
|
|
377
320
|
return att.notnull;
|
|
378
321
|
}
|
|
379
|
-
case "1":
|
|
322
|
+
case "1":
|
|
380
323
|
return this.queryColumn(rte["subquery"], attno);
|
|
381
|
-
case "2":
|
|
324
|
+
case "2":
|
|
382
325
|
return this.expr(list(rte["joinaliasvars"])[attno - 1], query);
|
|
383
|
-
case "3":
|
|
326
|
+
case "3":
|
|
384
327
|
return this.functionColumn(rte, attno);
|
|
385
|
-
case "9":
|
|
328
|
+
case "9":
|
|
386
329
|
return this.expr(list(rte["groupexprs"])[attno - 1], query);
|
|
387
330
|
}
|
|
388
|
-
return false;
|
|
331
|
+
return false;
|
|
389
332
|
}
|
|
390
333
|
|
|
391
|
-
/** Column `attno` of a single set-returning function in FROM (`WITH ORDINALITY` adds a NOT NULL counter). */
|
|
392
334
|
private async functionColumn(rte: Node, attno: number): Promise<boolean> {
|
|
393
335
|
const functions = list(rte["functions"]);
|
|
394
336
|
const only = node(functions[0]);
|
|
@@ -432,10 +374,8 @@ export class ViewNullability {
|
|
|
432
374
|
case "FUNCEXPR": {
|
|
433
375
|
if (e["funcretset"] !== "false") return false;
|
|
434
376
|
const proc = (await this.loadProcs()).get(String(e["funcid"]));
|
|
435
|
-
// format_type(type, typmod) is not strict: a NULL typmod still gives a name.
|
|
436
377
|
if (proc?.system && proc.name === "format_type")
|
|
437
378
|
return this.expr(list(e["args"])[0], query);
|
|
438
|
-
// Explicit and implicit casts (funcformat 1, 2) keep a non-NULL input non-NULL.
|
|
439
379
|
const castLike = e["funcformat"] === "1" || e["funcformat"] === "2";
|
|
440
380
|
const known =
|
|
441
381
|
proc?.system === true &&
|
|
@@ -448,8 +388,6 @@ export class ViewNullability {
|
|
|
448
388
|
case "OPEXPR":
|
|
449
389
|
return (await this.strictOperator(e)) && this.all(e["args"], query);
|
|
450
390
|
case "SCALARARRAYOPEXPR": {
|
|
451
|
-
// `x = ANY (arr)` is NULL when there is no match and `arr` holds a NULL: only an
|
|
452
|
-
// `ARRAY[…]` of NOT NULL elements is safe.
|
|
453
391
|
const [left, right] = list(e["args"]);
|
|
454
392
|
const array = node(right);
|
|
455
393
|
return (
|
|
@@ -474,7 +412,7 @@ export class ViewNullability {
|
|
|
474
412
|
for (const w of list(e["args"]))
|
|
475
413
|
if (!(await this.expr(node(w)?.["result"], query))) return false;
|
|
476
414
|
return this.expr(e["defresult"], query);
|
|
477
|
-
case "SUBLINK":
|
|
415
|
+
case "SUBLINK":
|
|
478
416
|
return e["subLinkType"] === "0" || e["subLinkType"] === "6";
|
|
479
417
|
case "AGGREF": {
|
|
480
418
|
const proc = (await this.loadProcs()).get(String(e["aggfnoid"]));
|
package/src/type-map.ts
CHANGED
|
@@ -1,56 +1,9 @@
|
|
|
1
1
|
import type { DefaultParamTypeMap, DefaultTypeMap } from "@typesql/core";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* The object node-postgres returns for an `interval` column (package `postgres-interval` 1.x,
|
|
5
|
-
* wired up by `pg-types`). Fields are only present when non-zero; `toPostgres()` renders the
|
|
6
|
-
* value back to Postgres syntax and `toISO()`/`toISOString()` produce an ISO 8601 duration.
|
|
7
|
-
*
|
|
8
|
-
* Written inline in `PgTypeMap` (this alias only reads it back), so row types stay plain
|
|
9
|
-
* object types: declaration emit in a package without a direct `@typesql/pg` dependency does
|
|
10
|
-
* not need to name `PgInterval` (TS2883).
|
|
11
|
-
*/
|
|
12
3
|
export type PgInterval = PgTypeMap["interval"];
|
|
13
4
|
|
|
14
|
-
/**
|
|
15
|
-
* Canonical Postgres type name → TS type as `pg` 8.x returns it with its default type
|
|
16
|
-
* parsers (`pg-types` 2.x, text protocol). Verified against `pg-types/lib/textParsers.js`:
|
|
17
|
-
*
|
|
18
|
-
* number int2 (21), int4 (23), oid (26), float4 (700), float8 (701)
|
|
19
|
-
* string int8 (20) — `parseBigInteger` keeps the digits as a string, numeric (1700),
|
|
20
|
-
* money, text, varchar, bpchar, char, name, uuid, time, timetz, inet, cidr,
|
|
21
|
-
* macaddr, macaddr8, xml, bit, varbit, tsvector, tsquery, every range type,
|
|
22
|
-
* regclass/regtype/regproc, line/lseg/box/path/polygon — anything without a
|
|
23
|
-
* registered parser comes back as the raw text representation
|
|
24
|
-
* boolean bool (16)
|
|
25
|
-
* Date date (1082), timestamp (1114), timestamptz (1184)
|
|
26
|
-
* (`'infinity'`/`'-infinity'` come back as the numbers Infinity/-Infinity)
|
|
27
|
-
* unknown json (114), jsonb (3802): `JSON.parse`d
|
|
28
|
-
* Buffer bytea (17)
|
|
29
|
-
* object interval (1186) → PgInterval, point (600) → { x, y }, circle (718) → { x, y, radius }
|
|
30
|
-
*
|
|
31
|
-
* `DefaultTypeMap` in core already describes pg; this interface only narrows the entries
|
|
32
|
-
* that differ in detail (`Buffer` instead of `Uint8Array`, the interval object's methods,
|
|
33
|
-
* `record`/`void` as their raw text).
|
|
34
|
-
*
|
|
35
|
-
* Arrays: core maps `T[]` element-wise (`TsTypeOf` checks the `[]` suffix before the map),
|
|
36
|
-
* which matches pg for the array types that `pg-types` registers: bool[], bytea[], int2[],
|
|
37
|
-
* int4[], int8[] (string[]), oid[], float4[], float8[], point[], bpchar[], varchar[], text[],
|
|
38
|
-
* macaddr[], inet[], cidr[], date[], timestamp[], timestamptz[], interval[], json[], jsonb[],
|
|
39
|
-
* uuid[], money[], time[], timetz[], numrange[]. Two kinds of mismatch exist and cannot be
|
|
40
|
-
* expressed in this map:
|
|
41
|
-
* - `numeric[]` (1231) is parsed with `parseFloat` → `number[]` at runtime, while the
|
|
42
|
-
* element-wise type says `string[]`.
|
|
43
|
-
* - Array types without a registered parser come back as the raw `{...}` string, not an
|
|
44
|
-
* array: name[], "char"[], xml[], bit[], varbit[], tsvector[], tsquery[], circle[],
|
|
45
|
-
* line[], lseg[], box[], path[], polygon[], int4range[], int8range[], tsrange[],
|
|
46
|
-
* tstzrange[], daterange[], macaddr8[], regclass[]/regtype[]/regproc[], record[].
|
|
47
|
-
* Register your own parser (`pg.types.setTypeParser`) or cast in SQL if you depend on these.
|
|
48
|
-
* The two that `PgCatalog` columns use are declared here as the text pg returns: `aclitem[]`
|
|
49
|
-
* (`relacl`, `datacl`, ...) and `"char"[]` (`proargmodes`).
|
|
50
|
-
*/
|
|
51
5
|
export interface PgTypeMap
|
|
52
6
|
extends Omit<DefaultTypeMap, "bytea" | "interval" | "record" | "void"> {
|
|
53
|
-
/** What parameters accept on top of the row types. */
|
|
54
7
|
$params: PgParamTypeMap;
|
|
55
8
|
bytea: Buffer;
|
|
56
9
|
interval: {
|
|
@@ -65,28 +18,10 @@ export interface PgTypeMap
|
|
|
65
18
|
toISO(): string;
|
|
66
19
|
toISOString(): string;
|
|
67
20
|
};
|
|
68
|
-
/** Anonymous `ROW(...)` results have no parser: raw text such as `"(1,foo)"`. */
|
|
69
21
|
record: string;
|
|
70
|
-
/** `void`-returning functions (`pg_sleep`) yield an empty string. */
|
|
71
22
|
void: string;
|
|
72
|
-
/** No array parser: the raw `{...}` text, e.g. `"{postgres=arwdDxtm/postgres}"`. */
|
|
73
23
|
"aclitem[]": string;
|
|
74
|
-
/** No array parser for `"char"[]`: the raw `{...}` text, e.g. `"{i,o}"`. */
|
|
75
24
|
"char[]": string;
|
|
76
25
|
}
|
|
77
26
|
|
|
78
|
-
/**
|
|
79
|
-
* TS types `pg` parameters accept on top of the row type (`PgTypeMap["$params"]`). Verified
|
|
80
|
-
* against `pg/lib/utils.js` (`prepareValue`, 8.x): a `Date` is sent as a timestamp with the
|
|
81
|
-
* local offset, `Buffer`/typed arrays as bytea, a `bigint` or number with `toString()`, and
|
|
82
|
-
* objects with `toPostgres()` (such as `PgInterval`) through it; Postgres parses the text.
|
|
83
|
-
*
|
|
84
|
-
* int8, numeric, money number | bigint | string
|
|
85
|
-
* date, timestamp, timestamptz Date | string
|
|
86
|
-
* interval string, besides the `PgInterval` row type
|
|
87
|
-
* bytea Uint8Array, besides `Buffer`
|
|
88
|
-
*
|
|
89
|
-
* Plain objects are `JSON.stringify`d and arrays become Postgres array literals, so neither is
|
|
90
|
-
* listed for other types.
|
|
91
|
-
*/
|
|
92
27
|
export interface PgParamTypeMap extends DefaultParamTypeMap {}
|