@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
@@ -7,7 +7,9 @@ import { format as oxfmtFormat } from "oxfmt";
7
7
  import type {
8
8
  GeneratorMetadata,
9
9
  PostgresColumn,
10
+ PostgresForeignTable,
10
11
  PostgresFunction,
12
+ PostgresMaterializedView,
11
13
  PostgresSchema,
12
14
  PostgresTable,
13
15
  PostgresType,
@@ -17,6 +19,7 @@ import {
17
19
  VALID_FUNCTION_ARGS_MODE,
18
20
  VALID_UNNAMED_FUNCTION_ARG_TYPES,
19
21
  } from "./constants.ts";
22
+ import { compareStrings } from "../collation.ts";
20
23
 
21
24
  export interface GenerateTypescriptOptions {
22
25
  /**
@@ -59,6 +62,23 @@ const defaultFormat = async (code: string): Promise<string> => {
59
62
  return formatted;
60
63
  };
61
64
 
65
+ /**
66
+ * Collections `pgTypeToTsType` resolves a relation-typed value against.
67
+ *
68
+ * Foreign tables are generated under `Tables` and materialized views under
69
+ * `Views`, exactly like their plain counterparts, so a value typed as one has
70
+ * a `Row` to point at and belongs here. Both are optional so the exported
71
+ * `pgTypeToTsType` signature stays backwards compatible.
72
+ */
73
+ export type TypeResolutionContext = {
74
+ types: PostgresType[];
75
+ schemas: PostgresSchema[];
76
+ tables: PostgresTable[];
77
+ views: PostgresView[];
78
+ foreignTables?: PostgresForeignTable[];
79
+ materializedViews?: PostgresMaterializedView[];
80
+ };
81
+
62
82
  type TsRelationship = Pick<
63
83
  GeneratorMetadata["relationships"][number],
64
84
  | "foreign_key_name"
@@ -95,6 +115,16 @@ export const generateTypescript = async (
95
115
  defaultSchema = "public",
96
116
  format = defaultFormat,
97
117
  } = opts;
118
+ // Every relation-typed value resolves against the same collections, so build
119
+ // the lookup context once rather than reassembling it at each call site.
120
+ const typeContext: TypeResolutionContext = {
121
+ types,
122
+ schemas,
123
+ tables,
124
+ views,
125
+ foreignTables,
126
+ materializedViews,
127
+ };
98
128
  // Ordering of all collections (incl. relationships, columns, args below) is
99
129
  // provided by `sortGeneratorMetadata`; this generator no longer re-sorts.
100
130
  const introspectionBySchema = Object.fromEntries<{
@@ -304,19 +334,19 @@ export const generateTypescript = async (
304
334
  // single-collection `sortGeneratorMetadata` pass and stays here.
305
335
  for (const schema in introspectionBySchema) {
306
336
  introspectionBySchema[schema].tables.sort((a, b) =>
307
- a.table.name.localeCompare(b.table.name),
337
+ compareStrings(a.table.name, b.table.name),
308
338
  );
309
339
  introspectionBySchema[schema].views.sort((a, b) =>
310
- a.view.name.localeCompare(b.view.name),
340
+ compareStrings(a.view.name, b.view.name),
311
341
  );
312
342
  introspectionBySchema[schema].functions.sort((a, b) =>
313
- a.fn.name.localeCompare(b.fn.name),
343
+ compareStrings(a.fn.name, b.fn.name),
314
344
  );
315
345
  introspectionBySchema[schema].enums.sort((a, b) =>
316
- a.name.localeCompare(b.name),
346
+ compareStrings(a.name, b.name),
317
347
  );
318
348
  introspectionBySchema[schema].compositeTypes.sort((a, b) =>
319
- a.name.localeCompare(b.name),
349
+ compareStrings(a.name, b.name),
320
350
  );
321
351
  }
322
352
 
@@ -395,12 +425,7 @@ export const generateTypescript = async (
395
425
  const type = typesById.get(type_id);
396
426
  let tsType = "unknown";
397
427
  if (type) {
398
- tsType = pgTypeToTsType(schema, type.name, {
399
- types,
400
- schemas,
401
- tables,
402
- views,
403
- });
428
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
404
429
  }
405
430
  return { name, type: tsType };
406
431
  });
@@ -431,12 +456,7 @@ export const generateTypescript = async (
431
456
  is_nullable: column.is_nullable,
432
457
  is_optional: false,
433
458
  },
434
- {
435
- types,
436
- schemas,
437
- tables,
438
- views,
439
- },
459
+ typeContext,
440
460
  ),
441
461
  )
442
462
  .join(",\n")}
@@ -446,12 +466,7 @@ export const generateTypescript = async (
446
466
  // Case 3: returns base/array/composite/enum type.
447
467
  const type = typesById.get(fn.return_type_id);
448
468
  if (type) {
449
- return pgTypeToTsType(schema, type.name, {
450
- types,
451
- schemas,
452
- tables,
453
- views,
454
- });
469
+ return pgTypeToTsType(schema, type.name, typeContext, type.schema);
455
470
  }
456
471
 
457
472
  return "unknown";
@@ -567,12 +582,12 @@ export const generateTypescript = async (
567
582
  const type = typesById.get(type_id);
568
583
  let tsType = "unknown";
569
584
  if (type) {
570
- tsType = pgTypeToTsType(schema, type.name, {
571
- types,
572
- schemas,
573
- tables,
574
- views,
575
- });
585
+ tsType = pgTypeToTsType(
586
+ schema,
587
+ type.name,
588
+ typeContext,
589
+ type.schema,
590
+ );
576
591
  }
577
592
  return { name, type: tsType, has_default };
578
593
  },
@@ -591,12 +606,12 @@ export const generateTypescript = async (
591
606
  const type = typesById.get(type_id);
592
607
  let tsType = "unknown";
593
608
  if (type) {
594
- tsType = pgTypeToTsType(schema, type.name, {
595
- types,
596
- schemas,
597
- tables,
598
- views,
599
- });
609
+ tsType = pgTypeToTsType(
610
+ schema,
611
+ type.name,
612
+ typeContext,
613
+ type.schema,
614
+ );
600
615
  }
601
616
  return { name, type: tsType, has_default };
602
617
  },
@@ -615,12 +630,12 @@ export const generateTypescript = async (
615
630
  const type = typesById.get(type_id);
616
631
  let tsType = "unknown";
617
632
  if (type) {
618
- tsType = pgTypeToTsType(schema, type.name, {
619
- types,
620
- schemas,
621
- tables,
622
- views,
623
- });
633
+ tsType = pgTypeToTsType(
634
+ schema,
635
+ type.name,
636
+ typeContext,
637
+ type.schema,
638
+ );
624
639
  }
625
640
  return { name, type: tsType, has_default };
626
641
  },
@@ -666,12 +681,7 @@ export const generateTypescript = async (
666
681
  is_nullable: boolean;
667
682
  is_optional: boolean;
668
683
  },
669
- context: {
670
- types: PostgresType[];
671
- schemas: PostgresSchema[];
672
- tables: PostgresTable[];
673
- views: PostgresView[];
674
- },
684
+ context: TypeResolutionContext,
675
685
  ) {
676
686
  return `${JSON.stringify(column.name)}${column.is_optional ? "?" : ""}: ${generateNullableUnionTsType(
677
687
  pgTypeToTsType(schema, column.format, context, column.type_schema),
@@ -679,6 +689,43 @@ export const generateTypescript = async (
679
689
  )}`;
680
690
  }
681
691
 
692
+ /**
693
+ * Is `fn` a PostgREST computed field on `relation` (a table or view)?
694
+ *
695
+ * A computed field is a function taking a single argument of the relation's
696
+ * own composite type, so it is selectable as if it were a column and belongs
697
+ * in `Row`.
698
+ *
699
+ * Matching on the argument's type OID rather than on `argument_types` is what
700
+ * makes named parameters work. `argument_types` is
701
+ * `pg_get_function_arguments()`, which renders the parameter *name* when the
702
+ * parameter has one — `category category` rather than `category` — and
703
+ * schema-qualifies the type when it is not visible in `search_path`. Both
704
+ * forms failed a bare string comparison against the relation name, so
705
+ * `create function name_translated(category category)` silently lost its
706
+ * computed field while the unnamed `name_translated(category)` kept it.
707
+ * See supabase/postgres-meta#1034.
708
+ */
709
+ const isComputedFieldOf = (
710
+ fn: PostgresFunction,
711
+ inArgs: PostgresFunction["args"],
712
+ relation: { id: number; name: string },
713
+ ) => {
714
+ if (inArgs.length === 1) {
715
+ // `relationTypeByIds` is already "types backed by a relation", so a hit
716
+ // identifies the relation regardless of how the argument was spelled.
717
+ // A `variadic` argument is an array type and never appears here.
718
+ const argType = relationTypeByIds.get(inArgs[0]!.type_id);
719
+ if (argType) {
720
+ return argType.type_relation_id === relation.id;
721
+ }
722
+ }
723
+ // Fall back to the historical comparison when the argument's type is not in
724
+ // the metadata (callers may pass a reduced `types` list), so a computed
725
+ // field that resolved before keeps resolving.
726
+ return fn.argument_types === relation.name;
727
+ };
728
+
682
729
  let output = `
683
730
  export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
684
731
 
@@ -714,11 +761,13 @@ export type Database = {
714
761
  is_nullable: column.is_nullable,
715
762
  is_optional: false,
716
763
  },
717
- { types, schemas, tables, views },
764
+ typeContext,
718
765
  ),
719
766
  ),
720
767
  ...schemaFunctions
721
- .filter(({ fn }) => fn.argument_types === table.name)
768
+ .filter(({ fn, inArgs }) =>
769
+ isComputedFieldOf(fn, inArgs, table),
770
+ )
722
771
  .map(({ fn }) => {
723
772
  return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
724
773
  getFunctionReturnType(schema, fn),
@@ -747,7 +796,7 @@ export type Database = {
747
796
  column.is_identity ||
748
797
  column.default_value !== null,
749
798
  },
750
- { types, schemas, tables, views },
799
+ typeContext,
751
800
  );
752
801
  })}
753
802
  }
@@ -769,7 +818,7 @@ export type Database = {
769
818
  is_nullable: column.is_nullable,
770
819
  is_optional: true,
771
820
  },
772
- { types, schemas, tables, views },
821
+ typeContext,
773
822
  );
774
823
  })}
775
824
  }
@@ -801,11 +850,13 @@ export type Database = {
801
850
  is_nullable: column.is_nullable,
802
851
  is_optional: false,
803
852
  },
804
- { types, schemas, tables, views },
853
+ typeContext,
805
854
  ),
806
855
  ),
807
856
  ...schemaFunctions
808
- .filter(({ fn }) => fn.argument_types === view.name)
857
+ .filter(({ fn, inArgs }) =>
858
+ isComputedFieldOf(fn, inArgs, view),
859
+ )
809
860
  .map(
810
861
  ({ fn }) =>
811
862
  `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
@@ -816,7 +867,9 @@ export type Database = {
816
867
  ]}
817
868
  }
818
869
  ${
819
- view.is_insert_enabled
870
+ // Metadata predating the trigger-aware flags (still valid
871
+ // version 1 documents) falls back to the old gate.
872
+ (view.is_insert_enabled ?? view.is_updatable)
820
873
  ? `Insert: {
821
874
  ${columnsByTableId[view.id].map((column) => {
822
875
  if (!column.is_updatable) {
@@ -831,14 +884,14 @@ export type Database = {
831
884
  is_nullable: true,
832
885
  is_optional: true,
833
886
  },
834
- { types, schemas, tables, views },
887
+ typeContext,
835
888
  );
836
889
  })}
837
890
  }
838
891
  `
839
892
  : ""
840
893
  }${
841
- view.is_update_enabled
894
+ (view.is_update_enabled ?? view.is_updatable)
842
895
  ? `Update: {
843
896
  ${columnsByTableId[view.id].map((column) => {
844
897
  if (!column.is_updatable) {
@@ -853,7 +906,7 @@ export type Database = {
853
906
  is_nullable: true,
854
907
  is_optional: true,
855
908
  },
856
- { types, schemas, tables, views },
909
+ typeContext,
857
910
  );
858
911
  })}
859
912
  }
@@ -882,8 +935,8 @@ export type Database = {
882
935
  for (const fnName in schemaFunctionsGroupedByName) {
883
936
  schemaFunctionsGroupedByName[fnName].sort(
884
937
  (a, b) =>
885
- a.fn.argument_types.localeCompare(b.fn.argument_types) ||
886
- a.fn.return_type.localeCompare(b.fn.return_type),
938
+ compareStrings(a.fn.argument_types, b.fn.argument_types) ||
939
+ compareStrings(a.fn.return_type, b.fn.return_type),
887
940
  );
888
941
  }
889
942
 
@@ -919,12 +972,12 @@ export type Database = {
919
972
  let tsType = "unknown";
920
973
  if (type) {
921
974
  tsType = `${generateNullableUnionTsType(
922
- pgTypeToTsType(schema, type.name, {
923
- types,
924
- schemas,
925
- tables,
926
- views,
927
- }),
975
+ pgTypeToTsType(
976
+ schema,
977
+ type.name,
978
+ typeContext,
979
+ type.schema,
980
+ ),
928
981
  true,
929
982
  )}`;
930
983
  }
@@ -1070,23 +1123,22 @@ export const Constants = {
1070
1123
  export const pgTypeToTsType = (
1071
1124
  schema: PostgresSchema,
1072
1125
  pgType: string,
1073
- {
1126
+ context: TypeResolutionContext,
1127
+ // The schema that actually owns `pgType`, from the column's `type_schema` or
1128
+ // the resolved `PostgresType.schema`. Disambiguates same-named enums,
1129
+ // composites and relations across schemas. Falls back to `schema.name` (the
1130
+ // schema being generated) when not provided, which is only a guess, so pass
1131
+ // it whenever the owning schema is known.
1132
+ typeSchema?: string,
1133
+ ): string => {
1134
+ const {
1074
1135
  types,
1075
1136
  schemas,
1076
1137
  tables,
1077
1138
  views,
1078
- }: {
1079
- types: PostgresType[];
1080
- schemas: PostgresSchema[];
1081
- tables: PostgresTable[];
1082
- views: PostgresView[];
1083
- },
1084
- // The schema that actually owns `pgType` (a column's `type_schema`), when
1085
- // known. Disambiguates same-named enums/composites across schemas; falls
1086
- // back to `schema.name` (the table's own schema) when not provided, e.g.
1087
- // for function args/return types where no equivalent field exists yet.
1088
- typeSchema?: string,
1089
- ): string => {
1139
+ foreignTables = [],
1140
+ materializedViews = [],
1141
+ } = context;
1090
1142
  if (pgType === "bool") {
1091
1143
  return "boolean";
1092
1144
  } else if (
@@ -1118,12 +1170,7 @@ export const pgTypeToTsType = (
1118
1170
  } else if (pgType === "record") {
1119
1171
  return "Record<string, unknown>";
1120
1172
  } else if (pgType.startsWith("_")) {
1121
- return `(${pgTypeToTsType(
1122
- schema,
1123
- pgType.substring(1),
1124
- { types, schemas, tables, views },
1125
- typeSchema,
1126
- )})[]`;
1173
+ return `(${pgTypeToTsType(schema, pgType.substring(1), context, typeSchema)})[]`;
1127
1174
  } else {
1128
1175
  const preferredSchema = typeSchema ?? schema.name;
1129
1176
  const enumTypes = types.filter(
@@ -1156,27 +1203,37 @@ export const pgTypeToTsType = (
1156
1203
  return "unknown";
1157
1204
  }
1158
1205
 
1159
- const tableRowTypes = tables.filter((table) => table.name === pgType);
1160
- if (tableRowTypes.length > 0) {
1161
- const tableRowType =
1162
- tableRowTypes.find((type) => type.schema === preferredSchema) ||
1163
- tableRowTypes[0];
1164
- if (schemas.some(({ name }) => name === tableRowType.schema)) {
1165
- return `Database[${JSON.stringify(tableRowType.schema)}]['Tables'][${JSON.stringify(
1166
- tableRowType.name,
1167
- )}]['Row']`;
1206
+ // A relation-typed value carries only a bare type name, so several relations
1207
+ // can share it and `preferredSchema` is what disambiguates them. Match on
1208
+ // the schema across every relation kind before falling back to kind order,
1209
+ // otherwise a relation in an unrelated schema outranks the one the caller
1210
+ // meant. Foreign tables are generated under `Tables` and materialized views
1211
+ // under `Views`, alongside their plain counterparts.
1212
+ const relationKinds = [
1213
+ ["Tables", tables],
1214
+ ["Tables", foreignTables],
1215
+ ["Views", views],
1216
+ ["Views", materializedViews],
1217
+ ] as const;
1218
+ const findRelation = (
1219
+ matches: (relation: { name: string; schema: string }) => boolean,
1220
+ ) => {
1221
+ for (const [kind, relations] of relationKinds) {
1222
+ const relation = relations.find(matches);
1223
+ if (relation) return { kind, relation };
1168
1224
  }
1169
- return "unknown";
1170
- }
1171
-
1172
- const viewRowTypes = views.filter((view) => view.name === pgType);
1173
- if (viewRowTypes.length > 0) {
1174
- const viewRowType =
1175
- viewRowTypes.find((type) => type.schema === preferredSchema) ||
1176
- viewRowTypes[0];
1177
- if (schemas.some(({ name }) => name === viewRowType.schema)) {
1178
- return `Database[${JSON.stringify(viewRowType.schema)}]['Views'][${JSON.stringify(
1179
- viewRowType.name,
1225
+ return undefined;
1226
+ };
1227
+ const match =
1228
+ findRelation(
1229
+ ({ name, schema: relationSchema }) =>
1230
+ name === pgType && relationSchema === preferredSchema,
1231
+ ) ?? findRelation(({ name }) => name === pgType);
1232
+ if (match) {
1233
+ const { kind, relation } = match;
1234
+ if (schemas.some(({ name }) => name === relation.schema)) {
1235
+ return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(
1236
+ relation.name,
1180
1237
  )}]['Row']`;
1181
1238
  }
1182
1239
  return "unknown";
@@ -44,20 +44,14 @@ export async function introspect(
44
44
  db: Queryable,
45
45
  opts: IntrospectOptions = {},
46
46
  ): Promise<GeneratorMetadata> {
47
- const includedSchemas = opts.includedSchemas ?? [];
48
- const excludedSchemas = opts.excludedSchemas ?? [];
49
- // Managers receive `undefined` (not an empty array) when no filter is set.
50
- const included = includedSchemas.length > 0 ? includedSchemas : undefined;
51
- const excluded = excludedSchemas.length > 0 ? excludedSchemas : undefined;
52
-
53
47
  // Most queries exclude the system schemas by default; foreign tables and
54
48
  // materialized views do not (matching the respective manager `list()` calls).
55
49
  const systemExcludingFilter = filterByList(
56
- included,
57
- excluded,
50
+ opts.includedSchemas,
51
+ opts.excludedSchemas,
58
52
  DEFAULT_SYSTEM_SCHEMAS,
59
53
  );
60
- const plainFilter = filterByList(included, excluded);
54
+ const plainFilter = filterByList(opts.includedSchemas, opts.excludedSchemas);
61
55
 
62
56
  const queryRows = async (sql: string) =>
63
57
  normalizeRows((await db.query(sql)).rows);
@@ -78,40 +72,21 @@ export async function introspect(
78
72
  functions,
79
73
  types,
80
74
  ] = await Promise.all([
81
- queryRows(
82
- SCHEMAS_SQL({
83
- includeSystemSchemas: false,
84
- nameFilter: systemExcludingFilter,
85
- }),
86
- ),
75
+ queryRows(SCHEMAS_SQL({ schemaFilter: systemExcludingFilter })),
87
76
  queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
88
77
  queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter })),
89
78
  queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter })),
90
79
  queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
91
80
  queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
92
81
  queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
93
- listRelationships(db, {
94
- includedSchemas: included,
95
- excludedSchemas: excluded,
96
- }),
82
+ listRelationships(db, systemExcludingFilter),
97
83
  queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
98
- // types: includeSystemSchemas true (no schema filter), table + array types included.
99
- queryRows(
100
- TYPES_SQL({
101
- schemaFilter: "",
102
- includeTableTypes: true,
103
- includeArrayTypes: true,
104
- }),
105
- ),
84
+ queryRows(TYPES_SQL),
106
85
  ]);
107
86
 
108
87
  return {
109
88
  version: GENERATOR_METADATA_VERSION,
110
- schemas: schemas.filter(
111
- ({ name }) =>
112
- !excludedSchemas.includes(name) &&
113
- (includedSchemas.length === 0 || includedSchemas.includes(name)),
114
- ),
89
+ schemas,
115
90
  tables,
116
91
  foreignTables,
117
92
  views,
@@ -1,6 +1,5 @@
1
1
  import type { PostgresRelationship } from "../types.ts";
2
- import type { IntrospectOptions, Queryable } from "./index.ts";
3
- import { DEFAULT_SYSTEM_SCHEMAS, filterByList } from "./sql/helpers.ts";
2
+ import type { Queryable } from "./index.ts";
4
3
  import { TABLE_RELATIONSHIPS_SQL } from "./sql/table_relationships.sql.ts";
5
4
  import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.ts";
6
5
 
@@ -52,16 +51,14 @@ export function expandViewRelationships(
52
51
  colDeps: ColDep[],
53
52
  ): { tableColumns: string[]; viewColumns: string[] }[] => {
54
53
  const tableColumns = colDeps.map(({ table_column }) => table_column);
55
- // https://gist.github.com/ssippe/1f92625532eef28be6974f898efb23ef?permalink_comment_id=3474581#gistcomment-3474581
56
- const cartesianProduct = <T>(allEntries: T[][]): T[][] => {
57
- return allEntries.reduce<T[][]>(
58
- (results, entries) =>
59
- results
60
- .map((result) => entries.map((entry) => result.concat(entry)))
61
- .reduce((subResults, result) => subResults.concat(result), []),
54
+ const cartesianProduct = <T>(lists: T[][]): T[][] =>
55
+ lists.reduce<T[][]>(
56
+ (combinations, entries) =>
57
+ combinations.flatMap((combination) =>
58
+ entries.map((entry) => [...combination, entry]),
59
+ ),
62
60
  [[]],
63
61
  );
64
- };
65
62
  const viewColumnsPermutations = cartesianProduct(
66
63
  colDeps.map((cd) => cd.view_columns),
67
64
  );
@@ -141,20 +138,14 @@ export function expandViewRelationships(
141
138
 
142
139
  /**
143
140
  * List all relationships (table↔table plus the view-derived ones) for the
144
- * given schema filter. Mirrors `PostgresMetaRelationships.list()` with the
145
- * generator path's defaults (`includeSystemSchemas: false`). Errors from the
141
+ * schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
142
+ * `introspect()` builds with the system schemas excluded. Errors from the
146
143
  * injected `Queryable` propagate by throwing — callers adapt.
147
144
  */
148
145
  export async function listRelationships(
149
146
  db: Queryable,
150
- { includedSchemas, excludedSchemas }: IntrospectOptions = {},
147
+ schemaFilter: string,
151
148
  ): Promise<PostgresRelationship[]> {
152
- const schemaFilter = filterByList(
153
- includedSchemas,
154
- excludedSchemas,
155
- DEFAULT_SYSTEM_SCHEMAS,
156
- );
157
-
158
149
  const { rows: allTableM2oAndO2oRelationships } = await db.query(
159
150
  TABLE_RELATIONSHIPS_SQL({ schemaFilter }),
160
151
  );
@@ -1,14 +1,6 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
3
2
 
4
- export const COLUMNS_SQL = (
5
- props: SQLQueryPropsWithSchemaFilter & {
6
- tableIdFilter?: string;
7
- tableIdentifierFilter?: string;
8
- columnNameFilter?: string;
9
- idsFilter?: string;
10
- },
11
- ) => /* SQL */ `
3
+ export const COLUMNS_SQL = (props: SchemaFilterProps) => /* SQL */ `
12
4
  -- Adapted from information_schema.columns
13
5
 
14
6
  SELECT
@@ -45,30 +37,43 @@ SELECT
45
37
  WHEN 'd' THEN 'BY DEFAULT'
46
38
  ELSE NULL
47
39
  END AS identity_generation,
48
- a.attgenerated IN ('s') AS is_generated,
40
+ -- 's' = stored, 'v' = virtual (PostgreSQL 18+); neither accepts writes.
41
+ a.attgenerated IN ('s', 'v') AS is_generated,
49
42
  NOT (
50
43
  a.attnotnull
51
44
  OR t.typtype = 'd' AND t.typnotnull
52
45
  ) AS is_nullable,
53
46
  (
54
47
  c.relkind IN ('r', 'p')
55
- -- Columns of views made writable by INSTEAD OF triggers can be written
56
- -- through PostgREST even though the view is not auto-updatable, so they
57
- -- must not degrade to \`?: never\` in generated Insert/Update types.
58
- -- include_triggers => TRUE covers INSTEAD OF UPDATE triggers, but
59
- -- pg_column_is_updatable only considers the UPDATE event, so INSTEAD OF
60
- -- INSERT triggers (tgtype bits: 64 = INSTEAD, 4 = INSERT) are checked
61
- -- separately. Which events a view accepts is gated per view via
48
+ -- Columns of views made writable by INSTEAD OF triggers or unconditional
49
+ -- INSTEAD rules can be written through PostgREST even though the view is
50
+ -- not auto-updatable, so they must not degrade to \`?: never\` in generated
51
+ -- Insert/Update types. pg_column_is_updatable cannot express this: it
52
+ -- requires the relation to support both UPDATE and DELETE, so a view with
53
+ -- only an INSTEAD OF INSERT or only an INSTEAD OF UPDATE trigger reports
54
+ -- every column as non-updatable regardless of include_triggers. Triggers
55
+ -- and rules rewrite whole rows, so their presence makes every column
56
+ -- writable (tgtype bits: 64 = INSTEAD, 4 = INSERT, 16 = UPDATE;
57
+ -- pg_rewrite ev_type: '2' = UPDATE, '3' = INSERT; ev_qual is '<>' for
58
+ -- unconditional rules, and only unconditional INSTEAD rules make a view
59
+ -- writable). Which events a view accepts is gated per view via
62
60
  -- is_insert_enabled / is_update_enabled.
63
61
  OR c.relkind IN ('v', 'f') AND (
64
- pg_column_is_updatable(c.oid, a.attnum, TRUE)
62
+ pg_column_is_updatable(c.oid, a.attnum, FALSE)
65
63
  OR EXISTS (
66
64
  SELECT 1 FROM pg_trigger tg
67
65
  WHERE tg.tgrelid = c.oid
68
66
  AND tg.tgtype & 64 <> 0
69
- AND tg.tgtype & 4 <> 0
67
+ AND tg.tgtype & 20 <> 0
70
68
  AND NOT tg.tgisinternal
71
69
  )
70
+ OR EXISTS (
71
+ SELECT 1 FROM pg_rewrite rw
72
+ WHERE rw.ev_class = c.oid
73
+ AND rw.is_instead
74
+ AND rw.ev_type IN ('2', '3')
75
+ AND rw.ev_qual :: text = '<>'
76
+ )
72
77
  )
73
78
  ) AS is_updatable,
74
79
  uniques.table_id IS NOT NULL AS is_unique,
@@ -127,10 +132,6 @@ FROM
127
132
  ) AS check_constraints ON check_constraints.table_id = c.oid AND check_constraints.ordinal_position = a.attnum
128
133
  WHERE
129
134
  ${props.schemaFilter ? `nc.nspname ${props.schemaFilter} AND` : ""}
130
- ${props.idsFilter ? `(c.oid || '.' || a.attnum) ${props.idsFilter} AND` : ""}
131
- ${props.columnNameFilter ? `(c.relname || '.' || a.attname) ${props.columnNameFilter} AND` : ""}
132
- ${props.tableIdFilter ? `c.oid ${props.tableIdFilter} AND` : ""}
133
- ${props.tableIdentifierFilter ? `nc.nspname || '.' || c.relname ${props.tableIdentifierFilter} AND` : ""}
134
135
  NOT pg_is_other_temp_schema(nc.oid)
135
136
  AND a.attnum > 0
136
137
  AND NOT a.attisdropped
@@ -143,6 +144,4 @@ WHERE
143
144
  'SELECT, INSERT, UPDATE, REFERENCES'
144
145
  )
145
146
  )
146
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
147
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
148
147
  `;