@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/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 relationsById = new Map([
29
+ const relations = [
28
30
  ...metadata.tables,
29
31
  ...metadata.foreignTables,
30
32
  ...metadata.views,
31
33
  ...metadata.materializedViews,
32
- ].map((relation) => [relation.id, relation]));
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
- const argType = soleInArg && relationTypesById.get(soleInArg.type_id);
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
- : [...relationsById.values()].find((relation) => relation.schema === fn.schema &&
73
- relation.name === fn.argument_types);
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
- // A sole unnamed row parameter makes the function a computed field, which
129
- // PostgREST only exposes under `/rpc` when it also returns rows.
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 = `${fn.schema}.${fn.name}`;
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 (…)` or `OUT` parameter result, in name
183
- * order; `null` for every other return type.
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 it is a computed field that
221
- * returns no rows. Generators surface the message instead of a callable
222
- * signature.
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.5.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.8"
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.87.0",
79
- "oxlint-tsgolint": "7.0.2004",
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
- return `${returnType}${overload.is_set_returning && !collapsibleSingleRow ? "[]" : ""}
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
- * verbatim from upstream.
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 verbatim from `PostgresMetaRelationships.list()` (the body of the
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 = viewsKeyDependencies.filter(
72
- (vkd) =>
73
- vkd.table_schema === r.schema &&
74
- vkd.table_name === r.relation &&
75
- vkd.constraint_name === r.foreign_key_name &&
76
- vkd.constraint_type === "f",
77
- );
78
- const tableToViewKeyDeps = viewsKeyDependencies.filter(
79
- (vkd) =>
80
- vkd.table_schema === r.referenced_schema &&
81
- vkd.table_name === r.referenced_relation &&
82
- vkd.constraint_name === r.foreign_key_name &&
83
- vkd.constraint_type === "f_ref",
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 = tableToViewKeyDeps.flatMap((tvkd) =>
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. Errors from the
143
- * injected `Queryable` propagate by throwing — callers adapt.
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 { rows: allTableM2oAndO2oRelationships } = await db.query(
177
+ const tableRelationshipsQuery = db.query(
150
178
  TABLE_RELATIONSHIPS_SQL({ schemaFilter }),
151
179
  );
152
- const { rows: viewsKeyDependencies } = await db.query(
153
- VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter }),
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
- allTableM2oAndO2oRelationships as PostgresRelationship[],
198
+ allTableRelationships as PostgresRelationship[],
158
199
  viewsKeyDependencies as ViewKeyDependency[],
200
+ (r) => emittedKeys.has(relationshipKey(r)),
159
201
  );
160
202
 
161
- return (allTableM2oAndO2oRelationships as PostgresRelationship[]).concat(
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.schemaFilter ? `and n.nspname ${props.schemaFilter}` : ""}
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.