@supabase/postgrest-typegen 0.2.1 → 0.2.3

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.
Files changed (58) hide show
  1. package/README.md +13 -5
  2. package/dist/collation.d.ts +2 -0
  3. package/dist/collation.js +15 -0
  4. package/dist/generation/index.d.ts +1 -1
  5. package/dist/generation/python.js +21 -15
  6. package/dist/generation/swift.js +9 -2
  7. package/dist/generation/typescript.d.ts +18 -7
  8. package/dist/generation/typescript.js +102 -79
  9. package/dist/introspection/index.js +6 -23
  10. package/dist/introspection/relationships.d.ts +4 -4
  11. package/dist/introspection/relationships.js +4 -11
  12. package/dist/introspection/sql/columns.sql.d.ts +2 -7
  13. package/dist/introspection/sql/columns.sql.js +23 -17
  14. package/dist/introspection/sql/common.d.ts +7 -15
  15. package/dist/introspection/sql/foreign_tables.sql.d.ts +2 -6
  16. package/dist/introspection/sql/foreign_tables.sql.js +0 -5
  17. package/dist/introspection/sql/functions.sql.d.ts +2 -5
  18. package/dist/introspection/sql/functions.sql.js +0 -33
  19. package/dist/introspection/sql/materialized_views.sql.d.ts +2 -4
  20. package/dist/introspection/sql/materialized_views.sql.js +0 -5
  21. package/dist/introspection/sql/primary_keys.sql.d.ts +2 -4
  22. package/dist/introspection/sql/primary_keys.sql.js +0 -4
  23. package/dist/introspection/sql/schemas.sql.d.ts +2 -6
  24. package/dist/introspection/sql/schemas.sql.js +2 -6
  25. package/dist/introspection/sql/table.sql.d.ts +2 -4
  26. package/dist/introspection/sql/table.sql.js +0 -5
  27. package/dist/introspection/sql/table_relationships.sql.d.ts +2 -2
  28. package/dist/introspection/sql/types.sql.d.ts +5 -5
  29. package/dist/introspection/sql/types.sql.js +6 -17
  30. package/dist/introspection/sql/views.sql.d.ts +2 -4
  31. package/dist/introspection/sql/views.sql.js +0 -5
  32. package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -2
  33. package/dist/introspection/sql/views_key_dependencies.sql.js +64 -75
  34. package/dist/sort.js +32 -16
  35. package/dist/types.d.ts +4 -4
  36. package/dist/types.js +6 -4
  37. package/package.json +4 -4
  38. package/src/collation.ts +16 -0
  39. package/src/generation/index.ts +1 -0
  40. package/src/generation/python.ts +28 -15
  41. package/src/generation/swift.ts +9 -2
  42. package/src/generation/typescript.ts +161 -104
  43. package/src/introspection/index.ts +7 -32
  44. package/src/introspection/relationships.ts +10 -19
  45. package/src/introspection/sql/columns.sql.ts +25 -26
  46. package/src/introspection/sql/common.ts +7 -17
  47. package/src/introspection/sql/foreign_tables.sql.ts +2 -13
  48. package/src/introspection/sql/functions.sql.ts +2 -44
  49. package/src/introspection/sql/materialized_views.sql.ts +2 -11
  50. package/src/introspection/sql/primary_keys.sql.ts +2 -10
  51. package/src/introspection/sql/schemas.sql.ts +4 -14
  52. package/src/introspection/sql/table.sql.ts +2 -11
  53. package/src/introspection/sql/table_relationships.sql.ts +2 -4
  54. package/src/introspection/sql/types.sql.ts +6 -26
  55. package/src/introspection/sql/views.sql.ts +2 -11
  56. package/src/introspection/sql/views_key_dependencies.sql.ts +83 -77
  57. package/src/sort.ts +37 -18
  58. package/src/types.ts +6 -4
package/README.md CHANGED
@@ -17,11 +17,19 @@ JSON output, not inside `postgrest-typegen`.
17
17
 
18
18
  The TypeScript, Go, Python, and Swift generators bundled here are a
19
19
  **deliberate transition**, not the target architecture for new languages.
20
- They were ported byte-parity from postgres-meta's own templates so
21
- `supabase gen types` keeps working unchanged while postgres-meta's copies get
22
- deprecated in favor of this package. It's decided that all four eventually
23
- move out to their own SDK repos too (tracked in SDK-1641), not yet scheduled
24
- — sequenced after postgres-meta's cutover to this package settles.
20
+ They were ported byte-for-byte from postgres-meta's own templates so
21
+ `supabase gen types` kept working unchanged; postgres-meta has since deleted
22
+ its copies and consumes this package instead. It's decided that all four
23
+ eventually move out to their own SDK repos too (tracked in SDK-1641), not yet
24
+ scheduled — sequenced after postgres-meta's cutover to this package settles.
25
+
26
+ > [!IMPORTANT]
27
+ > **We are no longer accepting contributions that fix the bundled generators.**
28
+ > Because each of the four is moving out to its own language's SDK repository,
29
+ > a fix landed here would have to be made a second time in the destination
30
+ > repository, and postgres-meta and the CLI only pick it up once they bump
31
+ > their pinned version of this package. Please open an issue describing the
32
+ > bug instead, so it can be tracked and carried across the move.
25
33
 
26
34
  ## Design
27
35
 
@@ -0,0 +1,2 @@
1
+ /** Compare two strings with the pinned English collation. */
2
+ export declare function compareStrings(a: string, b: string): number;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The one string comparison every ordering in this package uses.
3
+ *
4
+ * Generated output is ordered by name, and `String.prototype.localeCompare`
5
+ * without a locale follows the host's default locale, so the same database
6
+ * produced differently ordered code on differently configured machines (a
7
+ * Swedish host sorts `ä` after `z`, an English one next to `a`). Pinning the
8
+ * collator to English keeps the order postgres-meta's output has on the
9
+ * English CI runners and makes it the same everywhere else.
10
+ */
11
+ const collator = new Intl.Collator("en");
12
+ /** Compare two strings with the pinned English collation. */
13
+ export function compareStrings(a, b) {
14
+ return collator.compare(a, b);
15
+ }
@@ -1,5 +1,5 @@
1
1
  export { sortGeneratorMetadata } from "../sort.ts";
2
2
  export { generateGo } from "./go.ts";
3
3
  export { generatePython } from "./python.ts";
4
- export { generateTypescript, type GenerateTypescriptOptions, pgTypeToTsType, } from "./typescript.ts";
4
+ export { generateTypescript, type GenerateTypescriptOptions, pgTypeToTsType, type TypeResolutionContext, } from "./typescript.ts";
5
5
  export { type AccessControl, generateSwift, type GenerateSwiftOptions, } from "./swift.ts";
@@ -53,7 +53,7 @@ class PythonContext {
53
53
  schemas;
54
54
  constructor(types, columns, schemas) {
55
55
  this.schemas = Object.fromEntries(schemas.map((schema) => [schema.name, schema]));
56
- this.types = Object.fromEntries(types.map((type) => [type.name, type]));
56
+ this.types = Object.fromEntries(types.map((type) => [qualifiedTypeName(type.schema, type.name), type]));
57
57
  this.columns = columns.reduce((acc, curr) => {
58
58
  acc[curr.table_id] ??= [];
59
59
  acc[curr.table_id].push(curr);
@@ -61,30 +61,35 @@ class PythonContext {
61
61
  }, {});
62
62
  this.user_enums = Object.fromEntries(types
63
63
  .filter((type) => type.enums.length > 0)
64
- .map((type) => [type.name, new PythonEnum(type)]));
64
+ .map((type) => [
65
+ qualifiedTypeName(type.schema, type.name),
66
+ new PythonEnum(type),
67
+ ]));
65
68
  }
66
- resolveTypeName(name) {
67
- if (name in this.user_enums) {
68
- return this.user_enums[name].name;
69
+ resolveTypeName(name, typeSchema) {
70
+ const qualified = qualifiedTypeName(typeSchema, name);
71
+ if (qualified in this.user_enums) {
72
+ return this.user_enums[qualified].name;
69
73
  }
70
74
  if (name in PY_TYPE_MAP) {
71
75
  return PY_TYPE_MAP[name];
72
76
  }
73
- if (name in this.types) {
74
- const type = this.types[name];
75
- const schema = type.schema;
76
- return `${formatForPyClassName(schema)}${formatForPyClassName(type.name)}`;
77
+ if (qualified in this.types) {
78
+ const type = this.types[qualified];
79
+ return `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}`;
77
80
  }
78
81
  return "Any";
79
82
  }
80
- parsePgType(pg_type) {
83
+ // Array types live in the schema of their element type, so `typeSchema`
84
+ // carries over to the element.
85
+ parsePgType(pg_type, typeSchema) {
81
86
  if (pg_type.startsWith("_")) {
82
87
  const inner_str = pg_type.slice(1);
83
- const inner = this.parsePgType(inner_str);
88
+ const inner = this.parsePgType(inner_str, typeSchema);
84
89
  return new PythonListType(inner);
85
90
  }
86
91
  else {
87
- const type_name = this.resolveTypeName(pg_type);
92
+ const type_name = this.resolveTypeName(pg_type, typeSchema);
88
93
  return new PythonSimpleType(type_name);
89
94
  }
90
95
  }
@@ -98,7 +103,7 @@ class PythonContext {
98
103
  };
99
104
  });
100
105
  const attributeEntries = attributes.map((attribute) => {
101
- const type = this.parsePgType(attribute.type.name);
106
+ const type = this.parsePgType(attribute.type.name, attribute.type.schema);
102
107
  // Composite type attributes cannot carry NOT NULL constraints in
103
108
  // Postgres, so every field is inherently nullable.
104
109
  return new PythonBaseModelAttr(attribute.name, type, true);
@@ -109,14 +114,14 @@ class PythonContext {
109
114
  columnsToClassAttrs(table_id) {
110
115
  const attrs = this.columns[table_id] ?? [];
111
116
  return attrs.map((col) => {
112
- const type = this.parsePgType(col.format);
117
+ const type = this.parsePgType(col.format, col.type_schema);
113
118
  return new PythonBaseModelAttr(col.name, type, col.is_nullable);
114
119
  });
115
120
  }
116
121
  columnsToDictAttrs(table_id, not_required) {
117
122
  const attrs = this.columns[table_id] ?? [];
118
123
  return attrs.map((col) => {
119
- const type = this.parsePgType(col.format);
124
+ const type = this.parsePgType(col.format, col.type_schema);
120
125
  return new PythonTypedDictAttr(col.name, type, col.is_nullable, not_required ||
121
126
  col.is_nullable ||
122
127
  col.is_identity ||
@@ -139,6 +144,7 @@ class PythonContext {
139
144
  return new PythonBaseModel(matview.name, this.schemas[matview.schema], attributes);
140
145
  }
141
146
  }
147
+ const qualifiedTypeName = (schema, name) => `${schema}.${name}`;
142
148
  class PythonEnum {
143
149
  name;
144
150
  variants;
@@ -1,3 +1,4 @@
1
+ import { compareStrings } from "../collation.js";
1
2
  function formatForSwiftSchemaName(schema) {
2
3
  return `${formatForSwiftTypeName(schema)}Schema`;
3
4
  }
@@ -186,10 +187,10 @@ export const generateSwift = (metadata, opts = {}) => {
186
187
  ...schemas.flatMap((schema) => {
187
188
  const schemaTables = [...tables, ...foreignTables]
188
189
  .filter((table) => table.schema === schema.name)
189
- .sort(({ name: a }, { name: b }) => a.localeCompare(b));
190
+ .sort(({ name: a }, { name: b }) => compareStrings(a, b));
190
191
  const schemaViews = [...views, ...materializedViews]
191
192
  .filter((table) => table.schema === schema.name)
192
- .sort(({ name: a }, { name: b }) => a.localeCompare(b));
193
+ .sort(({ name: a }, { name: b }) => compareStrings(a, b));
193
194
  const schemaEnums = types.filter((type) => type.schema === schema.name && type.enums.length > 0);
194
195
  const schemaCompositeTypes = types.filter((type) => type.schema === schema.name && type.attributes.length > 0);
195
196
  return [
@@ -262,6 +263,12 @@ const pgTypeToSwiftType = (pgType, nullable, { types, views, tables, }) => {
262
263
  }
263
264
  else if (["json", "jsonb"].includes(pgType)) {
264
265
  swiftType = "AnyJSON";
266
+ // `void` and `record` are pseudo-types: Postgres rejects them as a column or
267
+ // composite attribute type ("column has pseudo-type void"), and this
268
+ // generator only renders columns and attributes, never functions. So these
269
+ // two arms are unreachable today and deliberately untested. They are kept
270
+ // because they are the mappings to use if Swift ever emits functions, which
271
+ // is where these types do appear.
265
272
  }
266
273
  else if (pgType === "void") {
267
274
  swiftType = "Void";
@@ -1,4 +1,4 @@
1
- import type { GeneratorMetadata, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "../types.ts";
1
+ import type { GeneratorMetadata, PostgresForeignTable, PostgresMaterializedView, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "../types.ts";
2
2
  export interface GenerateTypescriptOptions {
3
3
  /**
4
4
  * Emit `isOneToOne` on each relationship entry. Mirrors postgres-meta's
@@ -24,6 +24,22 @@ export interface GenerateTypescriptOptions {
24
24
  */
25
25
  format?: (code: string) => Promise<string>;
26
26
  }
27
+ /**
28
+ * Collections `pgTypeToTsType` resolves a relation-typed value against.
29
+ *
30
+ * Foreign tables are generated under `Tables` and materialized views under
31
+ * `Views`, exactly like their plain counterparts, so a value typed as one has
32
+ * a `Row` to point at and belongs here. Both are optional so the exported
33
+ * `pgTypeToTsType` signature stays backwards compatible.
34
+ */
35
+ export type TypeResolutionContext = {
36
+ types: PostgresType[];
37
+ schemas: PostgresSchema[];
38
+ tables: PostgresTable[];
39
+ views: PostgresView[];
40
+ foreignTables?: PostgresForeignTable[];
41
+ materializedViews?: PostgresMaterializedView[];
42
+ };
27
43
  /**
28
44
  * Does not re-sort collections; pass input pre-sorted with
29
45
  * `sortGeneratorMetadata` first. Still merge-sorts tables+foreign tables and
@@ -31,9 +47,4 @@ export interface GenerateTypescriptOptions {
31
47
  * can't be expressed by the single-collection `sortGeneratorMetadata` pass.
32
48
  */
33
49
  export declare const generateTypescript: (metadata: GeneratorMetadata, opts?: GenerateTypescriptOptions) => Promise<string>;
34
- export declare const pgTypeToTsType: (schema: PostgresSchema, pgType: string, { types, schemas, tables, views, }: {
35
- types: PostgresType[];
36
- schemas: PostgresSchema[];
37
- tables: PostgresTable[];
38
- views: PostgresView[];
39
- }, typeSchema?: string) => string;
50
+ export declare const pgTypeToTsType: (schema: PostgresSchema, pgType: string, context: TypeResolutionContext, typeSchema?: string) => string;
@@ -5,6 +5,7 @@
5
5
  // disable comment would corrupt the emitted string.
6
6
  import { format as oxfmtFormat } from "oxfmt";
7
7
  import { VALID_FUNCTION_ARGS_MODE, VALID_UNNAMED_FUNCTION_ARG_TYPES, } from "./constants.js";
8
+ import { compareStrings } from "../collation.js";
8
9
  const defaultFormat = async (code) => {
9
10
  const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
10
11
  semi: false,
@@ -26,6 +27,16 @@ const defaultFormat = async (code) => {
26
27
  export const generateTypescript = async (metadata, opts = {}) => {
27
28
  const { schemas, tables, foreignTables, views, materializedViews, columns, relationships, functions, types, } = metadata;
28
29
  const { detectOneToOneRelationships = false, postgrestVersion, defaultSchema = "public", format = defaultFormat, } = opts;
30
+ // Every relation-typed value resolves against the same collections, so build
31
+ // the lookup context once rather than reassembling it at each call site.
32
+ const typeContext = {
33
+ types,
34
+ schemas,
35
+ tables,
36
+ views,
37
+ foreignTables,
38
+ materializedViews,
39
+ };
29
40
  // Ordering of all collections (incl. relationships, columns, args below) is
30
41
  // provided by `sortGeneratorMetadata`; this generator no longer re-sorts.
31
42
  const introspectionBySchema = Object.fromEntries(schemas.map((s) => [
@@ -178,11 +189,11 @@ export const generateTypescript = async (metadata, opts = {}) => {
178
189
  // views — so the cross-collection name sort can't be expressed by the
179
190
  // single-collection `sortGeneratorMetadata` pass and stays here.
180
191
  for (const schema in introspectionBySchema) {
181
- introspectionBySchema[schema].tables.sort((a, b) => a.table.name.localeCompare(b.table.name));
182
- introspectionBySchema[schema].views.sort((a, b) => a.view.name.localeCompare(b.view.name));
183
- introspectionBySchema[schema].functions.sort((a, b) => a.fn.name.localeCompare(b.fn.name));
184
- introspectionBySchema[schema].enums.sort((a, b) => a.name.localeCompare(b.name));
185
- introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name.localeCompare(b.name));
192
+ introspectionBySchema[schema].tables.sort((a, b) => compareStrings(a.table.name, b.table.name));
193
+ introspectionBySchema[schema].views.sort((a, b) => compareStrings(a.view.name, b.view.name));
194
+ introspectionBySchema[schema].functions.sort((a, b) => compareStrings(a.fn.name, b.fn.name));
195
+ introspectionBySchema[schema].enums.sort((a, b) => compareStrings(a.name, b.name));
196
+ introspectionBySchema[schema].compositeTypes.sort((a, b) => compareStrings(a.name, b.name));
186
197
  }
187
198
  const getFunctionTsReturnType = (fn, returnType) => {
188
199
  // Determine if this function should have SetofOptions
@@ -244,12 +255,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
244
255
  const type = typesById.get(type_id);
245
256
  let tsType = "unknown";
246
257
  if (type) {
247
- tsType = pgTypeToTsType(schema, type.name, {
248
- types,
249
- schemas,
250
- tables,
251
- views,
252
- });
258
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
253
259
  }
254
260
  return { name, type: tsType };
255
261
  });
@@ -269,24 +275,14 @@ export const generateTypescript = async (metadata, opts = {}) => {
269
275
  type_schema: column.type_schema,
270
276
  is_nullable: column.is_nullable,
271
277
  is_optional: false,
272
- }, {
273
- types,
274
- schemas,
275
- tables,
276
- views,
277
- }))
278
+ }, typeContext))
278
279
  .join(",\n")}
279
280
  }`;
280
281
  }
281
282
  // Case 3: returns base/array/composite/enum type.
282
283
  const type = typesById.get(fn.return_type_id);
283
284
  if (type) {
284
- return pgTypeToTsType(schema, type.name, {
285
- types,
286
- schemas,
287
- tables,
288
- views,
289
- });
285
+ return pgTypeToTsType(schema, type.name, typeContext, type.schema);
290
286
  }
291
287
  return "unknown";
292
288
  };
@@ -369,12 +365,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
369
365
  const type = typesById.get(type_id);
370
366
  let tsType = "unknown";
371
367
  if (type) {
372
- tsType = pgTypeToTsType(schema, type.name, {
373
- types,
374
- schemas,
375
- tables,
376
- views,
377
- });
368
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
378
369
  }
379
370
  return { name, type: tsType, has_default };
380
371
  });
@@ -389,12 +380,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
389
380
  const type = typesById.get(type_id);
390
381
  let tsType = "unknown";
391
382
  if (type) {
392
- tsType = pgTypeToTsType(schema, type.name, {
393
- types,
394
- schemas,
395
- tables,
396
- views,
397
- });
383
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
398
384
  }
399
385
  return { name, type: tsType, has_default };
400
386
  });
@@ -407,12 +393,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
407
393
  const type = typesById.get(type_id);
408
394
  let tsType = "unknown";
409
395
  if (type) {
410
- tsType = pgTypeToTsType(schema, type.name, {
411
- types,
412
- schemas,
413
- tables,
414
- views,
415
- });
396
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
416
397
  }
417
398
  return { name, type: tsType, has_default };
418
399
  });
@@ -444,6 +425,38 @@ export const generateTypescript = async (metadata, opts = {}) => {
444
425
  function generateColumnTsDefinition(schema, column, context) {
445
426
  return `${JSON.stringify(column.name)}${column.is_optional ? "?" : ""}: ${generateNullableUnionTsType(pgTypeToTsType(schema, column.format, context, column.type_schema), column.is_nullable)}`;
446
427
  }
428
+ /**
429
+ * Is `fn` a PostgREST computed field on `relation` (a table or view)?
430
+ *
431
+ * A computed field is a function taking a single argument of the relation's
432
+ * own composite type, so it is selectable as if it were a column and belongs
433
+ * in `Row`.
434
+ *
435
+ * Matching on the argument's type OID rather than on `argument_types` is what
436
+ * makes named parameters work. `argument_types` is
437
+ * `pg_get_function_arguments()`, which renders the parameter *name* when the
438
+ * parameter has one — `category category` rather than `category` — and
439
+ * schema-qualifies the type when it is not visible in `search_path`. Both
440
+ * forms failed a bare string comparison against the relation name, so
441
+ * `create function name_translated(category category)` silently lost its
442
+ * computed field while the unnamed `name_translated(category)` kept it.
443
+ * See supabase/postgres-meta#1034.
444
+ */
445
+ const isComputedFieldOf = (fn, inArgs, relation) => {
446
+ if (inArgs.length === 1) {
447
+ // `relationTypeByIds` is already "types backed by a relation", so a hit
448
+ // identifies the relation regardless of how the argument was spelled.
449
+ // A `variadic` argument is an array type and never appears here.
450
+ const argType = relationTypeByIds.get(inArgs[0].type_id);
451
+ if (argType) {
452
+ return argType.type_relation_id === relation.id;
453
+ }
454
+ }
455
+ // Fall back to the historical comparison when the argument's type is not in
456
+ // the metadata (callers may pass a reduced `types` list), so a computed
457
+ // field that resolved before keeps resolving.
458
+ return fn.argument_types === relation.name;
459
+ };
447
460
  let output = `
448
461
  export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
449
462
 
@@ -464,9 +477,9 @@ export type Database = {
464
477
  type_schema: column.type_schema,
465
478
  is_nullable: column.is_nullable,
466
479
  is_optional: false,
467
- }, { types, schemas, tables, views })),
480
+ }, typeContext)),
468
481
  ...schemaFunctions
469
- .filter(({ fn }) => fn.argument_types === table.name)
482
+ .filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, table))
470
483
  .map(({ fn }) => {
471
484
  return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`;
472
485
  }),
@@ -486,7 +499,7 @@ export type Database = {
486
499
  is_optional: column.is_nullable ||
487
500
  column.is_identity ||
488
501
  column.default_value !== null,
489
- }, { types, schemas, tables, views });
502
+ }, typeContext);
490
503
  })}
491
504
  }
492
505
  Update: {
@@ -501,7 +514,7 @@ export type Database = {
501
514
  type_schema: column.type_schema,
502
515
  is_nullable: column.is_nullable,
503
516
  is_optional: true,
504
- }, { types, schemas, tables, views });
517
+ }, typeContext);
505
518
  })}
506
519
  }
507
520
  Relationships: [
@@ -521,13 +534,16 @@ export type Database = {
521
534
  type_schema: column.type_schema,
522
535
  is_nullable: column.is_nullable,
523
536
  is_optional: false,
524
- }, { types, schemas, tables, views })),
537
+ }, typeContext)),
525
538
  ...schemaFunctions
526
- .filter(({ fn }) => fn.argument_types === view.name)
539
+ .filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, view))
527
540
  .map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
528
541
  ]}
529
542
  }
530
- ${view.is_insert_enabled
543
+ ${
544
+ // Metadata predating the trigger-aware flags (still valid
545
+ // version 1 documents) falls back to the old gate.
546
+ (view.is_insert_enabled ?? view.is_updatable)
531
547
  ? `Insert: {
532
548
  ${columnsByTableId[view.id].map((column) => {
533
549
  if (!column.is_updatable) {
@@ -539,11 +555,11 @@ export type Database = {
539
555
  type_schema: column.type_schema,
540
556
  is_nullable: true,
541
557
  is_optional: true,
542
- }, { types, schemas, tables, views });
558
+ }, typeContext);
543
559
  })}
544
560
  }
545
561
  `
546
- : ""}${view.is_update_enabled
562
+ : ""}${(view.is_update_enabled ?? view.is_updatable)
547
563
  ? `Update: {
548
564
  ${columnsByTableId[view.id].map((column) => {
549
565
  if (!column.is_updatable) {
@@ -555,7 +571,7 @@ export type Database = {
555
571
  type_schema: column.type_schema,
556
572
  is_nullable: true,
557
573
  is_optional: true,
558
- }, { types, schemas, tables, views });
574
+ }, typeContext);
559
575
  })}
560
576
  }
561
577
  `
@@ -575,8 +591,8 @@ export type Database = {
575
591
  return acc;
576
592
  }, {});
577
593
  for (const fnName in schemaFunctionsGroupedByName) {
578
- schemaFunctionsGroupedByName[fnName].sort((a, b) => a.fn.argument_types.localeCompare(b.fn.argument_types) ||
579
- a.fn.return_type.localeCompare(b.fn.return_type));
594
+ schemaFunctionsGroupedByName[fnName].sort((a, b) => compareStrings(a.fn.argument_types, b.fn.argument_types) ||
595
+ compareStrings(a.fn.return_type, b.fn.return_type));
580
596
  }
581
597
  return Object.entries(schemaFunctionsGroupedByName)
582
598
  .map(([fnName, fns]) => {
@@ -601,12 +617,7 @@ export type Database = {
601
617
  const type = typesById.get(type_id);
602
618
  let tsType = "unknown";
603
619
  if (type) {
604
- tsType = `${generateNullableUnionTsType(pgTypeToTsType(schema, type.name, {
605
- types,
606
- schemas,
607
- tables,
608
- views,
609
- }), true)}`;
620
+ tsType = `${generateNullableUnionTsType(pgTypeToTsType(schema, type.name, typeContext, type.schema), true)}`;
610
621
  }
611
622
  return `${JSON.stringify(name)}: ${tsType}`;
612
623
  })}
@@ -738,12 +749,14 @@ export const Constants = {
738
749
  return output;
739
750
  };
740
751
  // TODO: Make this more robust. Currently doesn't handle range types - returns them as unknown.
741
- export const pgTypeToTsType = (schema, pgType, { types, schemas, tables, views, },
742
- // The schema that actually owns `pgType` (a column's `type_schema`), when
743
- // known. Disambiguates same-named enums/composites across schemas; falls
744
- // back to `schema.name` (the table's own schema) when not provided, e.g.
745
- // for function args/return types where no equivalent field exists yet.
752
+ export const pgTypeToTsType = (schema, pgType, context,
753
+ // The schema that actually owns `pgType`, from the column's `type_schema` or
754
+ // the resolved `PostgresType.schema`. Disambiguates same-named enums,
755
+ // composites and relations across schemas. Falls back to `schema.name` (the
756
+ // schema being generated) when not provided, which is only a guess, so pass
757
+ // it whenever the owning schema is known.
746
758
  typeSchema) => {
759
+ const { types, schemas, tables, views, foreignTables = [], materializedViews = [], } = context;
747
760
  if (pgType === "bool") {
748
761
  return "boolean";
749
762
  }
@@ -777,7 +790,7 @@ typeSchema) => {
777
790
  return "Record<string, unknown>";
778
791
  }
779
792
  else if (pgType.startsWith("_")) {
780
- return `(${pgTypeToTsType(schema, pgType.substring(1), { types, schemas, tables, views }, typeSchema)})[]`;
793
+ return `(${pgTypeToTsType(schema, pgType.substring(1), context, typeSchema)})[]`;
781
794
  }
782
795
  else {
783
796
  const preferredSchema = typeSchema ?? schema.name;
@@ -799,21 +812,31 @@ typeSchema) => {
799
812
  }
800
813
  return "unknown";
801
814
  }
802
- const tableRowTypes = tables.filter((table) => table.name === pgType);
803
- if (tableRowTypes.length > 0) {
804
- const tableRowType = tableRowTypes.find((type) => type.schema === preferredSchema) ||
805
- tableRowTypes[0];
806
- if (schemas.some(({ name }) => name === tableRowType.schema)) {
807
- return `Database[${JSON.stringify(tableRowType.schema)}]['Tables'][${JSON.stringify(tableRowType.name)}]['Row']`;
815
+ // A relation-typed value carries only a bare type name, so several relations
816
+ // can share it and `preferredSchema` is what disambiguates them. Match on
817
+ // the schema across every relation kind before falling back to kind order,
818
+ // otherwise a relation in an unrelated schema outranks the one the caller
819
+ // meant. Foreign tables are generated under `Tables` and materialized views
820
+ // under `Views`, alongside their plain counterparts.
821
+ const relationKinds = [
822
+ ["Tables", tables],
823
+ ["Tables", foreignTables],
824
+ ["Views", views],
825
+ ["Views", materializedViews],
826
+ ];
827
+ const findRelation = (matches) => {
828
+ for (const [kind, relations] of relationKinds) {
829
+ const relation = relations.find(matches);
830
+ if (relation)
831
+ return { kind, relation };
808
832
  }
809
- return "unknown";
810
- }
811
- const viewRowTypes = views.filter((view) => view.name === pgType);
812
- if (viewRowTypes.length > 0) {
813
- const viewRowType = viewRowTypes.find((type) => type.schema === preferredSchema) ||
814
- viewRowTypes[0];
815
- if (schemas.some(({ name }) => name === viewRowType.schema)) {
816
- return `Database[${JSON.stringify(viewRowType.schema)}]['Views'][${JSON.stringify(viewRowType.name)}]['Row']`;
833
+ return undefined;
834
+ };
835
+ const match = findRelation(({ name, schema: relationSchema }) => name === pgType && relationSchema === preferredSchema) ?? findRelation(({ name }) => name === pgType);
836
+ if (match) {
837
+ const { kind, relation } = match;
838
+ if (schemas.some(({ name }) => name === relation.schema)) {
839
+ return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(relation.name)}]['Row']`;
817
840
  }
818
841
  return "unknown";
819
842
  }
@@ -22,47 +22,30 @@ import { VIEWS_SQL } from "./sql/views.sql.js";
22
22
  * responsibility — unlike `getGeneratorMetadata`, this does not end the pool.
23
23
  */
24
24
  export async function introspect(db, opts = {}) {
25
- const includedSchemas = opts.includedSchemas ?? [];
26
- const excludedSchemas = opts.excludedSchemas ?? [];
27
- // Managers receive `undefined` (not an empty array) when no filter is set.
28
- const included = includedSchemas.length > 0 ? includedSchemas : undefined;
29
- const excluded = excludedSchemas.length > 0 ? excludedSchemas : undefined;
30
25
  // Most queries exclude the system schemas by default; foreign tables and
31
26
  // materialized views do not (matching the respective manager `list()` calls).
32
- const systemExcludingFilter = filterByList(included, excluded, DEFAULT_SYSTEM_SCHEMAS);
33
- const plainFilter = filterByList(included, excluded);
27
+ const systemExcludingFilter = filterByList(opts.includedSchemas, opts.excludedSchemas, DEFAULT_SYSTEM_SCHEMAS);
28
+ const plainFilter = filterByList(opts.includedSchemas, opts.excludedSchemas);
34
29
  const queryRows = async (sql) => normalizeRows((await db.query(sql)).rows);
35
30
  // Issued concurrently, matching postgres-meta's CLI path which ran its nine
36
31
  // introspection queries under Promise.all. A pooled Queryable runs them in
37
32
  // parallel; a single-connection one pipelines them, which is no worse than
38
33
  // awaiting each in turn.
39
34
  const [schemas, tables, foreignTables, views, materializedViews, columns, primaryKeys, relationships, functions, types,] = await Promise.all([
40
- queryRows(SCHEMAS_SQL({
41
- includeSystemSchemas: false,
42
- nameFilter: systemExcludingFilter,
43
- })),
35
+ queryRows(SCHEMAS_SQL({ schemaFilter: systemExcludingFilter })),
44
36
  queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
45
37
  queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter })),
46
38
  queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter })),
47
39
  queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
48
40
  queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
49
41
  queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
50
- listRelationships(db, {
51
- includedSchemas: included,
52
- excludedSchemas: excluded,
53
- }),
42
+ listRelationships(db, systemExcludingFilter),
54
43
  queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
55
- // types: includeSystemSchemas true (no schema filter), table + array types included.
56
- queryRows(TYPES_SQL({
57
- schemaFilter: "",
58
- includeTableTypes: true,
59
- includeArrayTypes: true,
60
- })),
44
+ queryRows(TYPES_SQL),
61
45
  ]);
62
46
  return {
63
47
  version: GENERATOR_METADATA_VERSION,
64
- schemas: schemas.filter(({ name }) => !excludedSchemas.includes(name) &&
65
- (includedSchemas.length === 0 || includedSchemas.includes(name))),
48
+ schemas,
66
49
  tables,
67
50
  foreignTables,
68
51
  views,
@@ -1,5 +1,5 @@
1
1
  import type { PostgresRelationship } from "../types.ts";
2
- import type { IntrospectOptions, Queryable } from "./index.ts";
2
+ import type { Queryable } from "./index.ts";
3
3
  /**
4
4
  * Port of `postgres-meta/src/lib/PostgresMetaRelationships.ts`.
5
5
  *
@@ -39,9 +39,9 @@ export type ViewKeyDependency = {
39
39
  export declare function expandViewRelationships(allTableM2oAndO2oRelationships: PostgresRelationship[], viewsKeyDependencies: ViewKeyDependency[]): PostgresRelationship[];
40
40
  /**
41
41
  * List all relationships (table↔table plus the view-derived ones) for the
42
- * given schema filter. Mirrors `PostgresMetaRelationships.list()` with the
43
- * generator path's defaults (`includeSystemSchemas: false`). Errors from the
42
+ * schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
43
+ * `introspect()` builds with the system schemas excluded. Errors from the
44
44
  * injected `Queryable` propagate by throwing — callers adapt.
45
45
  */
46
- export declare function listRelationships(db: Queryable, { includedSchemas, excludedSchemas }?: IntrospectOptions): Promise<PostgresRelationship[]>;
46
+ export declare function listRelationships(db: Queryable, schemaFilter: string): Promise<PostgresRelationship[]>;
47
47
  export {};