@supabase/postgrest-typegen 0.5.0 → 0.7.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/README.md +36 -2
- package/dist/generation/typescript.js +4 -2
- package/dist/introspection/index.d.ts +6 -0
- package/dist/introspection/index.js +6 -3
- package/dist/introspection/node-tree.d.ts +18 -0
- package/dist/introspection/node-tree.js +100 -0
- package/dist/introspection/relationships.d.ts +15 -7
- package/dist/introspection/relationships.js +41 -19
- package/dist/introspection/sql/view_definitions.sql.d.ts +8 -0
- package/dist/introspection/sql/view_definitions.sql.js +43 -0
- package/dist/introspection/sql/views_key_dependencies.sql.d.ts +3 -1
- package/dist/introspection/sql/views_key_dependencies.sql.js +14 -2
- package/dist/introspection/view-nullability.d.ts +14 -0
- package/dist/introspection/view-nullability.js +167 -0
- package/dist/rpc.js +22 -13
- package/dist/types.js +6 -5
- package/package.json +4 -4
- package/src/generation/typescript.ts +5 -2
- package/src/introspection/index.ts +12 -2
- package/src/introspection/node-tree.ts +129 -0
- package/src/introspection/relationships.ts +84 -26
- package/src/introspection/sql/view_definitions.sql.ts +45 -0
- package/src/introspection/sql/views_key_dependencies.sql.ts +17 -3
- package/src/introspection/view-nullability.ts +243 -0
- package/src/rpc.ts +28 -20
- package/src/types.ts +6 -5
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { PostgresColumn } from "../types.ts";
|
|
2
|
+
import {
|
|
3
|
+
isNode,
|
|
4
|
+
type NodeTreeNode,
|
|
5
|
+
type NodeTreeValue,
|
|
6
|
+
parseNodeTree,
|
|
7
|
+
} from "./node-tree.ts";
|
|
8
|
+
|
|
9
|
+
/** A row of `VIEW_DEFINITIONS_SQL`. */
|
|
10
|
+
export interface ViewDefinitionRow {
|
|
11
|
+
view_id: number | string;
|
|
12
|
+
definition: string;
|
|
13
|
+
non_null_columns: [number | string, number | string][];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const RTE_RELATION = "0";
|
|
17
|
+
const RTE_SUBQUERY = "1";
|
|
18
|
+
const RTE_JOIN = "2";
|
|
19
|
+
const RTE_CTE = "6";
|
|
20
|
+
const RTE_GROUP = "9";
|
|
21
|
+
|
|
22
|
+
const JOIN_INNER = "0";
|
|
23
|
+
const JOIN_LEFT = "1";
|
|
24
|
+
const JOIN_RIGHT = "3";
|
|
25
|
+
|
|
26
|
+
const NOT_NULL_DIRECTIVE = /^\s*@notNull\s*$/m;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mark view and materialized view columns as not nullable when PostgreSQL can
|
|
30
|
+
* prove it from the view's query tree (see {@link inferNonNullViewColumns}) or
|
|
31
|
+
* when the column comment has an `@notNull` line. PostgreSQL never sets
|
|
32
|
+
* `attnotnull` on view columns, so without this every one of them is nullable.
|
|
33
|
+
*/
|
|
34
|
+
export function applyViewColumnNullability<T extends PostgresColumn>(
|
|
35
|
+
columns: T[],
|
|
36
|
+
rows: ViewDefinitionRow[],
|
|
37
|
+
): T[] {
|
|
38
|
+
const viewIds = new Set(rows.map((row) => String(row.view_id)));
|
|
39
|
+
const nonNull = inferNonNullViewColumns(rows);
|
|
40
|
+
return columns.map((column) =>
|
|
41
|
+
column.is_nullable &&
|
|
42
|
+
viewIds.has(String(column.table_id)) &&
|
|
43
|
+
(nonNull.has(column.id) || NOT_NULL_DIRECTIVE.test(column.comment ?? ""))
|
|
44
|
+
? { ...column, is_nullable: false }
|
|
45
|
+
: column,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The ids (`<view oid>.<attnum>`) of the view columns that can never be null.
|
|
51
|
+
* A column qualifies when it is a plain reference to a `NOT NULL` column, or
|
|
52
|
+
* to a view column that qualifies, followed through joins, subqueries,
|
|
53
|
+
* non-recursive CTEs and `GROUP BY`, and nothing on the way puts it on the
|
|
54
|
+
* nullable side of an outer join, in a set operation or under grouping sets.
|
|
55
|
+
* The outer-join check walks the join tree instead of reading
|
|
56
|
+
* `varnullingrels`, which PostgreSQL 15 does not have.
|
|
57
|
+
*/
|
|
58
|
+
function inferNonNullViewColumns(rows: ViewDefinitionRow[]): Set<string> {
|
|
59
|
+
const definitions = new Map(
|
|
60
|
+
rows.map((row) => [String(row.view_id), row.definition]),
|
|
61
|
+
);
|
|
62
|
+
const nonNullBaseColumns = new Set(
|
|
63
|
+
rows.flatMap((row) =>
|
|
64
|
+
row.non_null_columns.map(([relation, attnum]) => `${relation}.${attnum}`),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
const resolved = new Map<string, Set<number>>();
|
|
68
|
+
const nullableIndexesByQuery = new Map<NodeTreeNode, Set<number>>();
|
|
69
|
+
const nullableIndexesOf = (query: NodeTreeNode): Set<number> => {
|
|
70
|
+
let indexes = nullableIndexesByQuery.get(query);
|
|
71
|
+
if (indexes === undefined) {
|
|
72
|
+
indexes = nullableRangeTableIndexes(query);
|
|
73
|
+
nullableIndexesByQuery.set(query, indexes);
|
|
74
|
+
}
|
|
75
|
+
return indexes;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const nonNullColumnsOfView = (viewId: string): Set<number> => {
|
|
79
|
+
const cached = resolved.get(viewId);
|
|
80
|
+
if (cached !== undefined) {
|
|
81
|
+
return cached;
|
|
82
|
+
}
|
|
83
|
+
const columns = new Set<number>();
|
|
84
|
+
resolved.set(viewId, columns);
|
|
85
|
+
try {
|
|
86
|
+
const tree = parseNodeTree(definitions.get(viewId) ?? "");
|
|
87
|
+
const query = Array.isArray(tree) ? tree[0] : tree;
|
|
88
|
+
if (isNode(query, "QUERY") && allowsNonNull(query)) {
|
|
89
|
+
for (const entry of list(query.fields.targetList)) {
|
|
90
|
+
if (
|
|
91
|
+
isNode(entry, "TARGETENTRY") &&
|
|
92
|
+
entry.fields.resjunk !== "true" &&
|
|
93
|
+
isNonNullExpression(entry.fields.expr, [query])
|
|
94
|
+
) {
|
|
95
|
+
columns.add(Number(entry.fields.resno));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
columns.clear();
|
|
101
|
+
}
|
|
102
|
+
return columns;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const isNonNullColumn = (relation: string, attnum: number): boolean =>
|
|
106
|
+
definitions.has(relation)
|
|
107
|
+
? nonNullColumnsOfView(relation).has(attnum)
|
|
108
|
+
: nonNullBaseColumns.has(`${relation}.${attnum}`);
|
|
109
|
+
|
|
110
|
+
const isNonNullExpression = (
|
|
111
|
+
expression: NodeTreeValue | undefined,
|
|
112
|
+
queries: NodeTreeNode[],
|
|
113
|
+
): boolean => {
|
|
114
|
+
const query = queries.at(-1);
|
|
115
|
+
if (
|
|
116
|
+
query === undefined ||
|
|
117
|
+
!isNode(expression, "VAR") ||
|
|
118
|
+
expression.fields.varlevelsup !== "0"
|
|
119
|
+
) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const varno = Number(expression.fields.varno);
|
|
123
|
+
const attnum = Number(expression.fields.varattno);
|
|
124
|
+
if (attnum <= 0 || nullableIndexesOf(query).has(varno)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const entry = list(query.fields.rtable)[varno - 1];
|
|
128
|
+
if (!isNode(entry, "RANGETBLENTRY")) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
switch (entry.fields.rtekind) {
|
|
132
|
+
case RTE_RELATION:
|
|
133
|
+
return (
|
|
134
|
+
typeof entry.fields.relid === "string" &&
|
|
135
|
+
isNonNullColumn(entry.fields.relid, attnum)
|
|
136
|
+
);
|
|
137
|
+
case RTE_SUBQUERY:
|
|
138
|
+
return isNonNullOutput(entry.fields.subquery, attnum, queries);
|
|
139
|
+
case RTE_JOIN:
|
|
140
|
+
return isNonNullExpression(
|
|
141
|
+
list(entry.fields.joinaliasvars)[attnum - 1],
|
|
142
|
+
queries,
|
|
143
|
+
);
|
|
144
|
+
case RTE_GROUP:
|
|
145
|
+
return isNonNullExpression(
|
|
146
|
+
list(entry.fields.groupexprs)[attnum - 1],
|
|
147
|
+
queries,
|
|
148
|
+
);
|
|
149
|
+
case RTE_CTE: {
|
|
150
|
+
const levelsUp = Number(entry.fields.ctelevelsup);
|
|
151
|
+
const owner = queries.at(-1 - levelsUp);
|
|
152
|
+
const cte = list(owner?.fields.cteList).find(
|
|
153
|
+
(candidate) =>
|
|
154
|
+
isNode(candidate, "COMMONTABLEEXPR") &&
|
|
155
|
+
candidate.fields.ctename === entry.fields.ctename,
|
|
156
|
+
);
|
|
157
|
+
return (
|
|
158
|
+
isNode(cte) &&
|
|
159
|
+
cte.fields.cterecursive !== "true" &&
|
|
160
|
+
isNonNullOutput(
|
|
161
|
+
cte.fields.ctequery,
|
|
162
|
+
attnum,
|
|
163
|
+
queries.slice(0, queries.length - levelsUp),
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const isNonNullOutput = (
|
|
173
|
+
subquery: NodeTreeValue | undefined,
|
|
174
|
+
resno: number,
|
|
175
|
+
queries: NodeTreeNode[],
|
|
176
|
+
): boolean => {
|
|
177
|
+
if (!isNode(subquery, "QUERY") || !allowsNonNull(subquery)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const entry = list(subquery.fields.targetList).find(
|
|
181
|
+
(candidate) =>
|
|
182
|
+
isNode(candidate, "TARGETENTRY") &&
|
|
183
|
+
Number(candidate.fields.resno) === resno,
|
|
184
|
+
);
|
|
185
|
+
return (
|
|
186
|
+
isNode(entry) &&
|
|
187
|
+
isNonNullExpression(entry.fields.expr, [...queries, subquery])
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const result = new Set<string>();
|
|
192
|
+
for (const viewId of definitions.keys()) {
|
|
193
|
+
for (const attnum of nonNullColumnsOfView(viewId)) {
|
|
194
|
+
result.add(`${viewId}.${attnum}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function allowsNonNull(query: NodeTreeNode): boolean {
|
|
201
|
+
return (
|
|
202
|
+
query.fields.setOperations === null &&
|
|
203
|
+
list(query.fields.groupingSets).length === 0
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The range table indexes that sit on the nullable side of an outer join in
|
|
209
|
+
* the query's join tree, including the indexes of nested joins themselves.
|
|
210
|
+
*/
|
|
211
|
+
function nullableRangeTableIndexes(query: NodeTreeNode): Set<number> {
|
|
212
|
+
const nullable = new Set<number>();
|
|
213
|
+
const visit = (node: NodeTreeValue | undefined, isNullable: boolean) => {
|
|
214
|
+
if (isNode(node, "RANGETBLREF")) {
|
|
215
|
+
if (isNullable) {
|
|
216
|
+
nullable.add(Number(node.fields.rtindex));
|
|
217
|
+
}
|
|
218
|
+
} else if (isNode(node, "FROMEXPR")) {
|
|
219
|
+
for (const item of list(node.fields.fromlist)) {
|
|
220
|
+
visit(item, isNullable);
|
|
221
|
+
}
|
|
222
|
+
} else if (isNode(node, "JOINEXPR")) {
|
|
223
|
+
const joinType = node.fields.jointype;
|
|
224
|
+
if (isNullable) {
|
|
225
|
+
nullable.add(Number(node.fields.rtindex));
|
|
226
|
+
}
|
|
227
|
+
visit(
|
|
228
|
+
node.fields.larg,
|
|
229
|
+
isNullable || (joinType !== JOIN_INNER && joinType !== JOIN_LEFT),
|
|
230
|
+
);
|
|
231
|
+
visit(
|
|
232
|
+
node.fields.rarg,
|
|
233
|
+
isNullable || (joinType !== JOIN_INNER && joinType !== JOIN_RIGHT),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
visit(query.fields.jointree, false);
|
|
238
|
+
return nullable;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function list(value: NodeTreeValue | undefined): NodeTreeValue[] {
|
|
242
|
+
return Array.isArray(value) ? value : [];
|
|
243
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -35,6 +35,9 @@ type InputArg = PostgresFunction["args"][number] & {
|
|
|
35
35
|
|
|
36
36
|
type Candidate = { fn: PostgresFunction; inArgs: InputArg[] };
|
|
37
37
|
|
|
38
|
+
/** A NUL separator cannot occur in an identifier, so names with dots never collide. */
|
|
39
|
+
const qualifiedKey = (schema: string, name: string) => `${schema}\0${name}`;
|
|
40
|
+
|
|
38
41
|
export function resolveRpcFunctions(
|
|
39
42
|
metadata: GeneratorMetadata,
|
|
40
43
|
): RpcFunction[] {
|
|
@@ -48,13 +51,20 @@ export function resolveRpcFunctions(
|
|
|
48
51
|
)
|
|
49
52
|
.map((type) => [type.id, type]),
|
|
50
53
|
);
|
|
54
|
+
const relations = [
|
|
55
|
+
...metadata.tables,
|
|
56
|
+
...metadata.foreignTables,
|
|
57
|
+
...metadata.views,
|
|
58
|
+
...metadata.materializedViews,
|
|
59
|
+
];
|
|
51
60
|
const relationsById = new Map(
|
|
52
|
-
[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
relations.map((relation) => [relation.id, relation]),
|
|
62
|
+
);
|
|
63
|
+
const relationsBySchemaName = new Map(
|
|
64
|
+
relations.map((relation) => [
|
|
65
|
+
qualifiedKey(relation.schema, relation.name),
|
|
66
|
+
relation,
|
|
67
|
+
]),
|
|
58
68
|
);
|
|
59
69
|
const columnNamesByRelationId = new Map<number, Set<string>>();
|
|
60
70
|
for (const column of metadata.columns) {
|
|
@@ -96,14 +106,13 @@ export function resolveRpcFunctions(
|
|
|
96
106
|
// the rendered argument list may still name the relation.
|
|
97
107
|
const computedFieldOf = ({ fn, inArgs }: Candidate): number | null => {
|
|
98
108
|
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
99
|
-
|
|
109
|
+
if (!soleInArg) return null;
|
|
110
|
+
const argType = relationTypesById.get(soleInArg.type_id);
|
|
100
111
|
const relation = argType
|
|
101
112
|
? relationsById.get(argType.type_relation_id)
|
|
102
|
-
:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
relation.name === fn.argument_types,
|
|
106
|
-
);
|
|
113
|
+
: typesById.has(soleInArg.type_id)
|
|
114
|
+
? undefined
|
|
115
|
+
: relationsBySchemaName.get(qualifiedKey(fn.schema, fn.argument_types));
|
|
107
116
|
if (!relation || relation.schema !== fn.schema) return null;
|
|
108
117
|
if (columnNamesByRelationId.get(relation.id)?.has(fn.name)) return null;
|
|
109
118
|
return relation.id;
|
|
@@ -178,13 +187,14 @@ export function resolveRpcFunctions(
|
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
}
|
|
181
|
-
//
|
|
182
|
-
//
|
|
190
|
+
// PostgREST passes a sole unnamed parameter only when it is json, jsonb or
|
|
191
|
+
// text, so a function taking a row this way is a computed field or a
|
|
192
|
+
// computed relationship, reachable by embedding it from its relation and
|
|
193
|
+
// never through `/rpc`, whatever it returns.
|
|
183
194
|
if (
|
|
184
195
|
soleInArg &&
|
|
185
196
|
soleInArg.name === "" &&
|
|
186
|
-
relationTypesById.has(soleInArg.type_id)
|
|
187
|
-
returnRelationName(fn) === null
|
|
197
|
+
relationTypesById.has(soleInArg.type_id)
|
|
188
198
|
) {
|
|
189
199
|
return `the function ${fn.schema}.${fn.name} with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache`;
|
|
190
200
|
}
|
|
@@ -205,9 +215,7 @@ export function resolveRpcFunctions(
|
|
|
205
215
|
...typeReference(arg.type_id),
|
|
206
216
|
has_default: arg.has_default === true,
|
|
207
217
|
relation_id:
|
|
208
|
-
relationId !== undefined &&
|
|
209
|
-
relationId !== null &&
|
|
210
|
-
relationsById.has(relationId)
|
|
218
|
+
relationId !== undefined && relationsById.has(relationId)
|
|
211
219
|
? relationId
|
|
212
220
|
: null,
|
|
213
221
|
};
|
|
@@ -266,7 +274,7 @@ export function resolveRpcFunctions(
|
|
|
266
274
|
);
|
|
267
275
|
const candidate = { fn, inArgs };
|
|
268
276
|
if (!isCallable(candidate)) continue;
|
|
269
|
-
const key =
|
|
277
|
+
const key = qualifiedKey(fn.schema, fn.name);
|
|
270
278
|
let group = groups.get(key);
|
|
271
279
|
if (!group) {
|
|
272
280
|
group = [];
|
package/src/types.ts
CHANGED
|
@@ -205,8 +205,8 @@ const rpcFunctionReturnsSchema = rpcTypeReferenceSchema.merge({
|
|
|
205
205
|
*/
|
|
206
206
|
relation_id: "number | null",
|
|
207
207
|
/**
|
|
208
|
-
* The columns of a `RETURNS TABLE (…)`
|
|
209
|
-
*
|
|
208
|
+
* The columns of a `RETURNS TABLE (…)` result, in name order; `null` for
|
|
209
|
+
* every other return type, including a record built from `OUT` parameters.
|
|
210
210
|
*/
|
|
211
211
|
columns: rpcTypeReferenceSchema.merge({ name: "string" }).array().or("null"),
|
|
212
212
|
});
|
|
@@ -245,9 +245,10 @@ const rpcFunctionOverloadSchema = type({
|
|
|
245
245
|
computed_field_of: "number | null",
|
|
246
246
|
/**
|
|
247
247
|
* Why PostgREST cannot resolve a call to this overload, when it cannot: the
|
|
248
|
-
* function conflicts with another overload, or
|
|
249
|
-
*
|
|
250
|
-
*
|
|
248
|
+
* function conflicts with another overload, or its sole parameter is an
|
|
249
|
+
* unnamed row, which PostgREST never matches under `/rpc` (the function is
|
|
250
|
+
* a computed field or computed relationship, reached by embedding).
|
|
251
|
+
* Generators surface the message instead of a callable signature.
|
|
251
252
|
*/
|
|
252
253
|
error: "string | null",
|
|
253
254
|
});
|