@supabase/postgrest-typegen 0.4.0 → 0.6.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 +154 -21
- package/dist/generation/index.d.ts +0 -1
- package/dist/generation/index.js +0 -1
- package/dist/generation/typescript.d.ts +2 -1
- package/dist/generation/typescript.js +72 -288
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/introspection/index.js +8 -3
- package/dist/introspection/node-tree.d.ts +18 -0
- package/dist/introspection/node-tree.js +100 -0
- package/dist/introspection/sql/view_definitions.sql.d.ts +8 -0
- package/dist/introspection/sql/view_definitions.sql.js +43 -0
- package/dist/introspection/view-nullability.d.ts +14 -0
- package/dist/introspection/view-nullability.js +167 -0
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.js +232 -0
- package/dist/sort.js +23 -0
- package/dist/types.d.ts +151 -0
- package/dist/types.js +92 -0
- package/package.json +5 -4
- package/src/generation/index.ts +0 -5
- package/src/generation/typescript.ts +102 -383
- package/src/index.ts +1 -0
- package/src/introspection/index.ts +8 -2
- package/src/introspection/node-tree.ts +129 -0
- package/src/introspection/sql/view_definitions.sql.ts +45 -0
- package/src/introspection/view-nullability.ts +243 -0
- package/src/rpc.ts +307 -0
- package/src/sort.ts +31 -0
- package/src/types.ts +102 -0
- package/dist/generation/constants.d.ts +0 -9
- package/dist/generation/constants.js +0 -10
- package/dist/generation/swift.d.ts +0 -11
- package/dist/generation/swift.js +0 -358
- package/src/generation/constants.ts +0 -11
- package/src/generation/swift.ts +0 -531
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SchemaFilterProps } from "./common.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The query tree of every view and materialized view in the filtered schemas,
|
|
4
|
+
* plus every view they read from in any schema, with the `NOT NULL` columns
|
|
5
|
+
* each one references. `inferNonNullViewColumns` resolves view column
|
|
6
|
+
* nullability from it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VIEW_DEFINITIONS_SQL: (props: SchemaFilterProps) => string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The query tree of every view and materialized view in the filtered schemas,
|
|
3
|
+
* plus every view they read from in any schema, with the `NOT NULL` columns
|
|
4
|
+
* each one references. `inferNonNullViewColumns` resolves view column
|
|
5
|
+
* nullability from it.
|
|
6
|
+
*/
|
|
7
|
+
export const VIEW_DEFINITIONS_SQL = (props) => /* SQL */ `
|
|
8
|
+
WITH RECURSIVE view_closure(oid) AS (
|
|
9
|
+
SELECT c.oid
|
|
10
|
+
FROM pg_class c
|
|
11
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
12
|
+
WHERE
|
|
13
|
+
${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
|
|
14
|
+
c.relkind IN ('v', 'm')
|
|
15
|
+
UNION
|
|
16
|
+
SELECT dep.refobjid
|
|
17
|
+
FROM view_closure vc
|
|
18
|
+
JOIN pg_rewrite r ON r.ev_class = vc.oid AND r.rulename = '_RETURN'
|
|
19
|
+
JOIN pg_depend dep ON dep.classid = 'pg_rewrite' :: regclass
|
|
20
|
+
AND dep.objid = r.oid
|
|
21
|
+
AND dep.refclassid = 'pg_class' :: regclass
|
|
22
|
+
JOIN pg_class rc ON rc.oid = dep.refobjid AND rc.relkind IN ('v', 'm')
|
|
23
|
+
)
|
|
24
|
+
SELECT
|
|
25
|
+
r.ev_class :: int8 AS view_id,
|
|
26
|
+
r.ev_action :: text AS definition,
|
|
27
|
+
COALESCE(
|
|
28
|
+
(
|
|
29
|
+
SELECT json_agg(json_build_array(dep.refobjid :: int8, dep.refobjsubid))
|
|
30
|
+
FROM pg_depend dep
|
|
31
|
+
JOIN pg_attribute a ON a.attrelid = dep.refobjid AND a.attnum = dep.refobjsubid
|
|
32
|
+
JOIN pg_type t ON t.oid = a.atttypid
|
|
33
|
+
WHERE dep.classid = 'pg_rewrite' :: regclass
|
|
34
|
+
AND dep.objid = r.oid
|
|
35
|
+
AND dep.refclassid = 'pg_class' :: regclass
|
|
36
|
+
AND dep.refobjsubid > 0
|
|
37
|
+
AND (a.attnotnull OR t.typtype = 'd' AND t.typnotnull)
|
|
38
|
+
),
|
|
39
|
+
'[]'
|
|
40
|
+
) AS non_null_columns
|
|
41
|
+
FROM view_closure vc
|
|
42
|
+
JOIN pg_rewrite r ON r.ev_class = vc.oid AND r.rulename = '_RETURN'
|
|
43
|
+
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PostgresColumn } from "../types.ts";
|
|
2
|
+
/** A row of `VIEW_DEFINITIONS_SQL`. */
|
|
3
|
+
export interface ViewDefinitionRow {
|
|
4
|
+
view_id: number | string;
|
|
5
|
+
definition: string;
|
|
6
|
+
non_null_columns: [number | string, number | string][];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Mark view and materialized view columns as not nullable when PostgreSQL can
|
|
10
|
+
* prove it from the view's query tree (see {@link inferNonNullViewColumns}) or
|
|
11
|
+
* when the column comment has an `@notNull` line. PostgreSQL never sets
|
|
12
|
+
* `attnotnull` on view columns, so without this every one of them is nullable.
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyViewColumnNullability<T extends PostgresColumn>(columns: T[], rows: ViewDefinitionRow[]): T[];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { isNode, parseNodeTree, } from "./node-tree.js";
|
|
2
|
+
const RTE_RELATION = "0";
|
|
3
|
+
const RTE_SUBQUERY = "1";
|
|
4
|
+
const RTE_JOIN = "2";
|
|
5
|
+
const RTE_CTE = "6";
|
|
6
|
+
const RTE_GROUP = "9";
|
|
7
|
+
const JOIN_INNER = "0";
|
|
8
|
+
const JOIN_LEFT = "1";
|
|
9
|
+
const JOIN_RIGHT = "3";
|
|
10
|
+
const NOT_NULL_DIRECTIVE = /^\s*@notNull\s*$/m;
|
|
11
|
+
/**
|
|
12
|
+
* Mark view and materialized view columns as not nullable when PostgreSQL can
|
|
13
|
+
* prove it from the view's query tree (see {@link inferNonNullViewColumns}) or
|
|
14
|
+
* when the column comment has an `@notNull` line. PostgreSQL never sets
|
|
15
|
+
* `attnotnull` on view columns, so without this every one of them is nullable.
|
|
16
|
+
*/
|
|
17
|
+
export function applyViewColumnNullability(columns, rows) {
|
|
18
|
+
const viewIds = new Set(rows.map((row) => String(row.view_id)));
|
|
19
|
+
const nonNull = inferNonNullViewColumns(rows);
|
|
20
|
+
return columns.map((column) => column.is_nullable &&
|
|
21
|
+
viewIds.has(String(column.table_id)) &&
|
|
22
|
+
(nonNull.has(column.id) || NOT_NULL_DIRECTIVE.test(column.comment ?? ""))
|
|
23
|
+
? { ...column, is_nullable: false }
|
|
24
|
+
: column);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The ids (`<view oid>.<attnum>`) of the view columns that can never be null.
|
|
28
|
+
* A column qualifies when it is a plain reference to a `NOT NULL` column, or
|
|
29
|
+
* to a view column that qualifies, followed through joins, subqueries,
|
|
30
|
+
* non-recursive CTEs and `GROUP BY`, and nothing on the way puts it on the
|
|
31
|
+
* nullable side of an outer join, in a set operation or under grouping sets.
|
|
32
|
+
* The outer-join check walks the join tree instead of reading
|
|
33
|
+
* `varnullingrels`, which PostgreSQL 15 does not have.
|
|
34
|
+
*/
|
|
35
|
+
function inferNonNullViewColumns(rows) {
|
|
36
|
+
const definitions = new Map(rows.map((row) => [String(row.view_id), row.definition]));
|
|
37
|
+
const nonNullBaseColumns = new Set(rows.flatMap((row) => row.non_null_columns.map(([relation, attnum]) => `${relation}.${attnum}`)));
|
|
38
|
+
const resolved = new Map();
|
|
39
|
+
const nullableIndexesByQuery = new Map();
|
|
40
|
+
const nullableIndexesOf = (query) => {
|
|
41
|
+
let indexes = nullableIndexesByQuery.get(query);
|
|
42
|
+
if (indexes === undefined) {
|
|
43
|
+
indexes = nullableRangeTableIndexes(query);
|
|
44
|
+
nullableIndexesByQuery.set(query, indexes);
|
|
45
|
+
}
|
|
46
|
+
return indexes;
|
|
47
|
+
};
|
|
48
|
+
const nonNullColumnsOfView = (viewId) => {
|
|
49
|
+
const cached = resolved.get(viewId);
|
|
50
|
+
if (cached !== undefined) {
|
|
51
|
+
return cached;
|
|
52
|
+
}
|
|
53
|
+
const columns = new Set();
|
|
54
|
+
resolved.set(viewId, columns);
|
|
55
|
+
try {
|
|
56
|
+
const tree = parseNodeTree(definitions.get(viewId) ?? "");
|
|
57
|
+
const query = Array.isArray(tree) ? tree[0] : tree;
|
|
58
|
+
if (isNode(query, "QUERY") && allowsNonNull(query)) {
|
|
59
|
+
for (const entry of list(query.fields.targetList)) {
|
|
60
|
+
if (isNode(entry, "TARGETENTRY") &&
|
|
61
|
+
entry.fields.resjunk !== "true" &&
|
|
62
|
+
isNonNullExpression(entry.fields.expr, [query])) {
|
|
63
|
+
columns.add(Number(entry.fields.resno));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
columns.clear();
|
|
70
|
+
}
|
|
71
|
+
return columns;
|
|
72
|
+
};
|
|
73
|
+
const isNonNullColumn = (relation, attnum) => definitions.has(relation)
|
|
74
|
+
? nonNullColumnsOfView(relation).has(attnum)
|
|
75
|
+
: nonNullBaseColumns.has(`${relation}.${attnum}`);
|
|
76
|
+
const isNonNullExpression = (expression, queries) => {
|
|
77
|
+
const query = queries.at(-1);
|
|
78
|
+
if (query === undefined ||
|
|
79
|
+
!isNode(expression, "VAR") ||
|
|
80
|
+
expression.fields.varlevelsup !== "0") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const varno = Number(expression.fields.varno);
|
|
84
|
+
const attnum = Number(expression.fields.varattno);
|
|
85
|
+
if (attnum <= 0 || nullableIndexesOf(query).has(varno)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const entry = list(query.fields.rtable)[varno - 1];
|
|
89
|
+
if (!isNode(entry, "RANGETBLENTRY")) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
switch (entry.fields.rtekind) {
|
|
93
|
+
case RTE_RELATION:
|
|
94
|
+
return (typeof entry.fields.relid === "string" &&
|
|
95
|
+
isNonNullColumn(entry.fields.relid, attnum));
|
|
96
|
+
case RTE_SUBQUERY:
|
|
97
|
+
return isNonNullOutput(entry.fields.subquery, attnum, queries);
|
|
98
|
+
case RTE_JOIN:
|
|
99
|
+
return isNonNullExpression(list(entry.fields.joinaliasvars)[attnum - 1], queries);
|
|
100
|
+
case RTE_GROUP:
|
|
101
|
+
return isNonNullExpression(list(entry.fields.groupexprs)[attnum - 1], queries);
|
|
102
|
+
case RTE_CTE: {
|
|
103
|
+
const levelsUp = Number(entry.fields.ctelevelsup);
|
|
104
|
+
const owner = queries.at(-1 - levelsUp);
|
|
105
|
+
const cte = list(owner?.fields.cteList).find((candidate) => isNode(candidate, "COMMONTABLEEXPR") &&
|
|
106
|
+
candidate.fields.ctename === entry.fields.ctename);
|
|
107
|
+
return (isNode(cte) &&
|
|
108
|
+
cte.fields.cterecursive !== "true" &&
|
|
109
|
+
isNonNullOutput(cte.fields.ctequery, attnum, queries.slice(0, queries.length - levelsUp)));
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const isNonNullOutput = (subquery, resno, queries) => {
|
|
116
|
+
if (!isNode(subquery, "QUERY") || !allowsNonNull(subquery)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const entry = list(subquery.fields.targetList).find((candidate) => isNode(candidate, "TARGETENTRY") &&
|
|
120
|
+
Number(candidate.fields.resno) === resno);
|
|
121
|
+
return (isNode(entry) &&
|
|
122
|
+
isNonNullExpression(entry.fields.expr, [...queries, subquery]));
|
|
123
|
+
};
|
|
124
|
+
const result = new Set();
|
|
125
|
+
for (const viewId of definitions.keys()) {
|
|
126
|
+
for (const attnum of nonNullColumnsOfView(viewId)) {
|
|
127
|
+
result.add(`${viewId}.${attnum}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function allowsNonNull(query) {
|
|
133
|
+
return (query.fields.setOperations === null &&
|
|
134
|
+
list(query.fields.groupingSets).length === 0);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The range table indexes that sit on the nullable side of an outer join in
|
|
138
|
+
* the query's join tree, including the indexes of nested joins themselves.
|
|
139
|
+
*/
|
|
140
|
+
function nullableRangeTableIndexes(query) {
|
|
141
|
+
const nullable = new Set();
|
|
142
|
+
const visit = (node, isNullable) => {
|
|
143
|
+
if (isNode(node, "RANGETBLREF")) {
|
|
144
|
+
if (isNullable) {
|
|
145
|
+
nullable.add(Number(node.fields.rtindex));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (isNode(node, "FROMEXPR")) {
|
|
149
|
+
for (const item of list(node.fields.fromlist)) {
|
|
150
|
+
visit(item, isNullable);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (isNode(node, "JOINEXPR")) {
|
|
154
|
+
const joinType = node.fields.jointype;
|
|
155
|
+
if (isNullable) {
|
|
156
|
+
nullable.add(Number(node.fields.rtindex));
|
|
157
|
+
}
|
|
158
|
+
visit(node.fields.larg, isNullable || (joinType !== JOIN_INNER && joinType !== JOIN_LEFT));
|
|
159
|
+
visit(node.fields.rarg, isNullable || (joinType !== JOIN_INNER && joinType !== JOIN_RIGHT));
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
visit(query.fields.jointree, false);
|
|
163
|
+
return nullable;
|
|
164
|
+
}
|
|
165
|
+
function list(value) {
|
|
166
|
+
return Array.isArray(value) ? value : [];
|
|
167
|
+
}
|
package/dist/rpc.d.ts
ADDED
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the PostgREST view of `GeneratorMetadata.functions`.
|
|
3
|
+
*
|
|
4
|
+
* The raw `functions` collection is `pg_proc` as introspected. Which of those
|
|
5
|
+
* functions PostgREST exposes under `/rpc`, how their overloads group, what a
|
|
6
|
+
* call returns and whether a function doubles as a computed field or an
|
|
7
|
+
* embeddable relation are PostgREST rules, not language rules. This pass
|
|
8
|
+
* applies them once so every generator, in this package or in another SDK
|
|
9
|
+
* repo reading the serialized document, emits the same RPC surface.
|
|
10
|
+
*
|
|
11
|
+
* Pure and deterministic: the output is canonically ordered regardless of the
|
|
12
|
+
* order of the input collections. `introspect()` attaches the result as
|
|
13
|
+
* `rpcFunctions`; generators that meet a document without it (produced by a
|
|
14
|
+
* version predating the collection, or by a custom producer) run this pass
|
|
15
|
+
* themselves.
|
|
16
|
+
*/
|
|
17
|
+
import { compareStrings } from "./collation.js";
|
|
18
|
+
/** json, jsonb and text: the types PostgREST accepts for a single unnamed parameter. */
|
|
19
|
+
const UNNAMED_ARG_TYPE_IDS = new Set([114, 3802, 25]);
|
|
20
|
+
const INPUT_ARG_MODES = new Set(["in", "inout", "variadic"]);
|
|
21
|
+
/** A NUL separator cannot occur in an identifier, so names with dots never collide. */
|
|
22
|
+
const qualifiedKey = (schema, name) => `${schema}\0${name}`;
|
|
23
|
+
export function resolveRpcFunctions(metadata) {
|
|
24
|
+
const schemaNames = new Set(metadata.schemas.map(({ name }) => name));
|
|
25
|
+
const typesById = new Map(metadata.types.map((type) => [type.id, type]));
|
|
26
|
+
const relationTypesById = new Map(metadata.types
|
|
27
|
+
.filter((type) => type.type_relation_id !== null)
|
|
28
|
+
.map((type) => [type.id, type]));
|
|
29
|
+
const relations = [
|
|
30
|
+
...metadata.tables,
|
|
31
|
+
...metadata.foreignTables,
|
|
32
|
+
...metadata.views,
|
|
33
|
+
...metadata.materializedViews,
|
|
34
|
+
];
|
|
35
|
+
const relationsById = new Map(relations.map((relation) => [relation.id, relation]));
|
|
36
|
+
const relationsBySchemaName = new Map(relations.map((relation) => [
|
|
37
|
+
qualifiedKey(relation.schema, relation.name),
|
|
38
|
+
relation,
|
|
39
|
+
]));
|
|
40
|
+
const columnNamesByRelationId = new Map();
|
|
41
|
+
for (const column of metadata.columns) {
|
|
42
|
+
let names = columnNamesByRelationId.get(column.table_id);
|
|
43
|
+
if (!names) {
|
|
44
|
+
names = new Set();
|
|
45
|
+
columnNamesByRelationId.set(column.table_id, names);
|
|
46
|
+
}
|
|
47
|
+
names.add(column.name);
|
|
48
|
+
}
|
|
49
|
+
const typeReference = (typeId) => {
|
|
50
|
+
const type = typesById.get(typeId);
|
|
51
|
+
return {
|
|
52
|
+
type_id: typeId,
|
|
53
|
+
format: type?.name ?? null,
|
|
54
|
+
type_schema: type?.schema ?? null,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const typeName = (typeId) => typesById.get(typeId)?.name ?? "unknown";
|
|
58
|
+
// The relation a return type stands for, or a composite type's name when the
|
|
59
|
+
// return type is one, since both are relations in `pg_class`. Resolved
|
|
60
|
+
// against every schema: a function may return rows of another schema.
|
|
61
|
+
const returnRelationName = (fn) => {
|
|
62
|
+
if (fn.return_type_relation_id === null)
|
|
63
|
+
return null;
|
|
64
|
+
return (relationsById.get(fn.return_type_relation_id)?.name ??
|
|
65
|
+
relationTypesById.get(fn.return_type_id)?.name ??
|
|
66
|
+
null);
|
|
67
|
+
};
|
|
68
|
+
// The function is a computed field of the relation whose row type its sole
|
|
69
|
+
// input parameter has, provided the relation lives in the function's schema
|
|
70
|
+
// and has no column of the function's name: PostgREST resolves the name to
|
|
71
|
+
// the column, so such a function is neither selectable nor part of `Row`.
|
|
72
|
+
// When the parameter's type is not in `types` (a reduced custom document),
|
|
73
|
+
// the rendered argument list may still name the relation.
|
|
74
|
+
const computedFieldOf = ({ fn, inArgs }) => {
|
|
75
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
76
|
+
if (!soleInArg)
|
|
77
|
+
return null;
|
|
78
|
+
const argType = relationTypesById.get(soleInArg.type_id);
|
|
79
|
+
const relation = argType
|
|
80
|
+
? relationsById.get(argType.type_relation_id)
|
|
81
|
+
: typesById.has(soleInArg.type_id)
|
|
82
|
+
? undefined
|
|
83
|
+
: relationsBySchemaName.get(qualifiedKey(fn.schema, fn.argument_types));
|
|
84
|
+
if (!relation || relation.schema !== fn.schema)
|
|
85
|
+
return null;
|
|
86
|
+
if (columnNamesByRelationId.get(relation.id)?.has(fn.name))
|
|
87
|
+
return null;
|
|
88
|
+
return relation.id;
|
|
89
|
+
};
|
|
90
|
+
const isCallable = ({ inArgs }) => {
|
|
91
|
+
if (inArgs.length === 0)
|
|
92
|
+
return true;
|
|
93
|
+
if (!inArgs.some(({ name }) => name === ""))
|
|
94
|
+
return true;
|
|
95
|
+
if (inArgs.every((arg) => arg.name !== "" ||
|
|
96
|
+
(arg.has_default === true && UNNAMED_ARG_TYPE_IDS.has(arg.type_id)))) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
100
|
+
return (soleInArg !== undefined &&
|
|
101
|
+
soleInArg.name === "" &&
|
|
102
|
+
(UNNAMED_ARG_TYPE_IDS.has(soleInArg.type_id) ||
|
|
103
|
+
relationTypesById.has(soleInArg.type_id)));
|
|
104
|
+
};
|
|
105
|
+
const overloadError = (candidate, group) => {
|
|
106
|
+
const { fn, inArgs } = candidate;
|
|
107
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
108
|
+
if (group.length > 1) {
|
|
109
|
+
// A no-argument overload next to one whose sole unnamed parameter has a
|
|
110
|
+
// default: PostgREST cannot pick one for an empty call.
|
|
111
|
+
if (inArgs.length === 0) {
|
|
112
|
+
const conflicting = group.find((other) => other !== candidate &&
|
|
113
|
+
other.inArgs.length === 1 &&
|
|
114
|
+
other.inArgs[0]?.name === "" &&
|
|
115
|
+
other.inArgs[0].has_default === true);
|
|
116
|
+
if (conflicting) {
|
|
117
|
+
return `Could not choose the best candidate function between: ${fn.schema}.${fn.name}(), ${fn.schema}.${fn.name}( => ${typeName(conflicting.fn.return_type_id)}). Try renaming the parameters or the function itself in the database so function overloading can be resolved`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Overloads whose sole parameter shares a name but not a type: a call
|
|
121
|
+
// carries no type information, so PostgREST cannot pick one.
|
|
122
|
+
if (soleInArg && soleInArg.name !== "") {
|
|
123
|
+
const conflicting = group.filter((other) => other !== candidate &&
|
|
124
|
+
other.inArgs.length === 1 &&
|
|
125
|
+
other.inArgs[0]?.name === soleInArg.name &&
|
|
126
|
+
other.inArgs[0].type_id !== soleInArg.type_id);
|
|
127
|
+
if (conflicting.length > 0) {
|
|
128
|
+
const conflictList = [candidate, ...conflicting]
|
|
129
|
+
.sort((a, b) => (a.inArgs[0]?.type_id || 0) - (b.inArgs[0]?.type_id || 0))
|
|
130
|
+
.map((other) => `${fn.schema}.${fn.name}(${other.inArgs
|
|
131
|
+
.map((arg) => `${arg.name} => ${typeName(arg.type_id)}`)
|
|
132
|
+
.join(", ")})`)
|
|
133
|
+
.join(", ");
|
|
134
|
+
return `Could not choose the best candidate function between: ${conflictList}. Try renaming the parameters or the function itself in the database so function overloading can be resolved`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// PostgREST passes a sole unnamed parameter only when it is json, jsonb or
|
|
139
|
+
// text, so a function taking a row this way is a computed field or a
|
|
140
|
+
// computed relationship, reachable by embedding it from its relation and
|
|
141
|
+
// never through `/rpc`, whatever it returns.
|
|
142
|
+
if (soleInArg &&
|
|
143
|
+
soleInArg.name === "" &&
|
|
144
|
+
relationTypesById.has(soleInArg.type_id)) {
|
|
145
|
+
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`;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
const resolveOverload = (candidate, group) => {
|
|
150
|
+
const { fn, inArgs } = candidate;
|
|
151
|
+
const args = inArgs
|
|
152
|
+
.map((arg) => {
|
|
153
|
+
const relationId = relationTypesById.get(arg.type_id)?.type_relation_id;
|
|
154
|
+
return {
|
|
155
|
+
name: arg.name,
|
|
156
|
+
mode: arg.mode,
|
|
157
|
+
...typeReference(arg.type_id),
|
|
158
|
+
has_default: arg.has_default === true,
|
|
159
|
+
relation_id: relationId !== undefined && relationsById.has(relationId)
|
|
160
|
+
? relationId
|
|
161
|
+
: null,
|
|
162
|
+
};
|
|
163
|
+
})
|
|
164
|
+
.sort((a, b) => compareStrings(a.name, b.name));
|
|
165
|
+
const tableColumns = fn.args
|
|
166
|
+
.filter(({ mode }) => mode === "table")
|
|
167
|
+
.map(({ name, type_id }) => ({ name, ...typeReference(type_id) }))
|
|
168
|
+
.sort((a, b) => compareStrings(a.name, b.name));
|
|
169
|
+
const relationName = returnRelationName(fn);
|
|
170
|
+
const returnsMultipleRows = fn.prorows !== null && fn.prorows > 1;
|
|
171
|
+
// A function whose sole parameter (of any mode) is a row type is embedded
|
|
172
|
+
// from that relation; any other row-returning function embeds from `*`.
|
|
173
|
+
const soleArg = fn.args.length === 1 ? fn.args[0] : undefined;
|
|
174
|
+
const sourceType = soleArg && relationTypesById.get(soleArg.type_id);
|
|
175
|
+
const setofOptions = relationName === null
|
|
176
|
+
? null
|
|
177
|
+
: {
|
|
178
|
+
from: sourceType ? sourceType.format : "*",
|
|
179
|
+
to: relationName,
|
|
180
|
+
is_one_to_one: sourceType && !fn.is_set_returning_function
|
|
181
|
+
? true
|
|
182
|
+
: !returnsMultipleRows,
|
|
183
|
+
is_setof_return: fn.is_set_returning_function,
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
function_id: fn.id,
|
|
187
|
+
args,
|
|
188
|
+
returns: {
|
|
189
|
+
...typeReference(fn.return_type_id),
|
|
190
|
+
relation_id: fn.return_type_relation_id !== null &&
|
|
191
|
+
relationsById.has(fn.return_type_relation_id)
|
|
192
|
+
? fn.return_type_relation_id
|
|
193
|
+
: null,
|
|
194
|
+
columns: tableColumns.length > 0 ? tableColumns : null,
|
|
195
|
+
},
|
|
196
|
+
is_set_returning: fn.is_set_returning_function,
|
|
197
|
+
rows_estimate: fn.prorows,
|
|
198
|
+
setof_options: setofOptions,
|
|
199
|
+
computed_field_of: computedFieldOf(candidate),
|
|
200
|
+
error: overloadError(candidate, group),
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
const groups = new Map();
|
|
204
|
+
for (const fn of metadata.functions) {
|
|
205
|
+
if (!schemaNames.has(fn.schema))
|
|
206
|
+
continue;
|
|
207
|
+
const inArgs = fn.args.filter((arg) => INPUT_ARG_MODES.has(arg.mode));
|
|
208
|
+
const candidate = { fn, inArgs };
|
|
209
|
+
if (!isCallable(candidate))
|
|
210
|
+
continue;
|
|
211
|
+
const key = qualifiedKey(fn.schema, fn.name);
|
|
212
|
+
let group = groups.get(key);
|
|
213
|
+
if (!group) {
|
|
214
|
+
group = [];
|
|
215
|
+
groups.set(key, group);
|
|
216
|
+
}
|
|
217
|
+
group.push(candidate);
|
|
218
|
+
}
|
|
219
|
+
return [...groups.values()]
|
|
220
|
+
.map((group) => {
|
|
221
|
+
const sorted = [...group].sort((a, b) => compareStrings(a.fn.argument_types, b.fn.argument_types) ||
|
|
222
|
+
compareStrings(a.fn.return_type, b.fn.return_type) ||
|
|
223
|
+
a.fn.id - b.fn.id);
|
|
224
|
+
const [{ fn }] = sorted;
|
|
225
|
+
return {
|
|
226
|
+
schema: fn.schema,
|
|
227
|
+
name: fn.name,
|
|
228
|
+
overloads: sorted.map((candidate) => resolveOverload(candidate, sorted)),
|
|
229
|
+
};
|
|
230
|
+
})
|
|
231
|
+
.sort((a, b) => compareStrings(a.schema, b.schema) || compareStrings(a.name, b.name));
|
|
232
|
+
}
|
package/dist/sort.js
CHANGED
|
@@ -60,5 +60,28 @@ export function sortGeneratorMetadata(metadata) {
|
|
|
60
60
|
args: [...fn.args].sort((a, b) => compareStrings(a.name, b.name)),
|
|
61
61
|
})),
|
|
62
62
|
types: [...metadata.types].sort(bySchemaName),
|
|
63
|
+
// `resolveRpcFunctions` orders the overloads of a function by signature,
|
|
64
|
+
// and that order is kept here, like composite attributes; the entries,
|
|
65
|
+
// their args and their table columns are addressed by name.
|
|
66
|
+
...(metadata.rpcFunctions === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: {
|
|
69
|
+
rpcFunctions: [...metadata.rpcFunctions]
|
|
70
|
+
.sort((a, b) => compareStrings(a.schema, b.schema) ||
|
|
71
|
+
compareStrings(a.name, b.name))
|
|
72
|
+
.map((fn) => ({
|
|
73
|
+
...fn,
|
|
74
|
+
overloads: fn.overloads.map((overload) => ({
|
|
75
|
+
...overload,
|
|
76
|
+
args: [...overload.args].sort((a, b) => compareStrings(a.name, b.name)),
|
|
77
|
+
returns: {
|
|
78
|
+
...overload.returns,
|
|
79
|
+
columns: overload.returns.columns === null
|
|
80
|
+
? null
|
|
81
|
+
: [...overload.returns.columns].sort((a, b) => compareStrings(a.name, b.name)),
|
|
82
|
+
},
|
|
83
|
+
})),
|
|
84
|
+
})),
|
|
85
|
+
}),
|
|
63
86
|
};
|
|
64
87
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -208,6 +208,119 @@ declare const postgresViewSchema: import("arktype/internal/variants/object.ts").
|
|
|
208
208
|
}[] | undefined;
|
|
209
209
|
}, {}>;
|
|
210
210
|
export type PostgresView = typeof postgresViewSchema.infer;
|
|
211
|
+
/**
|
|
212
|
+
* A reference to a `types` entry by oid. `format` is the type's `name` (the
|
|
213
|
+
* same spelling `PostgresColumn.format` uses, `_` prefixed for arrays) and
|
|
214
|
+
* `type_schema` its schema; both are `null` when the type is not in `types`,
|
|
215
|
+
* which never happens in a document produced by `introspect()`.
|
|
216
|
+
*/
|
|
217
|
+
declare const rpcTypeReferenceSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
218
|
+
type_id: number;
|
|
219
|
+
format: string | null;
|
|
220
|
+
type_schema: string | null;
|
|
221
|
+
}, {}>;
|
|
222
|
+
export type RpcTypeReference = typeof rpcTypeReferenceSchema.infer;
|
|
223
|
+
declare const rpcFunctionArgSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
224
|
+
type_id: number;
|
|
225
|
+
format: string | null;
|
|
226
|
+
type_schema: string | null;
|
|
227
|
+
name: string;
|
|
228
|
+
mode: "in" | "inout" | "variadic";
|
|
229
|
+
has_default: boolean;
|
|
230
|
+
relation_id: number | null;
|
|
231
|
+
}, {}>;
|
|
232
|
+
export type RpcFunctionArg = typeof rpcFunctionArgSchema.infer;
|
|
233
|
+
declare const rpcFunctionReturnsSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
234
|
+
type_id: number;
|
|
235
|
+
format: string | null;
|
|
236
|
+
type_schema: string | null;
|
|
237
|
+
relation_id: number | null;
|
|
238
|
+
columns: {
|
|
239
|
+
type_id: number;
|
|
240
|
+
format: string | null;
|
|
241
|
+
type_schema: string | null;
|
|
242
|
+
name: string;
|
|
243
|
+
}[] | null;
|
|
244
|
+
}, {}>;
|
|
245
|
+
export type RpcFunctionReturns = typeof rpcFunctionReturnsSchema.infer;
|
|
246
|
+
declare const rpcFunctionOverloadSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
247
|
+
function_id: number;
|
|
248
|
+
args: {
|
|
249
|
+
type_id: number;
|
|
250
|
+
format: string | null;
|
|
251
|
+
type_schema: string | null;
|
|
252
|
+
name: string;
|
|
253
|
+
mode: "in" | "inout" | "variadic";
|
|
254
|
+
has_default: boolean;
|
|
255
|
+
relation_id: number | null;
|
|
256
|
+
}[];
|
|
257
|
+
returns: {
|
|
258
|
+
type_id: number;
|
|
259
|
+
format: string | null;
|
|
260
|
+
type_schema: string | null;
|
|
261
|
+
relation_id: number | null;
|
|
262
|
+
columns: {
|
|
263
|
+
type_id: number;
|
|
264
|
+
format: string | null;
|
|
265
|
+
type_schema: string | null;
|
|
266
|
+
name: string;
|
|
267
|
+
}[] | null;
|
|
268
|
+
};
|
|
269
|
+
is_set_returning: boolean;
|
|
270
|
+
rows_estimate: number | null;
|
|
271
|
+
setof_options: {
|
|
272
|
+
from: string;
|
|
273
|
+
to: string;
|
|
274
|
+
is_one_to_one: boolean;
|
|
275
|
+
is_setof_return: boolean;
|
|
276
|
+
} | null;
|
|
277
|
+
computed_field_of: number | null;
|
|
278
|
+
error: string | null;
|
|
279
|
+
}, {}>;
|
|
280
|
+
export type RpcFunctionOverload = typeof rpcFunctionOverloadSchema.infer;
|
|
281
|
+
/**
|
|
282
|
+
* A function PostgREST exposes under `/rpc`, with its overloads resolved.
|
|
283
|
+
* Produced by `resolveRpcFunctions` from the raw `functions` collection.
|
|
284
|
+
*/
|
|
285
|
+
declare const rpcFunctionSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
286
|
+
schema: string;
|
|
287
|
+
name: string;
|
|
288
|
+
overloads: {
|
|
289
|
+
function_id: number;
|
|
290
|
+
args: {
|
|
291
|
+
type_id: number;
|
|
292
|
+
format: string | null;
|
|
293
|
+
type_schema: string | null;
|
|
294
|
+
name: string;
|
|
295
|
+
mode: "in" | "inout" | "variadic";
|
|
296
|
+
has_default: boolean;
|
|
297
|
+
relation_id: number | null;
|
|
298
|
+
}[];
|
|
299
|
+
returns: {
|
|
300
|
+
type_id: number;
|
|
301
|
+
format: string | null;
|
|
302
|
+
type_schema: string | null;
|
|
303
|
+
relation_id: number | null;
|
|
304
|
+
columns: {
|
|
305
|
+
type_id: number;
|
|
306
|
+
format: string | null;
|
|
307
|
+
type_schema: string | null;
|
|
308
|
+
name: string;
|
|
309
|
+
}[] | null;
|
|
310
|
+
};
|
|
311
|
+
is_set_returning: boolean;
|
|
312
|
+
rows_estimate: number | null;
|
|
313
|
+
setof_options: {
|
|
314
|
+
from: string;
|
|
315
|
+
to: string;
|
|
316
|
+
is_one_to_one: boolean;
|
|
317
|
+
is_setof_return: boolean;
|
|
318
|
+
} | null;
|
|
319
|
+
computed_field_of: number | null;
|
|
320
|
+
error: string | null;
|
|
321
|
+
}[];
|
|
322
|
+
}, {}>;
|
|
323
|
+
export type RpcFunction = typeof rpcFunctionSchema.infer;
|
|
211
324
|
/**
|
|
212
325
|
* Bumped whenever `GeneratorMetadata`'s shape changes in a way a consumer
|
|
213
326
|
* should branch on. Out-of-process consumers (a generator in another
|
|
@@ -341,6 +454,44 @@ export declare const generatorMetadataSchema: import("arktype/internal/variants/
|
|
|
341
454
|
comment: string | null;
|
|
342
455
|
type_relation_id: number | null;
|
|
343
456
|
}[];
|
|
457
|
+
rpcFunctions?: {
|
|
458
|
+
schema: string;
|
|
459
|
+
name: string;
|
|
460
|
+
overloads: {
|
|
461
|
+
function_id: number;
|
|
462
|
+
args: {
|
|
463
|
+
type_id: number;
|
|
464
|
+
format: string | null;
|
|
465
|
+
type_schema: string | null;
|
|
466
|
+
name: string;
|
|
467
|
+
mode: "in" | "inout" | "variadic";
|
|
468
|
+
has_default: boolean;
|
|
469
|
+
relation_id: number | null;
|
|
470
|
+
}[];
|
|
471
|
+
returns: {
|
|
472
|
+
type_id: number;
|
|
473
|
+
format: string | null;
|
|
474
|
+
type_schema: string | null;
|
|
475
|
+
relation_id: number | null;
|
|
476
|
+
columns: {
|
|
477
|
+
type_id: number;
|
|
478
|
+
format: string | null;
|
|
479
|
+
type_schema: string | null;
|
|
480
|
+
name: string;
|
|
481
|
+
}[] | null;
|
|
482
|
+
};
|
|
483
|
+
is_set_returning: boolean;
|
|
484
|
+
rows_estimate: number | null;
|
|
485
|
+
setof_options: {
|
|
486
|
+
from: string;
|
|
487
|
+
to: string;
|
|
488
|
+
is_one_to_one: boolean;
|
|
489
|
+
is_setof_return: boolean;
|
|
490
|
+
} | null;
|
|
491
|
+
computed_field_of: number | null;
|
|
492
|
+
error: string | null;
|
|
493
|
+
}[];
|
|
494
|
+
}[] | undefined;
|
|
344
495
|
}, {}>;
|
|
345
496
|
export type GeneratorMetadata = typeof generatorMetadataSchema.infer;
|
|
346
497
|
/**
|