@supabase/postgrest-typegen 0.6.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.
@@ -13,6 +13,12 @@ export interface Queryable {
13
13
  export interface IntrospectOptions {
14
14
  includedSchemas?: string[];
15
15
  excludedSchemas?: string[];
16
+ /**
17
+ * PostgREST's `db-extra-search-path` schemas. A view in a generated schema
18
+ * that selects from a view in one of them keeps the relationships of the
19
+ * underlying tables, as in PostgREST. Nothing in these schemas is generated.
20
+ */
21
+ extraSearchPath?: string[];
16
22
  }
17
23
  /**
18
24
  * Introspect a database into the `GeneratorMetadata` contract.
@@ -42,7 +42,7 @@ export async function introspect(db, opts = {}) {
42
42
  queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
43
43
  queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
44
44
  queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
45
- listRelationships(db, systemExcludingFilter),
45
+ listRelationships(db, systemExcludingFilter, opts.extraSearchPath),
46
46
  queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
47
47
  queryRows(TYPES_SQL),
48
48
  queryRows(VIEW_DEFINITIONS_SQL({ schemaFilter: systemExcludingFilter })),
@@ -10,7 +10,8 @@ import type { Queryable } from "./index.ts";
10
10
  * by mapping a base table's foreign key columns onto the view columns that
11
11
  * select them (`VIEWS_KEY_DEPENDENCIES_SQL` + the cartesian-product
12
12
  * expansion below). This second part is pure TypeScript, not SQL — ported
13
- * verbatim from upstream.
13
+ * from postgres-meta, except that the expansion sees every non-system
14
+ * schema's foreign keys, as PostgREST does (see `listRelationships`).
14
15
  *
15
16
  * Adapted from:
16
17
  * https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L392
@@ -33,15 +34,22 @@ export type ViewKeyDependency = {
33
34
  * view↔view relationships implied by the view key dependencies. Pure function
34
35
  * (no database) so the cartesian-product expansion can be unit-tested directly.
35
36
  *
36
- * Ported verbatim from `PostgresMetaRelationships.list()` (the body of the
37
- * `allViewM2oAndO2oRelationships` block).
37
+ * Ported from `PostgresMetaRelationships.list()` (the body of the
38
+ * `allViewM2oAndO2oRelationships` block). Every table relationship feeds the
39
+ * expansion, but a table→view relationship is emitted on the table itself, so
40
+ * `isTableEmitted` keeps it to the tables the caller returns relationships for.
38
41
  */
39
- export declare function expandViewRelationships(allTableM2oAndO2oRelationships: PostgresRelationship[], viewsKeyDependencies: ViewKeyDependency[]): PostgresRelationship[];
42
+ export declare function expandViewRelationships(allTableM2oAndO2oRelationships: PostgresRelationship[], viewsKeyDependencies: ViewKeyDependency[], isTableEmitted: (relationship: PostgresRelationship) => boolean): PostgresRelationship[];
40
43
  /**
41
44
  * List all relationships (table↔table plus the view-derived ones) for the
42
45
  * schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
43
- * `introspect()` builds with the system schemas excluded. Errors from the
44
- * injected `Queryable` propagate by throwing — callers adapt.
46
+ * `introspect()` builds with the system schemas excluded. Like PostgREST, view
47
+ * expansion sees the foreign keys of every non-system schema, so a view over a
48
+ * table in a schema that is not selected keeps the table's foreign keys; only
49
+ * the views and tables in the selected schemas get relationships. Views in the
50
+ * `extraSearchPath` schemas are followed to their base tables but get no
51
+ * relationships of their own. Errors from the injected `Queryable` propagate
52
+ * by throwing — callers adapt.
45
53
  */
46
- export declare function listRelationships(db: Queryable, schemaFilter: string): Promise<PostgresRelationship[]>;
54
+ export declare function listRelationships(db: Queryable, schemaFilter: string, extraSearchPath?: string[]): Promise<PostgresRelationship[]>;
47
55
  export {};
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_SYSTEM_SCHEMAS, filterByList } from "./sql/helpers.js";
1
2
  import { TABLE_RELATIONSHIPS_SQL } from "./sql/table_relationships.sql.js";
2
3
  import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.js";
3
4
  /**
@@ -5,10 +6,22 @@ import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.js"
5
6
  * view↔view relationships implied by the view key dependencies. Pure function
6
7
  * (no database) so the cartesian-product expansion can be unit-tested directly.
7
8
  *
8
- * Ported verbatim from `PostgresMetaRelationships.list()` (the body of the
9
- * `allViewM2oAndO2oRelationships` block).
9
+ * Ported from `PostgresMetaRelationships.list()` (the body of the
10
+ * `allViewM2oAndO2oRelationships` block). Every table relationship feeds the
11
+ * expansion, but a table→view relationship is emitted on the table itself, so
12
+ * `isTableEmitted` keeps it to the tables the caller returns relationships for.
10
13
  */
11
- export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKeyDependencies) {
14
+ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKeyDependencies, isTableEmitted) {
15
+ const keyDepsByConstraint = new Map();
16
+ for (const vkd of viewsKeyDependencies) {
17
+ const key = keyDependencyKey(vkd.table_schema, vkd.table_name, vkd.constraint_name, vkd.constraint_type);
18
+ let bucket = keyDepsByConstraint.get(key);
19
+ if (!bucket) {
20
+ bucket = [];
21
+ keyDepsByConstraint.set(key, bucket);
22
+ }
23
+ bucket.push(vkd);
24
+ }
12
25
  return allTableM2oAndO2oRelationships.flatMap((r) => {
13
26
  const expandKeyDepCols = (colDeps) => {
14
27
  const tableColumns = colDeps.map(({ table_column }) => table_column);
@@ -19,14 +32,8 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
19
32
  viewColumns,
20
33
  }));
21
34
  };
22
- const viewToTableKeyDeps = viewsKeyDependencies.filter((vkd) => vkd.table_schema === r.schema &&
23
- vkd.table_name === r.relation &&
24
- vkd.constraint_name === r.foreign_key_name &&
25
- vkd.constraint_type === "f");
26
- const tableToViewKeyDeps = viewsKeyDependencies.filter((vkd) => vkd.table_schema === r.referenced_schema &&
27
- vkd.table_name === r.referenced_relation &&
28
- vkd.constraint_name === r.foreign_key_name &&
29
- vkd.constraint_type === "f_ref");
35
+ const viewToTableKeyDeps = keyDepsByConstraint.get(keyDependencyKey(r.schema, r.relation, r.foreign_key_name, "f")) ?? [];
36
+ const tableToViewKeyDeps = keyDepsByConstraint.get(keyDependencyKey(r.referenced_schema, r.referenced_relation, r.foreign_key_name, "f_ref")) ?? [];
30
37
  const viewToTableRelationships = viewToTableKeyDeps.flatMap((vtkd) => expandKeyDepCols(vtkd.column_dependencies).map(({ viewColumns }) => ({
31
38
  foreign_key_name: r.foreign_key_name,
32
39
  schema: vtkd.view_schema,
@@ -37,7 +44,7 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
37
44
  referenced_relation: r.referenced_relation,
38
45
  referenced_columns: r.referenced_columns,
39
46
  })));
40
- const tableToViewRelationships = tableToViewKeyDeps.flatMap((tvkd) => expandKeyDepCols(tvkd.column_dependencies).map(({ viewColumns }) => ({
47
+ const tableToViewRelationships = (isTableEmitted(r) ? tableToViewKeyDeps : []).flatMap((tvkd) => expandKeyDepCols(tvkd.column_dependencies).map(({ viewColumns }) => ({
41
48
  foreign_key_name: r.foreign_key_name,
42
49
  schema: r.schema,
43
50
  relation: r.relation,
@@ -67,12 +74,27 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
67
74
  /**
68
75
  * List all relationships (table↔table plus the view-derived ones) for the
69
76
  * schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
70
- * `introspect()` builds with the system schemas excluded. Errors from the
71
- * injected `Queryable` propagate by throwing — callers adapt.
77
+ * `introspect()` builds with the system schemas excluded. Like PostgREST, view
78
+ * expansion sees the foreign keys of every non-system schema, so a view over a
79
+ * table in a schema that is not selected keeps the table's foreign keys; only
80
+ * the views and tables in the selected schemas get relationships. Views in the
81
+ * `extraSearchPath` schemas are followed to their base tables but get no
82
+ * relationships of their own. Errors from the injected `Queryable` propagate
83
+ * by throwing — callers adapt.
72
84
  */
73
- export async function listRelationships(db, schemaFilter) {
74
- const { rows: allTableM2oAndO2oRelationships } = await db.query(TABLE_RELATIONSHIPS_SQL({ schemaFilter }));
75
- const { rows: viewsKeyDependencies } = await db.query(VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter }));
76
- const allViewM2oAndO2oRelationships = expandViewRelationships(allTableM2oAndO2oRelationships, viewsKeyDependencies);
77
- return allTableM2oAndO2oRelationships.concat(allViewM2oAndO2oRelationships);
85
+ export async function listRelationships(db, schemaFilter, extraSearchPath = []) {
86
+ const tableRelationshipsQuery = db.query(TABLE_RELATIONSHIPS_SQL({ schemaFilter }));
87
+ const [{ rows: tableRelationships }, { rows: allTableRelationships }, { rows: viewsKeyDependencies },] = await Promise.all([
88
+ tableRelationshipsQuery,
89
+ schemaFilter === NON_SYSTEM_SCHEMAS_FILTER
90
+ ? tableRelationshipsQuery
91
+ : db.query(TABLE_RELATIONSHIPS_SQL({ schemaFilter: NON_SYSTEM_SCHEMAS_FILTER })),
92
+ db.query(VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter, extraSearchPath })),
93
+ ]);
94
+ const emittedKeys = new Set(tableRelationships.map(relationshipKey));
95
+ const allViewM2oAndO2oRelationships = expandViewRelationships(allTableRelationships, viewsKeyDependencies, (r) => emittedKeys.has(relationshipKey(r)));
96
+ return tableRelationships.concat(allViewM2oAndO2oRelationships);
78
97
  }
98
+ const NON_SYSTEM_SCHEMAS_FILTER = filterByList(undefined, undefined, DEFAULT_SYSTEM_SCHEMAS);
99
+ const relationshipKey = (r) => JSON.stringify([r.schema, r.relation, r.foreign_key_name]);
100
+ const keyDependencyKey = (tableSchema, tableName, constraintName, constraintType) => JSON.stringify([tableSchema, tableName, constraintName, constraintType]);
@@ -1,2 +1,4 @@
1
1
  import type { SchemaFilterProps } from "./common.ts";
2
- export declare const VIEWS_KEY_DEPENDENCIES_SQL: (props: SchemaFilterProps) => string;
2
+ export declare const VIEWS_KEY_DEPENDENCIES_SQL: (props: SchemaFilterProps & {
3
+ extraSearchPath?: string[];
4
+ }) => string;
@@ -1,3 +1,4 @@
1
+ import { filterByList } from "./helpers.js";
1
2
  import { literal } from "./pg-format.js";
2
3
  export const VIEWS_KEY_DEPENDENCIES_SQL = (props) => /* SQL */ `
3
4
  -- Adapted from
@@ -27,7 +28,6 @@ pks_fks as (
27
28
  from pg_constraint
28
29
  left join lateral unnest(confkey) with ordinality as _(col, ord) on true
29
30
  where contype='f'
30
- ${props.schemaFilter ? `and connamespace::regnamespace::text ${props.schemaFilter}` : ""}
31
31
  ),
32
32
  views as (
33
33
  select
@@ -39,7 +39,7 @@ views as (
39
39
  join pg_namespace n on n.oid = c.relnamespace
40
40
  join pg_rewrite r on r.ev_class = c.oid
41
41
  where c.relkind in ('v', 'm')
42
- ${props.schemaFilter ? `and n.nspname ${props.schemaFilter}` : ""}
42
+ ${viewSchemaFilter(props)}
43
43
  ),
44
44
  transform_json as (
45
45
  select
@@ -120,6 +120,18 @@ group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conn
120
120
  -- make sure we only return key for which all columns are referenced in the view - no partial PKs or FKs
121
121
  having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)
122
122
  `;
123
+ /**
124
+ * Like PostgREST, the views followed to their base tables also include the
125
+ * `db-extra-search-path` schemas; the recursion still starts from the selected
126
+ * schemas only.
127
+ */
128
+ function viewSchemaFilter({ schemaFilter, extraSearchPath = [], }) {
129
+ if (!schemaFilter)
130
+ return "";
131
+ if (extraSearchPath.length === 0)
132
+ return `and n.nspname ${schemaFilter}`;
133
+ return `and (n.nspname ${schemaFilter} or n.nspname ${filterByList(extraSearchPath)})`;
134
+ }
123
135
  /**
124
136
  * The rewrites, innermost first, that turn the `pg_node_tree` text of a view
125
137
  * definition into JSON exposing only `targetList`, `resno`, `resorigtbl` and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/postgrest-typegen",
3
- "version": "0.6.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"
@@ -31,6 +31,12 @@ export interface Queryable {
31
31
  export interface IntrospectOptions {
32
32
  includedSchemas?: string[];
33
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[];
34
40
  }
35
41
 
36
42
  /**
@@ -83,7 +89,7 @@ export async function introspect(
83
89
  queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
84
90
  queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
85
91
  queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
86
- listRelationships(db, systemExcludingFilter),
92
+ listRelationships(db, systemExcludingFilter, opts.extraSearchPath),
87
93
  queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
88
94
  queryRows(TYPES_SQL),
89
95
  queryRows(VIEW_DEFINITIONS_SQL({ schemaFilter: systemExcludingFilter })),
@@ -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]);
@@ -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.