Skip to content

fix: add | null to composite type fields in TypeScript generator - #1063

Closed
nancysangani wants to merge 3 commits into
supabase:masterfrom
nancysangani:fix/composite-type-fields-nullable
Closed

fix: add | null to composite type fields in TypeScript generator#1063
nancysangani wants to merge 3 commits into
supabase:masterfrom
nancysangani:fix/composite-type-fields-nullable

Conversation

@nancysangani

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix

What is the current behavior?

Composite type fields were not emitting | null in the generated TypeScript types, making them incorrectly non-nullable.

Fixes #763

What is the new behavior?

All composite type fields now emit | null, including domain-typed fields which previously fell through to unknown without | null.

Before:

CompositeTypes: {
  t: {
    a: number
    b: unknown
  }
}

After:

CompositeTypes: {
  t: {
    a: number | null
    b: unknown | null
  }
}

Additional context

Per the PostgreSQL docs, composite type fields cannot have NOT NULL constraints, so every field is inherently nullable.

Copilot AI review requested due to automatic review settings April 13, 2026 12:38
@nancysangani
nancysangani requested a review from a team as a code owner April 13, 2026 12:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes TypeScript type generation for PostgreSQL composite types by ensuring every composite type field is emitted as nullable (| null), including fields whose underlying type resolves to unknown (e.g., domain-typed fields).

Changes:

  • Updated the TypeScript template to always append | null to composite type attributes.
  • Extended the test database schema with a domain + composite type that uses it.
  • Updated TypeScript typegen snapshot expectations to include the new composite type and nullability.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/server/templates/typescript.ts Ensures composite type attributes are emitted as `${tsType}
test/db/00-init.sql Adds a domain and a composite type using that domain to exercise the domain-as-attribute case.
test/server/typegen.ts Updates expected generated TS output to include the new composite type and `

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@spydon

spydon commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Thank you for the contribution! postgres-meta's type generation is moving to the shared @supabase/postgrest-typegen package in supabase/sdk (see #1084), so open template fixes are being re-landed there. The underlying issue you identified (composite type attributes are inherently nullable in Postgres) is fixed for the Python generator in supabase/sdk#124 with credit to this PR. The TypeScript side is already handled on current master, where composite attributes emit | null.

@spydon spydon closed this Aug 31, 2026
spydon added a commit to supabase/sdk that referenced this pull request Sep 1, 2026
Postgres composite type attributes cannot carry NOT NULL constraints,
so every field is inherently nullable, yet the generated Python models
declared them as required non-None fields and rejected valid rows.

Ported from supabase/postgres-meta#1063 (the Python side of the same
nullability bug; the upstream PR only implemented the TypeScript part)
spydon added a commit to supabase/sdk that referenced this pull request Sep 1, 2026
…columns, composite nullability (#124)

## Summary

Ports the worthwhile Python generator fixes from postgres-meta's open
template PRs into this package (the templates are being deleted in favor
of this package in supabase/postgres-meta#1084, so open fixes there are
triaged and re-landed here). Four fixes, one commit each:

1. **Identifier escaping** (from supabase/postgres-meta#1082): enum
`Literal` labels and `Field(alias=...)` values were interpolated
unescaped, so a quote, backslash, or newline in a database name broke
the generated module. A shared `escapePythonString` helper (JSON
escaping, a strict subset of Python's) now covers all three
interpolation sites.
2. **Python 3.9/3.10 support** (from supabase/postgres-meta#1094):
`NotRequired` (3.11+) and `TypeAlias` (3.10+) now import from
`typing_extensions`, which is always installed as a required dependency
of pydantic.
3. **Deserialized json/jsonb** (from supabase/postgres-meta#1129):
`json`/`jsonb` map to pydantic's `JsonValue` instead of `Json[Any]`.
PostgREST returns these columns already deserialized, while `Json[Any]`
validates a JSON *string* and parses it, so every generated model with a
JSON column failed `model_validate` (supabase/supabase-py#1597).
4. **Composite type nullability** (the Python side of
supabase/postgres-meta#1063, reimplemented): composite type attributes
cannot carry NOT NULL constraints in Postgres, so their fields now emit
`Optional[...]`. The origin PR's Python hunks were dead code (an unused
`PythonDomain` class and a type-map entry for a name Postgres never
emits), so the actual fix was implemented instead of ported.

## Triage of origin PRs

| postgres-meta PR | Verdict | Reasoning |
|---|---|---|
| #1082 | Ported | Real invalid-syntax bug, correct approach. |
| #1094 | Ported | Import failure on Python 3.9/3.10, independently
verified by community comments on the PR. |
| #1129 | Ported | Every JSON column failed validation at runtime;
`JsonValue` is pydantic's native type for a parsed JSON value. |
| #1063 (python part) | Reimplemented | Real bug, but the PR's Python
changes did not actually fix it (dead code); the underlying fix is one
line in `typeToClass`. |
| #1072 (`frozen=True`) | Skipped | Author-labeled feature and an
opinionated behavior change that breaks consumers who mutate row models;
belongs behind a generator option if wanted. |
| #808 | Skipped | 2023 draft fully superseded by the
maintainer-authored template this package ports. |

## Validation

- Unit tests per fix (pathological enum labels and aliases, import block
assertions, json/jsonb mapping, composite `Optional` fields).
- Parity golden regenerated (39 lines): the `typing_extensions` import
split, 16 `Json[Any]` to `JsonValue` occurrences, and two composite
attributes gaining `Optional[...]`; reviewed line by line and the golden
gate was verified to actually trip on corruption.
- The regenerated golden imports cleanly under pydantic, passes `mypy`,
and runtime checks confirm deserialized JSON and `None` composite fields
now validate.
- `check-types`, `format-and-lint`, `knip`, `build`, `test` (97 pass,
includes Docker-backed introspection and parity) all green.
- Note: the nightly parity job against real postgres-meta will show this
intentional drift until postgres-meta consumes a release containing it
(supabase/postgres-meta#1084 replaces the templates with this package,
closing the gap).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Composite types can be null

3 participants