@supabase/postgrest-typegen 0.1.0 → 0.2.1

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 (40) hide show
  1. package/README.md +4 -0
  2. package/dist/generation/go.js +30 -2
  3. package/dist/generation/python.js +24 -10
  4. package/dist/generation/swift.js +43 -1
  5. package/dist/generation/typescript.d.ts +7 -0
  6. package/dist/generation/typescript.js +37 -11
  7. package/dist/introspection/index.js +27 -21
  8. package/dist/introspection/sql/columns.sql.js +19 -2
  9. package/dist/introspection/sql/foreign_tables.sql.js +1 -1
  10. package/dist/introspection/sql/functions.sql.js +1 -1
  11. package/dist/introspection/sql/helpers.d.ts +1 -1
  12. package/dist/introspection/sql/helpers.js +2 -2
  13. package/dist/introspection/sql/materialized_views.sql.js +1 -1
  14. package/dist/introspection/sql/pg-format.d.ts +21 -0
  15. package/dist/introspection/sql/pg-format.js +92 -0
  16. package/dist/introspection/sql/primary_keys.sql.js +1 -1
  17. package/dist/introspection/sql/schemas.sql.js +1 -1
  18. package/dist/introspection/sql/table.sql.js +1 -1
  19. package/dist/introspection/sql/types.sql.js +1 -1
  20. package/dist/introspection/sql/views.sql.js +7 -1
  21. package/dist/types.d.ts +4 -0
  22. package/dist/types.js +9 -0
  23. package/package.json +2 -5
  24. package/src/generation/go.ts +31 -2
  25. package/src/generation/python.ts +25 -10
  26. package/src/generation/swift.ts +44 -1
  27. package/src/generation/typescript.ts +53 -10
  28. package/src/introspection/index.ts +41 -38
  29. package/src/introspection/sql/columns.sql.ts +19 -2
  30. package/src/introspection/sql/foreign_tables.sql.ts +1 -1
  31. package/src/introspection/sql/functions.sql.ts +1 -1
  32. package/src/introspection/sql/helpers.ts +2 -2
  33. package/src/introspection/sql/materialized_views.sql.ts +1 -1
  34. package/src/introspection/sql/pg-format.ts +101 -0
  35. package/src/introspection/sql/primary_keys.sql.ts +1 -1
  36. package/src/introspection/sql/schemas.sql.ts +1 -1
  37. package/src/introspection/sql/table.sql.ts +1 -1
  38. package/src/introspection/sql/types.sql.ts +1 -1
  39. package/src/introspection/sql/views.sql.ts +7 -1
  40. package/src/types.ts +9 -0
package/dist/types.d.ts CHANGED
@@ -182,6 +182,8 @@ declare const postgresViewSchema: import("arktype/internal/variants/object.ts").
182
182
  schema: string;
183
183
  name: string;
184
184
  is_updatable: boolean;
185
+ is_insert_enabled: boolean;
186
+ is_update_enabled: boolean;
185
187
  comment: string | null;
186
188
  columns?: {
187
189
  table_id: number;
@@ -250,7 +252,9 @@ export declare const generatorMetadataSchema: import("arktype/internal/variants/
250
252
  views: {
251
253
  comment: string | null;
252
254
  id: number;
255
+ is_insert_enabled: boolean;
253
256
  is_updatable: boolean;
257
+ is_update_enabled: boolean;
254
258
  name: string;
255
259
  schema: string;
256
260
  }[];
package/dist/types.js CHANGED
@@ -136,6 +136,15 @@ const postgresViewSchema = type({
136
136
  schema: "string",
137
137
  name: "string",
138
138
  is_updatable: "boolean",
139
+ /**
140
+ * Whether INSERT works through this view: auto-updatable per
141
+ * `pg_relation_is_updatable`, or made insertable by an INSTEAD OF INSERT
142
+ * trigger. Tracked separately from `is_updatable` (which mirrors
143
+ * `information_schema.views` and ignores triggers).
144
+ */
145
+ is_insert_enabled: "boolean",
146
+ /** Same as `is_insert_enabled`, for UPDATE. */
147
+ is_update_enabled: "boolean",
139
148
  comment: "string | null",
140
149
  "columns?": postgresColumnSchema.array(),
141
150
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/postgrest-typegen",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Type generation for PostgREST from PostgreSQL schemas",
5
5
  "keywords": [
6
6
  "codegen",
@@ -62,8 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "arktype": "2.2.3",
65
- "pg-format": "1.0.4",
66
- "prettier": "3.5.3"
65
+ "oxfmt": "0.65.0"
67
66
  },
68
67
  "devDependencies": {
69
68
  "@testcontainers/postgresql": "^12.1.0",
@@ -72,9 +71,7 @@
72
71
  "@types/bun": "^1.4.0",
73
72
  "@types/node": "^26.3.0",
74
73
  "@types/pg": "^8.23.1",
75
- "@types/pg-format": "^1.0.5",
76
74
  "knip": "^6.32.3",
77
- "oxfmt": "0.65.0",
78
75
  "oxlint": "1.80.0",
79
76
  "oxlint-tsgolint": "7.0.2001",
80
77
  "pg": "^8.23.0",
@@ -118,6 +118,35 @@ function formatForGoTypeName(name: string): string {
118
118
  .join("");
119
119
  }
120
120
 
121
+ /**
122
+ * Renders the `json` struct tag for a column or composite attribute name.
123
+ *
124
+ * The name is quoted with `JSON.stringify`, whose escape sequences are a
125
+ * subset of Go's, so `reflect.StructTag.Get("json")` recovers the exact name
126
+ * via `strconv.Unquote` even when it contains quotes, backslashes or control
127
+ * characters. The tag is emitted as a raw string literal like before, except
128
+ * when the name contains a backtick: Go raw literals cannot contain one, so
129
+ * such tags fall back to an interpreted literal with a second round of
130
+ * escaping. Names the marshaler cannot represent still compile and
131
+ * round-trip through `reflect.StructTag`, but `encoding/json` applies its
132
+ * own tag rules at runtime: a comma splits the name from tag options (so
133
+ * only the part before it is used as the JSON name), and a name it rejects
134
+ * outright (for example one containing a quote) falls back to the Go field
135
+ * name. That is a limitation of the struct tag convention itself, not of
136
+ * the generated source.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * formatForGoStructTag('name') // `json:"name"`
141
+ * formatForGoStructTag('a"b') // `json:"a\"b"`
142
+ * formatForGoStructTag('back`tick') // "json:\"back`tick\""
143
+ * ```
144
+ */
145
+ function formatForGoStructTag(name: string): string {
146
+ const tag = `json:${JSON.stringify(name)}`;
147
+ return tag.includes("`") ? JSON.stringify(tag) : `\`${tag}\``;
148
+ }
149
+
121
150
  function generateTableStruct(
122
151
  schema: PostgresSchema,
123
152
  table: PostgresTable | PostgresView | PostgresMaterializedView,
@@ -170,7 +199,7 @@ function generateTableStruct(
170
199
  ([formattedName, type, name]) => {
171
200
  return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(
172
201
  maxTypeLength,
173
- )} \`json:"${name}"\``;
202
+ )} ${formatForGoStructTag(name)}`;
174
203
  },
175
204
  );
176
205
 
@@ -232,7 +261,7 @@ function generateCompositeTypeStruct(
232
261
  ([formattedName, type, name]) => {
233
262
  return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(
234
263
  maxTypeLength,
235
- )} \`json:"${name}"\``;
264
+ )} ${formatForGoStructTag(name)}`;
236
265
  },
237
266
  );
238
267
 
@@ -48,13 +48,12 @@ from typing import (
48
48
  Any,
49
49
  List,
50
50
  Literal,
51
- NotRequired,
52
51
  Optional,
53
- TypeAlias,
54
52
  TypedDict,
55
53
  )
54
+ from typing_extensions import NotRequired, TypeAlias
56
55
 
57
- from pydantic import BaseModel, Field, Json
56
+ from pydantic import BaseModel, Field, JsonValue
58
57
 
59
58
  ${concatLines(Object.values(ctx.user_enums))}
60
59
 
@@ -143,7 +142,9 @@ class PythonContext {
143
142
  const attributeEntries: PythonBaseModelAttr[] = attributes.map(
144
143
  (attribute) => {
145
144
  const type = this.parsePgType(attribute.type!.name);
146
- return new PythonBaseModelAttr(attribute.name, type, false);
145
+ // Composite type attributes cannot carry NOT NULL constraints in
146
+ // Postgres, so every field is inherently nullable.
147
+ return new PythonBaseModelAttr(attribute.name, type, true);
147
148
  },
148
149
  );
149
150
 
@@ -229,7 +230,9 @@ class PythonEnum implements Serializable {
229
230
  this.variants = type.enums;
230
231
  }
231
232
  serialize(): string {
232
- const variants = this.variants.map((item) => `"${item}"`).join(", ");
233
+ const variants = this.variants
234
+ .map((item) => escapePythonString(item))
235
+ .join(", ");
233
236
  return `${this.name}: TypeAlias = Literal[${variants}]`;
234
237
  }
235
238
  }
@@ -273,7 +276,7 @@ class PythonBaseModelAttr implements Serializable {
273
276
  const py_type = this.nullable
274
277
  ? `Optional[${this.py_type.serialize()}]`
275
278
  : this.py_type.serialize();
276
- return ` ${this.name}: ${py_type} = Field(alias="${this.pg_name}")`;
279
+ return ` ${this.name}: ${py_type} = Field(alias=${escapePythonString(this.pg_name)})`;
277
280
  }
278
281
  }
279
282
 
@@ -326,7 +329,7 @@ class PythonTypedDictAttr implements Serializable {
326
329
  const py_type = this.nullable
327
330
  ? `Optional[${this.py_type.serialize()}]`
328
331
  : this.py_type.serialize();
329
- const annotation = `Annotated[${py_type}, Field(alias="${this.pg_name}")]`;
332
+ const annotation = `Annotated[${py_type}, Field(alias=${escapePythonString(this.pg_name)})]`;
330
333
  const rhs = this.not_required ? `NotRequired[${annotation}]` : annotation;
331
334
  return ` ${this.name}: ${rhs}`;
332
335
  }
@@ -367,6 +370,16 @@ function concatLines(items: Serializable[]): string {
367
370
  return items.map((item) => item.serialize()).join("\n\n");
368
371
  }
369
372
 
373
+ /**
374
+ * Emits a Postgres name as a double-quoted Python string literal. JSON string
375
+ * escaping is a subset of Python's (`\"`, `\\`, `\n`, `\uXXXX`), so
376
+ * `JSON.stringify` yields a literal Python parses to the exact original name
377
+ * and keeps quotes, backslashes and newlines from breaking out of it.
378
+ */
379
+ function escapePythonString(value: string): string {
380
+ return JSON.stringify(value);
381
+ }
382
+
370
383
  const PY_TYPE_MAP: Record<string, string> = {
371
384
  // Bool
372
385
  bool: "bool",
@@ -395,9 +408,11 @@ const PY_TYPE_MAP: Record<string, string> = {
395
408
  vector: "list[Any]",
396
409
  interval: "str",
397
410
 
398
- // JSON
399
- json: "Json[Any]",
400
- jsonb: "Json[Any]",
411
+ // JSON. PostgREST returns these columns already deserialized, so the
412
+ // generated models must accept parsed values (JsonValue), not JSON strings
413
+ // (which is what pydantic's Json[...] validates and parses).
414
+ json: "JsonValue",
415
+ jsonb: "JsonValue",
401
416
 
402
417
  // Range types (can be adjusted to more complex types if needed)
403
418
  int4range: "str",
@@ -174,6 +174,49 @@ function generateProtocolConformances(protocols: string[]): string {
174
174
  return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
175
175
  }
176
176
 
177
+ /**
178
+ * Serializes a database-provided value as a Swift string literal. Backslashes
179
+ * would otherwise start escape or interpolation sequences (`\(`), double
180
+ * quotes would terminate the literal early, and line breaks or control
181
+ * characters are not allowed in single-line literals, so all of them are
182
+ * escaped per Swift's string literal grammar. U+2028 and U+2029 are legal in
183
+ * Swift literals but are escaped anyway because other tooling mishandles
184
+ * them. Swift unescapes at compile time, so raw values round-trip unchanged.
185
+ */
186
+ function swiftStringLiteral(value: string): string {
187
+ let escaped = "";
188
+ for (const character of value) {
189
+ switch (character) {
190
+ case "\\":
191
+ escaped += "\\\\";
192
+ break;
193
+ case '"':
194
+ escaped += '\\"';
195
+ break;
196
+ case "\t":
197
+ escaped += "\\t";
198
+ break;
199
+ case "\n":
200
+ escaped += "\\n";
201
+ break;
202
+ case "\r":
203
+ escaped += "\\r";
204
+ break;
205
+ default: {
206
+ const codePoint = character.codePointAt(0)!;
207
+ escaped +=
208
+ codePoint < 0x20 ||
209
+ codePoint === 0x7f ||
210
+ codePoint === 0x2028 ||
211
+ codePoint === 0x2029
212
+ ? `\\u{${codePoint.toString(16)}}`
213
+ : character;
214
+ }
215
+ }
216
+ }
217
+ return `"${escaped}"`;
218
+ }
219
+
177
220
  function generateEnum(
178
221
  enum_: SwiftEnum,
179
222
  { accessControl, level }: SwiftGeneratorOptions & { level: number },
@@ -184,7 +227,7 @@ function generateEnum(
184
227
  )} {`,
185
228
  ...enum_.cases.map(
186
229
  (case_) =>
187
- `${ident(level + 1)}case ${case_.formattedName} = "${case_.rawValue}"`,
230
+ `${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}`,
188
231
  ),
189
232
  `${ident(level)}}`,
190
233
  ];
@@ -3,7 +3,7 @@
3
3
  // literals (relying on Array#toString) to build the output. Preserving that is required for
4
4
  // byte-parity, and the offending expressions sit inside template literals where a targeted
5
5
  // disable comment would corrupt the emitted string.
6
- import prettier from "prettier";
6
+ import { format as oxfmtFormat } from "oxfmt";
7
7
  import type {
8
8
  GeneratorMetadata,
9
9
  PostgresColumn,
@@ -35,8 +35,30 @@ export interface GenerateTypescriptOptions {
35
35
  * env read. Default `'public'`.
36
36
  */
37
37
  defaultSchema?: string;
38
+ /**
39
+ * Formatter used on the generated output. Defaults to `oxfmt`, called
40
+ * inline. Callers on a latency-sensitive request path (e.g. postgres-meta's
41
+ * hosted `/generators/typescript` route) can substitute a worker-pool-backed
42
+ * formatter here instead of blocking the event loop on every call.
43
+ */
44
+ format?: (code: string) => Promise<string>;
38
45
  }
39
46
 
47
+ const defaultFormat = async (code: string): Promise<string> => {
48
+ const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
49
+ semi: false,
50
+ printWidth: 80,
51
+ });
52
+ if (errors.length > 0) {
53
+ throw new Error(
54
+ `oxfmt failed to format generated TypeScript output: ${errors
55
+ .map((error) => error.message)
56
+ .join("; ")}`,
57
+ );
58
+ }
59
+ return formatted;
60
+ };
61
+
40
62
  type TsRelationship = Pick<
41
63
  GeneratorMetadata["relationships"][number],
42
64
  | "foreign_key_name"
@@ -71,6 +93,7 @@ export const generateTypescript = async (
71
93
  detectOneToOneRelationships = false,
72
94
  postgrestVersion,
73
95
  defaultSchema = "public",
96
+ format = defaultFormat,
74
97
  } = opts;
75
98
  // Ordering of all collections (incl. relationships, columns, args below) is
76
99
  // provided by `sortGeneratorMetadata`; this generator no longer re-sorts.
@@ -209,6 +232,8 @@ export const generateTypescript = async (
209
232
  view: {
210
233
  ...materializedView,
211
234
  is_updatable: false,
235
+ is_insert_enabled: false,
236
+ is_update_enabled: false,
212
237
  },
213
238
  relationships: getRelationships(materializedView),
214
239
  });
@@ -524,7 +549,13 @@ export const generateTypescript = async (
524
549
  ) => {
525
550
  return fns
526
551
  .map(({ fn, inArgs }) => {
527
- let argsType = "never";
552
+ // `never` would be wrong here: postgrest-js treats `Args extends { '': Row }`
553
+ // as a computed field (`never extends T` is always true, so same-named table
554
+ // columns get omitted from select results), and an object type with a required
555
+ // `never` property is uninhabited for any tool doing sound type math on the
556
+ // generated types. `Record<PropertyKey, never>` is the accurate type for
557
+ // "callable with no arguments".
558
+ let argsType = "Record<PropertyKey, never>";
528
559
  let returnType = getFunctionReturnType(schema, fn);
529
560
 
530
561
  // Check for specific error cases
@@ -614,6 +645,11 @@ export const generateTypescript = async (
614
645
  : "";
615
646
 
616
647
  function generateNullableUnionTsType(tsType: string, isNullable: boolean) {
648
+ // The generated Json type includes null, so a NOT NULL json/jsonb column
649
+ // has to be narrowed with NonNullable to reflect the database constraint.
650
+ if (tsType === "Json" && !isNullable) {
651
+ return `NonNullable<${tsType}>`;
652
+ }
617
653
  // Only add the null union if the type is not unknown as unknown already includes null
618
654
  if (tsType === "unknown" || tsType === "any" || !isNullable) {
619
655
  return tsType;
@@ -693,7 +729,10 @@ export type Database = {
693
729
  }
694
730
  Insert: {
695
731
  ${columnsByTableId[table.id].map((column) => {
696
- if (column.identity_generation === "ALWAYS") {
732
+ if (
733
+ column.identity_generation === "ALWAYS" ||
734
+ column.is_generated
735
+ ) {
697
736
  return `${JSON.stringify(column.name)}?: never`;
698
737
  }
699
738
  return generateColumnTsDefinition(
@@ -714,7 +753,10 @@ export type Database = {
714
753
  }
715
754
  Update: {
716
755
  ${columnsByTableId[table.id].map((column) => {
717
- if (column.identity_generation === "ALWAYS") {
756
+ if (
757
+ column.identity_generation === "ALWAYS" ||
758
+ column.is_generated
759
+ ) {
718
760
  return `${JSON.stringify(column.name)}?: never`;
719
761
  }
720
762
 
@@ -774,7 +816,7 @@ export type Database = {
774
816
  ]}
775
817
  }
776
818
  ${
777
- view.is_updatable
819
+ view.is_insert_enabled
778
820
  ? `Insert: {
779
821
  ${columnsByTableId[view.id].map((column) => {
780
822
  if (!column.is_updatable) {
@@ -793,7 +835,11 @@ export type Database = {
793
835
  );
794
836
  })}
795
837
  }
796
- Update: {
838
+ `
839
+ : ""
840
+ }${
841
+ view.is_update_enabled
842
+ ? `Update: {
797
843
  ${columnsByTableId[view.id].map((column) => {
798
844
  if (!column.is_updatable) {
799
845
  return `${JSON.stringify(column.name)}?: never`;
@@ -1016,10 +1062,7 @@ export const Constants = {
1016
1062
  } as const
1017
1063
  `;
1018
1064
 
1019
- output = await prettier.format(output, {
1020
- parser: "typescript",
1021
- semi: false,
1022
- });
1065
+ output = await format(output);
1023
1066
  return output;
1024
1067
  };
1025
1068
 
@@ -62,45 +62,48 @@ export async function introspect(
62
62
  const queryRows = async (sql: string) =>
63
63
  normalizeRows((await db.query(sql)).rows);
64
64
 
65
- const schemas = await queryRows(
66
- SCHEMAS_SQL({
67
- includeSystemSchemas: false,
68
- nameFilter: systemExcludingFilter,
69
- }),
70
- );
71
- const tables = await queryRows(
72
- TABLES_SQL({ schemaFilter: systemExcludingFilter }),
73
- );
74
- const foreignTables = await queryRows(
75
- FOREIGN_TABLES_SQL({ schemaFilter: plainFilter }),
76
- );
77
- const views = await queryRows(
78
- VIEWS_SQL({ schemaFilter: systemExcludingFilter }),
79
- );
80
- const materializedViews = await queryRows(
81
- MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter }),
82
- );
83
- const columns = await queryRows(
84
- COLUMNS_SQL({ schemaFilter: systemExcludingFilter }),
85
- );
86
- const primaryKeys = await queryRows(
87
- PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter }),
88
- );
89
- const relationships = await listRelationships(db, {
90
- includedSchemas: included,
91
- excludedSchemas: excluded,
92
- });
93
- const functions = await queryRows(
94
- FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter }),
95
- );
96
- // types: includeSystemSchemas true (no schema filter), table + array types included.
97
- const types = await queryRows(
98
- TYPES_SQL({
99
- schemaFilter: "",
100
- includeTableTypes: true,
101
- includeArrayTypes: true,
65
+ // Issued concurrently, matching postgres-meta's CLI path which ran its nine
66
+ // introspection queries under Promise.all. A pooled Queryable runs them in
67
+ // parallel; a single-connection one pipelines them, which is no worse than
68
+ // awaiting each in turn.
69
+ const [
70
+ schemas,
71
+ tables,
72
+ foreignTables,
73
+ views,
74
+ materializedViews,
75
+ columns,
76
+ primaryKeys,
77
+ relationships,
78
+ functions,
79
+ types,
80
+ ] = await Promise.all([
81
+ queryRows(
82
+ SCHEMAS_SQL({
83
+ includeSystemSchemas: false,
84
+ nameFilter: systemExcludingFilter,
85
+ }),
86
+ ),
87
+ queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
88
+ queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter })),
89
+ queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter })),
90
+ queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
91
+ queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
92
+ queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
93
+ listRelationships(db, {
94
+ includedSchemas: included,
95
+ excludedSchemas: excluded,
102
96
  }),
103
- );
97
+ 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
+ ),
106
+ ]);
104
107
 
105
108
  return {
106
109
  version: GENERATOR_METADATA_VERSION,
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.ts";
2
2
  import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
3
3
 
4
4
  export const COLUMNS_SQL = (
@@ -52,7 +52,24 @@ SELECT
52
52
  ) AS is_nullable,
53
53
  (
54
54
  c.relkind IN ('r', 'p')
55
- OR c.relkind IN ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, FALSE)
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
62
+ -- is_insert_enabled / is_update_enabled.
63
+ OR c.relkind IN ('v', 'f') AND (
64
+ pg_column_is_updatable(c.oid, a.attnum, TRUE)
65
+ OR EXISTS (
66
+ SELECT 1 FROM pg_trigger tg
67
+ WHERE tg.tgrelid = c.oid
68
+ AND tg.tgtype & 64 <> 0
69
+ AND tg.tgtype & 4 <> 0
70
+ AND NOT tg.tgisinternal
71
+ )
72
+ )
56
73
  ) AS is_updatable,
57
74
  uniques.table_id IS NOT NULL AS is_unique,
58
75
  check_constraints.definition AS "check",
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.ts";
2
2
  import type { SQLQueryProps } from "./common.ts";
3
3
 
4
4
  export const FOREIGN_TABLES_SQL = (
@@ -1,7 +1,7 @@
1
1
  // oxlint-disable typescript/restrict-template-expressions -- verbatim port of postgres-meta's
2
2
  // builder, which interpolates the `args` string[] into the SQL (relying on Array#toString). The
3
3
  // expression sits inside a template literal where a targeted disable comment would corrupt the SQL.
4
- import { literal } from "pg-format";
4
+ import { literal } from "./pg-format.ts";
5
5
  import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
6
6
 
7
7
  export const FUNCTIONS_SQL = (
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.ts";
2
2
 
3
3
  /**
4
4
  * Schemas that postgres-meta excludes by default when `includeSystemSchemas`
@@ -13,7 +13,7 @@ export const DEFAULT_SYSTEM_SCHEMAS = [
13
13
  /**
14
14
  * Build a SQL `IN (...)` / `NOT IN (...)` fragment for an include/exclude list,
15
15
  * ported verbatim from `postgres-meta/src/lib/helpers.ts`. Values are escaped
16
- * with pg-format's `literal`. Returns an empty string when there is nothing to
16
+ * with the inlined pg-format `literal`. Returns an empty string when there is nothing to
17
17
  * filter, so callers interpolate it conditionally.
18
18
  */
19
19
  export const filterByList = (
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.ts";
2
2
  import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
3
3
 
4
4
  export const MATERIALIZED_VIEWS_SQL = (
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Minimal inline port of the `pg-format` helpers this package uses, so the
3
+ * package carries no runtime dependency for them (the CommonJS-only
4
+ * `pg-format` package is awkward to consume from ESM/bundled CLIs).
5
+ *
6
+ * Mirrors `pg-format@1.0.4` byte-for-byte for the inputs postgres-meta feeds
7
+ * it — in particular numbers are quoted (`literal(10)` is `'10'`), because
8
+ * that is what postgres-meta's SQL emits and this package is byte-parity
9
+ * constrained against it. The `supabase/supabase` `packages/pg-meta` port
10
+ * deliberately diverges here (unquoted numbers); do not "fix" this to match it.
11
+ *
12
+ * Originally ported from PostgreSQL's `src/interfaces/libpq/fe-exec.c`.
13
+ */
14
+
15
+ /** Convert a JS ISO string to Postgres' default ISO 8601 form. */
16
+ function formatDate(date: string): string {
17
+ return date.replace("T", " ").replace("Z", "+00");
18
+ }
19
+
20
+ function arrayToList(
21
+ useSpace: boolean,
22
+ array: unknown[],
23
+ formatter: (value: unknown) => string,
24
+ ): string {
25
+ let sql = useSpace ? " (" : "(";
26
+ for (const [index, element] of array.entries()) {
27
+ sql += (index === 0 ? "" : ", ") + formatter(element);
28
+ }
29
+ sql += ")";
30
+ return sql;
31
+ }
32
+
33
+ /**
34
+ * Quote a value as a SQL literal. `null`/`undefined` become `NULL`, booleans
35
+ * `'t'`/`'f'`, dates ISO 8601 strings, arrays comma-separated lists (nested
36
+ * arrays become parenthesised groups), plain objects `'…'::jsonb`, and
37
+ * everything else — including numbers — a single-quoted string with embedded
38
+ * quotes doubled. Strings containing a backslash use the `E'…'` form.
39
+ */
40
+ export function literal(value?: unknown): string {
41
+ let text: string;
42
+ let explicitCast: string | undefined;
43
+
44
+ if (value === undefined || value === null) {
45
+ return "NULL";
46
+ }
47
+ if (value === false) {
48
+ return "'f'";
49
+ }
50
+ if (value === true) {
51
+ return "'t'";
52
+ }
53
+ if (value instanceof Date) {
54
+ return `'${formatDate(value.toISOString())}'`;
55
+ }
56
+ if (Array.isArray(value)) {
57
+ const parts: string[] = [];
58
+ for (const [index, element] of value.entries()) {
59
+ if (Array.isArray(element)) {
60
+ parts.push(arrayToList(index !== 0, element, literal));
61
+ } else {
62
+ parts.push(literal(element));
63
+ }
64
+ }
65
+ return parts.toString();
66
+ }
67
+ if (typeof value === "string") {
68
+ text = value;
69
+ } else if (typeof value === "number" || typeof value === "bigint") {
70
+ text = value.toString();
71
+ } else {
72
+ explicitCast = "jsonb";
73
+ text = JSON.stringify(value);
74
+ }
75
+
76
+ let hasBackslash = false;
77
+ let quoted = "'";
78
+
79
+ for (const c of text) {
80
+ if (c === "'") {
81
+ quoted += c + c;
82
+ } else if (c === "\\") {
83
+ quoted += c + c;
84
+ hasBackslash = true;
85
+ } else {
86
+ quoted += c;
87
+ }
88
+ }
89
+
90
+ quoted += "'";
91
+
92
+ if (hasBackslash) {
93
+ quoted = `E${quoted}`;
94
+ }
95
+
96
+ if (explicitCast) {
97
+ quoted += `::${explicitCast}`;
98
+ }
99
+
100
+ return quoted;
101
+ }
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.ts";
2
2
  import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
3
3
 
4
4
  export const PRIMARY_KEYS_SQL = (