@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
package/dist/rpc.js
CHANGED
|
@@ -18,18 +18,25 @@ import { compareStrings } from "./collation.js";
|
|
|
18
18
|
/** json, jsonb and text: the types PostgREST accepts for a single unnamed parameter. */
|
|
19
19
|
const UNNAMED_ARG_TYPE_IDS = new Set([114, 3802, 25]);
|
|
20
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}`;
|
|
21
23
|
export function resolveRpcFunctions(metadata) {
|
|
22
24
|
const schemaNames = new Set(metadata.schemas.map(({ name }) => name));
|
|
23
25
|
const typesById = new Map(metadata.types.map((type) => [type.id, type]));
|
|
24
26
|
const relationTypesById = new Map(metadata.types
|
|
25
27
|
.filter((type) => type.type_relation_id !== null)
|
|
26
28
|
.map((type) => [type.id, type]));
|
|
27
|
-
const
|
|
29
|
+
const relations = [
|
|
28
30
|
...metadata.tables,
|
|
29
31
|
...metadata.foreignTables,
|
|
30
32
|
...metadata.views,
|
|
31
33
|
...metadata.materializedViews,
|
|
32
|
-
]
|
|
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
|
+
]));
|
|
33
40
|
const columnNamesByRelationId = new Map();
|
|
34
41
|
for (const column of metadata.columns) {
|
|
35
42
|
let names = columnNamesByRelationId.get(column.table_id);
|
|
@@ -66,11 +73,14 @@ export function resolveRpcFunctions(metadata) {
|
|
|
66
73
|
// the rendered argument list may still name the relation.
|
|
67
74
|
const computedFieldOf = ({ fn, inArgs }) => {
|
|
68
75
|
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
69
|
-
|
|
76
|
+
if (!soleInArg)
|
|
77
|
+
return null;
|
|
78
|
+
const argType = relationTypesById.get(soleInArg.type_id);
|
|
70
79
|
const relation = argType
|
|
71
80
|
? relationsById.get(argType.type_relation_id)
|
|
72
|
-
:
|
|
73
|
-
|
|
81
|
+
: typesById.has(soleInArg.type_id)
|
|
82
|
+
? undefined
|
|
83
|
+
: relationsBySchemaName.get(qualifiedKey(fn.schema, fn.argument_types));
|
|
74
84
|
if (!relation || relation.schema !== fn.schema)
|
|
75
85
|
return null;
|
|
76
86
|
if (columnNamesByRelationId.get(relation.id)?.has(fn.name))
|
|
@@ -125,12 +135,13 @@ export function resolveRpcFunctions(metadata) {
|
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
|
-
//
|
|
129
|
-
//
|
|
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.
|
|
130
142
|
if (soleInArg &&
|
|
131
143
|
soleInArg.name === "" &&
|
|
132
|
-
relationTypesById.has(soleInArg.type_id)
|
|
133
|
-
returnRelationName(fn) === null) {
|
|
144
|
+
relationTypesById.has(soleInArg.type_id)) {
|
|
134
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`;
|
|
135
146
|
}
|
|
136
147
|
return null;
|
|
@@ -145,9 +156,7 @@ export function resolveRpcFunctions(metadata) {
|
|
|
145
156
|
mode: arg.mode,
|
|
146
157
|
...typeReference(arg.type_id),
|
|
147
158
|
has_default: arg.has_default === true,
|
|
148
|
-
relation_id: relationId !== undefined &&
|
|
149
|
-
relationId !== null &&
|
|
150
|
-
relationsById.has(relationId)
|
|
159
|
+
relation_id: relationId !== undefined && relationsById.has(relationId)
|
|
151
160
|
? relationId
|
|
152
161
|
: null,
|
|
153
162
|
};
|
|
@@ -199,7 +208,7 @@ export function resolveRpcFunctions(metadata) {
|
|
|
199
208
|
const candidate = { fn, inArgs };
|
|
200
209
|
if (!isCallable(candidate))
|
|
201
210
|
continue;
|
|
202
|
-
const key =
|
|
211
|
+
const key = qualifiedKey(fn.schema, fn.name);
|
|
203
212
|
let group = groups.get(key);
|
|
204
213
|
if (!group) {
|
|
205
214
|
group = [];
|
package/dist/types.js
CHANGED
|
@@ -179,8 +179,8 @@ const rpcFunctionReturnsSchema = rpcTypeReferenceSchema.merge({
|
|
|
179
179
|
*/
|
|
180
180
|
relation_id: "number | null",
|
|
181
181
|
/**
|
|
182
|
-
* The columns of a `RETURNS TABLE (…)`
|
|
183
|
-
*
|
|
182
|
+
* The columns of a `RETURNS TABLE (…)` result, in name order; `null` for
|
|
183
|
+
* every other return type, including a record built from `OUT` parameters.
|
|
184
184
|
*/
|
|
185
185
|
columns: rpcTypeReferenceSchema.merge({ name: "string" }).array().or("null"),
|
|
186
186
|
});
|
|
@@ -217,9 +217,10 @@ const rpcFunctionOverloadSchema = type({
|
|
|
217
217
|
computed_field_of: "number | null",
|
|
218
218
|
/**
|
|
219
219
|
* Why PostgREST cannot resolve a call to this overload, when it cannot: the
|
|
220
|
-
* function conflicts with another overload, or
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* function conflicts with another overload, or its sole parameter is an
|
|
221
|
+
* unnamed row, which PostgREST never matches under `/rpc` (the function is
|
|
222
|
+
* a computed field or computed relationship, reached by embedding).
|
|
223
|
+
* Generators surface the message instead of a callable signature.
|
|
223
224
|
*/
|
|
224
225
|
error: "string | null",
|
|
225
226
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/postgrest-typegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Type generation for PostgREST from PostgreSQL schemas",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"test": "bun scripts/run-tests.ts test/"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"arktype": "2.2.
|
|
66
|
+
"arktype": "2.2.9"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@testcontainers/postgresql": "^12.1.0",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"@types/pg": "^8.23.1",
|
|
76
76
|
"knip": "^6.32.3",
|
|
77
77
|
"oxfmt": "0.66.0",
|
|
78
|
-
"oxlint": "1.
|
|
79
|
-
"oxlint-tsgolint": "7.0.
|
|
78
|
+
"oxlint": "1.90.0",
|
|
79
|
+
"oxlint-tsgolint": "7.0.2005",
|
|
80
80
|
"pg": "^8.23.0",
|
|
81
81
|
"testcontainers": "^12.1.0",
|
|
82
82
|
"typescript": "7.0.2"
|
|
@@ -121,7 +121,6 @@ export const generateTypescript = async (
|
|
|
121
121
|
relationships,
|
|
122
122
|
types,
|
|
123
123
|
} = metadata;
|
|
124
|
-
// Documents predating the collection (still valid version 1) resolve it here.
|
|
125
124
|
const rpcFunctions = metadata.rpcFunctions ?? resolveRpcFunctions(metadata);
|
|
126
125
|
const {
|
|
127
126
|
detectOneToOneRelationships = false,
|
|
@@ -334,7 +333,11 @@ export const generateTypescript = async (
|
|
|
334
333
|
// Without that metadata PostgREST still returns an array, so the collapse would make
|
|
335
334
|
// the type diverge from the actual response.
|
|
336
335
|
const collapsibleSingleRow = setof_options?.is_one_to_one === true;
|
|
337
|
-
|
|
336
|
+
const isArray =
|
|
337
|
+
overload.error === null &&
|
|
338
|
+
overload.is_set_returning &&
|
|
339
|
+
!collapsibleSingleRow;
|
|
340
|
+
return `${returnType}${isArray ? "[]" : ""}
|
|
338
341
|
${setofOptionsInfo}`;
|
|
339
342
|
};
|
|
340
343
|
|
|
@@ -14,7 +14,9 @@ import { PRIMARY_KEYS_SQL } from "./sql/primary_keys.sql.ts";
|
|
|
14
14
|
import { SCHEMAS_SQL } from "./sql/schemas.sql.ts";
|
|
15
15
|
import { TABLES_SQL } from "./sql/table.sql.ts";
|
|
16
16
|
import { TYPES_SQL } from "./sql/types.sql.ts";
|
|
17
|
+
import { VIEW_DEFINITIONS_SQL } from "./sql/view_definitions.sql.ts";
|
|
17
18
|
import { VIEWS_SQL } from "./sql/views.sql.ts";
|
|
19
|
+
import { applyViewColumnNullability } from "./view-nullability.ts";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Minimal structural database interface. `pg.Pool` / `pg.Client` satisfy it,
|
|
@@ -29,6 +31,12 @@ export interface Queryable {
|
|
|
29
31
|
export interface IntrospectOptions {
|
|
30
32
|
includedSchemas?: string[];
|
|
31
33
|
excludedSchemas?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* PostgREST's `db-extra-search-path` schemas. A view in a generated schema
|
|
36
|
+
* that selects from a view in one of them keeps the relationships of the
|
|
37
|
+
* underlying tables, as in PostgREST. Nothing in these schemas is generated.
|
|
38
|
+
*/
|
|
39
|
+
extraSearchPath?: string[];
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
@@ -72,6 +80,7 @@ export async function introspect(
|
|
|
72
80
|
relationships,
|
|
73
81
|
functions,
|
|
74
82
|
types,
|
|
83
|
+
viewDefinitions,
|
|
75
84
|
] = await Promise.all([
|
|
76
85
|
queryRows(SCHEMAS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
77
86
|
queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
|
|
@@ -80,9 +89,10 @@ export async function introspect(
|
|
|
80
89
|
queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
|
|
81
90
|
queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
82
91
|
queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
83
|
-
listRelationships(db, systemExcludingFilter),
|
|
92
|
+
listRelationships(db, systemExcludingFilter, opts.extraSearchPath),
|
|
84
93
|
queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
85
94
|
queryRows(TYPES_SQL),
|
|
95
|
+
queryRows(VIEW_DEFINITIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
86
96
|
]);
|
|
87
97
|
|
|
88
98
|
const metadata = {
|
|
@@ -92,7 +102,7 @@ export async function introspect(
|
|
|
92
102
|
foreignTables,
|
|
93
103
|
views,
|
|
94
104
|
materializedViews,
|
|
95
|
-
columns,
|
|
105
|
+
columns: applyViewColumnNullability(columns, viewDefinitions),
|
|
96
106
|
primaryKeys,
|
|
97
107
|
relationships,
|
|
98
108
|
functions: functions.filter(
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value in the text form of a `pg_node_tree`: a scalar token, `null` for
|
|
3
|
+
* `<>`, a list, or a node. Integer, OID and bitmapset lists (`(i 1 2)`,
|
|
4
|
+
* `(o 23)`, `(b 3)`) lose their marker and become lists of scalar tokens.
|
|
5
|
+
*/
|
|
6
|
+
export type NodeTreeValue = string | null | NodeTreeValue[] | NodeTreeNode;
|
|
7
|
+
|
|
8
|
+
export interface NodeTreeNode {
|
|
9
|
+
type: string;
|
|
10
|
+
fields: Record<string, NodeTreeValue>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Token {
|
|
14
|
+
raw: string;
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LIST_MARKERS = new Set(["i", "o", "b", "x"]);
|
|
19
|
+
const WHITESPACE = " \t\n";
|
|
20
|
+
const PUNCTUATION = "(){}";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse the text form of a `pg_node_tree` (for example
|
|
24
|
+
* `pg_rewrite.ev_action::text`), following the tokenizer of PostgreSQL's
|
|
25
|
+
* `read.c`: tokens are separated by whitespace, `(`, `)`, `{` and `}` are
|
|
26
|
+
* tokens of their own, and a backslash escapes the next character.
|
|
27
|
+
*/
|
|
28
|
+
export function parseNodeTree(text: string): NodeTreeValue {
|
|
29
|
+
const tokens = tokenize(text);
|
|
30
|
+
let position = 0;
|
|
31
|
+
|
|
32
|
+
const peek = (): Token | undefined => tokens[position];
|
|
33
|
+
const next = (): Token => {
|
|
34
|
+
const token = tokens[position++];
|
|
35
|
+
if (token === undefined) {
|
|
36
|
+
throw new Error("Unexpected end of pg_node_tree");
|
|
37
|
+
}
|
|
38
|
+
return token;
|
|
39
|
+
};
|
|
40
|
+
const isFieldEnd = (token: Token | undefined) =>
|
|
41
|
+
token === undefined || token.raw === "}" || token.raw.startsWith(":");
|
|
42
|
+
|
|
43
|
+
const parseValue = (): NodeTreeValue => {
|
|
44
|
+
const token = next();
|
|
45
|
+
if (token.raw === "{") {
|
|
46
|
+
const type = next().text;
|
|
47
|
+
const fields: Record<string, NodeTreeValue> = {};
|
|
48
|
+
while (peek()?.raw !== "}") {
|
|
49
|
+
const name = next().raw.slice(1);
|
|
50
|
+
fields[name] = isFieldEnd(peek()) ? null : parseValue();
|
|
51
|
+
// Some fields span several tokens, such as a constant's
|
|
52
|
+
// `:constvalue 4 [ 1 0 0 0 ]`; only the first one is kept.
|
|
53
|
+
while (!isFieldEnd(peek())) {
|
|
54
|
+
parseValue();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
next();
|
|
58
|
+
return { type, fields };
|
|
59
|
+
}
|
|
60
|
+
if (token.raw === "(") {
|
|
61
|
+
const items: NodeTreeValue[] = [];
|
|
62
|
+
const first = peek();
|
|
63
|
+
if (first !== undefined && LIST_MARKERS.has(first.raw)) {
|
|
64
|
+
next();
|
|
65
|
+
}
|
|
66
|
+
while (peek()?.raw !== ")") {
|
|
67
|
+
items.push(parseValue());
|
|
68
|
+
}
|
|
69
|
+
next();
|
|
70
|
+
return items;
|
|
71
|
+
}
|
|
72
|
+
if (token.raw === "<>") {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return token.text;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const value = parseValue();
|
|
79
|
+
if (position !== tokens.length) {
|
|
80
|
+
throw new Error("Trailing tokens after pg_node_tree");
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function tokenize(text: string): Token[] {
|
|
86
|
+
const tokens: Token[] = [];
|
|
87
|
+
let index = 0;
|
|
88
|
+
while (index < text.length) {
|
|
89
|
+
const character = text.charAt(index);
|
|
90
|
+
if (WHITESPACE.includes(character)) {
|
|
91
|
+
index++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (PUNCTUATION.includes(character)) {
|
|
95
|
+
tokens.push({ raw: character, text: character });
|
|
96
|
+
index++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const start = index;
|
|
100
|
+
let unescaped = "";
|
|
101
|
+
while (index < text.length) {
|
|
102
|
+
const current = text.charAt(index);
|
|
103
|
+
if (current === "\\" && index + 1 < text.length) {
|
|
104
|
+
unescaped += text.charAt(index + 1);
|
|
105
|
+
index += 2;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (WHITESPACE.includes(current) || PUNCTUATION.includes(current)) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
unescaped += current;
|
|
112
|
+
index++;
|
|
113
|
+
}
|
|
114
|
+
tokens.push({ raw: text.slice(start, index), text: unescaped });
|
|
115
|
+
}
|
|
116
|
+
return tokens;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isNode(
|
|
120
|
+
value: NodeTreeValue | undefined,
|
|
121
|
+
type?: string,
|
|
122
|
+
): value is NodeTreeNode {
|
|
123
|
+
return (
|
|
124
|
+
typeof value === "object" &&
|
|
125
|
+
value !== null &&
|
|
126
|
+
!Array.isArray(value) &&
|
|
127
|
+
(type === undefined || value.type === type)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PostgresRelationship } from "../types.ts";
|
|
2
2
|
import type { Queryable } from "./index.ts";
|
|
3
|
+
import { DEFAULT_SYSTEM_SCHEMAS, filterByList } from "./sql/helpers.ts";
|
|
3
4
|
import { TABLE_RELATIONSHIPS_SQL } from "./sql/table_relationships.sql.ts";
|
|
4
5
|
import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.ts";
|
|
5
6
|
|
|
@@ -13,7 +14,8 @@ import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.ts"
|
|
|
13
14
|
* by mapping a base table's foreign key columns onto the view columns that
|
|
14
15
|
* select them (`VIEWS_KEY_DEPENDENCIES_SQL` + the cartesian-product
|
|
15
16
|
* expansion below). This second part is pure TypeScript, not SQL — ported
|
|
16
|
-
*
|
|
17
|
+
* from postgres-meta, except that the expansion sees every non-system
|
|
18
|
+
* schema's foreign keys, as PostgREST does (see `listRelationships`).
|
|
17
19
|
*
|
|
18
20
|
* Adapted from:
|
|
19
21
|
* https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L392
|
|
@@ -39,13 +41,32 @@ export type ViewKeyDependency = {
|
|
|
39
41
|
* view↔view relationships implied by the view key dependencies. Pure function
|
|
40
42
|
* (no database) so the cartesian-product expansion can be unit-tested directly.
|
|
41
43
|
*
|
|
42
|
-
* Ported
|
|
43
|
-
* `allViewM2oAndO2oRelationships` block).
|
|
44
|
+
* Ported from `PostgresMetaRelationships.list()` (the body of the
|
|
45
|
+
* `allViewM2oAndO2oRelationships` block). Every table relationship feeds the
|
|
46
|
+
* expansion, but a table→view relationship is emitted on the table itself, so
|
|
47
|
+
* `isTableEmitted` keeps it to the tables the caller returns relationships for.
|
|
44
48
|
*/
|
|
45
49
|
export function expandViewRelationships(
|
|
46
50
|
allTableM2oAndO2oRelationships: PostgresRelationship[],
|
|
47
51
|
viewsKeyDependencies: ViewKeyDependency[],
|
|
52
|
+
isTableEmitted: (relationship: PostgresRelationship) => boolean,
|
|
48
53
|
): PostgresRelationship[] {
|
|
54
|
+
const keyDepsByConstraint = new Map<string, ViewKeyDependency[]>();
|
|
55
|
+
for (const vkd of viewsKeyDependencies) {
|
|
56
|
+
const key = keyDependencyKey(
|
|
57
|
+
vkd.table_schema,
|
|
58
|
+
vkd.table_name,
|
|
59
|
+
vkd.constraint_name,
|
|
60
|
+
vkd.constraint_type,
|
|
61
|
+
);
|
|
62
|
+
let bucket = keyDepsByConstraint.get(key);
|
|
63
|
+
if (!bucket) {
|
|
64
|
+
bucket = [];
|
|
65
|
+
keyDepsByConstraint.set(key, bucket);
|
|
66
|
+
}
|
|
67
|
+
bucket.push(vkd);
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
return allTableM2oAndO2oRelationships.flatMap((r) => {
|
|
50
71
|
const expandKeyDepCols = (
|
|
51
72
|
colDeps: ColDep[],
|
|
@@ -68,20 +89,19 @@ export function expandViewRelationships(
|
|
|
68
89
|
}));
|
|
69
90
|
};
|
|
70
91
|
|
|
71
|
-
const viewToTableKeyDeps =
|
|
72
|
-
(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
92
|
+
const viewToTableKeyDeps =
|
|
93
|
+
keyDepsByConstraint.get(
|
|
94
|
+
keyDependencyKey(r.schema, r.relation, r.foreign_key_name, "f"),
|
|
95
|
+
) ?? [];
|
|
96
|
+
const tableToViewKeyDeps =
|
|
97
|
+
keyDepsByConstraint.get(
|
|
98
|
+
keyDependencyKey(
|
|
99
|
+
r.referenced_schema,
|
|
100
|
+
r.referenced_relation,
|
|
101
|
+
r.foreign_key_name,
|
|
102
|
+
"f_ref",
|
|
103
|
+
),
|
|
104
|
+
) ?? [];
|
|
85
105
|
|
|
86
106
|
const viewToTableRelationships = viewToTableKeyDeps.flatMap((vtkd) =>
|
|
87
107
|
expandKeyDepCols(vtkd.column_dependencies).map(({ viewColumns }) => ({
|
|
@@ -96,7 +116,9 @@ export function expandViewRelationships(
|
|
|
96
116
|
})),
|
|
97
117
|
);
|
|
98
118
|
|
|
99
|
-
const tableToViewRelationships =
|
|
119
|
+
const tableToViewRelationships = (
|
|
120
|
+
isTableEmitted(r) ? tableToViewKeyDeps : []
|
|
121
|
+
).flatMap((tvkd) =>
|
|
100
122
|
expandKeyDepCols(tvkd.column_dependencies).map(({ viewColumns }) => ({
|
|
101
123
|
foreign_key_name: r.foreign_key_name,
|
|
102
124
|
schema: r.schema,
|
|
@@ -139,26 +161,62 @@ export function expandViewRelationships(
|
|
|
139
161
|
/**
|
|
140
162
|
* List all relationships (table↔table plus the view-derived ones) for the
|
|
141
163
|
* schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
|
|
142
|
-
* `introspect()` builds with the system schemas excluded.
|
|
143
|
-
*
|
|
164
|
+
* `introspect()` builds with the system schemas excluded. Like PostgREST, view
|
|
165
|
+
* expansion sees the foreign keys of every non-system schema, so a view over a
|
|
166
|
+
* table in a schema that is not selected keeps the table's foreign keys; only
|
|
167
|
+
* the views and tables in the selected schemas get relationships. Views in the
|
|
168
|
+
* `extraSearchPath` schemas are followed to their base tables but get no
|
|
169
|
+
* relationships of their own. Errors from the injected `Queryable` propagate
|
|
170
|
+
* by throwing — callers adapt.
|
|
144
171
|
*/
|
|
145
172
|
export async function listRelationships(
|
|
146
173
|
db: Queryable,
|
|
147
174
|
schemaFilter: string,
|
|
175
|
+
extraSearchPath: string[] = [],
|
|
148
176
|
): Promise<PostgresRelationship[]> {
|
|
149
|
-
const
|
|
177
|
+
const tableRelationshipsQuery = db.query(
|
|
150
178
|
TABLE_RELATIONSHIPS_SQL({ schemaFilter }),
|
|
151
179
|
);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
const [
|
|
181
|
+
{ rows: tableRelationships },
|
|
182
|
+
{ rows: allTableRelationships },
|
|
183
|
+
{ rows: viewsKeyDependencies },
|
|
184
|
+
] = await Promise.all([
|
|
185
|
+
tableRelationshipsQuery,
|
|
186
|
+
schemaFilter === NON_SYSTEM_SCHEMAS_FILTER
|
|
187
|
+
? tableRelationshipsQuery
|
|
188
|
+
: db.query(
|
|
189
|
+
TABLE_RELATIONSHIPS_SQL({ schemaFilter: NON_SYSTEM_SCHEMAS_FILTER }),
|
|
190
|
+
),
|
|
191
|
+
db.query(VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter, extraSearchPath })),
|
|
192
|
+
]);
|
|
155
193
|
|
|
194
|
+
const emittedKeys = new Set(
|
|
195
|
+
(tableRelationships as PostgresRelationship[]).map(relationshipKey),
|
|
196
|
+
);
|
|
156
197
|
const allViewM2oAndO2oRelationships = expandViewRelationships(
|
|
157
|
-
|
|
198
|
+
allTableRelationships as PostgresRelationship[],
|
|
158
199
|
viewsKeyDependencies as ViewKeyDependency[],
|
|
200
|
+
(r) => emittedKeys.has(relationshipKey(r)),
|
|
159
201
|
);
|
|
160
202
|
|
|
161
|
-
return (
|
|
203
|
+
return (tableRelationships as PostgresRelationship[]).concat(
|
|
162
204
|
allViewM2oAndO2oRelationships,
|
|
163
205
|
);
|
|
164
206
|
}
|
|
207
|
+
|
|
208
|
+
const NON_SYSTEM_SCHEMAS_FILTER = filterByList(
|
|
209
|
+
undefined,
|
|
210
|
+
undefined,
|
|
211
|
+
DEFAULT_SYSTEM_SCHEMAS,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const relationshipKey = (r: PostgresRelationship) =>
|
|
215
|
+
JSON.stringify([r.schema, r.relation, r.foreign_key_name]);
|
|
216
|
+
|
|
217
|
+
const keyDependencyKey = (
|
|
218
|
+
tableSchema: string,
|
|
219
|
+
tableName: string,
|
|
220
|
+
constraintName: string,
|
|
221
|
+
constraintType: ViewKeyDependency["constraint_type"],
|
|
222
|
+
) => JSON.stringify([tableSchema, tableName, constraintName, constraintType]);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SchemaFilterProps } from "./common.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The query tree of every view and materialized view in the filtered schemas,
|
|
5
|
+
* plus every view they read from in any schema, with the `NOT NULL` columns
|
|
6
|
+
* each one references. `inferNonNullViewColumns` resolves view column
|
|
7
|
+
* nullability from it.
|
|
8
|
+
*/
|
|
9
|
+
export const VIEW_DEFINITIONS_SQL = (props: SchemaFilterProps) => /* SQL */ `
|
|
10
|
+
WITH RECURSIVE view_closure(oid) AS (
|
|
11
|
+
SELECT c.oid
|
|
12
|
+
FROM pg_class c
|
|
13
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
14
|
+
WHERE
|
|
15
|
+
${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
|
|
16
|
+
c.relkind IN ('v', 'm')
|
|
17
|
+
UNION
|
|
18
|
+
SELECT dep.refobjid
|
|
19
|
+
FROM view_closure vc
|
|
20
|
+
JOIN pg_rewrite r ON r.ev_class = vc.oid AND r.rulename = '_RETURN'
|
|
21
|
+
JOIN pg_depend dep ON dep.classid = 'pg_rewrite' :: regclass
|
|
22
|
+
AND dep.objid = r.oid
|
|
23
|
+
AND dep.refclassid = 'pg_class' :: regclass
|
|
24
|
+
JOIN pg_class rc ON rc.oid = dep.refobjid AND rc.relkind IN ('v', 'm')
|
|
25
|
+
)
|
|
26
|
+
SELECT
|
|
27
|
+
r.ev_class :: int8 AS view_id,
|
|
28
|
+
r.ev_action :: text AS definition,
|
|
29
|
+
COALESCE(
|
|
30
|
+
(
|
|
31
|
+
SELECT json_agg(json_build_array(dep.refobjid :: int8, dep.refobjsubid))
|
|
32
|
+
FROM pg_depend dep
|
|
33
|
+
JOIN pg_attribute a ON a.attrelid = dep.refobjid AND a.attnum = dep.refobjsubid
|
|
34
|
+
JOIN pg_type t ON t.oid = a.atttypid
|
|
35
|
+
WHERE dep.classid = 'pg_rewrite' :: regclass
|
|
36
|
+
AND dep.objid = r.oid
|
|
37
|
+
AND dep.refclassid = 'pg_class' :: regclass
|
|
38
|
+
AND dep.refobjsubid > 0
|
|
39
|
+
AND (a.attnotnull OR t.typtype = 'd' AND t.typnotnull)
|
|
40
|
+
),
|
|
41
|
+
'[]'
|
|
42
|
+
) AS non_null_columns
|
|
43
|
+
FROM view_closure vc
|
|
44
|
+
JOIN pg_rewrite r ON r.ev_class = vc.oid AND r.rulename = '_RETURN'
|
|
45
|
+
`;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { SchemaFilterProps } from "./common.ts";
|
|
2
|
+
import { filterByList } from "./helpers.ts";
|
|
2
3
|
import { literal } from "./pg-format.ts";
|
|
3
4
|
|
|
4
5
|
export const VIEWS_KEY_DEPENDENCIES_SQL = (
|
|
5
|
-
props: SchemaFilterProps,
|
|
6
|
+
props: SchemaFilterProps & { extraSearchPath?: string[] },
|
|
6
7
|
) => /* SQL */ `
|
|
7
8
|
-- Adapted from
|
|
8
9
|
-- https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L820
|
|
@@ -31,7 +32,6 @@ pks_fks as (
|
|
|
31
32
|
from pg_constraint
|
|
32
33
|
left join lateral unnest(confkey) with ordinality as _(col, ord) on true
|
|
33
34
|
where contype='f'
|
|
34
|
-
${props.schemaFilter ? `and connamespace::regnamespace::text ${props.schemaFilter}` : ""}
|
|
35
35
|
),
|
|
36
36
|
views as (
|
|
37
37
|
select
|
|
@@ -43,7 +43,7 @@ views as (
|
|
|
43
43
|
join pg_namespace n on n.oid = c.relnamespace
|
|
44
44
|
join pg_rewrite r on r.ev_class = c.oid
|
|
45
45
|
where c.relkind in ('v', 'm')
|
|
46
|
-
${props
|
|
46
|
+
${viewSchemaFilter(props)}
|
|
47
47
|
),
|
|
48
48
|
transform_json as (
|
|
49
49
|
select
|
|
@@ -125,6 +125,20 @@ group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conn
|
|
|
125
125
|
having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)
|
|
126
126
|
`;
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Like PostgREST, the views followed to their base tables also include the
|
|
130
|
+
* `db-extra-search-path` schemas; the recursion still starts from the selected
|
|
131
|
+
* schemas only.
|
|
132
|
+
*/
|
|
133
|
+
function viewSchemaFilter({
|
|
134
|
+
schemaFilter,
|
|
135
|
+
extraSearchPath = [],
|
|
136
|
+
}: SchemaFilterProps & { extraSearchPath?: string[] }): string {
|
|
137
|
+
if (!schemaFilter) return "";
|
|
138
|
+
if (extraSearchPath.length === 0) return `and n.nspname ${schemaFilter}`;
|
|
139
|
+
return `and (n.nspname ${schemaFilter} or n.nspname ${filterByList(extraSearchPath)})`;
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
/**
|
|
129
143
|
* One text rewrite of the `pg_node_tree` to JSON conversion: a `replace`, or
|
|
130
144
|
* a global `regexp_replace` when `isRegex` is set.
|