@supabase/postgrest-typegen 0.2.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 (37) 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.js +22 -5
  6. package/dist/introspection/sql/columns.sql.js +19 -2
  7. package/dist/introspection/sql/foreign_tables.sql.js +1 -1
  8. package/dist/introspection/sql/functions.sql.js +1 -1
  9. package/dist/introspection/sql/helpers.d.ts +1 -1
  10. package/dist/introspection/sql/helpers.js +2 -2
  11. package/dist/introspection/sql/materialized_views.sql.js +1 -1
  12. package/dist/introspection/sql/pg-format.d.ts +21 -0
  13. package/dist/introspection/sql/pg-format.js +92 -0
  14. package/dist/introspection/sql/primary_keys.sql.js +1 -1
  15. package/dist/introspection/sql/schemas.sql.js +1 -1
  16. package/dist/introspection/sql/table.sql.js +1 -1
  17. package/dist/introspection/sql/types.sql.js +1 -1
  18. package/dist/introspection/sql/views.sql.js +7 -1
  19. package/dist/types.d.ts +4 -0
  20. package/dist/types.js +9 -0
  21. package/package.json +2 -4
  22. package/src/generation/go.ts +31 -2
  23. package/src/generation/python.ts +25 -10
  24. package/src/generation/swift.ts +44 -1
  25. package/src/generation/typescript.ts +28 -5
  26. package/src/introspection/sql/columns.sql.ts +19 -2
  27. package/src/introspection/sql/foreign_tables.sql.ts +1 -1
  28. package/src/introspection/sql/functions.sql.ts +1 -1
  29. package/src/introspection/sql/helpers.ts +2 -2
  30. package/src/introspection/sql/materialized_views.sql.ts +1 -1
  31. package/src/introspection/sql/pg-format.ts +101 -0
  32. package/src/introspection/sql/primary_keys.sql.ts +1 -1
  33. package/src/introspection/sql/schemas.sql.ts +1 -1
  34. package/src/introspection/sql/table.sql.ts +1 -1
  35. package/src/introspection/sql/types.sql.ts +1 -1
  36. package/src/introspection/sql/views.sql.ts +7 -1
  37. package/src/types.ts +9 -0
package/README.md CHANGED
@@ -128,6 +128,10 @@ Not yet published; consumed in-repo for now (`packages/postgrest-typegen`).
128
128
  npm install pg
129
129
  ```
130
130
 
131
+ ## Releasing
132
+
133
+ Unlike the rest of this repo, merging a change here does not open a release pull request on its own. Releases are cut on demand: run the `Release postgrest-typegen` workflow from the Actions tab, which runs release-please against `release-please-config.postgrest-typegen.json` and opens (or updates) the release pull request for this package. Merging that pull request tags the release and publishes to npm.
134
+
131
135
  ## License
132
136
 
133
137
  MIT
@@ -58,6 +58,34 @@ function formatForGoTypeName(name) {
58
58
  })
59
59
  .join("");
60
60
  }
61
+ /**
62
+ * Renders the `json` struct tag for a column or composite attribute name.
63
+ *
64
+ * The name is quoted with `JSON.stringify`, whose escape sequences are a
65
+ * subset of Go's, so `reflect.StructTag.Get("json")` recovers the exact name
66
+ * via `strconv.Unquote` even when it contains quotes, backslashes or control
67
+ * characters. The tag is emitted as a raw string literal like before, except
68
+ * when the name contains a backtick: Go raw literals cannot contain one, so
69
+ * such tags fall back to an interpreted literal with a second round of
70
+ * escaping. Names the marshaler cannot represent still compile and
71
+ * round-trip through `reflect.StructTag`, but `encoding/json` applies its
72
+ * own tag rules at runtime: a comma splits the name from tag options (so
73
+ * only the part before it is used as the JSON name), and a name it rejects
74
+ * outright (for example one containing a quote) falls back to the Go field
75
+ * name. That is a limitation of the struct tag convention itself, not of
76
+ * the generated source.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * formatForGoStructTag('name') // `json:"name"`
81
+ * formatForGoStructTag('a"b') // `json:"a\"b"`
82
+ * formatForGoStructTag('back`tick') // "json:\"back`tick\""
83
+ * ```
84
+ */
85
+ function formatForGoStructTag(name) {
86
+ const tag = `json:${JSON.stringify(name)}`;
87
+ return tag.includes("`") ? JSON.stringify(tag) : `\`${tag}\``;
88
+ }
61
89
  function generateTableStruct(schema, table, columns, types, operation) {
62
90
  // Storing columns as a tuple of [formattedName, type, name] rather than creating the string
63
91
  // representation of the line allows us to pre-format the entries. Go formats
@@ -97,7 +125,7 @@ function generateTableStruct(schema, table, columns, types, operation) {
97
125
  // Pad the formatted name and type to align the struct fields, then join
98
126
  // create the final string representation of the struct fields.
99
127
  const formattedColumnEntries = columnEntries.map(([formattedName, type, name]) => {
100
- return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)} \`json:"${name}"\``;
128
+ return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)} ${formatForGoStructTag(name)}`;
101
129
  });
102
130
  return `
103
131
  type ${formatForGoTypeName(schema.name)}${formatForGoTypeName(table.name)}${operation} struct {
@@ -134,7 +162,7 @@ function generateCompositeTypeStruct(schema, type, types) {
134
162
  // Pad the formatted name and type to align the struct fields, then join
135
163
  // create the final string representation of the struct fields.
136
164
  const formattedAttributeEntries = attributeEntries.map(([formattedName, type, name]) => {
137
- return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)} \`json:"${name}"\``;
165
+ return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)} ${formatForGoStructTag(name)}`;
138
166
  });
139
167
  return `
140
168
  type ${formatForGoTypeName(schema.name)}${formatForGoTypeName(type.name)} struct {
@@ -26,13 +26,12 @@ from typing import (
26
26
  Any,
27
27
  List,
28
28
  Literal,
29
- NotRequired,
30
29
  Optional,
31
- TypeAlias,
32
30
  TypedDict,
33
31
  )
32
+ from typing_extensions import NotRequired, TypeAlias
34
33
 
35
- from pydantic import BaseModel, Field, Json
34
+ from pydantic import BaseModel, Field, JsonValue
36
35
 
37
36
  ${concatLines(Object.values(ctx.user_enums))}
38
37
 
@@ -100,7 +99,9 @@ class PythonContext {
100
99
  });
101
100
  const attributeEntries = attributes.map((attribute) => {
102
101
  const type = this.parsePgType(attribute.type.name);
103
- return new PythonBaseModelAttr(attribute.name, type, false);
102
+ // Composite type attributes cannot carry NOT NULL constraints in
103
+ // Postgres, so every field is inherently nullable.
104
+ return new PythonBaseModelAttr(attribute.name, type, true);
104
105
  });
105
106
  const schema = this.schemas[type.schema];
106
107
  return new PythonBaseModel(type.name, schema, attributeEntries);
@@ -146,7 +147,9 @@ class PythonEnum {
146
147
  this.variants = type.enums;
147
148
  }
148
149
  serialize() {
149
- const variants = this.variants.map((item) => `"${item}"`).join(", ");
150
+ const variants = this.variants
151
+ .map((item) => escapePythonString(item))
152
+ .join(", ");
150
153
  return `${this.name}: TypeAlias = Literal[${variants}]`;
151
154
  }
152
155
  }
@@ -183,7 +186,7 @@ class PythonBaseModelAttr {
183
186
  const py_type = this.nullable
184
187
  ? `Optional[${this.py_type.serialize()}]`
185
188
  : this.py_type.serialize();
186
- return ` ${this.name}: ${py_type} = Field(alias="${this.pg_name}")`;
189
+ return ` ${this.name}: ${py_type} = Field(alias=${escapePythonString(this.pg_name)})`;
187
190
  }
188
191
  }
189
192
  class PythonBaseModel {
@@ -221,7 +224,7 @@ class PythonTypedDictAttr {
221
224
  const py_type = this.nullable
222
225
  ? `Optional[${this.py_type.serialize()}]`
223
226
  : this.py_type.serialize();
224
- const annotation = `Annotated[${py_type}, Field(alias="${this.pg_name}")]`;
227
+ const annotation = `Annotated[${py_type}, Field(alias=${escapePythonString(this.pg_name)})]`;
225
228
  const rhs = this.not_required ? `NotRequired[${annotation}]` : annotation;
226
229
  return ` ${this.name}: ${rhs}`;
227
230
  }
@@ -251,6 +254,15 @@ class PythonTypedDict {
251
254
  function concatLines(items) {
252
255
  return items.map((item) => item.serialize()).join("\n\n");
253
256
  }
257
+ /**
258
+ * Emits a Postgres name as a double-quoted Python string literal. JSON string
259
+ * escaping is a subset of Python's (`\"`, `\\`, `\n`, `\uXXXX`), so
260
+ * `JSON.stringify` yields a literal Python parses to the exact original name
261
+ * and keeps quotes, backslashes and newlines from breaking out of it.
262
+ */
263
+ function escapePythonString(value) {
264
+ return JSON.stringify(value);
265
+ }
254
266
  const PY_TYPE_MAP = {
255
267
  // Bool
256
268
  bool: "bool",
@@ -276,9 +288,11 @@ const PY_TYPE_MAP = {
276
288
  uuid: "uuid.UUID",
277
289
  vector: "list[Any]",
278
290
  interval: "str",
279
- // JSON
280
- json: "Json[Any]",
281
- jsonb: "Json[Any]",
291
+ // JSON. PostgREST returns these columns already deserialized, so the
292
+ // generated models must accept parsed values (JsonValue), not JSON strings
293
+ // (which is what pydantic's Json[...] validates and parses).
294
+ json: "JsonValue",
295
+ jsonb: "JsonValue",
282
296
  // Range types (can be adjusted to more complex types if needed)
283
297
  int4range: "str",
284
298
  int4multirange: "str",
@@ -94,10 +94,52 @@ function pgCompositeTypeToSwiftStruct(type, { types, views, tables, }) {
94
94
  function generateProtocolConformances(protocols) {
95
95
  return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
96
96
  }
97
+ /**
98
+ * Serializes a database-provided value as a Swift string literal. Backslashes
99
+ * would otherwise start escape or interpolation sequences (`\(`), double
100
+ * quotes would terminate the literal early, and line breaks or control
101
+ * characters are not allowed in single-line literals, so all of them are
102
+ * escaped per Swift's string literal grammar. U+2028 and U+2029 are legal in
103
+ * Swift literals but are escaped anyway because other tooling mishandles
104
+ * them. Swift unescapes at compile time, so raw values round-trip unchanged.
105
+ */
106
+ function swiftStringLiteral(value) {
107
+ let escaped = "";
108
+ for (const character of value) {
109
+ switch (character) {
110
+ case "\\":
111
+ escaped += "\\\\";
112
+ break;
113
+ case '"':
114
+ escaped += '\\"';
115
+ break;
116
+ case "\t":
117
+ escaped += "\\t";
118
+ break;
119
+ case "\n":
120
+ escaped += "\\n";
121
+ break;
122
+ case "\r":
123
+ escaped += "\\r";
124
+ break;
125
+ default: {
126
+ const codePoint = character.codePointAt(0);
127
+ escaped +=
128
+ codePoint < 0x20 ||
129
+ codePoint === 0x7f ||
130
+ codePoint === 0x2028 ||
131
+ codePoint === 0x2029
132
+ ? `\\u{${codePoint.toString(16)}}`
133
+ : character;
134
+ }
135
+ }
136
+ }
137
+ return `"${escaped}"`;
138
+ }
97
139
  function generateEnum(enum_, { accessControl, level }) {
98
140
  return [
99
141
  `${ident(level)}${accessControl} enum ${enum_.formattedEnumName}${generateProtocolConformances(enum_.protocolConformances)} {`,
100
- ...enum_.cases.map((case_) => `${ident(level + 1)}case ${case_.formattedName} = "${case_.rawValue}"`),
142
+ ...enum_.cases.map((case_) => `${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}`),
101
143
  `${ident(level)}}`,
102
144
  ];
103
145
  }
@@ -123,6 +123,8 @@ export const generateTypescript = async (metadata, opts = {}) => {
123
123
  view: {
124
124
  ...materializedView,
125
125
  is_updatable: false,
126
+ is_insert_enabled: false,
127
+ is_update_enabled: false,
126
128
  },
127
129
  relationships: getRelationships(materializedView),
128
130
  });
@@ -351,7 +353,13 @@ export const generateTypescript = async (metadata, opts = {}) => {
351
353
  const getFunctionSignatures = (schema, fns) => {
352
354
  return fns
353
355
  .map(({ fn, inArgs }) => {
354
- let argsType = "never";
356
+ // `never` would be wrong here: postgrest-js treats `Args extends { '': Row }`
357
+ // as a computed field (`never extends T` is always true, so same-named table
358
+ // columns get omitted from select results), and an object type with a required
359
+ // `never` property is uninhabited for any tool doing sound type math on the
360
+ // generated types. `Record<PropertyKey, never>` is the accurate type for
361
+ // "callable with no arguments".
362
+ let argsType = "Record<PropertyKey, never>";
355
363
  let returnType = getFunctionReturnType(schema, fn);
356
364
  // Check for specific error cases
357
365
  const conflictError = getConflictError(schema, fns, fn, inArgs);
@@ -422,6 +430,11 @@ export const generateTypescript = async (metadata, opts = {}) => {
422
430
  }`
423
431
  : "";
424
432
  function generateNullableUnionTsType(tsType, isNullable) {
433
+ // The generated Json type includes null, so a NOT NULL json/jsonb column
434
+ // has to be narrowed with NonNullable to reflect the database constraint.
435
+ if (tsType === "Json" && !isNullable) {
436
+ return `NonNullable<${tsType}>`;
437
+ }
425
438
  // Only add the null union if the type is not unknown as unknown already includes null
426
439
  if (tsType === "unknown" || tsType === "any" || !isNullable) {
427
440
  return tsType;
@@ -461,7 +474,8 @@ export type Database = {
461
474
  }
462
475
  Insert: {
463
476
  ${columnsByTableId[table.id].map((column) => {
464
- if (column.identity_generation === "ALWAYS") {
477
+ if (column.identity_generation === "ALWAYS" ||
478
+ column.is_generated) {
465
479
  return `${JSON.stringify(column.name)}?: never`;
466
480
  }
467
481
  return generateColumnTsDefinition(schema, {
@@ -477,7 +491,8 @@ export type Database = {
477
491
  }
478
492
  Update: {
479
493
  ${columnsByTableId[table.id].map((column) => {
480
- if (column.identity_generation === "ALWAYS") {
494
+ if (column.identity_generation === "ALWAYS" ||
495
+ column.is_generated) {
481
496
  return `${JSON.stringify(column.name)}?: never`;
482
497
  }
483
498
  return generateColumnTsDefinition(schema, {
@@ -512,7 +527,7 @@ export type Database = {
512
527
  .map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
513
528
  ]}
514
529
  }
515
- ${view.is_updatable
530
+ ${view.is_insert_enabled
516
531
  ? `Insert: {
517
532
  ${columnsByTableId[view.id].map((column) => {
518
533
  if (!column.is_updatable) {
@@ -527,7 +542,9 @@ export type Database = {
527
542
  }, { types, schemas, tables, views });
528
543
  })}
529
544
  }
530
- Update: {
545
+ `
546
+ : ""}${view.is_update_enabled
547
+ ? `Update: {
531
548
  ${columnsByTableId[view.id].map((column) => {
532
549
  if (!column.is_updatable) {
533
550
  return `${JSON.stringify(column.name)}?: never`;
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const COLUMNS_SQL = (props) => /* SQL */ `
3
3
  -- Adapted from information_schema.columns
4
4
 
@@ -43,7 +43,24 @@ SELECT
43
43
  ) AS is_nullable,
44
44
  (
45
45
  c.relkind IN ('r', 'p')
46
- OR c.relkind IN ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, FALSE)
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
53
+ -- is_insert_enabled / is_update_enabled.
54
+ OR c.relkind IN ('v', 'f') AND (
55
+ pg_column_is_updatable(c.oid, a.attnum, TRUE)
56
+ OR EXISTS (
57
+ SELECT 1 FROM pg_trigger tg
58
+ WHERE tg.tgrelid = c.oid
59
+ AND tg.tgtype & 64 <> 0
60
+ AND tg.tgtype & 4 <> 0
61
+ AND NOT tg.tgisinternal
62
+ )
63
+ )
47
64
  ) AS is_updatable,
48
65
  uniques.table_id IS NOT NULL AS is_unique,
49
66
  check_constraints.definition AS "check",
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const FOREIGN_TABLES_SQL = (props) => /* SQL */ `
3
3
  SELECT
4
4
  c.oid :: int8 AS id,
@@ -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.js";
5
5
  export const FUNCTIONS_SQL = (props) => /* SQL */ `
6
6
  -- CTE with sane arg_modes, arg_names, and arg_types.
7
7
  -- All three are always of the same length.
@@ -6,7 +6,7 @@ export declare const DEFAULT_SYSTEM_SCHEMAS: string[];
6
6
  /**
7
7
  * Build a SQL `IN (...)` / `NOT IN (...)` fragment for an include/exclude list,
8
8
  * ported verbatim from `postgres-meta/src/lib/helpers.ts`. Values are escaped
9
- * with pg-format's `literal`. Returns an empty string when there is nothing to
9
+ * with the inlined pg-format `literal`. Returns an empty string when there is nothing to
10
10
  * filter, so callers interpolate it conditionally.
11
11
  */
12
12
  export declare const filterByList: (include?: (string | number)[], exclude?: (string | number)[], defaultExclude?: (string | number)[]) => string;
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  /**
3
3
  * Schemas that postgres-meta excludes by default when `includeSystemSchemas`
4
4
  * is false. Ported from `postgres-meta/src/lib/constants.ts`.
@@ -11,7 +11,7 @@ export const DEFAULT_SYSTEM_SCHEMAS = [
11
11
  /**
12
12
  * Build a SQL `IN (...)` / `NOT IN (...)` fragment for an include/exclude list,
13
13
  * ported verbatim from `postgres-meta/src/lib/helpers.ts`. Values are escaped
14
- * with pg-format's `literal`. Returns an empty string when there is nothing to
14
+ * with the inlined pg-format `literal`. Returns an empty string when there is nothing to
15
15
  * filter, so callers interpolate it conditionally.
16
16
  */
17
17
  export const filterByList = (include, exclude, defaultExclude) => {
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const MATERIALIZED_VIEWS_SQL = (props) => /* SQL */ `
3
3
  select
4
4
  c.oid::int8 as id,
@@ -0,0 +1,21 @@
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
+ * Quote a value as a SQL literal. `null`/`undefined` become `NULL`, booleans
16
+ * `'t'`/`'f'`, dates ISO 8601 strings, arrays comma-separated lists (nested
17
+ * arrays become parenthesised groups), plain objects `'…'::jsonb`, and
18
+ * everything else — including numbers — a single-quoted string with embedded
19
+ * quotes doubled. Strings containing a backslash use the `E'…'` form.
20
+ */
21
+ export declare function literal(value?: unknown): string;
@@ -0,0 +1,92 @@
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
+ /** Convert a JS ISO string to Postgres' default ISO 8601 form. */
15
+ function formatDate(date) {
16
+ return date.replace("T", " ").replace("Z", "+00");
17
+ }
18
+ function arrayToList(useSpace, array, formatter) {
19
+ let sql = useSpace ? " (" : "(";
20
+ for (const [index, element] of array.entries()) {
21
+ sql += (index === 0 ? "" : ", ") + formatter(element);
22
+ }
23
+ sql += ")";
24
+ return sql;
25
+ }
26
+ /**
27
+ * Quote a value as a SQL literal. `null`/`undefined` become `NULL`, booleans
28
+ * `'t'`/`'f'`, dates ISO 8601 strings, arrays comma-separated lists (nested
29
+ * arrays become parenthesised groups), plain objects `'…'::jsonb`, and
30
+ * everything else — including numbers — a single-quoted string with embedded
31
+ * quotes doubled. Strings containing a backslash use the `E'…'` form.
32
+ */
33
+ export function literal(value) {
34
+ let text;
35
+ let explicitCast;
36
+ if (value === undefined || value === null) {
37
+ return "NULL";
38
+ }
39
+ if (value === false) {
40
+ return "'f'";
41
+ }
42
+ if (value === true) {
43
+ return "'t'";
44
+ }
45
+ if (value instanceof Date) {
46
+ return `'${formatDate(value.toISOString())}'`;
47
+ }
48
+ if (Array.isArray(value)) {
49
+ const parts = [];
50
+ for (const [index, element] of value.entries()) {
51
+ if (Array.isArray(element)) {
52
+ parts.push(arrayToList(index !== 0, element, literal));
53
+ }
54
+ else {
55
+ parts.push(literal(element));
56
+ }
57
+ }
58
+ return parts.toString();
59
+ }
60
+ if (typeof value === "string") {
61
+ text = value;
62
+ }
63
+ else if (typeof value === "number" || typeof value === "bigint") {
64
+ text = value.toString();
65
+ }
66
+ else {
67
+ explicitCast = "jsonb";
68
+ text = JSON.stringify(value);
69
+ }
70
+ let hasBackslash = false;
71
+ let quoted = "'";
72
+ for (const c of text) {
73
+ if (c === "'") {
74
+ quoted += c + c;
75
+ }
76
+ else if (c === "\\") {
77
+ quoted += c + c;
78
+ hasBackslash = true;
79
+ }
80
+ else {
81
+ quoted += c;
82
+ }
83
+ }
84
+ quoted += "'";
85
+ if (hasBackslash) {
86
+ quoted = `E${quoted}`;
87
+ }
88
+ if (explicitCast) {
89
+ quoted += `::${explicitCast}`;
90
+ }
91
+ return quoted;
92
+ }
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const PRIMARY_KEYS_SQL = (props) => /* SQL */ `
3
3
  SELECT
4
4
  c.oid :: int8 AS table_id,
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const SCHEMAS_SQL = (props) => /* SQL */ `
3
3
  -- Adapted from information_schema.schemata
4
4
  select
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const TABLES_SQL = (props) => /* SQL */ `
3
3
  SELECT
4
4
  c.oid :: int8 AS id,
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const TYPES_SQL = (props) => /* SQL */ `
3
3
  select
4
4
  t.oid::int8 as id,
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const VIEWS_SQL = (props) => /* SQL */ `
3
3
  SELECT
4
4
  c.oid :: int8 AS id,
@@ -6,6 +6,12 @@ SELECT
6
6
  c.relname AS name,
7
7
  -- See definition of information_schema.views
8
8
  (pg_relation_is_updatable(c.oid, false) & 20) = 20 AS is_updatable,
9
+ -- Passing include_triggers => true also counts views made writable by
10
+ -- INSTEAD OF triggers (and INSTEAD rules), which PostgREST can write
11
+ -- through even when the view is not auto-updatable. Bit 8 is INSERT,
12
+ -- bit 4 is UPDATE (see information_schema.views).
13
+ (pg_relation_is_updatable(c.oid, true) & 8) = 8 AS is_insert_enabled,
14
+ (pg_relation_is_updatable(c.oid, true) & 4) = 4 AS is_update_enabled,
9
15
  obj_description(c.oid) AS comment
10
16
  FROM
11
17
  pg_class c
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.2.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
- "oxfmt": "0.65.0",
66
- "pg-format": "1.0.4"
65
+ "oxfmt": "0.65.0"
67
66
  },
68
67
  "devDependencies": {
69
68
  "@testcontainers/postgresql": "^12.1.0",
@@ -72,7 +71,6 @@
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
75
  "oxlint": "1.80.0",
78
76
  "oxlint-tsgolint": "7.0.2001",
@@ -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
  ];
@@ -232,6 +232,8 @@ export const generateTypescript = async (
232
232
  view: {
233
233
  ...materializedView,
234
234
  is_updatable: false,
235
+ is_insert_enabled: false,
236
+ is_update_enabled: false,
235
237
  },
236
238
  relationships: getRelationships(materializedView),
237
239
  });
@@ -547,7 +549,13 @@ export const generateTypescript = async (
547
549
  ) => {
548
550
  return fns
549
551
  .map(({ fn, inArgs }) => {
550
- 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>";
551
559
  let returnType = getFunctionReturnType(schema, fn);
552
560
 
553
561
  // Check for specific error cases
@@ -637,6 +645,11 @@ export const generateTypescript = async (
637
645
  : "";
638
646
 
639
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
+ }
640
653
  // Only add the null union if the type is not unknown as unknown already includes null
641
654
  if (tsType === "unknown" || tsType === "any" || !isNullable) {
642
655
  return tsType;
@@ -716,7 +729,10 @@ export type Database = {
716
729
  }
717
730
  Insert: {
718
731
  ${columnsByTableId[table.id].map((column) => {
719
- if (column.identity_generation === "ALWAYS") {
732
+ if (
733
+ column.identity_generation === "ALWAYS" ||
734
+ column.is_generated
735
+ ) {
720
736
  return `${JSON.stringify(column.name)}?: never`;
721
737
  }
722
738
  return generateColumnTsDefinition(
@@ -737,7 +753,10 @@ export type Database = {
737
753
  }
738
754
  Update: {
739
755
  ${columnsByTableId[table.id].map((column) => {
740
- if (column.identity_generation === "ALWAYS") {
756
+ if (
757
+ column.identity_generation === "ALWAYS" ||
758
+ column.is_generated
759
+ ) {
741
760
  return `${JSON.stringify(column.name)}?: never`;
742
761
  }
743
762
 
@@ -797,7 +816,7 @@ export type Database = {
797
816
  ]}
798
817
  }
799
818
  ${
800
- view.is_updatable
819
+ view.is_insert_enabled
801
820
  ? `Insert: {
802
821
  ${columnsByTableId[view.id].map((column) => {
803
822
  if (!column.is_updatable) {
@@ -816,7 +835,11 @@ export type Database = {
816
835
  );
817
836
  })}
818
837
  }
819
- Update: {
838
+ `
839
+ : ""
840
+ }${
841
+ view.is_update_enabled
842
+ ? `Update: {
820
843
  ${columnsByTableId[view.id].map((column) => {
821
844
  if (!column.is_updatable) {
822
845
  return `${JSON.stringify(column.name)}?: never`;
@@ -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 = (
@@ -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 SCHEMAS_SQL = (
@@ -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 TABLES_SQL = (
@@ -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 TYPES_SQL = (
@@ -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 VIEWS_SQL = (
@@ -12,6 +12,12 @@ SELECT
12
12
  c.relname AS name,
13
13
  -- See definition of information_schema.views
14
14
  (pg_relation_is_updatable(c.oid, false) & 20) = 20 AS is_updatable,
15
+ -- Passing include_triggers => true also counts views made writable by
16
+ -- INSTEAD OF triggers (and INSTEAD rules), which PostgREST can write
17
+ -- through even when the view is not auto-updatable. Bit 8 is INSERT,
18
+ -- bit 4 is UPDATE (see information_schema.views).
19
+ (pg_relation_is_updatable(c.oid, true) & 8) = 8 AS is_insert_enabled,
20
+ (pg_relation_is_updatable(c.oid, true) & 4) = 4 AS is_update_enabled,
15
21
  obj_description(c.oid) AS comment
16
22
  FROM
17
23
  pg_class c
package/src/types.ts CHANGED
@@ -156,6 +156,15 @@ const postgresViewSchema = type({
156
156
  schema: "string",
157
157
  name: "string",
158
158
  is_updatable: "boolean",
159
+ /**
160
+ * Whether INSERT works through this view: auto-updatable per
161
+ * `pg_relation_is_updatable`, or made insertable by an INSTEAD OF INSERT
162
+ * trigger. Tracked separately from `is_updatable` (which mirrors
163
+ * `information_schema.views` and ignores triggers).
164
+ */
165
+ is_insert_enabled: "boolean",
166
+ /** Same as `is_insert_enabled`, for UPDATE. */
167
+ is_update_enabled: "boolean",
159
168
  comment: "string | null",
160
169
  "columns?": postgresColumnSchema.array(),
161
170
  });