Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
280c870
feat: add supabase_typegen package generating typed table definitions
spydon Jul 23, 2026
df80c5d
chore: trigger CI
spydon Jul 23, 2026
c0e83b0
refactor: model column type kinds as an enum in the schema description
spydon Jul 23, 2026
ad8fdf5
chore: trigger CI
spydon Jul 23, 2026
514450c
fix(supabase_typegen): review fixes for wire correctness and CLI robu…
spydon Jul 23, 2026
1d4a737
feat(supabase_typegen): generate setXToNull methods for explicit SQL …
spydon Jul 23, 2026
7f69d9d
feat: mark typed table access as experimental in typegen output
spydon Jul 23, 2026
c795ded
chore: drop duplicate sdk-parse-ignore entry
spydon Jul 23, 2026
9fa7841
Merge branch 'feat/typed-table-access' into feat/supabase-gen
spydon Aug 5, 2026
4e2aa86
feat(supabase_typegen): read the postgres-meta json metadata instead …
spydon Aug 17, 2026
f72fb21
Merge branch 'feat/typed-table-access' into feat/supabase-gen
spydon Aug 17, 2026
d99442d
fix(supabase_typegen): wrap generated doc comments at 80 characters
spydon Aug 17, 2026
2e6c294
feat(supabase_typegen): support writing the generated code to stdout …
spydon Aug 17, 2026
32f279c
fix(supabase_typegen): handle all-read-only tables and string-seriali…
spydon Aug 18, 2026
3ea9e56
docs(supabase_typegen): document the cross-schema enum name limitation
spydon Aug 18, 2026
6455a3e
docs(supabase_typegen): recommend the Supabase CLI as the easiest way…
spydon Aug 18, 2026
bef322b
docs(supabase_typegen): explain when committing schema.json makes sense
spydon Aug 18, 2026
29c584f
refactor(supabase_typegen): consume the postgrest-typegen GeneratorMe…
spydon Aug 18, 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
106 changes: 100 additions & 6 deletions packages/supabase_typegen/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,104 @@
# 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, then:

```sh
supabase gen types --lang dart --local > lib/supabase_schema.g.dart
```

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

To run the package yourself, dump the schema metadata first and pass it with
`--input` (a path, or `-` for stdin):

```sh
supabase gen types --lang json --local > schema.json
dart run supabase_typegen --input schema.json \
--output lib/supabase_schema.g.dart
```

The document is the `GeneratorMetadata` introspection contract of
[`@supabase/postgrest-typegen`](https://github.com/supabase/pg-toolbelt),
which is also what postgres-meta's own type generators consume. Until CLI
support for `--lang dart` and `--lang json` ships, the same document comes
from serializing that package's `introspect()` result.

## Committing schema.json

The SQL in your `supabase/` directory stays the single source of truth: the
CLI applies your migrations to the local database and the metadata document
is introspected from the result. `schema.json` is derived output, the same
category as the generated Dart file, so committing it is optional and the
recommended one-liner never writes it at all.

Committing a snapshot can still be worthwhile:

- it diffs nicely in review, so a migration's effect on the API surface is
visible next to the SQL that caused it,
- the generator can re-run from it offline, without Docker or a database,
which keeps CI checks and codegen fast and hermetic,
- a stale generated file is detectable by regenerating from the snapshot and
comparing.

If you commit it, treat it like a lockfile: regenerate it in the same change
as every migration, and never edit it by hand. When the snapshot and the
migrations disagree, the migrations win; regenerate the snapshot.

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 array columns degrade to
`List<String>`.
- `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.
- The metadata identifies a column's enum type only by its bare name. When
two schemas define enums with the same name, columns using the enum from
the other schema resolve to the generated schema's enum, so keep enum
names unique across schemas.
- Foreign key relationship getters and typed functions (rpc) are not
generated yet.
131 changes: 131 additions & 0 deletions packages/supabase_typegen/bin/supabase_typegen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import 'dart:convert';
import 'dart:io';

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

final _argParser = ArgParser()
..addOption(
'input',
abbr: 'i',
help:
'Path of the GeneratorMetadata document, or - to '
'read it from stdin. Produce it with '
'`supabase gen types --lang json`.',
)
..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 schema '
'metadata that postgres-meta emits.',
)
..writeln()
..writeln('Usage: dart run supabase_typegen --input schema.json')
..writeln(_argParser.usage);
return 0;
}

final input = options.option('input');
if (input == null) {
stderr.writeln(
'--input is required: the path of a GeneratorMetadata '
'document, or - to read it from stdin. Produce it with '
'`supabase gen types --lang json`.',
);
return 64;
}

final String contents;
if (input == '-') {
contents = await utf8.decodeStream(stdin);
} else {
final inputFile = File(input);
if (!inputFile.existsSync()) {
stderr.writeln('The input file $input does not exist.');
return 66;
}
contents = inputFile.readAsStringSync();
}

final schemaName = options.option('schema')!;
final SchemaDescription schema;
try {
schema = parseGeneratorMetadata(
jsonDecode(contents) as Map<String, dynamic>,
schemaName: schemaName,
);
} on FormatException catch (error) {
stderr.writeln('Could not parse $input: ${error.message}');
return 65;
} on TypeError {
stderr.writeln(
'The document in $input is not a GeneratorMetadata document. '
'Produce it with `supabase gen types --lang json`.',
);
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