Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b020093
feat: add supabase_typegen package generating typed table definitions
spydon Jul 23, 2026
064bb5c
chore: trigger CI
spydon Jul 23, 2026
2432ebb
refactor: model column type kinds as an enum in the schema description
spydon Jul 23, 2026
71fbaef
chore: trigger CI
spydon Jul 23, 2026
80b59d5
fix(supabase_typegen): review fixes for wire correctness and CLI robu…
spydon Jul 23, 2026
226ad80
feat(supabase_typegen): generate setXToNull methods for explicit SQL …
spydon Jul 23, 2026
edd4f95
feat: mark typed table access as experimental in typegen output
spydon Jul 23, 2026
d80e2cd
feat(supabase_typegen): read the postgres-meta json metadata instead …
spydon Aug 17, 2026
ec866b3
fix(supabase_typegen): wrap generated doc comments at 80 characters
spydon Aug 17, 2026
f6f3245
feat(supabase_typegen): support writing the generated code to stdout …
spydon Aug 17, 2026
115b3f9
fix(supabase_typegen): handle all-read-only tables and string-seriali…
spydon Aug 18, 2026
214dc4b
docs(supabase_typegen): document the cross-schema enum name limitation
spydon Aug 18, 2026
9b76b14
docs(supabase_typegen): recommend the Supabase CLI as the easiest way…
spydon Aug 18, 2026
047b1ba
docs(supabase_typegen): explain when committing schema.json makes sense
spydon Aug 18, 2026
45667c8
refactor(supabase_typegen): consume the postgrest-typegen GeneratorMe…
spydon Aug 18, 2026
28192b9
fix(supabase_typegen): match the workspace postgrest version
spydon Aug 26, 2026
ead96c3
feat(supabase_typegen): gate insert and update generation on relation…
spydon Aug 31, 2026
dd6dca7
test(supabase_typegen): regenerate the metadata fixture from a real p…
spydon Aug 31, 2026
774dc9d
feat(supabase_typegen): resolve enum types exactly via the column typ…
spydon Aug 31, 2026
e4070d6
refactor(supabase_typegen): emit columns in the canonical name order
spydon Aug 31, 2026
a31b132
docs(supabase_typegen): source the metadata document from postgrest-t…
spydon Sep 1, 2026
978d2ce
docs(supabase_typegen): drop the --lang json references, the CLI hand…
spydon Sep 1, 2026
da595a8
refactor(supabase_typegen): read the metadata document from stdin only
spydon Sep 1, 2026
52c4a42
fix: address review feedback on source encoding, parser validation, a…
spydon Sep 1, 2026
a738dc0
fix: address review feedback on stdin decoding, README database scopi…
spydon Sep 1, 2026
f17b229
style: fix DCM findings on inferrable type arguments and equal switch…
spydon Sep 1, 2026
4fe916d
fix: address review feedback on identifier shadowing, parser validati…
spydon Sep 1, 2026
438a14c
test(supabase_typegen): check in a hostile-schema golden so generated…
spydon Sep 1, 2026
dd642ea
style: fix DCM findings and keep code-shaped text out of the hostile …
spydon Sep 1, 2026
384b87a
ci: refresh the apt package index before the cached package install
spydon Sep 1, 2026
2907420
docs: note the git dependency source until supabase_typegen is published
spydon Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ jobs:
dart pub global activate melos
melos bootstrap

# The runner image ships a package index that goes stale as soon as a
# security pocket update supersedes an indexed version; resolving
# package versions from it then 404s, and the cache action saves an
# empty (poisoned) cache entry on top. Refresh the index first.
- name: Refresh apt package index
if: ${{ matrix.target == 'linux' }}
run: sudo apt-get update

- name: Install Linux build dependencies (cached)
if: ${{ matrix.target == 'linux' }}
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
Expand Down
2 changes: 1 addition & 1 deletion packages/supabase_realtime/lib/src/realtime_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class RealtimeChannel {
Map<String, dynamic> get parameters => _deepUnmodifiableMap(_parameters);

static Map<String, dynamic> _deepUnmodifiableMap(Map<String, dynamic> map) {
return Map<String, dynamic>.unmodifiable(
return Map.unmodifiable(
map.map((key, value) => MapEntry(key, _deepUnmodifiable(value))),
);
}
Expand Down
80 changes: 74 additions & 6 deletions packages/supabase_typegen/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,78 @@
# supabase_typegen

> [!WARNING]
> This is a placeholder release that reserves the package name on pub.dev. The
> code generator is still under development and this version does nothing yet.
Generates typed Supabase table definitions from your database schema, so
query results never expose raw `Map<String, dynamic>` data.

A command-line code generator that turns a Supabase database schema into typed
Dart table definitions for use with the Supabase client packages.
For every table the generator emits:

The generator implementation will land in a future release.
- a zero-cost row extension type over the decoded JSON map with typed getters,
- `Insert` and `Update` value types that enforce required columns at the
construction site,
- a `PostgrestTable` definition and `TableColumn` tokens for compile-time
checked filters,
- Dart enums for Postgres enums, with wire-name mapping.

## Usage

The easiest way is through the Supabase CLI, which handles the database
connection and runs this package for you. Add `supabase_typegen` as a dev
dependency of your project (until the package is published to
[pub.dev](https://pub.dev), depend on it with a `git` source pointing at
`packages/supabase_typegen` in this repository), then:

```sh
supabase gen types --lang dart --local > lib/supabase_schema.g.dart
Comment thread
spydon marked this conversation as resolved.
```

Any of the CLI's connection flags work (`--local`, `--linked`, `--db-url`,
`--project-id`).

Under the hood the CLI runs the introspection of
[`@supabase/postgrest-typegen`](https://github.com/supabase/sdk/tree/main/packages/postgrest-typegen)
in-process against the database (the same `GeneratorMetadata` intermediate
representation its TypeScript, Go, Swift, and Python generators consume,
ordered with `sortGeneratorMetadata`) and hands the document to this tool
over stdin. The types reflect the current state of the selected database:
with `--local` the SQL in your `supabase/` directory stays the single source
of truth, since the CLI applies your migrations to the local database and
generates from the result, while `--linked`, `--project-id`, and `--db-url`
generate from whatever that database currently contains.

Use `--schema` to generate for a schema other than `public`, and `--import`
to change which library the generated file imports `PostgrestTable` and
`TableColumn` from.

The metadata comes from the database catalog, so nullability, database
defaults, and identity columns are exact: a `NOT NULL` column with a default
reads as non-nullable but stays optional on insert, and `GENERATED ALWAYS`
columns appear in the row type but not in the insert and update types.

## Generated code in action

```dart
final books = await client.table(Books.table)
.select()
.where(Books.mood.eq(Mood.happy))
.order(Books.createdAt, ascending: false); // List<BooksRow>

await client.table(Books.table).insert(
BooksInsert(title: 'A typed row', tags: ['dart']),
);
```

## Known limitations

- Passing `null` to an `Insert`/`Update` parameter omits the column. To write
SQL NULL explicitly, use the generated `set…ToNull` methods, for example
`BooksUpdate(inPrint: false).setPriceToNull()`; they only exist for
nullable columns, so nulling a `NOT NULL` column is a compile error.
- Array elements are assumed non-null (`text[]` maps to `List<String>`),
matching the supabase-js type generator; arrays containing SQL NULL
elements throw when the element is read. Enum, date, and timestamp array
elements stay in their wire representation (`List<String>`); the Dart enum
for enum array elements is still generated for manual conversion.
- `timestamptz` values are written back in UTC, naive `timestamp` values as
local wall time, and `date` values date-only, so calendar dates never
shift with the client timezone.
- Foreign key relationship getters and typed functions (rpc) are not
generated yet.
106 changes: 106 additions & 0 deletions packages/supabase_typegen/bin/supabase_typegen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:supabase_typegen/supabase_typegen.dart';

final _argParser = ArgParser()
..addOption(
'schema',
defaultsTo: 'public',
help: 'The database schema to generate types for.',
)
..addOption(
'output',
abbr: 'o',
defaultsTo: 'lib/supabase_schema.g.dart',
help: 'Path of the generated Dart file, or - to write the code to stdout.',
)
..addOption(
'import',
defaultsTo: 'package:postgrest/postgrest.dart',
help:
'The import the generated file uses for PostgrestTable and '
'TableColumn.',
)
..addFlag('help', abbr: 'h', negatable: false, help: 'Show this usage.');

Future<void> main(List<String> arguments) async {
// The value returned from main is ignored by the Dart VM, so the exit
// code has to be set explicitly.
exitCode = await _run(arguments);
}

Future<int> _run(List<String> arguments) async {
final ArgResults options;
try {
options = _argParser.parse(arguments);
} on FormatException catch (error) {
stderr
..writeln(error.message)
..writeln(_argParser.usage);
return 64;
}

if (options.flag('help')) {
stdout
..writeln(
'Generates typed Supabase table definitions from the '
'GeneratorMetadata document that postgrest-typegen emits, '
'read from stdin.',
)
..writeln()
..writeln('Usage: dart run supabase_typegen < <metadata document>')
..writeln(_argParser.usage);
return 0;
}

if (stdin.hasTerminal) {
stderr.writeln(
'Expected a GeneratorMetadata document of '
'@supabase/postgrest-typegen on stdin. This tool is normally '
'invoked through `supabase gen types --lang dart`.',
);
return 64;
}

final schemaName = options.option('schema')!;
final SchemaDescription schema;
try {
final contents = await utf8.decodeStream(stdin);
schema = parseGeneratorMetadata(
jsonDecode(contents) as Map<String, dynamic>,
schemaName: schemaName,
);
} on FormatException catch (error) {
stderr.writeln('Could not parse the document on stdin: ${error.message}');
return 65;
}

final code = generateDartCode(schema, importUri: options.option('import')!);

final output = options.option('output')!;
final String generatedInto;
if (output == '-') {
stdout.write(code);
generatedInto = 'stdout';
} else {
final outputFile = File(output);
outputFile.parent.createSync(recursive: true);
outputFile.writeAsStringSync(code);
generatedInto = outputFile.path;
}

final emittedTables = schema.tables
.where((table) => table.columns.isNotEmpty)
.length;
final skippedTables = schema.tables.length - emittedTables;
final summarySink = output == '-' ? stderr : stdout;
summarySink.writeln(
'Generated $generatedInto with $emittedTables tables and '
'${schema.enums.length} enums from schema "$schemaName".'
'${skippedTables == 0 ? '' : ' Skipped $skippedTables tables '
'without columns.'}',
);
return 0;
}
Loading