@typesql/pg 0.1.5 → 0.2.1
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 +24 -0
- package/README.md +21 -59
- 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
|
@@ -10,25 +10,28 @@
|
|
|
10
10
|
* else (function calls outside a short list, scalar subqueries, set-returning function columns)
|
|
11
11
|
* is nullable. On PostgreSQL 15 and older every column reference counts as nullable.
|
|
12
12
|
*
|
|
13
|
-
* This module has no imports so that scripts/pg-catalog-from-pg.
|
|
13
|
+
* This module has no imports so that scripts/pg-catalog-from-pg.ts can load it with Node's
|
|
14
14
|
* type stripping.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/** Runs a query: node-postgres' `client.query(text, values)` fits. */
|
|
18
|
-
export type QueryFn = (
|
|
18
|
+
export type QueryFn = (
|
|
19
|
+
text: string,
|
|
20
|
+
values?: unknown[],
|
|
21
|
+
) => Promise<{ readonly rows: readonly any[] }>;
|
|
19
22
|
|
|
20
23
|
export interface NullabilityOptions {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
readonly notNull?: (view: string, column: string) => boolean;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/** A node of a parsed `pg_node_tree`: `{ _: "VAR", varno: "1", … }`. */
|
|
29
32
|
export interface Node {
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
readonly _: string;
|
|
34
|
+
readonly [field: string]: NodeValue;
|
|
32
35
|
}
|
|
33
36
|
export type NodeValue = Node | readonly NodeValue[] | string | null;
|
|
34
37
|
|
|
@@ -38,86 +41,155 @@ type Token = string | { readonly str: string };
|
|
|
38
41
|
|
|
39
42
|
/** Tokens of the text form of a node tree (`outfuncs.c`). `[ … ]` datum bytes are skipped. */
|
|
40
43
|
function tokenize(s: string): Token[] {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
const out: Token[] = [];
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < s.length) {
|
|
47
|
+
const c = s[i]!;
|
|
48
|
+
if (c === " " || c === "\n" || c === "\t") {
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (c === "(" || c === ")" || c === "{" || c === "}") {
|
|
53
|
+
out.push(c);
|
|
54
|
+
i++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (c === "[") {
|
|
58
|
+
i = s.indexOf("]", i) + 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (c === '"') {
|
|
62
|
+
let v = "";
|
|
63
|
+
i++;
|
|
64
|
+
while (i < s.length && s[i] !== '"') {
|
|
65
|
+
if (s[i] === "\\") i++;
|
|
66
|
+
v += s[i++];
|
|
67
|
+
}
|
|
68
|
+
i++;
|
|
69
|
+
out.push({ str: v });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let v = "";
|
|
73
|
+
while (i < s.length && !" \n\t(){}".includes(s[i]!)) {
|
|
74
|
+
if (s[i] === "\\") i++;
|
|
75
|
+
v += s[i++];
|
|
76
|
+
}
|
|
77
|
+
out.push(v);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
/** Parse a node tree into plain values: nodes as objects, lists as arrays, `<>` as null. */
|
|
64
83
|
export function parseNodeTree(text: string): NodeValue {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
const toks = tokenize(text);
|
|
85
|
+
let p = 0;
|
|
86
|
+
const isLabel = (t: Token | undefined): boolean =>
|
|
87
|
+
typeof t === "string" && t.startsWith(":");
|
|
88
|
+
const value = (): NodeValue => {
|
|
89
|
+
const t = toks[p++];
|
|
90
|
+
if (t === "{") {
|
|
91
|
+
const node: Record<string, NodeValue> = { _: toks[p++] as string };
|
|
92
|
+
while (p < toks.length && toks[p] !== "}") {
|
|
93
|
+
const label = toks[p++];
|
|
94
|
+
if (!isLabel(label)) continue; // a datum's length before its skipped bytes
|
|
95
|
+
if (toks[p] === "}" || isLabel(toks[p])) {
|
|
96
|
+
node[(label as string).slice(1)] = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
node[(label as string).slice(1)] = value();
|
|
100
|
+
}
|
|
101
|
+
p++;
|
|
102
|
+
return node as Node;
|
|
103
|
+
}
|
|
104
|
+
if (t === "(") {
|
|
105
|
+
const list: NodeValue[] = [];
|
|
106
|
+
while (p < toks.length && toks[p] !== ")") list.push(value());
|
|
107
|
+
p++;
|
|
108
|
+
return list;
|
|
109
|
+
}
|
|
110
|
+
if (t === undefined || t === "<>") return null;
|
|
111
|
+
return typeof t === "object" ? t.str : t;
|
|
112
|
+
};
|
|
113
|
+
return value();
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
const list = (x: NodeValue | undefined): readonly NodeValue[] =>
|
|
94
|
-
|
|
95
|
-
const
|
|
116
|
+
const list = (x: NodeValue | undefined): readonly NodeValue[] =>
|
|
117
|
+
Array.isArray(x) ? x : [];
|
|
118
|
+
const node = (x: NodeValue | undefined): Node | null =>
|
|
119
|
+
x !== null && typeof x === "object" && !Array.isArray(x) ? (x as Node) : null;
|
|
120
|
+
const str = (x: NodeValue | undefined): string | null =>
|
|
121
|
+
typeof x === "string" ? x : null;
|
|
96
122
|
|
|
97
123
|
// ───────────────────────────── function knowledge ─────────────────────────────
|
|
98
124
|
|
|
99
125
|
/** `pg_catalog` functions that return a non-NULL value for non-NULL arguments. */
|
|
100
126
|
const NON_NULL_FUNCTIONS = new Set([
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
"current_database",
|
|
128
|
+
"current_user",
|
|
129
|
+
"session_user",
|
|
130
|
+
"current_schema",
|
|
131
|
+
"pg_get_userbyid",
|
|
132
|
+
"pg_my_temp_schema",
|
|
133
|
+
"nameconcatoid",
|
|
134
|
+
"pg_has_role",
|
|
135
|
+
"has_table_privilege",
|
|
136
|
+
"has_column_privilege",
|
|
137
|
+
"has_any_column_privilege",
|
|
138
|
+
"has_function_privilege",
|
|
139
|
+
"has_schema_privilege",
|
|
140
|
+
"has_sequence_privilege",
|
|
141
|
+
"has_type_privilege",
|
|
142
|
+
"pg_relation_is_updatable",
|
|
143
|
+
"pg_column_is_updatable",
|
|
144
|
+
"pg_is_other_temp_schema",
|
|
145
|
+
"now",
|
|
146
|
+
"statement_timestamp",
|
|
147
|
+
"transaction_timestamp",
|
|
148
|
+
"clock_timestamp",
|
|
149
|
+
"lower",
|
|
150
|
+
"upper",
|
|
151
|
+
"length",
|
|
152
|
+
"char_length",
|
|
153
|
+
"octet_length",
|
|
154
|
+
"concat",
|
|
155
|
+
"concat_ws",
|
|
156
|
+
"md5",
|
|
157
|
+
"btrim",
|
|
158
|
+
"ltrim",
|
|
159
|
+
"rtrim",
|
|
160
|
+
"abs",
|
|
161
|
+
"round",
|
|
162
|
+
"trunc",
|
|
163
|
+
"floor",
|
|
164
|
+
"ceil",
|
|
165
|
+
"ceiling",
|
|
166
|
+
"date_trunc",
|
|
167
|
+
"age",
|
|
108
168
|
]);
|
|
109
169
|
|
|
110
170
|
/** `pg_stat_get_*` accessors returning a number report 0 when no statistics exist, except these. */
|
|
111
171
|
const NULLABLE_STAT_FUNCTIONS = new Set([
|
|
112
|
-
|
|
172
|
+
"pg_stat_get_db_checksum_failures",
|
|
173
|
+
"pg_stat_get_backend_pid",
|
|
174
|
+
"pg_stat_get_backend_dbid",
|
|
175
|
+
"pg_stat_get_backend_userid",
|
|
113
176
|
]);
|
|
114
177
|
const isNonNullStatFunction = (name: string, ret: string): boolean =>
|
|
115
|
-
|
|
178
|
+
name.startsWith("pg_stat_get_") &&
|
|
179
|
+
(ret === "int8" || ret === "int4" || ret === "float8") &&
|
|
180
|
+
!NULLABLE_STAT_FUNCTIONS.has(name);
|
|
116
181
|
|
|
117
182
|
/** Aggregates that never return NULL. */
|
|
118
183
|
const NON_NULL_AGGREGATES = new Set(["count"]);
|
|
119
184
|
/** Window functions that never return NULL. */
|
|
120
|
-
const NON_NULL_WINDOW_FUNCTIONS = new Set([
|
|
185
|
+
const NON_NULL_WINDOW_FUNCTIONS = new Set([
|
|
186
|
+
"count",
|
|
187
|
+
"row_number",
|
|
188
|
+
"rank",
|
|
189
|
+
"dense_rank",
|
|
190
|
+
"percent_rank",
|
|
191
|
+
"cume_dist",
|
|
192
|
+
]);
|
|
121
193
|
|
|
122
194
|
/**
|
|
123
195
|
* Set-returning functions in FROM and their output columns (1-based) that are never NULL. A
|
|
@@ -126,10 +198,18 @@ const NON_NULL_WINDOW_FUNCTIONS = new Set(["count", "row_number", "rank", "dense
|
|
|
126
198
|
* the `_text` variants turn a JSON null into SQL NULL.
|
|
127
199
|
*/
|
|
128
200
|
const NON_NULL_SRF_COLUMNS: Readonly<Record<string, readonly number[]>> = {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
201
|
+
generate_series: [1],
|
|
202
|
+
generate_subscripts: [1],
|
|
203
|
+
jsonb_each: [1, 2],
|
|
204
|
+
json_each: [1, 2],
|
|
205
|
+
jsonb_each_text: [1],
|
|
206
|
+
json_each_text: [1],
|
|
207
|
+
jsonb_object_keys: [1],
|
|
208
|
+
json_object_keys: [1],
|
|
209
|
+
jsonb_array_elements: [1],
|
|
210
|
+
json_array_elements: [1],
|
|
211
|
+
regexp_matches: [1],
|
|
212
|
+
regexp_split_to_table: [1],
|
|
133
213
|
};
|
|
134
214
|
|
|
135
215
|
/**
|
|
@@ -139,84 +219,106 @@ const NON_NULL_SRF_COLUMNS: Readonly<Record<string, readonly number[]>> = {
|
|
|
139
219
|
*/
|
|
140
220
|
const NULLABLE_OPERATORS = new Set(["->", "->>", "#>", "#>>", "@@", "@?"]);
|
|
141
221
|
|
|
142
|
-
interface Proc {
|
|
143
|
-
|
|
222
|
+
interface Proc {
|
|
223
|
+
readonly name: string;
|
|
224
|
+
readonly ret: string;
|
|
225
|
+
readonly system: boolean;
|
|
226
|
+
}
|
|
227
|
+
interface Att {
|
|
228
|
+
readonly attnum: number;
|
|
229
|
+
readonly notnull: boolean;
|
|
230
|
+
readonly relkind: string;
|
|
231
|
+
}
|
|
144
232
|
|
|
145
233
|
// ───────────────────────────── analysis ─────────────────────────────
|
|
146
234
|
|
|
147
235
|
/** Per-column NOT NULL of views and materialized views; caches what it reads. */
|
|
148
236
|
export class ViewNullability {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
237
|
+
private readonly query: QueryFn;
|
|
238
|
+
private readonly options: NullabilityOptions;
|
|
239
|
+
private readonly views = new Map<string, Promise<boolean[]>>();
|
|
240
|
+
private readonly analyzing = new Set<string>();
|
|
241
|
+
private readonly atts = new Map<string, Promise<Att[]>>();
|
|
242
|
+
private procs: Promise<Map<string, Proc>> | null = null;
|
|
243
|
+
private operators: Promise<Map<string, Proc>> | null = null;
|
|
155
244
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
245
|
+
constructor(query: QueryFn, options: NullabilityOptions = {}) {
|
|
246
|
+
this.query = query;
|
|
247
|
+
this.options = options;
|
|
248
|
+
}
|
|
160
249
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
250
|
+
/** NOT NULL flags of the view's columns, in column order. `relid` is the view's `pg_class.oid`. */
|
|
251
|
+
notNull(relid: string | number): Promise<boolean[]> {
|
|
252
|
+
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
|
+
if (this.analyzing.has(key)) return Promise.resolve([]);
|
|
256
|
+
let cached = this.views.get(key);
|
|
257
|
+
if (!cached) {
|
|
258
|
+
this.analyzing.add(key);
|
|
259
|
+
cached = this.analyze(key).finally(() => this.analyzing.delete(key));
|
|
260
|
+
this.views.set(key, cached);
|
|
261
|
+
}
|
|
262
|
+
return cached;
|
|
263
|
+
}
|
|
171
264
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
265
|
+
private async analyze(relid: string): Promise<boolean[]> {
|
|
266
|
+
const { rows } = await this.query(
|
|
267
|
+
`SELECT r.ev_action, c.relname FROM pg_rewrite r JOIN pg_class c ON c.oid = r.ev_class
|
|
175
268
|
WHERE r.ev_class = $1 AND r.rulename = '_RETURN'`,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
269
|
+
[relid],
|
|
270
|
+
);
|
|
271
|
+
const row = rows[0] as { ev_action: string; relname: string } | undefined;
|
|
272
|
+
if (!row) return [];
|
|
273
|
+
const query = node(list(parseNodeTree(row.ev_action))[0]);
|
|
274
|
+
if (!query) return [];
|
|
275
|
+
const out: boolean[] = [];
|
|
276
|
+
for (const te of list(query["targetList"])) {
|
|
277
|
+
const entry = node(te);
|
|
278
|
+
if (!entry || entry["resjunk"] === "true") continue;
|
|
279
|
+
const name = str(entry["resname"]) ?? "";
|
|
280
|
+
out.push(
|
|
281
|
+
(await this.expr(entry["expr"], query)) ||
|
|
282
|
+
(this.options.notNull?.(row.relname, name) ?? false),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
191
287
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
288
|
+
private loadProcs(): Promise<Map<string, Proc>> {
|
|
289
|
+
this.procs ??= this.query(
|
|
290
|
+
`SELECT p.oid::int8::text AS oid, p.proname AS name, t.typname AS ret, p.pronamespace = 'pg_catalog'::regnamespace AS system
|
|
195
291
|
FROM pg_proc p JOIN pg_type t ON t.oid = p.prorettype`,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
292
|
+
).then(
|
|
293
|
+
({ rows }) =>
|
|
294
|
+
new Map((rows as (Proc & { oid: string })[]).map((r) => [r.oid, r])),
|
|
295
|
+
);
|
|
296
|
+
return this.procs;
|
|
297
|
+
}
|
|
199
298
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
299
|
+
private loadOperators(): Promise<Map<string, Proc>> {
|
|
300
|
+
this.operators ??= this.query(
|
|
301
|
+
`SELECT o.oid::int8::text AS oid, o.oprname AS name, '' AS ret, o.oprnamespace = 'pg_catalog'::regnamespace AS system
|
|
203
302
|
FROM pg_operator o`,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
303
|
+
).then(
|
|
304
|
+
({ rows }) =>
|
|
305
|
+
new Map((rows as (Proc & { oid: string })[]).map((r) => [r.oid, r])),
|
|
306
|
+
);
|
|
307
|
+
return this.operators;
|
|
308
|
+
}
|
|
207
309
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
310
|
+
/** A built-in operator that returns a value for non-NULL operands. */
|
|
311
|
+
private async strictOperator(e: Node): Promise<boolean> {
|
|
312
|
+
const op = (await this.loadOperators()).get(str(e["opno"]) ?? "");
|
|
313
|
+
return op?.system === true && !NULLABLE_OPERATORS.has(op.name);
|
|
314
|
+
}
|
|
213
315
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
316
|
+
private columns(relid: string): Promise<Att[]> {
|
|
317
|
+
let cached = this.atts.get(relid);
|
|
318
|
+
if (!cached) {
|
|
319
|
+
cached = this.query(
|
|
320
|
+
// NOT NULL: the column's constraint, or a NOT NULL domain anywhere in its domain chain.
|
|
321
|
+
`SELECT a.attnum, c.relkind,
|
|
220
322
|
a.attnotnull OR coalesce((
|
|
221
323
|
WITH RECURSIVE chain(oid, nn) AS (
|
|
222
324
|
SELECT t.oid, t.typnotnull FROM pg_type t WHERE t.oid = a.atttypid AND t.typtype = 'd'
|
|
@@ -226,131 +328,171 @@ export class ViewNullability {
|
|
|
226
328
|
SELECT bool_or(nn) FROM chain), false) AS notnull
|
|
227
329
|
FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid
|
|
228
330
|
WHERE a.attrelid = $1 AND a.attnum > 0 AND NOT a.attisdropped`,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
331
|
+
[relid],
|
|
332
|
+
).then(({ rows }) => rows as Att[]);
|
|
333
|
+
this.atts.set(relid, cached);
|
|
334
|
+
}
|
|
335
|
+
return cached;
|
|
336
|
+
}
|
|
235
337
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
338
|
+
/** Is output column `attno` (1-based) of a (sub)query NOT NULL? */
|
|
339
|
+
private async queryColumn(query: Node, attno: number): Promise<boolean> {
|
|
340
|
+
if (query["setOperations"]) {
|
|
341
|
+
// UNION / INTERSECT / EXCEPT: every arm (a subquery in the range table) must be NOT NULL.
|
|
342
|
+
for (const r of list(query["rtable"])) {
|
|
343
|
+
const rte = node(r);
|
|
344
|
+
const sub = node(rte?.["subquery"]);
|
|
345
|
+
if (
|
|
346
|
+
rte?.["rtekind"] === "1" &&
|
|
347
|
+
sub &&
|
|
348
|
+
!(await this.queryColumn(sub, attno))
|
|
349
|
+
)
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
const entries = list(query["targetList"])
|
|
355
|
+
.map(node)
|
|
356
|
+
.filter((t): t is Node => t !== null && t["resjunk"] !== "true");
|
|
357
|
+
const te = entries[attno - 1];
|
|
358
|
+
return te ? this.expr(te["expr"], query) : false;
|
|
359
|
+
}
|
|
251
360
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
361
|
+
private async variable(v: Node, query: Node): Promise<boolean> {
|
|
362
|
+
// No varnullingrels (PostgreSQL ≤ 15): outer joins are not visible, so nothing is proven.
|
|
363
|
+
if (!("varnullingrels" in v) || v["varlevelsup"] !== "0") return false;
|
|
364
|
+
if (list(v["varnullingrels"]).length > 1) return false; // `(b 3)`: an outer join can null it
|
|
365
|
+
// ROLLUP / CUBE / GROUPING SETS null the grouped columns (PostgreSQL ≤ 17 does not mark them).
|
|
366
|
+
if (list(query["groupingSets"]).length > 0) return false;
|
|
367
|
+
const rte = node(list(query["rtable"])[Number(v["varno"]) - 1]);
|
|
368
|
+
const attno = Number(v["varattno"]);
|
|
369
|
+
if (!rte || !(attno > 0)) return false;
|
|
370
|
+
switch (rte["rtekind"]) {
|
|
371
|
+
case "0": {
|
|
372
|
+
// relation
|
|
373
|
+
const relid = str(rte["relid"])!;
|
|
374
|
+
const att = (await this.columns(relid)).find((a) => a.attnum === attno);
|
|
375
|
+
if (!att) return false;
|
|
376
|
+
if (att.relkind === "v" || att.relkind === "m")
|
|
377
|
+
return (await this.notNull(relid))[attno - 1] ?? false;
|
|
378
|
+
return att.notnull;
|
|
379
|
+
}
|
|
380
|
+
case "1": {
|
|
381
|
+
// subquery
|
|
382
|
+
const sub = node(rte["subquery"]);
|
|
383
|
+
return sub ? this.queryColumn(sub, attno) : false;
|
|
384
|
+
}
|
|
385
|
+
case "2": // join alias variable
|
|
386
|
+
return this.expr(list(rte["joinaliasvars"])[attno - 1] ?? null, query);
|
|
387
|
+
case "3": // function in FROM
|
|
388
|
+
return this.functionColumn(rte, attno);
|
|
389
|
+
case "9": // PostgreSQL 18 GROUP BY expressions
|
|
390
|
+
return this.expr(list(rte["groupexprs"])[attno - 1] ?? null, query);
|
|
391
|
+
default:
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
283
395
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
396
|
+
/** Column `attno` of a single set-returning function in FROM (`WITH ORDINALITY` adds a NOT NULL counter). */
|
|
397
|
+
private async functionColumn(rte: Node, attno: number): Promise<boolean> {
|
|
398
|
+
const functions = list(rte["functions"]);
|
|
399
|
+
const only = node(functions[0]);
|
|
400
|
+
if (functions.length !== 1 || !only) return false;
|
|
401
|
+
const count = Number(str(only["funccolcount"]));
|
|
402
|
+
if (rte["funcordinality"] === "true" && attno === count + 1) return true;
|
|
403
|
+
const call = node(only["funcexpr"]);
|
|
404
|
+
if (call?._ !== "FUNCEXPR") return false;
|
|
405
|
+
const proc = (await this.loadProcs()).get(str(call["funcid"]) ?? "");
|
|
406
|
+
return (
|
|
407
|
+
proc?.system === true &&
|
|
408
|
+
(NON_NULL_SRF_COLUMNS[proc.name]?.includes(attno) ?? false)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
296
411
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
412
|
+
private async all(
|
|
413
|
+
args: NodeValue | undefined,
|
|
414
|
+
query: Node,
|
|
415
|
+
): Promise<boolean> {
|
|
416
|
+
for (const a of list(args)) if (!(await this.expr(a, query))) return false;
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
301
419
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
420
|
+
private async expr(
|
|
421
|
+
value: NodeValue | undefined,
|
|
422
|
+
query: Node,
|
|
423
|
+
): Promise<boolean> {
|
|
424
|
+
const e = node(value);
|
|
425
|
+
if (!e) return false;
|
|
426
|
+
switch (e._) {
|
|
427
|
+
case "VAR":
|
|
428
|
+
return this.variable(e, query);
|
|
429
|
+
case "CONST":
|
|
430
|
+
return e["constisnull"] === "false";
|
|
431
|
+
case "RELABELTYPE":
|
|
432
|
+
case "COERCEVIAIO":
|
|
433
|
+
case "COERCETODOMAIN":
|
|
434
|
+
case "ARRAYCOERCEEXPR":
|
|
435
|
+
case "COLLATEEXPR":
|
|
436
|
+
return this.expr(e["arg"], query);
|
|
437
|
+
case "FUNCEXPR": {
|
|
438
|
+
if (e["funcretset"] !== "false") return false;
|
|
439
|
+
const proc = (await this.loadProcs()).get(str(e["funcid"]) ?? "");
|
|
440
|
+
// format_type(type, typmod) is not strict: a NULL typmod still gives a name.
|
|
441
|
+
if (proc?.system && proc.name === "format_type")
|
|
442
|
+
return this.expr(list(e["args"])[0] ?? null, query);
|
|
443
|
+
// Explicit and implicit casts (funcformat 1, 2) keep a non-NULL input non-NULL.
|
|
444
|
+
const castLike = e["funcformat"] === "1" || e["funcformat"] === "2";
|
|
445
|
+
const known =
|
|
446
|
+
proc?.system === true &&
|
|
447
|
+
(NON_NULL_FUNCTIONS.has(proc.name) ||
|
|
448
|
+
isNonNullStatFunction(proc.name, proc.ret));
|
|
449
|
+
return (castLike || known) && this.all(e["args"], query);
|
|
450
|
+
}
|
|
451
|
+
case "BOOLEXPR":
|
|
452
|
+
return this.all(e["args"], query);
|
|
453
|
+
case "OPEXPR":
|
|
454
|
+
return (await this.strictOperator(e)) && this.all(e["args"], query);
|
|
455
|
+
case "SCALARARRAYOPEXPR": {
|
|
456
|
+
// `x = ANY (arr)` is NULL when there is no match and `arr` holds a NULL: only an
|
|
457
|
+
// `ARRAY[…]` of NOT NULL elements is safe.
|
|
458
|
+
const [left, right] = list(e["args"]);
|
|
459
|
+
const array = node(right);
|
|
460
|
+
return (
|
|
461
|
+
(await this.strictOperator(e)) &&
|
|
462
|
+
array?._ === "ARRAYEXPR" &&
|
|
463
|
+
(await this.expr(left ?? null, query)) &&
|
|
464
|
+
this.all(array["elements"], query)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
case "DISTINCTEXPR":
|
|
468
|
+
case "NULLTEST":
|
|
469
|
+
case "BOOLEANTEST":
|
|
470
|
+
case "ARRAYEXPR":
|
|
471
|
+
case "ROW":
|
|
472
|
+
return true;
|
|
473
|
+
case "COALESCEEXPR":
|
|
474
|
+
case "MINMAXEXPR":
|
|
475
|
+
for (const a of list(e["args"]))
|
|
476
|
+
if (await this.expr(a, query)) return true;
|
|
477
|
+
return false;
|
|
478
|
+
case "CASEEXPR":
|
|
479
|
+
for (const w of list(e["args"]))
|
|
480
|
+
if (!(await this.expr(node(w)?.["result"], query))) return false;
|
|
481
|
+
return this.expr(e["defresult"], query);
|
|
482
|
+
case "SUBLINK": // EXISTS (0) and ARRAY (6) sublinks are never NULL
|
|
483
|
+
return e["subLinkType"] === "0" || e["subLinkType"] === "6";
|
|
484
|
+
case "AGGREF": {
|
|
485
|
+
const proc = (await this.loadProcs()).get(str(e["aggfnoid"]) ?? "");
|
|
486
|
+
return proc?.system === true && NON_NULL_AGGREGATES.has(proc.name);
|
|
487
|
+
}
|
|
488
|
+
case "WINDOWFUNC": {
|
|
489
|
+
const proc = (await this.loadProcs()).get(str(e["winfnoid"]) ?? "");
|
|
490
|
+
return (
|
|
491
|
+
proc?.system === true && NON_NULL_WINDOW_FUNCTIONS.has(proc.name)
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
default:
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
356
498
|
}
|