From b0200937bc129ffd5c3e81c6bd71a26c5d4c31e9 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 23 Jul 2026 09:13:25 +0200 Subject: [PATCH 01/31] feat: add supabase_typegen package generating typed table definitions --- packages/supabase_typegen/README.md | 53 ++- .../bin/supabase_typegen.dart | 113 +++++ .../lib/src/dart_generator.dart | 413 ++++++++++++++++++ .../supabase_typegen/lib/src/identifiers.dart | 132 ++++++ .../lib/src/openapi_parser.dart | 105 +++++ .../lib/src/schema_description.dart | 117 +++++ .../lib/supabase_typegen.dart | 12 +- packages/supabase_typegen/pubspec.yaml | 9 + .../test/dart_generator_test.dart | 53 +++ .../test/fixtures/openapi.json | 111 +++++ .../test/generated_schema_behavior_test.dart | 116 +++++ .../test/goldens/supabase_schema.dart | 208 +++++++++ .../test/identifiers_test.dart | 40 ++ .../test/openapi_parser_test.dart | 81 ++++ .../test/supabase_typegen_test.dart | 7 - .../tool/regenerate_goldens.dart | 17 + 16 files changed, 1569 insertions(+), 18 deletions(-) create mode 100644 packages/supabase_typegen/bin/supabase_typegen.dart create mode 100644 packages/supabase_typegen/lib/src/dart_generator.dart create mode 100644 packages/supabase_typegen/lib/src/identifiers.dart create mode 100644 packages/supabase_typegen/lib/src/openapi_parser.dart create mode 100644 packages/supabase_typegen/lib/src/schema_description.dart create mode 100644 packages/supabase_typegen/test/dart_generator_test.dart create mode 100644 packages/supabase_typegen/test/fixtures/openapi.json create mode 100644 packages/supabase_typegen/test/generated_schema_behavior_test.dart create mode 100644 packages/supabase_typegen/test/goldens/supabase_schema.dart create mode 100644 packages/supabase_typegen/test/identifiers_test.dart create mode 100644 packages/supabase_typegen/test/openapi_parser_test.dart delete mode 100644 packages/supabase_typegen/test/supabase_typegen_test.dart create mode 100644 packages/supabase_typegen/tool/regenerate_goldens.dart diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 103bf11e5..6e1decb3a 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -1,10 +1,51 @@ # 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` 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 + +```sh +dart run supabase_typegen \ + --url https://your-project.supabase.co \ + --key $SUPABASE_ANON_KEY \ + --output lib/supabase_schema.g.dart +``` + +`--url` and `--key` fall back to the `SUPABASE_URL` and `SUPABASE_ANON_KEY` +environment variables. 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 schema is read from the OpenAPI description that PostgREST serves at the +API root, so the key only needs read access; tables hidden from the key by +row level security settings are not included. + +## 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 + +await client.table(Books.table).insert( + BooksInsert(title: 'A typed row', tags: ['dart']), +); +``` + +## Known limitations + +The OpenAPI description does not distinguish nullable columns from `NOT NULL` +columns with a database default, so getters for defaulted columns other than +primary keys are conservatively nullable. Foreign key relationship getters +and typed functions (rpc) are not generated yet. diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart new file mode 100644 index 000000000..962cbe12d --- /dev/null +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -0,0 +1,113 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:http/http.dart' as http; +import 'package:supabase_typegen/supabase_typegen.dart'; + +final _argParser = ArgParser() + ..addOption( + 'url', + help: + 'The Supabase project URL, for example https://xyz.supabase.co. ' + 'Falls back to the SUPABASE_URL environment variable.', + ) + ..addOption( + 'key', + help: + 'The API key used to read the schema description. Falls back to ' + 'the SUPABASE_ANON_KEY or SUPABASE_KEY environment variable.', + ) + ..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.', + ) + ..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 main(List 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 a schema.') + ..writeln() + ..writeln('Usage: dart run supabase_typegen [options]') + ..writeln(_argParser.usage); + return 0; + } + + final url = options.option('url') ?? Platform.environment['SUPABASE_URL']; + final key = + options.option('key') ?? + Platform.environment['SUPABASE_ANON_KEY'] ?? + Platform.environment['SUPABASE_KEY']; + if (url == null || key == null) { + stderr.writeln( + 'Both --url and --key are required, either as options or through the ' + 'SUPABASE_URL and SUPABASE_ANON_KEY environment variables.', + ); + return 64; + } + + final schemaName = options.option('schema')!; + final endpoint = Uri.parse('$url/rest/v1/'); + final http.Response response; + try { + response = await http.get( + endpoint, + headers: { + 'apikey': key, + 'Authorization': 'Bearer $key', + 'Accept-Profile': schemaName, + }, + ); + } on http.ClientException catch (error) { + stderr.writeln('Failed to reach $endpoint: $error'); + return 1; + } + if (response.statusCode != 200) { + stderr.writeln( + 'Failed to fetch the schema description from $endpoint ' + '(HTTP ${response.statusCode}): ${response.body}', + ); + return 1; + } + + final schema = parseOpenApiDocument( + jsonDecode(response.body) as Map, + schemaName: schemaName, + ); + final code = generateDartCode(schema, importUri: options.option('import')!); + + final outputFile = File(options.option('output')!); + outputFile.parent.createSync(recursive: true); + outputFile.writeAsStringSync(code); + + stdout.writeln( + 'Generated ${outputFile.path} with ${schema.tables.length} tables and ' + '${schema.enums.length} enums from schema "$schemaName".', + ); + return 0; +} diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart new file mode 100644 index 000000000..12492285b --- /dev/null +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -0,0 +1,413 @@ +import 'package:dart_style/dart_style.dart'; + +import 'identifiers.dart'; +import 'schema_description.dart'; + +enum _Kind { direct, floating, dateTime, list, enumType, json } + +class _Binding { + const _Binding(this.dartType, this.kind); + + /// The non-nullable Dart type of the column. + final String dartType; + final _Kind kind; +} + +const _integerFormats = { + 'smallint', + 'integer', + 'bigint', + 'int2', + 'int4', + 'int8', +}; +const _floatingFormats = {'real', 'double precision', 'float4', 'float8'}; +const _numericFormats = {'numeric', 'decimal'}; +const _dateTimeFormats = { + 'date', + 'timestamp', + 'timestamp without time zone', + 'timestamp with time zone', + 'timestamptz', +}; +const _jsonFormats = {'json', 'jsonb'}; + +/// Generates a Dart source file with typed table definitions, row extension +/// types, insert and update value types, column tokens and Postgres enums for +/// [schema]. +/// +/// The generated code depends only on the library at [importUri], which must +/// export the typed table access API of `package:postgrest` (`PostgrestTable` +/// and `TableColumn`). +String generateDartCode( + SchemaDescription schema, { + String importUri = 'package:postgrest/postgrest.dart', +}) { + final buffer = StringBuffer() + ..writeln('// Generated by supabase_typegen. Do not edit by hand.') + ..writeln('//') + ..writeln('// Source schema: ${schema.schemaName}') + ..writeln() + ..writeln("import '$importUri';") + ..writeln(); + + final typeNames = _TypeNameRegistry(); + final enumTypeNames = {}; + + for (final enumDescription in schema.enums) { + final typeName = typeNames.claim(pascalCase(enumDescription.name)); + enumTypeNames[enumDescription.qualifiedName] = typeName; + _writeEnum(buffer, enumDescription, typeName); + } + + for (final table in schema.tables) { + if (table.columns.isEmpty) continue; + _writeTable(buffer, table, typeNames, enumTypeNames); + } + + return DartFormatter( + languageVersion: DartFormatter.latestLanguageVersion, + ).format(buffer.toString()); +} + +/// Hands out unique top level type names, suffixing `$` on collisions. +class _TypeNameRegistry { + final _used = {}; + + String claim(String name) { + var candidate = name; + while (!_used.add(candidate)) { + candidate = '$candidate\$'; + } + return candidate; + } +} + +void _writeEnum( + StringBuffer buffer, + EnumDescription enumDescription, + String typeName, +) { + final valueNames = _uniqueMemberNames(enumDescription.values); + + buffer + ..writeln('/// Postgres enum `${enumDescription.qualifiedName}`.') + ..writeln('enum $typeName {'); + for (final value in enumDescription.values) { + buffer.writeln(" ${valueNames[value]}(${_stringLiteral(value)}),"); + } + buffer + ..writeln(';') + ..writeln() + ..writeln(' const $typeName(this.wireName);') + ..writeln() + ..writeln(' /// The value as stored in the database.') + ..writeln(' final String wireName;') + ..writeln() + ..writeln(' /// Parses the database representation of the enum.') + ..writeln(' static $typeName fromWire(String wireName) =>') + ..writeln(' values.firstWhere((value) => value.wireName == wireName);') + ..writeln() + ..writeln(' @override') + ..writeln(' String toString() => wireName;') + ..writeln('}') + ..writeln(); +} + +void _writeTable( + StringBuffer buffer, + TableDescription table, + _TypeNameRegistry typeNames, + Map enumTypeNames, +) { + final baseName = pascalCase(table.name); + final rowType = typeNames.claim('${baseName}Row'); + final insertType = typeNames.claim('${baseName}Insert'); + final updateType = typeNames.claim('${baseName}Update'); + final namespaceType = typeNames.claim(baseName); + + final memberNames = _uniqueMemberNames([ + for (final column in table.columns) column.name, + ]); + final bindings = { + for (final column in table.columns) + column.name: _bindingFor(column, enumTypeNames), + }; + + _writeRow(buffer, table, rowType, memberNames, bindings); + _writeValues( + buffer, + table, + insertType, + memberNames, + bindings, + requireRequiredColumns: true, + docLine: + 'Values for inserting a row into `${table.name}`. Columns that are ' + 'nullable, part of a generated primary key, or covered by a database ' + 'default are optional; passing `null` omits the column so the ' + 'database default applies.', + ); + _writeValues( + buffer, + table, + updateType, + memberNames, + bindings, + requireRequiredColumns: false, + docLine: + 'Values for updating rows of `${table.name}`. All columns are ' + 'optional; passing `null` leaves the column unchanged.', + ); + _writeNamespace(buffer, table, namespaceType, rowType, memberNames, bindings); +} + +void _writeRow( + StringBuffer buffer, + TableDescription table, + String rowType, + Map memberNames, + Map bindings, +) { + buffer.writeln('/// A row of the `${table.name}` table.'); + _writeDocComment(buffer, table.comment); + buffer + ..writeln('extension type const $rowType(Map _json)') + ..writeln(' implements Map {'); + for (final column in table.columns) { + final binding = bindings[column.name]!; + _writeDocComment(buffer, column.comment, indent: ' '); + buffer.writeln( + ' ${_getterType(column, binding)} get ${memberNames[column.name]} => ' + '${_readExpression(column, binding)};', + ); + } + buffer + ..writeln('}') + ..writeln(); +} + +void _writeValues( + StringBuffer buffer, + TableDescription table, + String typeName, + Map memberNames, + Map bindings, { + required bool requireRequiredColumns, + required String docLine, +}) { + bool isRequired(ColumnDescription column) => + requireRequiredColumns && column.isRequired; + + buffer + ..writeln('/// $docLine') + ..writeln('extension type const $typeName._(Map _json)') + ..writeln(' implements Map {') + ..writeln(' $typeName({'); + for (final column in table.columns) { + final binding = bindings[column.name]!; + final name = memberNames[column.name]!; + if (isRequired(column)) { + buffer.writeln(' required ${binding.dartType} $name,'); + } else { + buffer.writeln(' ${binding.dartType}? $name,'); + } + } + buffer.writeln(' }) : this._({'); + for (final column in table.columns) { + final binding = bindings[column.name]!; + final name = memberNames[column.name]!; + final key = _stringLiteral(column.name); + if (isRequired(column)) { + buffer.writeln( + ' $key: ${_writeExpression(name, binding, nullable: false)},', + ); + } else { + buffer.writeln( + ' $key: ?${_writeExpression(name, binding, nullable: true)},', + ); + } + } + buffer + ..writeln(' });') + ..writeln('}') + ..writeln(); +} + +void _writeNamespace( + StringBuffer buffer, + TableDescription table, + String namespaceType, + String rowType, + Map memberNames, + Map bindings, +) { + final columnNames = _uniqueMemberNames( + [for (final column in table.columns) column.name], + reserved: {'table'}, + existing: memberNames, + ); + + buffer + ..writeln('/// Typed access to the `${table.name}` table.') + ..writeln('class $namespaceType {') + ..writeln(' const $namespaceType._();') + ..writeln() + ..writeln(' /// Table definition for [PostgrestClient.table].') + ..writeln( + ' static const table = PostgrestTable' + '(${_stringLiteral(table.name)}, $rowType.new);', + ) + ..writeln(); + for (final column in table.columns) { + final binding = bindings[column.name]!; + buffer.writeln( + ' static const ${columnNames[column.name]} = ' + 'TableColumn<${binding.dartType}>(${_stringLiteral(column.name)});', + ); + } + buffer + ..writeln('}') + ..writeln(); +} + +_Binding _bindingFor( + ColumnDescription column, + Map enumTypeNames, +) { + final format = column.postgresFormat; + + final enumTypeName = enumTypeNames[format]; + if (enumTypeName != null) { + return _Binding(enumTypeName, _Kind.enumType); + } + if (format.endsWith('[]')) { + return _Binding( + 'List<${_arrayElementType(column.arrayElementJsonType)}>', + _Kind.list, + ); + } + if (_integerFormats.contains(format)) { + return const _Binding('int', _Kind.direct); + } + if (_floatingFormats.contains(format)) { + return const _Binding('double', _Kind.floating); + } + if (_numericFormats.contains(format)) { + return const _Binding('num', _Kind.direct); + } + if (format == 'boolean') { + return const _Binding('bool', _Kind.direct); + } + if (_dateTimeFormats.contains(format)) { + return const _Binding('DateTime', _Kind.dateTime); + } + if (_jsonFormats.contains(format)) { + return const _Binding('Object', _Kind.json); + } + return switch (column.jsonType) { + 'integer' => const _Binding('int', _Kind.direct), + 'number' => const _Binding('num', _Kind.direct), + 'boolean' => const _Binding('bool', _Kind.direct), + 'string' => const _Binding('String', _Kind.direct), + _ => const _Binding('Object', _Kind.json), + }; +} + +String _arrayElementType(String? elementJsonType) => switch (elementJsonType) { + 'integer' => 'int', + 'number' => 'num', + 'boolean' => 'bool', + 'string' => 'String', + _ => 'Object', +}; + +String _getterType(ColumnDescription column, _Binding binding) { + if (binding.kind == _Kind.json) return 'Object?'; + return column.isNullable ? '${binding.dartType}?' : binding.dartType; +} + +String _readExpression(ColumnDescription column, _Binding binding) { + final access = "_json[${_stringLiteral(column.name)}]"; + final nullable = column.isNullable; + return switch (binding.kind) { + _Kind.direct => '$access as ${binding.dartType}${nullable ? '?' : ''}', + _Kind.floating => + nullable + ? '($access as num?)?.toDouble()' + : '($access as num).toDouble()', + _Kind.list => + nullable + ? '($access as List?)?.cast()' + : '($access as List).cast()', + _Kind.dateTime => + nullable + ? _nullableSwitch(access, 'DateTime.parse(value as String)') + : 'DateTime.parse($access as String)', + _Kind.enumType => + nullable + ? _nullableSwitch( + access, + '${binding.dartType}.fromWire(value as String)', + ) + : '${binding.dartType}.fromWire($access as String)', + _Kind.json => '$access as Object?', + }; +} + +String _nullableSwitch(String access, String conversion) => + 'switch ($access) { null => null, final Object value => $conversion }'; + +String _writeExpression( + String parameterName, + _Binding binding, { + required bool nullable, +}) { + final access = nullable ? '$parameterName?' : parameterName; + return switch (binding.kind) { + _Kind.dateTime => '$access.toIso8601String()', + _Kind.enumType => '$access.wireName', + _Kind.direct || _Kind.floating || _Kind.list || _Kind.json => parameterName, + }; +} + +/// Maps raw database names to unique Dart member identifiers. +/// +/// [reserved] seeds identifiers that must not be produced. When [existing] is +/// given, names are kept identical to it where possible so that, for example, +/// column tokens and row getters share their spelling. +Map _uniqueMemberNames( + List names, { + Set reserved = const {}, + Map? existing, +}) { + final used = {...reserved}; + final result = {}; + for (final name in names) { + var candidate = existing?[name] ?? memberIdentifier(name); + while (!used.add(candidate)) { + candidate = '$candidate\$'; + } + result[name] = candidate; + } + return result; +} + +void _writeDocComment( + StringBuffer buffer, + String? comment, { + String indent = '', +}) { + if (comment == null) return; + for (final line in comment.trim().split('\n')) { + buffer.writeln('$indent/// ${line.trim()}'); + } +} + +String _stringLiteral(String value) { + final escaped = value + .replaceAll(r'\', r'\\') + .replaceAll("'", r"\'") + .replaceAll(r'$', r'\$'); + return "'$escaped'"; +} diff --git a/packages/supabase_typegen/lib/src/identifiers.dart b/packages/supabase_typegen/lib/src/identifiers.dart new file mode 100644 index 000000000..130c435e5 --- /dev/null +++ b/packages/supabase_typegen/lib/src/identifiers.dart @@ -0,0 +1,132 @@ +const _reservedWords = { + 'abstract', + 'as', + 'assert', + 'async', + 'await', + 'base', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'covariant', + 'default', + 'deferred', + 'do', + 'dynamic', + 'else', + 'enum', + 'export', + 'extends', + 'extension', + 'external', + 'factory', + 'false', + 'final', + 'finally', + 'for', + 'get', + 'hide', + 'if', + 'implements', + 'import', + 'in', + 'interface', + 'is', + 'late', + 'library', + 'mixin', + 'new', + 'null', + 'of', + 'on', + 'operator', + 'part', + 'required', + 'rethrow', + 'return', + 'sealed', + 'set', + 'show', + 'static', + 'super', + 'switch', + 'sync', + 'this', + 'throw', + 'true', + 'try', + 'type', + 'typedef', + 'var', + 'void', + 'when', + 'while', + 'with', + 'yield', +}; + +/// Members that already exist on `Map`, which generated row +/// extension types implement, so column getters cannot use these names. +const _mapMembers = { + 'addAll', + 'addEntries', + 'cast', + 'clear', + 'containsKey', + 'containsValue', + 'entries', + 'forEach', + 'hashCode', + 'isEmpty', + 'isNotEmpty', + 'keys', + 'length', + 'map', + 'noSuchMethod', + 'putIfAbsent', + 'remove', + 'removeWhere', + 'runtimeType', + 'toString', + 'update', + 'updateAll', + 'values', +}; + +final _wordSeparator = RegExp('[^a-zA-Z0-9]+'); + +List _words(String name) => + name.split(_wordSeparator).where((word) => word.isNotEmpty).toList(); + +/// Converts [name] to PascalCase, for example `author_stats` to +/// `AuthorStats`. +String pascalCase(String name) { + final words = _words(name); + if (words.isEmpty) return r'$'; + final pascal = [ + for (final word in words) + word[0].toUpperCase() + word.substring(1).toLowerCase(), + ].join(); + return pascal.startsWith(RegExp('[0-9]')) ? '\$$pascal' : pascal; +} + +/// Converts [name] to camelCase, for example `created_at` to `createdAt`. +String camelCase(String name) { + final pascal = pascalCase(name); + return pascal[0].toLowerCase() + pascal.substring(1); +} + +/// Converts [name] to a valid Dart member identifier in camelCase. +/// +/// Reserved words and members that would collide with `Map` +/// get a `$` suffix, for example `class` becomes `class$`. +String memberIdentifier(String name) { + final identifier = camelCase(name); + if (_reservedWords.contains(identifier) || _mapMembers.contains(identifier)) { + return '$identifier\$'; + } + return identifier; +} diff --git a/packages/supabase_typegen/lib/src/openapi_parser.dart b/packages/supabase_typegen/lib/src/openapi_parser.dart new file mode 100644 index 000000000..3a7b838d8 --- /dev/null +++ b/packages/supabase_typegen/lib/src/openapi_parser.dart @@ -0,0 +1,105 @@ +import 'schema_description.dart'; + +final _foreignKeyPattern = RegExp(""); + +/// Parses the OpenAPI (Swagger 2.0) document that PostgREST serves at the +/// API root into a [SchemaDescription]. +/// +/// PostgREST encodes primary keys and foreign keys as `` and +/// `` markers inside column descriptions, and +/// lists `NOT NULL` columns without a database default under `required`. +SchemaDescription parseOpenApiDocument( + Map document, { + String schemaName = 'public', +}) { + final definitions = + document['definitions'] as Map? ?? const {}; + + final tables = []; + final enumsByQualifiedName = {}; + + for (final MapEntry(key: tableName, value: definition) + in definitions.entries) { + definition as Map; + final required = { + ...?(definition['required'] as List?)?.cast(), + }; + final properties = + definition['properties'] as Map? ?? const {}; + + final columns = []; + for (final MapEntry(key: columnName, value: property) + in properties.entries) { + property as Map; + final description = property['description'] as String?; + final format = property['format'] as String? ?? ''; + final enumValues = (property['enum'] as List?)?.cast(); + + if (enumValues != null) { + enumsByQualifiedName.putIfAbsent( + format, + () => EnumDescription(qualifiedName: format, values: enumValues), + ); + } + + final foreignKeyMatch = description == null + ? null + : _foreignKeyPattern.firstMatch(description); + + columns.add( + ColumnDescription( + name: columnName, + postgresFormat: format, + jsonType: property['type'] as String? ?? '', + arrayElementJsonType: + (property['items'] as Map?)?['type'] as String?, + enumValues: enumValues, + isRequired: required.contains(columnName), + isPrimaryKey: description?.contains('') ?? false, + hasDefault: property.containsKey('default'), + comment: _cleanComment(description), + foreignKey: foreignKeyMatch == null + ? null + : ForeignKeyDescription( + table: foreignKeyMatch.group(1)!, + column: foreignKeyMatch.group(2)!, + ), + ), + ); + } + + tables.add( + TableDescription( + name: tableName, + comment: _cleanComment(definition['description'] as String?), + columns: columns, + ), + ); + } + + tables.sort((a, b) => a.name.compareTo(b.name)); + final enums = enumsByQualifiedName.values.toList() + ..sort((a, b) => a.qualifiedName.compareTo(b.qualifiedName)); + + return SchemaDescription( + schemaName: schemaName, + tables: tables, + enums: enums, + ); +} + +/// Strips the PostgREST key markers from a column or table description, +/// keeping only the human written comment. +String? _cleanComment(String? description) { + if (description == null) return null; + final cleaned = description + .replaceAll(_foreignKeyPattern, '') + .replaceAll('', '') + .replaceAll(RegExp(r'Note:\s*'), '') + .replaceAll( + RegExp(r'This is a (Primary|Foreign) Key( to `[^`]+`)?\.'), + '', + ) + .trim(); + return cleaned.isEmpty ? null : cleaned; +} diff --git a/packages/supabase_typegen/lib/src/schema_description.dart b/packages/supabase_typegen/lib/src/schema_description.dart new file mode 100644 index 000000000..f986acae1 --- /dev/null +++ b/packages/supabase_typegen/lib/src/schema_description.dart @@ -0,0 +1,117 @@ +/// Description of a single database schema, the input to the code generator. +class SchemaDescription { + const SchemaDescription({ + required this.schemaName, + required this.tables, + required this.enums, + }); + + /// Name of the database schema, for example `public`. + final String schemaName; + + /// Tables and views of the schema, sorted by name. + final List tables; + + /// Postgres enums referenced by the tables, sorted by name. + final List enums; +} + +/// Description of a table or view. +class TableDescription { + const TableDescription({ + required this.name, + required this.columns, + this.comment, + }); + + /// Name of the table in the database. + final String name; + + /// The table comment, when one is set. + final String? comment; + + /// Columns of the table, in database order. + final List columns; +} + +/// Description of a single table column. +class ColumnDescription { + const ColumnDescription({ + required this.name, + required this.postgresFormat, + required this.jsonType, + required this.isRequired, + required this.isPrimaryKey, + required this.hasDefault, + this.arrayElementJsonType, + this.enumValues, + this.foreignKey, + this.comment, + }); + + /// Name of the column in the database. + final String name; + + /// The Postgres type, for example `bigint`, `text[]` or `public.mood`. + final String postgresFormat; + + /// The JSON schema type, for example `integer` or `string`. + final String jsonType; + + /// The JSON schema type of the array elements for array columns. + final String? arrayElementJsonType; + + /// The values of the Postgres enum for enum columns. + final List? enumValues; + + /// Whether the column is `NOT NULL` without a database default, which makes + /// it required on insert. + final bool isRequired; + + /// Whether the column is part of the primary key. + final bool isPrimaryKey; + + /// Whether the column has a database default. + final bool hasDefault; + + /// The column comment, when one is set. + final String? comment; + + /// The referenced table and column for foreign key columns. + final ForeignKeyDescription? foreignKey; + + /// Whether the column can be `null` in query results. + /// + /// Derived from the OpenAPI description: columns in the `required` list are + /// `NOT NULL`, and primary keys are always `NOT NULL`. Other columns are + /// treated as nullable, which is safe but over-approximates for `NOT NULL` + /// columns that have a database default. + bool get isNullable => !isRequired && !isPrimaryKey; +} + +/// The target of a foreign key column. +class ForeignKeyDescription { + const ForeignKeyDescription({required this.table, required this.column}); + + /// The referenced table. + final String table; + + /// The referenced column. + final String column; +} + +/// Description of a Postgres enum type. +class EnumDescription { + const EnumDescription({required this.qualifiedName, required this.values}); + + /// The schema-qualified name of the enum, for example `public.mood`. + final String qualifiedName; + + /// The values of the enum, in declaration order. + final List values; + + /// The enum name without the schema qualifier. + String get name => qualifiedName.contains('.') + ? qualifiedName.split('.').last + : qualifiedName; +} diff --git a/packages/supabase_typegen/lib/supabase_typegen.dart b/packages/supabase_typegen/lib/supabase_typegen.dart index 4b0d261bf..1373314f0 100644 --- a/packages/supabase_typegen/lib/supabase_typegen.dart +++ b/packages/supabase_typegen/lib/supabase_typegen.dart @@ -1,6 +1,8 @@ -/// Command-line code generator that turns a Supabase database schema into -/// typed Dart table definitions. -/// -/// This is a placeholder release that reserves the package name. The generator -/// implementation is not available yet. +/// Generates typed Supabase table definitions, row extension types and +/// column tokens from a database schema. library; + +export 'src/dart_generator.dart'; +export 'src/identifiers.dart'; +export 'src/openapi_parser.dart'; +export 'src/schema_description.dart'; diff --git a/packages/supabase_typegen/pubspec.yaml b/packages/supabase_typegen/pubspec.yaml index 7fe7b8966..d794da62e 100644 --- a/packages/supabase_typegen/pubspec.yaml +++ b/packages/supabase_typegen/pubspec.yaml @@ -14,6 +14,15 @@ environment: resolution: workspace +executables: + supabase_typegen: + +dependencies: + args: ^2.7.0 + dart_style: ^3.1.0 + http: ^1.6.0 + dev_dependencies: + postgrest: ^2.9.0 supabase_lints: ^0.1.1 test: ^1.25.0 diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart new file mode 100644 index 000000000..82ddbc115 --- /dev/null +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:supabase_typegen/supabase_typegen.dart'; +import 'package:test/test.dart'; + +final _whitespace = RegExp(r'\s+'); + +/// Collapses whitespace so the comparison is stable across formatter +/// versions; `tool/regenerate_goldens.dart` refreshes the golden. +String _normalize(String code) => code.replaceAll(_whitespace, ' ').trim(); + +void main() { + late SchemaDescription schema; + + setUpAll(() { + final document = + jsonDecode(File('test/fixtures/openapi.json').readAsStringSync()) + as Map; + schema = parseOpenApiDocument(document); + }); + + test('matches the golden output', () { + final golden = File('test/goldens/supabase_schema.dart').readAsStringSync(); + + expect( + _normalize(generateDartCode(schema)), + _normalize(golden), + reason: + 'The generator output changed. Regenerate the golden with ' + '`dart run tool/regenerate_goldens.dart` and review the diff.', + ); + }); + + test('respects a custom import', () { + final code = generateDartCode( + schema, + importUri: 'package:supabase_flutter/supabase_flutter.dart', + ); + + expect( + code, + contains("import 'package:supabase_flutter/supabase_flutter.dart';"), + ); + }); + + test('marks not null columns without default as required on insert', () { + final code = generateDartCode(schema); + + expect(code, contains('required String title')); + expect(code, contains('int? id')); + }); +} diff --git a/packages/supabase_typegen/test/fixtures/openapi.json b/packages/supabase_typegen/test/fixtures/openapi.json new file mode 100644 index 000000000..a64a325ad --- /dev/null +++ b/packages/supabase_typegen/test/fixtures/openapi.json @@ -0,0 +1,111 @@ +{ + "swagger": "2.0", + "info": { + "title": "PostgREST API", + "description": "standard public schema", + "version": "12.2.0" + }, + "definitions": { + "books": { + "description": "Books available in the library", + "required": ["title", "author_id"], + "properties": { + "id": { + "description": "Note:\nThis is a Primary Key.", + "format": "bigint", + "type": "integer", + "default": "nextval('books_id_seq'::regclass)" + }, + "title": { + "format": "text", + "type": "string" + }, + "author_id": { + "description": "Note:\nThis is a Foreign Key to `authors.id`.", + "format": "bigint", + "type": "integer" + }, + "price": { + "format": "numeric", + "type": "number" + }, + "rating": { + "format": "double precision", + "type": "number" + }, + "in_print": { + "format": "boolean", + "type": "boolean", + "default": true + }, + "mood": { + "enum": ["happy", "very happy", "sad"], + "format": "public.mood", + "type": "string" + }, + "tags": { + "format": "text[]", + "type": "array", + "items": { + "type": "string" + } + }, + "page_counts": { + "format": "integer[]", + "type": "array", + "items": { + "type": "integer" + } + }, + "metadata": { + "format": "jsonb" + }, + "cover_uuid": { + "format": "uuid", + "type": "string" + }, + "published_on": { + "format": "date", + "type": "string" + }, + "created_at": { + "description": "When the row was created", + "format": "timestamp with time zone", + "type": "string", + "default": "now()" + } + }, + "type": "object" + }, + "authors": { + "required": ["id", "name"], + "properties": { + "id": { + "description": "Note:\nThis is a Primary Key.", + "format": "bigint", + "type": "integer" + }, + "name": { + "format": "text", + "type": "string" + } + }, + "type": "object" + }, + "author_stats": { + "description": "Aggregated statistics per author", + "properties": { + "author_id": { + "format": "bigint", + "type": "integer" + }, + "book_count": { + "format": "bigint", + "type": "integer" + } + }, + "type": "object" + } + }, + "paths": {} +} diff --git a/packages/supabase_typegen/test/generated_schema_behavior_test.dart b/packages/supabase_typegen/test/generated_schema_behavior_test.dart new file mode 100644 index 000000000..512606b95 --- /dev/null +++ b/packages/supabase_typegen/test/generated_schema_behavior_test.dart @@ -0,0 +1,116 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:postgrest/postgrest.dart'; +import 'package:test/test.dart'; + +import 'goldens/supabase_schema.dart'; + +class MockHttpClient extends BaseClient { + String responseBody = '[]'; + BaseRequest? lastRequest; + String? lastRequestBody; + + @override + Future send(BaseRequest request) async { + lastRequest = request; + lastRequestBody = utf8.decode(await request.finalize().toBytes()); + return StreamedResponse( + Stream.value(utf8.encode(responseBody)), + 200, + headers: {'content-type': 'application/json'}, + request: request, + ); + } +} + +void main() { + late MockHttpClient httpClient; + late PostgrestClient client; + + setUp(() { + httpClient = MockHttpClient(); + client = PostgrestClient( + 'http://localhost/rest/v1', + httpClient: httpClient, + ); + }); + + tearDown(() async { + await client.dispose(); + }); + + test('select returns typed rows with converted values', () async { + httpClient.responseBody = jsonEncode([ + { + 'id': 1, + 'title': 'A typed row', + 'author_id': 7, + 'price': 12.5, + 'rating': 4, + 'mood': 'very happy', + 'tags': ['dart', 'types'], + 'metadata': {'reprint': true}, + 'created_at': '2026-07-23T10:00:00Z', + 'published_on': null, + }, + ]); + + final List books = await client.table(Books.table).select(); + + final book = books.single; + expect(book.id, 1); + expect(book.title, 'A typed row'); + expect(book.rating, 4.0); + expect(book.mood, Mood.veryHappy); + expect(book.tags, ['dart', 'types']); + expect(book.metadata, {'reprint': true}); + expect(book.createdAt, DateTime.utc(2026, 7, 23, 10)); + expect(book.publishedOn == null, isTrue); + }); + + test('enum column tokens filter with the wire name', () async { + await client.table(Books.table).select().where(Books.mood.eq(Mood.happy)); + + expect( + httpClient.lastRequest!.url.queryParameters['mood'], + 'eq.happy', + ); + }); + + test('insert sends converted values and omits absent columns', () async { + httpClient.responseBody = ''; + + await client + .table(Books.table) + .insert( + BooksInsert( + title: 'A typed row', + authorId: 7, + mood: Mood.happy, + createdAt: DateTime.utc(2026, 7, 23, 10), + ), + ); + + final sent = + jsonDecode(httpClient.lastRequestBody!) as Map; + expect(sent, { + 'title': 'A typed row', + 'author_id': 7, + 'mood': 'happy', + 'created_at': '2026-07-23T10:00:00.000Z', + }); + }); + + test('update sends only the provided columns', () async { + httpClient.responseBody = ''; + + await client + .table(Books.table) + .update(BooksUpdate(inPrint: false)) + .where(Books.id.eq(1)); + + expect(jsonDecode(httpClient.lastRequestBody!), {'in_print': false}); + expect(httpClient.lastRequest!.url.queryParameters['id'], 'eq.1'); + }); +} diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart new file mode 100644 index 000000000..d2add44cb --- /dev/null +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -0,0 +1,208 @@ +// Generated by supabase_typegen. Do not edit by hand. +// +// Source schema: public + +import 'package:postgrest/postgrest.dart'; + +/// Postgres enum `public.mood`. +enum Mood { + happy('happy'), + veryHappy('very happy'), + sad('sad'); + + const Mood(this.wireName); + + /// The value as stored in the database. + final String wireName; + + /// Parses the database representation of the enum. + static Mood fromWire(String wireName) => + values.firstWhere((value) => value.wireName == wireName); + + @override + String toString() => wireName; +} + +/// A row of the `author_stats` table. +/// Aggregated statistics per author +extension type const AuthorStatsRow(Map _json) + implements Map { + int? get authorId => _json['author_id'] as int?; + int? get bookCount => _json['book_count'] as int?; +} + +/// Values for inserting a row into `author_stats`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. +extension type const AuthorStatsInsert._(Map _json) + implements Map { + AuthorStatsInsert({int? authorId, int? bookCount}) + : this._({'author_id': ?authorId, 'book_count': ?bookCount}); +} + +/// Values for updating rows of `author_stats`. All columns are optional; passing `null` leaves the column unchanged. +extension type const AuthorStatsUpdate._(Map _json) + implements Map { + AuthorStatsUpdate({int? authorId, int? bookCount}) + : this._({'author_id': ?authorId, 'book_count': ?bookCount}); +} + +/// Typed access to the `author_stats` table. +class AuthorStats { + const AuthorStats._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable('author_stats', AuthorStatsRow.new); + + static const authorId = TableColumn('author_id'); + static const bookCount = TableColumn('book_count'); +} + +/// A row of the `authors` table. +extension type const AuthorsRow(Map _json) + implements Map { + int get id => _json['id'] as int; + String get name => _json['name'] as String; +} + +/// Values for inserting a row into `authors`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. +extension type const AuthorsInsert._(Map _json) + implements Map { + AuthorsInsert({required int id, required String name}) + : this._({'id': id, 'name': name}); +} + +/// Values for updating rows of `authors`. All columns are optional; passing `null` leaves the column unchanged. +extension type const AuthorsUpdate._(Map _json) + implements Map { + AuthorsUpdate({int? id, String? name}) : this._({'id': ?id, 'name': ?name}); +} + +/// Typed access to the `authors` table. +class Authors { + const Authors._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable('authors', AuthorsRow.new); + + static const id = TableColumn('id'); + static const name = TableColumn('name'); +} + +/// A row of the `books` table. +/// Books available in the library +extension type const BooksRow(Map _json) + implements Map { + int get id => _json['id'] as int; + String get title => _json['title'] as String; + int get authorId => _json['author_id'] as int; + num? get price => _json['price'] as num?; + double? get rating => (_json['rating'] as num?)?.toDouble(); + bool? get inPrint => _json['in_print'] as bool?; + Mood? get mood => switch (_json['mood']) { + null => null, + final Object value => Mood.fromWire(value as String), + }; + List? get tags => (_json['tags'] as List?)?.cast(); + List? get pageCounts => (_json['page_counts'] as List?)?.cast(); + Object? get metadata => _json['metadata'] as Object?; + String? get coverUuid => _json['cover_uuid'] as String?; + DateTime? get publishedOn => switch (_json['published_on']) { + null => null, + final Object value => DateTime.parse(value as String), + }; + + /// When the row was created + DateTime? get createdAt => switch (_json['created_at']) { + null => null, + final Object value => DateTime.parse(value as String), + }; +} + +/// Values for inserting a row into `books`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. +extension type const BooksInsert._(Map _json) + implements Map { + BooksInsert({ + int? id, + required String title, + required int authorId, + num? price, + double? rating, + bool? inPrint, + Mood? mood, + List? tags, + List? pageCounts, + Object? metadata, + String? coverUuid, + DateTime? publishedOn, + DateTime? createdAt, + }) : this._({ + 'id': ?id, + 'title': title, + 'author_id': authorId, + 'price': ?price, + 'rating': ?rating, + 'in_print': ?inPrint, + 'mood': ?mood?.wireName, + 'tags': ?tags, + 'page_counts': ?pageCounts, + 'metadata': ?metadata, + 'cover_uuid': ?coverUuid, + 'published_on': ?publishedOn?.toIso8601String(), + 'created_at': ?createdAt?.toIso8601String(), + }); +} + +/// Values for updating rows of `books`. All columns are optional; passing `null` leaves the column unchanged. +extension type const BooksUpdate._(Map _json) + implements Map { + BooksUpdate({ + int? id, + String? title, + int? authorId, + num? price, + double? rating, + bool? inPrint, + Mood? mood, + List? tags, + List? pageCounts, + Object? metadata, + String? coverUuid, + DateTime? publishedOn, + DateTime? createdAt, + }) : this._({ + 'id': ?id, + 'title': ?title, + 'author_id': ?authorId, + 'price': ?price, + 'rating': ?rating, + 'in_print': ?inPrint, + 'mood': ?mood?.wireName, + 'tags': ?tags, + 'page_counts': ?pageCounts, + 'metadata': ?metadata, + 'cover_uuid': ?coverUuid, + 'published_on': ?publishedOn?.toIso8601String(), + 'created_at': ?createdAt?.toIso8601String(), + }); +} + +/// Typed access to the `books` table. +class Books { + const Books._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable('books', BooksRow.new); + + static const id = TableColumn('id'); + static const title = TableColumn('title'); + static const authorId = TableColumn('author_id'); + static const price = TableColumn('price'); + static const rating = TableColumn('rating'); + static const inPrint = TableColumn('in_print'); + static const mood = TableColumn('mood'); + static const tags = TableColumn>('tags'); + static const pageCounts = TableColumn>('page_counts'); + static const metadata = TableColumn('metadata'); + static const coverUuid = TableColumn('cover_uuid'); + static const publishedOn = TableColumn('published_on'); + static const createdAt = TableColumn('created_at'); +} diff --git a/packages/supabase_typegen/test/identifiers_test.dart b/packages/supabase_typegen/test/identifiers_test.dart new file mode 100644 index 000000000..6907f96bb --- /dev/null +++ b/packages/supabase_typegen/test/identifiers_test.dart @@ -0,0 +1,40 @@ +import 'package:supabase_typegen/supabase_typegen.dart'; +import 'package:test/test.dart'; + +void main() { + group('pascalCase', () { + test('converts snake case names', () { + expect(pascalCase('author_stats'), 'AuthorStats'); + expect(pascalCase('books'), 'Books'); + expect(pascalCase('user-profiles'), 'UserProfiles'); + }); + + test('prefixes names starting with a digit', () { + expect(pascalCase('2fa_codes'), r'$2faCodes'); + }); + }); + + group('camelCase', () { + test('converts snake case names', () { + expect(camelCase('created_at'), 'createdAt'); + expect(camelCase('id'), 'id'); + }); + }); + + group('memberIdentifier', () { + test('suffixes reserved words', () { + expect(memberIdentifier('class'), r'class$'); + expect(memberIdentifier('in'), r'in$'); + }); + + test('suffixes Map member names', () { + expect(memberIdentifier('length'), r'length$'); + expect(memberIdentifier('keys'), r'keys$'); + }); + + test('keeps regular names untouched', () { + expect(memberIdentifier('title'), 'title'); + expect(memberIdentifier('author_id'), 'authorId'); + }); + }); +} diff --git a/packages/supabase_typegen/test/openapi_parser_test.dart b/packages/supabase_typegen/test/openapi_parser_test.dart new file mode 100644 index 000000000..44f5bf5e3 --- /dev/null +++ b/packages/supabase_typegen/test/openapi_parser_test.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:supabase_typegen/supabase_typegen.dart'; +import 'package:test/test.dart'; + +void main() { + late SchemaDescription schema; + + setUpAll(() { + final document = + jsonDecode(File('test/fixtures/openapi.json').readAsStringSync()) + as Map; + schema = parseOpenApiDocument(document); + }); + + test('parses all tables sorted by name', () { + expect(schema.tables.map((table) => table.name), [ + 'author_stats', + 'authors', + 'books', + ]); + }); + + test('parses table comments', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + expect(books.comment, 'Books available in the library'); + }); + + test('parses primary keys, requiredness and defaults', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + final id = books.columns.singleWhere((column) => column.name == 'id'); + expect(id.isPrimaryKey, isTrue); + expect(id.isRequired, isFalse); + expect(id.hasDefault, isTrue); + expect(id.isNullable, isFalse); + + final title = books.columns.singleWhere((column) => column.name == 'title'); + expect(title.isRequired, isTrue); + expect(title.isNullable, isFalse); + + final price = books.columns.singleWhere((column) => column.name == 'price'); + expect(price.isRequired, isFalse); + expect(price.isNullable, isTrue); + }); + + test('parses foreign keys', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + final authorId = books.columns.singleWhere( + (column) => column.name == 'author_id', + ); + expect(authorId.foreignKey?.table, 'authors'); + expect(authorId.foreignKey?.column, 'id'); + }); + + test('collects Postgres enums', () { + expect(schema.enums, hasLength(1)); + final mood = schema.enums.single; + expect(mood.qualifiedName, 'public.mood'); + expect(mood.name, 'mood'); + expect(mood.values, ['happy', 'very happy', 'sad']); + }); + + test('parses array columns', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + final tags = books.columns.singleWhere((column) => column.name == 'tags'); + expect(tags.postgresFormat, 'text[]'); + expect(tags.arrayElementJsonType, 'string'); + }); + + test('keeps human column comments without the key markers', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + final id = books.columns.singleWhere((column) => column.name == 'id'); + expect(id.comment, isNull); + + final createdAt = books.columns.singleWhere( + (column) => column.name == 'created_at', + ); + expect(createdAt.comment, 'When the row was created'); + }); +} diff --git a/packages/supabase_typegen/test/supabase_typegen_test.dart b/packages/supabase_typegen/test/supabase_typegen_test.dart deleted file mode 100644 index 7ff71c15d..000000000 --- a/packages/supabase_typegen/test/supabase_typegen_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:test/test.dart'; - -void main() { - test('supabase_typegen placeholder', () { - expect(true, isTrue); - }); -} diff --git a/packages/supabase_typegen/tool/regenerate_goldens.dart b/packages/supabase_typegen/tool/regenerate_goldens.dart new file mode 100644 index 000000000..9d453d5dd --- /dev/null +++ b/packages/supabase_typegen/tool/regenerate_goldens.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:supabase_typegen/supabase_typegen.dart'; + +/// Regenerates the golden files under `test/goldens` from the fixtures. +/// +/// Run from the package root with `dart run tool/regenerate_goldens.dart`. +void main() { + final document = + jsonDecode(File('test/fixtures/openapi.json').readAsStringSync()) + as Map; + final schema = parseOpenApiDocument(document); + File( + 'test/goldens/supabase_schema.dart', + ).writeAsStringSync(generateDartCode(schema)); +} From 064bb5c7e2bd007290291269b986981146e1c03b Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 23 Jul 2026 09:16:48 +0200 Subject: [PATCH 02/31] chore: trigger CI From 2432ebbde7975e672b955582a1c9e405437d3f2b Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 23 Jul 2026 09:56:42 +0200 Subject: [PATCH 03/31] refactor: model column type kinds as an enum in the schema description --- .../lib/src/dart_generator.dart | 135 ++++++++---------- .../lib/src/openapi_parser.dart | 67 ++++++++- .../lib/src/schema_description.dart | 49 ++++++- .../test/openapi_parser_test.dart | 19 ++- 4 files changed, 182 insertions(+), 88 deletions(-) diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index 12492285b..ac3e27df5 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -3,35 +3,14 @@ import 'package:dart_style/dart_style.dart'; import 'identifiers.dart'; import 'schema_description.dart'; -enum _Kind { direct, floating, dateTime, list, enumType, json } - class _Binding { const _Binding(this.dartType, this.kind); /// The non-nullable Dart type of the column. final String dartType; - final _Kind kind; + final ColumnTypeKind kind; } -const _integerFormats = { - 'smallint', - 'integer', - 'bigint', - 'int2', - 'int4', - 'int8', -}; -const _floatingFormats = {'real', 'double precision', 'float4', 'float8'}; -const _numericFormats = {'numeric', 'decimal'}; -const _dateTimeFormats = { - 'date', - 'timestamp', - 'timestamp without time zone', - 'timestamp with time zone', - 'timestamptz', -}; -const _jsonFormats = {'json', 'jsonb'}; - /// Generates a Dart source file with typed table definitions, row extension /// types, insert and update value types, column tokens and Postgres enums for /// [schema]. @@ -274,56 +253,45 @@ void _writeNamespace( _Binding _bindingFor( ColumnDescription column, Map enumTypeNames, -) { - final format = column.postgresFormat; - - final enumTypeName = enumTypeNames[format]; - if (enumTypeName != null) { - return _Binding(enumTypeName, _Kind.enumType); - } - if (format.endsWith('[]')) { - return _Binding( - 'List<${_arrayElementType(column.arrayElementJsonType)}>', - _Kind.list, - ); - } - if (_integerFormats.contains(format)) { - return const _Binding('int', _Kind.direct); - } - if (_floatingFormats.contains(format)) { - return const _Binding('double', _Kind.floating); - } - if (_numericFormats.contains(format)) { - return const _Binding('num', _Kind.direct); - } - if (format == 'boolean') { - return const _Binding('bool', _Kind.direct); - } - if (_dateTimeFormats.contains(format)) { - return const _Binding('DateTime', _Kind.dateTime); - } - if (_jsonFormats.contains(format)) { - return const _Binding('Object', _Kind.json); - } - return switch (column.jsonType) { - 'integer' => const _Binding('int', _Kind.direct), - 'number' => const _Binding('num', _Kind.direct), - 'boolean' => const _Binding('bool', _Kind.direct), - 'string' => const _Binding('String', _Kind.direct), - _ => const _Binding('Object', _Kind.json), - }; -} - -String _arrayElementType(String? elementJsonType) => switch (elementJsonType) { - 'integer' => 'int', - 'number' => 'num', - 'boolean' => 'bool', - 'string' => 'String', - _ => 'Object', +) => switch (column.typeKind) { + ColumnTypeKind.enumType => _Binding( + enumTypeNames[column.postgresFormat]!, + ColumnTypeKind.enumType, + ), + ColumnTypeKind.array => _Binding( + 'List<${_elementDartType(column.elementTypeKind)}>', + ColumnTypeKind.array, + ), + ColumnTypeKind.integer => const _Binding('int', ColumnTypeKind.integer), + ColumnTypeKind.floating => const _Binding('double', ColumnTypeKind.floating), + ColumnTypeKind.numeric => const _Binding('num', ColumnTypeKind.numeric), + ColumnTypeKind.boolean => const _Binding('bool', ColumnTypeKind.boolean), + ColumnTypeKind.dateTime => const _Binding( + 'DateTime', + ColumnTypeKind.dateTime, + ), + ColumnTypeKind.text => const _Binding('String', ColumnTypeKind.text), + ColumnTypeKind.json || + ColumnTypeKind.unknown => const _Binding('Object', ColumnTypeKind.json), }; +String _elementDartType(ColumnTypeKind? elementTypeKind) => + switch (elementTypeKind) { + ColumnTypeKind.integer => 'int', + ColumnTypeKind.floating => 'double', + ColumnTypeKind.numeric => 'num', + ColumnTypeKind.boolean => 'bool', + ColumnTypeKind.text => 'String', + ColumnTypeKind.dateTime || + ColumnTypeKind.json || + ColumnTypeKind.enumType || + ColumnTypeKind.array || + ColumnTypeKind.unknown || + null => 'Object', + }; + String _getterType(ColumnDescription column, _Binding binding) { - if (binding.kind == _Kind.json) return 'Object?'; + if (binding.kind == ColumnTypeKind.json) return 'Object?'; return column.isNullable ? '${binding.dartType}?' : binding.dartType; } @@ -331,27 +299,31 @@ String _readExpression(ColumnDescription column, _Binding binding) { final access = "_json[${_stringLiteral(column.name)}]"; final nullable = column.isNullable; return switch (binding.kind) { - _Kind.direct => '$access as ${binding.dartType}${nullable ? '?' : ''}', - _Kind.floating => + ColumnTypeKind.integer || + ColumnTypeKind.numeric || + ColumnTypeKind.boolean || + ColumnTypeKind.text => + '$access as ${binding.dartType}${nullable ? '?' : ''}', + ColumnTypeKind.floating => nullable ? '($access as num?)?.toDouble()' : '($access as num).toDouble()', - _Kind.list => + ColumnTypeKind.array => nullable ? '($access as List?)?.cast()' : '($access as List).cast()', - _Kind.dateTime => + ColumnTypeKind.dateTime => nullable ? _nullableSwitch(access, 'DateTime.parse(value as String)') : 'DateTime.parse($access as String)', - _Kind.enumType => + ColumnTypeKind.enumType => nullable ? _nullableSwitch( access, '${binding.dartType}.fromWire(value as String)', ) : '${binding.dartType}.fromWire($access as String)', - _Kind.json => '$access as Object?', + ColumnTypeKind.json || ColumnTypeKind.unknown => '$access as Object?', }; } @@ -365,9 +337,16 @@ String _writeExpression( }) { final access = nullable ? '$parameterName?' : parameterName; return switch (binding.kind) { - _Kind.dateTime => '$access.toIso8601String()', - _Kind.enumType => '$access.wireName', - _Kind.direct || _Kind.floating || _Kind.list || _Kind.json => parameterName, + ColumnTypeKind.dateTime => '$access.toIso8601String()', + ColumnTypeKind.enumType => '$access.wireName', + ColumnTypeKind.integer || + ColumnTypeKind.floating || + ColumnTypeKind.numeric || + ColumnTypeKind.boolean || + ColumnTypeKind.text || + ColumnTypeKind.array || + ColumnTypeKind.json || + ColumnTypeKind.unknown => parameterName, }; } diff --git a/packages/supabase_typegen/lib/src/openapi_parser.dart b/packages/supabase_typegen/lib/src/openapi_parser.dart index 3a7b838d8..0b37d5b20 100644 --- a/packages/supabase_typegen/lib/src/openapi_parser.dart +++ b/packages/supabase_typegen/lib/src/openapi_parser.dart @@ -2,6 +2,62 @@ import 'schema_description.dart'; final _foreignKeyPattern = RegExp(""); +const _integerFormats = { + 'smallint', + 'integer', + 'bigint', + 'int2', + 'int4', + 'int8', +}; +const _floatingFormats = {'real', 'double precision', 'float4', 'float8'}; +const _numericFormats = {'numeric', 'decimal'}; +const _dateTimeFormats = { + 'date', + 'timestamp', + 'timestamp without time zone', + 'timestamp with time zone', + 'timestamptz', +}; +const _jsonFormats = {'json', 'jsonb'}; + +/// Derives the [ColumnTypeKind] from the Postgres [format] and JSON schema +/// [jsonType] of a column. This is the single place where type names are +/// compared as strings; everything downstream works with the enum. +ColumnTypeKind _typeKind({ + required String format, + required String? jsonType, + required bool isEnum, +}) { + if (isEnum) return ColumnTypeKind.enumType; + if (format.endsWith('[]') || jsonType == 'array') { + return ColumnTypeKind.array; + } + if (_integerFormats.contains(format)) return ColumnTypeKind.integer; + if (_floatingFormats.contains(format)) return ColumnTypeKind.floating; + if (_numericFormats.contains(format)) return ColumnTypeKind.numeric; + if (format == 'boolean') return ColumnTypeKind.boolean; + if (_dateTimeFormats.contains(format)) return ColumnTypeKind.dateTime; + if (_jsonFormats.contains(format)) return ColumnTypeKind.json; + return switch (jsonType) { + 'integer' => ColumnTypeKind.integer, + 'number' => ColumnTypeKind.numeric, + 'boolean' => ColumnTypeKind.boolean, + 'string' => ColumnTypeKind.text, + _ => ColumnTypeKind.unknown, + }; +} + +ColumnTypeKind? _elementTypeKind(String? itemsJsonType) => itemsJsonType == null + ? null + : switch (itemsJsonType) { + 'integer' => ColumnTypeKind.integer, + 'number' => ColumnTypeKind.numeric, + 'boolean' => ColumnTypeKind.boolean, + 'string' => ColumnTypeKind.text, + _ => ColumnTypeKind.unknown, + }; + /// Parses the OpenAPI (Swagger 2.0) document that PostgREST serves at the /// API root into a [SchemaDescription]. /// @@ -50,9 +106,14 @@ SchemaDescription parseOpenApiDocument( ColumnDescription( name: columnName, postgresFormat: format, - jsonType: property['type'] as String? ?? '', - arrayElementJsonType: - (property['items'] as Map?)?['type'] as String?, + typeKind: _typeKind( + format: format, + jsonType: property['type'] as String?, + isEnum: enumValues != null, + ), + elementTypeKind: _elementTypeKind( + (property['items'] as Map?)?['type'] as String?, + ), enumValues: enumValues, isRequired: required.contains(columnName), isPrimaryKey: description?.contains('') ?? false, diff --git a/packages/supabase_typegen/lib/src/schema_description.dart b/packages/supabase_typegen/lib/src/schema_description.dart index f986acae1..e94c64650 100644 --- a/packages/supabase_typegen/lib/src/schema_description.dart +++ b/packages/supabase_typegen/lib/src/schema_description.dart @@ -1,3 +1,39 @@ +/// The Dart-relevant type of a column, derived from the Postgres type at +/// parse time so that later stages never have to compare type name strings. +enum ColumnTypeKind { + /// Whole number types such as `smallint`, `integer` and `bigint`. + integer, + + /// Floating point types such as `real` and `double precision`. + floating, + + /// Arbitrary precision types such as `numeric`, mapped to `num` since the + /// decoded JSON value may be either an integer or a double. + numeric, + + /// The `boolean` type. + boolean, + + /// Date and timestamp types, mapped to `DateTime`. + dateTime, + + /// Types carried as text, such as `text`, `uuid` and `character varying`. + text, + + /// The `json` and `jsonb` types, mapped to `Object?`. + json, + + /// A Postgres enum type. + enumType, + + /// An array type; the element type is in + /// [ColumnDescription.elementTypeKind]. + array, + + /// A type without a specific mapping, treated like [json]. + unknown, +} + /// Description of a single database schema, the input to the code generator. class SchemaDescription { const SchemaDescription({ @@ -39,11 +75,11 @@ class ColumnDescription { const ColumnDescription({ required this.name, required this.postgresFormat, - required this.jsonType, + required this.typeKind, required this.isRequired, required this.isPrimaryKey, required this.hasDefault, - this.arrayElementJsonType, + this.elementTypeKind, this.enumValues, this.foreignKey, this.comment, @@ -55,11 +91,12 @@ class ColumnDescription { /// The Postgres type, for example `bigint`, `text[]` or `public.mood`. final String postgresFormat; - /// The JSON schema type, for example `integer` or `string`. - final String jsonType; + /// The kind of Dart type the column maps to. + final ColumnTypeKind typeKind; - /// The JSON schema type of the array elements for array columns. - final String? arrayElementJsonType; + /// The kind of Dart type of the array elements for [ColumnTypeKind.array] + /// columns. + final ColumnTypeKind? elementTypeKind; /// The values of the Postgres enum for enum columns. final List? enumValues; diff --git a/packages/supabase_typegen/test/openapi_parser_test.dart b/packages/supabase_typegen/test/openapi_parser_test.dart index 44f5bf5e3..a3129d360 100644 --- a/packages/supabase_typegen/test/openapi_parser_test.dart +++ b/packages/supabase_typegen/test/openapi_parser_test.dart @@ -53,6 +53,22 @@ void main() { expect(authorId.foreignKey?.column, 'id'); }); + test('derives type kinds from formats', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + ColumnTypeKind kindOf(String name) => + books.columns.singleWhere((column) => column.name == name).typeKind; + + expect(kindOf('id'), ColumnTypeKind.integer); + expect(kindOf('title'), ColumnTypeKind.text); + expect(kindOf('price'), ColumnTypeKind.numeric); + expect(kindOf('rating'), ColumnTypeKind.floating); + expect(kindOf('in_print'), ColumnTypeKind.boolean); + expect(kindOf('mood'), ColumnTypeKind.enumType); + expect(kindOf('metadata'), ColumnTypeKind.json); + expect(kindOf('created_at'), ColumnTypeKind.dateTime); + expect(kindOf('cover_uuid'), ColumnTypeKind.text); + }); + test('collects Postgres enums', () { expect(schema.enums, hasLength(1)); final mood = schema.enums.single; @@ -65,7 +81,8 @@ void main() { final books = schema.tables.singleWhere((table) => table.name == 'books'); final tags = books.columns.singleWhere((column) => column.name == 'tags'); expect(tags.postgresFormat, 'text[]'); - expect(tags.arrayElementJsonType, 'string'); + expect(tags.typeKind, ColumnTypeKind.array); + expect(tags.elementTypeKind, ColumnTypeKind.text); }); test('keeps human column comments without the key markers', () { From 71fbaef854c0f5433e0692cd89c82a9aba9d3bf7 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 23 Jul 2026 10:04:36 +0200 Subject: [PATCH 04/31] chore: trigger CI From 80b59d527f2561f3a332e1600df37823da88104e Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 23 Jul 2026 11:18:30 +0200 Subject: [PATCH 05/31] fix(supabase_typegen): review fixes for wire correctness and CLI robustness --- packages/supabase_typegen/README.md | 22 +++-- .../bin/supabase_typegen.dart | 41 +++++++-- .../lib/src/dart_generator.dart | 85 ++++++++++++++++--- .../supabase_typegen/lib/src/identifiers.dart | 7 +- .../lib/src/openapi_parser.dart | 32 +++---- .../lib/src/schema_description.dart | 13 ++- .../test/fixtures/openapi.json | 20 ++++- .../test/generated_schema_behavior_test.dart | 39 +++++++++ .../test/goldens/supabase_schema.dart | 44 ++++++++-- .../test/identifiers_test.dart | 6 ++ .../test/openapi_parser_test.dart | 4 +- 11 files changed, 254 insertions(+), 59 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 6e1decb3a..98ca3d8cb 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -27,8 +27,8 @@ environment variables. Use `--schema` to generate for a schema other than `PostgrestTable` and `TableColumn` from. The schema is read from the OpenAPI description that PostgREST serves at the -API root, so the key only needs read access; tables hidden from the key by -row level security settings are not included. +API root, so the key only needs read access; tables whose role lacks +privileges (grants, not row level security) are not included. ## Generated code in action @@ -45,7 +45,17 @@ await client.table(Books.table).insert( ## Known limitations -The OpenAPI description does not distinguish nullable columns from `NOT NULL` -columns with a database default, so getters for defaulted columns other than -primary keys are conservatively nullable. Foreign key relationship getters -and typed functions (rpc) are not generated yet. +- The OpenAPI description does not distinguish nullable columns from + `NOT NULL` columns with a database default, so getters for defaulted + columns other than primary keys are conservatively nullable. +- Passing `null` to an `Insert`/`Update` parameter omits the column. To write + SQL NULL explicitly, set the raw key: `BooksUpdate()..['price'] = null`. +- Array elements are assumed non-null (`text[]` maps to `List`), + matching the supabase-js type generator; arrays containing SQL NULL + elements throw when the element is read. Enum array columns degrade to + `List`. +- `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. diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index 962cbe12d..e04e9dc00 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -38,7 +38,13 @@ final _argParser = ArgParser() ) ..addFlag('help', abbr: 'h', negatable: false, help: 'Show this usage.'); -Future main(List arguments) async { +Future main(List 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 _run(List arguments) async { final ArgResults options; try { options = _argParser.parse(arguments); @@ -72,7 +78,8 @@ Future main(List arguments) async { } final schemaName = options.option('schema')!; - final endpoint = Uri.parse('$url/rest/v1/'); + final baseUrl = url.replaceAll(RegExp(r'/+$'), ''); + final endpoint = Uri.parse('$baseUrl/rest/v1/'); final http.Response response; try { response = await http.get( @@ -95,19 +102,37 @@ Future main(List arguments) async { return 1; } - final schema = parseOpenApiDocument( - jsonDecode(response.body) as Map, - schemaName: schemaName, - ); + final Map document; + try { + document = + jsonDecode(utf8.decode(response.bodyBytes)) as Map; + } on FormatException catch (error) { + stderr.writeln('The response from $endpoint is not valid JSON: $error'); + return 1; + } on TypeError { + stderr.writeln( + 'The response from $endpoint is not an OpenAPI document. Check that ' + 'the URL points to a Supabase project or PostgREST instance.', + ); + return 1; + } + + final schema = parseOpenApiDocument(document, schemaName: schemaName); final code = generateDartCode(schema, importUri: options.option('import')!); final outputFile = File(options.option('output')!); outputFile.parent.createSync(recursive: true); outputFile.writeAsStringSync(code); + final emittedTables = schema.tables + .where((table) => table.columns.isNotEmpty) + .length; + final skippedTables = schema.tables.length - emittedTables; stdout.writeln( - 'Generated ${outputFile.path} with ${schema.tables.length} tables and ' - '${schema.enums.length} enums from schema "$schemaName".', + 'Generated ${outputFile.path} with $emittedTables tables and ' + '${schema.enums.length} enums from schema "$schemaName".' + '${skippedTables == 0 ? '' : ' Skipped $skippedTables tables ' + 'without columns.'}', ); return 0; } diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index ac3e27df5..d032fcd2e 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -22,6 +22,11 @@ String generateDartCode( SchemaDescription schema, { String importUri = 'package:postgrest/postgrest.dart', }) { + final usesDateColumns = schema.tables.any( + (table) => table.columns.any( + (column) => column.typeKind == ColumnTypeKind.date, + ), + ); final buffer = StringBuffer() ..writeln('// Generated by supabase_typegen. Do not edit by hand.') ..writeln('//') @@ -44,6 +49,15 @@ String generateDartCode( _writeTable(buffer, table, typeNames, enumTypeNames); } + if (usesDateColumns) { + buffer + ..writeln('String _dateString(DateTime date) =>') + ..writeln(" '\${date.year.toString().padLeft(4, '0')}-'") + ..writeln(" '\${date.month.toString().padLeft(2, '0')}-'") + ..writeln(" '\${date.day.toString().padLeft(2, '0')}';") + ..writeln(); + } + return DartFormatter( languageVersion: DartFormatter.latestLanguageVersion, ).format(buffer.toString()); @@ -67,7 +81,21 @@ void _writeEnum( EnumDescription enumDescription, String typeName, ) { - final valueNames = _uniqueMemberNames(enumDescription.values); + final valueNames = _uniqueMemberNames( + enumDescription.values, + reserved: { + typeName, + 'index', + 'name', + 'values', + 'wireName', + 'fromWire', + 'toString', + 'hashCode', + 'runtimeType', + 'noSuchMethod', + }, + ); buffer ..writeln('/// Postgres enum `${enumDescription.qualifiedName}`.') @@ -85,7 +113,14 @@ void _writeEnum( ..writeln() ..writeln(' /// Parses the database representation of the enum.') ..writeln(' static $typeName fromWire(String wireName) =>') - ..writeln(' values.firstWhere((value) => value.wireName == wireName);') + ..writeln(' values.firstWhere(') + ..writeln(' (value) => value.wireName == wireName,') + ..writeln(' orElse: () => throw ArgumentError.value(') + ..writeln(' wireName,') + ..writeln(" 'wireName',") + ..writeln(" 'No $typeName value with this wire name',") + ..writeln(' ),') + ..writeln(' );') ..writeln() ..writeln(' @override') ..writeln(' String toString() => wireName;') @@ -105,9 +140,10 @@ void _writeTable( final updateType = typeNames.claim('${baseName}Update'); final namespaceType = typeNames.claim(baseName); - final memberNames = _uniqueMemberNames([ - for (final column in table.columns) column.name, - ]); + final memberNames = _uniqueMemberNames( + [for (final column in table.columns) column.name], + reserved: {rowType, insertType, updateType}, + ); final bindings = { for (final column in table.columns) column.name: _bindingFor(column, enumTypeNames), @@ -136,7 +172,9 @@ void _writeTable( requireRequiredColumns: false, docLine: 'Values for updating rows of `${table.name}`. All columns are ' - 'optional; passing `null` leaves the column unchanged.', + 'optional; passing `null` omits the column, leaving it unchanged. ' + 'To write SQL NULL explicitly, set the raw key: ' + '`$updateType()..[\'column_name\'] = null`.', ); _writeNamespace(buffer, table, namespaceType, rowType, memberNames, bindings); } @@ -223,7 +261,7 @@ void _writeNamespace( ) { final columnNames = _uniqueMemberNames( [for (final column in table.columns) column.name], - reserved: {'table'}, + reserved: {'table', namespaceType}, existing: memberNames, ); @@ -266,9 +304,14 @@ _Binding _bindingFor( ColumnTypeKind.floating => const _Binding('double', ColumnTypeKind.floating), ColumnTypeKind.numeric => const _Binding('num', ColumnTypeKind.numeric), ColumnTypeKind.boolean => const _Binding('bool', ColumnTypeKind.boolean), - ColumnTypeKind.dateTime => const _Binding( + ColumnTypeKind.date => const _Binding('DateTime', ColumnTypeKind.date), + ColumnTypeKind.timestamp => const _Binding( + 'DateTime', + ColumnTypeKind.timestamp, + ), + ColumnTypeKind.timestampWithTimeZone => const _Binding( 'DateTime', - ColumnTypeKind.dateTime, + ColumnTypeKind.timestampWithTimeZone, ), ColumnTypeKind.text => const _Binding('String', ColumnTypeKind.text), ColumnTypeKind.json || @@ -282,7 +325,9 @@ String _elementDartType(ColumnTypeKind? elementTypeKind) => ColumnTypeKind.numeric => 'num', ColumnTypeKind.boolean => 'bool', ColumnTypeKind.text => 'String', - ColumnTypeKind.dateTime || + ColumnTypeKind.date || + ColumnTypeKind.timestamp || + ColumnTypeKind.timestampWithTimeZone || ColumnTypeKind.json || ColumnTypeKind.enumType || ColumnTypeKind.array || @@ -312,7 +357,9 @@ String _readExpression(ColumnDescription column, _Binding binding) { nullable ? '($access as List?)?.cast()' : '($access as List).cast()', - ColumnTypeKind.dateTime => + ColumnTypeKind.date || + ColumnTypeKind.timestamp || + ColumnTypeKind.timestampWithTimeZone => nullable ? _nullableSwitch(access, 'DateTime.parse(value as String)') : 'DateTime.parse($access as String)', @@ -337,7 +384,16 @@ String _writeExpression( }) { final access = nullable ? '$parameterName?' : parameterName; return switch (binding.kind) { - ColumnTypeKind.dateTime => '$access.toIso8601String()', + ColumnTypeKind.date => + nullable + ? 'switch ($parameterName) ' + '{ null => null, final value => _dateString(value) }' + : '_dateString($parameterName)', + ColumnTypeKind.timestamp => '$access.toIso8601String()', + ColumnTypeKind.timestampWithTimeZone => + nullable + ? '$access.toUtc().toIso8601String()' + : '$parameterName.toUtc().toIso8601String()', ColumnTypeKind.enumType => '$access.wireName', ColumnTypeKind.integer || ColumnTypeKind.floating || @@ -387,6 +443,9 @@ String _stringLiteral(String value) { final escaped = value .replaceAll(r'\', r'\\') .replaceAll("'", r"\'") - .replaceAll(r'$', r'\$'); + .replaceAll(r'$', r'\$') + .replaceAll('\n', r'\n') + .replaceAll('\r', r'\r') + .replaceAll('\t', r'\t'); return "'$escaped'"; } diff --git a/packages/supabase_typegen/lib/src/identifiers.dart b/packages/supabase_typegen/lib/src/identifiers.dart index 130c435e5..0c8dcb8b9 100644 --- a/packages/supabase_typegen/lib/src/identifiers.dart +++ b/packages/supabase_typegen/lib/src/identifiers.dart @@ -97,9 +97,12 @@ const _mapMembers = { }; final _wordSeparator = RegExp('[^a-zA-Z0-9]+'); +final _camelHumpBoundary = RegExp('(?<=[a-z0-9])(?=[A-Z])'); -List _words(String name) => - name.split(_wordSeparator).where((word) => word.isNotEmpty).toList(); +List _words(String name) => [ + for (final part in name.split(_wordSeparator)) + ...part.split(_camelHumpBoundary), +].where((word) => word.isNotEmpty).toList(); /// Converts [name] to PascalCase, for example `author_stats` to /// `AuthorStats`. diff --git a/packages/supabase_typegen/lib/src/openapi_parser.dart b/packages/supabase_typegen/lib/src/openapi_parser.dart index 0b37d5b20..44925d9c1 100644 --- a/packages/supabase_typegen/lib/src/openapi_parser.dart +++ b/packages/supabase_typegen/lib/src/openapi_parser.dart @@ -12,10 +12,8 @@ const _integerFormats = { }; const _floatingFormats = {'real', 'double precision', 'float4', 'float8'}; const _numericFormats = {'numeric', 'decimal'}; -const _dateTimeFormats = { - 'date', - 'timestamp', - 'timestamp without time zone', +const _timestampFormats = {'timestamp', 'timestamp without time zone'}; +const _timestampWithTimeZoneFormats = { 'timestamp with time zone', 'timestamptz', }; @@ -29,15 +27,19 @@ ColumnTypeKind _typeKind({ required String? jsonType, required bool isEnum, }) { - if (isEnum) return ColumnTypeKind.enumType; if (format.endsWith('[]') || jsonType == 'array') { return ColumnTypeKind.array; } + if (isEnum) return ColumnTypeKind.enumType; if (_integerFormats.contains(format)) return ColumnTypeKind.integer; if (_floatingFormats.contains(format)) return ColumnTypeKind.floating; if (_numericFormats.contains(format)) return ColumnTypeKind.numeric; if (format == 'boolean') return ColumnTypeKind.boolean; - if (_dateTimeFormats.contains(format)) return ColumnTypeKind.dateTime; + if (format == 'date') return ColumnTypeKind.date; + if (_timestampFormats.contains(format)) return ColumnTypeKind.timestamp; + if (_timestampWithTimeZoneFormats.contains(format)) { + return ColumnTypeKind.timestampWithTimeZone; + } if (_jsonFormats.contains(format)) return ColumnTypeKind.json; return switch (jsonType) { 'integer' => ColumnTypeKind.integer, @@ -76,7 +78,7 @@ SchemaDescription parseOpenApiDocument( for (final MapEntry(key: tableName, value: definition) in definitions.entries) { - definition as Map; + if (definition is! Map) continue; final required = { ...?(definition['required'] as List?)?.cast(), }; @@ -90,8 +92,13 @@ SchemaDescription parseOpenApiDocument( final description = property['description'] as String?; final format = property['format'] as String? ?? ''; final enumValues = (property['enum'] as List?)?.cast(); + final typeKind = _typeKind( + format: format, + jsonType: property['type'] as String?, + isEnum: enumValues != null, + ); - if (enumValues != null) { + if (enumValues != null && typeKind == ColumnTypeKind.enumType) { enumsByQualifiedName.putIfAbsent( format, () => EnumDescription(qualifiedName: format, values: enumValues), @@ -106,11 +113,7 @@ SchemaDescription parseOpenApiDocument( ColumnDescription( name: columnName, postgresFormat: format, - typeKind: _typeKind( - format: format, - jsonType: property['type'] as String?, - isEnum: enumValues != null, - ), + typeKind: typeKind, elementTypeKind: _elementTypeKind( (property['items'] as Map?)?['type'] as String?, ), @@ -156,9 +159,8 @@ String? _cleanComment(String? description) { final cleaned = description .replaceAll(_foreignKeyPattern, '') .replaceAll('', '') - .replaceAll(RegExp(r'Note:\s*'), '') .replaceAll( - RegExp(r'This is a (Primary|Foreign) Key( to `[^`]+`)?\.'), + RegExp(r'Note:\s*This is a (Primary|Foreign) Key( to `[^`]+`)?\.'), '', ) .trim(); diff --git a/packages/supabase_typegen/lib/src/schema_description.dart b/packages/supabase_typegen/lib/src/schema_description.dart index e94c64650..6ab5a59b4 100644 --- a/packages/supabase_typegen/lib/src/schema_description.dart +++ b/packages/supabase_typegen/lib/src/schema_description.dart @@ -14,8 +14,17 @@ enum ColumnTypeKind { /// The `boolean` type. boolean, - /// Date and timestamp types, mapped to `DateTime`. - dateTime, + /// The `date` type, mapped to `DateTime` and written back date-only so + /// the calendar date never shifts with the client timezone. + date, + + /// Timestamps without a timezone, mapped to `DateTime` and written back as + /// the local wall time. + timestamp, + + /// Timestamps with a timezone, mapped to `DateTime` and written back in + /// UTC. + timestampWithTimeZone, /// Types carried as text, such as `text`, `uuid` and `character varying`. text, diff --git a/packages/supabase_typegen/test/fixtures/openapi.json b/packages/supabase_typegen/test/fixtures/openapi.json index a64a325ad..8b215ab99 100644 --- a/packages/supabase_typegen/test/fixtures/openapi.json +++ b/packages/supabase_typegen/test/fixtures/openapi.json @@ -8,7 +8,10 @@ "definitions": { "books": { "description": "Books available in the library", - "required": ["title", "author_id"], + "required": [ + "title", + "author_id" + ], "properties": { "id": { "description": "Note:\nThis is a Primary Key.", @@ -39,7 +42,11 @@ "default": true }, "mood": { - "enum": ["happy", "very happy", "sad"], + "enum": [ + "happy", + "very happy", + "sad" + ], "format": "public.mood", "type": "string" }, @@ -73,12 +80,19 @@ "format": "timestamp with time zone", "type": "string", "default": "now()" + }, + "updated_at": { + "format": "timestamp without time zone", + "type": "string" } }, "type": "object" }, "authors": { - "required": ["id", "name"], + "required": [ + "id", + "name" + ], "properties": { "id": { "description": "Note:\nThis is a Primary Key.", diff --git a/packages/supabase_typegen/test/generated_schema_behavior_test.dart b/packages/supabase_typegen/test/generated_schema_behavior_test.dart index 512606b95..14cc69b96 100644 --- a/packages/supabase_typegen/test/generated_schema_behavior_test.dart +++ b/packages/supabase_typegen/test/generated_schema_behavior_test.dart @@ -102,6 +102,45 @@ void main() { }); }); + test( + 'timestamps are sent as UTC instants and dates keep their day', + () async { + httpClient.responseBody = ''; + + await client + .table(Books.table) + .insert( + BooksInsert( + title: 'A typed row', + authorId: 7, + createdAt: DateTime(2026, 7, 23, 10), // local wall time + publishedOn: DateTime(2026, 7, 23, 23, 30), + ), + ); + + final sent = + jsonDecode(httpClient.lastRequestBody!) as Map; + expect( + sent['created_at'], + DateTime(2026, 7, 23, 10).toUtc().toIso8601String(), + ); + expect(sent['published_on'], '2026-07-23'); + }, + ); + + test('unknown enum wire values throw a descriptive error', () { + expect( + () => Mood.fromWire('grumpy'), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('No Mood value'), + ), + ), + ); + }); + test('update sends only the provided columns', () async { httpClient.responseBody = ''; diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index d2add44cb..d7f29262d 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -16,8 +16,14 @@ enum Mood { final String wireName; /// Parses the database representation of the enum. - static Mood fromWire(String wireName) => - values.firstWhere((value) => value.wireName == wireName); + static Mood fromWire(String wireName) => values.firstWhere( + (value) => value.wireName == wireName, + orElse: () => throw ArgumentError.value( + wireName, + 'wireName', + 'No Mood value with this wire name', + ), + ); @override String toString() => wireName; @@ -38,7 +44,7 @@ extension type const AuthorStatsInsert._(Map _json) : this._({'author_id': ?authorId, 'book_count': ?bookCount}); } -/// Values for updating rows of `author_stats`. All columns are optional; passing `null` leaves the column unchanged. +/// Values for updating rows of `author_stats`. All columns are optional; passing `null` omits the column, leaving it unchanged. To write SQL NULL explicitly, set the raw key: `AuthorStatsUpdate()..['column_name'] = null`. extension type const AuthorStatsUpdate._(Map _json) implements Map { AuthorStatsUpdate({int? authorId, int? bookCount}) @@ -70,7 +76,7 @@ extension type const AuthorsInsert._(Map _json) : this._({'id': id, 'name': name}); } -/// Values for updating rows of `authors`. All columns are optional; passing `null` leaves the column unchanged. +/// Values for updating rows of `authors`. All columns are optional; passing `null` omits the column, leaving it unchanged. To write SQL NULL explicitly, set the raw key: `AuthorsUpdate()..['column_name'] = null`. extension type const AuthorsUpdate._(Map _json) implements Map { AuthorsUpdate({int? id, String? name}) : this._({'id': ?id, 'name': ?name}); @@ -115,6 +121,10 @@ extension type const BooksRow(Map _json) null => null, final Object value => DateTime.parse(value as String), }; + DateTime? get updatedAt => switch (_json['updated_at']) { + null => null, + final Object value => DateTime.parse(value as String), + }; } /// Values for inserting a row into `books`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. @@ -134,6 +144,7 @@ extension type const BooksInsert._(Map _json) String? coverUuid, DateTime? publishedOn, DateTime? createdAt, + DateTime? updatedAt, }) : this._({ 'id': ?id, 'title': title, @@ -146,12 +157,16 @@ extension type const BooksInsert._(Map _json) 'page_counts': ?pageCounts, 'metadata': ?metadata, 'cover_uuid': ?coverUuid, - 'published_on': ?publishedOn?.toIso8601String(), - 'created_at': ?createdAt?.toIso8601String(), + 'published_on': ?switch (publishedOn) { + null => null, + final value => _dateString(value), + }, + 'created_at': ?createdAt?.toUtc().toIso8601String(), + 'updated_at': ?updatedAt?.toIso8601String(), }); } -/// Values for updating rows of `books`. All columns are optional; passing `null` leaves the column unchanged. +/// Values for updating rows of `books`. All columns are optional; passing `null` omits the column, leaving it unchanged. To write SQL NULL explicitly, set the raw key: `BooksUpdate()..['column_name'] = null`. extension type const BooksUpdate._(Map _json) implements Map { BooksUpdate({ @@ -168,6 +183,7 @@ extension type const BooksUpdate._(Map _json) String? coverUuid, DateTime? publishedOn, DateTime? createdAt, + DateTime? updatedAt, }) : this._({ 'id': ?id, 'title': ?title, @@ -180,8 +196,12 @@ extension type const BooksUpdate._(Map _json) 'page_counts': ?pageCounts, 'metadata': ?metadata, 'cover_uuid': ?coverUuid, - 'published_on': ?publishedOn?.toIso8601String(), - 'created_at': ?createdAt?.toIso8601String(), + 'published_on': ?switch (publishedOn) { + null => null, + final value => _dateString(value), + }, + 'created_at': ?createdAt?.toUtc().toIso8601String(), + 'updated_at': ?updatedAt?.toIso8601String(), }); } @@ -205,4 +225,10 @@ class Books { static const coverUuid = TableColumn('cover_uuid'); static const publishedOn = TableColumn('published_on'); static const createdAt = TableColumn('created_at'); + static const updatedAt = TableColumn('updated_at'); } + +String _dateString(DateTime date) => + '${date.year.toString().padLeft(4, '0')}-' + '${date.month.toString().padLeft(2, '0')}-' + '${date.day.toString().padLeft(2, '0')}'; diff --git a/packages/supabase_typegen/test/identifiers_test.dart b/packages/supabase_typegen/test/identifiers_test.dart index 6907f96bb..5755922e9 100644 --- a/packages/supabase_typegen/test/identifiers_test.dart +++ b/packages/supabase_typegen/test/identifiers_test.dart @@ -9,6 +9,12 @@ void main() { expect(pascalCase('user-profiles'), 'UserProfiles'); }); + test('keeps existing camel humps', () { + expect(pascalCase('UserProfiles'), 'UserProfiles'); + expect(pascalCase('userId'), 'UserId'); + expect(camelCase('userId'), 'userId'); + }); + test('prefixes names starting with a digit', () { expect(pascalCase('2fa_codes'), r'$2faCodes'); }); diff --git a/packages/supabase_typegen/test/openapi_parser_test.dart b/packages/supabase_typegen/test/openapi_parser_test.dart index a3129d360..2c71355f7 100644 --- a/packages/supabase_typegen/test/openapi_parser_test.dart +++ b/packages/supabase_typegen/test/openapi_parser_test.dart @@ -65,7 +65,9 @@ void main() { expect(kindOf('in_print'), ColumnTypeKind.boolean); expect(kindOf('mood'), ColumnTypeKind.enumType); expect(kindOf('metadata'), ColumnTypeKind.json); - expect(kindOf('created_at'), ColumnTypeKind.dateTime); + expect(kindOf('created_at'), ColumnTypeKind.timestampWithTimeZone); + expect(kindOf('updated_at'), ColumnTypeKind.timestamp); + expect(kindOf('published_on'), ColumnTypeKind.date); expect(kindOf('cover_uuid'), ColumnTypeKind.text); }); From 226ad80d9f339aca128b6107e75b4550734ad281 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 23 Jul 2026 12:08:19 +0200 Subject: [PATCH 06/31] feat(supabase_typegen): generate setXToNull methods for explicit SQL NULL writes --- packages/supabase_typegen/README.md | 4 +- .../lib/src/dart_generator.dart | 23 +++- .../test/generated_schema_behavior_test.dart | 33 ++++++ .../test/goldens/supabase_schema.dart | 106 +++++++++++++++++- 4 files changed, 155 insertions(+), 11 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 98ca3d8cb..2c2828844 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -49,7 +49,9 @@ await client.table(Books.table).insert( `NOT NULL` columns with a database default, so getters for defaulted columns other than primary keys are conservatively nullable. - Passing `null` to an `Insert`/`Update` parameter omits the column. To write - SQL NULL explicitly, set the raw key: `BooksUpdate()..['price'] = null`. + 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`), matching the supabase-js type generator; arrays containing SQL NULL elements throw when the element is read. Enum array columns degrade to diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index d032fcd2e..ba97d2333 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -161,7 +161,8 @@ void _writeTable( 'Values for inserting a row into `${table.name}`. Columns that are ' 'nullable, part of a generated primary key, or covered by a database ' 'default are optional; passing `null` omits the column so the ' - 'database default applies.', + 'database default applies. Use the `set…ToNull` methods to insert ' + 'SQL NULL explicitly.', ); _writeValues( buffer, @@ -173,8 +174,7 @@ void _writeTable( docLine: 'Values for updating rows of `${table.name}`. All columns are ' 'optional; passing `null` omits the column, leaving it unchanged. ' - 'To write SQL NULL explicitly, set the raw key: ' - '`$updateType()..[\'column_name\'] = null`.', + 'Use the `set…ToNull` methods to write SQL NULL explicitly.', ); _writeNamespace(buffer, table, namespaceType, rowType, memberNames, bindings); } @@ -245,8 +245,23 @@ void _writeValues( ); } } + buffer.writeln(' });'); + for (final column in table.columns) { + if (!column.isNullable) continue; + final name = memberNames[column.name]!; + final methodName = 'set${name[0].toUpperCase()}${name.substring(1)}ToNull'; + buffer + ..writeln() + ..writeln( + ' /// Returns a copy with `${column.name}` set to SQL NULL, ' + 'overriding any database default.', + ) + ..writeln( + ' $typeName $methodName() => ' + '$typeName._({..._json, ${_stringLiteral(column.name)}: null});', + ); + } buffer - ..writeln(' });') ..writeln('}') ..writeln(); } diff --git a/packages/supabase_typegen/test/generated_schema_behavior_test.dart b/packages/supabase_typegen/test/generated_schema_behavior_test.dart index 14cc69b96..50164f79f 100644 --- a/packages/supabase_typegen/test/generated_schema_behavior_test.dart +++ b/packages/supabase_typegen/test/generated_schema_behavior_test.dart @@ -152,4 +152,37 @@ void main() { expect(jsonDecode(httpClient.lastRequestBody!), {'in_print': false}); expect(httpClient.lastRequest!.url.queryParameters['id'], 'eq.1'); }); + + test('setXToNull writes SQL NULL explicitly', () async { + httpClient.responseBody = ''; + + final update = BooksUpdate(inPrint: false); + await client + .table(Books.table) + .update(update.setPriceToNull().setMoodToNull()) + .where(Books.id.eq(1)); + + expect(jsonDecode(httpClient.lastRequestBody!), { + 'in_print': false, + 'price': null, + 'mood': null, + }); + expect( + update.containsKey('price'), + isFalse, + reason: 'setPriceToNull returns a copy and must not mutate', + ); + + await client + .table(Books.table) + .insert( + BooksInsert(title: 'x', authorId: 7).setPublishedOnToNull(), + ); + + expect(jsonDecode(httpClient.lastRequestBody!), { + 'title': 'x', + 'author_id': 7, + 'published_on': null, + }); + }); } diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index d7f29262d..a581d6f21 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -37,18 +37,34 @@ extension type const AuthorStatsRow(Map _json) int? get bookCount => _json['book_count'] as int?; } -/// Values for inserting a row into `author_stats`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. +/// Values for inserting a row into `author_stats`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. Use the `set…ToNull` methods to insert SQL NULL explicitly. extension type const AuthorStatsInsert._(Map _json) implements Map { AuthorStatsInsert({int? authorId, int? bookCount}) : this._({'author_id': ?authorId, 'book_count': ?bookCount}); + + /// Returns a copy with `author_id` set to SQL NULL, overriding any database default. + AuthorStatsInsert setAuthorIdToNull() => + AuthorStatsInsert._({..._json, 'author_id': null}); + + /// Returns a copy with `book_count` set to SQL NULL, overriding any database default. + AuthorStatsInsert setBookCountToNull() => + AuthorStatsInsert._({..._json, 'book_count': null}); } -/// Values for updating rows of `author_stats`. All columns are optional; passing `null` omits the column, leaving it unchanged. To write SQL NULL explicitly, set the raw key: `AuthorStatsUpdate()..['column_name'] = null`. +/// Values for updating rows of `author_stats`. All columns are optional; passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods to write SQL NULL explicitly. extension type const AuthorStatsUpdate._(Map _json) implements Map { AuthorStatsUpdate({int? authorId, int? bookCount}) : this._({'author_id': ?authorId, 'book_count': ?bookCount}); + + /// Returns a copy with `author_id` set to SQL NULL, overriding any database default. + AuthorStatsUpdate setAuthorIdToNull() => + AuthorStatsUpdate._({..._json, 'author_id': null}); + + /// Returns a copy with `book_count` set to SQL NULL, overriding any database default. + AuthorStatsUpdate setBookCountToNull() => + AuthorStatsUpdate._({..._json, 'book_count': null}); } /// Typed access to the `author_stats` table. @@ -69,14 +85,14 @@ extension type const AuthorsRow(Map _json) String get name => _json['name'] as String; } -/// Values for inserting a row into `authors`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. +/// Values for inserting a row into `authors`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. Use the `set…ToNull` methods to insert SQL NULL explicitly. extension type const AuthorsInsert._(Map _json) implements Map { AuthorsInsert({required int id, required String name}) : this._({'id': id, 'name': name}); } -/// Values for updating rows of `authors`. All columns are optional; passing `null` omits the column, leaving it unchanged. To write SQL NULL explicitly, set the raw key: `AuthorsUpdate()..['column_name'] = null`. +/// Values for updating rows of `authors`. All columns are optional; passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods to write SQL NULL explicitly. extension type const AuthorsUpdate._(Map _json) implements Map { AuthorsUpdate({int? id, String? name}) : this._({'id': ?id, 'name': ?name}); @@ -127,7 +143,7 @@ extension type const BooksRow(Map _json) }; } -/// Values for inserting a row into `books`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. +/// Values for inserting a row into `books`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. Use the `set…ToNull` methods to insert SQL NULL explicitly. extension type const BooksInsert._(Map _json) implements Map { BooksInsert({ @@ -164,9 +180,48 @@ extension type const BooksInsert._(Map _json) 'created_at': ?createdAt?.toUtc().toIso8601String(), 'updated_at': ?updatedAt?.toIso8601String(), }); + + /// Returns a copy with `price` set to SQL NULL, overriding any database default. + BooksInsert setPriceToNull() => BooksInsert._({..._json, 'price': null}); + + /// Returns a copy with `rating` set to SQL NULL, overriding any database default. + BooksInsert setRatingToNull() => BooksInsert._({..._json, 'rating': null}); + + /// Returns a copy with `in_print` set to SQL NULL, overriding any database default. + BooksInsert setInPrintToNull() => BooksInsert._({..._json, 'in_print': null}); + + /// Returns a copy with `mood` set to SQL NULL, overriding any database default. + BooksInsert setMoodToNull() => BooksInsert._({..._json, 'mood': null}); + + /// Returns a copy with `tags` set to SQL NULL, overriding any database default. + BooksInsert setTagsToNull() => BooksInsert._({..._json, 'tags': null}); + + /// Returns a copy with `page_counts` set to SQL NULL, overriding any database default. + BooksInsert setPageCountsToNull() => + BooksInsert._({..._json, 'page_counts': null}); + + /// Returns a copy with `metadata` set to SQL NULL, overriding any database default. + BooksInsert setMetadataToNull() => + BooksInsert._({..._json, 'metadata': null}); + + /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database default. + BooksInsert setCoverUuidToNull() => + BooksInsert._({..._json, 'cover_uuid': null}); + + /// Returns a copy with `published_on` set to SQL NULL, overriding any database default. + BooksInsert setPublishedOnToNull() => + BooksInsert._({..._json, 'published_on': null}); + + /// Returns a copy with `created_at` set to SQL NULL, overriding any database default. + BooksInsert setCreatedAtToNull() => + BooksInsert._({..._json, 'created_at': null}); + + /// Returns a copy with `updated_at` set to SQL NULL, overriding any database default. + BooksInsert setUpdatedAtToNull() => + BooksInsert._({..._json, 'updated_at': null}); } -/// Values for updating rows of `books`. All columns are optional; passing `null` omits the column, leaving it unchanged. To write SQL NULL explicitly, set the raw key: `BooksUpdate()..['column_name'] = null`. +/// Values for updating rows of `books`. All columns are optional; passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods to write SQL NULL explicitly. extension type const BooksUpdate._(Map _json) implements Map { BooksUpdate({ @@ -203,6 +258,45 @@ extension type const BooksUpdate._(Map _json) 'created_at': ?createdAt?.toUtc().toIso8601String(), 'updated_at': ?updatedAt?.toIso8601String(), }); + + /// Returns a copy with `price` set to SQL NULL, overriding any database default. + BooksUpdate setPriceToNull() => BooksUpdate._({..._json, 'price': null}); + + /// Returns a copy with `rating` set to SQL NULL, overriding any database default. + BooksUpdate setRatingToNull() => BooksUpdate._({..._json, 'rating': null}); + + /// Returns a copy with `in_print` set to SQL NULL, overriding any database default. + BooksUpdate setInPrintToNull() => BooksUpdate._({..._json, 'in_print': null}); + + /// Returns a copy with `mood` set to SQL NULL, overriding any database default. + BooksUpdate setMoodToNull() => BooksUpdate._({..._json, 'mood': null}); + + /// Returns a copy with `tags` set to SQL NULL, overriding any database default. + BooksUpdate setTagsToNull() => BooksUpdate._({..._json, 'tags': null}); + + /// Returns a copy with `page_counts` set to SQL NULL, overriding any database default. + BooksUpdate setPageCountsToNull() => + BooksUpdate._({..._json, 'page_counts': null}); + + /// Returns a copy with `metadata` set to SQL NULL, overriding any database default. + BooksUpdate setMetadataToNull() => + BooksUpdate._({..._json, 'metadata': null}); + + /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database default. + BooksUpdate setCoverUuidToNull() => + BooksUpdate._({..._json, 'cover_uuid': null}); + + /// Returns a copy with `published_on` set to SQL NULL, overriding any database default. + BooksUpdate setPublishedOnToNull() => + BooksUpdate._({..._json, 'published_on': null}); + + /// Returns a copy with `created_at` set to SQL NULL, overriding any database default. + BooksUpdate setCreatedAtToNull() => + BooksUpdate._({..._json, 'created_at': null}); + + /// Returns a copy with `updated_at` set to SQL NULL, overriding any database default. + BooksUpdate setUpdatedAtToNull() => + BooksUpdate._({..._json, 'updated_at': null}); } /// Typed access to the `books` table. From edd4f95d815cecec2bf81a42258179279457eca2 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 23 Jul 2026 12:18:48 +0200 Subject: [PATCH 07/31] feat: mark typed table access as experimental in typegen output --- packages/supabase_typegen/lib/src/dart_generator.dart | 3 +++ .../supabase_typegen/test/generated_schema_behavior_test.dart | 3 +++ packages/supabase_typegen/test/goldens/supabase_schema.dart | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index ba97d2333..316a9b2b6 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -32,6 +32,9 @@ String generateDartCode( ..writeln('//') ..writeln('// Source schema: ${schema.schemaName}') ..writeln() + ..writeln('// The typed table access API is still experimental.') + ..writeln('// ignore_for_file: experimental_member_use') + ..writeln() ..writeln("import '$importUri';") ..writeln(); diff --git a/packages/supabase_typegen/test/generated_schema_behavior_test.dart b/packages/supabase_typegen/test/generated_schema_behavior_test.dart index 50164f79f..2c749c1e2 100644 --- a/packages/supabase_typegen/test/generated_schema_behavior_test.dart +++ b/packages/supabase_typegen/test/generated_schema_behavior_test.dart @@ -1,3 +1,6 @@ +// The typed table access API under test is annotated @experimental. +// ignore_for_file: experimental_member_use + import 'dart:convert'; import 'package:http/http.dart'; diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index a581d6f21..e73e20e7f 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -2,6 +2,9 @@ // // Source schema: public +// The typed table access API is still experimental. +// ignore_for_file: experimental_member_use + import 'package:postgrest/postgrest.dart'; /// Postgres enum `public.mood`. From d80e2cd8c1b8409867a72eb847ac03ac092845cc Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 17 Aug 2026 15:15:47 +0200 Subject: [PATCH 08/31] feat(supabase_typegen): read the postgres-meta json metadata instead of the OpenAPI description --- packages/supabase_typegen/README.md | 35 +- .../bin/supabase_typegen.dart | 88 ++-- .../lib/src/dart_generator.dart | 18 +- .../lib/src/openapi_parser.dart | 168 ------- .../lib/src/postgres_meta_parser.dart | 231 +++++++++ .../lib/src/schema_description.dart | 16 +- .../lib/supabase_typegen.dart | 2 +- packages/supabase_typegen/pubspec.yaml | 2 +- .../test/dart_generator_test.dart | 29 +- .../test/fixtures/openapi.json | 125 ----- .../test/fixtures/postgres_meta_schema.json | 440 ++++++++++++++++++ .../test/generated_schema_behavior_test.dart | 2 + .../test/goldens/supabase_schema.dart | 32 +- ...st.dart => postgres_meta_parser_test.dart} | 93 +++- .../tool/regenerate_goldens.dart | 6 +- 15 files changed, 874 insertions(+), 413 deletions(-) delete mode 100644 packages/supabase_typegen/lib/src/openapi_parser.dart create mode 100644 packages/supabase_typegen/lib/src/postgres_meta_parser.dart delete mode 100644 packages/supabase_typegen/test/fixtures/openapi.json create mode 100644 packages/supabase_typegen/test/fixtures/postgres_meta_schema.json rename packages/supabase_typegen/test/{openapi_parser_test.dart => postgres_meta_parser_test.dart} (52%) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 2c2828844..e70289ed4 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -14,21 +14,33 @@ For every table the generator emits: ## Usage +First dump the schema metadata with the Supabase CLI, then generate: + ```sh -dart run supabase_typegen \ - --url https://your-project.supabase.co \ - --key $SUPABASE_ANON_KEY \ +supabase gen types --lang json --local > schema.json +dart run supabase_typegen --input schema.json \ --output lib/supabase_schema.g.dart ``` -`--url` and `--key` fall back to the `SUPABASE_URL` and `SUPABASE_ANON_KEY` -environment variables. Use `--schema` to generate for a schema other than -`public`, and `--import` to change which library the generated file imports -`PostgrestTable` and `TableColumn` from. +Any of the CLI's connection flags work (`--local`, `--linked`, +`--db-url`, `--project-id`). Until CLI support for `--lang json` ships, the +same document comes straight from +[postgres-meta](https://github.com/supabase/postgres-meta) with +`PG_META_GENERATE_TYPES=json` or its `/generators/json` endpoint. Pass +`--input -` to read the document from stdin: + +```sh +supabase gen types --lang json --local | dart run supabase_typegen --input - +``` + +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 schema is read from the OpenAPI description that PostgREST serves at the -API root, so the key only needs read access; tables whose role lacks -privileges (grants, not row level security) are not included. +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 @@ -45,9 +57,6 @@ await client.table(Books.table).insert( ## Known limitations -- The OpenAPI description does not distinguish nullable columns from - `NOT NULL` columns with a database default, so getters for defaulted - columns other than primary keys are conservatively nullable. - 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 diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index e04e9dc00..57af6626a 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -2,21 +2,16 @@ import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; -import 'package:http/http.dart' as http; import 'package:supabase_typegen/supabase_typegen.dart'; final _argParser = ArgParser() ..addOption( - 'url', + 'input', + abbr: 'i', help: - 'The Supabase project URL, for example https://xyz.supabase.co. ' - 'Falls back to the SUPABASE_URL environment variable.', - ) - ..addOption( - 'key', - help: - 'The API key used to read the schema description. Falls back to ' - 'the SUPABASE_ANON_KEY or SUPABASE_KEY environment variable.', + 'Path of the postgres-meta generator metadata document, or - to ' + 'read it from stdin. Produce it with ' + '`supabase gen types --lang json`.', ) ..addOption( 'schema', @@ -57,67 +52,56 @@ Future _run(List arguments) async { if (options.flag('help')) { stdout - ..writeln('Generates typed Supabase table definitions from a schema.') + ..writeln( + 'Generates typed Supabase table definitions from the schema ' + 'metadata that postgres-meta emits.', + ) ..writeln() - ..writeln('Usage: dart run supabase_typegen [options]') + ..writeln('Usage: dart run supabase_typegen --input schema.json') ..writeln(_argParser.usage); return 0; } - final url = options.option('url') ?? Platform.environment['SUPABASE_URL']; - final key = - options.option('key') ?? - Platform.environment['SUPABASE_ANON_KEY'] ?? - Platform.environment['SUPABASE_KEY']; - if (url == null || key == null) { + final input = options.option('input'); + if (input == null) { stderr.writeln( - 'Both --url and --key are required, either as options or through the ' - 'SUPABASE_URL and SUPABASE_ANON_KEY environment variables.', + '--input is required: the path of a postgres-meta generator metadata ' + 'document, or - to read it from stdin. Produce it with ' + '`supabase gen types --lang json`.', ); return 64; } - final schemaName = options.option('schema')!; - final baseUrl = url.replaceAll(RegExp(r'/+$'), ''); - final endpoint = Uri.parse('$baseUrl/rest/v1/'); - final http.Response response; - try { - response = await http.get( - endpoint, - headers: { - 'apikey': key, - 'Authorization': 'Bearer $key', - 'Accept-Profile': schemaName, - }, - ); - } on http.ClientException catch (error) { - stderr.writeln('Failed to reach $endpoint: $error'); - return 1; - } - if (response.statusCode != 200) { - stderr.writeln( - 'Failed to fetch the schema description from $endpoint ' - '(HTTP ${response.statusCode}): ${response.body}', - ); - return 1; + 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 Map document; + final schemaName = options.option('schema')!; + final SchemaDescription schema; try { - document = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; + schema = parsePostgresMetaDocument( + jsonDecode(contents) as Map, + schemaName: schemaName, + ); } on FormatException catch (error) { - stderr.writeln('The response from $endpoint is not valid JSON: $error'); - return 1; + stderr.writeln('Could not parse $input: ${error.message}'); + return 65; } on TypeError { stderr.writeln( - 'The response from $endpoint is not an OpenAPI document. Check that ' - 'the URL points to a Supabase project or PostgREST instance.', + 'The document in $input is not postgres-meta generator metadata. ' + 'Produce it with `supabase gen types --lang json`.', ); - return 1; + return 65; } - final schema = parseOpenApiDocument(document, schemaName: schemaName); final code = generateDartCode(schema, importUri: options.option('import')!); final outputFile = File(options.option('output')!); diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index 316a9b2b6..6944dbbc2 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -162,10 +162,10 @@ void _writeTable( requireRequiredColumns: true, docLine: 'Values for inserting a row into `${table.name}`. Columns that are ' - 'nullable, part of a generated primary key, or covered by a database ' - 'default are optional; passing `null` omits the column so the ' - 'database default applies. Use the `set…ToNull` methods to insert ' - 'SQL NULL explicitly.', + 'nullable, identity, or covered by a database default are optional; ' + 'passing `null` omits the column so the database default applies. ' + 'Columns the database always generates itself are left out entirely. ' + 'Use the `set…ToNull` methods to insert SQL NULL explicitly.', ); _writeValues( buffer, @@ -218,13 +218,17 @@ void _writeValues( }) { bool isRequired(ColumnDescription column) => requireRequiredColumns && column.isRequired; + final writableColumns = [ + for (final column in table.columns) + if (!column.isReadOnly) column, + ]; buffer ..writeln('/// $docLine') ..writeln('extension type const $typeName._(Map _json)') ..writeln(' implements Map {') ..writeln(' $typeName({'); - for (final column in table.columns) { + for (final column in writableColumns) { final binding = bindings[column.name]!; final name = memberNames[column.name]!; if (isRequired(column)) { @@ -234,7 +238,7 @@ void _writeValues( } } buffer.writeln(' }) : this._({'); - for (final column in table.columns) { + for (final column in writableColumns) { final binding = bindings[column.name]!; final name = memberNames[column.name]!; final key = _stringLiteral(column.name); @@ -249,7 +253,7 @@ void _writeValues( } } buffer.writeln(' });'); - for (final column in table.columns) { + for (final column in writableColumns) { if (!column.isNullable) continue; final name = memberNames[column.name]!; final methodName = 'set${name[0].toUpperCase()}${name.substring(1)}ToNull'; diff --git a/packages/supabase_typegen/lib/src/openapi_parser.dart b/packages/supabase_typegen/lib/src/openapi_parser.dart deleted file mode 100644 index 44925d9c1..000000000 --- a/packages/supabase_typegen/lib/src/openapi_parser.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'schema_description.dart'; - -final _foreignKeyPattern = RegExp(""); - -const _integerFormats = { - 'smallint', - 'integer', - 'bigint', - 'int2', - 'int4', - 'int8', -}; -const _floatingFormats = {'real', 'double precision', 'float4', 'float8'}; -const _numericFormats = {'numeric', 'decimal'}; -const _timestampFormats = {'timestamp', 'timestamp without time zone'}; -const _timestampWithTimeZoneFormats = { - 'timestamp with time zone', - 'timestamptz', -}; -const _jsonFormats = {'json', 'jsonb'}; - -/// Derives the [ColumnTypeKind] from the Postgres [format] and JSON schema -/// [jsonType] of a column. This is the single place where type names are -/// compared as strings; everything downstream works with the enum. -ColumnTypeKind _typeKind({ - required String format, - required String? jsonType, - required bool isEnum, -}) { - if (format.endsWith('[]') || jsonType == 'array') { - return ColumnTypeKind.array; - } - if (isEnum) return ColumnTypeKind.enumType; - if (_integerFormats.contains(format)) return ColumnTypeKind.integer; - if (_floatingFormats.contains(format)) return ColumnTypeKind.floating; - if (_numericFormats.contains(format)) return ColumnTypeKind.numeric; - if (format == 'boolean') return ColumnTypeKind.boolean; - if (format == 'date') return ColumnTypeKind.date; - if (_timestampFormats.contains(format)) return ColumnTypeKind.timestamp; - if (_timestampWithTimeZoneFormats.contains(format)) { - return ColumnTypeKind.timestampWithTimeZone; - } - if (_jsonFormats.contains(format)) return ColumnTypeKind.json; - return switch (jsonType) { - 'integer' => ColumnTypeKind.integer, - 'number' => ColumnTypeKind.numeric, - 'boolean' => ColumnTypeKind.boolean, - 'string' => ColumnTypeKind.text, - _ => ColumnTypeKind.unknown, - }; -} - -ColumnTypeKind? _elementTypeKind(String? itemsJsonType) => itemsJsonType == null - ? null - : switch (itemsJsonType) { - 'integer' => ColumnTypeKind.integer, - 'number' => ColumnTypeKind.numeric, - 'boolean' => ColumnTypeKind.boolean, - 'string' => ColumnTypeKind.text, - _ => ColumnTypeKind.unknown, - }; - -/// Parses the OpenAPI (Swagger 2.0) document that PostgREST serves at the -/// API root into a [SchemaDescription]. -/// -/// PostgREST encodes primary keys and foreign keys as `` and -/// `` markers inside column descriptions, and -/// lists `NOT NULL` columns without a database default under `required`. -SchemaDescription parseOpenApiDocument( - Map document, { - String schemaName = 'public', -}) { - final definitions = - document['definitions'] as Map? ?? const {}; - - final tables = []; - final enumsByQualifiedName = {}; - - for (final MapEntry(key: tableName, value: definition) - in definitions.entries) { - if (definition is! Map) continue; - final required = { - ...?(definition['required'] as List?)?.cast(), - }; - final properties = - definition['properties'] as Map? ?? const {}; - - final columns = []; - for (final MapEntry(key: columnName, value: property) - in properties.entries) { - property as Map; - final description = property['description'] as String?; - final format = property['format'] as String? ?? ''; - final enumValues = (property['enum'] as List?)?.cast(); - final typeKind = _typeKind( - format: format, - jsonType: property['type'] as String?, - isEnum: enumValues != null, - ); - - if (enumValues != null && typeKind == ColumnTypeKind.enumType) { - enumsByQualifiedName.putIfAbsent( - format, - () => EnumDescription(qualifiedName: format, values: enumValues), - ); - } - - final foreignKeyMatch = description == null - ? null - : _foreignKeyPattern.firstMatch(description); - - columns.add( - ColumnDescription( - name: columnName, - postgresFormat: format, - typeKind: typeKind, - elementTypeKind: _elementTypeKind( - (property['items'] as Map?)?['type'] as String?, - ), - enumValues: enumValues, - isRequired: required.contains(columnName), - isPrimaryKey: description?.contains('') ?? false, - hasDefault: property.containsKey('default'), - comment: _cleanComment(description), - foreignKey: foreignKeyMatch == null - ? null - : ForeignKeyDescription( - table: foreignKeyMatch.group(1)!, - column: foreignKeyMatch.group(2)!, - ), - ), - ); - } - - tables.add( - TableDescription( - name: tableName, - comment: _cleanComment(definition['description'] as String?), - columns: columns, - ), - ); - } - - tables.sort((a, b) => a.name.compareTo(b.name)); - final enums = enumsByQualifiedName.values.toList() - ..sort((a, b) => a.qualifiedName.compareTo(b.qualifiedName)); - - return SchemaDescription( - schemaName: schemaName, - tables: tables, - enums: enums, - ); -} - -/// Strips the PostgREST key markers from a column or table description, -/// keeping only the human written comment. -String? _cleanComment(String? description) { - if (description == null) return null; - final cleaned = description - .replaceAll(_foreignKeyPattern, '') - .replaceAll('', '') - .replaceAll( - RegExp(r'Note:\s*This is a (Primary|Foreign) Key( to `[^`]+`)?\.'), - '', - ) - .trim(); - return cleaned.isEmpty ? null : cleaned; -} diff --git a/packages/supabase_typegen/lib/src/postgres_meta_parser.dart b/packages/supabase_typegen/lib/src/postgres_meta_parser.dart new file mode 100644 index 000000000..9eff89af1 --- /dev/null +++ b/packages/supabase_typegen/lib/src/postgres_meta_parser.dart @@ -0,0 +1,231 @@ +import 'schema_description.dart'; + +/// The metadata document version this parser understands. +const supportedPostgresMetaVersion = 1; + +const _integerFormats = {'int2', 'int4', 'int8'}; +const _floatingFormats = {'float4', 'float8'}; +const _textFormats = { + 'text', + 'citext', + 'varchar', + 'bpchar', + 'char', + 'uuid', + 'time', + 'timetz', + 'interval', + 'bytea', +}; +const _jsonFormats = {'json', 'jsonb'}; + +/// Derives the [ColumnTypeKind] from the postgres-meta [format] of a column, +/// for example `int8`, `timestamptz` or `_text` for a `text[]` array. This is +/// the single place where type names are compared as strings; everything +/// downstream works with the enum. +ColumnTypeKind _typeKind(String format, {required bool isEnum}) { + if (format.startsWith('_')) return ColumnTypeKind.array; + if (isEnum) return ColumnTypeKind.enumType; + if (_integerFormats.contains(format)) return ColumnTypeKind.integer; + if (_floatingFormats.contains(format)) return ColumnTypeKind.floating; + if (format == 'numeric') return ColumnTypeKind.numeric; + if (format == 'bool') return ColumnTypeKind.boolean; + if (format == 'date') return ColumnTypeKind.date; + if (format == 'timestamp') return ColumnTypeKind.timestamp; + if (format == 'timestamptz') return ColumnTypeKind.timestampWithTimeZone; + if (_textFormats.contains(format)) return ColumnTypeKind.text; + if (_jsonFormats.contains(format)) return ColumnTypeKind.json; + return ColumnTypeKind.unknown; +} + +/// The kind of the elements of an array column, where enum elements are +/// carried as their wire strings. +ColumnTypeKind _elementTypeKind(String elementFormat, {required bool isEnum}) { + final kind = _typeKind(elementFormat, isEnum: isEnum); + return kind == ColumnTypeKind.enumType ? ColumnTypeKind.text : kind; +} + +/// Parses the generator metadata document that postgres-meta emits from its +/// `json` generator (`supabase gen types --lang json`, the +/// `/generators/json` endpoint, or `PG_META_GENERATE_TYPES=json`) into a +/// [SchemaDescription] for [schemaName]. +/// +/// Throws a [FormatException] when the document does not carry the supported +/// `version`. +SchemaDescription parsePostgresMetaDocument( + Map document, { + String schemaName = 'public', +}) { + final version = document['version']; + if (version != supportedPostgresMetaVersion) { + throw FormatException( + 'Unsupported postgres-meta document version $version; this version of ' + 'supabase_typegen supports version $supportedPostgresMetaVersion.', + ); + } + + final relations = [ + for (final key in ['tables', 'foreignTables', 'views', 'materializedViews']) + ...?(document[key] as List?)?.cast>(), + ].where((relation) => relation['schema'] == schemaName); + + final columnsByRelationId = >>{}; + for (final column + in (document['columns'] as List? ?? const []) + .cast>()) { + columnsByRelationId + .putIfAbsent(column['table_id'] as int, () => []) + .add(column); + } + for (final columns in columnsByRelationId.values) { + columns.sort( + (a, b) => (a['ordinal_position'] as int).compareTo( + b['ordinal_position'] as int, + ), + ); + } + + final foreignKeysByColumn = _foreignKeysByColumn(document, schemaName); + final enumTypes = _enumTypes(document, schemaName); + + final tables = []; + final enumsByQualifiedName = {}; + + for (final relation in relations) { + final relationName = relation['name'] as String; + final primaryKeyNames = { + for (final primaryKey + in (relation['primary_keys'] as List? ?? const []) + .cast>()) + primaryKey['name'] as String, + }; + + final columns = []; + for (final column + in columnsByRelationId[relation['id'] as int] ?? const []) { + final name = column['name'] as String; + final format = column['format'] as String; + final enumValues = (column['enums'] as List? ?? const []) + .cast(); + final isEnum = enumValues.isNotEmpty; + final typeKind = _typeKind(format, isEnum: isEnum); + final isArray = typeKind == ColumnTypeKind.array; + + var postgresFormat = format; + if (isEnum && !isArray) { + final enumDescription = _enumDescription(format, enumValues, enumTypes); + postgresFormat = enumDescription.qualifiedName; + enumsByQualifiedName.putIfAbsent( + enumDescription.qualifiedName, + () => enumDescription, + ); + } + + final hasDefault = + column['default_value'] != null || + column['is_identity'] as bool || + column['is_generated'] as bool; + final isNullable = column['is_nullable'] as bool; + + columns.add( + ColumnDescription( + name: name, + postgresFormat: postgresFormat, + typeKind: typeKind, + elementTypeKind: isArray + ? _elementTypeKind(format.substring(1), isEnum: isEnum) + : null, + enumValues: isEnum ? enumValues : null, + isRequired: !isNullable && !hasDefault, + isPrimaryKey: primaryKeyNames.contains(name), + hasDefault: hasDefault, + isNullable: isNullable, + isReadOnly: + column['identity_generation'] == 'ALWAYS' || + column['is_generated'] as bool, + comment: column['comment'] as String?, + foreignKey: foreignKeysByColumn[(relationName, name)], + ), + ); + } + + tables.add( + TableDescription( + name: relationName, + comment: relation['comment'] as String?, + columns: columns, + ), + ); + } + + tables.sort((a, b) => a.name.compareTo(b.name)); + final enums = enumsByQualifiedName.values.toList() + ..sort((a, b) => a.qualifiedName.compareTo(b.qualifiedName)); + + return SchemaDescription( + schemaName: schemaName, + tables: tables, + enums: enums, + ); +} + +/// Maps `(table, column)` pairs of [schemaName] to their foreign key targets, +/// pairing the source and referenced columns of each relationship by index. +Map<(String, String), ForeignKeyDescription> _foreignKeysByColumn( + Map document, + String schemaName, +) { + final foreignKeys = <(String, String), ForeignKeyDescription>{}; + for (final relationship + in (document['relationships'] as List? ?? const []) + .cast>()) { + if (relationship['schema'] != schemaName) continue; + final table = relationship['relation'] as String; + final columns = (relationship['columns'] as List).cast(); + final referencedColumns = + (relationship['referenced_columns'] as List).cast(); + for (var i = 0; i < columns.length; i++) { + foreignKeys.putIfAbsent( + (table, columns[i]), + () => ForeignKeyDescription( + table: relationship['referenced_relation'] as String, + column: referencedColumns[i], + ), + ); + } + } + return foreignKeys; +} + +/// Maps enum type names to `(schema, values)`, preferring types of +/// [schemaName] when the same name exists in several schemas. +Map)> _enumTypes( + Map document, + String schemaName, +) { + final enumTypes = )>{}; + for (final type + in (document['types'] as List? ?? const []) + .cast>()) { + final values = (type['enums'] as List? ?? const []).cast(); + if (values.isEmpty) continue; + final name = type['name'] as String; + final schema = type['schema'] as String; + if (schema == schemaName || !enumTypes.containsKey(name)) { + enumTypes[name] = (schema, values); + } + } + return enumTypes; +} + +EnumDescription _enumDescription( + String format, + List columnEnumValues, + Map)> enumTypes, +) { + final type = enumTypes[format]; + return EnumDescription( + qualifiedName: type == null ? format : '${type.$1}.$format', + values: type == null ? columnEnumValues : type.$2, + ); +} diff --git a/packages/supabase_typegen/lib/src/schema_description.dart b/packages/supabase_typegen/lib/src/schema_description.dart index 6ab5a59b4..657a8b176 100644 --- a/packages/supabase_typegen/lib/src/schema_description.dart +++ b/packages/supabase_typegen/lib/src/schema_description.dart @@ -88,6 +88,8 @@ class ColumnDescription { required this.isRequired, required this.isPrimaryKey, required this.hasDefault, + required this.isNullable, + this.isReadOnly = false, this.elementTypeKind, this.enumValues, this.foreignKey, @@ -97,7 +99,7 @@ class ColumnDescription { /// Name of the column in the database. final String name; - /// The Postgres type, for example `bigint`, `text[]` or `public.mood`. + /// The Postgres type, for example `int8`, `_text` or `public.mood`. final String postgresFormat; /// The kind of Dart type the column maps to. @@ -127,12 +129,12 @@ class ColumnDescription { final ForeignKeyDescription? foreignKey; /// Whether the column can be `null` in query results. - /// - /// Derived from the OpenAPI description: columns in the `required` list are - /// `NOT NULL`, and primary keys are always `NOT NULL`. Other columns are - /// treated as nullable, which is safe but over-approximates for `NOT NULL` - /// columns that have a database default. - bool get isNullable => !isRequired && !isPrimaryKey; + final bool isNullable; + + /// Whether the column can never be written, because it is a + /// `GENERATED ALWAYS` identity or a generated column. Read-only columns + /// appear in the row type but not in the insert and update value types. + final bool isReadOnly; } /// The target of a foreign key column. diff --git a/packages/supabase_typegen/lib/supabase_typegen.dart b/packages/supabase_typegen/lib/supabase_typegen.dart index 1373314f0..bb41ad3a1 100644 --- a/packages/supabase_typegen/lib/supabase_typegen.dart +++ b/packages/supabase_typegen/lib/supabase_typegen.dart @@ -4,5 +4,5 @@ library; export 'src/dart_generator.dart'; export 'src/identifiers.dart'; -export 'src/openapi_parser.dart'; +export 'src/postgres_meta_parser.dart'; export 'src/schema_description.dart'; diff --git a/packages/supabase_typegen/pubspec.yaml b/packages/supabase_typegen/pubspec.yaml index d794da62e..228705f95 100644 --- a/packages/supabase_typegen/pubspec.yaml +++ b/packages/supabase_typegen/pubspec.yaml @@ -20,9 +20,9 @@ executables: dependencies: args: ^2.7.0 dart_style: ^3.1.0 - http: ^1.6.0 dev_dependencies: + http: ^1.6.0 postgrest: ^2.9.0 supabase_lints: ^0.1.1 test: ^1.25.0 diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index 82ddbc115..511ec4950 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -15,9 +15,13 @@ void main() { setUpAll(() { final document = - jsonDecode(File('test/fixtures/openapi.json').readAsStringSync()) + jsonDecode( + File( + 'test/fixtures/postgres_meta_schema.json', + ).readAsStringSync(), + ) as Map; - schema = parseOpenApiDocument(document); + schema = parsePostgresMetaDocument(document); }); test('matches the golden output', () { @@ -50,4 +54,25 @@ void main() { expect(code, contains('required String title')); expect(code, contains('int? id')); }); + + test('not null columns with a default read non-nullable', () { + final code = generateDartCode(schema); + + expect(code, contains("bool get inPrint => _json['in_print'] as bool;")); + expect( + code, + contains( + "DateTime get createdAt => " + "DateTime.parse(_json['created_at'] as String);", + ), + ); + }); + + test('always generated columns are excluded from insert and update', () { + final code = generateDartCode(schema); + + expect(code, contains('AuthorsInsert({required String name})')); + expect(code, contains('AuthorsUpdate({String? name})')); + expect(code, contains("TableColumn('id')")); + }); } diff --git a/packages/supabase_typegen/test/fixtures/openapi.json b/packages/supabase_typegen/test/fixtures/openapi.json deleted file mode 100644 index 8b215ab99..000000000 --- a/packages/supabase_typegen/test/fixtures/openapi.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "PostgREST API", - "description": "standard public schema", - "version": "12.2.0" - }, - "definitions": { - "books": { - "description": "Books available in the library", - "required": [ - "title", - "author_id" - ], - "properties": { - "id": { - "description": "Note:\nThis is a Primary Key.", - "format": "bigint", - "type": "integer", - "default": "nextval('books_id_seq'::regclass)" - }, - "title": { - "format": "text", - "type": "string" - }, - "author_id": { - "description": "Note:\nThis is a Foreign Key to `authors.id`.", - "format": "bigint", - "type": "integer" - }, - "price": { - "format": "numeric", - "type": "number" - }, - "rating": { - "format": "double precision", - "type": "number" - }, - "in_print": { - "format": "boolean", - "type": "boolean", - "default": true - }, - "mood": { - "enum": [ - "happy", - "very happy", - "sad" - ], - "format": "public.mood", - "type": "string" - }, - "tags": { - "format": "text[]", - "type": "array", - "items": { - "type": "string" - } - }, - "page_counts": { - "format": "integer[]", - "type": "array", - "items": { - "type": "integer" - } - }, - "metadata": { - "format": "jsonb" - }, - "cover_uuid": { - "format": "uuid", - "type": "string" - }, - "published_on": { - "format": "date", - "type": "string" - }, - "created_at": { - "description": "When the row was created", - "format": "timestamp with time zone", - "type": "string", - "default": "now()" - }, - "updated_at": { - "format": "timestamp without time zone", - "type": "string" - } - }, - "type": "object" - }, - "authors": { - "required": [ - "id", - "name" - ], - "properties": { - "id": { - "description": "Note:\nThis is a Primary Key.", - "format": "bigint", - "type": "integer" - }, - "name": { - "format": "text", - "type": "string" - } - }, - "type": "object" - }, - "author_stats": { - "description": "Aggregated statistics per author", - "properties": { - "author_id": { - "format": "bigint", - "type": "integer" - }, - "book_count": { - "format": "bigint", - "type": "integer" - } - }, - "type": "object" - } - }, - "paths": {} -} diff --git a/packages/supabase_typegen/test/fixtures/postgres_meta_schema.json b/packages/supabase_typegen/test/fixtures/postgres_meta_schema.json new file mode 100644 index 000000000..f0058015b --- /dev/null +++ b/packages/supabase_typegen/test/fixtures/postgres_meta_schema.json @@ -0,0 +1,440 @@ +{ + "version": 1, + "schemas": [ + { "id": 2200, "name": "public", "owner": "postgres" } + ], + "tables": [ + { + "id": 16385, + "schema": "public", + "name": "books", + "rls_enabled": true, + "rls_forced": false, + "replica_identity": "DEFAULT", + "bytes": 8192, + "size": "8192 bytes", + "live_rows_estimate": 0, + "dead_rows_estimate": 0, + "comment": "Books available in the library", + "primary_keys": [ + { "schema": "public", "table_name": "books", "name": "id", "table_id": 16385 } + ], + "relationships": [] + }, + { + "id": 16401, + "schema": "public", + "name": "authors", + "rls_enabled": true, + "rls_forced": false, + "replica_identity": "DEFAULT", + "bytes": 8192, + "size": "8192 bytes", + "live_rows_estimate": 0, + "dead_rows_estimate": 0, + "comment": null, + "primary_keys": [ + { "schema": "public", "table_name": "authors", "name": "id", "table_id": 16401 } + ], + "relationships": [] + } + ], + "foreignTables": [], + "views": [ + { + "id": 16420, + "schema": "public", + "name": "author_stats", + "is_updatable": false, + "comment": "Aggregated statistics per author" + } + ], + "materializedViews": [], + "columns": [ + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.1", + "ordinal_position": 1, + "name": "id", + "default_value": null, + "data_type": "bigint", + "format": "int8", + "is_identity": true, + "identity_generation": "BY DEFAULT", + "is_generated": false, + "is_nullable": false, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.2", + "ordinal_position": 2, + "name": "title", + "default_value": null, + "data_type": "text", + "format": "text", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": false, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.3", + "ordinal_position": 3, + "name": "author_id", + "default_value": null, + "data_type": "bigint", + "format": "int8", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": false, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.4", + "ordinal_position": 4, + "name": "price", + "default_value": null, + "data_type": "numeric", + "format": "numeric", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.5", + "ordinal_position": 5, + "name": "rating", + "default_value": null, + "data_type": "double precision", + "format": "float8", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.6", + "ordinal_position": 6, + "name": "in_print", + "default_value": "true", + "data_type": "boolean", + "format": "bool", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": false, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.7", + "ordinal_position": 7, + "name": "mood", + "default_value": null, + "data_type": "USER-DEFINED", + "format": "mood", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": ["happy", "very happy", "sad"], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.8", + "ordinal_position": 8, + "name": "tags", + "default_value": null, + "data_type": "ARRAY", + "format": "_text", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.9", + "ordinal_position": 9, + "name": "page_counts", + "default_value": null, + "data_type": "ARRAY", + "format": "_int4", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.10", + "ordinal_position": 10, + "name": "metadata", + "default_value": null, + "data_type": "jsonb", + "format": "jsonb", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.11", + "ordinal_position": 11, + "name": "cover_uuid", + "default_value": null, + "data_type": "uuid", + "format": "uuid", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.12", + "ordinal_position": 12, + "name": "published_on", + "default_value": null, + "data_type": "date", + "format": "date", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.13", + "ordinal_position": 13, + "name": "created_at", + "default_value": "now()", + "data_type": "timestamp with time zone", + "format": "timestamptz", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": false, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": "When the row was created" + }, + { + "table_id": 16385, + "schema": "public", + "table": "books", + "id": "16385.14", + "ordinal_position": 14, + "name": "updated_at", + "default_value": null, + "data_type": "timestamp without time zone", + "format": "timestamp", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16401, + "schema": "public", + "table": "authors", + "id": "16401.1", + "ordinal_position": 1, + "name": "id", + "default_value": null, + "data_type": "bigint", + "format": "int8", + "is_identity": true, + "identity_generation": "ALWAYS", + "is_generated": false, + "is_nullable": false, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16401, + "schema": "public", + "table": "authors", + "id": "16401.2", + "ordinal_position": 2, + "name": "name", + "default_value": null, + "data_type": "text", + "format": "text", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": false, + "is_updatable": true, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16420, + "schema": "public", + "table": "author_stats", + "id": "16420.1", + "ordinal_position": 1, + "name": "author_id", + "default_value": null, + "data_type": "bigint", + "format": "int8", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": false, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + }, + { + "table_id": 16420, + "schema": "public", + "table": "author_stats", + "id": "16420.2", + "ordinal_position": 2, + "name": "book_count", + "default_value": null, + "data_type": "bigint", + "format": "int8", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": false, + "is_unique": false, + "enums": [], + "check": null, + "comment": null + } + ], + "relationships": [ + { + "foreign_key_name": "books_author_id_fkey", + "schema": "public", + "relation": "books", + "columns": ["author_id"], + "is_one_to_one": false, + "referenced_schema": "public", + "referenced_relation": "authors", + "referenced_columns": ["id"] + } + ], + "functions": [], + "types": [ + { + "id": 16390, + "name": "mood", + "schema": "public", + "format": "mood", + "enums": ["happy", "very happy", "sad"], + "attributes": [], + "comment": null, + "type_relation_id": null + } + ] +} diff --git a/packages/supabase_typegen/test/generated_schema_behavior_test.dart b/packages/supabase_typegen/test/generated_schema_behavior_test.dart index 2c749c1e2..31ada9d93 100644 --- a/packages/supabase_typegen/test/generated_schema_behavior_test.dart +++ b/packages/supabase_typegen/test/generated_schema_behavior_test.dart @@ -51,6 +51,7 @@ void main() { 'author_id': 7, 'price': 12.5, 'rating': 4, + 'in_print': true, 'mood': 'very happy', 'tags': ['dart', 'types'], 'metadata': {'reprint': true}, @@ -65,6 +66,7 @@ void main() { expect(book.id, 1); expect(book.title, 'A typed row'); expect(book.rating, 4.0); + expect(book.inPrint, isTrue); expect(book.mood, Mood.veryHappy); expect(book.tags, ['dart', 'types']); expect(book.metadata, {'reprint': true}); diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index e73e20e7f..bc2df41d5 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -40,7 +40,7 @@ extension type const AuthorStatsRow(Map _json) int? get bookCount => _json['book_count'] as int?; } -/// Values for inserting a row into `author_stats`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. Use the `set…ToNull` methods to insert SQL NULL explicitly. +/// Values for inserting a row into `author_stats`. Columns that are nullable, identity, or covered by a database default are optional; passing `null` omits the column so the database default applies. Columns the database always generates itself are left out entirely. Use the `set…ToNull` methods to insert SQL NULL explicitly. extension type const AuthorStatsInsert._(Map _json) implements Map { AuthorStatsInsert({int? authorId, int? bookCount}) @@ -88,17 +88,16 @@ extension type const AuthorsRow(Map _json) String get name => _json['name'] as String; } -/// Values for inserting a row into `authors`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. Use the `set…ToNull` methods to insert SQL NULL explicitly. +/// Values for inserting a row into `authors`. Columns that are nullable, identity, or covered by a database default are optional; passing `null` omits the column so the database default applies. Columns the database always generates itself are left out entirely. Use the `set…ToNull` methods to insert SQL NULL explicitly. extension type const AuthorsInsert._(Map _json) implements Map { - AuthorsInsert({required int id, required String name}) - : this._({'id': id, 'name': name}); + AuthorsInsert({required String name}) : this._({'name': name}); } /// Values for updating rows of `authors`. All columns are optional; passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods to write SQL NULL explicitly. extension type const AuthorsUpdate._(Map _json) implements Map { - AuthorsUpdate({int? id, String? name}) : this._({'id': ?id, 'name': ?name}); + AuthorsUpdate({String? name}) : this._({'name': ?name}); } /// Typed access to the `authors` table. @@ -121,7 +120,7 @@ extension type const BooksRow(Map _json) int get authorId => _json['author_id'] as int; num? get price => _json['price'] as num?; double? get rating => (_json['rating'] as num?)?.toDouble(); - bool? get inPrint => _json['in_print'] as bool?; + bool get inPrint => _json['in_print'] as bool; Mood? get mood => switch (_json['mood']) { null => null, final Object value => Mood.fromWire(value as String), @@ -136,17 +135,14 @@ extension type const BooksRow(Map _json) }; /// When the row was created - DateTime? get createdAt => switch (_json['created_at']) { - null => null, - final Object value => DateTime.parse(value as String), - }; + DateTime get createdAt => DateTime.parse(_json['created_at'] as String); DateTime? get updatedAt => switch (_json['updated_at']) { null => null, final Object value => DateTime.parse(value as String), }; } -/// Values for inserting a row into `books`. Columns that are nullable, part of a generated primary key, or covered by a database default are optional; passing `null` omits the column so the database default applies. Use the `set…ToNull` methods to insert SQL NULL explicitly. +/// Values for inserting a row into `books`. Columns that are nullable, identity, or covered by a database default are optional; passing `null` omits the column so the database default applies. Columns the database always generates itself are left out entirely. Use the `set…ToNull` methods to insert SQL NULL explicitly. extension type const BooksInsert._(Map _json) implements Map { BooksInsert({ @@ -190,9 +186,6 @@ extension type const BooksInsert._(Map _json) /// Returns a copy with `rating` set to SQL NULL, overriding any database default. BooksInsert setRatingToNull() => BooksInsert._({..._json, 'rating': null}); - /// Returns a copy with `in_print` set to SQL NULL, overriding any database default. - BooksInsert setInPrintToNull() => BooksInsert._({..._json, 'in_print': null}); - /// Returns a copy with `mood` set to SQL NULL, overriding any database default. BooksInsert setMoodToNull() => BooksInsert._({..._json, 'mood': null}); @@ -215,10 +208,6 @@ extension type const BooksInsert._(Map _json) BooksInsert setPublishedOnToNull() => BooksInsert._({..._json, 'published_on': null}); - /// Returns a copy with `created_at` set to SQL NULL, overriding any database default. - BooksInsert setCreatedAtToNull() => - BooksInsert._({..._json, 'created_at': null}); - /// Returns a copy with `updated_at` set to SQL NULL, overriding any database default. BooksInsert setUpdatedAtToNull() => BooksInsert._({..._json, 'updated_at': null}); @@ -268,9 +257,6 @@ extension type const BooksUpdate._(Map _json) /// Returns a copy with `rating` set to SQL NULL, overriding any database default. BooksUpdate setRatingToNull() => BooksUpdate._({..._json, 'rating': null}); - /// Returns a copy with `in_print` set to SQL NULL, overriding any database default. - BooksUpdate setInPrintToNull() => BooksUpdate._({..._json, 'in_print': null}); - /// Returns a copy with `mood` set to SQL NULL, overriding any database default. BooksUpdate setMoodToNull() => BooksUpdate._({..._json, 'mood': null}); @@ -293,10 +279,6 @@ extension type const BooksUpdate._(Map _json) BooksUpdate setPublishedOnToNull() => BooksUpdate._({..._json, 'published_on': null}); - /// Returns a copy with `created_at` set to SQL NULL, overriding any database default. - BooksUpdate setCreatedAtToNull() => - BooksUpdate._({..._json, 'created_at': null}); - /// Returns a copy with `updated_at` set to SQL NULL, overriding any database default. BooksUpdate setUpdatedAtToNull() => BooksUpdate._({..._json, 'updated_at': null}); diff --git a/packages/supabase_typegen/test/openapi_parser_test.dart b/packages/supabase_typegen/test/postgres_meta_parser_test.dart similarity index 52% rename from packages/supabase_typegen/test/openapi_parser_test.dart rename to packages/supabase_typegen/test/postgres_meta_parser_test.dart index 2c71355f7..043e51cab 100644 --- a/packages/supabase_typegen/test/openapi_parser_test.dart +++ b/packages/supabase_typegen/test/postgres_meta_parser_test.dart @@ -5,16 +5,34 @@ import 'package:supabase_typegen/supabase_typegen.dart'; import 'package:test/test.dart'; void main() { + late Map document; late SchemaDescription schema; setUpAll(() { - final document = - jsonDecode(File('test/fixtures/openapi.json').readAsStringSync()) + document = + jsonDecode( + File( + 'test/fixtures/postgres_meta_schema.json', + ).readAsStringSync(), + ) as Map; - schema = parseOpenApiDocument(document); + schema = parsePostgresMetaDocument(document); }); - test('parses all tables sorted by name', () { + test('rejects unsupported document versions', () { + expect( + () => parsePostgresMetaDocument({...document, 'version': 2}), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('version 2'), + ), + ), + ); + }); + + test('parses tables and views sorted by name', () { expect(schema.tables.map((table) => table.name), [ 'author_stats', 'authors', @@ -22,18 +40,24 @@ void main() { ]); }); - test('parses table comments', () { + test('parses table and view comments', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); expect(books.comment, 'Books available in the library'); + + final authorStats = schema.tables.singleWhere( + (table) => table.name == 'author_stats', + ); + expect(authorStats.comment, 'Aggregated statistics per author'); }); - test('parses primary keys, requiredness and defaults', () { + test('parses primary keys, requiredness, defaults and nullability', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); final id = books.columns.singleWhere((column) => column.name == 'id'); expect(id.isPrimaryKey, isTrue); expect(id.isRequired, isFalse); expect(id.hasDefault, isTrue); expect(id.isNullable, isFalse); + expect(id.isReadOnly, isFalse); final title = books.columns.singleWhere((column) => column.name == 'title'); expect(title.isRequired, isTrue); @@ -44,7 +68,34 @@ void main() { expect(price.isNullable, isTrue); }); - test('parses foreign keys', () { + test('not null columns with a database default are non-nullable reads ' + 'but optional writes', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + final inPrint = books.columns.singleWhere( + (column) => column.name == 'in_print', + ); + expect(inPrint.isNullable, isFalse); + expect(inPrint.isRequired, isFalse); + expect(inPrint.hasDefault, isTrue); + + final createdAt = books.columns.singleWhere( + (column) => column.name == 'created_at', + ); + expect(createdAt.isNullable, isFalse); + expect(createdAt.isRequired, isFalse); + }); + + test('always generated identity columns are read-only', () { + final authors = schema.tables.singleWhere( + (table) => table.name == 'authors', + ); + final id = authors.columns.singleWhere((column) => column.name == 'id'); + expect(id.isReadOnly, isTrue); + expect(id.isRequired, isFalse); + expect(id.isNullable, isFalse); + }); + + test('parses foreign keys from the relationships', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); final authorId = books.columns.singleWhere( (column) => column.name == 'author_id', @@ -71,23 +122,34 @@ void main() { expect(kindOf('cover_uuid'), ColumnTypeKind.text); }); - test('collects Postgres enums', () { + test('collects Postgres enums with their schema qualification', () { expect(schema.enums, hasLength(1)); final mood = schema.enums.single; expect(mood.qualifiedName, 'public.mood'); expect(mood.name, 'mood'); expect(mood.values, ['happy', 'very happy', 'sad']); + + final books = schema.tables.singleWhere((table) => table.name == 'books'); + final moodColumn = books.columns.singleWhere( + (column) => column.name == 'mood', + ); + expect(moodColumn.postgresFormat, 'public.mood'); }); test('parses array columns', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); final tags = books.columns.singleWhere((column) => column.name == 'tags'); - expect(tags.postgresFormat, 'text[]'); + expect(tags.postgresFormat, '_text'); expect(tags.typeKind, ColumnTypeKind.array); expect(tags.elementTypeKind, ColumnTypeKind.text); + + final pageCounts = books.columns.singleWhere( + (column) => column.name == 'page_counts', + ); + expect(pageCounts.elementTypeKind, ColumnTypeKind.integer); }); - test('keeps human column comments without the key markers', () { + test('keeps column comments', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); final id = books.columns.singleWhere((column) => column.name == 'id'); expect(id.comment, isNull); @@ -97,4 +159,15 @@ void main() { ); expect(createdAt.comment, 'When the row was created'); }); + + test('view columns come through like table columns', () { + final authorStats = schema.tables.singleWhere( + (table) => table.name == 'author_stats', + ); + expect(authorStats.columns.map((column) => column.name), [ + 'author_id', + 'book_count', + ]); + expect(authorStats.columns.first.isNullable, isTrue); + }); } diff --git a/packages/supabase_typegen/tool/regenerate_goldens.dart b/packages/supabase_typegen/tool/regenerate_goldens.dart index 9d453d5dd..0dd1e046f 100644 --- a/packages/supabase_typegen/tool/regenerate_goldens.dart +++ b/packages/supabase_typegen/tool/regenerate_goldens.dart @@ -8,9 +8,11 @@ import 'package:supabase_typegen/supabase_typegen.dart'; /// Run from the package root with `dart run tool/regenerate_goldens.dart`. void main() { final document = - jsonDecode(File('test/fixtures/openapi.json').readAsStringSync()) + jsonDecode( + File('test/fixtures/postgres_meta_schema.json').readAsStringSync(), + ) as Map; - final schema = parseOpenApiDocument(document); + final schema = parsePostgresMetaDocument(document); File( 'test/goldens/supabase_schema.dart', ).writeAsStringSync(generateDartCode(schema)); From ec866b3032073bd29230d1d52f5de6861e90171f Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 17 Aug 2026 15:28:45 +0200 Subject: [PATCH 09/31] fix(supabase_typegen): wrap generated doc comments at 80 characters --- .../lib/src/dart_generator.dart | 44 ++++++--- .../test/goldens/supabase_schema.dart | 96 +++++++++++++------ 2 files changed, 100 insertions(+), 40 deletions(-) diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index 6944dbbc2..468817e7d 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -223,8 +223,8 @@ void _writeValues( if (!column.isReadOnly) column, ]; + _writeDocComment(buffer, docLine); buffer - ..writeln('/// $docLine') ..writeln('extension type const $typeName._(Map _json)') ..writeln(' implements Map {') ..writeln(' $typeName({'); @@ -257,16 +257,17 @@ void _writeValues( if (!column.isNullable) continue; final name = memberNames[column.name]!; final methodName = 'set${name[0].toUpperCase()}${name.substring(1)}ToNull'; - buffer - ..writeln() - ..writeln( - ' /// Returns a copy with `${column.name}` set to SQL NULL, ' - 'overriding any database default.', - ) - ..writeln( - ' $typeName $methodName() => ' - '$typeName._({..._json, ${_stringLiteral(column.name)}: null});', - ); + buffer.writeln(); + _writeDocComment( + buffer, + 'Returns a copy with `${column.name}` set to SQL NULL, overriding any ' + 'database default.', + indent: ' ', + ); + buffer.writeln( + ' $typeName $methodName() => ' + '$typeName._({..._json, ${_stringLiteral(column.name)}: null});', + ); } buffer ..writeln('}') @@ -456,9 +457,28 @@ void _writeDocComment( String indent = '', }) { if (comment == null) return; + final width = 80 - indent.length - '/// '.length; for (final line in comment.trim().split('\n')) { - buffer.writeln('$indent/// ${line.trim()}'); + for (final wrapped in _wrap(line.trim(), width)) { + buffer.writeln('$indent/// $wrapped'); + } + } +} + +/// Greedily wraps [text] into lines of at most [width] characters, keeping +/// words longer than [width] on their own line. +Iterable _wrap(String text, int width) sync* { + final words = text.split(' ').where((word) => word.isNotEmpty); + final line = StringBuffer(); + for (final word in words) { + if (line.isNotEmpty && line.length + 1 + word.length > width) { + yield line.toString(); + line.clear(); + } + if (line.isNotEmpty) line.write(' '); + line.write(word); } + if (line.isNotEmpty) yield line.toString(); } String _stringLiteral(String value) { diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index bc2df41d5..de46fcc15 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -40,32 +40,42 @@ extension type const AuthorStatsRow(Map _json) int? get bookCount => _json['book_count'] as int?; } -/// Values for inserting a row into `author_stats`. Columns that are nullable, identity, or covered by a database default are optional; passing `null` omits the column so the database default applies. Columns the database always generates itself are left out entirely. Use the `set…ToNull` methods to insert SQL NULL explicitly. +/// Values for inserting a row into `author_stats`. Columns that are nullable, +/// identity, or covered by a database default are optional; passing `null` +/// omits the column so the database default applies. Columns the database +/// always generates itself are left out entirely. Use the `set…ToNull` methods +/// to insert SQL NULL explicitly. extension type const AuthorStatsInsert._(Map _json) implements Map { AuthorStatsInsert({int? authorId, int? bookCount}) : this._({'author_id': ?authorId, 'book_count': ?bookCount}); - /// Returns a copy with `author_id` set to SQL NULL, overriding any database default. + /// Returns a copy with `author_id` set to SQL NULL, overriding any database + /// default. AuthorStatsInsert setAuthorIdToNull() => AuthorStatsInsert._({..._json, 'author_id': null}); - /// Returns a copy with `book_count` set to SQL NULL, overriding any database default. + /// Returns a copy with `book_count` set to SQL NULL, overriding any database + /// default. AuthorStatsInsert setBookCountToNull() => AuthorStatsInsert._({..._json, 'book_count': null}); } -/// Values for updating rows of `author_stats`. All columns are optional; passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods to write SQL NULL explicitly. +/// Values for updating rows of `author_stats`. All columns are optional; +/// passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` +/// methods to write SQL NULL explicitly. extension type const AuthorStatsUpdate._(Map _json) implements Map { AuthorStatsUpdate({int? authorId, int? bookCount}) : this._({'author_id': ?authorId, 'book_count': ?bookCount}); - /// Returns a copy with `author_id` set to SQL NULL, overriding any database default. + /// Returns a copy with `author_id` set to SQL NULL, overriding any database + /// default. AuthorStatsUpdate setAuthorIdToNull() => AuthorStatsUpdate._({..._json, 'author_id': null}); - /// Returns a copy with `book_count` set to SQL NULL, overriding any database default. + /// Returns a copy with `book_count` set to SQL NULL, overriding any database + /// default. AuthorStatsUpdate setBookCountToNull() => AuthorStatsUpdate._({..._json, 'book_count': null}); } @@ -88,13 +98,19 @@ extension type const AuthorsRow(Map _json) String get name => _json['name'] as String; } -/// Values for inserting a row into `authors`. Columns that are nullable, identity, or covered by a database default are optional; passing `null` omits the column so the database default applies. Columns the database always generates itself are left out entirely. Use the `set…ToNull` methods to insert SQL NULL explicitly. +/// Values for inserting a row into `authors`. Columns that are nullable, +/// identity, or covered by a database default are optional; passing `null` +/// omits the column so the database default applies. Columns the database +/// always generates itself are left out entirely. Use the `set…ToNull` methods +/// to insert SQL NULL explicitly. extension type const AuthorsInsert._(Map _json) implements Map { AuthorsInsert({required String name}) : this._({'name': name}); } -/// Values for updating rows of `authors`. All columns are optional; passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods to write SQL NULL explicitly. +/// Values for updating rows of `authors`. All columns are optional; passing +/// `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods +/// to write SQL NULL explicitly. extension type const AuthorsUpdate._(Map _json) implements Map { AuthorsUpdate({String? name}) : this._({'name': ?name}); @@ -142,7 +158,11 @@ extension type const BooksRow(Map _json) }; } -/// Values for inserting a row into `books`. Columns that are nullable, identity, or covered by a database default are optional; passing `null` omits the column so the database default applies. Columns the database always generates itself are left out entirely. Use the `set…ToNull` methods to insert SQL NULL explicitly. +/// Values for inserting a row into `books`. Columns that are nullable, +/// identity, or covered by a database default are optional; passing `null` +/// omits the column so the database default applies. Columns the database +/// always generates itself are left out entirely. Use the `set…ToNull` methods +/// to insert SQL NULL explicitly. extension type const BooksInsert._(Map _json) implements Map { BooksInsert({ @@ -180,40 +200,51 @@ extension type const BooksInsert._(Map _json) 'updated_at': ?updatedAt?.toIso8601String(), }); - /// Returns a copy with `price` set to SQL NULL, overriding any database default. + /// Returns a copy with `price` set to SQL NULL, overriding any database + /// default. BooksInsert setPriceToNull() => BooksInsert._({..._json, 'price': null}); - /// Returns a copy with `rating` set to SQL NULL, overriding any database default. + /// Returns a copy with `rating` set to SQL NULL, overriding any database + /// default. BooksInsert setRatingToNull() => BooksInsert._({..._json, 'rating': null}); - /// Returns a copy with `mood` set to SQL NULL, overriding any database default. + /// Returns a copy with `mood` set to SQL NULL, overriding any database + /// default. BooksInsert setMoodToNull() => BooksInsert._({..._json, 'mood': null}); - /// Returns a copy with `tags` set to SQL NULL, overriding any database default. + /// Returns a copy with `tags` set to SQL NULL, overriding any database + /// default. BooksInsert setTagsToNull() => BooksInsert._({..._json, 'tags': null}); - /// Returns a copy with `page_counts` set to SQL NULL, overriding any database default. + /// Returns a copy with `page_counts` set to SQL NULL, overriding any database + /// default. BooksInsert setPageCountsToNull() => BooksInsert._({..._json, 'page_counts': null}); - /// Returns a copy with `metadata` set to SQL NULL, overriding any database default. + /// Returns a copy with `metadata` set to SQL NULL, overriding any database + /// default. BooksInsert setMetadataToNull() => BooksInsert._({..._json, 'metadata': null}); - /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database default. + /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database + /// default. BooksInsert setCoverUuidToNull() => BooksInsert._({..._json, 'cover_uuid': null}); - /// Returns a copy with `published_on` set to SQL NULL, overriding any database default. + /// Returns a copy with `published_on` set to SQL NULL, overriding any + /// database default. BooksInsert setPublishedOnToNull() => BooksInsert._({..._json, 'published_on': null}); - /// Returns a copy with `updated_at` set to SQL NULL, overriding any database default. + /// Returns a copy with `updated_at` set to SQL NULL, overriding any database + /// default. BooksInsert setUpdatedAtToNull() => BooksInsert._({..._json, 'updated_at': null}); } -/// Values for updating rows of `books`. All columns are optional; passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods to write SQL NULL explicitly. +/// Values for updating rows of `books`. All columns are optional; passing +/// `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods +/// to write SQL NULL explicitly. extension type const BooksUpdate._(Map _json) implements Map { BooksUpdate({ @@ -251,35 +282,44 @@ extension type const BooksUpdate._(Map _json) 'updated_at': ?updatedAt?.toIso8601String(), }); - /// Returns a copy with `price` set to SQL NULL, overriding any database default. + /// Returns a copy with `price` set to SQL NULL, overriding any database + /// default. BooksUpdate setPriceToNull() => BooksUpdate._({..._json, 'price': null}); - /// Returns a copy with `rating` set to SQL NULL, overriding any database default. + /// Returns a copy with `rating` set to SQL NULL, overriding any database + /// default. BooksUpdate setRatingToNull() => BooksUpdate._({..._json, 'rating': null}); - /// Returns a copy with `mood` set to SQL NULL, overriding any database default. + /// Returns a copy with `mood` set to SQL NULL, overriding any database + /// default. BooksUpdate setMoodToNull() => BooksUpdate._({..._json, 'mood': null}); - /// Returns a copy with `tags` set to SQL NULL, overriding any database default. + /// Returns a copy with `tags` set to SQL NULL, overriding any database + /// default. BooksUpdate setTagsToNull() => BooksUpdate._({..._json, 'tags': null}); - /// Returns a copy with `page_counts` set to SQL NULL, overriding any database default. + /// Returns a copy with `page_counts` set to SQL NULL, overriding any database + /// default. BooksUpdate setPageCountsToNull() => BooksUpdate._({..._json, 'page_counts': null}); - /// Returns a copy with `metadata` set to SQL NULL, overriding any database default. + /// Returns a copy with `metadata` set to SQL NULL, overriding any database + /// default. BooksUpdate setMetadataToNull() => BooksUpdate._({..._json, 'metadata': null}); - /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database default. + /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database + /// default. BooksUpdate setCoverUuidToNull() => BooksUpdate._({..._json, 'cover_uuid': null}); - /// Returns a copy with `published_on` set to SQL NULL, overriding any database default. + /// Returns a copy with `published_on` set to SQL NULL, overriding any + /// database default. BooksUpdate setPublishedOnToNull() => BooksUpdate._({..._json, 'published_on': null}); - /// Returns a copy with `updated_at` set to SQL NULL, overriding any database default. + /// Returns a copy with `updated_at` set to SQL NULL, overriding any database + /// default. BooksUpdate setUpdatedAtToNull() => BooksUpdate._({..._json, 'updated_at': null}); } From f6f3245923e9947e5759dcd2e915e40a02c174be Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 17 Aug 2026 16:13:37 +0200 Subject: [PATCH 10/31] feat(supabase_typegen): support writing the generated code to stdout with --output - --- .../bin/supabase_typegen.dart | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index 57af6626a..71cd1f5e7 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -22,7 +22,7 @@ final _argParser = ArgParser() 'output', abbr: 'o', defaultsTo: 'lib/supabase_schema.g.dart', - help: 'Path of the generated Dart file.', + help: 'Path of the generated Dart file, or - to write the code to stdout.', ) ..addOption( 'import', @@ -104,16 +104,25 @@ Future _run(List arguments) async { final code = generateDartCode(schema, importUri: options.option('import')!); - final outputFile = File(options.option('output')!); - outputFile.parent.createSync(recursive: true); - outputFile.writeAsStringSync(code); + 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; - stdout.writeln( - 'Generated ${outputFile.path} with $emittedTables tables and ' + 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.'}', From 115b3f9f505c339d2f6ed227bf5fe6b38ff6f7be Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 09:39:30 +0200 Subject: [PATCH 11/31] fix(supabase_typegen): handle all-read-only tables and string-serialized Postgres types --- .../lib/src/dart_generator.dart | 54 ++++++++++--------- .../lib/src/postgres_meta_parser.dart | 15 +++++- .../test/dart_generator_test.dart | 27 ++++++++++ .../test/postgres_meta_parser_test.dart | 44 +++++++++++++++ 4 files changed, 115 insertions(+), 25 deletions(-) diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index 468817e7d..ddf769620 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -226,33 +226,39 @@ void _writeValues( _writeDocComment(buffer, docLine); buffer ..writeln('extension type const $typeName._(Map _json)') - ..writeln(' implements Map {') - ..writeln(' $typeName({'); - for (final column in writableColumns) { - final binding = bindings[column.name]!; - final name = memberNames[column.name]!; - if (isRequired(column)) { - buffer.writeln(' required ${binding.dartType} $name,'); - } else { - buffer.writeln(' ${binding.dartType}? $name,'); + ..writeln(' implements Map {'); + if (writableColumns.isEmpty) { + // A named parameter list cannot be empty, so a table whose columns are + // all read-only gets a parameterless constructor. + buffer.writeln(' $typeName() : this._({});'); + } else { + buffer.writeln(' $typeName({'); + for (final column in writableColumns) { + final binding = bindings[column.name]!; + final name = memberNames[column.name]!; + if (isRequired(column)) { + buffer.writeln(' required ${binding.dartType} $name,'); + } else { + buffer.writeln(' ${binding.dartType}? $name,'); + } } - } - buffer.writeln(' }) : this._({'); - for (final column in writableColumns) { - final binding = bindings[column.name]!; - final name = memberNames[column.name]!; - final key = _stringLiteral(column.name); - if (isRequired(column)) { - buffer.writeln( - ' $key: ${_writeExpression(name, binding, nullable: false)},', - ); - } else { - buffer.writeln( - ' $key: ?${_writeExpression(name, binding, nullable: true)},', - ); + buffer.writeln(' }) : this._({'); + for (final column in writableColumns) { + final binding = bindings[column.name]!; + final name = memberNames[column.name]!; + final key = _stringLiteral(column.name); + if (isRequired(column)) { + buffer.writeln( + ' $key: ${_writeExpression(name, binding, nullable: false)},', + ); + } else { + buffer.writeln( + ' $key: ?${_writeExpression(name, binding, nullable: true)},', + ); + } } + buffer.writeln(' });'); } - buffer.writeln(' });'); for (final column in writableColumns) { if (!column.isNullable) continue; final name = memberNames[column.name]!; diff --git a/packages/supabase_typegen/lib/src/postgres_meta_parser.dart b/packages/supabase_typegen/lib/src/postgres_meta_parser.dart index 9eff89af1..ca227cf1f 100644 --- a/packages/supabase_typegen/lib/src/postgres_meta_parser.dart +++ b/packages/supabase_typegen/lib/src/postgres_meta_parser.dart @@ -3,19 +3,32 @@ import 'schema_description.dart'; /// The metadata document version this parser understands. const supportedPostgresMetaVersion = 1; -const _integerFormats = {'int2', 'int4', 'int8'}; +const _integerFormats = {'int2', 'int4', 'int8', 'oid'}; const _floatingFormats = {'float4', 'float8'}; + +/// Types that PostgREST serializes as JSON strings. const _textFormats = { 'text', 'citext', 'varchar', 'bpchar', 'char', + 'name', 'uuid', 'time', 'timetz', 'interval', 'bytea', + 'inet', + 'cidr', + 'macaddr', + 'macaddr8', + 'money', + 'xml', + 'bit', + 'varbit', + 'tsvector', + 'tsquery', }; const _jsonFormats = {'json', 'jsonb'}; diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index 511ec4950..d34360261 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -75,4 +75,31 @@ void main() { expect(code, contains('AuthorsUpdate({String? name})')); expect(code, contains("TableColumn('id')")); }); + + test('tables whose columns are all read-only get parameterless ' + 'insert and update constructors', () { + final table = TableDescription( + name: 'counters', + comment: null, + columns: [ + ColumnDescription( + name: 'id', + postgresFormat: 'int8', + typeKind: ColumnTypeKind.integer, + isRequired: false, + isPrimaryKey: true, + hasDefault: true, + isNullable: false, + isReadOnly: true, + ), + ], + ); + final code = generateDartCode( + SchemaDescription(schemaName: 'public', tables: [table], enums: []), + ); + + expect(code, contains('CountersInsert() : this._({});')); + expect(code, contains('CountersUpdate() : this._({});')); + expect(code, contains("int get id => _json['id'] as int;")); + }); } diff --git a/packages/supabase_typegen/test/postgres_meta_parser_test.dart b/packages/supabase_typegen/test/postgres_meta_parser_test.dart index 043e51cab..d08e1e408 100644 --- a/packages/supabase_typegen/test/postgres_meta_parser_test.dart +++ b/packages/supabase_typegen/test/postgres_meta_parser_test.dart @@ -122,6 +122,50 @@ void main() { expect(kindOf('cover_uuid'), ColumnTypeKind.text); }); + test('types that PostgREST serializes as strings read as text', () { + Map columnOf(String format) => { + 'table_id': 1, + 'schema': 'public', + 'table': 'servers', + 'id': '1.1', + 'ordinal_position': 1, + 'name': 'value', + 'default_value': null, + 'data_type': format, + 'format': format, + 'is_identity': false, + 'identity_generation': null, + 'is_generated': false, + 'is_nullable': true, + 'is_updatable': true, + 'is_unique': false, + 'enums': [], + 'check': null, + 'comment': null, + }; + + for (final format in ['inet', 'cidr', 'macaddr', 'money', 'xml', 'name']) { + final parsed = parsePostgresMetaDocument({ + 'version': 1, + 'tables': [ + { + 'id': 1, + 'schema': 'public', + 'name': 'servers', + 'comment': null, + 'primary_keys': >[], + }, + ], + 'columns': [columnOf(format)], + }); + expect( + parsed.tables.single.columns.single.typeKind, + ColumnTypeKind.text, + reason: '$format should map to text', + ); + } + }); + test('collects Postgres enums with their schema qualification', () { expect(schema.enums, hasLength(1)); final mood = schema.enums.single; From 214dc4b15bf1b6c4046ebfb480ffb5561fd5dff1 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 09:41:20 +0200 Subject: [PATCH 12/31] docs(supabase_typegen): document the cross-schema enum name limitation --- packages/supabase_typegen/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index e70289ed4..c70424e09 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -68,5 +68,9 @@ await client.table(Books.table).insert( - `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. From 9b76b147e1a3ffc32c5cd2f2b1b46bab0ef11d48 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 10:09:35 +0200 Subject: [PATCH 13/31] docs(supabase_typegen): recommend the Supabase CLI as the easiest way to run the generator --- packages/supabase_typegen/README.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index c70424e09..2f3cfc0c4 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -14,7 +14,19 @@ For every table the generator emits: ## Usage -First dump the schema metadata with the Supabase CLI, then generate: +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 @@ -22,16 +34,10 @@ dart run supabase_typegen --input schema.json \ --output lib/supabase_schema.g.dart ``` -Any of the CLI's connection flags work (`--local`, `--linked`, -`--db-url`, `--project-id`). Until CLI support for `--lang json` ships, the -same document comes straight from +Until CLI support for `--lang dart` and `--lang json` ships, the same +metadata document comes straight from [postgres-meta](https://github.com/supabase/postgres-meta) with -`PG_META_GENERATE_TYPES=json` or its `/generators/json` endpoint. Pass -`--input -` to read the document from stdin: - -```sh -supabase gen types --lang json --local | dart run supabase_typegen --input - -``` +`PG_META_GENERATE_TYPES=json` or its `/generators/json` endpoint. Use `--schema` to generate for a schema other than `public`, and `--import` to change which library the generated file imports `PostgrestTable` and From 047b1baf9d7546fdb9dbe3ba38e9d481663814a4 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 10:15:17 +0200 Subject: [PATCH 14/31] docs(supabase_typegen): explain when committing schema.json makes sense --- packages/supabase_typegen/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 2f3cfc0c4..12bb4257d 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -39,6 +39,27 @@ metadata document comes straight from [postgres-meta](https://github.com/supabase/postgres-meta) with `PG_META_GENERATE_TYPES=json` or its `/generators/json` endpoint. +## 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. From 45667c83d01605c4e9da295ceb1894894adfca21 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 18 Aug 2026 13:48:17 +0200 Subject: [PATCH 15/31] refactor(supabase_typegen): consume the postgrest-typegen GeneratorMetadata contract --- packages/supabase_typegen/README.md | 9 +++-- .../bin/supabase_typegen.dart | 8 ++-- ...er.dart => generator_metadata_parser.dart} | 34 ++++++---------- .../lib/src/schema_description.dart | 4 -- .../lib/supabase_typegen.dart | 2 +- .../test/dart_generator_test.dart | 5 +-- ...ta_schema.json => generator_metadata.json} | 39 +++++++++++-------- ...rt => generator_metadata_parser_test.dart} | 17 ++++---- .../tool/regenerate_goldens.dart | 4 +- 9 files changed, 56 insertions(+), 66 deletions(-) rename packages/supabase_typegen/lib/src/{postgres_meta_parser.dart => generator_metadata_parser.dart} (87%) rename packages/supabase_typegen/test/fixtures/{postgres_meta_schema.json => generator_metadata.json} (94%) rename packages/supabase_typegen/test/{postgres_meta_parser_test.dart => generator_metadata_parser_test.dart} (93%) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 12bb4257d..a88b2a6cc 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -34,10 +34,11 @@ dart run supabase_typegen --input schema.json \ --output lib/supabase_schema.g.dart ``` -Until CLI support for `--lang dart` and `--lang json` ships, the same -metadata document comes straight from -[postgres-meta](https://github.com/supabase/postgres-meta) with -`PG_META_GENERATE_TYPES=json` or its `/generators/json` endpoint. +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 diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index 71cd1f5e7..eeb740e11 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -9,7 +9,7 @@ final _argParser = ArgParser() 'input', abbr: 'i', help: - 'Path of the postgres-meta generator metadata document, or - to ' + 'Path of the GeneratorMetadata document, or - to ' 'read it from stdin. Produce it with ' '`supabase gen types --lang json`.', ) @@ -65,7 +65,7 @@ Future _run(List arguments) async { final input = options.option('input'); if (input == null) { stderr.writeln( - '--input is required: the path of a postgres-meta generator metadata ' + '--input is required: the path of a GeneratorMetadata ' 'document, or - to read it from stdin. Produce it with ' '`supabase gen types --lang json`.', ); @@ -87,7 +87,7 @@ Future _run(List arguments) async { final schemaName = options.option('schema')!; final SchemaDescription schema; try { - schema = parsePostgresMetaDocument( + schema = parseGeneratorMetadata( jsonDecode(contents) as Map, schemaName: schemaName, ); @@ -96,7 +96,7 @@ Future _run(List arguments) async { return 65; } on TypeError { stderr.writeln( - 'The document in $input is not postgres-meta generator metadata. ' + 'The document in $input is not a GeneratorMetadata document. ' 'Produce it with `supabase gen types --lang json`.', ); return 65; diff --git a/packages/supabase_typegen/lib/src/postgres_meta_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart similarity index 87% rename from packages/supabase_typegen/lib/src/postgres_meta_parser.dart rename to packages/supabase_typegen/lib/src/generator_metadata_parser.dart index ca227cf1f..136c60a51 100644 --- a/packages/supabase_typegen/lib/src/postgres_meta_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -1,8 +1,5 @@ import 'schema_description.dart'; -/// The metadata document version this parser understands. -const supportedPostgresMetaVersion = 1; - const _integerFormats = {'int2', 'int4', 'int8', 'oid'}; const _floatingFormats = {'float4', 'float8'}; @@ -58,22 +55,22 @@ ColumnTypeKind _elementTypeKind(String elementFormat, {required bool isEnum}) { return kind == ColumnTypeKind.enumType ? ColumnTypeKind.text : kind; } -/// Parses the generator metadata document that postgres-meta emits from its -/// `json` generator (`supabase gen types --lang json`, the -/// `/generators/json` endpoint, or `PG_META_GENERATE_TYPES=json`) into a -/// [SchemaDescription] for [schemaName]. +/// Parses a `GeneratorMetadata` document, the introspection contract shared +/// by `@supabase/postgrest-typegen` and postgres-meta +/// (`supabase gen types --lang json`), into a [SchemaDescription] for +/// [schemaName]. /// -/// Throws a [FormatException] when the document does not carry the supported -/// `version`. -SchemaDescription parsePostgresMetaDocument( +/// Throws a [FormatException] when the document does not have the +/// `GeneratorMetadata` shape. +SchemaDescription parseGeneratorMetadata( Map document, { String schemaName = 'public', }) { - final version = document['version']; - if (version != supportedPostgresMetaVersion) { - throw FormatException( - 'Unsupported postgres-meta document version $version; this version of ' - 'supabase_typegen supports version $supportedPostgresMetaVersion.', + if (document['tables'] is! List || + document['columns'] is! List) { + throw const FormatException( + 'Not a GeneratorMetadata document: expected the introspection contract ' + 'of @supabase/postgrest-typegen, with "tables" and "columns" lists.', ); } @@ -106,12 +103,6 @@ SchemaDescription parsePostgresMetaDocument( for (final relation in relations) { final relationName = relation['name'] as String; - final primaryKeyNames = { - for (final primaryKey - in (relation['primary_keys'] as List? ?? const []) - .cast>()) - primaryKey['name'] as String, - }; final columns = []; for (final column @@ -150,7 +141,6 @@ SchemaDescription parsePostgresMetaDocument( : null, enumValues: isEnum ? enumValues : null, isRequired: !isNullable && !hasDefault, - isPrimaryKey: primaryKeyNames.contains(name), hasDefault: hasDefault, isNullable: isNullable, isReadOnly: diff --git a/packages/supabase_typegen/lib/src/schema_description.dart b/packages/supabase_typegen/lib/src/schema_description.dart index 657a8b176..d8a3b5a77 100644 --- a/packages/supabase_typegen/lib/src/schema_description.dart +++ b/packages/supabase_typegen/lib/src/schema_description.dart @@ -86,7 +86,6 @@ class ColumnDescription { required this.postgresFormat, required this.typeKind, required this.isRequired, - required this.isPrimaryKey, required this.hasDefault, required this.isNullable, this.isReadOnly = false, @@ -116,9 +115,6 @@ class ColumnDescription { /// it required on insert. final bool isRequired; - /// Whether the column is part of the primary key. - final bool isPrimaryKey; - /// Whether the column has a database default. final bool hasDefault; diff --git a/packages/supabase_typegen/lib/supabase_typegen.dart b/packages/supabase_typegen/lib/supabase_typegen.dart index bb41ad3a1..aeb9b9e2d 100644 --- a/packages/supabase_typegen/lib/supabase_typegen.dart +++ b/packages/supabase_typegen/lib/supabase_typegen.dart @@ -4,5 +4,5 @@ library; export 'src/dart_generator.dart'; export 'src/identifiers.dart'; -export 'src/postgres_meta_parser.dart'; +export 'src/generator_metadata_parser.dart'; export 'src/schema_description.dart'; diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index d34360261..7a9573780 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -17,11 +17,11 @@ void main() { final document = jsonDecode( File( - 'test/fixtures/postgres_meta_schema.json', + 'test/fixtures/generator_metadata.json', ).readAsStringSync(), ) as Map; - schema = parsePostgresMetaDocument(document); + schema = parseGeneratorMetadata(document); }); test('matches the golden output', () { @@ -87,7 +87,6 @@ void main() { postgresFormat: 'int8', typeKind: ColumnTypeKind.integer, isRequired: false, - isPrimaryKey: true, hasDefault: true, isNullable: false, isReadOnly: true, diff --git a/packages/supabase_typegen/test/fixtures/postgres_meta_schema.json b/packages/supabase_typegen/test/fixtures/generator_metadata.json similarity index 94% rename from packages/supabase_typegen/test/fixtures/postgres_meta_schema.json rename to packages/supabase_typegen/test/fixtures/generator_metadata.json index f0058015b..e4286148c 100644 --- a/packages/supabase_typegen/test/fixtures/postgres_meta_schema.json +++ b/packages/supabase_typegen/test/fixtures/generator_metadata.json @@ -1,7 +1,10 @@ { - "version": 1, "schemas": [ - { "id": 2200, "name": "public", "owner": "postgres" } + { + "id": 2200, + "name": "public", + "owner": "postgres" + } ], "tables": [ { @@ -15,11 +18,7 @@ "size": "8192 bytes", "live_rows_estimate": 0, "dead_rows_estimate": 0, - "comment": "Books available in the library", - "primary_keys": [ - { "schema": "public", "table_name": "books", "name": "id", "table_id": 16385 } - ], - "relationships": [] + "comment": "Books available in the library" }, { "id": 16401, @@ -32,11 +31,7 @@ "size": "8192 bytes", "live_rows_estimate": 0, "dead_rows_estimate": 0, - "comment": null, - "primary_keys": [ - { "schema": "public", "table_name": "authors", "name": "id", "table_id": 16401 } - ], - "relationships": [] + "comment": null } ], "foreignTables": [], @@ -187,7 +182,11 @@ "is_nullable": true, "is_updatable": true, "is_unique": false, - "enums": ["happy", "very happy", "sad"], + "enums": [ + "happy", + "very happy", + "sad" + ], "check": null, "comment": null }, @@ -417,11 +416,15 @@ "foreign_key_name": "books_author_id_fkey", "schema": "public", "relation": "books", - "columns": ["author_id"], + "columns": [ + "author_id" + ], "is_one_to_one": false, "referenced_schema": "public", "referenced_relation": "authors", - "referenced_columns": ["id"] + "referenced_columns": [ + "id" + ] } ], "functions": [], @@ -431,7 +434,11 @@ "name": "mood", "schema": "public", "format": "mood", - "enums": ["happy", "very happy", "sad"], + "enums": [ + "happy", + "very happy", + "sad" + ], "attributes": [], "comment": null, "type_relation_id": null diff --git a/packages/supabase_typegen/test/postgres_meta_parser_test.dart b/packages/supabase_typegen/test/generator_metadata_parser_test.dart similarity index 93% rename from packages/supabase_typegen/test/postgres_meta_parser_test.dart rename to packages/supabase_typegen/test/generator_metadata_parser_test.dart index d08e1e408..324d475c3 100644 --- a/packages/supabase_typegen/test/postgres_meta_parser_test.dart +++ b/packages/supabase_typegen/test/generator_metadata_parser_test.dart @@ -12,21 +12,21 @@ void main() { document = jsonDecode( File( - 'test/fixtures/postgres_meta_schema.json', + 'test/fixtures/generator_metadata.json', ).readAsStringSync(), ) as Map; - schema = parsePostgresMetaDocument(document); + schema = parseGeneratorMetadata(document); }); - test('rejects unsupported document versions', () { + test('rejects documents without the GeneratorMetadata shape', () { expect( - () => parsePostgresMetaDocument({...document, 'version': 2}), + () => parseGeneratorMetadata({'swagger': '2.0', 'definitions': {}}), throwsA( isA().having( (error) => error.message, 'message', - contains('version 2'), + contains('GeneratorMetadata'), ), ), ); @@ -50,10 +50,9 @@ void main() { expect(authorStats.comment, 'Aggregated statistics per author'); }); - test('parses primary keys, requiredness, defaults and nullability', () { + test('parses requiredness, defaults and nullability', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); final id = books.columns.singleWhere((column) => column.name == 'id'); - expect(id.isPrimaryKey, isTrue); expect(id.isRequired, isFalse); expect(id.hasDefault, isTrue); expect(id.isNullable, isFalse); @@ -145,15 +144,13 @@ void main() { }; for (final format in ['inet', 'cidr', 'macaddr', 'money', 'xml', 'name']) { - final parsed = parsePostgresMetaDocument({ - 'version': 1, + final parsed = parseGeneratorMetadata({ 'tables': [ { 'id': 1, 'schema': 'public', 'name': 'servers', 'comment': null, - 'primary_keys': >[], }, ], 'columns': [columnOf(format)], diff --git a/packages/supabase_typegen/tool/regenerate_goldens.dart b/packages/supabase_typegen/tool/regenerate_goldens.dart index 0dd1e046f..aa1f77cbf 100644 --- a/packages/supabase_typegen/tool/regenerate_goldens.dart +++ b/packages/supabase_typegen/tool/regenerate_goldens.dart @@ -9,10 +9,10 @@ import 'package:supabase_typegen/supabase_typegen.dart'; void main() { final document = jsonDecode( - File('test/fixtures/postgres_meta_schema.json').readAsStringSync(), + File('test/fixtures/generator_metadata.json').readAsStringSync(), ) as Map; - final schema = parsePostgresMetaDocument(document); + final schema = parseGeneratorMetadata(document); File( 'test/goldens/supabase_schema.dart', ).writeAsStringSync(generateDartCode(schema)); From 28192b9525da361450b5d76e45472af6d723c7c1 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 26 Aug 2026 16:56:12 +0200 Subject: [PATCH 16/31] fix(supabase_typegen): match the workspace postgrest version --- packages/supabase_typegen/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supabase_typegen/pubspec.yaml b/packages/supabase_typegen/pubspec.yaml index 228705f95..3958d0899 100644 --- a/packages/supabase_typegen/pubspec.yaml +++ b/packages/supabase_typegen/pubspec.yaml @@ -23,6 +23,6 @@ dependencies: dev_dependencies: http: ^1.6.0 - postgrest: ^2.9.0 + postgrest: 3.0.0-dev.1 supabase_lints: ^0.1.1 test: ^1.25.0 From ead96c30c5cd87ee6ac6477567404c83c8fe91ea Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 31 Aug 2026 19:49:12 +0200 Subject: [PATCH 17/31] feat(supabase_typegen): gate insert and update generation on relation and column writability Tables and foreign tables stay fully writable. Views follow the is_insert_enabled and is_update_enabled flags of the GeneratorMetadata contract, falling back to is_updatable for documents that predate the flags, and materialized views are never writable. Columns the database reports as not updatable, such as computed view columns, are read-only like generated columns. --- .../lib/src/dart_generator.dart | 67 +++++----- .../lib/src/generator_metadata_parser.dart | 54 +++++++- .../lib/src/schema_description.dart | 19 ++- .../test/dart_generator_test.dart | 46 +++++++ .../test/generator_metadata_parser_test.dart | 122 ++++++++++++++++++ .../test/goldens/supabase_schema.dart | 40 ------ 6 files changed, 272 insertions(+), 76 deletions(-) diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index ddf769620..e24b5d35c 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -139,13 +139,17 @@ void _writeTable( ) { final baseName = pascalCase(table.name); final rowType = typeNames.claim('${baseName}Row'); - final insertType = typeNames.claim('${baseName}Insert'); - final updateType = typeNames.claim('${baseName}Update'); + final insertType = table.isInsertable + ? typeNames.claim('${baseName}Insert') + : null; + final updateType = table.isUpdatable + ? typeNames.claim('${baseName}Update') + : null; final namespaceType = typeNames.claim(baseName); final memberNames = _uniqueMemberNames( [for (final column in table.columns) column.name], - reserved: {rowType, insertType, updateType}, + reserved: {rowType, ?insertType, ?updateType}, ); final bindings = { for (final column in table.columns) @@ -153,32 +157,37 @@ void _writeTable( }; _writeRow(buffer, table, rowType, memberNames, bindings); - _writeValues( - buffer, - table, - insertType, - memberNames, - bindings, - requireRequiredColumns: true, - docLine: - 'Values for inserting a row into `${table.name}`. Columns that are ' - 'nullable, identity, or covered by a database default are optional; ' - 'passing `null` omits the column so the database default applies. ' - 'Columns the database always generates itself are left out entirely. ' - 'Use the `set…ToNull` methods to insert SQL NULL explicitly.', - ); - _writeValues( - buffer, - table, - updateType, - memberNames, - bindings, - requireRequiredColumns: false, - docLine: - 'Values for updating rows of `${table.name}`. All columns are ' - 'optional; passing `null` omits the column, leaving it unchanged. ' - 'Use the `set…ToNull` methods to write SQL NULL explicitly.', - ); + if (insertType != null) { + _writeValues( + buffer, + table, + insertType, + memberNames, + bindings, + requireRequiredColumns: true, + docLine: + 'Values for inserting a row into `${table.name}`. Columns that are ' + 'nullable, identity, or covered by a database default are optional; ' + 'passing `null` omits the column so the database default applies. ' + 'Columns the database always generates itself are left out ' + 'entirely. Use the `set…ToNull` methods to insert SQL NULL ' + 'explicitly.', + ); + } + if (updateType != null) { + _writeValues( + buffer, + table, + updateType, + memberNames, + bindings, + requireRequiredColumns: false, + docLine: + 'Values for updating rows of `${table.name}`. All columns are ' + 'optional; passing `null` omits the column, leaving it unchanged. ' + 'Use the `set…ToNull` methods to write SQL NULL explicitly.', + ); + } _writeNamespace(buffer, table, namespaceType, rowType, memberNames, bindings); } diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index 136c60a51..762074055 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -60,6 +60,17 @@ ColumnTypeKind _elementTypeKind(String elementFormat, {required bool isEnum}) { /// (`supabase gen types --lang json`), into a [SchemaDescription] for /// [schemaName]. /// +/// The document carries a `version` field, currently 1, and the +/// semantically sorted collections produced by `sortGeneratorMetadata`: +/// `tables`, `foreignTables`, `views`, `materializedViews`, `columns`, +/// `primaryKeys`, `relationships`, `functions` and `types`. Collections and +/// fields the generator does not need, such as `primaryKeys`, are ignored. +/// +/// Tables and foreign tables are always insertable and updatable. Views use +/// the `is_insert_enabled` and `is_update_enabled` flags, falling back to +/// `is_updatable` for documents that predate the flags. Materialized views +/// are never writable. +/// /// Throws a [FormatException] when the document does not have the /// `GeneratorMetadata` shape. SchemaDescription parseGeneratorMetadata( @@ -75,9 +86,29 @@ SchemaDescription parseGeneratorMetadata( } final relations = [ - for (final key in ['tables', 'foreignTables', 'views', 'materializedViews']) - ...?(document[key] as List?)?.cast>(), - ].where((relation) => relation['schema'] == schemaName); + for (final table in _relationsOf(document, 'tables', schemaName)) + (relation: table, isInsertable: true, isUpdatable: true), + for (final foreignTable in _relationsOf( + document, + 'foreignTables', + schemaName, + )) + (relation: foreignTable, isInsertable: true, isUpdatable: true), + for (final view in _relationsOf(document, 'views', schemaName)) + ( + relation: view, + isInsertable: + (view['is_insert_enabled'] ?? view['is_updatable']) as bool, + isUpdatable: + (view['is_update_enabled'] ?? view['is_updatable']) as bool, + ), + for (final materializedView in _relationsOf( + document, + 'materializedViews', + schemaName, + )) + (relation: materializedView, isInsertable: false, isUpdatable: false), + ]; final columnsByRelationId = >>{}; for (final column @@ -101,7 +132,7 @@ SchemaDescription parseGeneratorMetadata( final tables = []; final enumsByQualifiedName = {}; - for (final relation in relations) { + for (final (:relation, :isInsertable, :isUpdatable) in relations) { final relationName = relation['name'] as String; final columns = []; @@ -145,7 +176,8 @@ SchemaDescription parseGeneratorMetadata( isNullable: isNullable, isReadOnly: column['identity_generation'] == 'ALWAYS' || - column['is_generated'] as bool, + column['is_generated'] as bool || + !(column['is_updatable'] as bool), comment: column['comment'] as String?, foreignKey: foreignKeysByColumn[(relationName, name)], ), @@ -157,6 +189,8 @@ SchemaDescription parseGeneratorMetadata( name: relationName, comment: relation['comment'] as String?, columns: columns, + isInsertable: isInsertable, + isUpdatable: isUpdatable, ), ); } @@ -172,6 +206,16 @@ SchemaDescription parseGeneratorMetadata( ); } +/// The relations of one document collection, such as `views`, that belong to +/// [schemaName]. +Iterable> _relationsOf( + Map document, + String collection, + String schemaName, +) => (document[collection] as List? ?? const []) + .cast>() + .where((relation) => relation['schema'] == schemaName); + /// Maps `(table, column)` pairs of [schemaName] to their foreign key targets, /// pairing the source and referenced columns of each relationship by index. Map<(String, String), ForeignKeyDescription> _foreignKeysByColumn( diff --git a/packages/supabase_typegen/lib/src/schema_description.dart b/packages/supabase_typegen/lib/src/schema_description.dart index d8a3b5a77..16f3b75a8 100644 --- a/packages/supabase_typegen/lib/src/schema_description.dart +++ b/packages/supabase_typegen/lib/src/schema_description.dart @@ -67,6 +67,8 @@ class TableDescription { required this.name, required this.columns, this.comment, + this.isInsertable = true, + this.isUpdatable = true, }); /// Name of the table in the database. @@ -77,6 +79,17 @@ class TableDescription { /// Columns of the table, in database order. final List columns; + + /// Whether rows can be inserted through the relation. Tables and foreign + /// tables always are; views only when the database reports that INSERT + /// works through them; materialized views never are. Relations that are + /// not insertable get no insert value type in the generated code. + final bool isInsertable; + + /// Whether rows can be updated through the relation, with the same rules + /// as [isInsertable]. Relations that are not updatable get no update value + /// type in the generated code. + final bool isUpdatable; } /// Description of a single table column. @@ -128,8 +141,10 @@ class ColumnDescription { final bool isNullable; /// Whether the column can never be written, because it is a - /// `GENERATED ALWAYS` identity or a generated column. Read-only columns - /// appear in the row type but not in the insert and update value types. + /// `GENERATED ALWAYS` identity, a generated column, or a column the + /// database reports as not updatable, such as a computed column of an + /// otherwise writable view. Read-only columns appear in the row type but + /// not in the insert and update value types. final bool isReadOnly; } diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index 7a9573780..282931f89 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -76,6 +76,52 @@ void main() { expect(code, contains("TableColumn('id')")); }); + test('read-only views generate no insert or update surface', () { + final code = generateDartCode(schema); + + expect(code, contains('extension type const AuthorStatsRow')); + expect(code, isNot(contains('AuthorStatsInsert'))); + expect(code, isNot(contains('AuthorStatsUpdate'))); + }); + + test('insert-only and update-only relations generate a single value ' + 'type', () { + ColumnDescription titleColumn() => const ColumnDescription( + name: 'title', + postgresFormat: 'text', + typeKind: ColumnTypeKind.text, + isRequired: false, + hasDefault: false, + isNullable: true, + ); + final code = generateDartCode( + SchemaDescription( + schemaName: 'public', + tables: [ + TableDescription( + name: 'book_submissions', + columns: [titleColumn()], + isInsertable: true, + isUpdatable: false, + ), + TableDescription( + name: 'book_corrections', + columns: [titleColumn()], + isInsertable: false, + isUpdatable: true, + ), + ], + enums: [], + ), + ); + + expect(code, contains('BookSubmissionsInsert({String? title})')); + expect(code, isNot(contains('BookSubmissionsUpdate'))); + + expect(code, contains('BookCorrectionsUpdate({String? title})')); + expect(code, isNot(contains('BookCorrectionsInsert'))); + }); + test('tables whose columns are all read-only get parameterless ' 'insert and update constructors', () { final table = TableDescription( diff --git a/packages/supabase_typegen/test/generator_metadata_parser_test.dart b/packages/supabase_typegen/test/generator_metadata_parser_test.dart index 324d475c3..c9c7f7599 100644 --- a/packages/supabase_typegen/test/generator_metadata_parser_test.dart +++ b/packages/supabase_typegen/test/generator_metadata_parser_test.dart @@ -40,6 +40,20 @@ void main() { ]); }); + test('tables are insertable and updatable', () { + final books = schema.tables.singleWhere((table) => table.name == 'books'); + expect(books.isInsertable, isTrue); + expect(books.isUpdatable, isTrue); + }); + + test('read-only views are neither insertable nor updatable', () { + final authorStats = schema.tables.singleWhere( + (table) => table.name == 'author_stats', + ); + expect(authorStats.isInsertable, isFalse); + expect(authorStats.isUpdatable, isFalse); + }); + test('parses table and view comments', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); expect(books.comment, 'Books available in the library'); @@ -211,4 +225,112 @@ void main() { ]); expect(authorStats.columns.first.isNullable, isTrue); }); + + test('foreign tables are insertable and updatable', () { + final parsed = parseGeneratorMetadata({ + 'version': 1, + 'tables': [], + 'foreignTables': [ + {'id': 1, 'schema': 'public', 'name': 'remote_logs', 'comment': null}, + ], + 'columns': [_column(tableId: 1, table: 'remote_logs', name: 'message')], + }); + + final remoteLogs = parsed.tables.single; + expect(remoteLogs.name, 'remote_logs'); + expect(remoteLogs.isInsertable, isTrue); + expect(remoteLogs.isUpdatable, isTrue); + }); + + group('view writability flags', () { + SchemaDescription parseView(Map view) => + parseGeneratorMetadata({ + 'version': 1, + 'tables': [], + 'views': [view], + 'columns': [ + _column(tableId: 1, table: view['name'] as String, name: 'title'), + ], + }); + + test('is_insert_enabled alone makes a view insert-only', () { + final parsed = parseView({ + 'id': 1, + 'schema': 'public', + 'name': 'book_submissions', + 'is_updatable': false, + 'is_insert_enabled': true, + 'is_update_enabled': false, + 'comment': null, + }); + + expect(parsed.tables.single.isInsertable, isTrue); + expect(parsed.tables.single.isUpdatable, isFalse); + }); + + test('is_update_enabled alone makes a view update-only', () { + final parsed = parseView({ + 'id': 1, + 'schema': 'public', + 'name': 'book_corrections', + 'is_updatable': false, + 'is_insert_enabled': false, + 'is_update_enabled': true, + 'comment': null, + }); + + expect(parsed.tables.single.isInsertable, isFalse); + expect(parsed.tables.single.isUpdatable, isTrue); + }); + + test('absent flags fall back to is_updatable', () { + for (final isUpdatable in [true, false]) { + final parsed = parseView({ + 'id': 1, + 'schema': 'public', + 'name': 'book_prices', + 'is_updatable': isUpdatable, + 'comment': null, + }); + + expect( + parsed.tables.single.isInsertable, + isUpdatable, + reason: 'is_updatable: $isUpdatable', + ); + expect( + parsed.tables.single.isUpdatable, + isUpdatable, + reason: 'is_updatable: $isUpdatable', + ); + } + }); + }); } + +/// A minimal column document of the GeneratorMetadata contract. +Map _column({ + required int tableId, + required String table, + required String name, +}) => { + 'table_id': tableId, + 'schema': 'public', + 'table': table, + 'id': '$tableId.1', + 'ordinal_position': 1, + 'name': name, + 'default_value': null, + 'data_type': 'text', + 'format': 'text', + 'type_schema': 'pg_catalog', + 'is_identity': false, + 'identity_generation': null, + 'is_generated': false, + 'is_nullable': true, + 'is_updatable': true, + 'is_unique': false, + 'enums': [], + 'check': null, + 'comment': null, +}; diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index de46fcc15..0ed7f1558 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -40,46 +40,6 @@ extension type const AuthorStatsRow(Map _json) int? get bookCount => _json['book_count'] as int?; } -/// Values for inserting a row into `author_stats`. Columns that are nullable, -/// identity, or covered by a database default are optional; passing `null` -/// omits the column so the database default applies. Columns the database -/// always generates itself are left out entirely. Use the `set…ToNull` methods -/// to insert SQL NULL explicitly. -extension type const AuthorStatsInsert._(Map _json) - implements Map { - AuthorStatsInsert({int? authorId, int? bookCount}) - : this._({'author_id': ?authorId, 'book_count': ?bookCount}); - - /// Returns a copy with `author_id` set to SQL NULL, overriding any database - /// default. - AuthorStatsInsert setAuthorIdToNull() => - AuthorStatsInsert._({..._json, 'author_id': null}); - - /// Returns a copy with `book_count` set to SQL NULL, overriding any database - /// default. - AuthorStatsInsert setBookCountToNull() => - AuthorStatsInsert._({..._json, 'book_count': null}); -} - -/// Values for updating rows of `author_stats`. All columns are optional; -/// passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` -/// methods to write SQL NULL explicitly. -extension type const AuthorStatsUpdate._(Map _json) - implements Map { - AuthorStatsUpdate({int? authorId, int? bookCount}) - : this._({'author_id': ?authorId, 'book_count': ?bookCount}); - - /// Returns a copy with `author_id` set to SQL NULL, overriding any database - /// default. - AuthorStatsUpdate setAuthorIdToNull() => - AuthorStatsUpdate._({..._json, 'author_id': null}); - - /// Returns a copy with `book_count` set to SQL NULL, overriding any database - /// default. - AuthorStatsUpdate setBookCountToNull() => - AuthorStatsUpdate._({..._json, 'book_count': null}); -} - /// Typed access to the `author_stats` table. class AuthorStats { const AuthorStats._(); From dd6dca789f2379dd0403d461bcc1a8f35b68606d Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 31 Aug 2026 19:50:10 +0200 Subject: [PATCH 18/31] test(supabase_typegen): regenerate the metadata fixture from a real postgrest-typegen introspection The fixture is now produced by seeding a disposable Postgres 15 with test/fixtures/seed.sql and running tool/regenerate_fixture.ts, which introspects with the released @supabase/postgrest-typegen 0.2.0 and applies its sortGeneratorMetadata ordering pass, exactly like postgres-meta 0.99.0 does. The document now carries the version field, primaryKeys, type_schema on columns, the full cross-schema types list, and semantically sorted collections. The seed adds an automatically updatable view with a computed column, a materialized view, and a join view that is insertable only through an INSTEAD OF INSERT trigger. --- packages/supabase_typegen/README.md | 3 +- .../test/dart_generator_test.dart | 27 +- .../test/fixtures/generator_metadata.json | 6822 ++++++++++++++++- .../supabase_typegen/test/fixtures/seed.sql | 90 + .../test/generator_metadata_parser_test.dart | 45 +- .../test/goldens/supabase_schema.dart | 111 + .../tool/regenerate_fixture.ts | 33 + 7 files changed, 6962 insertions(+), 169 deletions(-) create mode 100644 packages/supabase_typegen/test/fixtures/seed.sql create mode 100644 packages/supabase_typegen/tool/regenerate_fixture.ts diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index a88b2a6cc..baac35f2f 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -38,7 +38,8 @@ 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. +from serializing that package's `introspect()` result, ordered with its +`sortGeneratorMetadata` pass. ## Committing schema.json diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index 282931f89..6f805c929 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -76,12 +76,37 @@ void main() { expect(code, contains("TableColumn('id')")); }); - test('read-only views generate no insert or update surface', () { + test('read-only views and materialized views generate no insert or ' + 'update surface', () { final code = generateDartCode(schema); expect(code, contains('extension type const AuthorStatsRow')); expect(code, isNot(contains('AuthorStatsInsert'))); expect(code, isNot(contains('AuthorStatsUpdate'))); + + expect(code, contains('extension type const BookSummariesRow')); + expect(code, isNot(contains('BookSummariesInsert'))); + expect(code, isNot(contains('BookSummariesUpdate'))); + + expect(code, contains('extension type const BookSubmissionsRow')); + expect(code, isNot(contains('BookSubmissionsInsert'))); + expect(code, isNot(contains('BookSubmissionsUpdate'))); + }); + + test('non-updatable view columns read but are excluded from insert and ' + 'update', () { + final code = generateDartCode(schema); + + expect(code, contains('num? get discountedPrice')); + expect(code, contains("TableColumn('discounted_price')")); + expect( + code, + contains('BookPricesInsert({int? id, String? title, num? price})'), + ); + expect( + code, + contains('BookPricesUpdate({int? id, String? title, num? price})'), + ); }); test('insert-only and update-only relations generate a single value ' diff --git a/packages/supabase_typegen/test/fixtures/generator_metadata.json b/packages/supabase_typegen/test/fixtures/generator_metadata.json index e4286148c..6421e28d5 100644 --- a/packages/supabase_typegen/test/fixtures/generator_metadata.json +++ b/packages/supabase_typegen/test/fixtures/generator_metadata.json @@ -1,416 +1,660 @@ { + "version": 1, "schemas": [ { "id": 2200, "name": "public", - "owner": "postgres" + "owner": "pg_database_owner" } ], "tables": [ { - "id": 16385, + "id": 16392, "schema": "public", - "name": "books", + "name": "authors", "rls_enabled": true, "rls_forced": false, "replica_identity": "DEFAULT", - "bytes": 8192, - "size": "8192 bytes", + "bytes": 16384, + "size": "16 kB", "live_rows_estimate": 0, "dead_rows_estimate": 0, - "comment": "Books available in the library" + "comment": null }, { - "id": 16401, + "id": 16400, "schema": "public", - "name": "authors", + "name": "books", "rls_enabled": true, "rls_forced": false, "replica_identity": "DEFAULT", - "bytes": 8192, - "size": "8192 bytes", + "bytes": 16384, + "size": "16 kB", "live_rows_estimate": 0, "dead_rows_estimate": 0, - "comment": null + "comment": "Books available in the library" } ], "foreignTables": [], "views": [ { - "id": 16420, + "id": 16414, "schema": "public", "name": "author_stats", "is_updatable": false, "comment": "Aggregated statistics per author" + }, + { + "id": 16418, + "schema": "public", + "name": "book_prices", + "is_updatable": true, + "comment": "Prices per book, with the standard discount precomputed" + }, + { + "id": 16428, + "schema": "public", + "name": "book_submissions", + "is_updatable": false, + "comment": null + } + ], + "materializedViews": [ + { + "id": 16422, + "schema": "public", + "name": "book_summaries", + "is_populated": true, + "comment": "Denormalized book and author names" } ], - "materializedViews": [], "columns": [ { - "table_id": 16385, + "table_id": 16414, "schema": "public", - "table": "books", - "id": "16385.1", + "table": "author_stats", + "id": "16414.1", + "ordinal_position": 1, + "name": "author_id", + "default_value": null, + "data_type": "bigint", + "format": "int8", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": false, + "is_unique": false, + "check": null, + "enums": [], + "comment": null + }, + { + "table_id": 16414, + "schema": "public", + "table": "author_stats", + "id": "16414.2", + "ordinal_position": 2, + "name": "book_count", + "default_value": null, + "data_type": "bigint", + "format": "int8", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": false, + "is_unique": false, + "check": null, + "enums": [], + "comment": null + }, + { + "table_id": 16392, + "schema": "public", + "table": "authors", + "id": "16392.1", "ordinal_position": 1, "name": "id", "default_value": null, "data_type": "bigint", "format": "int8", + "type_schema": "pg_catalog", "is_identity": true, - "identity_generation": "BY DEFAULT", + "identity_generation": "ALWAYS", "is_generated": false, "is_nullable": false, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16392, "schema": "public", - "table": "books", - "id": "16385.2", + "table": "authors", + "id": "16392.2", "ordinal_position": 2, - "name": "title", + "name": "name", "default_value": null, "data_type": "text", "format": "text", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": false, "is_updatable": true, "is_unique": false, + "check": null, "enums": [], + "comment": null + }, + { + "table_id": 16418, + "schema": "public", + "table": "book_prices", + "id": "16418.4", + "ordinal_position": 4, + "name": "discounted_price", + "default_value": null, + "data_type": "numeric", + "format": "numeric", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": false, + "is_unique": false, "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16418, "schema": "public", - "table": "books", - "id": "16385.3", - "ordinal_position": 3, - "name": "author_id", + "table": "book_prices", + "id": "16418.1", + "ordinal_position": 1, + "name": "id", "default_value": null, "data_type": "bigint", "format": "int8", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, - "is_nullable": false, + "is_nullable": true, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16418, "schema": "public", - "table": "books", - "id": "16385.4", - "ordinal_position": 4, + "table": "book_prices", + "id": "16418.3", + "ordinal_position": 3, "name": "price", "default_value": null, "data_type": "numeric", "format": "numeric", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16418, "schema": "public", - "table": "books", - "id": "16385.5", - "ordinal_position": 5, - "name": "rating", + "table": "book_prices", + "id": "16418.2", + "ordinal_position": 2, + "name": "title", "default_value": null, - "data_type": "double precision", - "format": "float8", + "data_type": "text", + "format": "text", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16428, "schema": "public", - "table": "books", - "id": "16385.6", - "ordinal_position": 6, - "name": "in_print", - "default_value": "true", - "data_type": "boolean", - "format": "bool", + "table": "book_submissions", + "id": "16428.2", + "ordinal_position": 2, + "name": "author_name", + "default_value": null, + "data_type": "text", + "format": "text", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, - "is_nullable": false, - "is_updatable": true, + "is_nullable": true, + "is_updatable": false, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16428, "schema": "public", - "table": "books", - "id": "16385.7", - "ordinal_position": 7, - "name": "mood", + "table": "book_submissions", + "id": "16428.1", + "ordinal_position": 1, + "name": "title", "default_value": null, - "data_type": "USER-DEFINED", - "format": "mood", + "data_type": "text", + "format": "text", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, - "is_updatable": true, + "is_updatable": false, "is_unique": false, - "enums": [ - "happy", - "very happy", - "sad" - ], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16422, "schema": "public", - "table": "books", - "id": "16385.8", - "ordinal_position": 8, - "name": "tags", + "table": "book_summaries", + "id": "16422.3", + "ordinal_position": 3, + "name": "author_name", "default_value": null, - "data_type": "ARRAY", - "format": "_text", + "data_type": "text", + "format": "text", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, - "is_updatable": true, + "is_updatable": false, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16422, "schema": "public", - "table": "books", - "id": "16385.9", - "ordinal_position": 9, - "name": "page_counts", + "table": "book_summaries", + "id": "16422.1", + "ordinal_position": 1, + "name": "id", "default_value": null, - "data_type": "ARRAY", - "format": "_int4", + "data_type": "bigint", + "format": "int8", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, - "is_updatable": true, + "is_updatable": false, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16422, "schema": "public", - "table": "books", - "id": "16385.10", - "ordinal_position": 10, - "name": "metadata", + "table": "book_summaries", + "id": "16422.2", + "ordinal_position": 2, + "name": "title", "default_value": null, - "data_type": "jsonb", - "format": "jsonb", + "data_type": "text", + "format": "text", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, - "is_updatable": true, + "is_updatable": false, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16400, "schema": "public", "table": "books", - "id": "16385.11", - "ordinal_position": 11, - "name": "cover_uuid", + "id": "16400.3", + "ordinal_position": 3, + "name": "author_id", "default_value": null, - "data_type": "uuid", - "format": "uuid", + "data_type": "bigint", + "format": "int8", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, - "is_nullable": true, + "is_nullable": false, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16400, "schema": "public", "table": "books", - "id": "16385.12", - "ordinal_position": 12, - "name": "published_on", + "id": "16400.11", + "ordinal_position": 11, + "name": "cover_uuid", "default_value": null, - "data_type": "date", - "format": "date", + "data_type": "uuid", + "format": "uuid", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16385, + "table_id": 16400, "schema": "public", "table": "books", - "id": "16385.13", + "id": "16400.13", "ordinal_position": 13, "name": "created_at", "default_value": "now()", "data_type": "timestamp with time zone", "format": "timestamptz", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": false, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": "When the row was created" }, { - "table_id": 16385, + "table_id": 16400, "schema": "public", "table": "books", - "id": "16385.14", - "ordinal_position": 14, - "name": "updated_at", - "default_value": null, - "data_type": "timestamp without time zone", - "format": "timestamp", - "is_identity": false, - "identity_generation": null, - "is_generated": false, - "is_nullable": true, - "is_updatable": true, - "is_unique": false, - "enums": [], - "check": null, - "comment": null - }, - { - "table_id": 16401, - "schema": "public", - "table": "authors", - "id": "16401.1", + "id": "16400.1", "ordinal_position": 1, "name": "id", "default_value": null, "data_type": "bigint", "format": "int8", + "type_schema": "pg_catalog", "is_identity": true, - "identity_generation": "ALWAYS", + "identity_generation": "BY DEFAULT", "is_generated": false, "is_nullable": false, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16401, + "table_id": 16400, "schema": "public", - "table": "authors", - "id": "16401.2", - "ordinal_position": 2, - "name": "name", - "default_value": null, - "data_type": "text", - "format": "text", + "table": "books", + "id": "16400.6", + "ordinal_position": 6, + "name": "in_print", + "default_value": "true", + "data_type": "boolean", + "format": "bool", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": false, "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16420, + "table_id": 16400, "schema": "public", - "table": "author_stats", - "id": "16420.1", - "ordinal_position": 1, - "name": "author_id", + "table": "books", + "id": "16400.10", + "ordinal_position": 10, + "name": "metadata", "default_value": null, - "data_type": "bigint", - "format": "int8", + "data_type": "jsonb", + "format": "jsonb", + "type_schema": "pg_catalog", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, - "is_updatable": false, + "is_updatable": true, "is_unique": false, - "enums": [], "check": null, + "enums": [], "comment": null }, { - "table_id": 16420, + "table_id": 16400, "schema": "public", - "table": "author_stats", - "id": "16420.2", - "ordinal_position": 2, - "name": "book_count", + "table": "books", + "id": "16400.7", + "ordinal_position": 7, + "name": "mood", "default_value": null, - "data_type": "bigint", - "format": "int8", + "data_type": "USER-DEFINED", + "format": "mood", + "type_schema": "public", "is_identity": false, "identity_generation": null, "is_generated": false, "is_nullable": true, - "is_updatable": false, + "is_updatable": true, + "is_unique": false, + "check": null, + "enums": [ + "happy", + "very happy", + "sad" + ], + "comment": null + }, + { + "table_id": 16400, + "schema": "public", + "table": "books", + "id": "16400.9", + "ordinal_position": 9, + "name": "page_counts", + "default_value": null, + "data_type": "ARRAY", + "format": "_int4", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "check": null, + "enums": [], + "comment": null + }, + { + "table_id": 16400, + "schema": "public", + "table": "books", + "id": "16400.4", + "ordinal_position": 4, + "name": "price", + "default_value": null, + "data_type": "numeric", + "format": "numeric", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "check": null, + "enums": [], + "comment": null + }, + { + "table_id": 16400, + "schema": "public", + "table": "books", + "id": "16400.12", + "ordinal_position": 12, + "name": "published_on", + "default_value": null, + "data_type": "date", + "format": "date", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "check": null, + "enums": [], + "comment": null + }, + { + "table_id": 16400, + "schema": "public", + "table": "books", + "id": "16400.5", + "ordinal_position": 5, + "name": "rating", + "default_value": null, + "data_type": "double precision", + "format": "float8", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "check": null, + "enums": [], + "comment": null + }, + { + "table_id": 16400, + "schema": "public", + "table": "books", + "id": "16400.8", + "ordinal_position": 8, + "name": "tags", + "default_value": null, + "data_type": "ARRAY", + "format": "_text", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, + "check": null, + "enums": [], + "comment": null + }, + { + "table_id": 16400, + "schema": "public", + "table": "books", + "id": "16400.2", + "ordinal_position": 2, + "name": "title", + "default_value": null, + "data_type": "text", + "format": "text", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": false, + "is_updatable": true, "is_unique": false, + "check": null, "enums": [], + "comment": null + }, + { + "table_id": 16400, + "schema": "public", + "table": "books", + "id": "16400.14", + "ordinal_position": 14, + "name": "updated_at", + "default_value": null, + "data_type": "timestamp without time zone", + "format": "timestamp", + "type_schema": "pg_catalog", + "is_identity": false, + "identity_generation": null, + "is_generated": false, + "is_nullable": true, + "is_updatable": true, + "is_unique": false, "check": null, + "enums": [], "comment": null } ], + "primaryKeys": [ + { + "table_id": 16392, + "schema": "public", + "table_name": "authors", + "name": "id" + }, + { + "table_id": 16400, + "schema": "public", + "table_name": "books", + "name": "id" + } + ], "relationships": [ { "foreign_key_name": "books_author_id_fkey", @@ -419,6 +663,20 @@ "columns": [ "author_id" ], + "referenced_schema": "public", + "referenced_relation": "authors", + "referenced_columns": [ + "id" + ], + "is_one_to_one": false + }, + { + "foreign_key_name": "books_author_id_fkey", + "schema": "public", + "relation": "author_stats", + "columns": [ + "author_id" + ], "is_one_to_one": false, "referenced_schema": "public", "referenced_relation": "authors", @@ -430,7 +688,6247 @@ "functions": [], "types": [ { - "id": 16390, + "id": 13574, + "name": "__pg_foreign_data_wrappers", + "schema": "information_schema", + "format": "information_schema._pg_foreign_data_wrappers[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13586, + "name": "__pg_foreign_servers", + "schema": "information_schema", + "format": "information_schema._pg_foreign_servers[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13565, + "name": "__pg_foreign_table_columns", + "schema": "information_schema", + "format": "information_schema._pg_foreign_table_columns[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13599, + "name": "__pg_foreign_tables", + "schema": "information_schema", + "format": "information_schema._pg_foreign_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13612, + "name": "__pg_user_mappings", + "schema": "information_schema", + "format": "information_schema._pg_user_mappings[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13308, + "name": "_administrable_role_authorizations", + "schema": "information_schema", + "format": "information_schema.administrable_role_authorizations[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13303, + "name": "_applicable_roles", + "schema": "information_schema", + "format": "information_schema.applicable_roles[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13312, + "name": "_attributes", + "schema": "information_schema", + "format": "information_schema.attributes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13286, + "name": "_cardinal_number", + "schema": "information_schema", + "format": "information_schema.cardinal_number[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13289, + "name": "_character_data", + "schema": "information_schema", + "format": "information_schema.character_data[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13317, + "name": "_character_sets", + "schema": "information_schema", + "format": "information_schema.character_sets[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13322, + "name": "_check_constraint_routine_usage", + "schema": "information_schema", + "format": "information_schema.check_constraint_routine_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13327, + "name": "_check_constraints", + "schema": "information_schema", + "format": "information_schema.check_constraints[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13337, + "name": "_collation_character_set_applicability", + "schema": "information_schema", + "format": "information_schema.collation_character_set_applicability[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13332, + "name": "_collations", + "schema": "information_schema", + "format": "information_schema.collations[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13342, + "name": "_column_column_usage", + "schema": "information_schema", + "format": "information_schema.column_column_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13347, + "name": "_column_domain_usage", + "schema": "information_schema", + "format": "information_schema.column_domain_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13570, + "name": "_column_options", + "schema": "information_schema", + "format": "information_schema.column_options[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13352, + "name": "_column_privileges", + "schema": "information_schema", + "format": "information_schema.column_privileges[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13357, + "name": "_column_udt_usage", + "schema": "information_schema", + "format": "information_schema.column_udt_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13362, + "name": "_columns", + "schema": "information_schema", + "format": "information_schema.columns[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13367, + "name": "_constraint_column_usage", + "schema": "information_schema", + "format": "information_schema.constraint_column_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13372, + "name": "_constraint_table_usage", + "schema": "information_schema", + "format": "information_schema.constraint_table_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13555, + "name": "_data_type_privileges", + "schema": "information_schema", + "format": "information_schema.data_type_privileges[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13377, + "name": "_domain_constraints", + "schema": "information_schema", + "format": "information_schema.domain_constraints[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13382, + "name": "_domain_udt_usage", + "schema": "information_schema", + "format": "information_schema.domain_udt_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13387, + "name": "_domains", + "schema": "information_schema", + "format": "information_schema.domains[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13560, + "name": "_element_types", + "schema": "information_schema", + "format": "information_schema.element_types[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13392, + "name": "_enabled_roles", + "schema": "information_schema", + "format": "information_schema.enabled_roles[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13578, + "name": "_foreign_data_wrapper_options", + "schema": "information_schema", + "format": "information_schema.foreign_data_wrapper_options[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13582, + "name": "_foreign_data_wrappers", + "schema": "information_schema", + "format": "information_schema.foreign_data_wrappers[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13591, + "name": "_foreign_server_options", + "schema": "information_schema", + "format": "information_schema.foreign_server_options[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13595, + "name": "_foreign_servers", + "schema": "information_schema", + "format": "information_schema.foreign_servers[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13604, + "name": "_foreign_table_options", + "schema": "information_schema", + "format": "information_schema.foreign_table_options[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13608, + "name": "_foreign_tables", + "schema": "information_schema", + "format": "information_schema.foreign_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13294, + "name": "_information_schema_catalog_name", + "schema": "information_schema", + "format": "information_schema.information_schema_catalog_name[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13396, + "name": "_key_column_usage", + "schema": "information_schema", + "format": "information_schema.key_column_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13401, + "name": "_parameters", + "schema": "information_schema", + "format": "information_schema.parameters[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13575, + "name": "_pg_foreign_data_wrappers", + "schema": "information_schema", + "format": "information_schema._pg_foreign_data_wrappers", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13573 + }, + { + "id": 13587, + "name": "_pg_foreign_servers", + "schema": "information_schema", + "format": "information_schema._pg_foreign_servers", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13585 + }, + { + "id": 13566, + "name": "_pg_foreign_table_columns", + "schema": "information_schema", + "format": "information_schema._pg_foreign_table_columns", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13564 + }, + { + "id": 13600, + "name": "_pg_foreign_tables", + "schema": "information_schema", + "format": "information_schema._pg_foreign_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13598 + }, + { + "id": 13613, + "name": "_pg_user_mappings", + "schema": "information_schema", + "format": "information_schema._pg_user_mappings", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13611 + }, + { + "id": 13406, + "name": "_referential_constraints", + "schema": "information_schema", + "format": "information_schema.referential_constraints[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13411, + "name": "_role_column_grants", + "schema": "information_schema", + "format": "information_schema.role_column_grants[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13425, + "name": "_role_routine_grants", + "schema": "information_schema", + "format": "information_schema.role_routine_grants[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13488, + "name": "_role_table_grants", + "schema": "information_schema", + "format": "information_schema.role_table_grants[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13517, + "name": "_role_udt_grants", + "schema": "information_schema", + "format": "information_schema.role_udt_grants[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13526, + "name": "_role_usage_grants", + "schema": "information_schema", + "format": "information_schema.role_usage_grants[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13415, + "name": "_routine_column_usage", + "schema": "information_schema", + "format": "information_schema.routine_column_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13420, + "name": "_routine_privileges", + "schema": "information_schema", + "format": "information_schema.routine_privileges[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13429, + "name": "_routine_routine_usage", + "schema": "information_schema", + "format": "information_schema.routine_routine_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13434, + "name": "_routine_sequence_usage", + "schema": "information_schema", + "format": "information_schema.routine_sequence_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13439, + "name": "_routine_table_usage", + "schema": "information_schema", + "format": "information_schema.routine_table_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13444, + "name": "_routines", + "schema": "information_schema", + "format": "information_schema.routines[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13449, + "name": "_schemata", + "schema": "information_schema", + "format": "information_schema.schemata[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13453, + "name": "_sequences", + "schema": "information_schema", + "format": "information_schema.sequences[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13458, + "name": "_sql_features", + "schema": "information_schema", + "format": "information_schema.sql_features[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13291, + "name": "_sql_identifier", + "schema": "information_schema", + "format": "information_schema.sql_identifier[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13463, + "name": "_sql_implementation_info", + "schema": "information_schema", + "format": "information_schema.sql_implementation_info[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13468, + "name": "_sql_parts", + "schema": "information_schema", + "format": "information_schema.sql_parts[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13473, + "name": "_sql_sizing", + "schema": "information_schema", + "format": "information_schema.sql_sizing[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13478, + "name": "_table_constraints", + "schema": "information_schema", + "format": "information_schema.table_constraints[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13483, + "name": "_table_privileges", + "schema": "information_schema", + "format": "information_schema.table_privileges[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13492, + "name": "_tables", + "schema": "information_schema", + "format": "information_schema.tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13297, + "name": "_time_stamp", + "schema": "information_schema", + "format": "information_schema.time_stamp[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13497, + "name": "_transforms", + "schema": "information_schema", + "format": "information_schema.transforms[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13502, + "name": "_triggered_update_columns", + "schema": "information_schema", + "format": "information_schema.triggered_update_columns[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13507, + "name": "_triggers", + "schema": "information_schema", + "format": "information_schema.triggers[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13512, + "name": "_udt_privileges", + "schema": "information_schema", + "format": "information_schema.udt_privileges[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13521, + "name": "_usage_privileges", + "schema": "information_schema", + "format": "information_schema.usage_privileges[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13530, + "name": "_user_defined_types", + "schema": "information_schema", + "format": "information_schema.user_defined_types[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13617, + "name": "_user_mapping_options", + "schema": "information_schema", + "format": "information_schema.user_mapping_options[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13622, + "name": "_user_mappings", + "schema": "information_schema", + "format": "information_schema.user_mappings[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13535, + "name": "_view_column_usage", + "schema": "information_schema", + "format": "information_schema.view_column_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13540, + "name": "_view_routine_usage", + "schema": "information_schema", + "format": "information_schema.view_routine_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13545, + "name": "_view_table_usage", + "schema": "information_schema", + "format": "information_schema.view_table_usage[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13550, + "name": "_views", + "schema": "information_schema", + "format": "information_schema.views[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13299, + "name": "_yes_or_no", + "schema": "information_schema", + "format": "information_schema.yes_or_no[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13309, + "name": "administrable_role_authorizations", + "schema": "information_schema", + "format": "information_schema.administrable_role_authorizations", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13307 + }, + { + "id": 13304, + "name": "applicable_roles", + "schema": "information_schema", + "format": "information_schema.applicable_roles", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13302 + }, + { + "id": 13313, + "name": "attributes", + "schema": "information_schema", + "format": "information_schema.attributes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13311 + }, + { + "id": 13287, + "name": "cardinal_number", + "schema": "information_schema", + "format": "information_schema.cardinal_number", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13290, + "name": "character_data", + "schema": "information_schema", + "format": "information_schema.character_data", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13318, + "name": "character_sets", + "schema": "information_schema", + "format": "information_schema.character_sets", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13316 + }, + { + "id": 13323, + "name": "check_constraint_routine_usage", + "schema": "information_schema", + "format": "information_schema.check_constraint_routine_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13321 + }, + { + "id": 13328, + "name": "check_constraints", + "schema": "information_schema", + "format": "information_schema.check_constraints", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13326 + }, + { + "id": 13338, + "name": "collation_character_set_applicability", + "schema": "information_schema", + "format": "information_schema.collation_character_set_applicability", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13336 + }, + { + "id": 13333, + "name": "collations", + "schema": "information_schema", + "format": "information_schema.collations", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13331 + }, + { + "id": 13343, + "name": "column_column_usage", + "schema": "information_schema", + "format": "information_schema.column_column_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13341 + }, + { + "id": 13348, + "name": "column_domain_usage", + "schema": "information_schema", + "format": "information_schema.column_domain_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13346 + }, + { + "id": 13571, + "name": "column_options", + "schema": "information_schema", + "format": "information_schema.column_options", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13569 + }, + { + "id": 13353, + "name": "column_privileges", + "schema": "information_schema", + "format": "information_schema.column_privileges", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13351 + }, + { + "id": 13358, + "name": "column_udt_usage", + "schema": "information_schema", + "format": "information_schema.column_udt_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13356 + }, + { + "id": 13363, + "name": "columns", + "schema": "information_schema", + "format": "information_schema.columns", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13361 + }, + { + "id": 13368, + "name": "constraint_column_usage", + "schema": "information_schema", + "format": "information_schema.constraint_column_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13366 + }, + { + "id": 13373, + "name": "constraint_table_usage", + "schema": "information_schema", + "format": "information_schema.constraint_table_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13371 + }, + { + "id": 13556, + "name": "data_type_privileges", + "schema": "information_schema", + "format": "information_schema.data_type_privileges", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13554 + }, + { + "id": 13378, + "name": "domain_constraints", + "schema": "information_schema", + "format": "information_schema.domain_constraints", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13376 + }, + { + "id": 13383, + "name": "domain_udt_usage", + "schema": "information_schema", + "format": "information_schema.domain_udt_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13381 + }, + { + "id": 13388, + "name": "domains", + "schema": "information_schema", + "format": "information_schema.domains", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13386 + }, + { + "id": 13561, + "name": "element_types", + "schema": "information_schema", + "format": "information_schema.element_types", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13559 + }, + { + "id": 13393, + "name": "enabled_roles", + "schema": "information_schema", + "format": "information_schema.enabled_roles", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13391 + }, + { + "id": 13579, + "name": "foreign_data_wrapper_options", + "schema": "information_schema", + "format": "information_schema.foreign_data_wrapper_options", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13577 + }, + { + "id": 13583, + "name": "foreign_data_wrappers", + "schema": "information_schema", + "format": "information_schema.foreign_data_wrappers", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13581 + }, + { + "id": 13592, + "name": "foreign_server_options", + "schema": "information_schema", + "format": "information_schema.foreign_server_options", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13590 + }, + { + "id": 13596, + "name": "foreign_servers", + "schema": "information_schema", + "format": "information_schema.foreign_servers", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13594 + }, + { + "id": 13605, + "name": "foreign_table_options", + "schema": "information_schema", + "format": "information_schema.foreign_table_options", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13603 + }, + { + "id": 13609, + "name": "foreign_tables", + "schema": "information_schema", + "format": "information_schema.foreign_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13607 + }, + { + "id": 13295, + "name": "information_schema_catalog_name", + "schema": "information_schema", + "format": "information_schema.information_schema_catalog_name", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13293 + }, + { + "id": 13397, + "name": "key_column_usage", + "schema": "information_schema", + "format": "information_schema.key_column_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13395 + }, + { + "id": 13402, + "name": "parameters", + "schema": "information_schema", + "format": "information_schema.parameters", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13400 + }, + { + "id": 13407, + "name": "referential_constraints", + "schema": "information_schema", + "format": "information_schema.referential_constraints", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13405 + }, + { + "id": 13412, + "name": "role_column_grants", + "schema": "information_schema", + "format": "information_schema.role_column_grants", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13410 + }, + { + "id": 13426, + "name": "role_routine_grants", + "schema": "information_schema", + "format": "information_schema.role_routine_grants", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13424 + }, + { + "id": 13489, + "name": "role_table_grants", + "schema": "information_schema", + "format": "information_schema.role_table_grants", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13487 + }, + { + "id": 13518, + "name": "role_udt_grants", + "schema": "information_schema", + "format": "information_schema.role_udt_grants", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13516 + }, + { + "id": 13527, + "name": "role_usage_grants", + "schema": "information_schema", + "format": "information_schema.role_usage_grants", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13525 + }, + { + "id": 13416, + "name": "routine_column_usage", + "schema": "information_schema", + "format": "information_schema.routine_column_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13414 + }, + { + "id": 13421, + "name": "routine_privileges", + "schema": "information_schema", + "format": "information_schema.routine_privileges", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13419 + }, + { + "id": 13430, + "name": "routine_routine_usage", + "schema": "information_schema", + "format": "information_schema.routine_routine_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13428 + }, + { + "id": 13435, + "name": "routine_sequence_usage", + "schema": "information_schema", + "format": "information_schema.routine_sequence_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13433 + }, + { + "id": 13440, + "name": "routine_table_usage", + "schema": "information_schema", + "format": "information_schema.routine_table_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13438 + }, + { + "id": 13445, + "name": "routines", + "schema": "information_schema", + "format": "information_schema.routines", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13443 + }, + { + "id": 13450, + "name": "schemata", + "schema": "information_schema", + "format": "information_schema.schemata", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13448 + }, + { + "id": 13454, + "name": "sequences", + "schema": "information_schema", + "format": "information_schema.sequences", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13452 + }, + { + "id": 13459, + "name": "sql_features", + "schema": "information_schema", + "format": "information_schema.sql_features", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13457 + }, + { + "id": 13292, + "name": "sql_identifier", + "schema": "information_schema", + "format": "information_schema.sql_identifier", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13464, + "name": "sql_implementation_info", + "schema": "information_schema", + "format": "information_schema.sql_implementation_info", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13462 + }, + { + "id": 13469, + "name": "sql_parts", + "schema": "information_schema", + "format": "information_schema.sql_parts", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13467 + }, + { + "id": 13474, + "name": "sql_sizing", + "schema": "information_schema", + "format": "information_schema.sql_sizing", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13472 + }, + { + "id": 13479, + "name": "table_constraints", + "schema": "information_schema", + "format": "information_schema.table_constraints", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13477 + }, + { + "id": 13484, + "name": "table_privileges", + "schema": "information_schema", + "format": "information_schema.table_privileges", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13482 + }, + { + "id": 13493, + "name": "tables", + "schema": "information_schema", + "format": "information_schema.tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13491 + }, + { + "id": 13298, + "name": "time_stamp", + "schema": "information_schema", + "format": "information_schema.time_stamp", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 13498, + "name": "transforms", + "schema": "information_schema", + "format": "information_schema.transforms", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13496 + }, + { + "id": 13503, + "name": "triggered_update_columns", + "schema": "information_schema", + "format": "information_schema.triggered_update_columns", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13501 + }, + { + "id": 13508, + "name": "triggers", + "schema": "information_schema", + "format": "information_schema.triggers", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13506 + }, + { + "id": 13513, + "name": "udt_privileges", + "schema": "information_schema", + "format": "information_schema.udt_privileges", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13511 + }, + { + "id": 13522, + "name": "usage_privileges", + "schema": "information_schema", + "format": "information_schema.usage_privileges", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13520 + }, + { + "id": 13531, + "name": "user_defined_types", + "schema": "information_schema", + "format": "information_schema.user_defined_types", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13529 + }, + { + "id": 13618, + "name": "user_mapping_options", + "schema": "information_schema", + "format": "information_schema.user_mapping_options", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13616 + }, + { + "id": 13623, + "name": "user_mappings", + "schema": "information_schema", + "format": "information_schema.user_mappings", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13621 + }, + { + "id": 13536, + "name": "view_column_usage", + "schema": "information_schema", + "format": "information_schema.view_column_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13534 + }, + { + "id": 13541, + "name": "view_routine_usage", + "schema": "information_schema", + "format": "information_schema.view_routine_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13539 + }, + { + "id": 13546, + "name": "view_table_usage", + "schema": "information_schema", + "format": "information_schema.view_table_usage", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13544 + }, + { + "id": 13551, + "name": "views", + "schema": "information_schema", + "format": "information_schema.views", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 13549 + }, + { + "id": 13300, + "name": "yes_or_no", + "schema": "information_schema", + "format": "information_schema.yes_or_no", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1034, + "name": "_aclitem", + "schema": "pg_catalog", + "format": "aclitem[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1561, + "name": "_bit", + "schema": "pg_catalog", + "format": "bit[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1000, + "name": "_bool", + "schema": "pg_catalog", + "format": "boolean[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1020, + "name": "_box", + "schema": "pg_catalog", + "format": "box[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1014, + "name": "_bpchar", + "schema": "pg_catalog", + "format": "character[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1001, + "name": "_bytea", + "schema": "pg_catalog", + "format": "bytea[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1002, + "name": "_char", + "schema": "pg_catalog", + "format": "\"char\"[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1012, + "name": "_cid", + "schema": "pg_catalog", + "format": "cid[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 651, + "name": "_cidr", + "schema": "pg_catalog", + "format": "cidr[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 719, + "name": "_circle", + "schema": "pg_catalog", + "format": "circle[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1263, + "name": "_cstring", + "schema": "pg_catalog", + "format": "cstring[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1182, + "name": "_date", + "schema": "pg_catalog", + "format": "date[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 6155, + "name": "_datemultirange", + "schema": "pg_catalog", + "format": "datemultirange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3913, + "name": "_daterange", + "schema": "pg_catalog", + "format": "daterange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1021, + "name": "_float4", + "schema": "pg_catalog", + "format": "real[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1022, + "name": "_float8", + "schema": "pg_catalog", + "format": "double precision[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3644, + "name": "_gtsvector", + "schema": "pg_catalog", + "format": "gtsvector[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1041, + "name": "_inet", + "schema": "pg_catalog", + "format": "inet[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1005, + "name": "_int2", + "schema": "pg_catalog", + "format": "smallint[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1006, + "name": "_int2vector", + "schema": "pg_catalog", + "format": "int2vector[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1007, + "name": "_int4", + "schema": "pg_catalog", + "format": "integer[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 6150, + "name": "_int4multirange", + "schema": "pg_catalog", + "format": "int4multirange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3905, + "name": "_int4range", + "schema": "pg_catalog", + "format": "int4range[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1016, + "name": "_int8", + "schema": "pg_catalog", + "format": "bigint[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 6157, + "name": "_int8multirange", + "schema": "pg_catalog", + "format": "int8multirange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3927, + "name": "_int8range", + "schema": "pg_catalog", + "format": "int8range[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1187, + "name": "_interval", + "schema": "pg_catalog", + "format": "interval[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 199, + "name": "_json", + "schema": "pg_catalog", + "format": "json[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3807, + "name": "_jsonb", + "schema": "pg_catalog", + "format": "jsonb[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 4073, + "name": "_jsonpath", + "schema": "pg_catalog", + "format": "jsonpath[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 629, + "name": "_line", + "schema": "pg_catalog", + "format": "line[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1018, + "name": "_lseg", + "schema": "pg_catalog", + "format": "lseg[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1040, + "name": "_macaddr", + "schema": "pg_catalog", + "format": "macaddr[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 775, + "name": "_macaddr8", + "schema": "pg_catalog", + "format": "macaddr8[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 791, + "name": "_money", + "schema": "pg_catalog", + "format": "money[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1003, + "name": "_name", + "schema": "pg_catalog", + "format": "name[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1231, + "name": "_numeric", + "schema": "pg_catalog", + "format": "numeric[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 6151, + "name": "_nummultirange", + "schema": "pg_catalog", + "format": "nummultirange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3907, + "name": "_numrange", + "schema": "pg_catalog", + "format": "numrange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1028, + "name": "_oid", + "schema": "pg_catalog", + "format": "oid[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1013, + "name": "_oidvector", + "schema": "pg_catalog", + "format": "oidvector[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1019, + "name": "_path", + "schema": "pg_catalog", + "format": "path[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10026, + "name": "_pg_aggregate", + "schema": "pg_catalog", + "format": "pg_aggregate[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10014, + "name": "_pg_am", + "schema": "pg_catalog", + "format": "pg_am[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10016, + "name": "_pg_amop", + "schema": "pg_catalog", + "format": "pg_amop[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10018, + "name": "_pg_amproc", + "schema": "pg_catalog", + "format": "pg_amproc[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10000, + "name": "_pg_attrdef", + "schema": "pg_catalog", + "format": "pg_attrdef[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 270, + "name": "_pg_attribute", + "schema": "pg_catalog", + "format": "pg_attribute[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10058, + "name": "_pg_auth_members", + "schema": "pg_catalog", + "format": "pg_auth_members[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10057, + "name": "_pg_authid", + "schema": "pg_catalog", + "format": "pg_authid[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12086, + "name": "_pg_available_extension_versions", + "schema": "pg_catalog", + "format": "pg_available_extension_versions[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12082, + "name": "_pg_available_extensions", + "schema": "pg_catalog", + "format": "pg_available_extensions[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12139, + "name": "_pg_backend_memory_contexts", + "schema": "pg_catalog", + "format": "pg_backend_memory_contexts[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10042, + "name": "_pg_cast", + "schema": "pg_catalog", + "format": "pg_cast[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 273, + "name": "_pg_class", + "schema": "pg_catalog", + "format": "pg_class[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10094, + "name": "_pg_collation", + "schema": "pg_catalog", + "format": "pg_collation[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12131, + "name": "_pg_config", + "schema": "pg_catalog", + "format": "pg_config[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10002, + "name": "_pg_constraint", + "schema": "pg_catalog", + "format": "pg_constraint[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10048, + "name": "_pg_conversion", + "schema": "pg_catalog", + "format": "pg_conversion[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12078, + "name": "_pg_cursors", + "schema": "pg_catalog", + "format": "pg_cursors[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10052, + "name": "_pg_database", + "schema": "pg_catalog", + "format": "pg_database[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10053, + "name": "_pg_db_role_setting", + "schema": "pg_catalog", + "format": "pg_db_role_setting[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10087, + "name": "_pg_default_acl", + "schema": "pg_catalog", + "format": "pg_default_acl[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10050, + "name": "_pg_depend", + "schema": "pg_catalog", + "format": "pg_depend[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10040, + "name": "_pg_description", + "schema": "pg_catalog", + "format": "pg_description[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10044, + "name": "_pg_enum", + "schema": "pg_catalog", + "format": "pg_enum[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10038, + "name": "_pg_event_trigger", + "schema": "pg_catalog", + "format": "pg_event_trigger[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10073, + "name": "_pg_extension", + "schema": "pg_catalog", + "format": "pg_extension[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12111, + "name": "_pg_file_settings", + "schema": "pg_catalog", + "format": "pg_file_settings[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10075, + "name": "_pg_foreign_data_wrapper", + "schema": "pg_catalog", + "format": "pg_foreign_data_wrapper[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10077, + "name": "_pg_foreign_server", + "schema": "pg_catalog", + "format": "pg_foreign_server[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10081, + "name": "_pg_foreign_table", + "schema": "pg_catalog", + "format": "pg_foreign_table[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12011, + "name": "_pg_group", + "schema": "pg_catalog", + "format": "pg_group[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12115, + "name": "_pg_hba_file_rules", + "schema": "pg_catalog", + "format": "pg_hba_file_rules[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12119, + "name": "_pg_ident_file_mappings", + "schema": "pg_catalog", + "format": "pg_ident_file_mappings[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10006, + "name": "_pg_index", + "schema": "pg_catalog", + "format": "pg_index[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12044, + "name": "_pg_indexes", + "schema": "pg_catalog", + "format": "pg_indexes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10004, + "name": "_pg_inherits", + "schema": "pg_catalog", + "format": "pg_inherits[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10089, + "name": "_pg_init_privs", + "schema": "pg_catalog", + "format": "pg_init_privs[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10020, + "name": "_pg_language", + "schema": "pg_catalog", + "format": "pg_language[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10024, + "name": "_pg_largeobject", + "schema": "pg_catalog", + "format": "pg_largeobject[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10022, + "name": "_pg_largeobject_metadata", + "schema": "pg_catalog", + "format": "pg_largeobject_metadata[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12074, + "name": "_pg_locks", + "schema": "pg_catalog", + "format": "pg_locks[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3221, + "name": "_pg_lsn", + "schema": "pg_catalog", + "format": "pg_lsn[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12039, + "name": "_pg_matviews", + "schema": "pg_catalog", + "format": "pg_matviews[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10046, + "name": "_pg_namespace", + "schema": "pg_catalog", + "format": "pg_namespace[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10012, + "name": "_pg_opclass", + "schema": "pg_catalog", + "format": "pg_opclass[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10008, + "name": "_pg_operator", + "schema": "pg_catalog", + "format": "pg_operator[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10010, + "name": "_pg_opfamily", + "schema": "pg_catalog", + "format": "pg_opfamily[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10096, + "name": "_pg_parameter_acl", + "schema": "pg_catalog", + "format": "pg_parameter_acl[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10098, + "name": "_pg_partitioned_table", + "schema": "pg_catalog", + "format": "pg_partitioned_table[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12019, + "name": "_pg_policies", + "schema": "pg_catalog", + "format": "pg_policies[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10083, + "name": "_pg_policy", + "schema": "pg_catalog", + "format": "pg_policy[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12096, + "name": "_pg_prepared_statements", + "schema": "pg_catalog", + "format": "pg_prepared_statements[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12091, + "name": "_pg_prepared_xacts", + "schema": "pg_catalog", + "format": "pg_prepared_xacts[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 272, + "name": "_pg_proc", + "schema": "pg_catalog", + "format": "pg_proc[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10106, + "name": "_pg_publication", + "schema": "pg_catalog", + "format": "pg_publication[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10108, + "name": "_pg_publication_namespace", + "schema": "pg_catalog", + "format": "pg_publication_namespace[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10110, + "name": "_pg_publication_rel", + "schema": "pg_catalog", + "format": "pg_publication_rel[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12069, + "name": "_pg_publication_tables", + "schema": "pg_catalog", + "format": "pg_publication_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10100, + "name": "_pg_range", + "schema": "pg_catalog", + "format": "pg_range[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10085, + "name": "_pg_replication_origin", + "schema": "pg_catalog", + "format": "pg_replication_origin[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12333, + "name": "_pg_replication_origin_status", + "schema": "pg_catalog", + "format": "pg_replication_origin_status[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12258, + "name": "_pg_replication_slots", + "schema": "pg_catalog", + "format": "pg_replication_slots[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10034, + "name": "_pg_rewrite", + "schema": "pg_catalog", + "format": "pg_rewrite[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12001, + "name": "_pg_roles", + "schema": "pg_catalog", + "format": "pg_roles[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12024, + "name": "_pg_rules", + "schema": "pg_catalog", + "format": "pg_rules[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10091, + "name": "_pg_seclabel", + "schema": "pg_catalog", + "format": "pg_seclabel[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12100, + "name": "_pg_seclabels", + "schema": "pg_catalog", + "format": "pg_seclabels[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10104, + "name": "_pg_sequence", + "schema": "pg_catalog", + "format": "pg_sequence[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12049, + "name": "_pg_sequences", + "schema": "pg_catalog", + "format": "pg_sequences[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12105, + "name": "_pg_settings", + "schema": "pg_catalog", + "format": "pg_settings[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12006, + "name": "_pg_shadow", + "schema": "pg_catalog", + "format": "pg_shadow[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10059, + "name": "_pg_shdepend", + "schema": "pg_catalog", + "format": "pg_shdepend[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10061, + "name": "_pg_shdescription", + "schema": "pg_catalog", + "format": "pg_shdescription[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12135, + "name": "_pg_shmem_allocations", + "schema": "pg_catalog", + "format": "pg_shmem_allocations[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10093, + "name": "_pg_shseclabel", + "schema": "pg_catalog", + "format": "pg_shseclabel[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 5039, + "name": "_pg_snapshot", + "schema": "pg_catalog", + "format": "pg_snapshot[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12223, + "name": "_pg_stat_activity", + "schema": "pg_catalog", + "format": "pg_stat_activity[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12184, + "name": "_pg_stat_all_indexes", + "schema": "pg_catalog", + "format": "pg_stat_all_indexes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12143, + "name": "_pg_stat_all_tables", + "schema": "pg_catalog", + "format": "pg_stat_all_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12286, + "name": "_pg_stat_archiver", + "schema": "pg_catalog", + "format": "pg_stat_archiver[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12290, + "name": "_pg_stat_bgwriter", + "schema": "pg_catalog", + "format": "pg_stat_bgwriter[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12267, + "name": "_pg_stat_database", + "schema": "pg_catalog", + "format": "pg_stat_database[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12272, + "name": "_pg_stat_database_conflicts", + "schema": "pg_catalog", + "format": "pg_stat_database_conflicts[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12254, + "name": "_pg_stat_gssapi", + "schema": "pg_catalog", + "format": "pg_stat_gssapi[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12298, + "name": "_pg_stat_progress_analyze", + "schema": "pg_catalog", + "format": "pg_stat_progress_analyze[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12318, + "name": "_pg_stat_progress_basebackup", + "schema": "pg_catalog", + "format": "pg_stat_progress_basebackup[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12308, + "name": "_pg_stat_progress_cluster", + "schema": "pg_catalog", + "format": "pg_stat_progress_cluster[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12323, + "name": "_pg_stat_progress_copy", + "schema": "pg_catalog", + "format": "pg_stat_progress_copy[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12313, + "name": "_pg_stat_progress_create_index", + "schema": "pg_catalog", + "format": "pg_stat_progress_create_index[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12303, + "name": "_pg_stat_progress_vacuum", + "schema": "pg_catalog", + "format": "pg_stat_progress_vacuum[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12241, + "name": "_pg_stat_recovery_prefetch", + "schema": "pg_catalog", + "format": "pg_stat_recovery_prefetch[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12228, + "name": "_pg_stat_replication", + "schema": "pg_catalog", + "format": "pg_stat_replication[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12263, + "name": "_pg_stat_replication_slots", + "schema": "pg_catalog", + "format": "pg_stat_replication_slots[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12233, + "name": "_pg_stat_slru", + "schema": "pg_catalog", + "format": "pg_stat_slru[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12250, + "name": "_pg_stat_ssl", + "schema": "pg_catalog", + "format": "pg_stat_ssl[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12245, + "name": "_pg_stat_subscription", + "schema": "pg_catalog", + "format": "pg_stat_subscription[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12337, + "name": "_pg_stat_subscription_stats", + "schema": "pg_catalog", + "format": "pg_stat_subscription_stats[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12189, + "name": "_pg_stat_sys_indexes", + "schema": "pg_catalog", + "format": "pg_stat_sys_indexes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12153, + "name": "_pg_stat_sys_tables", + "schema": "pg_catalog", + "format": "pg_stat_sys_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12276, + "name": "_pg_stat_user_functions", + "schema": "pg_catalog", + "format": "pg_stat_user_functions[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12193, + "name": "_pg_stat_user_indexes", + "schema": "pg_catalog", + "format": "pg_stat_user_indexes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12162, + "name": "_pg_stat_user_tables", + "schema": "pg_catalog", + "format": "pg_stat_user_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12294, + "name": "_pg_stat_wal", + "schema": "pg_catalog", + "format": "pg_stat_wal[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12237, + "name": "_pg_stat_wal_receiver", + "schema": "pg_catalog", + "format": "pg_stat_wal_receiver[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12148, + "name": "_pg_stat_xact_all_tables", + "schema": "pg_catalog", + "format": "pg_stat_xact_all_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12158, + "name": "_pg_stat_xact_sys_tables", + "schema": "pg_catalog", + "format": "pg_stat_xact_sys_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12281, + "name": "_pg_stat_xact_user_functions", + "schema": "pg_catalog", + "format": "pg_stat_xact_user_functions[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12167, + "name": "_pg_stat_xact_user_tables", + "schema": "pg_catalog", + "format": "pg_stat_xact_user_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12197, + "name": "_pg_statio_all_indexes", + "schema": "pg_catalog", + "format": "pg_statio_all_indexes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12210, + "name": "_pg_statio_all_sequences", + "schema": "pg_catalog", + "format": "pg_statio_all_sequences[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12171, + "name": "_pg_statio_all_tables", + "schema": "pg_catalog", + "format": "pg_statio_all_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12202, + "name": "_pg_statio_sys_indexes", + "schema": "pg_catalog", + "format": "pg_statio_sys_indexes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12215, + "name": "_pg_statio_sys_sequences", + "schema": "pg_catalog", + "format": "pg_statio_sys_sequences[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12176, + "name": "_pg_statio_sys_tables", + "schema": "pg_catalog", + "format": "pg_statio_sys_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12206, + "name": "_pg_statio_user_indexes", + "schema": "pg_catalog", + "format": "pg_statio_user_indexes[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12219, + "name": "_pg_statio_user_sequences", + "schema": "pg_catalog", + "format": "pg_statio_user_sequences[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12180, + "name": "_pg_statio_user_tables", + "schema": "pg_catalog", + "format": "pg_statio_user_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10028, + "name": "_pg_statistic", + "schema": "pg_catalog", + "format": "pg_statistic[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10030, + "name": "_pg_statistic_ext", + "schema": "pg_catalog", + "format": "pg_statistic_ext[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10032, + "name": "_pg_statistic_ext_data", + "schema": "pg_catalog", + "format": "pg_statistic_ext_data[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12054, + "name": "_pg_stats", + "schema": "pg_catalog", + "format": "pg_stats[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12059, + "name": "_pg_stats_ext", + "schema": "pg_catalog", + "format": "pg_stats_ext[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12064, + "name": "_pg_stats_ext_exprs", + "schema": "pg_catalog", + "format": "pg_stats_ext_exprs[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10112, + "name": "_pg_subscription", + "schema": "pg_catalog", + "format": "pg_subscription[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10113, + "name": "_pg_subscription_rel", + "schema": "pg_catalog", + "format": "pg_subscription_rel[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12034, + "name": "_pg_tables", + "schema": "pg_catalog", + "format": "pg_tables[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10055, + "name": "_pg_tablespace", + "schema": "pg_catalog", + "format": "pg_tablespace[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12123, + "name": "_pg_timezone_abbrevs", + "schema": "pg_catalog", + "format": "pg_timezone_abbrevs[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12127, + "name": "_pg_timezone_names", + "schema": "pg_catalog", + "format": "pg_timezone_names[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10102, + "name": "_pg_transform", + "schema": "pg_catalog", + "format": "pg_transform[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10036, + "name": "_pg_trigger", + "schema": "pg_catalog", + "format": "pg_trigger[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10063, + "name": "_pg_ts_config", + "schema": "pg_catalog", + "format": "pg_ts_config[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10065, + "name": "_pg_ts_config_map", + "schema": "pg_catalog", + "format": "pg_ts_config_map[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10067, + "name": "_pg_ts_dict", + "schema": "pg_catalog", + "format": "pg_ts_dict[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10069, + "name": "_pg_ts_parser", + "schema": "pg_catalog", + "format": "pg_ts_parser[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10071, + "name": "_pg_ts_template", + "schema": "pg_catalog", + "format": "pg_ts_template[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 210, + "name": "_pg_type", + "schema": "pg_catalog", + "format": "pg_type[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12015, + "name": "_pg_user", + "schema": "pg_catalog", + "format": "pg_user[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 10079, + "name": "_pg_user_mapping", + "schema": "pg_catalog", + "format": "pg_user_mapping[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12328, + "name": "_pg_user_mappings", + "schema": "pg_catalog", + "format": "pg_user_mappings[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 12029, + "name": "_pg_views", + "schema": "pg_catalog", + "format": "pg_views[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1017, + "name": "_point", + "schema": "pg_catalog", + "format": "point[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1027, + "name": "_polygon", + "schema": "pg_catalog", + "format": "polygon[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2287, + "name": "_record", + "schema": "pg_catalog", + "format": "record[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2201, + "name": "_refcursor", + "schema": "pg_catalog", + "format": "refcursor[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2210, + "name": "_regclass", + "schema": "pg_catalog", + "format": "regclass[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 4192, + "name": "_regcollation", + "schema": "pg_catalog", + "format": "regcollation[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3735, + "name": "_regconfig", + "schema": "pg_catalog", + "format": "regconfig[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3770, + "name": "_regdictionary", + "schema": "pg_catalog", + "format": "regdictionary[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 4090, + "name": "_regnamespace", + "schema": "pg_catalog", + "format": "regnamespace[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2208, + "name": "_regoper", + "schema": "pg_catalog", + "format": "regoper[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2209, + "name": "_regoperator", + "schema": "pg_catalog", + "format": "regoperator[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1008, + "name": "_regproc", + "schema": "pg_catalog", + "format": "regproc[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2207, + "name": "_regprocedure", + "schema": "pg_catalog", + "format": "regprocedure[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 4097, + "name": "_regrole", + "schema": "pg_catalog", + "format": "regrole[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2211, + "name": "_regtype", + "schema": "pg_catalog", + "format": "regtype[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1009, + "name": "_text", + "schema": "pg_catalog", + "format": "text[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1010, + "name": "_tid", + "schema": "pg_catalog", + "format": "tid[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1183, + "name": "_time", + "schema": "pg_catalog", + "format": "time without time zone[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1115, + "name": "_timestamp", + "schema": "pg_catalog", + "format": "timestamp without time zone[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1185, + "name": "_timestamptz", + "schema": "pg_catalog", + "format": "timestamp with time zone[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1270, + "name": "_timetz", + "schema": "pg_catalog", + "format": "time with time zone[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 6152, + "name": "_tsmultirange", + "schema": "pg_catalog", + "format": "tsmultirange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3645, + "name": "_tsquery", + "schema": "pg_catalog", + "format": "tsquery[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3909, + "name": "_tsrange", + "schema": "pg_catalog", + "format": "tsrange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 6153, + "name": "_tstzmultirange", + "schema": "pg_catalog", + "format": "tstzmultirange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3911, + "name": "_tstzrange", + "schema": "pg_catalog", + "format": "tstzrange[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 3643, + "name": "_tsvector", + "schema": "pg_catalog", + "format": "tsvector[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2949, + "name": "_txid_snapshot", + "schema": "pg_catalog", + "format": "txid_snapshot[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 2951, + "name": "_uuid", + "schema": "pg_catalog", + "format": "uuid[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1563, + "name": "_varbit", + "schema": "pg_catalog", + "format": "bit varying[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1015, + "name": "_varchar", + "schema": "pg_catalog", + "format": "character varying[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1011, + "name": "_xid", + "schema": "pg_catalog", + "format": "xid[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 271, + "name": "_xid8", + "schema": "pg_catalog", + "format": "xid8[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 143, + "name": "_xml", + "schema": "pg_catalog", + "format": "xml[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 1033, + "name": "aclitem", + "schema": "pg_catalog", + "format": "aclitem", + "enums": [], + "attributes": [], + "comment": "access control list", + "type_relation_id": null + }, + { + "id": 2276, + "name": "any", + "schema": "pg_catalog", + "format": "\"any\"", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing any type", + "type_relation_id": null + }, + { + "id": 2277, + "name": "anyarray", + "schema": "pg_catalog", + "format": "anyarray", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a polymorphic array type", + "type_relation_id": null + }, + { + "id": 5077, + "name": "anycompatible", + "schema": "pg_catalog", + "format": "anycompatible", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a polymorphic common type", + "type_relation_id": null + }, + { + "id": 5078, + "name": "anycompatiblearray", + "schema": "pg_catalog", + "format": "anycompatiblearray", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing an array of polymorphic common type elements", + "type_relation_id": null + }, + { + "id": 4538, + "name": "anycompatiblemultirange", + "schema": "pg_catalog", + "format": "anycompatiblemultirange", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a multirange over a polymorphic common type", + "type_relation_id": null + }, + { + "id": 5079, + "name": "anycompatiblenonarray", + "schema": "pg_catalog", + "format": "anycompatiblenonarray", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a polymorphic common type that is not an array", + "type_relation_id": null + }, + { + "id": 5080, + "name": "anycompatiblerange", + "schema": "pg_catalog", + "format": "anycompatiblerange", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a range over a polymorphic common type", + "type_relation_id": null + }, + { + "id": 2283, + "name": "anyelement", + "schema": "pg_catalog", + "format": "anyelement", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a polymorphic base type", + "type_relation_id": null + }, + { + "id": 3500, + "name": "anyenum", + "schema": "pg_catalog", + "format": "anyenum", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a polymorphic base type that is an enum", + "type_relation_id": null + }, + { + "id": 4537, + "name": "anymultirange", + "schema": "pg_catalog", + "format": "anymultirange", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a polymorphic base type that is a multirange", + "type_relation_id": null + }, + { + "id": 2776, + "name": "anynonarray", + "schema": "pg_catalog", + "format": "anynonarray", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a polymorphic base type that is not an array", + "type_relation_id": null + }, + { + "id": 3831, + "name": "anyrange", + "schema": "pg_catalog", + "format": "anyrange", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing a range over a polymorphic base type", + "type_relation_id": null + }, + { + "id": 1560, + "name": "bit", + "schema": "pg_catalog", + "format": "bit", + "enums": [], + "attributes": [], + "comment": "fixed-length bit string", + "type_relation_id": null + }, + { + "id": 16, + "name": "bool", + "schema": "pg_catalog", + "format": "boolean", + "enums": [], + "attributes": [], + "comment": "boolean, 'true'/'false'", + "type_relation_id": null + }, + { + "id": 603, + "name": "box", + "schema": "pg_catalog", + "format": "box", + "enums": [], + "attributes": [], + "comment": "geometric box '(lower left,upper right)'", + "type_relation_id": null + }, + { + "id": 1042, + "name": "bpchar", + "schema": "pg_catalog", + "format": "character", + "enums": [], + "attributes": [], + "comment": "char(length), blank-padded string, fixed storage length", + "type_relation_id": null + }, + { + "id": 17, + "name": "bytea", + "schema": "pg_catalog", + "format": "bytea", + "enums": [], + "attributes": [], + "comment": "variable-length string, binary values escaped", + "type_relation_id": null + }, + { + "id": 18, + "name": "char", + "schema": "pg_catalog", + "format": "\"char\"", + "enums": [], + "attributes": [], + "comment": "single character", + "type_relation_id": null + }, + { + "id": 29, + "name": "cid", + "schema": "pg_catalog", + "format": "cid", + "enums": [], + "attributes": [], + "comment": "command identifier type, sequence in transaction id", + "type_relation_id": null + }, + { + "id": 650, + "name": "cidr", + "schema": "pg_catalog", + "format": "cidr", + "enums": [], + "attributes": [], + "comment": "network IP address/netmask, network address", + "type_relation_id": null + }, + { + "id": 718, + "name": "circle", + "schema": "pg_catalog", + "format": "circle", + "enums": [], + "attributes": [], + "comment": "geometric circle '(center,radius)'", + "type_relation_id": null + }, + { + "id": 2275, + "name": "cstring", + "schema": "pg_catalog", + "format": "cstring", + "enums": [], + "attributes": [], + "comment": "C-style string", + "type_relation_id": null + }, + { + "id": 1082, + "name": "date", + "schema": "pg_catalog", + "format": "date", + "enums": [], + "attributes": [], + "comment": "date", + "type_relation_id": null + }, + { + "id": 4535, + "name": "datemultirange", + "schema": "pg_catalog", + "format": "datemultirange", + "enums": [], + "attributes": [], + "comment": "multirange of dates", + "type_relation_id": null + }, + { + "id": 3912, + "name": "daterange", + "schema": "pg_catalog", + "format": "daterange", + "enums": [], + "attributes": [], + "comment": "range of dates", + "type_relation_id": null + }, + { + "id": 3838, + "name": "event_trigger", + "schema": "pg_catalog", + "format": "event_trigger", + "enums": [], + "attributes": [], + "comment": "pseudo-type for the result of an event trigger function", + "type_relation_id": null + }, + { + "id": 3115, + "name": "fdw_handler", + "schema": "pg_catalog", + "format": "fdw_handler", + "enums": [], + "attributes": [], + "comment": "pseudo-type for the result of an FDW handler function", + "type_relation_id": null + }, + { + "id": 700, + "name": "float4", + "schema": "pg_catalog", + "format": "real", + "enums": [], + "attributes": [], + "comment": "single-precision floating point number, 4-byte storage", + "type_relation_id": null + }, + { + "id": 701, + "name": "float8", + "schema": "pg_catalog", + "format": "double precision", + "enums": [], + "attributes": [], + "comment": "double-precision floating point number, 8-byte storage", + "type_relation_id": null + }, + { + "id": 3642, + "name": "gtsvector", + "schema": "pg_catalog", + "format": "gtsvector", + "enums": [], + "attributes": [], + "comment": "GiST index internal text representation for text search", + "type_relation_id": null + }, + { + "id": 325, + "name": "index_am_handler", + "schema": "pg_catalog", + "format": "index_am_handler", + "enums": [], + "attributes": [], + "comment": "pseudo-type for the result of an index AM handler function", + "type_relation_id": null + }, + { + "id": 869, + "name": "inet", + "schema": "pg_catalog", + "format": "inet", + "enums": [], + "attributes": [], + "comment": "IP address/netmask, host address, netmask optional", + "type_relation_id": null + }, + { + "id": 21, + "name": "int2", + "schema": "pg_catalog", + "format": "smallint", + "enums": [], + "attributes": [], + "comment": "-32 thousand to 32 thousand, 2-byte storage", + "type_relation_id": null + }, + { + "id": 22, + "name": "int2vector", + "schema": "pg_catalog", + "format": "int2vector", + "enums": [], + "attributes": [], + "comment": "array of int2, used in system tables", + "type_relation_id": null + }, + { + "id": 23, + "name": "int4", + "schema": "pg_catalog", + "format": "integer", + "enums": [], + "attributes": [], + "comment": "-2 billion to 2 billion integer, 4-byte storage", + "type_relation_id": null + }, + { + "id": 4451, + "name": "int4multirange", + "schema": "pg_catalog", + "format": "int4multirange", + "enums": [], + "attributes": [], + "comment": "multirange of integers", + "type_relation_id": null + }, + { + "id": 3904, + "name": "int4range", + "schema": "pg_catalog", + "format": "int4range", + "enums": [], + "attributes": [], + "comment": "range of integers", + "type_relation_id": null + }, + { + "id": 20, + "name": "int8", + "schema": "pg_catalog", + "format": "bigint", + "enums": [], + "attributes": [], + "comment": "~18 digit integer, 8-byte storage", + "type_relation_id": null + }, + { + "id": 4536, + "name": "int8multirange", + "schema": "pg_catalog", + "format": "int8multirange", + "enums": [], + "attributes": [], + "comment": "multirange of bigints", + "type_relation_id": null + }, + { + "id": 3926, + "name": "int8range", + "schema": "pg_catalog", + "format": "int8range", + "enums": [], + "attributes": [], + "comment": "range of bigints", + "type_relation_id": null + }, + { + "id": 2281, + "name": "internal", + "schema": "pg_catalog", + "format": "internal", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing an internal data structure", + "type_relation_id": null + }, + { + "id": 1186, + "name": "interval", + "schema": "pg_catalog", + "format": "interval", + "enums": [], + "attributes": [], + "comment": "@ , time interval", + "type_relation_id": null + }, + { + "id": 114, + "name": "json", + "schema": "pg_catalog", + "format": "json", + "enums": [], + "attributes": [], + "comment": "JSON stored as text", + "type_relation_id": null + }, + { + "id": 3802, + "name": "jsonb", + "schema": "pg_catalog", + "format": "jsonb", + "enums": [], + "attributes": [], + "comment": "Binary JSON", + "type_relation_id": null + }, + { + "id": 4072, + "name": "jsonpath", + "schema": "pg_catalog", + "format": "jsonpath", + "enums": [], + "attributes": [], + "comment": "JSON path", + "type_relation_id": null + }, + { + "id": 2280, + "name": "language_handler", + "schema": "pg_catalog", + "format": "language_handler", + "enums": [], + "attributes": [], + "comment": "pseudo-type for the result of a language handler function", + "type_relation_id": null + }, + { + "id": 628, + "name": "line", + "schema": "pg_catalog", + "format": "line", + "enums": [], + "attributes": [], + "comment": "geometric line", + "type_relation_id": null + }, + { + "id": 601, + "name": "lseg", + "schema": "pg_catalog", + "format": "lseg", + "enums": [], + "attributes": [], + "comment": "geometric line segment '(pt1,pt2)'", + "type_relation_id": null + }, + { + "id": 829, + "name": "macaddr", + "schema": "pg_catalog", + "format": "macaddr", + "enums": [], + "attributes": [], + "comment": "XX:XX:XX:XX:XX:XX, MAC address", + "type_relation_id": null + }, + { + "id": 774, + "name": "macaddr8", + "schema": "pg_catalog", + "format": "macaddr8", + "enums": [], + "attributes": [], + "comment": "XX:XX:XX:XX:XX:XX:XX:XX, MAC address", + "type_relation_id": null + }, + { + "id": 790, + "name": "money", + "schema": "pg_catalog", + "format": "money", + "enums": [], + "attributes": [], + "comment": "monetary amounts, $d,ddd.cc", + "type_relation_id": null + }, + { + "id": 19, + "name": "name", + "schema": "pg_catalog", + "format": "name", + "enums": [], + "attributes": [], + "comment": "63-byte type for storing system identifiers", + "type_relation_id": null + }, + { + "id": 1700, + "name": "numeric", + "schema": "pg_catalog", + "format": "numeric", + "enums": [], + "attributes": [], + "comment": "numeric(precision, decimal), arbitrary precision number", + "type_relation_id": null + }, + { + "id": 4532, + "name": "nummultirange", + "schema": "pg_catalog", + "format": "nummultirange", + "enums": [], + "attributes": [], + "comment": "multirange of numerics", + "type_relation_id": null + }, + { + "id": 3906, + "name": "numrange", + "schema": "pg_catalog", + "format": "numrange", + "enums": [], + "attributes": [], + "comment": "range of numerics", + "type_relation_id": null + }, + { + "id": 26, + "name": "oid", + "schema": "pg_catalog", + "format": "oid", + "enums": [], + "attributes": [], + "comment": "object identifier(oid), maximum 4 billion", + "type_relation_id": null + }, + { + "id": 30, + "name": "oidvector", + "schema": "pg_catalog", + "format": "oidvector", + "enums": [], + "attributes": [], + "comment": "array of oids, used in system tables", + "type_relation_id": null + }, + { + "id": 602, + "name": "path", + "schema": "pg_catalog", + "format": "path", + "enums": [], + "attributes": [], + "comment": "geometric path '(pt1,...)'", + "type_relation_id": null + }, + { + "id": 10027, + "name": "pg_aggregate", + "schema": "pg_catalog", + "format": "pg_aggregate", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2600 + }, + { + "id": 10015, + "name": "pg_am", + "schema": "pg_catalog", + "format": "pg_am", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2601 + }, + { + "id": 10017, + "name": "pg_amop", + "schema": "pg_catalog", + "format": "pg_amop", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2602 + }, + { + "id": 10019, + "name": "pg_amproc", + "schema": "pg_catalog", + "format": "pg_amproc", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2603 + }, + { + "id": 10001, + "name": "pg_attrdef", + "schema": "pg_catalog", + "format": "pg_attrdef", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2604 + }, + { + "id": 75, + "name": "pg_attribute", + "schema": "pg_catalog", + "format": "pg_attribute", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1249 + }, + { + "id": 2843, + "name": "pg_auth_members", + "schema": "pg_catalog", + "format": "pg_auth_members", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1261 + }, + { + "id": 2842, + "name": "pg_authid", + "schema": "pg_catalog", + "format": "pg_authid", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1260 + }, + { + "id": 12087, + "name": "pg_available_extension_versions", + "schema": "pg_catalog", + "format": "pg_available_extension_versions", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12085 + }, + { + "id": 12083, + "name": "pg_available_extensions", + "schema": "pg_catalog", + "format": "pg_available_extensions", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12081 + }, + { + "id": 12140, + "name": "pg_backend_memory_contexts", + "schema": "pg_catalog", + "format": "pg_backend_memory_contexts", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12138 + }, + { + "id": 4600, + "name": "pg_brin_bloom_summary", + "schema": "pg_catalog", + "format": "pg_brin_bloom_summary", + "enums": [], + "attributes": [], + "comment": "BRIN bloom summary", + "type_relation_id": null + }, + { + "id": 4601, + "name": "pg_brin_minmax_multi_summary", + "schema": "pg_catalog", + "format": "pg_brin_minmax_multi_summary", + "enums": [], + "attributes": [], + "comment": "BRIN minmax-multi summary", + "type_relation_id": null + }, + { + "id": 10043, + "name": "pg_cast", + "schema": "pg_catalog", + "format": "pg_cast", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2605 + }, + { + "id": 83, + "name": "pg_class", + "schema": "pg_catalog", + "format": "pg_class", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1259 + }, + { + "id": 10095, + "name": "pg_collation", + "schema": "pg_catalog", + "format": "pg_collation", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3456 + }, + { + "id": 12132, + "name": "pg_config", + "schema": "pg_catalog", + "format": "pg_config", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12130 + }, + { + "id": 10003, + "name": "pg_constraint", + "schema": "pg_catalog", + "format": "pg_constraint", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2606 + }, + { + "id": 10049, + "name": "pg_conversion", + "schema": "pg_catalog", + "format": "pg_conversion", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2607 + }, + { + "id": 12079, + "name": "pg_cursors", + "schema": "pg_catalog", + "format": "pg_cursors", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12077 + }, + { + "id": 1248, + "name": "pg_database", + "schema": "pg_catalog", + "format": "pg_database", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1262 + }, + { + "id": 10054, + "name": "pg_db_role_setting", + "schema": "pg_catalog", + "format": "pg_db_role_setting", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2964 + }, + { + "id": 32, + "name": "pg_ddl_command", + "schema": "pg_catalog", + "format": "pg_ddl_command", + "enums": [], + "attributes": [], + "comment": "internal type for passing CollectedCommand", + "type_relation_id": null + }, + { + "id": 10088, + "name": "pg_default_acl", + "schema": "pg_catalog", + "format": "pg_default_acl", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 826 + }, + { + "id": 10051, + "name": "pg_depend", + "schema": "pg_catalog", + "format": "pg_depend", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2608 + }, + { + "id": 3402, + "name": "pg_dependencies", + "schema": "pg_catalog", + "format": "pg_dependencies", + "enums": [], + "attributes": [], + "comment": "multivariate dependencies", + "type_relation_id": null + }, + { + "id": 10041, + "name": "pg_description", + "schema": "pg_catalog", + "format": "pg_description", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2609 + }, + { + "id": 10045, + "name": "pg_enum", + "schema": "pg_catalog", + "format": "pg_enum", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3501 + }, + { + "id": 10039, + "name": "pg_event_trigger", + "schema": "pg_catalog", + "format": "pg_event_trigger", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3466 + }, + { + "id": 10074, + "name": "pg_extension", + "schema": "pg_catalog", + "format": "pg_extension", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3079 + }, + { + "id": 12112, + "name": "pg_file_settings", + "schema": "pg_catalog", + "format": "pg_file_settings", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12110 + }, + { + "id": 10076, + "name": "pg_foreign_data_wrapper", + "schema": "pg_catalog", + "format": "pg_foreign_data_wrapper", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2328 + }, + { + "id": 10078, + "name": "pg_foreign_server", + "schema": "pg_catalog", + "format": "pg_foreign_server", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1417 + }, + { + "id": 10082, + "name": "pg_foreign_table", + "schema": "pg_catalog", + "format": "pg_foreign_table", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3118 + }, + { + "id": 12012, + "name": "pg_group", + "schema": "pg_catalog", + "format": "pg_group", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12010 + }, + { + "id": 12116, + "name": "pg_hba_file_rules", + "schema": "pg_catalog", + "format": "pg_hba_file_rules", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12114 + }, + { + "id": 12120, + "name": "pg_ident_file_mappings", + "schema": "pg_catalog", + "format": "pg_ident_file_mappings", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12118 + }, + { + "id": 10007, + "name": "pg_index", + "schema": "pg_catalog", + "format": "pg_index", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2610 + }, + { + "id": 12045, + "name": "pg_indexes", + "schema": "pg_catalog", + "format": "pg_indexes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12043 + }, + { + "id": 10005, + "name": "pg_inherits", + "schema": "pg_catalog", + "format": "pg_inherits", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2611 + }, + { + "id": 10090, + "name": "pg_init_privs", + "schema": "pg_catalog", + "format": "pg_init_privs", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3394 + }, + { + "id": 10021, + "name": "pg_language", + "schema": "pg_catalog", + "format": "pg_language", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2612 + }, + { + "id": 10025, + "name": "pg_largeobject", + "schema": "pg_catalog", + "format": "pg_largeobject", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2613 + }, + { + "id": 10023, + "name": "pg_largeobject_metadata", + "schema": "pg_catalog", + "format": "pg_largeobject_metadata", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2995 + }, + { + "id": 12075, + "name": "pg_locks", + "schema": "pg_catalog", + "format": "pg_locks", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12073 + }, + { + "id": 3220, + "name": "pg_lsn", + "schema": "pg_catalog", + "format": "pg_lsn", + "enums": [], + "attributes": [], + "comment": "PostgreSQL LSN datatype", + "type_relation_id": null + }, + { + "id": 12040, + "name": "pg_matviews", + "schema": "pg_catalog", + "format": "pg_matviews", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12038 + }, + { + "id": 5017, + "name": "pg_mcv_list", + "schema": "pg_catalog", + "format": "pg_mcv_list", + "enums": [], + "attributes": [], + "comment": "multivariate MCV list", + "type_relation_id": null + }, + { + "id": 10047, + "name": "pg_namespace", + "schema": "pg_catalog", + "format": "pg_namespace", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2615 + }, + { + "id": 3361, + "name": "pg_ndistinct", + "schema": "pg_catalog", + "format": "pg_ndistinct", + "enums": [], + "attributes": [], + "comment": "multivariate ndistinct coefficients", + "type_relation_id": null + }, + { + "id": 194, + "name": "pg_node_tree", + "schema": "pg_catalog", + "format": "pg_node_tree", + "enums": [], + "attributes": [], + "comment": "string representing an internal node tree", + "type_relation_id": null + }, + { + "id": 10013, + "name": "pg_opclass", + "schema": "pg_catalog", + "format": "pg_opclass", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2616 + }, + { + "id": 10009, + "name": "pg_operator", + "schema": "pg_catalog", + "format": "pg_operator", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2617 + }, + { + "id": 10011, + "name": "pg_opfamily", + "schema": "pg_catalog", + "format": "pg_opfamily", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2753 + }, + { + "id": 10097, + "name": "pg_parameter_acl", + "schema": "pg_catalog", + "format": "pg_parameter_acl", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 6243 + }, + { + "id": 10099, + "name": "pg_partitioned_table", + "schema": "pg_catalog", + "format": "pg_partitioned_table", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3350 + }, + { + "id": 12020, + "name": "pg_policies", + "schema": "pg_catalog", + "format": "pg_policies", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12018 + }, + { + "id": 10084, + "name": "pg_policy", + "schema": "pg_catalog", + "format": "pg_policy", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3256 + }, + { + "id": 12097, + "name": "pg_prepared_statements", + "schema": "pg_catalog", + "format": "pg_prepared_statements", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12095 + }, + { + "id": 12092, + "name": "pg_prepared_xacts", + "schema": "pg_catalog", + "format": "pg_prepared_xacts", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12090 + }, + { + "id": 81, + "name": "pg_proc", + "schema": "pg_catalog", + "format": "pg_proc", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1255 + }, + { + "id": 10107, + "name": "pg_publication", + "schema": "pg_catalog", + "format": "pg_publication", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 6104 + }, + { + "id": 10109, + "name": "pg_publication_namespace", + "schema": "pg_catalog", + "format": "pg_publication_namespace", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 6237 + }, + { + "id": 10111, + "name": "pg_publication_rel", + "schema": "pg_catalog", + "format": "pg_publication_rel", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 6106 + }, + { + "id": 12070, + "name": "pg_publication_tables", + "schema": "pg_catalog", + "format": "pg_publication_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12068 + }, + { + "id": 10101, + "name": "pg_range", + "schema": "pg_catalog", + "format": "pg_range", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3541 + }, + { + "id": 10086, + "name": "pg_replication_origin", + "schema": "pg_catalog", + "format": "pg_replication_origin", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 6000 + }, + { + "id": 12334, + "name": "pg_replication_origin_status", + "schema": "pg_catalog", + "format": "pg_replication_origin_status", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12332 + }, + { + "id": 12259, + "name": "pg_replication_slots", + "schema": "pg_catalog", + "format": "pg_replication_slots", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12257 + }, + { + "id": 10035, + "name": "pg_rewrite", + "schema": "pg_catalog", + "format": "pg_rewrite", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2618 + }, + { + "id": 12002, + "name": "pg_roles", + "schema": "pg_catalog", + "format": "pg_roles", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12000 + }, + { + "id": 12025, + "name": "pg_rules", + "schema": "pg_catalog", + "format": "pg_rules", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12023 + }, + { + "id": 10092, + "name": "pg_seclabel", + "schema": "pg_catalog", + "format": "pg_seclabel", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3596 + }, + { + "id": 12101, + "name": "pg_seclabels", + "schema": "pg_catalog", + "format": "pg_seclabels", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12099 + }, + { + "id": 10105, + "name": "pg_sequence", + "schema": "pg_catalog", + "format": "pg_sequence", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2224 + }, + { + "id": 12050, + "name": "pg_sequences", + "schema": "pg_catalog", + "format": "pg_sequences", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12048 + }, + { + "id": 12106, + "name": "pg_settings", + "schema": "pg_catalog", + "format": "pg_settings", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12104 + }, + { + "id": 12007, + "name": "pg_shadow", + "schema": "pg_catalog", + "format": "pg_shadow", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12005 + }, + { + "id": 10060, + "name": "pg_shdepend", + "schema": "pg_catalog", + "format": "pg_shdepend", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1214 + }, + { + "id": 10062, + "name": "pg_shdescription", + "schema": "pg_catalog", + "format": "pg_shdescription", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2396 + }, + { + "id": 12136, + "name": "pg_shmem_allocations", + "schema": "pg_catalog", + "format": "pg_shmem_allocations", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12134 + }, + { + "id": 4066, + "name": "pg_shseclabel", + "schema": "pg_catalog", + "format": "pg_shseclabel", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3592 + }, + { + "id": 5038, + "name": "pg_snapshot", + "schema": "pg_catalog", + "format": "pg_snapshot", + "enums": [], + "attributes": [], + "comment": "snapshot", + "type_relation_id": null + }, + { + "id": 12224, + "name": "pg_stat_activity", + "schema": "pg_catalog", + "format": "pg_stat_activity", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12222 + }, + { + "id": 12185, + "name": "pg_stat_all_indexes", + "schema": "pg_catalog", + "format": "pg_stat_all_indexes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12183 + }, + { + "id": 12144, + "name": "pg_stat_all_tables", + "schema": "pg_catalog", + "format": "pg_stat_all_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12142 + }, + { + "id": 12287, + "name": "pg_stat_archiver", + "schema": "pg_catalog", + "format": "pg_stat_archiver", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12285 + }, + { + "id": 12291, + "name": "pg_stat_bgwriter", + "schema": "pg_catalog", + "format": "pg_stat_bgwriter", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12289 + }, + { + "id": 12268, + "name": "pg_stat_database", + "schema": "pg_catalog", + "format": "pg_stat_database", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12266 + }, + { + "id": 12273, + "name": "pg_stat_database_conflicts", + "schema": "pg_catalog", + "format": "pg_stat_database_conflicts", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12271 + }, + { + "id": 12255, + "name": "pg_stat_gssapi", + "schema": "pg_catalog", + "format": "pg_stat_gssapi", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12253 + }, + { + "id": 12299, + "name": "pg_stat_progress_analyze", + "schema": "pg_catalog", + "format": "pg_stat_progress_analyze", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12297 + }, + { + "id": 12319, + "name": "pg_stat_progress_basebackup", + "schema": "pg_catalog", + "format": "pg_stat_progress_basebackup", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12317 + }, + { + "id": 12309, + "name": "pg_stat_progress_cluster", + "schema": "pg_catalog", + "format": "pg_stat_progress_cluster", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12307 + }, + { + "id": 12324, + "name": "pg_stat_progress_copy", + "schema": "pg_catalog", + "format": "pg_stat_progress_copy", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12322 + }, + { + "id": 12314, + "name": "pg_stat_progress_create_index", + "schema": "pg_catalog", + "format": "pg_stat_progress_create_index", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12312 + }, + { + "id": 12304, + "name": "pg_stat_progress_vacuum", + "schema": "pg_catalog", + "format": "pg_stat_progress_vacuum", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12302 + }, + { + "id": 12242, + "name": "pg_stat_recovery_prefetch", + "schema": "pg_catalog", + "format": "pg_stat_recovery_prefetch", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12240 + }, + { + "id": 12229, + "name": "pg_stat_replication", + "schema": "pg_catalog", + "format": "pg_stat_replication", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12227 + }, + { + "id": 12264, + "name": "pg_stat_replication_slots", + "schema": "pg_catalog", + "format": "pg_stat_replication_slots", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12262 + }, + { + "id": 12234, + "name": "pg_stat_slru", + "schema": "pg_catalog", + "format": "pg_stat_slru", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12232 + }, + { + "id": 12251, + "name": "pg_stat_ssl", + "schema": "pg_catalog", + "format": "pg_stat_ssl", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12249 + }, + { + "id": 12246, + "name": "pg_stat_subscription", + "schema": "pg_catalog", + "format": "pg_stat_subscription", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12244 + }, + { + "id": 12338, + "name": "pg_stat_subscription_stats", + "schema": "pg_catalog", + "format": "pg_stat_subscription_stats", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12336 + }, + { + "id": 12190, + "name": "pg_stat_sys_indexes", + "schema": "pg_catalog", + "format": "pg_stat_sys_indexes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12188 + }, + { + "id": 12154, + "name": "pg_stat_sys_tables", + "schema": "pg_catalog", + "format": "pg_stat_sys_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12152 + }, + { + "id": 12277, + "name": "pg_stat_user_functions", + "schema": "pg_catalog", + "format": "pg_stat_user_functions", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12275 + }, + { + "id": 12194, + "name": "pg_stat_user_indexes", + "schema": "pg_catalog", + "format": "pg_stat_user_indexes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12192 + }, + { + "id": 12163, + "name": "pg_stat_user_tables", + "schema": "pg_catalog", + "format": "pg_stat_user_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12161 + }, + { + "id": 12295, + "name": "pg_stat_wal", + "schema": "pg_catalog", + "format": "pg_stat_wal", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12293 + }, + { + "id": 12238, + "name": "pg_stat_wal_receiver", + "schema": "pg_catalog", + "format": "pg_stat_wal_receiver", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12236 + }, + { + "id": 12149, + "name": "pg_stat_xact_all_tables", + "schema": "pg_catalog", + "format": "pg_stat_xact_all_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12147 + }, + { + "id": 12159, + "name": "pg_stat_xact_sys_tables", + "schema": "pg_catalog", + "format": "pg_stat_xact_sys_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12157 + }, + { + "id": 12282, + "name": "pg_stat_xact_user_functions", + "schema": "pg_catalog", + "format": "pg_stat_xact_user_functions", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12280 + }, + { + "id": 12168, + "name": "pg_stat_xact_user_tables", + "schema": "pg_catalog", + "format": "pg_stat_xact_user_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12166 + }, + { + "id": 12198, + "name": "pg_statio_all_indexes", + "schema": "pg_catalog", + "format": "pg_statio_all_indexes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12196 + }, + { + "id": 12211, + "name": "pg_statio_all_sequences", + "schema": "pg_catalog", + "format": "pg_statio_all_sequences", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12209 + }, + { + "id": 12172, + "name": "pg_statio_all_tables", + "schema": "pg_catalog", + "format": "pg_statio_all_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12170 + }, + { + "id": 12203, + "name": "pg_statio_sys_indexes", + "schema": "pg_catalog", + "format": "pg_statio_sys_indexes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12201 + }, + { + "id": 12216, + "name": "pg_statio_sys_sequences", + "schema": "pg_catalog", + "format": "pg_statio_sys_sequences", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12214 + }, + { + "id": 12177, + "name": "pg_statio_sys_tables", + "schema": "pg_catalog", + "format": "pg_statio_sys_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12175 + }, + { + "id": 12207, + "name": "pg_statio_user_indexes", + "schema": "pg_catalog", + "format": "pg_statio_user_indexes", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12205 + }, + { + "id": 12220, + "name": "pg_statio_user_sequences", + "schema": "pg_catalog", + "format": "pg_statio_user_sequences", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12218 + }, + { + "id": 12181, + "name": "pg_statio_user_tables", + "schema": "pg_catalog", + "format": "pg_statio_user_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12179 + }, + { + "id": 10029, + "name": "pg_statistic", + "schema": "pg_catalog", + "format": "pg_statistic", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2619 + }, + { + "id": 10031, + "name": "pg_statistic_ext", + "schema": "pg_catalog", + "format": "pg_statistic_ext", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3381 + }, + { + "id": 10033, + "name": "pg_statistic_ext_data", + "schema": "pg_catalog", + "format": "pg_statistic_ext_data", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3429 + }, + { + "id": 12055, + "name": "pg_stats", + "schema": "pg_catalog", + "format": "pg_stats", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12053 + }, + { + "id": 12060, + "name": "pg_stats_ext", + "schema": "pg_catalog", + "format": "pg_stats_ext", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12058 + }, + { + "id": 12065, + "name": "pg_stats_ext_exprs", + "schema": "pg_catalog", + "format": "pg_stats_ext_exprs", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12063 + }, + { + "id": 6101, + "name": "pg_subscription", + "schema": "pg_catalog", + "format": "pg_subscription", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 6100 + }, + { + "id": 10114, + "name": "pg_subscription_rel", + "schema": "pg_catalog", + "format": "pg_subscription_rel", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 6102 + }, + { + "id": 12035, + "name": "pg_tables", + "schema": "pg_catalog", + "format": "pg_tables", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12033 + }, + { + "id": 10056, + "name": "pg_tablespace", + "schema": "pg_catalog", + "format": "pg_tablespace", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1213 + }, + { + "id": 12124, + "name": "pg_timezone_abbrevs", + "schema": "pg_catalog", + "format": "pg_timezone_abbrevs", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12122 + }, + { + "id": 12128, + "name": "pg_timezone_names", + "schema": "pg_catalog", + "format": "pg_timezone_names", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12126 + }, + { + "id": 10103, + "name": "pg_transform", + "schema": "pg_catalog", + "format": "pg_transform", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3576 + }, + { + "id": 10037, + "name": "pg_trigger", + "schema": "pg_catalog", + "format": "pg_trigger", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 2620 + }, + { + "id": 10064, + "name": "pg_ts_config", + "schema": "pg_catalog", + "format": "pg_ts_config", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3602 + }, + { + "id": 10066, + "name": "pg_ts_config_map", + "schema": "pg_catalog", + "format": "pg_ts_config_map", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3603 + }, + { + "id": 10068, + "name": "pg_ts_dict", + "schema": "pg_catalog", + "format": "pg_ts_dict", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3600 + }, + { + "id": 10070, + "name": "pg_ts_parser", + "schema": "pg_catalog", + "format": "pg_ts_parser", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3601 + }, + { + "id": 10072, + "name": "pg_ts_template", + "schema": "pg_catalog", + "format": "pg_ts_template", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 3764 + }, + { + "id": 71, + "name": "pg_type", + "schema": "pg_catalog", + "format": "pg_type", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1247 + }, + { + "id": 12016, + "name": "pg_user", + "schema": "pg_catalog", + "format": "pg_user", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12014 + }, + { + "id": 10080, + "name": "pg_user_mapping", + "schema": "pg_catalog", + "format": "pg_user_mapping", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 1418 + }, + { + "id": 12329, + "name": "pg_user_mappings", + "schema": "pg_catalog", + "format": "pg_user_mappings", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12327 + }, + { + "id": 12030, + "name": "pg_views", + "schema": "pg_catalog", + "format": "pg_views", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 12028 + }, + { + "id": 600, + "name": "point", + "schema": "pg_catalog", + "format": "point", + "enums": [], + "attributes": [], + "comment": "geometric point '(x, y)'", + "type_relation_id": null + }, + { + "id": 604, + "name": "polygon", + "schema": "pg_catalog", + "format": "polygon", + "enums": [], + "attributes": [], + "comment": "geometric polygon '(pt1,...)'", + "type_relation_id": null + }, + { + "id": 2249, + "name": "record", + "schema": "pg_catalog", + "format": "record", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing any composite type", + "type_relation_id": null + }, + { + "id": 1790, + "name": "refcursor", + "schema": "pg_catalog", + "format": "refcursor", + "enums": [], + "attributes": [], + "comment": "reference to cursor (portal name)", + "type_relation_id": null + }, + { + "id": 2205, + "name": "regclass", + "schema": "pg_catalog", + "format": "regclass", + "enums": [], + "attributes": [], + "comment": "registered class", + "type_relation_id": null + }, + { + "id": 4191, + "name": "regcollation", + "schema": "pg_catalog", + "format": "regcollation", + "enums": [], + "attributes": [], + "comment": "registered collation", + "type_relation_id": null + }, + { + "id": 3734, + "name": "regconfig", + "schema": "pg_catalog", + "format": "regconfig", + "enums": [], + "attributes": [], + "comment": "registered text search configuration", + "type_relation_id": null + }, + { + "id": 3769, + "name": "regdictionary", + "schema": "pg_catalog", + "format": "regdictionary", + "enums": [], + "attributes": [], + "comment": "registered text search dictionary", + "type_relation_id": null + }, + { + "id": 4089, + "name": "regnamespace", + "schema": "pg_catalog", + "format": "regnamespace", + "enums": [], + "attributes": [], + "comment": "registered namespace", + "type_relation_id": null + }, + { + "id": 2203, + "name": "regoper", + "schema": "pg_catalog", + "format": "regoper", + "enums": [], + "attributes": [], + "comment": "registered operator", + "type_relation_id": null + }, + { + "id": 2204, + "name": "regoperator", + "schema": "pg_catalog", + "format": "regoperator", + "enums": [], + "attributes": [], + "comment": "registered operator (with args)", + "type_relation_id": null + }, + { + "id": 24, + "name": "regproc", + "schema": "pg_catalog", + "format": "regproc", + "enums": [], + "attributes": [], + "comment": "registered procedure", + "type_relation_id": null + }, + { + "id": 2202, + "name": "regprocedure", + "schema": "pg_catalog", + "format": "regprocedure", + "enums": [], + "attributes": [], + "comment": "registered procedure (with args)", + "type_relation_id": null + }, + { + "id": 4096, + "name": "regrole", + "schema": "pg_catalog", + "format": "regrole", + "enums": [], + "attributes": [], + "comment": "registered role", + "type_relation_id": null + }, + { + "id": 2206, + "name": "regtype", + "schema": "pg_catalog", + "format": "regtype", + "enums": [], + "attributes": [], + "comment": "registered type", + "type_relation_id": null + }, + { + "id": 269, + "name": "table_am_handler", + "schema": "pg_catalog", + "format": "table_am_handler", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 25, + "name": "text", + "schema": "pg_catalog", + "format": "text", + "enums": [], + "attributes": [], + "comment": "variable-length string, no limit specified", + "type_relation_id": null + }, + { + "id": 27, + "name": "tid", + "schema": "pg_catalog", + "format": "tid", + "enums": [], + "attributes": [], + "comment": "(block, offset), physical location of tuple", + "type_relation_id": null + }, + { + "id": 1083, + "name": "time", + "schema": "pg_catalog", + "format": "time without time zone", + "enums": [], + "attributes": [], + "comment": "time of day", + "type_relation_id": null + }, + { + "id": 1114, + "name": "timestamp", + "schema": "pg_catalog", + "format": "timestamp without time zone", + "enums": [], + "attributes": [], + "comment": "date and time", + "type_relation_id": null + }, + { + "id": 1184, + "name": "timestamptz", + "schema": "pg_catalog", + "format": "timestamp with time zone", + "enums": [], + "attributes": [], + "comment": "date and time with time zone", + "type_relation_id": null + }, + { + "id": 1266, + "name": "timetz", + "schema": "pg_catalog", + "format": "time with time zone", + "enums": [], + "attributes": [], + "comment": "time of day with time zone", + "type_relation_id": null + }, + { + "id": 2279, + "name": "trigger", + "schema": "pg_catalog", + "format": "trigger", + "enums": [], + "attributes": [], + "comment": "pseudo-type for the result of a trigger function", + "type_relation_id": null + }, + { + "id": 3310, + "name": "tsm_handler", + "schema": "pg_catalog", + "format": "tsm_handler", + "enums": [], + "attributes": [], + "comment": "pseudo-type for the result of a tablesample method function", + "type_relation_id": null + }, + { + "id": 4533, + "name": "tsmultirange", + "schema": "pg_catalog", + "format": "tsmultirange", + "enums": [], + "attributes": [], + "comment": "multirange of timestamps without time zone", + "type_relation_id": null + }, + { + "id": 3615, + "name": "tsquery", + "schema": "pg_catalog", + "format": "tsquery", + "enums": [], + "attributes": [], + "comment": "query representation for text search", + "type_relation_id": null + }, + { + "id": 3908, + "name": "tsrange", + "schema": "pg_catalog", + "format": "tsrange", + "enums": [], + "attributes": [], + "comment": "range of timestamps without time zone", + "type_relation_id": null + }, + { + "id": 4534, + "name": "tstzmultirange", + "schema": "pg_catalog", + "format": "tstzmultirange", + "enums": [], + "attributes": [], + "comment": "multirange of timestamps with time zone", + "type_relation_id": null + }, + { + "id": 3910, + "name": "tstzrange", + "schema": "pg_catalog", + "format": "tstzrange", + "enums": [], + "attributes": [], + "comment": "range of timestamps with time zone", + "type_relation_id": null + }, + { + "id": 3614, + "name": "tsvector", + "schema": "pg_catalog", + "format": "tsvector", + "enums": [], + "attributes": [], + "comment": "text representation for text search", + "type_relation_id": null + }, + { + "id": 2970, + "name": "txid_snapshot", + "schema": "pg_catalog", + "format": "txid_snapshot", + "enums": [], + "attributes": [], + "comment": "txid snapshot", + "type_relation_id": null + }, + { + "id": 705, + "name": "unknown", + "schema": "pg_catalog", + "format": "unknown", + "enums": [], + "attributes": [], + "comment": "pseudo-type representing an undetermined type", + "type_relation_id": null + }, + { + "id": 2950, + "name": "uuid", + "schema": "pg_catalog", + "format": "uuid", + "enums": [], + "attributes": [], + "comment": "UUID datatype", + "type_relation_id": null + }, + { + "id": 1562, + "name": "varbit", + "schema": "pg_catalog", + "format": "bit varying", + "enums": [], + "attributes": [], + "comment": "variable-length bit string", + "type_relation_id": null + }, + { + "id": 1043, + "name": "varchar", + "schema": "pg_catalog", + "format": "character varying", + "enums": [], + "attributes": [], + "comment": "varchar(length), non-blank-padded string, variable storage length", + "type_relation_id": null + }, + { + "id": 2278, + "name": "void", + "schema": "pg_catalog", + "format": "void", + "enums": [], + "attributes": [], + "comment": "pseudo-type for the result of a function with no real result", + "type_relation_id": null + }, + { + "id": 28, + "name": "xid", + "schema": "pg_catalog", + "format": "xid", + "enums": [], + "attributes": [], + "comment": "transaction id", + "type_relation_id": null + }, + { + "id": 5069, + "name": "xid8", + "schema": "pg_catalog", + "format": "xid8", + "enums": [], + "attributes": [], + "comment": "full transaction id", + "type_relation_id": null + }, + { + "id": 142, + "name": "xml", + "schema": "pg_catalog", + "format": "xml", + "enums": [], + "attributes": [], + "comment": "XML content", + "type_relation_id": null + }, + { + "id": 16415, + "name": "_author_stats", + "schema": "public", + "format": "author_stats[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 16393, + "name": "_authors", + "schema": "public", + "format": "authors[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 16419, + "name": "_book_prices", + "schema": "public", + "format": "book_prices[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 16429, + "name": "_book_submissions", + "schema": "public", + "format": "book_submissions[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 16423, + "name": "_book_summaries", + "schema": "public", + "format": "book_summaries[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 16401, + "name": "_books", + "schema": "public", + "format": "books[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 16384, + "name": "_mood", + "schema": "public", + "format": "mood[]", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": null + }, + { + "id": 16416, + "name": "author_stats", + "schema": "public", + "format": "author_stats", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 16414 + }, + { + "id": 16394, + "name": "authors", + "schema": "public", + "format": "authors", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 16392 + }, + { + "id": 16420, + "name": "book_prices", + "schema": "public", + "format": "book_prices", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 16418 + }, + { + "id": 16430, + "name": "book_submissions", + "schema": "public", + "format": "book_submissions", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 16428 + }, + { + "id": 16424, + "name": "book_summaries", + "schema": "public", + "format": "book_summaries", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 16422 + }, + { + "id": 16402, + "name": "books", + "schema": "public", + "format": "books", + "enums": [], + "attributes": [], + "comment": null, + "type_relation_id": 16400 + }, + { + "id": 16385, "name": "mood", "schema": "public", "format": "mood", diff --git a/packages/supabase_typegen/test/fixtures/seed.sql b/packages/supabase_typegen/test/fixtures/seed.sql new file mode 100644 index 000000000..35ceb9be5 --- /dev/null +++ b/packages/supabase_typegen/test/fixtures/seed.sql @@ -0,0 +1,90 @@ +-- Schema behind test/fixtures/generator_metadata.json. +-- +-- Apply this to a disposable Postgres database and run +-- tool/regenerate_fixture.ts to refresh the fixture; the exact commands are +-- documented at the top of that script. + +CREATE TYPE public.mood AS ENUM ('happy', 'very happy', 'sad'); + +CREATE TABLE public.authors ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name text NOT NULL +); +ALTER TABLE public.authors ENABLE ROW LEVEL SECURITY; + +CREATE TABLE public.books ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + title text NOT NULL, + author_id bigint NOT NULL REFERENCES public.authors (id), + price numeric, + rating double precision, + in_print boolean NOT NULL DEFAULT true, + mood public.mood, + tags text[], + page_counts integer[], + metadata jsonb, + cover_uuid uuid, + published_on date, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamp +); +ALTER TABLE public.books ENABLE ROW LEVEL SECURITY; +COMMENT ON TABLE public.books IS 'Books available in the library'; +COMMENT ON COLUMN public.books.created_at IS 'When the row was created'; + +-- A read-only aggregating view: not auto-updatable, so it must generate no +-- insert or update surface. +CREATE VIEW public.author_stats AS +SELECT + books.author_id, + count(*) AS book_count +FROM public.books +GROUP BY books.author_id; +COMMENT ON VIEW public.author_stats IS 'Aggregated statistics per author'; + +-- An auto-updatable view with one computed, non-updatable column +-- (discounted_price), which must be read-only in the generated code. +CREATE VIEW public.book_prices AS +SELECT + books.id, + books.title, + books.price, + books.price * 0.9 AS discounted_price +FROM public.books; +COMMENT ON VIEW public.book_prices IS 'Prices per book, with the standard discount precomputed'; + +-- A materialized view: never insertable or updatable. +CREATE MATERIALIZED VIEW public.book_summaries AS +SELECT + books.id, + books.title, + authors.name AS author_name +FROM public.books +JOIN public.authors ON authors.id = books.author_id; +COMMENT ON MATERIALIZED VIEW public.book_summaries IS 'Denormalized book and author names'; + +-- A join view made insertable only through an INSTEAD OF INSERT trigger. +-- postgrest-typegen 0.2.0 reports it as not updatable; the is_insert_enabled +-- flag of the next release will surface the trigger. +CREATE VIEW public.book_submissions AS +SELECT + books.title, + authors.name AS author_name +FROM public.books +JOIN public.authors ON authors.id = books.author_id; + +CREATE FUNCTION public.insert_book_submission() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO public.books (title, author_id) + VALUES ( + NEW.title, + (SELECT id FROM public.authors WHERE name = NEW.author_name) + ); + RETURN NEW; +END; +$$; + +CREATE TRIGGER book_submissions_insert +INSTEAD OF INSERT ON public.book_submissions +FOR EACH ROW EXECUTE FUNCTION public.insert_book_submission(); diff --git a/packages/supabase_typegen/test/generator_metadata_parser_test.dart b/packages/supabase_typegen/test/generator_metadata_parser_test.dart index c9c7f7599..bfb76a15b 100644 --- a/packages/supabase_typegen/test/generator_metadata_parser_test.dart +++ b/packages/supabase_typegen/test/generator_metadata_parser_test.dart @@ -32,10 +32,19 @@ void main() { ); }); + test('tolerates the version and primaryKeys fields of the document', () { + expect(document['version'], 1); + expect(document['primaryKeys'], isA>()); + expect(schema.tables, isNotEmpty); + }); + test('parses tables and views sorted by name', () { expect(schema.tables.map((table) => table.name), [ 'author_stats', 'authors', + 'book_prices', + 'book_submissions', + 'book_summaries', 'books', ]); }); @@ -46,12 +55,38 @@ void main() { expect(books.isUpdatable, isTrue); }); - test('read-only views are neither insertable nor updatable', () { - final authorStats = schema.tables.singleWhere( - (table) => table.name == 'author_stats', + test('read-only views and materialized views are neither insertable nor ' + 'updatable', () { + for (final name in ['author_stats', 'book_submissions', 'book_summaries']) { + final relation = schema.tables.singleWhere( + (table) => table.name == name, + ); + expect(relation.isInsertable, isFalse, reason: name); + expect(relation.isUpdatable, isFalse, reason: name); + } + }); + + test('automatically updatable views are insertable and updatable', () { + final bookPrices = schema.tables.singleWhere( + (table) => table.name == 'book_prices', + ); + expect(bookPrices.isInsertable, isTrue); + expect(bookPrices.isUpdatable, isTrue); + }); + + test('non-updatable columns of a writable view are read-only', () { + final bookPrices = schema.tables.singleWhere( + (table) => table.name == 'book_prices', + ); + final discountedPrice = bookPrices.columns.singleWhere( + (column) => column.name == 'discounted_price', + ); + expect(discountedPrice.isReadOnly, isTrue); + + final price = bookPrices.columns.singleWhere( + (column) => column.name == 'price', ); - expect(authorStats.isInsertable, isFalse); - expect(authorStats.isUpdatable, isFalse); + expect(price.isReadOnly, isFalse); }); test('parses table and view comments', () { diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index 0ed7f1558..1f2fe76e1 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -87,6 +87,117 @@ class Authors { static const name = TableColumn('name'); } +/// A row of the `book_prices` table. +/// Prices per book, with the standard discount precomputed +extension type const BookPricesRow(Map _json) + implements Map { + int? get id => _json['id'] as int?; + String? get title => _json['title'] as String?; + num? get price => _json['price'] as num?; + num? get discountedPrice => _json['discounted_price'] as num?; +} + +/// Values for inserting a row into `book_prices`. Columns that are nullable, +/// identity, or covered by a database default are optional; passing `null` +/// omits the column so the database default applies. Columns the database +/// always generates itself are left out entirely. Use the `set…ToNull` methods +/// to insert SQL NULL explicitly. +extension type const BookPricesInsert._(Map _json) + implements Map { + BookPricesInsert({int? id, String? title, num? price}) + : this._({'id': ?id, 'title': ?title, 'price': ?price}); + + /// Returns a copy with `id` set to SQL NULL, overriding any database default. + BookPricesInsert setIdToNull() => BookPricesInsert._({..._json, 'id': null}); + + /// Returns a copy with `title` set to SQL NULL, overriding any database + /// default. + BookPricesInsert setTitleToNull() => + BookPricesInsert._({..._json, 'title': null}); + + /// Returns a copy with `price` set to SQL NULL, overriding any database + /// default. + BookPricesInsert setPriceToNull() => + BookPricesInsert._({..._json, 'price': null}); +} + +/// Values for updating rows of `book_prices`. All columns are optional; passing +/// `null` omits the column, leaving it unchanged. Use the `set…ToNull` methods +/// to write SQL NULL explicitly. +extension type const BookPricesUpdate._(Map _json) + implements Map { + BookPricesUpdate({int? id, String? title, num? price}) + : this._({'id': ?id, 'title': ?title, 'price': ?price}); + + /// Returns a copy with `id` set to SQL NULL, overriding any database default. + BookPricesUpdate setIdToNull() => BookPricesUpdate._({..._json, 'id': null}); + + /// Returns a copy with `title` set to SQL NULL, overriding any database + /// default. + BookPricesUpdate setTitleToNull() => + BookPricesUpdate._({..._json, 'title': null}); + + /// Returns a copy with `price` set to SQL NULL, overriding any database + /// default. + BookPricesUpdate setPriceToNull() => + BookPricesUpdate._({..._json, 'price': null}); +} + +/// Typed access to the `book_prices` table. +class BookPrices { + const BookPrices._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable('book_prices', BookPricesRow.new); + + static const id = TableColumn('id'); + static const title = TableColumn('title'); + static const price = TableColumn('price'); + static const discountedPrice = TableColumn('discounted_price'); +} + +/// A row of the `book_submissions` table. +extension type const BookSubmissionsRow(Map _json) + implements Map { + String? get title => _json['title'] as String?; + String? get authorName => _json['author_name'] as String?; +} + +/// Typed access to the `book_submissions` table. +class BookSubmissions { + const BookSubmissions._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable( + 'book_submissions', + BookSubmissionsRow.new, + ); + + static const title = TableColumn('title'); + static const authorName = TableColumn('author_name'); +} + +/// A row of the `book_summaries` table. +/// Denormalized book and author names +extension type const BookSummariesRow(Map _json) + implements Map { + int? get id => _json['id'] as int?; + String? get title => _json['title'] as String?; + String? get authorName => _json['author_name'] as String?; +} + +/// Typed access to the `book_summaries` table. +class BookSummaries { + const BookSummaries._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable('book_summaries', BookSummariesRow.new); + + static const id = TableColumn('id'); + static const title = TableColumn('title'); + static const authorName = TableColumn('author_name'); +} + /// A row of the `books` table. /// Books available in the library extension type const BooksRow(Map _json) diff --git a/packages/supabase_typegen/tool/regenerate_fixture.ts b/packages/supabase_typegen/tool/regenerate_fixture.ts new file mode 100644 index 000000000..7ceeb4f7d --- /dev/null +++ b/packages/supabase_typegen/tool/regenerate_fixture.ts @@ -0,0 +1,33 @@ +// Regenerates test/fixtures/generator_metadata.json by introspecting a real +// Postgres database seeded with test/fixtures/seed.sql, using the released +// @supabase/postgrest-typegen package that also ships inside postgres-meta. +// +// Run from the package root (Bun installs the imports on first run): +// +// docker run --rm --detach --name supabase_typegen_fixture \ +// --env POSTGRES_PASSWORD=postgres --publish 55432:5432 postgres:15 +// until docker exec supabase_typegen_fixture pg_isready --username postgres \ +// ; do sleep 1; done +// docker exec --interactive supabase_typegen_fixture \ +// psql --username postgres --set ON_ERROR_STOP=1 < test/fixtures/seed.sql +// bun tool/regenerate_fixture.ts +// docker rm --force supabase_typegen_fixture + +import { + introspect, + sortGeneratorMetadata, +} from "@supabase/postgrest-typegen@0.2.0"; +import pg from "pg@8.23.0"; + +const pool = new pg.Pool({ + connectionString: + process.env.DATABASE_URL ?? + "postgresql://postgres:postgres@localhost:55432/postgres", +}); +const metadata = sortGeneratorMetadata(await introspect(pool)); +await pool.end(); + +await Bun.write( + new URL("../test/fixtures/generator_metadata.json", import.meta.url), + `${JSON.stringify(metadata, null, 2)}\n`, +); From 774dc9d26931c8bd9aecd258133aae45aadaaa2d Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 31 Aug 2026 20:47:32 +0200 Subject: [PATCH 19/31] feat(supabase_typegen): resolve enum types exactly via the column type_schema The contract carries type_schema on every column, so enum types are resolved by schema-qualified name instead of bare-name matching with a schema preference. Removes the README limitation about same-named enums across schemas. --- packages/supabase_typegen/README.md | 4 -- .../lib/src/generator_metadata_parser.dart | 38 +++++++------ .../test/generator_metadata_parser_test.dart | 56 +++++++++++++++---- 3 files changed, 64 insertions(+), 34 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index baac35f2f..52e78ece2 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -97,9 +97,5 @@ await client.table(Books.table).insert( - `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. diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index 762074055..652f74f11 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -127,7 +127,7 @@ SchemaDescription parseGeneratorMetadata( } final foreignKeysByColumn = _foreignKeysByColumn(document, schemaName); - final enumTypes = _enumTypes(document, schemaName); + final enumTypes = _enumTypes(document); final tables = []; final enumsByQualifiedName = {}; @@ -148,7 +148,12 @@ SchemaDescription parseGeneratorMetadata( var postgresFormat = format; if (isEnum && !isArray) { - final enumDescription = _enumDescription(format, enumValues, enumTypes); + final enumDescription = _enumDescription( + format, + column['type_schema'] as String, + enumValues, + enumTypes, + ); postgresFormat = enumDescription.qualifiedName; enumsByQualifiedName.putIfAbsent( enumDescription.qualifiedName, @@ -244,35 +249,32 @@ Map<(String, String), ForeignKeyDescription> _foreignKeysByColumn( return foreignKeys; } -/// Maps enum type names to `(schema, values)`, preferring types of -/// [schemaName] when the same name exists in several schemas. -Map)> _enumTypes( - Map document, - String schemaName, -) { - final enumTypes = )>{}; +/// Maps schema-qualified enum type names, for example `public.mood`, to +/// their values in declaration order. +Map> _enumTypes(Map document) { + final enumTypes = >{}; for (final type in (document['types'] as List? ?? const []) .cast>()) { final values = (type['enums'] as List? ?? const []).cast(); if (values.isEmpty) continue; - final name = type['name'] as String; - final schema = type['schema'] as String; - if (schema == schemaName || !enumTypes.containsKey(name)) { - enumTypes[name] = (schema, values); - } + enumTypes['${type['schema']}.${type['name']}'] = values; } return enumTypes; } +/// Resolves the enum type of a column exactly, by the column's `type_schema` +/// and type name. Falls back to the values carried on the column itself when +/// the type is missing from the document's `types` list. EnumDescription _enumDescription( String format, + String typeSchema, List columnEnumValues, - Map)> enumTypes, + Map> enumTypes, ) { - final type = enumTypes[format]; + final qualifiedName = '$typeSchema.$format'; return EnumDescription( - qualifiedName: type == null ? format : '${type.$1}.$format', - values: type == null ? columnEnumValues : type.$2, + qualifiedName: qualifiedName, + values: enumTypes[qualifiedName] ?? columnEnumValues, ); } diff --git a/packages/supabase_typegen/test/generator_metadata_parser_test.dart b/packages/supabase_typegen/test/generator_metadata_parser_test.dart index bfb76a15b..db149b20b 100644 --- a/packages/supabase_typegen/test/generator_metadata_parser_test.dart +++ b/packages/supabase_typegen/test/generator_metadata_parser_test.dart @@ -11,9 +11,7 @@ void main() { setUpAll(() { document = jsonDecode( - File( - 'test/fixtures/generator_metadata.json', - ).readAsStringSync(), + File('test/fixtures/generator_metadata.json').readAsStringSync(), ) as Map; schema = parseGeneratorMetadata(document); @@ -58,9 +56,7 @@ void main() { test('read-only views and materialized views are neither insertable nor ' 'updatable', () { for (final name in ['author_stats', 'book_submissions', 'book_summaries']) { - final relation = schema.tables.singleWhere( - (table) => table.name == name, - ); + final relation = schema.tables.singleWhere((table) => table.name == name); expect(relation.isInsertable, isFalse, reason: name); expect(relation.isUpdatable, isFalse, reason: name); } @@ -195,12 +191,7 @@ void main() { for (final format in ['inet', 'cidr', 'macaddr', 'money', 'xml', 'name']) { final parsed = parseGeneratorMetadata({ 'tables': [ - { - 'id': 1, - 'schema': 'public', - 'name': 'servers', - 'comment': null, - }, + {'id': 1, 'schema': 'public', 'name': 'servers', 'comment': null}, ], 'columns': [columnOf(format)], }); @@ -226,6 +217,47 @@ void main() { expect(moodColumn.postgresFormat, 'public.mood'); }); + test( + 'resolves same-named enums across schemas by the column type_schema', + () { + final parsed = parseGeneratorMetadata({ + 'version': 1, + 'tables': [ + {'id': 1, 'schema': 'public', 'name': 'reviews', 'comment': null}, + ], + 'columns': [ + { + ..._column(tableId: 1, table: 'reviews', name: 'mood'), + 'data_type': 'USER-DEFINED', + 'format': 'mood', + 'type_schema': 'internal', + 'enums': ['up', 'down'], + }, + ], + 'types': [ + { + 'id': 10, + 'schema': 'public', + 'name': 'mood', + 'enums': ['happy', 'sad'], + }, + { + 'id': 11, + 'schema': 'internal', + 'name': 'mood', + 'enums': ['up', 'down'], + }, + ], + }); + + final mood = parsed.tables.single.columns.single; + expect(mood.postgresFormat, 'internal.mood'); + final enumDescription = parsed.enums.single; + expect(enumDescription.qualifiedName, 'internal.mood'); + expect(enumDescription.values, ['up', 'down']); + }, + ); + test('parses array columns', () { final books = schema.tables.singleWhere((table) => table.name == 'books'); final tags = books.columns.singleWhere((column) => column.name == 'tags'); From e4070d6ecb767fcac83d792457dab9959ef9b88c Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 31 Aug 2026 21:03:10 +0200 Subject: [PATCH 20/31] refactor(supabase_typegen): emit columns in the canonical name order Drops the ordinal_position re-sort so columns flow through in the order sortGeneratorMetadata produces (name order within a table), matching every other postgrest-typegen generator and keeping output insensitive to column declaration order. --- .../lib/src/generator_metadata_parser.dart | 10 +- .../test/dart_generator_test.dart | 4 +- .../test/goldens/supabase_schema.dart | 208 +++++++++--------- 3 files changed, 109 insertions(+), 113 deletions(-) diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index 652f74f11..8dae1474f 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -110,6 +110,9 @@ SchemaDescription parseGeneratorMetadata( (relation: materializedView, isInsertable: false, isUpdatable: false), ]; + // Document order is kept: `sortGeneratorMetadata` orders columns by name + // within a table, the canonical order every postgrest-typegen generator + // emits. final columnsByRelationId = >>{}; for (final column in (document['columns'] as List? ?? const []) @@ -118,13 +121,6 @@ SchemaDescription parseGeneratorMetadata( .putIfAbsent(column['table_id'] as int, () => []) .add(column); } - for (final columns in columnsByRelationId.values) { - columns.sort( - (a, b) => (a['ordinal_position'] as int).compareTo( - b['ordinal_position'] as int, - ), - ); - } final foreignKeysByColumn = _foreignKeysByColumn(document, schemaName); final enumTypes = _enumTypes(document); diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index 6f805c929..d794c8c6b 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -101,11 +101,11 @@ void main() { expect(code, contains("TableColumn('discounted_price')")); expect( code, - contains('BookPricesInsert({int? id, String? title, num? price})'), + contains('BookPricesInsert({int? id, num? price, String? title})'), ); expect( code, - contains('BookPricesUpdate({int? id, String? title, num? price})'), + contains('BookPricesUpdate({int? id, num? price, String? title})'), ); }); diff --git a/packages/supabase_typegen/test/goldens/supabase_schema.dart b/packages/supabase_typegen/test/goldens/supabase_schema.dart index 1f2fe76e1..f7711c2c7 100644 --- a/packages/supabase_typegen/test/goldens/supabase_schema.dart +++ b/packages/supabase_typegen/test/goldens/supabase_schema.dart @@ -91,10 +91,10 @@ class Authors { /// Prices per book, with the standard discount precomputed extension type const BookPricesRow(Map _json) implements Map { + num? get discountedPrice => _json['discounted_price'] as num?; int? get id => _json['id'] as int?; - String? get title => _json['title'] as String?; num? get price => _json['price'] as num?; - num? get discountedPrice => _json['discounted_price'] as num?; + String? get title => _json['title'] as String?; } /// Values for inserting a row into `book_prices`. Columns that are nullable, @@ -104,21 +104,21 @@ extension type const BookPricesRow(Map _json) /// to insert SQL NULL explicitly. extension type const BookPricesInsert._(Map _json) implements Map { - BookPricesInsert({int? id, String? title, num? price}) - : this._({'id': ?id, 'title': ?title, 'price': ?price}); + BookPricesInsert({int? id, num? price, String? title}) + : this._({'id': ?id, 'price': ?price, 'title': ?title}); /// Returns a copy with `id` set to SQL NULL, overriding any database default. BookPricesInsert setIdToNull() => BookPricesInsert._({..._json, 'id': null}); - /// Returns a copy with `title` set to SQL NULL, overriding any database - /// default. - BookPricesInsert setTitleToNull() => - BookPricesInsert._({..._json, 'title': null}); - /// Returns a copy with `price` set to SQL NULL, overriding any database /// default. BookPricesInsert setPriceToNull() => BookPricesInsert._({..._json, 'price': null}); + + /// Returns a copy with `title` set to SQL NULL, overriding any database + /// default. + BookPricesInsert setTitleToNull() => + BookPricesInsert._({..._json, 'title': null}); } /// Values for updating rows of `book_prices`. All columns are optional; passing @@ -126,21 +126,21 @@ extension type const BookPricesInsert._(Map _json) /// to write SQL NULL explicitly. extension type const BookPricesUpdate._(Map _json) implements Map { - BookPricesUpdate({int? id, String? title, num? price}) - : this._({'id': ?id, 'title': ?title, 'price': ?price}); + BookPricesUpdate({int? id, num? price, String? title}) + : this._({'id': ?id, 'price': ?price, 'title': ?title}); /// Returns a copy with `id` set to SQL NULL, overriding any database default. BookPricesUpdate setIdToNull() => BookPricesUpdate._({..._json, 'id': null}); - /// Returns a copy with `title` set to SQL NULL, overriding any database - /// default. - BookPricesUpdate setTitleToNull() => - BookPricesUpdate._({..._json, 'title': null}); - /// Returns a copy with `price` set to SQL NULL, overriding any database /// default. BookPricesUpdate setPriceToNull() => BookPricesUpdate._({..._json, 'price': null}); + + /// Returns a copy with `title` set to SQL NULL, overriding any database + /// default. + BookPricesUpdate setTitleToNull() => + BookPricesUpdate._({..._json, 'title': null}); } /// Typed access to the `book_prices` table. @@ -150,17 +150,17 @@ class BookPrices { /// Table definition for [PostgrestClient.table]. static const table = PostgrestTable('book_prices', BookPricesRow.new); + static const discountedPrice = TableColumn('discounted_price'); static const id = TableColumn('id'); - static const title = TableColumn('title'); static const price = TableColumn('price'); - static const discountedPrice = TableColumn('discounted_price'); + static const title = TableColumn('title'); } /// A row of the `book_submissions` table. extension type const BookSubmissionsRow(Map _json) implements Map { - String? get title => _json['title'] as String?; String? get authorName => _json['author_name'] as String?; + String? get title => _json['title'] as String?; } /// Typed access to the `book_submissions` table. @@ -173,17 +173,17 @@ class BookSubmissions { BookSubmissionsRow.new, ); - static const title = TableColumn('title'); static const authorName = TableColumn('author_name'); + static const title = TableColumn('title'); } /// A row of the `book_summaries` table. /// Denormalized book and author names extension type const BookSummariesRow(Map _json) implements Map { + String? get authorName => _json['author_name'] as String?; int? get id => _json['id'] as int?; String? get title => _json['title'] as String?; - String? get authorName => _json['author_name'] as String?; } /// Typed access to the `book_summaries` table. @@ -193,36 +193,36 @@ class BookSummaries { /// Table definition for [PostgrestClient.table]. static const table = PostgrestTable('book_summaries', BookSummariesRow.new); + static const authorName = TableColumn('author_name'); static const id = TableColumn('id'); static const title = TableColumn('title'); - static const authorName = TableColumn('author_name'); } /// A row of the `books` table. /// Books available in the library extension type const BooksRow(Map _json) implements Map { - int get id => _json['id'] as int; - String get title => _json['title'] as String; int get authorId => _json['author_id'] as int; - num? get price => _json['price'] as num?; - double? get rating => (_json['rating'] as num?)?.toDouble(); + String? get coverUuid => _json['cover_uuid'] as String?; + + /// When the row was created + DateTime get createdAt => DateTime.parse(_json['created_at'] as String); + int get id => _json['id'] as int; bool get inPrint => _json['in_print'] as bool; + Object? get metadata => _json['metadata'] as Object?; Mood? get mood => switch (_json['mood']) { null => null, final Object value => Mood.fromWire(value as String), }; - List? get tags => (_json['tags'] as List?)?.cast(); List? get pageCounts => (_json['page_counts'] as List?)?.cast(); - Object? get metadata => _json['metadata'] as Object?; - String? get coverUuid => _json['cover_uuid'] as String?; + num? get price => _json['price'] as num?; DateTime? get publishedOn => switch (_json['published_on']) { null => null, final Object value => DateTime.parse(value as String), }; - - /// When the row was created - DateTime get createdAt => DateTime.parse(_json['created_at'] as String); + double? get rating => (_json['rating'] as num?)?.toDouble(); + List? get tags => (_json['tags'] as List?)?.cast(); + String get title => _json['title'] as String; DateTime? get updatedAt => switch (_json['updated_at']) { null => null, final Object value => DateTime.parse(value as String), @@ -237,76 +237,76 @@ extension type const BooksRow(Map _json) extension type const BooksInsert._(Map _json) implements Map { BooksInsert({ - int? id, - required String title, required int authorId, - num? price, - double? rating, + String? coverUuid, + DateTime? createdAt, + int? id, bool? inPrint, + Object? metadata, Mood? mood, - List? tags, List? pageCounts, - Object? metadata, - String? coverUuid, + num? price, DateTime? publishedOn, - DateTime? createdAt, + double? rating, + List? tags, + required String title, DateTime? updatedAt, }) : this._({ - 'id': ?id, - 'title': title, 'author_id': authorId, - 'price': ?price, - 'rating': ?rating, + 'cover_uuid': ?coverUuid, + 'created_at': ?createdAt?.toUtc().toIso8601String(), + 'id': ?id, 'in_print': ?inPrint, + 'metadata': ?metadata, 'mood': ?mood?.wireName, - 'tags': ?tags, 'page_counts': ?pageCounts, - 'metadata': ?metadata, - 'cover_uuid': ?coverUuid, + 'price': ?price, 'published_on': ?switch (publishedOn) { null => null, final value => _dateString(value), }, - 'created_at': ?createdAt?.toUtc().toIso8601String(), + 'rating': ?rating, + 'tags': ?tags, + 'title': title, 'updated_at': ?updatedAt?.toIso8601String(), }); - /// Returns a copy with `price` set to SQL NULL, overriding any database + /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database /// default. - BooksInsert setPriceToNull() => BooksInsert._({..._json, 'price': null}); + BooksInsert setCoverUuidToNull() => + BooksInsert._({..._json, 'cover_uuid': null}); - /// Returns a copy with `rating` set to SQL NULL, overriding any database + /// Returns a copy with `metadata` set to SQL NULL, overriding any database /// default. - BooksInsert setRatingToNull() => BooksInsert._({..._json, 'rating': null}); + BooksInsert setMetadataToNull() => + BooksInsert._({..._json, 'metadata': null}); /// Returns a copy with `mood` set to SQL NULL, overriding any database /// default. BooksInsert setMoodToNull() => BooksInsert._({..._json, 'mood': null}); - /// Returns a copy with `tags` set to SQL NULL, overriding any database - /// default. - BooksInsert setTagsToNull() => BooksInsert._({..._json, 'tags': null}); - /// Returns a copy with `page_counts` set to SQL NULL, overriding any database /// default. BooksInsert setPageCountsToNull() => BooksInsert._({..._json, 'page_counts': null}); - /// Returns a copy with `metadata` set to SQL NULL, overriding any database - /// default. - BooksInsert setMetadataToNull() => - BooksInsert._({..._json, 'metadata': null}); - - /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database + /// Returns a copy with `price` set to SQL NULL, overriding any database /// default. - BooksInsert setCoverUuidToNull() => - BooksInsert._({..._json, 'cover_uuid': null}); + BooksInsert setPriceToNull() => BooksInsert._({..._json, 'price': null}); /// Returns a copy with `published_on` set to SQL NULL, overriding any /// database default. BooksInsert setPublishedOnToNull() => BooksInsert._({..._json, 'published_on': null}); + /// Returns a copy with `rating` set to SQL NULL, overriding any database + /// default. + BooksInsert setRatingToNull() => BooksInsert._({..._json, 'rating': null}); + + /// Returns a copy with `tags` set to SQL NULL, overriding any database + /// default. + BooksInsert setTagsToNull() => BooksInsert._({..._json, 'tags': null}); + /// Returns a copy with `updated_at` set to SQL NULL, overriding any database /// default. BooksInsert setUpdatedAtToNull() => @@ -319,76 +319,76 @@ extension type const BooksInsert._(Map _json) extension type const BooksUpdate._(Map _json) implements Map { BooksUpdate({ - int? id, - String? title, int? authorId, - num? price, - double? rating, + String? coverUuid, + DateTime? createdAt, + int? id, bool? inPrint, + Object? metadata, Mood? mood, - List? tags, List? pageCounts, - Object? metadata, - String? coverUuid, + num? price, DateTime? publishedOn, - DateTime? createdAt, + double? rating, + List? tags, + String? title, DateTime? updatedAt, }) : this._({ - 'id': ?id, - 'title': ?title, 'author_id': ?authorId, - 'price': ?price, - 'rating': ?rating, + 'cover_uuid': ?coverUuid, + 'created_at': ?createdAt?.toUtc().toIso8601String(), + 'id': ?id, 'in_print': ?inPrint, + 'metadata': ?metadata, 'mood': ?mood?.wireName, - 'tags': ?tags, 'page_counts': ?pageCounts, - 'metadata': ?metadata, - 'cover_uuid': ?coverUuid, + 'price': ?price, 'published_on': ?switch (publishedOn) { null => null, final value => _dateString(value), }, - 'created_at': ?createdAt?.toUtc().toIso8601String(), + 'rating': ?rating, + 'tags': ?tags, + 'title': ?title, 'updated_at': ?updatedAt?.toIso8601String(), }); - /// Returns a copy with `price` set to SQL NULL, overriding any database + /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database /// default. - BooksUpdate setPriceToNull() => BooksUpdate._({..._json, 'price': null}); + BooksUpdate setCoverUuidToNull() => + BooksUpdate._({..._json, 'cover_uuid': null}); - /// Returns a copy with `rating` set to SQL NULL, overriding any database + /// Returns a copy with `metadata` set to SQL NULL, overriding any database /// default. - BooksUpdate setRatingToNull() => BooksUpdate._({..._json, 'rating': null}); + BooksUpdate setMetadataToNull() => + BooksUpdate._({..._json, 'metadata': null}); /// Returns a copy with `mood` set to SQL NULL, overriding any database /// default. BooksUpdate setMoodToNull() => BooksUpdate._({..._json, 'mood': null}); - /// Returns a copy with `tags` set to SQL NULL, overriding any database - /// default. - BooksUpdate setTagsToNull() => BooksUpdate._({..._json, 'tags': null}); - /// Returns a copy with `page_counts` set to SQL NULL, overriding any database /// default. BooksUpdate setPageCountsToNull() => BooksUpdate._({..._json, 'page_counts': null}); - /// Returns a copy with `metadata` set to SQL NULL, overriding any database - /// default. - BooksUpdate setMetadataToNull() => - BooksUpdate._({..._json, 'metadata': null}); - - /// Returns a copy with `cover_uuid` set to SQL NULL, overriding any database + /// Returns a copy with `price` set to SQL NULL, overriding any database /// default. - BooksUpdate setCoverUuidToNull() => - BooksUpdate._({..._json, 'cover_uuid': null}); + BooksUpdate setPriceToNull() => BooksUpdate._({..._json, 'price': null}); /// Returns a copy with `published_on` set to SQL NULL, overriding any /// database default. BooksUpdate setPublishedOnToNull() => BooksUpdate._({..._json, 'published_on': null}); + /// Returns a copy with `rating` set to SQL NULL, overriding any database + /// default. + BooksUpdate setRatingToNull() => BooksUpdate._({..._json, 'rating': null}); + + /// Returns a copy with `tags` set to SQL NULL, overriding any database + /// default. + BooksUpdate setTagsToNull() => BooksUpdate._({..._json, 'tags': null}); + /// Returns a copy with `updated_at` set to SQL NULL, overriding any database /// default. BooksUpdate setUpdatedAtToNull() => @@ -402,19 +402,19 @@ class Books { /// Table definition for [PostgrestClient.table]. static const table = PostgrestTable('books', BooksRow.new); - static const id = TableColumn('id'); - static const title = TableColumn('title'); static const authorId = TableColumn('author_id'); - static const price = TableColumn('price'); - static const rating = TableColumn('rating'); + static const coverUuid = TableColumn('cover_uuid'); + static const createdAt = TableColumn('created_at'); + static const id = TableColumn('id'); static const inPrint = TableColumn('in_print'); + static const metadata = TableColumn('metadata'); static const mood = TableColumn('mood'); - static const tags = TableColumn>('tags'); static const pageCounts = TableColumn>('page_counts'); - static const metadata = TableColumn('metadata'); - static const coverUuid = TableColumn('cover_uuid'); + static const price = TableColumn('price'); static const publishedOn = TableColumn('published_on'); - static const createdAt = TableColumn('created_at'); + static const rating = TableColumn('rating'); + static const tags = TableColumn>('tags'); + static const title = TableColumn('title'); static const updatedAt = TableColumn('updated_at'); } From a31b132ca802348fb2726f5cb51a23e1fea6c0f5 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 10:24:36 +0200 Subject: [PATCH 21/31] docs(supabase_typegen): source the metadata document from postgrest-typegen, not postgres-meta --- packages/supabase_typegen/README.md | 11 ++++++----- packages/supabase_typegen/bin/supabase_typegen.dart | 2 +- .../lib/src/generator_metadata_parser.dart | 9 ++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 52e78ece2..4d4fb68e9 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -35,11 +35,12 @@ dart run supabase_typegen --input schema.json \ ``` 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, ordered with its -`sortGeneratorMetadata` pass. +[`@supabase/postgrest-typegen`](https://github.com/supabase/sdk/tree/main/packages/postgrest-typegen), +the same intermediate representation its TypeScript, Go, Swift, and Python +generators consume. The CLI produces it by running that package's +`introspect()` in-process against the database, ordered with its +`sortGeneratorMetadata` pass; until `--lang dart` and `--lang json` ship, +serializing that result by hand yields the identical document. ## Committing schema.json diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index eeb740e11..766d0d146 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -54,7 +54,7 @@ Future _run(List arguments) async { stdout ..writeln( 'Generates typed Supabase table definitions from the schema ' - 'metadata that postgres-meta emits.', + 'metadata that postgrest-typegen emits.', ) ..writeln() ..writeln('Usage: dart run supabase_typegen --input schema.json') diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index 8dae1474f..f2654e421 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -29,7 +29,7 @@ const _textFormats = { }; const _jsonFormats = {'json', 'jsonb'}; -/// Derives the [ColumnTypeKind] from the postgres-meta [format] of a column, +/// Derives the [ColumnTypeKind] from the metadata [format] of a column, /// for example `int8`, `timestamptz` or `_text` for a `text[]` array. This is /// the single place where type names are compared as strings; everything /// downstream works with the enum. @@ -55,10 +55,9 @@ ColumnTypeKind _elementTypeKind(String elementFormat, {required bool isEnum}) { return kind == ColumnTypeKind.enumType ? ColumnTypeKind.text : kind; } -/// Parses a `GeneratorMetadata` document, the introspection contract shared -/// by `@supabase/postgrest-typegen` and postgres-meta -/// (`supabase gen types --lang json`), into a [SchemaDescription] for -/// [schemaName]. +/// Parses a `GeneratorMetadata` document, the introspection contract of +/// `@supabase/postgrest-typegen` (`supabase gen types --lang json`), into a +/// [SchemaDescription] for [schemaName]. /// /// The document carries a `version` field, currently 1, and the /// semantically sorted collections produced by `sortGeneratorMetadata`: From 978d2cef843c42e28e3c00cf4c099cbcdbc18b80 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 10:27:53 +0200 Subject: [PATCH 22/31] docs(supabase_typegen): drop the --lang json references, the CLI hands the document over stdin --- packages/supabase_typegen/README.md | 9 +++++---- packages/supabase_typegen/bin/supabase_typegen.dart | 13 ++++++------- .../lib/src/generator_metadata_parser.dart | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 4d4fb68e9..5a78745aa 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -25,11 +25,10 @@ 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 +To run the package yourself, pass a `GeneratorMetadata` document 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 ``` @@ -39,8 +38,10 @@ The document is the `GeneratorMetadata` introspection contract of the same intermediate representation its TypeScript, Go, Swift, and Python generators consume. The CLI produces it by running that package's `introspect()` in-process against the database, ordered with its -`sortGeneratorMetadata` pass; until `--lang dart` and `--lang json` ship, -serializing that result by hand yields the identical document. +`sortGeneratorMetadata` pass, and hands it to this tool over stdin; until +`--lang dart` ships, serializing that result yourself yields the identical +document (`tool/regenerate_fixture.ts` in this package is a working +template). ## Committing schema.json diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index 766d0d146..1534208d7 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -9,9 +9,8 @@ final _argParser = ArgParser() 'input', abbr: 'i', help: - 'Path of the GeneratorMetadata document, or - to ' - 'read it from stdin. Produce it with ' - '`supabase gen types --lang json`.', + 'Path of the GeneratorMetadata document of ' + '@supabase/postgrest-typegen, or - to read it from stdin.', ) ..addOption( 'schema', @@ -66,8 +65,8 @@ Future _run(List arguments) async { 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`.', + 'document of @supabase/postgrest-typegen, or - to read it ' + 'from stdin.', ); return 64; } @@ -96,8 +95,8 @@ Future _run(List arguments) async { return 65; } on TypeError { stderr.writeln( - 'The document in $input is not a GeneratorMetadata document. ' - 'Produce it with `supabase gen types --lang json`.', + 'The document in $input is not a GeneratorMetadata document ' + 'of @supabase/postgrest-typegen.', ); return 65; } diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index f2654e421..9d05b0f41 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -56,8 +56,8 @@ ColumnTypeKind _elementTypeKind(String elementFormat, {required bool isEnum}) { } /// Parses a `GeneratorMetadata` document, the introspection contract of -/// `@supabase/postgrest-typegen` (`supabase gen types --lang json`), into a -/// [SchemaDescription] for [schemaName]. +/// `@supabase/postgrest-typegen`, into a [SchemaDescription] for +/// [schemaName]. /// /// The document carries a `version` field, currently 1, and the /// semantically sorted collections produced by `sortGeneratorMetadata`: From da595a8b1bff208a5624532c82c7828633a11df1 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 11:15:30 +0200 Subject: [PATCH 23/31] refactor(supabase_typegen): read the metadata document from stdin only The CLI hands the GeneratorMetadata document to the tool over stdin, so the --input flag and the schema.json file workflow are gone; the README describes only the real supabase gen types --lang dart flow. --- packages/supabase_typegen/README.md | 46 ++++--------------- .../bin/supabase_typegen.dart | 39 +++++----------- 2 files changed, 19 insertions(+), 66 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 5a78745aa..6411f4ec5 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -25,44 +25,14 @@ 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, pass a `GeneratorMetadata` document with -`--input` (a path, or `-` for stdin): - -```sh -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/sdk/tree/main/packages/postgrest-typegen), -the same intermediate representation its TypeScript, Go, Swift, and Python -generators consume. The CLI produces it by running that package's -`introspect()` in-process against the database, ordered with its -`sortGeneratorMetadata` pass, and hands it to this tool over stdin; until -`--lang dart` ships, serializing that result yourself yields the identical -document (`tool/regenerate_fixture.ts` in this package is a working -template). - -## 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. +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 SQL in your `supabase/` directory stays the single source of +truth: the CLI applies your migrations to the local database and the types +are generated from the result. Use `--schema` to generate for a schema other than `public`, and `--import` to change which library the generated file imports `PostgrestTable` and diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index 1534208d7..8a5da1ada 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -5,13 +5,6 @@ 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 of ' - '@supabase/postgrest-typegen, or - to read it from stdin.', - ) ..addOption( 'schema', defaultsTo: 'public', @@ -52,36 +45,26 @@ Future _run(List arguments) async { if (options.flag('help')) { stdout ..writeln( - 'Generates typed Supabase table definitions from the schema ' - 'metadata that postgrest-typegen emits.', + 'Generates typed Supabase table definitions from the ' + 'GeneratorMetadata document that postgrest-typegen emits, ' + 'read from stdin.', ) ..writeln() - ..writeln('Usage: dart run supabase_typegen --input schema.json') + ..writeln('Usage: dart run supabase_typegen < ') ..writeln(_argParser.usage); return 0; } - final input = options.option('input'); - if (input == null) { + if (stdin.hasTerminal) { stderr.writeln( - '--input is required: the path of a GeneratorMetadata ' - 'document of @supabase/postgrest-typegen, or - to read it ' - 'from stdin.', + 'Expected a GeneratorMetadata document of ' + '@supabase/postgrest-typegen on stdin. This tool is normally ' + 'invoked through `supabase gen types --lang dart`.', ); 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 contents = await utf8.decodeStream(stdin); final schemaName = options.option('schema')!; final SchemaDescription schema; @@ -91,11 +74,11 @@ Future _run(List arguments) async { schemaName: schemaName, ); } on FormatException catch (error) { - stderr.writeln('Could not parse $input: ${error.message}'); + stderr.writeln('Could not parse the document on stdin: ${error.message}'); return 65; } on TypeError { stderr.writeln( - 'The document in $input is not a GeneratorMetadata document ' + 'The document on stdin is not a GeneratorMetadata document ' 'of @supabase/postgrest-typegen.', ); return 65; From 52c4a420ec38d9eeac6803628357c46fef2802da Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 11:29:13 +0200 Subject: [PATCH 24/31] fix: address review feedback on source encoding, parser validation, and array elements --- packages/supabase_typegen/README.md | 5 +- .../bin/supabase_typegen.dart | 6 --- .../lib/src/dart_generator.dart | 20 +++++--- .../lib/src/generator_metadata_parser.dart | 24 +++++++-- .../test/dart_generator_test.dart | 47 ++++++++++++++++++ .../test/generator_metadata_parser_test.dart | 49 +++++++++++++++++++ 6 files changed, 134 insertions(+), 17 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 6411f4ec5..bf32ac670 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -64,8 +64,9 @@ await client.table(Books.table).insert( nullable columns, so nulling a `NOT NULL` column is a compile error. - Array elements are assumed non-null (`text[]` maps to `List`), matching the supabase-js type generator; arrays containing SQL NULL - elements throw when the element is read. Enum array columns degrade to - `List`. + elements throw when the element is read. Enum, date, and timestamp array + elements stay in their wire representation (`List`); 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. diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index 8a5da1ada..85944ea34 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -76,12 +76,6 @@ Future _run(List arguments) async { } on FormatException catch (error) { stderr.writeln('Could not parse the document on stdin: ${error.message}'); return 65; - } on TypeError { - stderr.writeln( - 'The document on stdin is not a GeneratorMetadata document ' - 'of @supabase/postgrest-typegen.', - ); - return 65; } final code = generateDartCode(schema, importUri: options.option('import')!); diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index e24b5d35c..7e0662a73 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -23,19 +23,25 @@ String generateDartCode( String importUri = 'package:postgrest/postgrest.dart', }) { final usesDateColumns = schema.tables.any( - (table) => table.columns.any( - (column) => column.typeKind == ColumnTypeKind.date, - ), + (table) => + table.columns.any((column) => column.typeKind == ColumnTypeKind.date), + ); + // Caller-provided values are encoded before they are written into source: + // a line terminator in the schema name would escape the comment, and a + // quote in the import URI would escape the import string. + final schemaComment = schema.schemaName.replaceAll( + RegExp(r'[\r\n\u2028\u2029]'), + ' ', ); final buffer = StringBuffer() ..writeln('// Generated by supabase_typegen. Do not edit by hand.') ..writeln('//') - ..writeln('// Source schema: ${schema.schemaName}') + ..writeln('// Source schema: $schemaComment') ..writeln() ..writeln('// The typed table access API is still experimental.') ..writeln('// ignore_for_file: experimental_member_use') ..writeln() - ..writeln("import '$importUri';") + ..writeln('import ${_stringLiteral(importUri)};') ..writeln(); final typeNames = _TypeNameRegistry(); @@ -363,11 +369,13 @@ String _elementDartType(ColumnTypeKind? elementTypeKind) => ColumnTypeKind.numeric => 'num', ColumnTypeKind.boolean => 'bool', ColumnTypeKind.text => 'String', + // Temporal and enum elements stay in their wire representation, a + // documented limitation of array columns. ColumnTypeKind.date || ColumnTypeKind.timestamp || ColumnTypeKind.timestampWithTimeZone || + ColumnTypeKind.enumType => 'String', ColumnTypeKind.json || - ColumnTypeKind.enumType || ColumnTypeKind.array || ColumnTypeKind.unknown || null => 'Object', diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index 9d05b0f41..20b589bac 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -75,6 +75,20 @@ ColumnTypeKind _elementTypeKind(String elementFormat, {required bool isEnum}) { SchemaDescription parseGeneratorMetadata( Map document, { String schemaName = 'public', +}) { + try { + return _parseGeneratorMetadata(document, schemaName: schemaName); + } on TypeError catch (error) { + throw FormatException( + 'Not a GeneratorMetadata document: a record does not have the ' + 'expected shape ($error).', + ); + } +} + +SchemaDescription _parseGeneratorMetadata( + Map document, { + required String schemaName, }) { if (document['tables'] is! List || document['columns'] is! List) { @@ -142,14 +156,18 @@ SchemaDescription parseGeneratorMetadata( final isArray = typeKind == ColumnTypeKind.array; var postgresFormat = format; - if (isEnum && !isArray) { + if (isEnum) { final enumDescription = _enumDescription( - format, + isArray ? format.substring(1) : format, column['type_schema'] as String, enumValues, enumTypes, ); - postgresFormat = enumDescription.qualifiedName; + // Array elements stay in their wire representation, but the enum the + // elements belong to is still emitted for manual conversion. + if (!isArray) { + postgresFormat = enumDescription.qualifiedName; + } enumsByQualifiedName.putIfAbsent( enumDescription.qualifiedName, () => enumDescription, diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index d794c8c6b..d05c15b58 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -147,6 +147,53 @@ void main() { expect(code, isNot(contains('BookCorrectionsInsert'))); }); + test('encodes hostile schema names and import URIs in the header', () { + final code = generateDartCode( + SchemaDescription( + schemaName: 'evil\nimport "dart:io";', + tables: const [], + enums: const [], + ), + importUri: "package:postgrest/postgrest.dart'; import 'dart:io", + ); + + expect(code, isNot(contains('evil\nimport'))); + expect(code, contains('// Source schema: evil import "dart:io";')); + expect( + code, + contains( + "import 'package:postgrest/postgrest.dart\\'; import \\'dart:io';", + ), + ); + }); + + test('temporal and enum array elements read as wire strings', () { + final code = generateDartCode( + SchemaDescription( + schemaName: 'public', + tables: const [ + TableDescription( + name: 'events', + columns: [ + ColumnDescription( + name: 'days', + postgresFormat: '_date', + typeKind: ColumnTypeKind.array, + elementTypeKind: ColumnTypeKind.date, + isRequired: true, + hasDefault: false, + isNullable: false, + ), + ], + ), + ], + enums: const [], + ), + ); + + expect(code, contains('List get days')); + }); + test('tables whose columns are all read-only get parameterless ' 'insert and update constructors', () { final table = TableDescription( diff --git a/packages/supabase_typegen/test/generator_metadata_parser_test.dart b/packages/supabase_typegen/test/generator_metadata_parser_test.dart index db149b20b..47d1c9c50 100644 --- a/packages/supabase_typegen/test/generator_metadata_parser_test.dart +++ b/packages/supabase_typegen/test/generator_metadata_parser_test.dart @@ -30,6 +30,55 @@ void main() { ); }); + test('rejects documents with malformed collection entries', () { + expect( + () => parseGeneratorMetadata({ + 'tables': [], + 'columns': [null], + }), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('GeneratorMetadata'), + ), + ), + ); + }); + + test('registers the enum of enum array columns', () { + final parsed = parseGeneratorMetadata({ + 'version': 1, + 'tables': [ + {'id': 1, 'schema': 'public', 'name': 'reviews', 'comment': null}, + ], + 'columns': [ + { + ..._column(tableId: 1, table: 'reviews', name: 'moods'), + 'data_type': 'ARRAY', + 'format': '_mood', + 'type_schema': 'public', + 'enums': ['happy', 'sad'], + }, + ], + 'types': [ + { + 'id': 10, + 'schema': 'public', + 'name': 'mood', + 'enums': ['happy', 'sad'], + }, + ], + }); + + final moods = parsed.tables.single.columns.single; + expect(moods.typeKind, ColumnTypeKind.array); + expect(moods.postgresFormat, '_mood'); + final enumDescription = parsed.enums.single; + expect(enumDescription.qualifiedName, 'public.mood'); + expect(enumDescription.values, ['happy', 'sad']); + }); + test('tolerates the version and primaryKeys fields of the document', () { expect(document['version'], 1); expect(document['primaryKeys'], isA>()); From a738dc0da60cba3cdf2a1575090b4fb3259f9f38 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 11:49:26 +0200 Subject: [PATCH 25/31] fix: address review feedback on stdin decoding, README database scoping, and line length --- packages/supabase_typegen/README.md | 8 +++++--- packages/supabase_typegen/bin/supabase_typegen.dart | 3 +-- packages/supabase_typegen/test/dart_generator_test.dart | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index bf32ac670..45cdb717d 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -30,9 +30,11 @@ Under the hood the CLI runs the introspection of 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 SQL in your `supabase/` directory stays the single source of -truth: the CLI applies your migrations to the local database and the types -are generated from the result. +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 diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index 85944ea34..a605409d5 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -64,11 +64,10 @@ Future _run(List arguments) async { return 64; } - final contents = await utf8.decodeStream(stdin); - final schemaName = options.option('schema')!; final SchemaDescription schema; try { + final contents = await utf8.decodeStream(stdin); schema = parseGeneratorMetadata( jsonDecode(contents) as Map, schemaName: schemaName, diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index d05c15b58..1546129a0 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -162,7 +162,8 @@ void main() { expect( code, contains( - "import 'package:postgrest/postgrest.dart\\'; import \\'dart:io';", + "import 'package:postgrest/postgrest.dart\\'; " + "import \\'dart:io';", ), ); }); From f17b2291a3e0c4c6c25cab2113cf5e76c04a583f Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 11:58:40 +0200 Subject: [PATCH 26/31] style: fix DCM findings on inferrable type arguments and equal switch cases --- packages/supabase_realtime/lib/src/realtime_channel.dart | 2 +- packages/supabase_typegen/lib/src/dart_generator.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/supabase_realtime/lib/src/realtime_channel.dart b/packages/supabase_realtime/lib/src/realtime_channel.dart index e65454b08..670ba05ce 100644 --- a/packages/supabase_realtime/lib/src/realtime_channel.dart +++ b/packages/supabase_realtime/lib/src/realtime_channel.dart @@ -138,7 +138,7 @@ class RealtimeChannel { Map get parameters => _deepUnmodifiableMap(_parameters); static Map _deepUnmodifiableMap(Map map) { - return Map.unmodifiable( + return Map.unmodifiable( map.map((key, value) => MapEntry(key, _deepUnmodifiable(value))), ); } diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index 7e0662a73..270e445c9 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -368,9 +368,9 @@ String _elementDartType(ColumnTypeKind? elementTypeKind) => ColumnTypeKind.floating => 'double', ColumnTypeKind.numeric => 'num', ColumnTypeKind.boolean => 'bool', - ColumnTypeKind.text => 'String', // Temporal and enum elements stay in their wire representation, a // documented limitation of array columns. + ColumnTypeKind.text || ColumnTypeKind.date || ColumnTypeKind.timestamp || ColumnTypeKind.timestampWithTimeZone || From 4fe916df878860892a1142469fe3b44c8fa0358a Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 12:10:17 +0200 Subject: [PATCH 27/31] fix: address review feedback on identifier shadowing, parser validation, and source encoding --- .../lib/src/dart_generator.dart | 44 ++++++- .../lib/src/generator_metadata_parser.dart | 7 ++ packages/supabase_typegen/pubspec.yaml | 1 + .../test/dart_generator_test.dart | 114 ++++++++++++++++++ .../test/generator_metadata_parser_test.dart | 29 +++++ 5 files changed, 191 insertions(+), 4 deletions(-) diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index 270e445c9..35a0ae219 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -73,8 +73,25 @@ String generateDartCode( } /// Hands out unique top level type names, suffixing `$` on collisions. +/// +/// Starts out with every identifier the generated source references +/// unqualified, so a schema object named like one of them (for example an +/// enum named `string`) cannot shadow it. class _TypeNameRegistry { - final _used = {}; + final _used = { + 'String', + 'Object', + 'Map', + 'MapEntry', + 'List', + 'DateTime', + 'int', + 'double', + 'num', + 'bool', + 'PostgrestTable', + 'TableColumn', + }; String claim(String name) { var candidate = name; @@ -127,7 +144,9 @@ void _writeEnum( ..writeln(' orElse: () => throw ArgumentError.value(') ..writeln(' wireName,') ..writeln(" 'wireName',") - ..writeln(" 'No $typeName value with this wire name',") + ..writeln( + ' ${_stringLiteral('No $typeName value with this wire name')},', + ) ..writeln(' ),') ..writeln(' );') ..writeln() @@ -399,6 +418,16 @@ String _readExpression(ColumnDescription column, _Binding binding) { nullable ? '($access as num?)?.toDouble()' : '($access as num).toDouble()', + // PostgREST encodes integral float4[]/float8[] elements as JSON + // integers, so floating elements convert through num like the scalars; + // a lazy cast would throw on access. + ColumnTypeKind.array + when column.elementTypeKind == ColumnTypeKind.floating => + nullable + ? '($access as List?)' + '?.map((element) => (element as num).toDouble()).toList()' + : '($access as List)' + '.map((element) => (element as num).toDouble()).toList()', ColumnTypeKind.array => nullable ? '($access as List?)?.cast()' @@ -481,13 +510,18 @@ void _writeDocComment( }) { if (comment == null) return; final width = 80 - indent.length - '/// '.length; - for (final line in comment.trim().split('\n')) { + for (final line in comment.trim().split(_lineTerminators)) { for (final wrapped in _wrap(line.trim(), width)) { buffer.writeln('$indent/// $wrapped'); } } } +/// Every Dart source line terminator: a comment line broken on LF alone +/// would let a CR or U+2028/U+2029 end the generated `///` comment early and +/// turn the remainder into source code. +final _lineTerminators = RegExp('\\r\\n?|[\\n\\u2028\\u2029]'); + /// Greedily wraps [text] into lines of at most [width] characters, keeping /// words longer than [width] on their own line. Iterable _wrap(String text, int width) sync* { @@ -511,6 +545,8 @@ String _stringLiteral(String value) { .replaceAll(r'$', r'\$') .replaceAll('\n', r'\n') .replaceAll('\r', r'\r') - .replaceAll('\t', r'\t'); + .replaceAll('\t', r'\t') + .replaceAll('\u2028', r'\u{2028}') + .replaceAll('\u2029', r'\u{2029}'); return "'$escaped'"; } diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index 20b589bac..6164fbb6f 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -249,6 +249,13 @@ Map<(String, String), ForeignKeyDescription> _foreignKeysByColumn( final columns = (relationship['columns'] as List).cast(); final referencedColumns = (relationship['referenced_columns'] as List).cast(); + if (columns.length != referencedColumns.length) { + throw FormatException( + 'Not a GeneratorMetadata document: the relationship ' + '"${relationship['foreign_key_name']}" pairs ${columns.length} ' + 'columns with ${referencedColumns.length} referenced columns.', + ); + } for (var i = 0; i < columns.length; i++) { foreignKeys.putIfAbsent( (table, columns[i]), diff --git a/packages/supabase_typegen/pubspec.yaml b/packages/supabase_typegen/pubspec.yaml index 3958d0899..2559bebda 100644 --- a/packages/supabase_typegen/pubspec.yaml +++ b/packages/supabase_typegen/pubspec.yaml @@ -1,6 +1,7 @@ name: supabase_typegen description: Command-line code generator that turns a Supabase database schema into typed Dart table definitions. version: 0.1.1 +publish_to: none homepage: 'https://supabase.com' repository: 'https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_typegen' issue_tracker: 'https://github.com/supabase/supabase-flutter/issues' diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index 1546129a0..848263b9d 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -168,6 +168,120 @@ void main() { ); }); + test('schema names shadowing core or imported types are suffixed', () { + final code = generateDartCode( + SchemaDescription( + schemaName: 'public', + tables: const [ + TableDescription( + name: 'postgrest_table', + columns: [ + ColumnDescription( + name: 'name', + postgresFormat: 'text', + typeKind: ColumnTypeKind.text, + isRequired: true, + hasDefault: false, + isNullable: false, + ), + ], + ), + ], + enums: const [ + EnumDescription(qualifiedName: 'public.string', values: ['a']), + ], + ), + ); + + expect(code, contains('enum String\$ ')); + expect(code, contains('final String wireName;')); + expect(code, isNot(contains('extension type const PostgrestTable._'))); + }); + + test('floating array elements convert through num', () { + final code = generateDartCode( + SchemaDescription( + schemaName: 'public', + tables: const [ + TableDescription( + name: 'metrics', + columns: [ + ColumnDescription( + name: 'samples', + postgresFormat: '_float8', + typeKind: ColumnTypeKind.array, + elementTypeKind: ColumnTypeKind.floating, + isRequired: true, + hasDefault: false, + isNullable: false, + ), + ], + ), + ], + enums: const [], + ), + ); + + expect(code, contains('List get samples')); + expect(code, contains('(element as num).toDouble()')); + }); + + test('database comments cannot escape generated doc comments', () { + final code = generateDartCode( + SchemaDescription( + schemaName: 'public', + tables: const [ + TableDescription( + name: 'books', + comment: 'first\rimport "dart:io";\u2028second', + columns: [ + ColumnDescription( + name: 'id', + postgresFormat: 'int8', + typeKind: ColumnTypeKind.integer, + isRequired: true, + hasDefault: false, + isNullable: false, + ), + ], + ), + ], + enums: const [], + ), + ); + + expect(code, contains('/// first')); + expect(code, contains('/// import "dart:io";')); + expect(code, contains('/// second')); + }); + + test('string literals escape unicode line separators', () { + final code = generateDartCode( + SchemaDescription( + schemaName: 'public', + tables: const [ + TableDescription( + name: 'books', + columns: [ + ColumnDescription( + name: 'line\u2028break', + postgresFormat: 'text', + typeKind: ColumnTypeKind.text, + isRequired: true, + hasDefault: false, + isNullable: false, + ), + ], + ), + ], + enums: const [], + ), + ); + + expect(code, contains(r"'line\u{2028}break'")); + expect(code, isNot(contains('line\u2028break'))); + }); + test('temporal and enum array elements read as wire strings', () { final code = generateDartCode( SchemaDescription( diff --git a/packages/supabase_typegen/test/generator_metadata_parser_test.dart b/packages/supabase_typegen/test/generator_metadata_parser_test.dart index 47d1c9c50..858db2031 100644 --- a/packages/supabase_typegen/test/generator_metadata_parser_test.dart +++ b/packages/supabase_typegen/test/generator_metadata_parser_test.dart @@ -46,6 +46,35 @@ void main() { ); }); + test('rejects relationships with mismatched column counts', () { + expect( + () => parseGeneratorMetadata({ + 'tables': [ + {'id': 1, 'schema': 'public', 'name': 'books', 'comment': null}, + ], + 'columns': [_column(tableId: 1, table: 'books', name: 'author_id')], + 'relationships': [ + { + 'foreign_key_name': 'books_author_id_fkey', + 'schema': 'public', + 'relation': 'books', + 'columns': ['author_id'], + 'referenced_schema': 'public', + 'referenced_relation': 'authors', + 'referenced_columns': [], + }, + ], + }), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('books_author_id_fkey'), + ), + ), + ); + }); + test('registers the enum of enum array columns', () { final parsed = parseGeneratorMetadata({ 'version': 1, From 438a14c4fad7af52542518ccd0bffd8ed3fbbf64 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 12:13:51 +0200 Subject: [PATCH 28/31] test(supabase_typegen): check in a hostile-schema golden so generated code is analyzed in CI --- .../test/dart_generator_test.dart | 16 ++ .../test/goldens/hostile_fixture.dart | 83 +++++++++ .../test/goldens/hostile_schema.dart | 173 ++++++++++++++++++ .../tool/regenerate_goldens.dart | 5 + 4 files changed, 277 insertions(+) create mode 100644 packages/supabase_typegen/test/goldens/hostile_fixture.dart create mode 100644 packages/supabase_typegen/test/goldens/hostile_schema.dart diff --git a/packages/supabase_typegen/test/dart_generator_test.dart b/packages/supabase_typegen/test/dart_generator_test.dart index 848263b9d..948f9f769 100644 --- a/packages/supabase_typegen/test/dart_generator_test.dart +++ b/packages/supabase_typegen/test/dart_generator_test.dart @@ -4,6 +4,8 @@ import 'dart:io'; import 'package:supabase_typegen/supabase_typegen.dart'; import 'package:test/test.dart'; +import 'goldens/hostile_fixture.dart'; + final _whitespace = RegExp(r'\s+'); /// Collapses whitespace so the comparison is stable across formatter @@ -36,6 +38,20 @@ void main() { ); }); + test('matches the hostile golden output', () { + final golden = File('test/goldens/hostile_schema.dart').readAsStringSync(); + + expect( + _normalize(generateDartCode(hostileSchema)), + _normalize(golden), + reason: + 'The generator output changed for the pathological schema. ' + 'Regenerate the golden with `dart run tool/regenerate_goldens.dart` ' + 'and review the diff; the checked-in golden is what proves the ' + 'generated code analyzes cleanly for hostile names.', + ); + }); + test('respects a custom import', () { final code = generateDartCode( schema, diff --git a/packages/supabase_typegen/test/goldens/hostile_fixture.dart b/packages/supabase_typegen/test/goldens/hostile_fixture.dart new file mode 100644 index 000000000..44e891865 --- /dev/null +++ b/packages/supabase_typegen/test/goldens/hostile_fixture.dart @@ -0,0 +1,83 @@ +import 'package:supabase_typegen/supabase_typegen.dart'; + +/// A schema built from valid but pathological database names: identifiers +/// that shadow core and imported types, comments and values carrying every +/// line terminator and Dart string metacharacter, and array element kinds +/// with dedicated conversions. +/// +/// The golden generated from it, `hostile_schema.dart`, is checked in so the +/// package's own `dart analyze` run proves the generated code stays valid, +/// not merely parseable, for schemas like these. +const SchemaDescription hostileSchema = SchemaDescription( + schemaName: 'evil\nimport "dart:io";', + tables: [ + TableDescription( + name: 'postgrest_table', + comment: 'first\rimport "dart:io";\u2028second \$interpolation', + columns: [ + ColumnDescription( + name: "quote'name
tail", + postgresFormat: 'text', + typeKind: ColumnTypeKind.text, + isRequired: true, + hasDefault: false, + isNullable: false, + comment: 'says "hi" \\ and \$more', + ), + ColumnDescription( + name: 'mood', + postgresFormat: 'public.string', + typeKind: ColumnTypeKind.enumType, + isRequired: false, + hasDefault: false, + isNullable: true, + ), + ColumnDescription( + name: 'samples', + postgresFormat: '_float8', + typeKind: ColumnTypeKind.array, + elementTypeKind: ColumnTypeKind.floating, + isRequired: true, + hasDefault: false, + isNullable: false, + ), + ColumnDescription( + name: 'days', + postgresFormat: '_date', + typeKind: ColumnTypeKind.array, + elementTypeKind: ColumnTypeKind.date, + isRequired: false, + hasDefault: false, + isNullable: true, + ), + ], + ), + TableDescription( + name: 'map', + columns: [ + ColumnDescription( + name: 'list', + postgresFormat: 'int8', + typeKind: ColumnTypeKind.integer, + isRequired: true, + hasDefault: false, + isNullable: false, + ), + ColumnDescription( + name: 'date_time', + postgresFormat: 'timestamptz', + typeKind: ColumnTypeKind.timestampWithTimeZone, + isRequired: false, + hasDefault: true, + isNullable: true, + ), + ], + ), + ], + enums: [ + EnumDescription( + qualifiedName: 'public.string', + values: ["it's \$a\u2028trap", 'plain'], + ), + ], +); diff --git a/packages/supabase_typegen/test/goldens/hostile_schema.dart b/packages/supabase_typegen/test/goldens/hostile_schema.dart new file mode 100644 index 000000000..cbf2684d9 --- /dev/null +++ b/packages/supabase_typegen/test/goldens/hostile_schema.dart @@ -0,0 +1,173 @@ +// Generated by supabase_typegen. Do not edit by hand. +// +// Source schema: evil import "dart:io"; + +// The typed table access API is still experimental. +// ignore_for_file: experimental_member_use + +import 'package:postgrest/postgrest.dart'; + +/// Postgres enum `public.string`. +enum String$ { + itSATrap('it\'s \$a\u{2028}trap'), + plain('plain'); + + const String$(this.wireName); + + /// The value as stored in the database. + final String wireName; + + /// Parses the database representation of the enum. + static String$ fromWire(String wireName) => values.firstWhere( + (value) => value.wireName == wireName, + orElse: () => throw ArgumentError.value( + wireName, + 'wireName', + 'No String\$ value with this wire name', + ), + ); + + @override + String toString() => wireName; +} + +/// A row of the `postgrest_table` table. +/// first +/// import "dart:io"; +/// second $interpolation +extension type const PostgrestTableRow(Map _json) + implements Map { + /// says "hi" \ and $more + String get quoteNameTail => _json['quote\'name\u{2029}tail'] as String; + String$? get mood => switch (_json['mood']) { + null => null, + final Object value => String$.fromWire(value as String), + }; + List get samples => (_json['samples'] as List) + .map((element) => (element as num).toDouble()) + .toList(); + List? get days => (_json['days'] as List?)?.cast(); +} + +/// Values for inserting a row into `postgrest_table`. Columns that are +/// nullable, identity, or covered by a database default are optional; passing +/// `null` omits the column so the database default applies. Columns the +/// database always generates itself are left out entirely. Use the `set…ToNull` +/// methods to insert SQL NULL explicitly. +extension type const PostgrestTableInsert._(Map _json) + implements Map { + PostgrestTableInsert({ + required String quoteNameTail, + String$? mood, + required List samples, + List? days, + }) : this._({ + 'quote\'name\u{2029}tail': quoteNameTail, + 'mood': ?mood?.wireName, + 'samples': samples, + 'days': ?days, + }); + + /// Returns a copy with `mood` set to SQL NULL, overriding any database + /// default. + PostgrestTableInsert setMoodToNull() => + PostgrestTableInsert._({..._json, 'mood': null}); + + /// Returns a copy with `days` set to SQL NULL, overriding any database + /// default. + PostgrestTableInsert setDaysToNull() => + PostgrestTableInsert._({..._json, 'days': null}); +} + +/// Values for updating rows of `postgrest_table`. All columns are optional; +/// passing `null` omits the column, leaving it unchanged. Use the `set…ToNull` +/// methods to write SQL NULL explicitly. +extension type const PostgrestTableUpdate._(Map _json) + implements Map { + PostgrestTableUpdate({ + String? quoteNameTail, + String$? mood, + List? samples, + List? days, + }) : this._({ + 'quote\'name\u{2029}tail': ?quoteNameTail, + 'mood': ?mood?.wireName, + 'samples': ?samples, + 'days': ?days, + }); + + /// Returns a copy with `mood` set to SQL NULL, overriding any database + /// default. + PostgrestTableUpdate setMoodToNull() => + PostgrestTableUpdate._({..._json, 'mood': null}); + + /// Returns a copy with `days` set to SQL NULL, overriding any database + /// default. + PostgrestTableUpdate setDaysToNull() => + PostgrestTableUpdate._({..._json, 'days': null}); +} + +/// Typed access to the `postgrest_table` table. +class PostgrestTable$ { + const PostgrestTable$._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable('postgrest_table', PostgrestTableRow.new); + + static const quoteNameTail = TableColumn('quote\'name\u{2029}tail'); + static const mood = TableColumn('mood'); + static const samples = TableColumn>('samples'); + static const days = TableColumn>('days'); +} + +/// A row of the `map` table. +extension type const MapRow(Map _json) + implements Map { + int get list => _json['list'] as int; + DateTime? get dateTime => switch (_json['date_time']) { + null => null, + final Object value => DateTime.parse(value as String), + }; +} + +/// Values for inserting a row into `map`. Columns that are nullable, identity, +/// or covered by a database default are optional; passing `null` omits the +/// column so the database default applies. Columns the database always +/// generates itself are left out entirely. Use the `set…ToNull` methods to +/// insert SQL NULL explicitly. +extension type const MapInsert._(Map _json) + implements Map { + MapInsert({required int list, DateTime? dateTime}) + : this._({'list': list, 'date_time': ?dateTime?.toUtc().toIso8601String()}); + + /// Returns a copy with `date_time` set to SQL NULL, overriding any database + /// default. + MapInsert setDateTimeToNull() => MapInsert._({..._json, 'date_time': null}); +} + +/// Values for updating rows of `map`. All columns are optional; passing `null` +/// omits the column, leaving it unchanged. Use the `set…ToNull` methods to +/// write SQL NULL explicitly. +extension type const MapUpdate._(Map _json) + implements Map { + MapUpdate({int? list, DateTime? dateTime}) + : this._({ + 'list': ?list, + 'date_time': ?dateTime?.toUtc().toIso8601String(), + }); + + /// Returns a copy with `date_time` set to SQL NULL, overriding any database + /// default. + MapUpdate setDateTimeToNull() => MapUpdate._({..._json, 'date_time': null}); +} + +/// Typed access to the `map` table. +class Map$ { + const Map$._(); + + /// Table definition for [PostgrestClient.table]. + static const table = PostgrestTable('map', MapRow.new); + + static const list = TableColumn('list'); + static const dateTime = TableColumn('date_time'); +} diff --git a/packages/supabase_typegen/tool/regenerate_goldens.dart b/packages/supabase_typegen/tool/regenerate_goldens.dart index aa1f77cbf..01d579cce 100644 --- a/packages/supabase_typegen/tool/regenerate_goldens.dart +++ b/packages/supabase_typegen/tool/regenerate_goldens.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:supabase_typegen/supabase_typegen.dart'; +import '../test/goldens/hostile_fixture.dart'; + /// Regenerates the golden files under `test/goldens` from the fixtures. /// /// Run from the package root with `dart run tool/regenerate_goldens.dart`. @@ -16,4 +18,7 @@ void main() { File( 'test/goldens/supabase_schema.dart', ).writeAsStringSync(generateDartCode(schema)); + File( + 'test/goldens/hostile_schema.dart', + ).writeAsStringSync(generateDartCode(hostileSchema)); } From dd642ea43d6e888bd58cacca797e265455433bdb Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 12:23:58 +0200 Subject: [PATCH 29/31] style: fix DCM findings and keep code-shaped text out of the hostile golden comments --- packages/supabase_typegen/lib/src/dart_generator.dart | 8 ++++---- .../supabase_typegen/test/goldens/hostile_fixture.dart | 4 ++-- .../supabase_typegen/test/goldens/hostile_schema.dart | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/supabase_typegen/lib/src/dart_generator.dart b/packages/supabase_typegen/lib/src/dart_generator.dart index 35a0ae219..7cd06731a 100644 --- a/packages/supabase_typegen/lib/src/dart_generator.dart +++ b/packages/supabase_typegen/lib/src/dart_generator.dart @@ -78,7 +78,7 @@ String generateDartCode( /// unqualified, so a schema object named like one of them (for example an /// enum named `string`) cannot shadow it. class _TypeNameRegistry { - final _used = { + final _used = { 'String', 'Object', 'Map', @@ -418,9 +418,9 @@ String _readExpression(ColumnDescription column, _Binding binding) { nullable ? '($access as num?)?.toDouble()' : '($access as num).toDouble()', - // PostgREST encodes integral float4[]/float8[] elements as JSON - // integers, so floating elements convert through num like the scalars; - // a lazy cast would throw on access. + // PostgREST encodes integral elements of floating point arrays as JSON + // integers, so floating elements convert through num like the scalars + // instead of a lazy cast that would throw on access. ColumnTypeKind.array when column.elementTypeKind == ColumnTypeKind.floating => nullable diff --git a/packages/supabase_typegen/test/goldens/hostile_fixture.dart b/packages/supabase_typegen/test/goldens/hostile_fixture.dart index 44e891865..898a8710d 100644 --- a/packages/supabase_typegen/test/goldens/hostile_fixture.dart +++ b/packages/supabase_typegen/test/goldens/hostile_fixture.dart @@ -9,11 +9,11 @@ import 'package:supabase_typegen/supabase_typegen.dart'; /// package's own `dart analyze` run proves the generated code stays valid, /// not merely parseable, for schemas like these. const SchemaDescription hostileSchema = SchemaDescription( - schemaName: 'evil\nimport "dart:io";', + schemaName: 'evil\nmultiline "schema" name', tables: [ TableDescription( name: 'postgrest_table', - comment: 'first\rimport "dart:io";\u2028second \$interpolation', + comment: 'first\rsecond\u2028third \$interpolation "quoted"', columns: [ ColumnDescription( name: "quote'name
tail", diff --git a/packages/supabase_typegen/test/goldens/hostile_schema.dart b/packages/supabase_typegen/test/goldens/hostile_schema.dart index cbf2684d9..0c3a5ad3e 100644 --- a/packages/supabase_typegen/test/goldens/hostile_schema.dart +++ b/packages/supabase_typegen/test/goldens/hostile_schema.dart @@ -1,6 +1,6 @@ // Generated by supabase_typegen. Do not edit by hand. // -// Source schema: evil import "dart:io"; +// Source schema: evil multiline "schema" name // The typed table access API is still experimental. // ignore_for_file: experimental_member_use @@ -33,8 +33,8 @@ enum String$ { /// A row of the `postgrest_table` table. /// first -/// import "dart:io"; -/// second $interpolation +/// second +/// third $interpolation "quoted" extension type const PostgrestTableRow(Map _json) implements Map { /// says "hi" \ and $more From 384b87a849c1d7c3b16da92fad9d5b8682f08c28 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 12:32:02 +0200 Subject: [PATCH 30/31] ci: refresh the apt package index before the cached package install --- .github/workflows/build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8dd0e8a3d..6ae96da74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 From 2907420a8f423806dc1fa4cb2e814a284b5ed5b0 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 1 Sep 2026 12:38:36 +0200 Subject: [PATCH 31/31] docs: note the git dependency source until supabase_typegen is published --- packages/supabase_typegen/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 45cdb717d..6f92be78d 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -16,7 +16,9 @@ For every table the generator emits: 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: +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