@supabase/postgrest-typegen 0.2.1 → 0.2.2

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 (56) hide show
  1. package/README.md +8 -0
  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/swift.js +9 -2
  6. package/dist/generation/typescript.d.ts +18 -7
  7. package/dist/generation/typescript.js +102 -79
  8. package/dist/introspection/index.js +6 -23
  9. package/dist/introspection/relationships.d.ts +4 -4
  10. package/dist/introspection/relationships.js +4 -11
  11. package/dist/introspection/sql/columns.sql.d.ts +2 -7
  12. package/dist/introspection/sql/columns.sql.js +23 -17
  13. package/dist/introspection/sql/common.d.ts +7 -15
  14. package/dist/introspection/sql/foreign_tables.sql.d.ts +2 -6
  15. package/dist/introspection/sql/foreign_tables.sql.js +0 -5
  16. package/dist/introspection/sql/functions.sql.d.ts +2 -5
  17. package/dist/introspection/sql/functions.sql.js +0 -33
  18. package/dist/introspection/sql/materialized_views.sql.d.ts +2 -4
  19. package/dist/introspection/sql/materialized_views.sql.js +0 -5
  20. package/dist/introspection/sql/primary_keys.sql.d.ts +2 -4
  21. package/dist/introspection/sql/primary_keys.sql.js +0 -4
  22. package/dist/introspection/sql/schemas.sql.d.ts +2 -6
  23. package/dist/introspection/sql/schemas.sql.js +2 -6
  24. package/dist/introspection/sql/table.sql.d.ts +2 -4
  25. package/dist/introspection/sql/table.sql.js +0 -5
  26. package/dist/introspection/sql/table_relationships.sql.d.ts +2 -2
  27. package/dist/introspection/sql/types.sql.d.ts +5 -5
  28. package/dist/introspection/sql/types.sql.js +6 -17
  29. package/dist/introspection/sql/views.sql.d.ts +2 -4
  30. package/dist/introspection/sql/views.sql.js +0 -5
  31. package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -2
  32. package/dist/introspection/sql/views_key_dependencies.sql.js +64 -75
  33. package/dist/sort.js +16 -15
  34. package/dist/types.d.ts +4 -4
  35. package/dist/types.js +6 -4
  36. package/package.json +3 -3
  37. package/src/collation.ts +16 -0
  38. package/src/generation/index.ts +1 -0
  39. package/src/generation/swift.ts +9 -2
  40. package/src/generation/typescript.ts +161 -104
  41. package/src/introspection/index.ts +7 -32
  42. package/src/introspection/relationships.ts +10 -19
  43. package/src/introspection/sql/columns.sql.ts +25 -26
  44. package/src/introspection/sql/common.ts +7 -17
  45. package/src/introspection/sql/foreign_tables.sql.ts +2 -13
  46. package/src/introspection/sql/functions.sql.ts +2 -44
  47. package/src/introspection/sql/materialized_views.sql.ts +2 -11
  48. package/src/introspection/sql/primary_keys.sql.ts +2 -10
  49. package/src/introspection/sql/schemas.sql.ts +4 -14
  50. package/src/introspection/sql/table.sql.ts +2 -11
  51. package/src/introspection/sql/table_relationships.sql.ts +2 -4
  52. package/src/introspection/sql/types.sql.ts +6 -26
  53. package/src/introspection/sql/views.sql.ts +2 -11
  54. package/src/introspection/sql/views_key_dependencies.sql.ts +83 -77
  55. package/src/sort.ts +20 -15
  56. package/src/types.ts +6 -4
package/README.md CHANGED
@@ -23,6 +23,14 @@ deprecated in favor of this package. It's decided that all four eventually
23
23
  move out to their own SDK repos too (tracked in SDK-1641), not yet scheduled
24
24
  — sequenced after postgres-meta's cutover to this package settles.
25
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 it also breaks byte parity with postgres-meta until a release
31
+ > propagates. Please open an issue describing the bug instead, so it can be
32
+ > tracked and carried across the move.
33
+
26
34
  ## Design
27
35
 
28
36
  There is a hard split between **introspection** (database → metadata) and
@@ -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";
@@ -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 {};
@@ -1,4 +1,3 @@
1
- import { DEFAULT_SYSTEM_SCHEMAS, filterByList } from "./sql/helpers.js";
2
1
  import { TABLE_RELATIONSHIPS_SQL } from "./sql/table_relationships.sql.js";
3
2
  import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.js";
4
3
  /**
@@ -13,12 +12,7 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
13
12
  return allTableM2oAndO2oRelationships.flatMap((r) => {
14
13
  const expandKeyDepCols = (colDeps) => {
15
14
  const tableColumns = colDeps.map(({ table_column }) => table_column);
16
- // https://gist.github.com/ssippe/1f92625532eef28be6974f898efb23ef?permalink_comment_id=3474581#gistcomment-3474581
17
- const cartesianProduct = (allEntries) => {
18
- return allEntries.reduce((results, entries) => results
19
- .map((result) => entries.map((entry) => result.concat(entry)))
20
- .reduce((subResults, result) => subResults.concat(result), []), [[]]);
21
- };
15
+ const cartesianProduct = (lists) => lists.reduce((combinations, entries) => combinations.flatMap((combination) => entries.map((entry) => [...combination, entry])), [[]]);
22
16
  const viewColumnsPermutations = cartesianProduct(colDeps.map((cd) => cd.view_columns));
23
17
  return viewColumnsPermutations.map((viewColumns) => ({
24
18
  tableColumns,
@@ -72,12 +66,11 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
72
66
  }
73
67
  /**
74
68
  * List all relationships (table↔table plus the view-derived ones) for the
75
- * given schema filter. Mirrors `PostgresMetaRelationships.list()` with the
76
- * generator path's defaults (`includeSystemSchemas: false`). Errors from the
69
+ * schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
70
+ * `introspect()` builds with the system schemas excluded. Errors from the
77
71
  * injected `Queryable` propagate by throwing — callers adapt.
78
72
  */
79
- export async function listRelationships(db, { includedSchemas, excludedSchemas } = {}) {
80
- const schemaFilter = filterByList(includedSchemas, excludedSchemas, DEFAULT_SYSTEM_SCHEMAS);
73
+ export async function listRelationships(db, schemaFilter) {
81
74
  const { rows: allTableM2oAndO2oRelationships } = await db.query(TABLE_RELATIONSHIPS_SQL({ schemaFilter }));
82
75
  const { rows: viewsKeyDependencies } = await db.query(VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter }));
83
76
  const allViewM2oAndO2oRelationships = expandViewRelationships(allTableM2oAndO2oRelationships, viewsKeyDependencies);
@@ -1,7 +1,2 @@
1
- import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
2
- export declare const COLUMNS_SQL: (props: SQLQueryPropsWithSchemaFilter & {
3
- tableIdFilter?: string;
4
- tableIdentifierFilter?: string;
5
- columnNameFilter?: string;
6
- idsFilter?: string;
7
- }) => string;
1
+ import type { SchemaFilterProps } from "./common.ts";
2
+ export declare const COLUMNS_SQL: (props: SchemaFilterProps) => string;
@@ -1,4 +1,3 @@
1
- import { literal } from "./pg-format.js";
2
1
  export const COLUMNS_SQL = (props) => /* SQL */ `
3
2
  -- Adapted from information_schema.columns
4
3
 
@@ -36,30 +35,43 @@ SELECT
36
35
  WHEN 'd' THEN 'BY DEFAULT'
37
36
  ELSE NULL
38
37
  END AS identity_generation,
39
- a.attgenerated IN ('s') AS is_generated,
38
+ -- 's' = stored, 'v' = virtual (PostgreSQL 18+); neither accepts writes.
39
+ a.attgenerated IN ('s', 'v') AS is_generated,
40
40
  NOT (
41
41
  a.attnotnull
42
42
  OR t.typtype = 'd' AND t.typnotnull
43
43
  ) AS is_nullable,
44
44
  (
45
45
  c.relkind IN ('r', 'p')
46
- -- Columns of views made writable by INSTEAD OF triggers can be written
47
- -- through PostgREST even though the view is not auto-updatable, so they
48
- -- must not degrade to \`?: never\` in generated Insert/Update types.
49
- -- include_triggers => TRUE covers INSTEAD OF UPDATE triggers, but
50
- -- pg_column_is_updatable only considers the UPDATE event, so INSTEAD OF
51
- -- INSERT triggers (tgtype bits: 64 = INSTEAD, 4 = INSERT) are checked
52
- -- separately. Which events a view accepts is gated per view via
46
+ -- Columns of views made writable by INSTEAD OF triggers or unconditional
47
+ -- INSTEAD rules can be written through PostgREST even though the view is
48
+ -- not auto-updatable, so they must not degrade to \`?: never\` in generated
49
+ -- Insert/Update types. pg_column_is_updatable cannot express this: it
50
+ -- requires the relation to support both UPDATE and DELETE, so a view with
51
+ -- only an INSTEAD OF INSERT or only an INSTEAD OF UPDATE trigger reports
52
+ -- every column as non-updatable regardless of include_triggers. Triggers
53
+ -- and rules rewrite whole rows, so their presence makes every column
54
+ -- writable (tgtype bits: 64 = INSTEAD, 4 = INSERT, 16 = UPDATE;
55
+ -- pg_rewrite ev_type: '2' = UPDATE, '3' = INSERT; ev_qual is '<>' for
56
+ -- unconditional rules, and only unconditional INSTEAD rules make a view
57
+ -- writable). Which events a view accepts is gated per view via
53
58
  -- is_insert_enabled / is_update_enabled.
54
59
  OR c.relkind IN ('v', 'f') AND (
55
- pg_column_is_updatable(c.oid, a.attnum, TRUE)
60
+ pg_column_is_updatable(c.oid, a.attnum, FALSE)
56
61
  OR EXISTS (
57
62
  SELECT 1 FROM pg_trigger tg
58
63
  WHERE tg.tgrelid = c.oid
59
64
  AND tg.tgtype & 64 <> 0
60
- AND tg.tgtype & 4 <> 0
65
+ AND tg.tgtype & 20 <> 0
61
66
  AND NOT tg.tgisinternal
62
67
  )
68
+ OR EXISTS (
69
+ SELECT 1 FROM pg_rewrite rw
70
+ WHERE rw.ev_class = c.oid
71
+ AND rw.is_instead
72
+ AND rw.ev_type IN ('2', '3')
73
+ AND rw.ev_qual :: text = '<>'
74
+ )
63
75
  )
64
76
  ) AS is_updatable,
65
77
  uniques.table_id IS NOT NULL AS is_unique,
@@ -118,10 +130,6 @@ FROM
118
130
  ) AS check_constraints ON check_constraints.table_id = c.oid AND check_constraints.ordinal_position = a.attnum
119
131
  WHERE
120
132
  ${props.schemaFilter ? `nc.nspname ${props.schemaFilter} AND` : ""}
121
- ${props.idsFilter ? `(c.oid || '.' || a.attnum) ${props.idsFilter} AND` : ""}
122
- ${props.columnNameFilter ? `(c.relname || '.' || a.attname) ${props.columnNameFilter} AND` : ""}
123
- ${props.tableIdFilter ? `c.oid ${props.tableIdFilter} AND` : ""}
124
- ${props.tableIdentifierFilter ? `nc.nspname || '.' || c.relname ${props.tableIdentifierFilter} AND` : ""}
125
133
  NOT pg_is_other_temp_schema(nc.oid)
126
134
  AND a.attnum > 0
127
135
  AND NOT a.attisdropped
@@ -134,6 +142,4 @@ WHERE
134
142
  'SELECT, INSERT, UPDATE, REFERENCES'
135
143
  )
136
144
  )
137
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
138
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
139
145
  `;