diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2125aa459..09fc04c74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -142,7 +142,7 @@ jobs: run: dart analyze --fatal-infos . - name: Setup Supabase CLI - if: ${{ matrix.package.backend || matrix.package.name == 'supabase_typegen' }} + if: ${{ matrix.package.backend }} uses: supabase/setup-cli@45a513f8c64c0bc8e0e3dfe572b5c95be85f6359 # v3.0.1 with: version: 2.120.0 @@ -191,29 +191,8 @@ jobs: docker save "$image" -o "/tmp/supabase-images/${file}.tar" done - # Port 14432 sits next to the test stack's 14420 to 14429 block, below - # Linux's default ephemeral port range, so an outgoing connection the - # runner opens while the container starts cannot claim it first. - - name: Start Postgres for the typegen parity test - if: ${{ matrix.package.name == 'supabase_typegen' }} - run: | - docker run --detach --name supabase_typegen_parity \ - --env POSTGRES_PASSWORD=postgres --publish 14432:5432 postgres:15 \ - -c autovacuum=off - for _ in $(seq 1 30); do - if docker exec supabase_typegen_parity pg_isready --host localhost --username postgres; then - exit 0 - fi - sleep 1 - done - echo "Postgres did not become ready in time." >&2 - docker logs supabase_typegen_parity - exit 1 - - name: Run tests working-directory: packages/${{ matrix.package.name }} - env: - SUPABASE_TYPEGEN_PARITY_DATABASE_URL: ${{ matrix.package.name == 'supabase_typegen' && 'postgresql://postgres:postgres@localhost:14432/postgres?sslmode=disable' || '' }} run: | CONCURRENCY="" if [ "${{ matrix.package.concurrency }}" = "1" ]; then diff --git a/.github/workflows/typegen-drift.yml b/.github/workflows/typegen-drift.yml deleted file mode 100644 index 8b5b738a1..000000000 --- a/.github/workflows/typegen-drift.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Typegen introspection drift - -on: - pull_request: - paths: - - 'packages/supabase_typegen/**' - - '.github/workflows/typegen-drift.yml' - schedule: - # Mondays at 06:00 UTC, comparing against supabase/sdk main so a change to - # the upstream introspection SQL gets noticed. - - cron: '0 6 * * 1' - workflow_dispatch: - -permissions: - contents: read - -jobs: - drift: - name: Compare the vendored SQL with postgrest-typegen - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # The drift check executes TypeScript fetched from supabase/sdk. - persist-credentials: false - - - name: Setup Flutter - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 - with: - channel: 'stable' - cache: true - - - name: Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - - - name: Resolve workspace dependencies - run: dart pub get - - - name: Check the introspection SQL against the pinned revision - if: ${{ github.event_name != 'schedule' }} - working-directory: packages/supabase_typegen - run: bun tool/check_introspection_drift.ts - - - name: Check the introspection SQL against supabase/sdk main - if: ${{ github.event_name == 'schedule' }} - working-directory: packages/supabase_typegen - run: bun tool/check_introspection_drift.ts --latest diff --git a/MIGRATION.md b/MIGRATION.md index b6dccbf61..d67d51f09 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2367,20 +2367,17 @@ create table books ( ### Generate the table definitions -Add `supabase_typegen` as a dev dependency and run it against your database. With `--local` it reads -the database of the running local stack, which the Supabase CLI builds from the migrations in your -`supabase/` directory: +Add `supabase_typegen` as a dev dependency and generate the types with the Supabase CLI. With +`--local` it reads the database of the running local stack, which the CLI builds from the +migrations in your `supabase/` directory: ```sh dart pub add dev:supabase_typegen -dart run supabase_typegen --local +supabase gen types --lang dart --local > lib/supabase_schema.g.dart ``` -This writes `lib/supabase_schema.g.dart`. `--linked`, `--project-ref` and `--db-url` generate from -a hosted project instead; see the -[`supabase_typegen` README](packages/supabase_typegen/README.md) for every option. Once the Supabase -CLI ships Dart support, `supabase gen types --lang dart --local > lib/supabase_schema.g.dart` -replaces the direct connection. +`--linked` and `--project-id` generate from a hosted project instead; see the +[`supabase_typegen` README](packages/supabase_typegen/README.md) for details. For every table the file holds a row type (`BooksRow`), an insert type (`BooksInsert`), an update type (`BooksUpdate`), and a class (`Books`) with the table definition and one constant per column diff --git a/packages/supabase_typegen/README.md b/packages/supabase_typegen/README.md index 5afcd0b10..7b36cde3a 100644 --- a/packages/supabase_typegen/README.md +++ b/packages/supabase_typegen/README.md @@ -68,93 +68,45 @@ its `condition` enum `InventoryCondition`. ## Usage -Add `supabase_typegen` as a dev dependency of your project and run it with -`dart run supabase_typegen`, or install it globally with -`dart install supabase_typegen` and run it as `supabase_typegen`. The tool -connects through the -[Supabase CLI](https://supabase.com/docs/guides/cli/getting-started), so have -it installed and, for hosted projects, logged in with `supabase login`. Then -point it at your database: +Add `supabase_typegen` as a dev dependency of your project and generate the +types with the +[Supabase CLI](https://supabase.com/docs/guides/cli/getting-started), from +the project directory: ```sh +dart pub add dev:supabase_typegen + # The database of the running local Supabase stack (`supabase start`). -dart run supabase_typegen --local +supabase gen types --lang dart --local > lib/supabase_schema.g.dart # The project linked with `supabase link`, or any project by ref. -dart run supabase_typegen --linked -dart run supabase_typegen --project-ref abcdefghijklmnopqrst - -# Any Postgres database. -dart run supabase_typegen --db-url 'postgresql://postgres:…@db.…supabase.co:5432/postgres' +supabase gen types --lang dart --linked > lib/supabase_schema.g.dart +supabase gen types --lang dart --project-id abcdefghijklmnopqrst > lib/supabase_schema.g.dart ``` -All of them write `lib/supabase_schema.g.dart`; pass `--output` to change the -path or `--output -` to print the code. The types reflect the current state -of the 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 this tool generates from the result, while the other -modes generate from whatever that database currently contains. The code is -formatted with the `dart format` of the SDK running the tool, for the language -version of the project it is written into (the lower bound of the -`environment.sdk` constraint of the nearest `pubspec.yaml`), so `dart format` -in that project leaves it unchanged; with `--output -` the project of the -current directory decides. - -`--linked` and `--project-ref` reach the database through the Management API -with your `supabase login` credentials, so no database password is needed; -`--project-ref` needs a CLI that accepts it on `db query` (2.116 or newer), -older ones want `supabase link --project-ref ` followed by `--linked`. -`--db-url` is handed to the CLI as is; it requires TLS unless the connection -string says `sslmode=disable`. - -The schemas are chosen the way `supabase gen types` chooses them: `public` -plus the `api.schemas` of the project's `supabase/config.toml`, found under -`SUPABASE_WORKDIR` or by walking up from the working directory, so the -exposed schemas of the project are generated. Pass `--schema` to name the schemas yourself, repeated -or comma separated (`--schema public,inventory`). Only schemas exposed through -the Data API can be queried at runtime. The generated file imports +The CLI introspects the database into the `GeneratorMetadata` document of +[`@supabase/postgrest-typegen`](https://github.com/supabase/sdk/tree/main/packages/postgrest-typegen), +the intermediate representation its TypeScript, Go, Swift and Python +generators consume, and pipes it into this package, which generates the Dart +code from it. Pass `--schema` to choose the schema to generate. Only schemas +exposed through the Data API can be queried at runtime. 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. + +The code is formatted with the `dart format` of the SDK running the tool, for +the language version of the project it is written into (the lower bound of +the `environment.sdk` constraint of the nearest `pubspec.yaml`), so +`dart format` in that project leaves it unchanged. The generated file imports `PostgrestTable` and `PostgrestColumn` from the library of `supabase_flutter`, `supabase` or `postgrest`, preferred in that order, that the `pubspec.yaml` -nearest to the output lists under `dependencies`, and from `package:postgrest` -when it lists none of them. Use `--import` to import them from another library. - -### How it works - -The tool introspects the database with a Dart port of the introspection of -[`@supabase/postgrest-typegen`](https://github.com/supabase/sdk/tree/main/packages/postgrest-typegen) -into the `GeneratorMetadata` intermediate representation its TypeScript, Go, -Swift, and Python generators consume, ordered with `sortGeneratorMetadata`, -and generates the Dart code from that document. The queries run through -`supabase db query`, so the CLI resolves and authenticates the connection. -The port is pinned to a revision of the TypeScript package and produces a -document with the same records; `--dump-metadata` prints it instead of the -generated code, which helps when reporting a generator issue. - -The built-in introspection, and with it the `--local`, `--linked`, -`--project-ref`, `--db-url` and `--dump-metadata` options, is a stopgap. It -will be removed once the Supabase CLI ships Dart support for -`supabase gen types`, which then becomes the only way to run this tool; see -the next section. +lists under `dependencies`, and from `package:postgrest` when it lists none of +them. 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. -### Through the Supabase CLI - -Once `supabase gen types` ships a Dart language, the CLI will run the same -introspection in-process and hand the document to this tool over stdin. The -direct connection modes above will be removed in the release that follows, so -prefer the CLI as soon as it is available: - -```sh -supabase gen types --lang dart --local > lib/supabase_schema.g.dart -``` - -Reading the document from stdin is what the tool does when no connection -option is given, so that path already works with a hand-built document. - ## Generated code in action ```dart diff --git a/packages/supabase_typegen/bin/supabase_typegen.dart b/packages/supabase_typegen/bin/supabase_typegen.dart index a123cb954..9cf7e3fdb 100644 --- a/packages/supabase_typegen/bin/supabase_typegen.dart +++ b/packages/supabase_typegen/bin/supabase_typegen.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:supabase_typegen/introspection.dart'; import 'package:supabase_typegen/supabase_typegen.dart'; final _argParser = ArgParser() @@ -12,9 +11,8 @@ final _argParser = ArgParser() valueHelp: 'name', help: 'A database schema to generate types for; repeat the option or ' - 'separate names with commas for several. Defaults to public and the ' - 'api.schemas of the nearest supabase/config.toml, like ' - '`supabase gen types`.', + 'separate names with commas for several. Defaults to every schema ' + 'of the document.', ) ..addOption( 'output', @@ -32,37 +30,6 @@ final _argParser = ArgParser() 'pubspec.yaml nearest to the output lists under dependencies, or to ' 'package:postgrest/postgrest.dart when it lists none of them.', ) - ..addFlag( - 'local', - negatable: false, - help: 'Introspect the database of the running local Supabase stack.', - ) - ..addFlag( - 'linked', - negatable: false, - help: - 'Introspect the database of the project linked with `supabase link`, ' - 'through the Management API.', - ) - ..addOption( - 'project-ref', - valueHelp: 'ref', - help: - 'Introspect the database of this Supabase project instead of the ' - 'linked one; implies --linked.', - ) - ..addOption( - 'db-url', - valueHelp: 'postgresql://…', - help: 'Introspect the Postgres database at this connection string.', - ) - ..addFlag( - 'dump-metadata', - negatable: false, - help: - 'Write the introspected GeneratorMetadata document as JSON instead ' - 'of generated Dart code. Defaults --output to stdout.', - ) ..addFlag('help', abbr: 'h', negatable: false, help: 'Show this usage.'); Future main(List arguments) async { @@ -85,111 +52,52 @@ Future _run(List arguments) async { if (options.flag('help')) { stdout ..writeln( - 'Generates typed Supabase table definitions from a database, ' - 'either by introspecting it through the Supabase CLI (--local, ' - '--linked, --project-ref or --db-url) or from the GeneratorMetadata ' - 'document that postgrest-typegen emits, read from stdin.', + 'Generates typed Supabase table definitions from the ' + 'GeneratorMetadata document that `supabase gen types --lang dart` ' + 'pipes in on stdin.', ) ..writeln() - ..writeln('Usage: dart run supabase_typegen --local') - ..writeln(' dart run supabase_typegen --linked') - ..writeln(' dart run supabase_typegen --project-ref ') - ..writeln(' dart run supabase_typegen --db-url ') - ..writeln(' dart run supabase_typegen < ') + ..writeln('Usage: supabase gen types --lang dart --local') ..writeln(_argParser.usage); return 0; } - final namedSchemas = options.multiOption('schema'); - final dumpMetadata = options.flag('dump-metadata'); - + if (stdin.hasTerminal) { + stderr.writeln( + 'Expected a GeneratorMetadata document on stdin. Run ' + '`supabase gen types --lang dart` to generate types.', + ); + return 64; + } final Map document; - // The schemas to describe: the named ones, or for a database the default - // set; a document on stdin already holds the schemas its producer chose. - var requestedSchemas = namedSchemas; - switch (_resolveTarget(options)) { - case _Failure(:final message): - stderr.writeln(message); - return 64; - case _Introspect(:final target): - final List schemaNames; - try { - schemaNames = namedSchemas.isEmpty ? defaultSchemas() : namedSchemas; - } on FormatException catch (error) { - stderr.writeln(error.message); - return 78; - } - requestedSchemas = schemaNames; - try { - document = await introspectDatabase( - target, - includedSchemas: schemaNames, - ); - } on SupabaseCliException catch (error) { - stderr.writeln(error.message); - return 69; - } - case _Stdin(): - if (dumpMetadata) { - stderr.writeln( - '--dump-metadata requires --local, --linked, --project-ref or ' - '--db-url.', - ); - return 64; - } - if (stdin.hasTerminal) { - stderr.writeln( - 'Expected a GeneratorMetadata document of ' - '@supabase/postgrest-typegen on stdin. Pass --local, --linked, ' - '--project-ref or --db-url to introspect a database instead.', - ); - return 64; - } - try { - final contents = await utf8.decodeStream(stdin); - final decoded = jsonDecode(contents); - if (decoded is! Map) { - throw const FormatException( - 'expected a JSON object with the GeneratorMetadata shape.', - ); - } - document = decoded; - } on FormatException catch (error) { - stderr.writeln( - 'Could not parse the document on stdin: ${error.message}', - ); - return 65; - } + try { + final contents = await utf8.decodeStream(stdin); + final decoded = jsonDecode(contents); + if (decoded is! Map) { + throw const FormatException( + 'expected a JSON object with the GeneratorMetadata shape.', + ); + } + document = decoded; + } on FormatException catch (error) { + stderr.writeln('Could not parse the document on stdin: ${error.message}'); + return 65; } - final output = options.wasParsed('output') || !dumpMetadata - ? options.option('output')! - : '-'; + final namedSchemas = options.multiOption('schema'); + final output = options.option('output')!; final summarySink = output == '-' ? stderr : stdout; - if (dumpMetadata) { - final generatedInto = _write( - output, - '${const JsonEncoder.withIndent(' ').convert(document)}\n', - ); - summarySink.writeln( - 'Wrote the GeneratorMetadata document to $generatedInto.', - ); - return 0; - } - final DatabaseDescription database; try { database = parseGeneratorMetadata( document, - schemaNames: requestedSchemas.isEmpty ? null : requestedSchemas, + schemaNames: namedSchemas.isEmpty ? null : namedSchemas, ); } on FormatException catch (error) { stderr.writeln('Could not parse the document: ${error.message}'); return 65; } - // Only schemas named on the command line are worth a warning; a default - // taken from the configuration may well be absent from the database. for (final schema in namedSchemas) { if (!database.schemaNames.contains(schema)) { stderr.writeln('The database has no schema "$schema".'); @@ -269,39 +177,3 @@ String _write(String output, String contents) { outputFile.writeAsStringSync(contents); return outputFile.path; } - -sealed class _MetadataSource {} - -final class _Stdin implements _MetadataSource { - const _Stdin(); -} - -final class _Introspect implements _MetadataSource { - const _Introspect(this.target); - - final DatabaseTarget target; -} - -final class _Failure implements _MetadataSource { - const _Failure(this.message); - - final String message; -} - -_MetadataSource _resolveTarget(ArgResults options) { - final databaseUrl = options.option('db-url'); - final projectRef = options.option('project-ref'); - final targets = [ - if (options.flag('local')) const LocalDatabase(), - if (options.flag('linked') || projectRef != null) - LinkedProject(projectRef: projectRef), - if (databaseUrl != null) DatabaseUrl(databaseUrl), - ]; - return switch (targets) { - [] => const _Stdin(), - [final target] => _Introspect(target), - _ => const _Failure( - 'Pass only one of --local, --linked (or --project-ref) and --db-url.', - ), - }; -} diff --git a/packages/supabase_typegen/lib/introspection.dart b/packages/supabase_typegen/lib/introspection.dart deleted file mode 100644 index 6a4651f3d..000000000 --- a/packages/supabase_typegen/lib/introspection.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// Introspects a Postgres database into the `GeneratorMetadata` document of -/// `@supabase/postgrest-typegen` through `supabase db query`, so types can be -/// generated before `supabase gen types` produces the document itself. -/// -/// This is a port of the introspection of that package, pinned to -/// [postgrestTypegenRevision]. It exists until `supabase gen types` ships a -/// Dart path and can be removed as a whole once it does. -library; - -export 'src/introspection/introspect.dart'; -export 'src/introspection/queryable.dart'; -export 'src/introspection/sort.dart'; -export 'src/introspection/supabase_config_io.dart'; -export 'src/introspection/supabase_cli_io.dart'; diff --git a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart index 8124425e7..d104714ad 100644 --- a/packages/supabase_typegen/lib/src/generator_metadata_parser.dart +++ b/packages/supabase_typegen/lib/src/generator_metadata_parser.dart @@ -1,4 +1,3 @@ -import 'rpc_functions.dart'; import 'schema_description.dart'; const _integerFormats = {'int2', 'int4', 'int8', 'oid'}; @@ -75,9 +74,8 @@ ColumnTypeKind _typeKind(String format, {required bool isEnum}) { /// only the computed fields and computed relationships are read: functions /// whose single input argument is the row type of a described relation of /// the same schema. The functions callable through `/rpc` come from the -/// optional `rpcFunctions` collection, resolved with [resolveRpcFunctions] -/// when the document predates it. Fields the generator does not need are -/// ignored. +/// `rpcFunctions` collection; a document without it describes none. Fields +/// the generator does not need 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 @@ -621,8 +619,7 @@ ValueTypeDescription _valueType( } /// The functions of [schemas] callable through `/rpc`, from the -/// `rpcFunctions` collection of [document], or resolved from its `functions` -/// when the document predates the collection. +/// `rpcFunctions` collection of [document]. List _functions( Map document, Set schemas, { @@ -632,10 +629,8 @@ List _functions( required Map relationsById, required Set<(String, String)> generatedRelations, }) { - final rpcFunctions = - (document['rpcFunctions'] as List?) - ?.cast>() ?? - resolveRpcFunctions(document); + final rpcFunctions = (document['rpcFunctions'] as List? ?? const []) + .cast>(); ValueTypeDescription valueType(Map reference) => _valueType( typesById[reference['type_id'] as int], diff --git a/packages/supabase_typegen/lib/src/introspection/collation.dart b/packages/supabase_typegen/lib/src/introspection/collation.dart deleted file mode 100644 index 8fa28bb30..000000000 --- a/packages/supabase_typegen/lib/src/introspection/collation.dart +++ /dev/null @@ -1,52 +0,0 @@ -/// Compares [a] and [b] the way JavaScript's `String.prototype.localeCompare` -/// orders them under the ICU root collation, which is what -/// `sortGeneratorMetadata` of `@supabase/postgrest-typegen` sorts with. -/// -/// Printable ASCII is ordered exactly like ICU: space and punctuation first, -/// then digits, then letters, with upper and lower case of the same letter -/// adjacent and compared only when the strings are otherwise equal, lower -/// case first. Every other code unit sorts after ASCII by code unit, which -/// deviates from ICU for accented and non-Latin names; Postgres identifiers -/// are overwhelmingly ASCII, and only the order of the emitted collections -/// depends on it. -int localeCompare(String a, String b) { - final length = a.length < b.length ? a.length : b.length; - for (var i = 0; i < length; i++) { - final difference = _primary(a.codeUnitAt(i)) - _primary(b.codeUnitAt(i)); - if (difference != 0) return difference.sign; - } - if (a.length != b.length) return a.length < b.length ? -1 : 1; - for (var i = 0; i < length; i++) { - final difference = _tertiary(a.codeUnitAt(i)) - _tertiary(b.codeUnitAt(i)); - if (difference != 0) return difference.sign; - } - return 0; -} - -/// Printable ASCII in ICU root collation order, one primary weight per -/// character except that the two cases of a letter share one. -const _asciiOrder = - " _-,;:!?.'\"()[]{}@*/\\&#%`^+<=>|~\$0123456789" - 'aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ'; - -final List _asciiPrimary = () { - final weights = List.filled(128, 0); - var weight = 1; - for (var i = 0; i < _asciiOrder.length; i++) { - final codeUnit = _asciiOrder.codeUnitAt(i); - weights[codeUnit] = weight; - final isLowerCaseLetter = codeUnit >= 0x61 && codeUnit <= 0x7A; - if (!isLowerCaseLetter) weight++; - } - return weights; -}(); - -int _primary(int codeUnit) { - if (codeUnit < 128) { - final weight = _asciiPrimary[codeUnit]; - if (weight != 0) return weight; - } - return 128 + codeUnit; -} - -int _tertiary(int codeUnit) => codeUnit >= 0x41 && codeUnit <= 0x5A ? 1 : 0; diff --git a/packages/supabase_typegen/lib/src/introspection/introspect.dart b/packages/supabase_typegen/lib/src/introspection/introspect.dart deleted file mode 100644 index 6c59d74b6..000000000 --- a/packages/supabase_typegen/lib/src/introspection/introspect.dart +++ /dev/null @@ -1,124 +0,0 @@ -import '../rpc_functions.dart'; -import 'queryable.dart'; -import 'relationships.dart'; -import 'sort.dart'; -import 'sql/columns_sql.dart'; -import 'sql/foreign_tables_sql.dart'; -import 'sql/functions_sql.dart'; -import 'sql/helpers.dart'; -import 'sql/materialized_views_sql.dart'; -import 'sql/primary_keys_sql.dart'; -import 'sql/schemas_sql.dart'; -import 'sql/table_relationships_sql.dart'; -import 'sql/tables_sql.dart'; -import 'sql/types_sql.dart'; -import 'sql/views_key_dependencies_sql.dart'; -import 'sql/views_sql.dart'; -import 'supabase_cli_io.dart'; - -/// The supabase/sdk revision of `@supabase/postgrest-typegen` whose -/// introspection this library ports, a commit or a release tag. -/// `tool/check_introspection_drift.ts` compares the SQL of the port against -/// it and `tool/regenerate_fixture.ts` expects a checkout of it. -const postgrestTypegenRevision = 'postgrest-typegen/v0.5.0'; - -/// The `version` field of the emitted `GeneratorMetadata` document. -const generatorMetadataVersion = 1; - -/// Introspects the database [target] through `supabase db query` and returns -/// the `GeneratorMetadata` document in canonical order, equal to -/// `sortGeneratorMetadata(await introspect(pool))` of -/// `@supabase/postgrest-typegen` at [postgrestTypegenRevision] up to the -/// order of keys within a record. -/// -/// [includedSchemas] and [excludedSchemas] restrict the schemas whose -/// relations, columns, keys and functions are listed; the system schemas are -/// always left out and types are always listed for every schema. -/// -/// Throws a [SupabaseCliException] when the Supabase CLI is missing, not -/// logged in, or cannot reach the database. -Future> introspectDatabase( - DatabaseTarget target, { - List includedSchemas = const [], - List excludedSchemas = const [], -}) async => sortGeneratorMetadata( - await introspect( - SupabaseCliQueryable(target), - includedSchemas: includedSchemas, - excludedSchemas: excludedSchemas, - ), -); - -/// Introspects the database behind [database] into the `GeneratorMetadata` -/// document, in query order. Apply [sortGeneratorMetadata] before generating -/// from it. -/// -/// Port of `introspect` of `@supabase/postgrest-typegen`: the same queries -/// with the same option combination, issued as one [Queryable.query] call, so -/// the result holds the same records as the TypeScript output for the same -/// database. -Future> introspect( - Queryable database, { - List includedSchemas = const [], - List excludedSchemas = const [], -}) async { - final systemExcludingFilter = filterByList( - include: includedSchemas, - exclude: excludedSchemas, - defaultExclude: defaultSystemSchemas, - ); - final plainFilter = filterByList( - include: includedSchemas, - exclude: excludedSchemas, - ); - - final results = await database.query({ - 'schemas': schemasSql(schemaFilter: systemExcludingFilter), - 'tables': tablesSql(schemaFilter: systemExcludingFilter), - 'foreignTables': foreignTablesSql(schemaFilter: plainFilter), - 'views': viewsSql(schemaFilter: systemExcludingFilter), - 'materializedViews': materializedViewsSql(schemaFilter: plainFilter), - 'columns': columnsSql(schemaFilter: systemExcludingFilter), - 'primaryKeys': primaryKeysSql(schemaFilter: systemExcludingFilter), - 'tableRelationships': tableRelationshipsSql( - schemaFilter: systemExcludingFilter, - ), - 'viewsKeyDependencies': viewsKeyDependenciesSql( - schemaFilter: systemExcludingFilter, - ), - 'functions': functionsSql(schemaFilter: systemExcludingFilter), - 'types': typesSql, - }); - List> rows(String name) => results[name]!; - - final tableRelationships = rows('tableRelationships'); - final relationships = [ - ...tableRelationships, - ...expandViewRelationships( - tableRelationships, - rows('viewsKeyDependencies'), - ), - ]; - - final document = { - 'version': generatorMetadataVersion, - 'schemas': rows('schemas'), - 'tables': rows('tables'), - 'foreignTables': rows('foreignTables'), - 'views': rows('views'), - 'materializedViews': rows('materializedViews'), - 'columns': rows('columns'), - 'primaryKeys': rows('primaryKeys'), - 'relationships': relationships, - 'functions': [ - for (final function in rows('functions')) - if (!const { - 'trigger', - 'event_trigger', - }.contains(function['return_type'])) - function, - ], - 'types': rows('types'), - }; - return {...document, 'rpcFunctions': resolveRpcFunctions(document)}; -} diff --git a/packages/supabase_typegen/lib/src/introspection/pg_format.dart b/packages/supabase_typegen/lib/src/introspection/pg_format.dart deleted file mode 100644 index efd0955cc..000000000 --- a/packages/supabase_typegen/lib/src/introspection/pg_format.dart +++ /dev/null @@ -1,58 +0,0 @@ -/// Quotes [value] as a SQL literal the way `pg-format` 1.0.4 does, which is -/// what the introspection SQL of `@supabase/postgrest-typegen` interpolates. -/// -/// `null` becomes `NULL`, booleans `'t'` and `'f'`, lists comma separated -/// literals with nested lists parenthesised, and strings and numbers a single -/// quoted string with embedded quotes doubled. Numbers are quoted on purpose, -/// `literal(10)` is `'10'`, because that is what the TypeScript output -/// contains. A string containing a backslash uses the `E'…'` form. -String literal(Object? value) { - switch (value) { - case null: - return 'NULL'; - case false: - return "'f'"; - case true: - return "'t'"; - case List list: - return [ - for (final (index, element) in list.indexed) - if (element is List) - _listToGroup(element, useSpace: index != 0) - else - literal(element), - ].join(','); - case String text: - return _quote(text); - case num number: - return _quote(number.toString()); - default: - throw ArgumentError.value( - value, - 'value', - 'Only null, bool, num, String and List values can be quoted.', - ); - } -} - -String _listToGroup(List list, {required bool useSpace}) => - '${useSpace ? ' (' : '('}${list.map(literal).join(', ')})'; - -String _quote(String text) { - final buffer = StringBuffer("'"); - var hasBackslash = false; - for (final rune in text.runes) { - final character = String.fromCharCode(rune); - switch (character) { - case "'": - buffer.write("''"); - case r'\': - buffer.write(r'\\'); - hasBackslash = true; - default: - buffer.write(character); - } - } - buffer.write("'"); - return hasBackslash ? 'E$buffer' : buffer.toString(); -} diff --git a/packages/supabase_typegen/lib/src/introspection/queryable.dart b/packages/supabase_typegen/lib/src/introspection/queryable.dart deleted file mode 100644 index 5378c7825..000000000 --- a/packages/supabase_typegen/lib/src/introspection/queryable.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// The single database operation the introspection needs: run a set of named -/// queries and return the rows of each. -abstract interface class Queryable { - /// Runs every SQL statement in [queries] and returns its rows, as column - /// name to value maps, under the same key. - Future>>> query( - Map queries, - ); -} diff --git a/packages/supabase_typegen/lib/src/introspection/relationships.dart b/packages/supabase_typegen/lib/src/introspection/relationships.dart deleted file mode 100644 index 10192fed5..000000000 --- a/packages/supabase_typegen/lib/src/introspection/relationships.dart +++ /dev/null @@ -1,114 +0,0 @@ -/// A `relationships` entry of the `GeneratorMetadata` document, as the -/// JSON object the TypeScript introspection emits. -typedef RelationshipRecord = Map; - -/// A row of the views key dependencies query: a primary or foreign key of -/// a table whose columns a view selects, with `column_dependencies` mapping -/// each table column to the view columns that carry it. -typedef ViewKeyDependency = Map; - -/// Expands the table to table [relationships] into the view to table, table -/// to view and view to view relationships implied by [viewsKeyDependencies], -/// in that order per relationship. A key column selected under several view -/// columns yields one relationship per combination. -/// -/// Port of `expandViewRelationships` of `@supabase/postgrest-typegen`. -List expandViewRelationships( - List relationships, - List viewsKeyDependencies, -) => [ - for (final relationship in relationships) - ..._expandRelationship(relationship, viewsKeyDependencies), -]; - -List _expandRelationship( - RelationshipRecord relationship, - List viewsKeyDependencies, -) { - final viewToTableKeyDeps = viewsKeyDependencies.where( - (dependency) => - dependency['table_schema'] == relationship['schema'] && - dependency['table_name'] == relationship['relation'] && - dependency['constraint_name'] == relationship['foreign_key_name'] && - dependency['constraint_type'] == 'f', - ); - final tableToViewKeyDeps = viewsKeyDependencies.where( - (dependency) => - dependency['table_schema'] == relationship['referenced_schema'] && - dependency['table_name'] == relationship['referenced_relation'] && - dependency['constraint_name'] == relationship['foreign_key_name'] && - dependency['constraint_type'] == 'f_ref', - ); - - RelationshipRecord record({ - required Object? schema, - required Object? relation, - required Object? columns, - required Object? referencedSchema, - required Object? referencedRelation, - required Object? referencedColumns, - }) => { - 'foreign_key_name': relationship['foreign_key_name'], - 'schema': schema, - 'relation': relation, - 'columns': columns, - 'is_one_to_one': relationship['is_one_to_one'], - 'referenced_schema': referencedSchema, - 'referenced_relation': referencedRelation, - 'referenced_columns': referencedColumns, - }; - - return [ - for (final dependency in viewToTableKeyDeps) - for (final viewColumns in _viewColumnCombinations(dependency)) - record( - schema: dependency['view_schema'], - relation: dependency['view_name'], - columns: viewColumns, - referencedSchema: relationship['referenced_schema'], - referencedRelation: relationship['referenced_relation'], - referencedColumns: relationship['referenced_columns'], - ), - for (final dependency in tableToViewKeyDeps) - for (final viewColumns in _viewColumnCombinations(dependency)) - record( - schema: relationship['schema'], - relation: relationship['relation'], - columns: relationship['columns'], - referencedSchema: dependency['view_schema'], - referencedRelation: dependency['view_name'], - referencedColumns: viewColumns, - ), - for (final viewDependency in viewToTableKeyDeps) - for (final viewColumns in _viewColumnCombinations(viewDependency)) - for (final referencedDependency in tableToViewKeyDeps) - for (final referencedViewColumns in _viewColumnCombinations( - referencedDependency, - )) - record( - schema: viewDependency['view_schema'], - relation: viewDependency['view_name'], - columns: viewColumns, - referencedSchema: referencedDependency['view_schema'], - referencedRelation: referencedDependency['view_name'], - referencedColumns: referencedViewColumns, - ), - ]; -} - -/// The cartesian product of the view columns of every column dependency, in -/// dependency order: one list of view columns per way of naming the key. -List> _viewColumnCombinations(ViewKeyDependency dependency) { - var combinations = [[]]; - for (final columnDependency - in (dependency['column_dependencies'] as List) - .cast>()) { - final viewColumns = (columnDependency['view_columns'] as List) - .cast(); - combinations = [ - for (final combination in combinations) - for (final viewColumn in viewColumns) [...combination, viewColumn], - ]; - } - return combinations; -} diff --git a/packages/supabase_typegen/lib/src/introspection/sort.dart b/packages/supabase_typegen/lib/src/introspection/sort.dart deleted file mode 100644 index eeda57832..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sort.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'dart:convert'; - -import 'package:collection/collection.dart'; - -import 'collation.dart'; - -/// Returns a copy of the `GeneratorMetadata` [document] with every collection -/// in the canonical order of `sortGeneratorMetadata` of -/// `@supabase/postgrest-typegen`: relations and types by schema and name, -/// columns by schema, table and name, primary keys grouped by table with -/// their declared column order kept, relationships by foreign key name, -/// referenced relation and referenced columns and then by referenced schema, -/// schema, relation and columns, functions by schema, name and signature with -/// their arguments by name, and the `rpcFunctions` entries by schema and name -/// with the arguments and `RETURNS TABLE` columns of each overload by name. -/// Names compare with [localeCompare]. The sort is stable, so the primary keys -/// of one table keep their declared order. -Map sortGeneratorMetadata(Map document) { - int bySchemaName(Map a, Map b) => - _firstNonZero([ - () => localeCompare(a['schema'] as String, b['schema'] as String), - () => localeCompare(a['name'] as String, b['name'] as String), - () => (a['id'] as int) - (b['id'] as int), - ]); - - return { - 'version': document['version'], - 'schemas': _sorted( - document['schemas'], - (a, b) => _firstNonZero([ - () => localeCompare(a['name'] as String, b['name'] as String), - () => (a['id'] as int) - (b['id'] as int), - ]), - ), - 'tables': _sorted(document['tables'], bySchemaName), - 'foreignTables': _sorted(document['foreignTables'], bySchemaName), - 'views': _sorted(document['views'], bySchemaName), - 'materializedViews': _sorted(document['materializedViews'], bySchemaName), - 'columns': _sorted( - document['columns'], - (a, b) => _firstNonZero([ - () => localeCompare(a['schema'] as String, b['schema'] as String), - () => localeCompare(a['table'] as String, b['table'] as String), - () => localeCompare(a['name'] as String, b['name'] as String), - ]), - ), - 'primaryKeys': _sorted( - document['primaryKeys'], - (a, b) => _firstNonZero([ - () => localeCompare(a['schema'] as String, b['schema'] as String), - () => localeCompare( - a['table_name'] as String, - b['table_name'] as String, - ), - ]), - ), - 'relationships': _sorted( - document['relationships'], - (a, b) => _firstNonZero([ - () => localeCompare( - a['foreign_key_name'] as String, - b['foreign_key_name'] as String, - ), - () => localeCompare( - a['referenced_relation'] as String, - b['referenced_relation'] as String, - ), - () => localeCompare( - jsonEncode(a['referenced_columns']), - jsonEncode(b['referenced_columns']), - ), - // The view expansion copies one foreign key onto every view exposing - // it, so the copies tie on the keys above and the view key dependency - // query does not order the view columns it aggregates. - () => localeCompare( - a['referenced_schema'] as String, - b['referenced_schema'] as String, - ), - () => localeCompare(a['schema'] as String, b['schema'] as String), - () => localeCompare(a['relation'] as String, b['relation'] as String), - () => localeCompare(jsonEncode(a['columns']), jsonEncode(b['columns'])), - ]), - ), - 'functions': [ - for (final function in _sorted( - document['functions'], - (a, b) => _firstNonZero([ - () => localeCompare(a['schema'] as String, b['schema'] as String), - () => localeCompare(a['name'] as String, b['name'] as String), - () => localeCompare( - a['identity_argument_types'] as String, - b['identity_argument_types'] as String, - ), - () => (a['id'] as int) - (b['id'] as int), - ]), - )) - { - ...function, - 'args': _sorted( - function['args'], - (a, b) => localeCompare(a['name'] as String, b['name'] as String), - ), - }, - ], - 'types': _sorted(document['types'], bySchemaName), - // The resolver orders the overloads of a function by signature, and that - // order is kept; the entries, their args and their table columns are - // addressed by name. - if (document['rpcFunctions'] case final List rpcFunctions) - 'rpcFunctions': [ - for (final function in _sorted( - rpcFunctions, - (a, b) => _firstNonZero([ - () => localeCompare(a['schema'] as String, b['schema'] as String), - () => localeCompare(a['name'] as String, b['name'] as String), - ]), - )) - { - ...function, - 'overloads': [ - for (final overload - in (function['overloads'] as List) - .cast>()) - { - ...overload, - 'args': _sorted(overload['args'], _byName), - 'returns': { - ...overload['returns'] as Map, - 'columns': switch ((overload['returns'] - as Map)['columns']) { - null => null, - final Object columns => _sorted(columns, _byName), - }, - }, - }, - ], - }, - ], - }; -} - -int _byName(Map a, Map b) => - localeCompare(a['name'] as String, b['name'] as String); - -List> _sorted( - Object? collection, - int Function(Map a, Map b) compare, -) { - final records = (collection as List) - .cast>() - .toList(); - mergeSort(records, compare: compare); - return records; -} - -int _firstNonZero(List comparisons) { - for (final comparison in comparisons) { - final result = comparison(); - if (result != 0) return result; - } - return 0; -} diff --git a/packages/supabase_typegen/lib/src/introspection/sql/columns_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/columns_sql.dart deleted file mode 100644 index 9254b6216..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/columns_sql.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'helpers.dart'; - -/// `COLUMNS_SQL` of `@supabase/postgrest-typegen`. -String columnsSql({required String schemaFilter}) => - ''' - --- Adapted from information_schema.columns - -SELECT - c.oid :: int8 AS table_id, - nc.nspname AS schema, - c.relname AS table, - (c.oid || '.' || a.attnum) AS id, - a.attnum AS ordinal_position, - a.attname AS name, - CASE - WHEN a.atthasdef THEN pg_get_expr(ad.adbin, ad.adrelid) - ELSE NULL - END AS default_value, - CASE - WHEN t.typtype = 'd' THEN CASE - WHEN bt.typelem <> 0 :: oid - AND bt.typlen = -1 THEN 'ARRAY' - WHEN nbt.nspname = 'pg_catalog' THEN format_type(t.typbasetype, NULL) - ELSE 'USER-DEFINED' - END - ELSE CASE - WHEN t.typelem <> 0 :: oid - AND t.typlen = -1 THEN 'ARRAY' - WHEN nt.nspname = 'pg_catalog' THEN format_type(a.atttypid, NULL) - ELSE 'USER-DEFINED' - END - END AS data_type, - COALESCE(bt.typname, t.typname) AS format, - COALESCE(nbt.nspname, nt.nspname) AS type_schema, - a.attidentity IN ('a', 'd') AS is_identity, - CASE - a.attidentity - WHEN 'a' THEN 'ALWAYS' - WHEN 'd' THEN 'BY DEFAULT' - ELSE NULL - END AS identity_generation, - -- 's' = stored, 'v' = virtual (PostgreSQL 18+); neither accepts writes. - a.attgenerated IN ('s', 'v') AS is_generated, - NOT ( - a.attnotnull - OR t.typtype = 'd' AND t.typnotnull - ) AS is_nullable, - ( - c.relkind IN ('r', 'p') - -- Columns of views made writable by INSTEAD OF triggers or unconditional - -- INSTEAD rules can be written through PostgREST even though the view is - -- not auto-updatable, so they must not degrade to `?: never` in generated - -- Insert/Update types. pg_column_is_updatable cannot express this: it - -- requires the relation to support both UPDATE and DELETE, so a view with - -- only an INSTEAD OF INSERT or only an INSTEAD OF UPDATE trigger reports - -- every column as non-updatable regardless of include_triggers. Triggers - -- and rules rewrite whole rows, so their presence makes every column - -- writable (tgtype bits: 64 = INSTEAD, 4 = INSERT, 16 = UPDATE; - -- pg_rewrite ev_type: '2' = UPDATE, '3' = INSERT; ev_qual is '<>' for - -- unconditional rules, and only unconditional INSTEAD rules make a view - -- writable). Which events a view accepts is gated per view via - -- is_insert_enabled / is_update_enabled. - OR c.relkind IN ('v', 'f') AND ( - pg_column_is_updatable(c.oid, a.attnum, FALSE) - OR EXISTS ( - SELECT 1 FROM pg_trigger tg - WHERE tg.tgrelid = c.oid - AND tg.tgtype & 64 <> 0 - AND tg.tgtype & 20 <> 0 - AND NOT tg.tgisinternal - ) - OR EXISTS ( - SELECT 1 FROM pg_rewrite rw - WHERE rw.ev_class = c.oid - AND rw.is_instead - AND rw.ev_type IN ('2', '3') - AND rw.ev_qual :: text = '<>' - ) - ) - ) AS is_updatable, - uniques.table_id IS NOT NULL AS is_unique, - check_constraints.definition AS "check", - array_to_json( - array( - SELECT - enumlabel - FROM - pg_catalog.pg_enum enums - WHERE - enums.enumtypid = coalesce(bt.oid, t.oid) - OR enums.enumtypid = coalesce(bt.typelem, t.typelem) - ORDER BY - enums.enumsortorder - ) - ) AS enums, - col_description(c.oid, a.attnum) AS comment -FROM - pg_attribute a - LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid - AND a.attnum = ad.adnum - JOIN ( - pg_class c - JOIN pg_namespace nc ON c.relnamespace = nc.oid - ) ON a.attrelid = c.oid - JOIN ( - pg_type t - JOIN pg_namespace nt ON t.typnamespace = nt.oid - ) ON a.atttypid = t.oid - LEFT JOIN ( - pg_type bt - JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid - ) ON t.typtype = 'd' - AND t.typbasetype = bt.oid - LEFT JOIN ( - SELECT DISTINCT ON (table_id, ordinal_position) - conrelid AS table_id, - conkey[1] AS ordinal_position - FROM pg_catalog.pg_constraint - WHERE contype = 'u' AND cardinality(conkey) = 1 - ) AS uniques ON uniques.table_id = c.oid AND uniques.ordinal_position = a.attnum - LEFT JOIN ( - -- We only select the first column check - SELECT DISTINCT ON (table_id, ordinal_position) - conrelid AS table_id, - conkey[1] AS ordinal_position, - substring( - pg_get_constraintdef(pg_constraint.oid, true), - 8, - length(pg_get_constraintdef(pg_constraint.oid, true)) - 8 - ) AS "definition" - FROM pg_constraint - WHERE contype = 'c' AND cardinality(conkey) = 1 - ORDER BY table_id, ordinal_position, oid asc - ) AS check_constraints ON check_constraints.table_id = c.oid AND check_constraints.ordinal_position = a.attnum -WHERE - ${when(schemaFilter, 'nc.nspname $schemaFilter AND')} - NOT pg_is_other_temp_schema(nc.oid) - AND a.attnum > 0 - AND NOT a.attisdropped - AND (c.relkind IN ('r', 'v', 'm', 'f', 'p')) - AND ( - pg_has_role(c.relowner, 'USAGE') - OR has_column_privilege( - c.oid, - a.attnum, - 'SELECT, INSERT, UPDATE, REFERENCES' - ) - ) -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/foreign_tables_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/foreign_tables_sql.dart deleted file mode 100644 index 94f365e2b..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/foreign_tables_sql.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'helpers.dart'; - -/// `FOREIGN_TABLES_SQL` of `@supabase/postgrest-typegen`. -String foreignTablesSql({required String schemaFilter}) => - ''' - -SELECT - c.oid :: int8 AS id, - n.nspname AS schema, - c.relname AS name, - obj_description(c.oid) AS comment -FROM - pg_class c - JOIN pg_namespace n ON n.oid = c.relnamespace -WHERE - ${when(schemaFilter, 'n.nspname $schemaFilter AND')} - c.relkind = 'f' -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/functions_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/functions_sql.dart deleted file mode 100644 index fab453969..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/functions_sql.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'helpers.dart'; - -/// `FUNCTIONS_SQL` of `@supabase/postgrest-typegen`. -String functionsSql({required String schemaFilter}) => - ''' - --- CTE with sane arg_modes, arg_names, and arg_types. --- All three are always of the same length. --- All three include all args, including OUT and TABLE args. -with functions as ( - select - p.*, - -- proargmodes is null when all arg modes are IN - coalesce( - p.proargmodes, - array_fill('i'::text, array[cardinality(coalesce(p.proallargtypes, p.proargtypes))]) - ) as arg_modes, - -- proargnames is null when all args are unnamed - coalesce( - p.proargnames, - array_fill(''::text, array[cardinality(coalesce(p.proallargtypes, p.proargtypes))]) - ) as arg_names, - -- proallargtypes is null when all arg modes are IN - coalesce(p.proallargtypes, p.proargtypes) as arg_types, - array_cat( - array_fill(false, array[pronargs - pronargdefaults]), - array_fill(true, array[pronargdefaults])) as arg_has_defaults - from - pg_proc as p - ${when(schemaFilter, 'join pg_namespace n on p.pronamespace = n.oid')} - where - ${when(schemaFilter, 'n.nspname $schemaFilter AND')} - p.prokind = 'f' -) -select - f.oid::int8 as id, - n.nspname as schema, - f.proname as name, - l.lanname as language, - case - when l.lanname = 'internal' then '' - else f.prosrc - end as definition, - case - when l.lanname = 'internal' then f.prosrc - else pg_get_functiondef(f.oid) - end as complete_statement, - coalesce(f_args.args, '[]') as args, - pg_get_function_arguments(f.oid) as argument_types, - pg_get_function_identity_arguments(f.oid) as identity_argument_types, - f.prorettype::int8 as return_type_id, - pg_get_function_result(f.oid) as return_type, - nullif(rt.typrelid::int8, 0) as return_type_relation_id, - f.proretset as is_set_returning_function, - case - when f.proretset then nullif(f.prorows, 0) - else null - end as prorows, - case - when f.provolatile = 'i' then 'IMMUTABLE' - when f.provolatile = 's' then 'STABLE' - when f.provolatile = 'v' then 'VOLATILE' - end as behavior, - f.prosecdef as security_definer, - f_config.config_params as config_params -from - functions f - left join pg_namespace n on f.pronamespace = n.oid - left join pg_language l on f.prolang = l.oid - left join pg_type rt on rt.oid = f.prorettype - left join ( - select - oid, - jsonb_object_agg(param, value) filter (where param is not null) as config_params - from - ( - select - oid, - (string_to_array(unnest(proconfig), '='))[1] as param, - (string_to_array(unnest(proconfig), '='))[2] as value - from - functions - ) as t - group by - oid - ) f_config on f_config.oid = f.oid - left join ( - select - oid, - jsonb_agg(jsonb_build_object( - 'mode', t2.mode, - 'name', name, - 'type_id', type_id, - 'has_default', has_default - )) as args - from - ( - select - oid, - unnest(arg_modes) as mode, - unnest(arg_names) as name, - unnest(arg_types)::int8 as type_id, - unnest(arg_has_defaults) as has_default - from - functions - ) as t1, - lateral ( - select - case - when t1.mode = 'i' then 'in' - when t1.mode = 'o' then 'out' - when t1.mode = 'b' then 'inout' - when t1.mode = 'v' then 'variadic' - else 'table' - end as mode - ) as t2 - group by - t1.oid - ) f_args on f_args.oid = f.oid -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/helpers.dart b/packages/supabase_typegen/lib/src/introspection/sql/helpers.dart deleted file mode 100644 index 59d840ef2..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/helpers.dart +++ /dev/null @@ -1,31 +0,0 @@ -import '../pg_format.dart'; - -/// The schemas postgres-meta leaves out unless system schemas are requested. -const defaultSystemSchemas = ['information_schema', 'pg_catalog', 'pg_toast']; - -/// Builds the `IN (…)` or `NOT IN (…)` fragment the SQL builders interpolate -/// after a schema name column. -/// -/// [include] wins over [exclude]; [defaultExclude] is prepended to [exclude]. -/// Values are quoted with [literal]. Returns the empty string when there is -/// nothing to filter, so the builders leave the clause out. -String filterByList({ - List? include, - List? exclude, - List? defaultExclude, -}) { - if (defaultExclude != null) { - exclude = [...defaultExclude, ...?exclude]; - } - if (include != null && include.isNotEmpty) { - return 'IN (${include.map(literal).join(',')})'; - } - if (exclude != null && exclude.isNotEmpty) { - return 'NOT IN (${exclude.map(literal).join(',')})'; - } - return ''; -} - -/// Renders [clause] when [filter] is set and nothing otherwise, mirroring the -/// `${props.filter ? `…` : ""}` conditionals of the TypeScript builders. -String when(String filter, String clause) => filter.isEmpty ? '' : clause; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/materialized_views_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/materialized_views_sql.dart deleted file mode 100644 index e5ce9b0bf..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/materialized_views_sql.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'helpers.dart'; - -/// `MATERIALIZED_VIEWS_SQL` of `@supabase/postgrest-typegen`. -String materializedViewsSql({required String schemaFilter}) => - ''' - -select - c.oid::int8 as id, - n.nspname as schema, - c.relname as name, - c.relispopulated as is_populated, - obj_description(c.oid) as comment -from - pg_class c - join pg_namespace n on n.oid = c.relnamespace -where - ${when(schemaFilter, 'n.nspname $schemaFilter AND')} - c.relkind = 'm' -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/primary_keys_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/primary_keys_sql.dart deleted file mode 100644 index 454dc8f66..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/primary_keys_sql.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'helpers.dart'; - -/// `PRIMARY_KEYS_SQL` of `@supabase/postgrest-typegen`. -String primaryKeysSql({required String schemaFilter}) => - ''' - -SELECT - c.oid :: int8 AS table_id, - n.nspname AS schema, - c.relname AS table_name, - a.attname AS name -FROM - pg_index i - JOIN pg_class c ON i.indrelid = c.oid - JOIN pg_namespace n ON c.relnamespace = n.oid - JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey) -WHERE - ${when(schemaFilter, 'n.nspname $schemaFilter AND')} - i.indisprimary - AND c.relkind IN ('r', 'p') - AND NOT pg_is_other_temp_schema(n.oid) - AND ( - pg_has_role(c.relowner, 'USAGE') - OR has_table_privilege( - c.oid, - 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER' - ) - OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') - ) -ORDER BY - c.oid, - array_position(i.indkey, a.attnum) -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/schemas_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/schemas_sql.dart deleted file mode 100644 index 09a1bfc41..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/schemas_sql.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'helpers.dart'; - -/// `SCHEMAS_SQL` of `@supabase/postgrest-typegen`. -String schemasSql({required String schemaFilter}) => - ''' - --- Adapted from information_schema.schemata -select - n.oid::int8 as id, - n.nspname as name, - u.rolname as owner -from - pg_namespace n, - pg_roles u -where - n.nspowner = u.oid - ${when(schemaFilter, 'and n.nspname $schemaFilter')} - and not pg_catalog.starts_with(n.nspname, 'pg_') - and ( - pg_has_role(n.nspowner, 'USAGE') - or has_schema_privilege(n.oid, 'CREATE, USAGE') - ) - and not pg_catalog.starts_with(n.nspname, 'pg_temp_') - and not pg_catalog.starts_with(n.nspname, 'pg_toast_temp_') -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/table_relationships_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/table_relationships_sql.dart deleted file mode 100644 index e8fb25aef..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/table_relationships_sql.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'helpers.dart'; - -/// `TABLE_RELATIONSHIPS_SQL` of `@supabase/postgrest-typegen`. -String tableRelationshipsSql({required String schemaFilter}) => - ''' - --- Adapted from --- https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L722 -WITH -pks_uniques_cols AS ( - SELECT - connamespace, - conrelid, - jsonb_agg(column_info.cols) as cols - FROM pg_constraint - JOIN lateral ( - SELECT array_agg(cols.attname order by cols.attnum) as cols - FROM ( select unnest(conkey) as col) _ - JOIN pg_attribute cols on cols.attrelid = conrelid and cols.attnum = col - ) column_info ON TRUE - WHERE - contype IN ('p', 'u') and - connamespace::regnamespace::text <> 'pg_catalog' - ${when(schemaFilter, 'and connamespace::regnamespace::text $schemaFilter')} - GROUP BY connamespace, conrelid -) -SELECT - traint.conname AS foreign_key_name, - ns1.nspname AS schema, - tab.relname AS relation, - column_info.cols AS columns, - ns2.nspname AS referenced_schema, - other.relname AS referenced_relation, - column_info.refs AS referenced_columns, - (column_info.cols IN (SELECT * FROM jsonb_array_elements(pks_uqs.cols))) AS is_one_to_one -FROM pg_constraint traint -JOIN LATERAL ( - SELECT - jsonb_agg(cols.attname order by ord) AS cols, - jsonb_agg(refs.attname order by ord) AS refs - FROM unnest(traint.conkey, traint.confkey) WITH ORDINALITY AS _(col, ref, ord) - JOIN pg_attribute cols ON cols.attrelid = traint.conrelid AND cols.attnum = col - JOIN pg_attribute refs ON refs.attrelid = traint.confrelid AND refs.attnum = ref - WHERE ${schemaFilter.isEmpty ? 'true' : 'traint.connamespace::regnamespace::text $schemaFilter'} -) AS column_info ON TRUE -JOIN pg_namespace ns1 ON ns1.oid = traint.connamespace -JOIN pg_class tab ON tab.oid = traint.conrelid -JOIN pg_class other ON other.oid = traint.confrelid -JOIN pg_namespace ns2 ON ns2.oid = other.relnamespace -LEFT JOIN pks_uniques_cols pks_uqs ON pks_uqs.connamespace = traint.connamespace AND pks_uqs.conrelid = traint.conrelid -WHERE traint.contype = 'f' -AND traint.conparentid = 0 -${when(schemaFilter, 'and ns1.nspname $schemaFilter')} -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/tables_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/tables_sql.dart deleted file mode 100644 index ad057f762..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/tables_sql.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'helpers.dart'; - -/// `TABLES_SQL` of `@supabase/postgrest-typegen`. -String tablesSql({required String schemaFilter}) => - ''' - -SELECT - c.oid :: int8 AS id, - nc.nspname AS schema, - c.relname AS name, - c.relrowsecurity AS rls_enabled, - c.relforcerowsecurity AS rls_forced, - CASE - WHEN c.relreplident = 'd' THEN 'DEFAULT' - WHEN c.relreplident = 'i' THEN 'INDEX' - WHEN c.relreplident = 'f' THEN 'FULL' - ELSE 'NOTHING' - END AS replica_identity, - pg_total_relation_size(format('%I.%I', nc.nspname, c.relname)) :: int8 AS bytes, - pg_size_pretty( - pg_total_relation_size(format('%I.%I', nc.nspname, c.relname)) - ) AS size, - pg_stat_get_live_tuples(c.oid) AS live_rows_estimate, - pg_stat_get_dead_tuples(c.oid) AS dead_rows_estimate, - obj_description(c.oid) AS comment -FROM - pg_namespace nc - JOIN pg_class c ON nc.oid = c.relnamespace -WHERE - ${when(schemaFilter, 'nc.nspname $schemaFilter AND')} - c.relkind IN ('r', 'p') - AND NOT pg_is_other_temp_schema(nc.oid) - AND ( - pg_has_role(c.relowner, 'USAGE') - OR has_table_privilege( - c.oid, - 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER' - ) - OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') - ) -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/types_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/types_sql.dart deleted file mode 100644 index 5bac1a4e6..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/types_sql.dart +++ /dev/null @@ -1,53 +0,0 @@ -/// `TYPES_SQL` of `@supabase/postgrest-typegen`: every type of every schema, -/// including the row types of tables and views and the array types. -const typesSql = ''' - -select - t.oid::int8 as id, - t.typname as name, - n.nspname as schema, - format_type (t.oid, null) as format, - coalesce(t_enums.enums, '[]') as enums, - coalesce(t_attributes.attributes, '[]') as attributes, - obj_description (t.oid, 'pg_type') as comment, - nullif(t.typrelid::int8, 0) as type_relation_id -from - pg_type t - left join pg_namespace n on n.oid = t.typnamespace - left join ( - select - enumtypid, - jsonb_agg(enumlabel order by enumsortorder) as enums - from - pg_enum - group by - enumtypid - ) as t_enums on t_enums.enumtypid = t.oid - left join ( - select - oid, - jsonb_agg( - jsonb_build_object('name', a.attname, 'type_id', a.atttypid::int8) - order by a.attnum asc - ) as attributes - from - pg_class c - join pg_attribute a on a.attrelid = c.oid - where - c.relkind = 'c' and not a.attisdropped - group by - c.oid - ) as t_attributes on t_attributes.oid = t.typrelid - where - ( - t.typrelid = 0 - or ( - select - c.relkind in ('c', 'r', 'v', 'm', 'p', 'f') - from - pg_class c - where - c.oid = t.typrelid - ) - ) -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/sql/views_key_dependencies_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/views_key_dependencies_sql.dart deleted file mode 100644 index 49514e1e8..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/views_key_dependencies_sql.dart +++ /dev/null @@ -1,177 +0,0 @@ -import '../pg_format.dart'; -import 'helpers.dart'; - -/// `VIEWS_KEY_DEPENDENCIES_SQL` of `@supabase/postgrest-typegen`. -String viewsKeyDependenciesSql({required String schemaFilter}) => - ''' - --- Adapted from --- https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L820 -with recursive -pks_fks as ( - -- pk + fk referencing col - select - contype::text as contype, - conname, - array_length(conkey, 1) as ncol, - conrelid as resorigtbl, - col as resorigcol, - ord - from pg_constraint - left join lateral unnest(conkey) with ordinality as _(col, ord) on true - where contype IN ('p', 'f') - union - -- fk referenced col - select - concat(contype, '_ref') as contype, - conname, - array_length(confkey, 1) as ncol, - confrelid, - col, - ord - from pg_constraint - left join lateral unnest(confkey) with ordinality as _(col, ord) on true - where contype='f' - ${when(schemaFilter, 'and connamespace::regnamespace::text $schemaFilter')} -), -views as ( - select - c.oid as view_id, - n.nspname as view_schema, - c.relname as view_name, - r.ev_action as view_definition - from pg_class c - join pg_namespace n on n.oid = c.relnamespace - join pg_rewrite r on r.ev_class = c.oid - where c.relkind in ('v', 'm') - ${when(schemaFilter, 'and n.nspname $schemaFilter')} -), -transform_json as ( - select - view_id, view_schema, view_name, -${nodeTreeToJson('view_definition::text')}::json as view_definition - from views -), -target_entries as( - select - view_id, view_schema, view_name, - json_array_elements(view_definition->0->'targetList') as entry - from transform_json -), -results as( - select - view_id, view_schema, view_name, - (entry->>'resno')::int as view_column, - (entry->>'resorigtbl')::oid as resorigtbl, - (entry->>'resorigcol')::int as resorigcol - from target_entries -), --- CYCLE detection according to PG docs: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CYCLE --- Can be replaced with CYCLE clause once PG v13 is EOL. -recursion(view_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as( - select - r.*, - false, - ARRAY[resorigtbl] - from results r - where ${schemaFilter.isEmpty ? 'true' : 'view_schema $schemaFilter'} - union all - select - view.view_id, - view.view_schema, - view.view_name, - view.view_column, - tab.resorigtbl, - tab.resorigcol, - tab.resorigtbl = ANY(path), - path || tab.resorigtbl - from recursion view - join results tab on view.resorigtbl=tab.view_id and view.resorigcol=tab.view_column - where not is_cycle -), -repeated_references as( - select - view_id, - view_schema, - view_name, - resorigtbl, - resorigcol, - array_agg(attname) as view_columns - from recursion - join pg_attribute vcol on vcol.attrelid = view_id and vcol.attnum = view_column - group by - view_id, - view_schema, - view_name, - resorigtbl, - resorigcol -) -select - sch.nspname as table_schema, - tbl.relname as table_name, - rep.view_schema, - rep.view_name, - pks_fks.conname as constraint_name, - pks_fks.contype as constraint_type, - jsonb_agg( - jsonb_build_object('table_column', col.attname, 'view_columns', view_columns) order by pks_fks.ord - ) as column_dependencies -from repeated_references rep -join pks_fks using (resorigtbl, resorigcol) -join pg_class tbl on tbl.oid = rep.resorigtbl -join pg_attribute col on col.attrelid = tbl.oid and col.attnum = rep.resorigcol -join pg_namespace sch on sch.oid = tbl.relnamespace -group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conname, pks_fks.contype, pks_fks.ncol --- make sure we only return key for which all columns are referenced in the view - no partial PKs or FKs -having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1) -'''; - -/// One text rewrite of the `pg_node_tree` to JSON conversion: a `replace`, -/// or a global `regexp_replace` when [isRegex] is set. -typedef NodeTreeRewrite = ({String pattern, String replacement, bool isRegex}); - -/// The rewrites, innermost first, that turn the `pg_node_tree` text of a view -/// definition into JSON exposing only `targetList`, `resno`, `resorigtbl` -/// and `resorigcol`, as `NODE_TREE_TO_JSON_REWRITES` of -/// `@supabase/postgrest-typegen` lists them; the order matters. -const List nodeTreeToJsonRewrites = [ - (pattern: '<>', replacement: '()', isRegex: false), - (pattern: ',', replacement: '', isRegex: false), - (pattern: r'\{', replacement: '', isRegex: false), - (pattern: r'\}', replacement: '', isRegex: false), - (pattern: ' :targetList ', replacement: ',"targetList":', isRegex: false), - (pattern: ' :resno ', replacement: ',"resno":', isRegex: false), - (pattern: ' :resorigtbl ', replacement: ',"resorigtbl":', isRegex: false), - (pattern: ' :resorigcol ', replacement: ',"resorigcol":', isRegex: false), - (pattern: '{', replacement: '{ :', isRegex: false), - (pattern: '((', replacement: '{((', isRegex: false), - (pattern: '({', replacement: '{({', isRegex: false), - (pattern: ' :[^}{,]+', replacement: ',"":', isRegex: true), - (pattern: ',"":}', replacement: '}', isRegex: false), - (pattern: ',"":,', replacement: ',', isRegex: false), - (pattern: '{(', replacement: '(', isRegex: false), - (pattern: '{,', replacement: '{', isRegex: false), - (pattern: '(', replacement: '[', isRegex: false), - (pattern: ')', replacement: ']', isRegex: false), - (pattern: ' ', replacement: ',', isRegex: false), -]; - -/// Renders [nodeTreeToJsonRewrites] applied to [expression] as nested SQL -/// calls, the opening calls stacked and each step's arguments on the line -/// that closes it, exactly as `nodeTreeToJson` of the TypeScript package. -String nodeTreeToJson(String expression) { - final buffer = StringBuffer(); - for (final rewrite in nodeTreeToJsonRewrites.reversed) { - buffer.writeln(' ${rewrite.isRegex ? 'regexp_replace' : 'replace'}('); - } - buffer.writeln(' $expression,'); - for (final (index, rewrite) in nodeTreeToJsonRewrites.indexed) { - final flags = rewrite.isRegex ? ", 'g'" : ''; - final closing = index == nodeTreeToJsonRewrites.length - 1 ? ')' : '),'; - buffer.writeln( - ' ${literal(rewrite.pattern)}, ' - '${literal(rewrite.replacement)}$flags$closing', - ); - } - return buffer.toString().trimRight(); -} diff --git a/packages/supabase_typegen/lib/src/introspection/sql/views_sql.dart b/packages/supabase_typegen/lib/src/introspection/sql/views_sql.dart deleted file mode 100644 index d7cd1ec4b..000000000 --- a/packages/supabase_typegen/lib/src/introspection/sql/views_sql.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'helpers.dart'; - -/// `VIEWS_SQL` of `@supabase/postgrest-typegen`. -String viewsSql({required String schemaFilter}) => - ''' - -SELECT - c.oid :: int8 AS id, - n.nspname AS schema, - c.relname AS name, - -- See definition of information_schema.views - (pg_relation_is_updatable(c.oid, false) & 20) = 20 AS is_updatable, - -- Passing include_triggers => true also counts views made writable by - -- INSTEAD OF triggers (and INSTEAD rules), which PostgREST can write - -- through even when the view is not auto-updatable. Bit 8 is INSERT, - -- bit 4 is UPDATE (see information_schema.views). - (pg_relation_is_updatable(c.oid, true) & 8) = 8 AS is_insert_enabled, - (pg_relation_is_updatable(c.oid, true) & 4) = 4 AS is_update_enabled, - obj_description(c.oid) AS comment -FROM - pg_class c - JOIN pg_namespace n ON n.oid = c.relnamespace -WHERE - ${when(schemaFilter, 'n.nspname $schemaFilter AND')} - c.relkind = 'v' -'''; diff --git a/packages/supabase_typegen/lib/src/introspection/supabase_cli_io.dart b/packages/supabase_typegen/lib/src/introspection/supabase_cli_io.dart deleted file mode 100644 index 4d10094ca..000000000 --- a/packages/supabase_typegen/lib/src/introspection/supabase_cli_io.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'queryable.dart'; - -/// The database `supabase db query` connects to. -sealed class DatabaseTarget { - const DatabaseTarget(); - - /// The `supabase db query` flags selecting this target. - List get arguments; -} - -/// The database of the local Supabase stack started with `supabase start`. -final class LocalDatabase extends DatabaseTarget { - /// Creates the local stack target. - const LocalDatabase(); - - @override - List get arguments => const ['--local']; -} - -/// The database of the project linked with `supabase link`, or of -/// [projectRef], reached through the Management API with the credentials of -/// `supabase login`. -final class LinkedProject extends DatabaseTarget { - /// Creates the linked project target, optionally overriding the linked - /// project with [projectRef]. - const LinkedProject({this.projectRef}); - - /// The project ref to query instead of the linked one. - final String? projectRef; - - @override - List get arguments => [ - '--linked', - if (projectRef != null) ...['--project-ref', projectRef!], - ]; -} - -/// Any Postgres database, by connection string. -final class DatabaseUrl extends DatabaseTarget { - /// Creates the connection string target. - const DatabaseUrl(this.url); - - /// The `postgresql://` connection string. - final String url; - - @override - List get arguments => ['--db-url', url]; -} - -/// Thrown when `supabase db query` cannot run or fails, with a [message] -/// that tells the user what to do. -class SupabaseCliException implements Exception { - /// Creates the exception carrying [message]. - const SupabaseCliException(this.message); - - /// What went wrong and how to fix it. - final String message; - - @override - String toString() => message; -} - -/// A [Queryable] that runs its queries through `supabase db query`, so the -/// Supabase CLI resolves and authenticates the connection. -/// -/// All queries go out as one statement that aggregates each result with -/// `json_agg`, one round trip per introspection. -class SupabaseCliQueryable implements Queryable { - /// Creates a [Queryable] for [target] using the [executable] on `PATH`. - const SupabaseCliQueryable(this.target, {this.executable = 'supabase'}); - - /// Where to connect. - final DatabaseTarget target; - - /// The Supabase CLI binary. - final String executable; - - @override - Future>>> query( - Map queries, - ) async { - final directory = Directory.systemTemp.createTempSync('supabase_typegen_'); - try { - final file = File('${directory.path}/introspection.sql') - ..writeAsStringSync(combineQueries(queries)); - final ProcessResult result; - try { - result = await Process.run(executable, [ - 'db', - 'query', - ...target.arguments, - '--output', - 'json', - '--file', - file.absolute.path, - ]); - } on ProcessException catch (error) { - throw SupabaseCliException( - 'The Supabase CLI could not be started (${error.message}). ' - 'Install it from https://supabase.com/docs/guides/cli/getting-started ' - 'and make sure `$executable` is on PATH.', - ); - } - if (result.exitCode != 0) { - throw SupabaseCliException( - describeFailure( - _stripAnsi('${result.stderr}\n${result.stdout}'), - target: target, - ), - ); - } - return _rows(result.stdout as String, queries.keys); - } finally { - directory.deleteSync(recursive: true); - } - } -} - -/// Folds [queries] into one `SELECT` with a `json_agg` column per query, so -/// a single `supabase db query` call returns every result set. -String combineQueries(Map queries) => [ - 'select', - [ - for (final MapEntry(key: name, value: sql) in queries.entries) - ' (select coalesce(json_agg(t), \'[]\'::json) from (\n' - '$sql\n' - ' ) t) as "$name"', - ].join(',\n'), -].join('\n'); - -/// Turns the [output] of a failed `supabase db query` into the message shown -/// to the user, with dedicated wording for the setup problems the tool can -/// name. Passwords of connection strings echoed by the CLI are redacted. -String describeFailure(String rawOutput, {DatabaseTarget? target}) { - final output = rawOutput - .split('\n') - .where((line) => !line.startsWith('Connecting to ')) - .join('\n') - .replaceAllMapped(_connectionStringPassword, (match) => '${match[1]}***@') - .trim(); - if (target is LinkedProject && - target.projectRef != null && - output.contains('supabase db query [flags]')) { - // CLI releases before 2.116 have no --project-ref on db query and answer - // with the usage text. - return 'This Supabase CLI does not accept --project-ref for db query. ' - 'Upgrade to 2.116 or newer, or run `supabase link --project-ref ' - '${target.projectRef}` and pass --linked instead.'; - } - if (output.contains('Access token not provided')) { - return 'The Supabase CLI is not logged in. Run `supabase login`, or set ' - 'SUPABASE_ACCESS_TOKEN, and try again.'; - } - if (output.contains('Cannot find project ref')) { - return 'No linked Supabase project was found. Run `supabase link` in ' - 'your project directory, or pass --project-ref.'; - } - return 'supabase db query failed:\n$output'; -} - -Map>> _rows( - String stdout, - Iterable names, -) { - final Object? decoded; - try { - decoded = jsonDecode(stdout); - } on FormatException { - throw SupabaseCliException( - 'supabase db query did not return JSON:\n${stdout.trim()}', - ); - } - // Older CLI releases print the rows as a bare array; newer ones wrap them - // in an object next to advisories and warnings. - final rows = switch (decoded) { - final List list => list, - {'rows': final List list} => list, - _ => null, - }; - if (rows == null || rows.length != 1) { - throw const SupabaseCliException( - 'supabase db query returned an unexpected result shape.', - ); - } - final row = rows.single as Map; - return { - for (final name in names) - name: switch (row[name]) { - final List records => records.cast>(), - _ => throw SupabaseCliException( - 'supabase db query returned no "$name" collection.', - ), - }, - }; -} - -final _ansi = RegExp(r'\x1B\[[0-9;]*m'); - -/// The `scheme://user:password@` prefix of a connection string, with the -/// password captured for redaction. -final _connectionStringPassword = RegExp(r'(\w+://[^:/\s@]*:)[^@\s]*@'); - -String _stripAnsi(String text) => text.replaceAll(_ansi, ''); diff --git a/packages/supabase_typegen/lib/src/introspection/supabase_config_io.dart b/packages/supabase_typegen/lib/src/introspection/supabase_config_io.dart deleted file mode 100644 index 2d0535b4a..000000000 --- a/packages/supabase_typegen/lib/src/introspection/supabase_config_io.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io'; - -import 'package:toml/toml.dart'; - -/// The schemas `supabase gen types` generates for when none are named: the -/// `api.schemas` of the project's `supabase/config.toml`, always including -/// `public`. -/// -/// The configuration is looked up the way the Supabase CLI does: under -/// `SUPABASE_WORKDIR` when that variable is set, otherwise in -/// [startDirectory], which defaults to the current directory, or the nearest -/// parent of it holding a `supabase/config.toml`. Without a configuration file -/// only `public` is returned, sorted like every other schema list. -/// -/// Throws a [FormatException] when the file exists but is not valid TOML or -/// `api.schemas` is not a list of strings. -List defaultSchemas({ - Map? environment, - Directory? startDirectory, -}) { - final workingDirectory = - (environment ?? Platform.environment)['SUPABASE_WORKDIR']; - final configFile = workingDirectory == null - ? _nearestConfigFile(startDirectory ?? Directory.current) - : File('$workingDirectory/supabase/config.toml'); - final schemas = {'public'}; - if (configFile != null && configFile.existsSync()) { - schemas.addAll(_exposedSchemas(configFile)); - } - return schemas.toList()..sort(); -} - -/// The `supabase/config.toml` of [directory] or of its nearest ancestor, -/// `null` when none of them holds one. -File? _nearestConfigFile(Directory directory) { - var current = directory.absolute; - while (true) { - final configFile = File('${current.path}/supabase/config.toml'); - if (configFile.existsSync()) return configFile; - final parent = current.parent; - if (parent.path == current.path) return null; - current = parent; - } -} - -List _exposedSchemas(File configFile) { - final Map document; - try { - document = TomlDocument.parse(configFile.readAsStringSync()).toMap(); - } on TomlException catch (error) { - throw FormatException('${configFile.path} is not valid TOML: $error'); - } - final api = document['api']; - final schemas = api is Map ? api['schemas'] : null; - if (schemas == null) return const []; - if (schemas is! List || schemas.any((schema) => schema is! String)) { - throw FormatException( - '${configFile.path}: api.schemas must be a list of schema names.', - ); - } - return schemas.cast(); -} diff --git a/packages/supabase_typegen/lib/src/rpc_functions.dart b/packages/supabase_typegen/lib/src/rpc_functions.dart deleted file mode 100644 index b89a5d8ea..000000000 --- a/packages/supabase_typegen/lib/src/rpc_functions.dart +++ /dev/null @@ -1,340 +0,0 @@ -import 'package:collection/collection.dart'; - -import 'introspection/collation.dart'; - -/// json, jsonb and text: the types PostgREST accepts for a single unnamed -/// parameter. -const _unnamedArgumentTypeIds = {114, 3802, 25}; -const _inputArgumentModes = {'in', 'inout', 'variadic'}; - -typedef _Candidate = ({ - Map function, - List> inputArguments, -}); - -/// Resolves the PostgREST view of the `functions` collection of the -/// `GeneratorMetadata` [document]: which functions are callable through -/// `/rpc`, how their overloads group and what a call returns. -/// -/// Port of `resolveRpcFunctions` of `@supabase/postgrest-typegen`, which -/// `introspect` attaches to the document as `rpcFunctions`. The result has -/// the shape of that collection and its canonical order: entries by schema -/// and name, overloads by argument list and return type, arguments and -/// `RETURNS TABLE` columns by name. -List> resolveRpcFunctions(Map document) { - List> records(String collection) => - (document[collection] as List? ?? const []) - .cast>(); - - final schemaNames = { - for (final schema in records('schemas')) schema['name'] as String, - }; - final typesById = { - for (final type in records('types')) type['id'] as int: type, - }; - final relationTypesById = { - for (final type in records('types')) - if (type['type_relation_id'] != null) type['id'] as int: type, - }; - final relations = [ - for (final collection in const [ - 'tables', - 'foreignTables', - 'views', - 'materializedViews', - ]) - ...records(collection), - ]; - final relationsById = { - for (final relation in relations) relation['id'] as int: relation, - }; - final relationsBySchemaName = { - for (final relation in relations) - (relation['schema'] as String, relation['name'] as String): relation, - }; - final columnNamesByRelationId = >{}; - for (final column in records('columns')) { - columnNamesByRelationId - .putIfAbsent(column['table_id'] as int, () => {}) - .add(column['name'] as String); - } - - Map typeReference(int typeId) { - final type = typesById[typeId]; - return { - 'type_id': typeId, - 'format': type?['name'], - 'type_schema': type?['schema'], - }; - } - - String typeName(int typeId) => - typesById[typeId]?['name'] as String? ?? 'unknown'; - - // The relation a return type stands for, or a composite type's name when - // the return type is one, since both are relations in `pg_class`. - String? returnRelationName(Map function) { - final relationId = function['return_type_relation_id'] as int?; - if (relationId == null) return null; - return relationsById[relationId]?['name'] as String? ?? - relationTypesById[function['return_type_id'] as int]?['name'] - as String?; - } - - // The relation whose row type the sole input parameter has, provided it - // lives in the function's schema and has no column of the function's name. - // When the parameter's type is not in `types`, the rendered argument list - // may still name the relation. - int? computedFieldOf(_Candidate candidate) { - final (:function, :inputArguments) = candidate; - if (inputArguments case [final argument]) { - final schema = function['schema'] as String; - final argumentType = relationTypesById[argument['type_id'] as int]; - final relation = argumentType != null - ? relationsById[argumentType['type_relation_id'] as int] - : typesById.containsKey(argument['type_id']) - ? null - : relationsBySchemaName[( - schema, - function['argument_types'] as String, - )]; - if (relation == null || relation['schema'] != schema) return null; - final relationId = relation['id'] as int; - if (columnNamesByRelationId[relationId]?.contains(function['name']) ?? - false) { - return null; - } - return relationId; - } - return null; - } - - bool isUnnamed(Map argument) => argument['name'] == ''; - - bool isCallable(_Candidate candidate) { - final arguments = candidate.inputArguments; - if (arguments.isEmpty || !arguments.any(isUnnamed)) return true; - if (arguments.every( - (argument) => - !isUnnamed(argument) || - (argument['has_default'] == true && - _unnamedArgumentTypeIds.contains(argument['type_id'])), - )) { - return true; - } - if (arguments case [final argument] when isUnnamed(argument)) { - return _unnamedArgumentTypeIds.contains(argument['type_id']) || - relationTypesById.containsKey(argument['type_id']); - } - return false; - } - - String? overloadError(_Candidate candidate, List<_Candidate> group) { - final (:function, :inputArguments) = candidate; - final qualifiedName = '${function['schema']}.${function['name']}'; - final soleArgument = inputArguments.length == 1 - ? inputArguments.single - : null; - if (group.length > 1) { - // A no-argument overload next to one whose sole unnamed parameter has - // a default: PostgREST cannot pick one for an empty call. - if (inputArguments.isEmpty) { - for (final other in group) { - if (identical(other, candidate)) continue; - if (other.inputArguments case [ - final argument, - ] when isUnnamed(argument) && argument['has_default'] == true) { - return 'Could not choose the best candidate function between: ' - '$qualifiedName(), $qualifiedName( => ' - '${typeName(other.function['return_type_id'] as int)}). Try ' - 'renaming the parameters or the function itself in the ' - 'database so function overloading can be resolved'; - } - } - } - // Overloads whose sole parameter shares a name but not a type: a call - // carries no type information, so PostgREST cannot pick one. - if (soleArgument != null && !isUnnamed(soleArgument)) { - final conflicting = [ - for (final other in group) - if (!identical(other, candidate)) - if (other.inputArguments case [final argument] - when argument['name'] == soleArgument['name'] && - argument['type_id'] != soleArgument['type_id']) - other, - ]; - if (conflicting.isNotEmpty) { - int firstTypeId(_Candidate other) => - other.inputArguments.firstOrNull?['type_id'] as int? ?? 0; - final ordered = [candidate, ...conflicting]; - mergeSort( - ordered, - compare: (a, b) => firstTypeId(a) - firstTypeId(b), - ); - String signature(_Candidate other) => other.inputArguments - .map( - (argument) => - '${argument['name']} => ' - '${typeName(argument['type_id'] as int)}', - ) - .join(', '); - final conflictList = ordered - .map((other) => '$qualifiedName(${signature(other)})') - .join(', '); - return 'Could not choose the best candidate function between: ' - '$conflictList. Try renaming the parameters or the function ' - 'itself in the database so function overloading can be resolved'; - } - } - } - // A sole unnamed row parameter makes the function a computed field, - // which PostgREST only exposes under `/rpc` when it also returns rows. - if (soleArgument != null && - isUnnamed(soleArgument) && - relationTypesById.containsKey(soleArgument['type_id']) && - returnRelationName(function) == null) { - return 'the function $qualifiedName with parameter or with a single ' - 'unnamed json/jsonb parameter, but no matches were found in the ' - 'schema cache'; - } - return null; - } - - Map resolveOverload( - _Candidate candidate, - List<_Candidate> group, - ) { - final (:function, :inputArguments) = candidate; - final arguments = [ - for (final argument in inputArguments) - { - 'name': argument['name'], - 'mode': argument['mode'], - ...typeReference(argument['type_id'] as int), - 'has_default': argument['has_default'] == true, - 'relation_id': switch (relationTypesById[argument['type_id'] - as int]) { - {'type_relation_id': final int relationId} - when relationsById.containsKey(relationId) => - relationId, - _ => null, - }, - }, - ]; - mergeSort(arguments, compare: _byName); - - final allArguments = (function['args'] as List? ?? const []) - .cast>(); - final tableColumns = [ - for (final argument in allArguments) - if (argument['mode'] == 'table') - { - 'name': argument['name'], - ...typeReference(argument['type_id'] as int), - }, - ]; - mergeSort(tableColumns, compare: _byName); - - final relationName = returnRelationName(function); - final rowsEstimate = function['prorows'] as num?; - final returnsMultipleRows = rowsEstimate != null && rowsEstimate > 1; - final isSetReturning = function['is_set_returning_function'] as bool; - // A function whose sole parameter (of any mode) is a row type is embedded - // from that relation; any other row-returning function embeds from `*`. - final sourceType = allArguments.length == 1 - ? relationTypesById[allArguments.single['type_id'] as int] - : null; - final returnRelationId = function['return_type_relation_id'] as int?; - - return { - 'function_id': function['id'], - 'args': arguments, - 'returns': { - ...typeReference(function['return_type_id'] as int), - 'relation_id': - returnRelationId != null && - relationsById.containsKey(returnRelationId) - ? returnRelationId - : null, - 'columns': tableColumns.isEmpty ? null : tableColumns, - }, - 'is_set_returning': isSetReturning, - 'rows_estimate': rowsEstimate, - 'setof_options': relationName == null - ? null - : { - 'from': sourceType == null ? '*' : sourceType['format'], - 'to': relationName, - 'is_one_to_one': sourceType != null && !isSetReturning - ? true - : !returnsMultipleRows, - 'is_setof_return': isSetReturning, - }, - 'computed_field_of': computedFieldOf(candidate), - 'error': overloadError(candidate, group), - }; - } - - final groups = <(String, String), List<_Candidate>>{}; - for (final function in records('functions')) { - final schema = function['schema'] as String; - if (!schemaNames.contains(schema)) continue; - final candidate = ( - function: function, - inputArguments: [ - for (final argument - in (function['args'] as List? ?? const []) - .cast>()) - if (_inputArgumentModes.contains(argument['mode'])) argument, - ], - ); - if (!isCallable(candidate)) continue; - groups - .putIfAbsent((schema, function['name'] as String), () => []) - .add(candidate); - } - - final entries = [ - for (final MapEntry(key: (schema, name), value: group) in groups.entries) - () { - final sorted = [...group] - ..sort( - (a, b) => _firstNonZero([ - () => localeCompare( - a.function['argument_types'] as String, - b.function['argument_types'] as String, - ), - () => localeCompare( - a.function['return_type'] as String, - b.function['return_type'] as String, - ), - () => (a.function['id'] as int) - (b.function['id'] as int), - ]), - ); - return { - 'schema': schema, - 'name': name, - 'overloads': [ - for (final candidate in sorted) resolveOverload(candidate, sorted), - ], - }; - }(), - ]; - return entries..sort( - (a, b) => _firstNonZero([ - () => localeCompare(a['schema'] as String, b['schema'] as String), - () => localeCompare(a['name'] as String, b['name'] as String), - ]), - ); -} - -int _byName(Map a, Map b) => - localeCompare(a['name'] as String, b['name'] as String); - -int _firstNonZero(List comparisons) { - for (final comparison in comparisons) { - final result = comparison(); - if (result != 0) return result; - } - return 0; -} diff --git a/packages/supabase_typegen/pubspec.yaml b/packages/supabase_typegen/pubspec.yaml index 1dee34650..0db390303 100644 --- a/packages/supabase_typegen/pubspec.yaml +++ b/packages/supabase_typegen/pubspec.yaml @@ -19,15 +19,12 @@ executables: dependencies: args: ^2.7.0 - collection: ^1.19.0 dart_style: ^3.1.0 meta: ^1.16.0 pub_semver: ^2.2.0 - toml: ^0.18.0 yaml: ^3.1.0 dev_dependencies: - postgres: ^3.5.9 postgrest: 3.0.0-dev.10 supabase_lints: ^0.1.2 supabase_test: 0.5.0 diff --git a/packages/supabase_typegen/test/generator_metadata_parser_test.dart b/packages/supabase_typegen/test/generator_metadata_parser_test.dart index 8b1642857..31189f998 100644 --- a/packages/supabase_typegen/test/generator_metadata_parser_test.dart +++ b/packages/supabase_typegen/test/generator_metadata_parser_test.dart @@ -1341,17 +1341,6 @@ void main() { expect(overload('search_books').error, isNull); }); - test('resolves a document that predates rpcFunctions the same way', () { - final legacy = parseGeneratorMetadata( - {...document}..remove('rpcFunctions'), - ); - - expect( - legacy.functions.map((function) => function.name), - schema.functions.map((function) => function.name), - ); - }); - test('lists the functions of the named schemas only', () { final public = parseGeneratorMetadata(document, schemaNames: ['public']); diff --git a/packages/supabase_typegen/test/introspection/introspect_test.dart b/packages/supabase_typegen/test/introspection/introspect_test.dart deleted file mode 100644 index 2018032af..000000000 --- a/packages/supabase_typegen/test/introspection/introspect_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:supabase_typegen/introspection.dart'; -import 'package:test/test.dart'; - -const _schema = { - 'id': 2200, - 'name': 'public', - 'owner': 'postgres', -}; - -const _foreignKey = { - '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'], -}; - -const _viewDependency = { - 'table_schema': 'public', - 'table_name': 'books', - 'view_schema': 'public', - 'view_name': 'book_prices', - 'constraint_name': 'books_author_id_fkey', - 'constraint_type': 'f', - 'column_dependencies': [ - { - 'table_column': 'author_id', - 'view_columns': ['author_id'], - }, - ], -}; - -Map _function(String name, String returnType) => { - 'id': 1, - 'schema': 'public', - 'name': name, - 'args': >[], - 'argument_types': '', - 'return_type_id': 2249, - 'return_type': returnType, - 'return_type_relation_id': null, - 'is_set_returning_function': false, - 'prorows': null, -}; - -void main() { - late _RecordingQueryable database; - - setUp(() { - database = _RecordingQueryable({ - 'schemas': [_schema], - 'tableRelationships': [_foreignKey], - 'viewsKeyDependencies': [_viewDependency], - 'functions': [ - _function('insert_book', 'trigger'), - _function('audit', 'event_trigger'), - _function('search_books', 'SETOF books'), - ], - }); - }); - - test('runs every introspection query in one call', () async { - await introspect(database); - - expect(database.calls, hasLength(1)); - expect(database.calls.single.keys, [ - 'schemas', - 'tables', - 'foreignTables', - 'views', - 'materializedViews', - 'columns', - 'primaryKeys', - 'tableRelationships', - 'viewsKeyDependencies', - 'functions', - 'types', - ]); - }); - - test('assembles the GeneratorMetadata document', () async { - final document = await introspect(database); - - expect(document['version'], generatorMetadataVersion); - expect(document['schemas'], [_schema]); - expect(document['tables'], isEmpty); - expect(document.keys, [ - 'version', - 'schemas', - 'tables', - 'foreignTables', - 'views', - 'materializedViews', - 'columns', - 'primaryKeys', - 'relationships', - 'functions', - 'types', - 'rpcFunctions', - ]); - }); - - test('resolves the functions callable through /rpc', () async { - final document = await introspect(database); - - expect( - (document['rpcFunctions'] as List).map( - (function) => function['name'], - ), - ['search_books'], - ); - }); - - test('appends the view derived relationships to the foreign keys', () async { - final document = await introspect(database); - - expect(document['relationships'], [ - _foreignKey, - {..._foreignKey, 'relation': 'book_prices'}, - ]); - }); - - test('drops trigger functions', () async { - final document = await introspect(database); - - expect( - (document['functions'] as List).map( - (function) => function['name'], - ), - ['search_books'], - ); - }); - - test('applies the schema filter to all queries except types', () async { - await introspect(database, includedSchemas: ['public', 'api']); - - final queries = database.calls.single; - for (final MapEntry(key: name, value: sql) in queries.entries) { - if (name == 'types') { - expect(sql, isNot(contains("IN ('public','api')")), reason: name); - } else { - expect(sql, contains("IN ('public','api')"), reason: name); - } - } - }); - - test('excludes the system schemas by default except for foreign tables ' - 'and materialized views', () async { - await introspect(database); - - final queries = database.calls.single; - const systemExclusion = - "NOT IN ('information_schema','pg_catalog','pg_toast')"; - expect(queries['tables'], contains(systemExclusion)); - expect(queries['columns'], contains(systemExclusion)); - expect(queries['foreignTables'], isNot(contains('NOT IN'))); - expect(queries['materializedViews'], isNot(contains('NOT IN'))); - }); -} - -class _RecordingQueryable implements Queryable { - _RecordingQueryable(this.rows); - - final Map>> rows; - final calls = >[]; - - @override - Future>>> query( - Map queries, - ) async { - calls.add(queries); - return {for (final name in queries.keys) name: rows[name] ?? const []}; - } -} diff --git a/packages/supabase_typegen/test/introspection/parity_test.dart b/packages/supabase_typegen/test/introspection/parity_test.dart deleted file mode 100644 index 3480f6438..000000000 --- a/packages/supabase_typegen/test/introspection/parity_test.dart +++ /dev/null @@ -1,328 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:postgres/postgres.dart'; -import 'package:supabase_typegen/introspection.dart'; -import 'package:test/test.dart'; - -/// Connection URL of a fresh `postgres:15` database without autovacuum, the -/// way `tool/regenerate_fixture.ts` starts one, with `sslmode=disable` since -/// the Supabase CLI requires TLS otherwise. The test seeds it with -/// `test/fixtures/seed.sql` on first use. -final _databaseUrl = - Platform.environment['SUPABASE_TYPEGEN_PARITY_DATABASE_URL'] ?? ''; - -final _whitespace = RegExp(r'\s+'); -final _headerVersion = RegExp(r'supabase_typegen \S+ from'); - -/// `dart run` compiles the binary from source on every invocation. -const _compileTimeout = Timeout(Duration(minutes: 3)); - -void main() { - if (_databaseUrl.isEmpty) { - test( - 'parity with @supabase/postgrest-typegen', - () {}, - skip: - 'Set SUPABASE_TYPEGEN_PARITY_DATABASE_URL to a fresh postgres:15 ' - 'database started with -c autovacuum=off to run the parity tests; ' - 'see the README.', - ); - return; - } - - late Map fixture; - - setUpAll(() async { - fixture = - jsonDecode( - File( - 'test/fixtures/generator_metadata.json', - ).readAsStringSync(), - ) - as Map; - await _seedIfEmpty(_databaseUrl); - }); - - test('the introspection equals the postgrest-typegen fixture', () async { - final document = await introspectDatabase(DatabaseUrl(_databaseUrl)); - - _expectSameDocument(document, fixture); - }); - - test('naming both schemas yields the same document', () async { - final document = await introspectDatabase( - DatabaseUrl(_databaseUrl), - includedSchemas: ['inventory', 'public'], - ); - - _expectSameDocument(document, fixture); - }); - - test('restricting the schemas to public yields its records only', () async { - final document = await introspectDatabase( - DatabaseUrl(_databaseUrl), - includedSchemas: ['public'], - ); - - _expectSameDocument(document, _restrictedToPublic(fixture)); - }); - - test('--dump-metadata writes a document with the fixture records', () async { - final result = await _runBinary([ - '--db-url', - _databaseUrl, - '--schema', - 'inventory,public', - '--dump-metadata', - ]); - - expect(result.exitCode, 0, reason: result.stderr as String); - _expectSameDocument( - jsonDecode(result.stdout as String) as Map, - fixture, - ); - }, timeout: _compileTimeout); - - test('the binary generates public alone without a configuration', () async { - // The repository holds a supabase/config.toml above the package, so the - // lookup is pointed at an empty directory to stand in for a project - // without one; the default schema set is then public, the way - // `supabase gen types` behaves. - final workingDirectory = Directory.systemTemp.createTempSync( - 'supabase_typegen_no_config', - ); - addTearDown(() => workingDirectory.deleteSync(recursive: true)); - final result = await _runBinary( - [ - '--db-url', - _databaseUrl, - '--output', - '-', - ], - environment: {'SUPABASE_WORKDIR': workingDirectory.path}, - ); - - expect(result.exitCode, 0, reason: result.stderr as String); - expect(result.stdout, contains('// Source schemas: public')); - expect(result.stdout, contains('class Books {')); - expect(result.stdout, isNot(contains('class InventoryStock {'))); - expect( - result.stderr, - contains( - 'Generated stdout with 6 tables and 1 enums from schema "public".', - ), - ); - }, timeout: _compileTimeout); - - test('the binary follows api.schemas of the repository configuration ' - 'found above the package', () async { - // supabase/config.toml at the repository root exposes public, - // graphql_public and personal; the fixture database only has public and - // inventory, so public alone is generated and, since nothing was named - // on the command line, nothing is reported as missing. - final result = await _runBinary([ - '--db-url', - _databaseUrl, - '--output', - '-', - ]); - - expect(result.exitCode, 0, reason: result.stderr as String); - expect(result.stdout, contains('// Source schemas: public')); - expect(result.stderr, isNot(contains('has no schema'))); - }, timeout: _compileTimeout); - - test('the binary follows api.schemas of supabase/config.toml', () async { - final workingDirectory = Directory.systemTemp.createTempSync( - 'supabase_typegen_config', - ); - addTearDown(() => workingDirectory.deleteSync(recursive: true)); - File('${workingDirectory.path}/supabase/config.toml') - ..createSync(recursive: true) - ..writeAsStringSync('[api]\nschemas = ["public", "inventory"]\n'); - - final result = await _runBinary( - [ - '--db-url', - _databaseUrl, - '--output', - '-', - ], - environment: {'SUPABASE_WORKDIR': workingDirectory.path}, - ); - final golden = File('test/goldens/supabase_schema.dart').readAsStringSync(); - - expect(result.exitCode, 0, reason: result.stderr as String); - expect(_normalize(result.stdout as String), _normalize(golden)); - }, timeout: _compileTimeout); - - test('the binary warns about a named schema the database lacks', () async { - final result = await _runBinary([ - '--db-url', - _databaseUrl, - '--schema', - 'public,missing', - '--output', - '-', - ]); - - expect(result.exitCode, 0, reason: result.stderr as String); - expect(result.stdout, contains('// Source schemas: public')); - expect(result.stderr, contains('The database has no schema "missing".')); - }, timeout: _compileTimeout); - - test('the binary generates the golden code from the database', () async { - final result = await _runBinary([ - '--db-url', - _databaseUrl, - '--schema', - 'inventory,public', - '--output', - '-', - ]); - final golden = File('test/goldens/supabase_schema.dart').readAsStringSync(); - - expect(result.exitCode, 0, reason: result.stderr as String); - expect(_normalize(result.stdout as String), _normalize(golden)); - expect( - result.stderr, - contains( - 'Generated stdout with 8 tables and 2 enums from schemas ' - '"inventory", "public".', - ), - ); - }, timeout: _compileTimeout); - - test('--schema takes repeated and comma separated names alike', () async { - final repeated = await _runBinary([ - '--db-url', - _databaseUrl, - '--schema', - 'inventory', - '--schema', - 'public', - '--output', - '-', - ]); - final commaSeparated = await _runBinary([ - '--db-url', - _databaseUrl, - '--schema', - 'inventory,public', - '--output', - '-', - ]); - final golden = File('test/goldens/supabase_schema.dart').readAsStringSync(); - - expect(repeated.exitCode, 0, reason: repeated.stderr as String); - expect(commaSeparated.exitCode, 0, reason: commaSeparated.stderr as String); - expect(_normalize(repeated.stdout as String), _normalize(golden)); - expect(_normalize(commaSeparated.stdout as String), _normalize(golden)); - }, timeout: _compileTimeout); - - test('the binary restricts the generated schemas to --schema', () async { - final result = await _runBinary([ - '--db-url', - _databaseUrl, - '--schema', - 'inventory', - '--output', - '-', - ]); - - expect(result.exitCode, 0, reason: result.stderr as String); - expect(result.stdout, contains('// Source schemas: inventory')); - expect(result.stdout, contains('class InventoryStock {')); - expect(result.stdout, isNot(contains('class Authors {'))); - expect( - result.stderr, - contains( - 'Generated stdout with 2 tables and 1 enums from schema "inventory".', - ), - ); - }, timeout: _compileTimeout); -} - -/// The fixture as the introspection of `public` alone reports it: the -/// records of the schema-bearing collections that belong to `public`, with -/// `types` untouched since types are listed for every schema. -Map _restrictedToPublic(Map fixture) => { - for (final MapEntry(:key, :value) in fixture.entries) - key: value is List && key != 'types' - ? [ - for (final record in value.cast>()) - if ((record['schema'] ?? record['name']) == 'public' && - (record['referenced_schema'] ?? 'public') == 'public') - record, - ] - : value, -}; - -String _normalize(String code) => code - .replaceAll(_whitespace, ' ') - .replaceFirst(_headerVersion, 'supabase_typegen from') - .trim(); - -Future _runBinary( - List arguments, { - Map? environment, -}) => Process.run( - Platform.resolvedExecutable, - ['run', 'bin/supabase_typegen.dart', ...arguments], - environment: environment, -); - -/// Compares collection by collection so a mismatch names the record that -/// differs instead of dumping two documents. Records are maps, so the order -/// of their keys does not matter; the order of records within a collection -/// does, since the document is sorted. -void _expectSameDocument( - Map document, - Map fixture, -) { - expect(document.keys, fixture.keys); - for (final collection in fixture.keys) { - final actual = document[collection]; - final expected = fixture[collection]; - if (expected is! List) { - expect(actual, expected, reason: collection); - continue; - } - expect( - (actual as List).length, - expected.length, - reason: '$collection has a different number of records', - ); - for (var i = 0; i < expected.length; i++) { - expect( - actual[i], - expected[i], - reason: - '$collection[$i] differs. The fixture was generated on a fresh ' - 'postgres:15 database without autovacuum, so the object ids only ' - 'match when the database under test was seeded the same way.', - ); - } - } -} - -Future _seedIfEmpty(String databaseUrl) async { - final connection = await Connection.openFromUrl( - databaseUrl.contains('sslmode=') - ? databaseUrl - : '$databaseUrl${databaseUrl.contains('?') ? '&' : '?'}sslmode=disable', - ); - try { - final existing = await connection.execute( - "select to_regclass('public.books') is not null", - ); - if (existing.first.first == true) return; - await connection.execute( - Sql(File('test/fixtures/seed.sql').readAsStringSync()), - queryMode: QueryMode.simple, - ); - } finally { - await connection.close(); - } -} diff --git a/packages/supabase_typegen/test/introspection/pg_format_test.dart b/packages/supabase_typegen/test/introspection/pg_format_test.dart deleted file mode 100644 index 27aac988a..000000000 --- a/packages/supabase_typegen/test/introspection/pg_format_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:supabase_typegen/src/introspection/pg_format.dart'; -import 'package:test/test.dart'; - -void main() { - group('literal', () { - test('quotes strings and doubles embedded quotes', () { - expect(literal('public'), "'public'"); - expect(literal("it's"), "'it''s'"); - expect(literal(''), "''"); - }); - - test('uses the escape string form for backslashes', () { - expect(literal(r'a\b'), r"E'a\\b'"); - expect(literal(r"a\'b"), r"E'a\\''b'"); - }); - - test('quotes numbers like pg-format does', () { - expect(literal(10), "'10'"); - expect(literal(1.5), "'1.5'"); - }); - - test('renders null and booleans', () { - expect(literal(null), 'NULL'); - expect(literal(true), "'t'"); - expect(literal(false), "'f'"); - }); - - test('joins lists and parenthesises nested lists', () { - expect(literal(['a', 'b']), "'a','b'"); - expect( - literal([ - ['a', 'b'], - ['c', 'd'], - ]), - "('a', 'b'), ('c', 'd')", - ); - }); - - test('rejects values pg-format would serialize as JSON', () { - expect(() => literal({'a': 1}), throwsArgumentError); - }); - }); -} diff --git a/packages/supabase_typegen/test/introspection/relationships_test.dart b/packages/supabase_typegen/test/introspection/relationships_test.dart deleted file mode 100644 index a77d9adec..000000000 --- a/packages/supabase_typegen/test/introspection/relationships_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:supabase_typegen/src/introspection/relationships.dart'; -import 'package:test/test.dart'; - -const postsAuthorFk = { - 'foreign_key_name': 'posts_author_id_fkey', - 'schema': 'public', - 'relation': 'posts', - 'columns': ['author_id'], - 'is_one_to_one': false, - 'referenced_schema': 'public', - 'referenced_relation': 'users', - 'referenced_columns': ['id'], -}; - -const viewToTableDep = { - 'table_schema': 'public', - 'table_name': 'posts', - 'view_schema': 'public', - 'view_name': 'posts_view', - 'constraint_name': 'posts_author_id_fkey', - 'constraint_type': 'f', - 'column_dependencies': [ - { - 'table_column': 'author_id', - 'view_columns': ['author_id'], - }, - ], -}; - -const tableToViewDep = { - 'table_schema': 'public', - 'table_name': 'users', - 'view_schema': 'public', - 'view_name': 'users_view', - 'constraint_name': 'posts_author_id_fkey', - 'constraint_type': 'f_ref', - 'column_dependencies': [ - { - 'table_column': 'id', - 'view_columns': ['id'], - }, - ], -}; - -const postsViewToUsers = { - 'foreign_key_name': 'posts_author_id_fkey', - 'schema': 'public', - 'relation': 'posts_view', - 'columns': ['author_id'], - 'is_one_to_one': false, - 'referenced_schema': 'public', - 'referenced_relation': 'users', - 'referenced_columns': ['id'], -}; - -const postsToUsersView = { - 'foreign_key_name': 'posts_author_id_fkey', - 'schema': 'public', - 'relation': 'posts', - 'columns': ['author_id'], - 'is_one_to_one': false, - 'referenced_schema': 'public', - 'referenced_relation': 'users_view', - 'referenced_columns': ['id'], -}; - -const postsViewToUsersView = { - 'foreign_key_name': 'posts_author_id_fkey', - 'schema': 'public', - 'relation': 'posts_view', - 'columns': ['author_id'], - 'is_one_to_one': false, - 'referenced_schema': 'public', - 'referenced_relation': 'users_view', - 'referenced_columns': ['id'], -}; - -void main() { - group('expandViewRelationships', () { - test('expands a view to table dependency', () { - expect(expandViewRelationships([postsAuthorFk], [viewToTableDep]), [ - postsViewToUsers, - ]); - }); - - test('expands a table to view dependency', () { - expect(expandViewRelationships([postsAuthorFk], [tableToViewDep]), [ - postsToUsersView, - ]); - }); - - test('combines both dependency kinds into a view to view relationship', () { - expect( - expandViewRelationships( - [postsAuthorFk], - [viewToTableDep, tableToViewDep], - ), - [postsViewToUsers, postsToUsersView, postsViewToUsersView], - ); - }); - - test('takes the cartesian product over view columns of composite keys', () { - const compositeFk = { - 'foreign_key_name': 'memberships_org_fkey', - 'schema': 'public', - 'relation': 'memberships', - 'columns': ['org_id', 'user_id'], - 'is_one_to_one': true, - 'referenced_schema': 'public', - 'referenced_relation': 'orgs', - 'referenced_columns': ['org_id', 'owner_id'], - }; - const compositeDep = { - 'table_schema': 'public', - 'table_name': 'memberships', - 'view_schema': 'public', - 'view_name': 'memberships_view', - 'constraint_name': 'memberships_org_fkey', - 'constraint_type': 'f', - 'column_dependencies': [ - { - 'table_column': 'org_id', - 'view_columns': ['org_id', 'organization_id'], - }, - { - 'table_column': 'user_id', - 'view_columns': ['user_id', 'member_id'], - }, - ], - }; - - final result = expandViewRelationships([compositeFk], [compositeDep]); - - expect(result.map((relationship) => relationship['columns']), [ - ['org_id', 'user_id'], - ['org_id', 'member_id'], - ['organization_id', 'user_id'], - ['organization_id', 'member_id'], - ]); - expect( - result.every( - (relationship) => - relationship['relation'] == 'memberships_view' && - relationship['is_one_to_one'] == true, - ), - isTrue, - ); - }); - - test('returns nothing when no view depends on the relationship', () { - expect(expandViewRelationships([postsAuthorFk], []), isEmpty); - }); - }); -} diff --git a/packages/supabase_typegen/test/introspection/sort_test.dart b/packages/supabase_typegen/test/introspection/sort_test.dart deleted file mode 100644 index e7c2acc15..000000000 --- a/packages/supabase_typegen/test/introspection/sort_test.dart +++ /dev/null @@ -1,367 +0,0 @@ -import 'package:supabase_typegen/introspection.dart'; -import 'package:supabase_typegen/src/introspection/collation.dart'; -import 'package:test/test.dart'; - -Map _relation( - int id, - String name, { - String schema = 'public', -}) => {'id': id, 'schema': schema, 'name': name}; - -Map _column( - int tableId, - String table, - String name, { - int ordinalPosition = 1, -}) => { - 'table_id': tableId, - 'schema': 'public', - 'table': table, - 'id': '$tableId.$ordinalPosition', - 'ordinal_position': ordinalPosition, - 'name': name, -}; - -Map _function( - int id, - String name, { - String identityArgumentTypes = '', - List> args = const [], -}) => { - 'id': id, - 'schema': 'public', - 'name': name, - 'identity_argument_types': identityArgumentTypes, - 'args': args, -}; - -Map _relationship( - String foreignKeyName, - String relation, - String referencedRelation, - List referencedColumns, -) => { - 'foreign_key_name': foreignKeyName, - 'schema': 'public', - 'relation': relation, - 'columns': ['id'], - 'is_one_to_one': false, - 'referenced_schema': 'public', - 'referenced_relation': referencedRelation, - 'referenced_columns': referencedColumns, -}; - -Map _document({ - List> schemas = const [], - List> tables = const [], - List> views = const [], - List> columns = const [], - List> primaryKeys = const [], - List> relationships = const [], - List> functions = const [], - List> types = const [], -}) => { - 'version': 1, - 'schemas': schemas, - 'tables': tables, - 'foreignTables': >[], - 'views': views, - 'materializedViews': >[], - 'columns': columns, - 'primaryKeys': primaryKeys, - 'relationships': relationships, - 'functions': functions, - 'types': types, -}; - -void main() { - group('localeCompare', () { - test('orders punctuation before digits before letters', () { - expect(localeCompare('col_1', 'col1'), -1); - expect(localeCompare('a_b', 'a1b'), -1); - expect(localeCompare('_int4', 'int4'), -1); - expect(localeCompare('1', 'a'), -1); - expect(localeCompare('a\$', 'a0'), -1); - expect(localeCompare('a~', 'a\$'), -1); - expect(localeCompare('a-b', 'a_b'), 1); - expect(localeCompare('a.b', 'a_b'), 1); - expect(localeCompare('a b', 'a_b'), -1); - }); - - test('ignores case unless the strings are otherwise equal', () { - expect(localeCompare('a', 'A'), -1); - expect(localeCompare('A', 'b'), -1); - expect(localeCompare('Z', 'a'), 1); - expect(localeCompare('Books', 'authors'), 1); - expect(localeCompare('ab', 'Aa'), 1); - expect(localeCompare('abc', 'ABD'), -1); - expect(localeCompare('aB', 'Ab'), -1); - expect(localeCompare('Id', 'iD'), 1); - expect(localeCompare('aA', 'Aa'), -1); - expect(localeCompare('aaa', 'Aa'), 1); - expect(localeCompare('A', 'aa'), -1); - }); - - test('sorts a prefix first', () { - expect(localeCompare('a', 'aa'), -1); - expect(localeCompare('x', 'xa'), -1); - expect(localeCompare('', 'a'), -1); - expect(localeCompare('author_stats', 'authors'), -1); - }); - - test('treats equal strings as equal', () { - expect(localeCompare('books', 'books'), 0); - expect(localeCompare('', ''), 0); - }); - }); - - group('sortGeneratorMetadata', () { - test('orders relations, functions and columns by name, not by oid', () { - final result = sortGeneratorMetadata( - _document( - tables: [_relation(3, 'a'), _relation(1, 'b'), _relation(2, 'c')], - views: [_relation(9, 'v_a'), _relation(5, 'v_b')], - functions: [_function(20, 'f_a'), _function(10, 'f_b')], - columns: [ - _column(1, 'b', 'x'), - _column(3, 'a', 'y', ordinalPosition: 2), - _column(3, 'a', 'z'), - ], - ), - ); - - expect(result['tables'].map((table) => table['name']), ['a', 'b', 'c']); - expect(result['tables'].map((table) => table['id']), [3, 1, 2]); - expect(result['views'].map((view) => view['name']), ['v_a', 'v_b']); - expect(result['functions'].map((function) => function['name']), [ - 'f_a', - 'f_b', - ]); - expect( - result['columns'].map((column) => [column['table'], column['name']]), - [ - ['a', 'y'], - ['a', 'z'], - ['b', 'x'], - ], - ); - }); - - test('orders by schema before name and breaks ties by id', () { - final result = sortGeneratorMetadata( - _document( - types: [ - _relation(2, 'mood'), - _relation(1, 'mood', schema: 'archive'), - _relation(3, 'mood', schema: 'archive'), - ], - ), - ); - - expect( - result['types'].map((type) => [type['schema'], type['id']]), - [ - ['archive', 1], - ['archive', 3], - ['public', 2], - ], - ); - }); - - test('groups primary keys by table and keeps the column order', () { - final result = sortGeneratorMetadata( - _document( - primaryKeys: [ - { - 'schema': 'public', - 'table_name': 'b', - 'name': 'id', - 'table_id': 1, - }, - { - 'schema': 'public', - 'table_name': 'a', - 'name': 'tenant_id', - 'table_id': 3, - }, - { - 'schema': 'public', - 'table_name': 'a', - 'name': 'id', - 'table_id': 3, - }, - ], - ), - ); - - expect( - result['primaryKeys'].map((key) => [key['table_name'], key['name']]), - [ - ['a', 'tenant_id'], - ['a', 'id'], - ['b', 'id'], - ], - ); - }); - - test('disambiguates overloaded functions by signature and sorts args', () { - final result = sortGeneratorMetadata( - _document( - functions: [ - _function( - 2, - 'f', - identityArgumentTypes: 'text', - args: [ - {'name': 'b'}, - {'name': 'a'}, - ], - ), - _function(1, 'f', identityArgumentTypes: 'integer'), - ], - ), - ); - - expect( - result['functions'].map((function) => function['id']), - [1, 2], - ); - expect( - result['functions'].last['args'].map((argument) => argument['name']), - ['a', 'b'], - ); - }); - - test('orders relationships by key, referenced relation and columns', () { - final result = sortGeneratorMetadata( - _document( - relationships: [ - _relationship('b_fkey', 'b', 'users', ['id']), - _relationship('a_fkey', 'a', 'users_view', ['id']), - _relationship('a_fkey', 'a', 'users', ['uuid']), - _relationship('a_fkey', 'a', 'users', ['id']), - ], - ), - ); - - expect( - result['relationships'].map( - (relationship) => [ - relationship['foreign_key_name'], - relationship['referenced_relation'], - relationship['referenced_columns'], - ], - ), - [ - [ - 'a_fkey', - 'users', - ['id'], - ], - [ - 'a_fkey', - 'users', - ['uuid'], - ], - [ - 'a_fkey', - 'users_view', - ['id'], - ], - [ - 'b_fkey', - 'users', - ['id'], - ], - ], - ); - }); - - test('orders the view copies of one foreign key deterministically', () { - final copies = [ - { - ..._relationship('fk', 't_view', 'users', ['id']), - 'columns': ['owner_id'], - }, - { - ..._relationship('fk', 't', 'users', ['id']), - 'schema': 'reporting', - }, - { - ..._relationship('fk', 't', 'users', ['id']), - 'referenced_schema': 'reporting', - }, - { - ..._relationship('fk', 't_view', 'users', ['id']), - 'columns': ['assignee_id'], - }, - _relationship('fk', 't', 'users', ['id']), - ]; - const expected = [ - [ - 'public', - 'public', - 't', - ['id'], - ], - [ - 'public', - 'public', - 't_view', - ['assignee_id'], - ], - [ - 'public', - 'public', - 't_view', - ['owner_id'], - ], - [ - 'public', - 'reporting', - 't', - ['id'], - ], - [ - 'reporting', - 'public', - 't', - ['id'], - ], - ]; - List> order(Map result) => [ - for (final relationship in result['relationships'] as List) - [ - relationship['referenced_schema'], - relationship['schema'], - relationship['relation'], - relationship['columns'], - ], - ]; - - expect( - order(sortGeneratorMetadata(_document(relationships: copies))), - expected, - ); - expect( - order( - sortGeneratorMetadata( - _document(relationships: copies.reversed.toList()), - ), - ), - expected, - ); - }); - - test('does not mutate the input and keeps the version', () { - final tables = [_relation(2, 'b'), _relation(1, 'a')]; - final document = _document(tables: tables); - - final result = sortGeneratorMetadata(document); - - expect(result['version'], 1); - expect(tables.map((table) => table['name']), ['b', 'a']); - expect(result['tables'].map((table) => table['name']), ['a', 'b']); - }); - }); -} diff --git a/packages/supabase_typegen/test/introspection/sql_test.dart b/packages/supabase_typegen/test/introspection/sql_test.dart deleted file mode 100644 index c7bbba60d..000000000 --- a/packages/supabase_typegen/test/introspection/sql_test.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:supabase_typegen/src/introspection/sql/columns_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/functions_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/helpers.dart'; -import 'package:supabase_typegen/src/introspection/sql/schemas_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/table_relationships_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/types_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/views_key_dependencies_sql.dart'; -import 'package:test/test.dart'; - -void main() { - group('filterByList', () { - test('produces a NOT IN clause for the default system schemas', () { - expect( - filterByList(defaultExclude: defaultSystemSchemas), - "NOT IN ('information_schema','pg_catalog','pg_toast')", - ); - }); - - test('produces an IN clause for included schemas', () { - expect( - filterByList( - include: ['public', 'api'], - defaultExclude: defaultSystemSchemas, - ), - "IN ('public','api')", - ); - }); - - test('prepends the default exclusions to the excluded schemas', () { - expect( - filterByList( - exclude: ['graphql'], - defaultExclude: defaultSystemSchemas, - ), - "NOT IN ('information_schema','pg_catalog','pg_toast','graphql')", - ); - }); - - test('quotes schema names', () { - expect(filterByList(include: ["it's"]), "IN ('it''s')"); - }); - - test('returns the empty string when there is nothing to filter', () { - expect(filterByList(), ''); - expect(filterByList(include: [], exclude: []), ''); - }); - }); - - final schemaFilter = filterByList(defaultExclude: defaultSystemSchemas); - - group('SQL builders', () { - test('render the schemas query of the generator path', () { - expect(schemasSql(schemaFilter: schemaFilter), ''' - --- Adapted from information_schema.schemata -select - n.oid::int8 as id, - n.nspname as name, - u.rolname as owner -from - pg_namespace n, - pg_roles u -where - n.nspowner = u.oid - and n.nspname NOT IN ('information_schema','pg_catalog','pg_toast') - and not pg_catalog.starts_with(n.nspname, 'pg_') - and ( - pg_has_role(n.nspowner, 'USAGE') - or has_schema_privilege(n.oid, 'CREATE, USAGE') - ) - and not pg_catalog.starts_with(n.nspname, 'pg_temp_') - and not pg_catalog.starts_with(n.nspname, 'pg_toast_temp_') -'''); - }); - - test('leave the filter line blank when no filter is set', () { - final sql = columnsSql(schemaFilter: ''); - expect(sql, isNot(contains('nc.nspname NOT IN'))); - expect(sql, contains('WHERE\n \n NOT pg_is_other_temp_schema')); - }); - - test('interpolate the schema filter into every clause', () { - expect( - columnsSql(schemaFilter: schemaFilter), - contains( - 'nc.nspname NOT IN ' - "('information_schema','pg_catalog','pg_toast') AND", - ), - ); - expect( - tableRelationshipsSql(schemaFilter: schemaFilter), - allOf( - contains( - 'and connamespace::regnamespace::text NOT IN ' - "('information_schema','pg_catalog','pg_toast')", - ), - contains( - 'WHERE traint.connamespace::regnamespace::text NOT IN ' - "('information_schema','pg_catalog','pg_toast')", - ), - contains( - 'and ns1.nspname NOT IN ' - "('information_schema','pg_catalog','pg_toast')", - ), - ), - ); - expect( - viewsKeyDependenciesSql(schemaFilter: schemaFilter), - contains( - 'where view_schema NOT IN ' - "('information_schema','pg_catalog','pg_toast')", - ), - ); - }); - - test('fall back to true where an unfiltered clause needs a predicate', () { - expect( - tableRelationshipsSql(schemaFilter: ''), - contains('WHERE true\n'), - ); - expect( - viewsKeyDependenciesSql(schemaFilter: ''), - contains('where true\n'), - ); - }); - - test('only join pg_namespace in the functions query when filtering', () { - expect( - functionsSql(schemaFilter: schemaFilter), - contains('join pg_namespace n on p.pronamespace = n.oid'), - ); - expect( - functionsSql(schemaFilter: ''), - isNot(contains('join pg_namespace n on p.pronamespace = n.oid')), - ); - }); - - test('list every type including table row types and array types', () { - expect(typesSql, contains("c.relkind in ('c', 'r', 'v', 'm', 'p', 'f')")); - expect(typesSql, isNot(contains('and not exists'))); - expect(typesSql, isNot(contains('and n.nspname'))); - }); - - test('render the view definition rewrites as nested calls', () { - final sql = viewsKeyDependenciesSql(schemaFilter: ''); - expect(sql, contains(' regexp_replace(\n')); - expect( - sql, - contains(" view_definition::text,\n '<>', '()'),\n"), - ); - expect(sql, contains(" ' :[^}{,]+', ',\"\":', 'g'),\n")); - expect(sql, contains(" ' ', ',')::json as view_definition\n")); - expect(nodeTreeToJsonRewrites, hasLength(19)); - expect(nodeTreeToJson('x'), contains(r"E'\\{', ''")); - }); - }); -} diff --git a/packages/supabase_typegen/test/introspection/supabase_cli_test.dart b/packages/supabase_typegen/test/introspection/supabase_cli_test.dart deleted file mode 100644 index 7d33ce9f2..000000000 --- a/packages/supabase_typegen/test/introspection/supabase_cli_test.dart +++ /dev/null @@ -1,269 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:supabase_typegen/introspection.dart'; -import 'package:test/test.dart'; - -void main() { - group('combineQueries', () { - test('aggregates every query into one select', () { - final sql = combineQueries({ - 'schemas': 'select 1 as id', - 'tables': 'with t as (select 2 as id) select id from t', - }); - - expect(sql, ''' -select - (select coalesce(json_agg(t), '[]'::json) from ( -select 1 as id - ) t) as "schemas", - (select coalesce(json_agg(t), '[]'::json) from ( -with t as (select 2 as id) select id from t - ) t) as "tables"'''); - }); - }); - - group('describeFailure', () { - test('explains a missing login', () { - expect( - describeFailure( - 'Access token not provided. Supply an access token by running ' - '`supabase login` or setting the SUPABASE_ACCESS_TOKEN environment ' - 'variable.\nTry rerunning the command with --debug.', - ), - allOf(contains('not logged in'), contains('supabase login')), - ); - }); - - test('explains a missing link', () { - expect( - describeFailure('Cannot find project ref. Have you run supabase link?'), - allOf(contains('supabase link'), contains('--project-ref')), - ); - }); - - test('explains --project-ref on a CLI without it', () { - expect( - describeFailure( - 'DESCRIPTION\n Execute a SQL query against the database.\n' - 'USAGE\n supabase db query [flags] []\n', - target: const LinkedProject(projectRef: 'abcdefghijklmnopqrst'), - ), - allOf( - contains('does not accept --project-ref'), - contains('supabase link --project-ref abcdefghijklmnopqrst'), - ), - ); - }); - - test('redacts passwords of connection strings', () { - expect( - describeFailure( - 'failed to connect to postgres: cannot parse ' - '`postgresql://postgres:s3cret@db.example.com:5432/postgres`', - ), - allOf( - contains('postgresql://postgres:***@db.example.com:5432/postgres'), - isNot(contains('s3cret')), - ), - ); - }); - - test('passes other failures through without the progress line', () { - expect( - describeFailure( - 'Connecting to local database...\n' - 'failed to connect to postgres: ECONNREFUSED\n', - ), - 'supabase db query failed:\n' - 'failed to connect to postgres: ECONNREFUSED', - ); - }); - }); - - group('SupabaseCliQueryable', () { - late Directory directory; - - setUp(() { - directory = Directory.systemTemp.createTempSync('supabase_cli_test_'); - }); - - tearDown(() => directory.deleteSync(recursive: true)); - - File fakeCli(String script) { - final file = File('${directory.path}/supabase') - ..writeAsStringSync('#!/bin/sh\n$script\n'); - Process.runSync('chmod', ['+x', file.path]); - return file; - } - - test('reports a CLI that is not installed', () { - const database = SupabaseCliQueryable( - LocalDatabase(), - executable: 'supabase-typegen-missing-binary', - ); - - expect( - () => database.query({'schemas': 'select 1'}), - throwsA( - isA().having( - (error) => error.message, - 'message', - allOf( - contains('could not be started'), - contains('supabase-typegen-missing-binary'), - ), - ), - ), - ); - }); - - test('reports a CLI that is not logged in', () { - final cli = fakeCli( - r'printf "\033[31mAccess token not provided. Supply an access token ' - r'by running `supabase login`\033[39m\n" >&2; exit 1', - ); - final database = SupabaseCliQueryable( - const LinkedProject(), - executable: cli.path, - ); - - expect( - () => database.query({'schemas': 'select 1'}), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('supabase login'), - ), - ), - ); - }); - - test('passes the target flags and reads the rows of each query', () async { - final cli = fakeCli( - 'echo "\$@" > "${directory.path}/arguments"\n' - 'cat "\$9" > "${directory.path}/statement"\n' - 'echo \'{"rows":[{"schemas":[{"id":1,"name":"public"}],' - '"tables":[]}],"warning":null}\'', - ); - final database = SupabaseCliQueryable( - const LinkedProject(projectRef: 'abcdefghijklmnopqrst'), - executable: cli.path, - ); - - final rows = await database.query({ - 'schemas': 'select 1 as id', - 'tables': 'select 2 as id', - }); - - expect(rows, { - 'schemas': [ - {'id': 1, 'name': 'public'}, - ], - 'tables': >[], - }); - final arguments = File( - '${directory.path}/arguments', - ).readAsStringSync().trim().split(' '); - expect(arguments.sublist(0, 7), [ - 'db', - 'query', - '--linked', - '--project-ref', - 'abcdefghijklmnopqrst', - '--output', - 'json', - ]); - expect(arguments[7], '--file'); - expect( - File('${directory.path}/statement').readAsStringSync(), - contains('as "schemas"'), - ); - }); - - test('reads rows printed as a bare array by older CLI releases', () async { - final cli = fakeCli( - 'echo \'[{"schemas":[{"id":1,"name":"public"}]}]\'', - ); - final database = SupabaseCliQueryable( - const LocalDatabase(), - executable: cli.path, - ); - - expect(await database.query({'schemas': 'select 1 as id'}), { - 'schemas': [ - {'id': 1, 'name': 'public'}, - ], - }); - }); - - test('rejects a result without one of the collections', () { - final cli = fakeCli('echo \'[{"schemas":[]}]\''); - final database = SupabaseCliQueryable( - const LocalDatabase(), - executable: cli.path, - ); - - expect( - () => database.query({'schemas': 'select 1', 'tables': 'select 2'}), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('no "tables" collection'), - ), - ), - ); - }); - - test('rejects an unexpected result shape', () { - final cli = fakeCli('echo \'{"rows":[]}\''); - final database = SupabaseCliQueryable( - const LocalDatabase(), - executable: cli.path, - ); - - expect( - () => database.query({'schemas': 'select 1'}), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('unexpected result shape'), - ), - ), - ); - }); - - test('rejects output that is not JSON', () { - final cli = fakeCli('echo "Connecting to local database..."'); - final database = SupabaseCliQueryable( - const LocalDatabase(), - executable: cli.path, - ); - - expect( - () => database.query({'schemas': 'select 1'}), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('did not return JSON'), - ), - ), - ); - }); - - test('renders the target flags', () { - expect(const LocalDatabase().arguments, ['--local']); - expect(const LinkedProject().arguments, ['--linked']); - expect(const DatabaseUrl('postgresql://x').arguments, [ - '--db-url', - 'postgresql://x', - ]); - expect(jsonEncode(const LinkedProject(projectRef: 'r').arguments), ''' -["--linked","--project-ref","r"]'''); - }); - }); -} diff --git a/packages/supabase_typegen/test/introspection/supabase_config_test.dart b/packages/supabase_typegen/test/introspection/supabase_config_test.dart deleted file mode 100644 index 2058d3f6b..000000000 --- a/packages/supabase_typegen/test/introspection/supabase_config_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:io'; - -import 'package:supabase_typegen/introspection.dart'; -import 'package:test/test.dart'; - -void main() { - late Directory workingDirectory; - - setUp(() { - workingDirectory = Directory.systemTemp.createTempSync( - 'supabase_typegen_config', - ); - }); - - tearDown(() => workingDirectory.deleteSync(recursive: true)); - - Map environment() => { - 'SUPABASE_WORKDIR': workingDirectory.path, - }; - - void writeConfig(String contents) { - File('${workingDirectory.path}/supabase/config.toml') - ..createSync(recursive: true) - ..writeAsStringSync(contents); - } - - test('is public alone without a configuration file', () { - expect(defaultSchemas(environment: environment()), ['public']); - }); - - test('finds the configuration of the nearest parent directory', () { - writeConfig('[api]\nschemas = ["public", "inventory"]\n'); - final nested = Directory('${workingDirectory.path}/packages/app') - ..createSync(recursive: true); - - expect(defaultSchemas(environment: {}, startDirectory: nested), [ - 'inventory', - 'public', - ]); - }); - - test('SUPABASE_WORKDIR wins over the start directory', () { - writeConfig('[api]\nschemas = ["public", "inventory"]\n'); - final other = Directory.systemTemp.createTempSync('supabase_typegen_other'); - addTearDown(() => other.deleteSync(recursive: true)); - File('${other.path}/supabase/config.toml') - ..createSync(recursive: true) - ..writeAsStringSync('[api]\nschemas = ["public", "archive"]\n'); - - expect(defaultSchemas(environment: environment(), startDirectory: other), [ - 'inventory', - 'public', - ]); - }); - - test('adds the exposed schemas of the configuration, sorted', () { - writeConfig(''' -[api] -enabled = true -schemas = ["public", "graphql_public", "personal"] -'''); - - expect(defaultSchemas(environment: environment()), [ - 'graphql_public', - 'personal', - 'public', - ]); - }); - - test('always includes public', () { - writeConfig('[api]\nschemas = ["inventory"]\n'); - - expect(defaultSchemas(environment: environment()), [ - 'inventory', - 'public', - ]); - }); - - test('is public alone when the configuration names no schemas', () { - writeConfig('[api]\nenabled = true\n'); - - expect(defaultSchemas(environment: environment()), ['public']); - }); - - test('rejects a schema list that is not made of strings', () { - writeConfig('[api]\nschemas = ["public", 1]\n'); - - expect( - () => defaultSchemas(environment: environment()), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('api.schemas'), - ), - ), - ); - }); - - test('rejects a configuration that is not TOML', () { - writeConfig('[api\n'); - - expect( - () => defaultSchemas(environment: environment()), - throwsA(isA()), - ); - }); -} diff --git a/packages/supabase_typegen/test/rpc_functions_test.dart b/packages/supabase_typegen/test/rpc_functions_test.dart deleted file mode 100644 index d2e77a219..000000000 --- a/packages/supabase_typegen/test/rpc_functions_test.dart +++ /dev/null @@ -1,346 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:supabase_typegen/introspection.dart'; -import 'package:supabase_typegen/src/rpc_functions.dart'; -import 'package:test/test.dart'; - -const _int4 = 23; -const _text = 25; -const _jsonb = 3802; -const _todosRowType = 500; - -Map _argument( - String name, - int typeId, { - String mode = 'in', - bool hasDefault = false, -}) => { - 'mode': mode, - 'name': name, - 'type_id': typeId, - 'has_default': hasDefault, -}; - -Map _function( - String name, { - int id = 1, - String schema = 'public', - List> arguments = const [], - String argumentTypes = '', - int returnTypeId = _int4, - String returnType = 'integer', - int? returnTypeRelationId, - bool isSetReturning = false, - int? prorows, -}) => { - 'id': id, - 'schema': schema, - 'name': name, - 'args': arguments, - 'argument_types': argumentTypes, - 'return_type_id': returnTypeId, - 'return_type': returnType, - 'return_type_relation_id': returnTypeRelationId, - 'is_set_returning_function': isSetReturning, - 'prorows': prorows, -}; - -Map _type( - int id, - String name, { - String schema = 'pg_catalog', - int? relationId, -}) => { - 'id': id, - 'name': name, - 'schema': schema, - 'format': name, - 'enums': [], - 'type_relation_id': relationId, -}; - -Map _document( - List> functions, { - List schemas = const ['public'], -}) => { - 'schemas': [ - for (final (index, schema) in schemas.indexed) - {'id': index, 'name': schema}, - ], - 'tables': [ - {'id': 1, 'schema': 'public', 'name': 'todos'}, - ], - 'columns': [ - {'table_id': 1, 'name': 'id'}, - {'table_id': 1, 'name': 'title'}, - ], - 'functions': functions, - 'types': [ - _type(_int4, 'int4'), - _type(_text, 'text'), - _type(_jsonb, 'jsonb'), - _type(_todosRowType, 'todos', schema: 'public', relationId: 1), - ], -}; - -List> _overloads(Map function) => - (function['overloads'] as List).cast(); - -void main() { - test('resolves the fixture database like @supabase/postgrest-typegen', () { - final fixture = - jsonDecode( - File('test/fixtures/generator_metadata.json').readAsStringSync(), - ) - as Map; - final withoutRpcFunctions = {...fixture}..remove('rpcFunctions'); - - final resolved = sortGeneratorMetadata({ - ...withoutRpcFunctions, - 'rpcFunctions': resolveRpcFunctions(withoutRpcFunctions), - }); - - expect(resolved['rpcFunctions'], fixture['rpcFunctions']); - }); - - test('leaves out functions with an unnamed parameter PostgREST cannot ' - 'pass', () { - final resolved = resolveRpcFunctions( - _document([ - _function('by_text', arguments: [_argument('', _text)]), - _function( - 'mixed', - id: 2, - arguments: [_argument('', _int4), _argument('b', _int4)], - ), - _function( - 'defaulted', - id: 3, - arguments: [ - _argument('', _jsonb, hasDefault: true), - _argument('b', _int4), - ], - ), - ]), - ); - - expect(resolved.map((function) => function['name']), [ - 'by_text', - 'defaulted', - ]); - }); - - test('reads only the input parameters and the RETURNS TABLE columns', () { - final [function] = resolveRpcFunctions( - _document([ - _function( - 'counts', - arguments: [ - _argument('since', _int4), - _argument('total', _int4, mode: 'out'), - _argument('name', _text, mode: 'table'), - ], - returnType: 'TABLE(name text)', - isSetReturning: true, - ), - ]), - ); - final [overload] = _overloads(function); - - expect( - (overload['args'] as List).map((argument) => argument['name']), - ['since'], - ); - expect(overload['returns'], { - 'type_id': _int4, - 'format': 'int4', - 'type_schema': 'pg_catalog', - 'relation_id': null, - 'columns': [ - { - 'name': 'name', - 'type_id': _text, - 'format': 'text', - 'type_schema': 'pg_catalog', - }, - ], - }); - }); - - test('marks rows of a relation as embeddable from the row parameter', () { - final [function] = resolveRpcFunctions( - _document([ - _function( - 'similar', - arguments: [_argument('todo', _todosRowType)], - argumentTypes: 'todo todos', - returnTypeId: _todosRowType, - returnType: 'SETOF todos', - returnTypeRelationId: 1, - isSetReturning: true, - prorows: 1000, - ), - ]), - ); - final [overload] = _overloads(function); - - expect((overload['args'] as List).single['relation_id'], 1); - expect(overload['setof_options'], { - 'from': 'todos', - 'to': 'todos', - 'is_one_to_one': false, - 'is_setof_return': true, - }); - expect(overload['computed_field_of'], 1); - expect(overload['error'], isNull); - }); - - test('a computed field named like a column is none', () { - final [function] = resolveRpcFunctions( - _document([ - _function( - 'title', - arguments: [_argument('todo', _todosRowType)], - argumentTypes: 'todo todos', - returnTypeId: _text, - returnType: 'text', - ), - ]), - ); - - expect(_overloads(function).single['computed_field_of'], isNull); - }); - - test('a scalar computed field with an unnamed row parameter is an ' - 'error', () { - final [function] = resolveRpcFunctions( - _document([ - _function( - 'title_upper', - arguments: [_argument('', _todosRowType)], - argumentTypes: 'todos', - returnTypeId: _text, - returnType: 'text', - ), - ]), - ); - - expect( - _overloads(function).single['error'], - 'the function public.title_upper with parameter or with a single ' - 'unnamed json/jsonb parameter, but no matches were found in the schema ' - 'cache', - ); - }); - - test('a parameter of a type the document lacks falls back to the argument ' - 'list, a known scalar never does', () { - final resolved = resolveRpcFunctions( - _document([ - _function( - 'unknown_type', - arguments: [_argument('todo', 9999)], - argumentTypes: 'todos', - ), - _function( - 'known_type', - id: 2, - arguments: [_argument('todo', _int4)], - argumentTypes: 'todos', - ), - ]), - ); - - expect( - { - for (final function in resolved) - function['name']: _overloads(function).single['computed_field_of'], - }, - {'known_type': null, 'unknown_type': 1}, - ); - }); - - test('overloads PostgREST cannot tell apart carry an error', () { - final [emptyCall, singleParameter] = resolveRpcFunctions( - _document([ - _function('ping', returnTypeId: _int4, returnType: 'integer'), - _function( - 'ping', - id: 2, - arguments: [_argument('', _text, hasDefault: true)], - argumentTypes: 'text DEFAULT NULL', - returnTypeId: _text, - returnType: 'text', - ), - _function( - 'label', - id: 3, - arguments: [_argument('amount', _int4)], - argumentTypes: 'amount integer', - ), - _function( - 'label', - id: 4, - arguments: [_argument('amount', _text)], - argumentTypes: 'amount text', - ), - ]), - ); - - expect(emptyCall['name'], 'label'); - expect( - _overloads(emptyCall).map((overload) => overload['error']), - everyElement( - 'Could not choose the best candidate function between: ' - 'public.label(amount => int4), public.label(amount => text). Try ' - 'renaming the parameters or the function itself in the database so ' - 'function overloading can be resolved', - ), - ); - expect( - _overloads(singleParameter).first['error'], - 'Could not choose the best candidate function between: public.ping(), ' - 'public.ping( => text). Try renaming the parameters or the function ' - 'itself in the database so function overloading can be resolved', - ); - expect(_overloads(singleParameter).last['error'], isNull); - }); - - test('orders overloads by argument list and keeps schemas apart', () { - final resolved = resolveRpcFunctions( - _document( - [ - _function( - 'find', - arguments: [_argument('b', _int4)], - argumentTypes: 'b integer', - ), - _function( - 'find', - id: 2, - arguments: [_argument('a', _int4)], - argumentTypes: 'a integer', - ), - _function('c', id: 3, schema: 'a.b'), - _function('b.c', id: 4, schema: 'a'), - _function('hidden', id: 5, schema: 'private'), - ], - schemas: ['public', 'a', 'a.b'], - ), - ); - - String describe(Map function) { - final ids = _overloads(function).map( - (overload) => overload['function_id'], - ); - return '${function['schema']}|${function['name']}|${ids.join(',')}'; - } - - expect(resolved.map(describe), [ - 'a|b.c|4', - 'a.b|c|3', - 'public|find|2,1', - ]); - }); -} diff --git a/packages/supabase_typegen/tool/check_introspection_drift.ts b/packages/supabase_typegen/tool/check_introspection_drift.ts deleted file mode 100644 index 9cd3f85cd..000000000 --- a/packages/supabase_typegen/tool/check_introspection_drift.ts +++ /dev/null @@ -1,184 +0,0 @@ -// Guards the vendored introspection SQL in lib/src/introspection/sql against -// drifting from @supabase/postgrest-typegen. It fetches the TypeScript builders -// of the pinned supabase/sdk revision (or another ref), renders every query -// with them and with the Dart port for the schema filters introspect() uses, -// and fails on any difference. -// -// Run from the package root: -// -// bun tool/check_introspection_drift.ts # against the pinned revision -// bun tool/check_introspection_drift.ts --latest # against supabase/sdk main -// bun tool/check_introspection_drift.ts --ref postgrest-typegen-v0.2.1 -// -// A diff against a newer ref is the list of changes a pin bump has to port; a -// diff against the pinned revision means the Dart port was edited. - -import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { $ } from "bun"; - -type Scenario = { included?: string[]; excluded?: string[] }; - -const SCENARIOS: Record = { - unfiltered: {}, - included: { included: ["public", "it's"] }, - excluded: { excluded: ["graphql", "extensions"] }, -}; - -const SOURCE_FILES = [ - "common.ts", - "helpers.ts", - "pg-format.ts", - "schemas.sql.ts", - "table.sql.ts", - "foreign_tables.sql.ts", - "views.sql.ts", - "materialized_views.sql.ts", - "columns.sql.ts", - "primary_keys.sql.ts", - "table_relationships.sql.ts", - "views_key_dependencies.sql.ts", - "functions.sql.ts", - "types.sql.ts", -]; - -const dart = JSON.parse( - await $`dart run tool/dump_introspection_sql.dart`.text(), -) as { revision: string; queries: Record> }; - -const ref = resolveRef(dart.revision); -console.log( - `Comparing the Dart port (pinned to ${dart.revision}) against supabase/sdk@${ref}`, -); - -const workdir = await mkdtemp(join(tmpdir(), "supabase_typegen_drift_")); -try { - const sqlDir = join(workdir, "sql"); - await mkdir(sqlDir); - let needsPgFormatPackage = false; - for (const file of SOURCE_FILES) { - const response = await fetch( - `https://raw.githubusercontent.com/supabase/sdk/${ref}/packages/postgrest-typegen/src/introspection/sql/${file}`, - ); - if (response.status === 404 && file === "pg-format.ts") { - // Releases before 0.2.1 used the pg-format npm package instead. - needsPgFormatPackage = true; - continue; - } - if (!response.ok) { - throw new Error(`Fetching ${file} at ${ref} answered ${response.status}`); - } - await writeFile(join(sqlDir, file), await response.text()); - } - if (needsPgFormatPackage) { - await writeFile( - join(workdir, "package.json"), - JSON.stringify({ - name: "drift-check", - private: true, - dependencies: { "pg-format": "1.0.4" }, - }), - ); - await $`bun install --cwd ${workdir} --silent`.quiet(); - } - - const load = async (file: string) => import(join(sqlDir, file)); - const helpers = await load("helpers.ts"); - const builders = { - schemas: (await load("schemas.sql.ts")).SCHEMAS_SQL, - tables: (await load("table.sql.ts")).TABLES_SQL, - foreign_tables: (await load("foreign_tables.sql.ts")).FOREIGN_TABLES_SQL, - views: (await load("views.sql.ts")).VIEWS_SQL, - materialized_views: (await load("materialized_views.sql.ts")) - .MATERIALIZED_VIEWS_SQL, - columns: (await load("columns.sql.ts")).COLUMNS_SQL, - primary_keys: (await load("primary_keys.sql.ts")).PRIMARY_KEYS_SQL, - table_relationships: (await load("table_relationships.sql.ts")) - .TABLE_RELATIONSHIPS_SQL, - views_key_dependencies: (await load("views_key_dependencies.sql.ts")) - .VIEWS_KEY_DEPENDENCIES_SQL, - functions: (await load("functions.sql.ts")).FUNCTIONS_SQL, - types: (await load("types.sql.ts")).TYPES_SQL, - }; - - let differences = 0; - for (const [scenario, { included, excluded }] of Object.entries(SCENARIOS)) { - // Mirrors introspect(): system schemas excluded for most queries, plain - // include/exclude for foreign tables and materialized views, no schema - // filter for types. - const systemExcludingFilter = helpers.filterByList( - included, - excluded, - helpers.DEFAULT_SYSTEM_SCHEMAS, - ); - const plainFilter = helpers.filterByList(included, excluded); - const expected: Record = { - schemas: builders.schemas({ schemaFilter: systemExcludingFilter }), - tables: builders.tables({ schemaFilter: systemExcludingFilter }), - foreign_tables: builders.foreign_tables({ schemaFilter: plainFilter }), - views: builders.views({ schemaFilter: systemExcludingFilter }), - materialized_views: builders.materialized_views({ - schemaFilter: plainFilter, - }), - columns: builders.columns({ schemaFilter: systemExcludingFilter }), - primary_keys: builders.primary_keys({ - schemaFilter: systemExcludingFilter, - }), - table_relationships: builders.table_relationships({ - schemaFilter: systemExcludingFilter, - }), - views_key_dependencies: builders.views_key_dependencies({ - schemaFilter: systemExcludingFilter, - }), - functions: builders.functions({ schemaFilter: systemExcludingFilter }), - types: - typeof builders.types === "string" ? builders.types : builders.types(), - }; - - for (const [query, expectedSql] of Object.entries(expected)) { - const actualSql = dart.queries[scenario]?.[query]; - if (actualSql === expectedSql) continue; - differences++; - console.log(`\n${query} (${scenario}) differs:`); - printDiff(expectedSql, actualSql ?? ""); - } - } - - if (differences > 0) { - console.error( - `\n${differences} quer${differences === 1 ? "y" : "ies"} differ from supabase/sdk@${ref}.`, - ); - process.exit(1); - } - console.log(`All queries match supabase/sdk@${ref}.`); -} finally { - await rm(workdir, { recursive: true, force: true }); -} - -function resolveRef(pinned: string): string { - const args = process.argv.slice(2); - const refIndex = args.indexOf("--ref"); - if (refIndex !== -1) { - const requested = args[refIndex + 1]; - if (!requested) throw new Error("--ref needs a value"); - return requested; - } - return args.includes("--latest") ? "main" : pinned; -} - -/** Prints the lines that differ, with the TypeScript output as `-` and the Dart output as `+`. */ -function printDiff(expected: string, actual: string): void { - const expectedLines = expected.split("\n"); - const actualLines = actual.split("\n"); - const length = Math.max(expectedLines.length, actualLines.length); - for (let i = 0; i < length; i++) { - if (expectedLines[i] === actualLines[i]) continue; - if (expectedLines[i] !== undefined) { - console.log(` -${i + 1}: ${JSON.stringify(expectedLines[i])}`); - } - if (actualLines[i] !== undefined) { - console.log(` +${i + 1}: ${JSON.stringify(actualLines[i])}`); - } - } -} diff --git a/packages/supabase_typegen/tool/dump_introspection_sql.dart b/packages/supabase_typegen/tool/dump_introspection_sql.dart deleted file mode 100644 index cebb827e5..000000000 --- a/packages/supabase_typegen/tool/dump_introspection_sql.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:convert'; - -import 'package:supabase_typegen/introspection.dart'; -import 'package:supabase_typegen/src/introspection/sql/columns_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/foreign_tables_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/functions_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/helpers.dart'; -import 'package:supabase_typegen/src/introspection/sql/materialized_views_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/primary_keys_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/schemas_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/table_relationships_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/tables_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/types_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/views_key_dependencies_sql.dart'; -import 'package:supabase_typegen/src/introspection/sql/views_sql.dart'; - -/// Prints, as JSON, the SQL the Dart port renders for the schema filters -/// `tool/check_introspection_drift.ts` compares against the TypeScript -/// builders, keyed by scenario and query name. -void main() { - const scenarios = - ? included, List? excluded})>{ - 'unfiltered': (included: null, excluded: null), - 'included': (included: ['public', "it's"], excluded: null), - 'excluded': (included: null, excluded: ['graphql', 'extensions']), - }; - - print( - jsonEncode({ - 'revision': postgrestTypegenRevision, - 'queries': { - for (final MapEntry(key: scenario, value: filter) in scenarios.entries) - scenario: _queries( - included: filter.included, - excluded: filter.excluded, - ), - }, - }), - ); -} - -Map _queries({ - required List? included, - required List? excluded, -}) { - final systemExcludingFilter = filterByList( - include: included, - exclude: excluded, - defaultExclude: defaultSystemSchemas, - ); - final plainFilter = filterByList(include: included, exclude: excluded); - return { - 'schemas': schemasSql(schemaFilter: systemExcludingFilter), - 'tables': tablesSql(schemaFilter: systemExcludingFilter), - 'foreign_tables': foreignTablesSql(schemaFilter: plainFilter), - 'views': viewsSql(schemaFilter: systemExcludingFilter), - 'materialized_views': materializedViewsSql(schemaFilter: plainFilter), - 'columns': columnsSql(schemaFilter: systemExcludingFilter), - 'primary_keys': primaryKeysSql(schemaFilter: systemExcludingFilter), - 'table_relationships': tableRelationshipsSql( - schemaFilter: systemExcludingFilter, - ), - 'views_key_dependencies': viewsKeyDependenciesSql( - schemaFilter: systemExcludingFilter, - ), - 'functions': functionsSql(schemaFilter: systemExcludingFilter), - 'types': typesSql, - }; -} diff --git a/packages/supabase_typegen/tool/regenerate_fixture.ts b/packages/supabase_typegen/tool/regenerate_fixture.ts index 4da609cb3..ec252effc 100644 --- a/packages/supabase_typegen/tool/regenerate_fixture.ts +++ b/packages/supabase_typegen/tool/regenerate_fixture.ts @@ -1,7 +1,7 @@ // Regenerates test/fixtures/generator_metadata.json by introspecting a fresh // Postgres database seeded with test/fixtures/seed.sql, using the // @supabase/postgrest-typegen sources of a supabase/sdk checkout at the -// revision lib/src/introspection/introspect.dart pins. +// revision the Supabase CLI bundles. // // Run from the package root (Bun resolves the checkout's own dependencies): // diff --git a/pubspec.lock b/pubspec.lock index 5ebbd2641..0c09fa65b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1100,14 +1100,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" - toml: - dependency: transitive - description: - name: toml - sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e" - url: "https://pub.dev" - source: hosted - version: "0.18.0" typed_data: dependency: transitive description: