From 039ff942f77888adce75b406234ecb4fdbd60264 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 17 Feb 2023 17:56:48 +0100 Subject: [PATCH] Add docs on schema reflection --- .../snippets/modular/schema_inspection.dart | 36 +++++++++++ .../Advanced Features/schema_inspection.md | 59 +++++++++++++++++++ docs/pages/index.html | 5 +- docs/test/generated/database.dart | 2 +- docs/test/snippet_test.dart | 23 +++++++- .../query_builder/schema/entities.dart | 8 +++ .../query_builder/schema/table_info.dart | 2 +- .../query_builder/schema/view_info.dart | 9 +++ 8 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 docs/lib/snippets/modular/schema_inspection.dart create mode 100644 docs/pages/docs/Advanced Features/schema_inspection.md diff --git a/docs/lib/snippets/modular/schema_inspection.dart b/docs/lib/snippets/modular/schema_inspection.dart new file mode 100644 index 00000000..b7908bfe --- /dev/null +++ b/docs/lib/snippets/modular/schema_inspection.dart @@ -0,0 +1,36 @@ +import 'package:drift/drift.dart'; + +import 'drift/example.drift.dart'; + +// #docregion findById +extension FindById + on ResultSetImplementation { + Selectable findById(int id) { + return select() + ..where((row) { + final idColumn = columnsByName['id']; + + if (idColumn == null) { + throw ArgumentError.value( + this, 'this', 'Must be a table with an id column'); + } + + if (idColumn.type != DriftSqlType.int) { + throw ArgumentError('Column `id` is not an integer'); + } + + return idColumn.equals(id); + }); + } +} +// #enddocregion findById + +extension FindTodoEntryById on GeneratedDatabase { + Todos get todos => Todos(this); + + // #docregion findTodoEntryById + Selectable findTodoEntryById(int id) { + return select(todos)..where((row) => row.id.equals(id)); + } + // #enddocregion findTodoEntryById +} diff --git a/docs/pages/docs/Advanced Features/schema_inspection.md b/docs/pages/docs/Advanced Features/schema_inspection.md new file mode 100644 index 00000000..cbf72ff9 --- /dev/null +++ b/docs/pages/docs/Advanced Features/schema_inspection.md @@ -0,0 +1,59 @@ +--- +data: + title: Runtime schema inspection + description: Use generated table classes to reflectively inspect the schema of your database. +template: layouts/docs/single +--- + +{% assign snippets = 'package:drift_docs/snippets/modular/schema_inspection.dart.excerpt.json' | readString | json_decode %} + +Thanks to the typesafe table classes generated by drift, [writing SQL queries]({{ '../Getting started/writing_queries.md' | pageUrl }}) in Dart +is simple and safe. +However, these queries are usually written against a specific table. And while drift supports inheritance for tables, sometimes it is easier +to access tables reflectively. Luckily, code generated by drift implements interfaces which can be used to do just that. + +Since this is a topic that most drift users will not need, this page mostly gives motivating examples and links to the documentation for relevant +drift classes. +For instance, you might have multiple independent tables that have an `id` column. And you might want to filter rows by their `id` column. +When writing this query against a single table, like the `Todos` table as seen in the [getting started]({{'../Getting started/index.md' | pageUrl }}) page, +that's pretty straightforward: + +{% include "blocks/snippet" snippets = snippets name = 'findTodoEntryById' %} + +But let's say we want to generalize this query to every database table, how could that look like? +This following snippet shows how this can be done (note that the links in the snippet point directly towards the relevant documentation): + +{% include "blocks/snippet" snippets = snippets name = 'findById' %} + +Since that is much more complicated than the query that only works for a single table, let's take a look at each interesting line in detail: + + - `FindById` is an extension on [ResultSetImplementation]. This class is the superclass for every table or view generated by drift. + It defines useful methods to inspect the schema, or to translate a raw `Map` representing a database row into the generated data class. + - `ResultSetImplementation` is instantiated with two type arguments: The original table class and the generated row class. + For instance, if you define a table `class Todos extends Table`, drift would generate a class that extends `Todos` while also implementing. + `ResultSetImplementation` (with `Todo` being the generated data class). + - `ResultSetImplementation` has two subclasses: [TableInfo] and [ViewInfo] which are mixed in to generated table and view classes, respectively. + - `HasResultSet` is the superclass for `Table` and `View`, the two classes used to declare tables and views in drift. +- `Selectable` represents a query, you can use methods like `get()`, `watch()`, `getSingle()` and `watchSingle()` on it to run the query. +- The `select()` extension used in `findById` can be used to start a select statement without a reference to a database class - all you need is + the table instance. +- We can use `columnsByName` to find a column by its name in SQL. Here, we expect an `int` column to exist. +- The [GeneratedColumn] class represents a column in a database. Things like column constraints, the type or default values can be read from the + column instance. + - In particular, we use this to assert that the table indeed has an `IntColumn` named `id`. + +To call this extension, `await myDatabase.todos.findById(3).getSingle()` could be used. +A nice thing about defining the method as an extension is that type inference works really well - calling `findById` on `todos` +returns a `Todo` instance, the generated data class for this table. + +The same approach also works to construct update, delete and insert statements (although those require a [TableInfo] instead of a [ResultSetImplementation] +as views are read-only). + +Hopefully, this page gives you some pointers to start reflectively inspecting your drift databases. +The linked Dart documentation also expains the concepts in more detail. +If you have questions about this, or have a suggestion for more examples to include on this page, feel free to [start a discussion](https://github.com/simolus3/drift/discussions/new?category=q-a) about this. + +[ResultSetImplementation]: https://drift.simonbinder.eu/api/drift/resultsetimplementation-class +[TableInfo]: https://drift.simonbinder.eu/api/drift/tableinfo-mixin +[ViewInfo]: https://drift.simonbinder.eu/api/drift/viewinfo-class +[GeneratedColumn]: https://drift.simonbinder.eu/api/drift/generatedcolumn-class \ No newline at end of file diff --git a/docs/pages/index.html b/docs/pages/index.html index c65cdac1..5ee2dc6f 100644 --- a/docs/pages/index.html +++ b/docs/pages/index.html @@ -14,10 +14,7 @@ data: Write type-safe queries in Dart or SQL, enjoy auto-updating streams, easily managed transactions and so much more to make persistence fun.

- - Learn more - - + Get started diff --git a/docs/test/generated/database.dart b/docs/test/generated/database.dart index 21283458..c283cb79 100644 --- a/docs/test/generated/database.dart +++ b/docs/test/generated/database.dart @@ -11,7 +11,7 @@ class Users extends Table { @DriftDatabase(tables: [Users]) class Database extends _$Database { - Database.connect(DatabaseConnection c) : super(c); + Database(QueryExecutor c) : super(c); @override int get schemaVersion => 1; diff --git a/docs/test/snippet_test.dart b/docs/test/snippet_test.dart index 60d39315..6b996b7b 100644 --- a/docs/test/snippet_test.dart +++ b/docs/test/snippet_test.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:drift_docs/snippets/migrations/datetime_conversion.dart'; +import 'package:drift_docs/snippets/modular/schema_inspection.dart'; import 'package:test/test.dart'; import 'generated/database.dart'; @@ -11,7 +12,7 @@ import 'generated/database.dart'; void main() { group('changing datetime format', () { test('unix timestamp to text', () async { - final db = Database.connect(DatabaseConnection(NativeDatabase.memory())); + final db = Database(DatabaseConnection(NativeDatabase.memory())); addTearDown(db.close); final time = DateTime.fromMillisecondsSinceEpoch( @@ -42,7 +43,7 @@ void main() { test('text to unix timestamp', () async { // First, create all tables using text as datetime - final db = Database.connect(DatabaseConnection(NativeDatabase.memory())); + final db = Database(DatabaseConnection(NativeDatabase.memory())); db.options = const DriftDatabaseOptions(storeDateTimeAsText: true); addTearDown(db.close); @@ -72,7 +73,7 @@ void main() { test('text to unix timestamp, support old sqlite', () async { // First, create all tables using text as datetime - final db = Database.connect(DatabaseConnection(NativeDatabase.memory())); + final db = Database(DatabaseConnection(NativeDatabase.memory())); db.options = const DriftDatabaseOptions(storeDateTimeAsText: true); addTearDown(db.close); @@ -100,4 +101,20 @@ void main() { ]); }); }); + + group('runtime schema inspection', () { + test('findById', () async { + final db = Database(NativeDatabase.memory()); + addTearDown(db.close); + + await db.batch((batch) { + batch.insert(db.users, UsersCompanion.insert(name: 'foo')); // 1 + batch.insert(db.users, UsersCompanion.insert(name: 'bar')); // 2 + batch.insert(db.users, UsersCompanion.insert(name: 'baz')); // 3 + }); + + final row = await db.users.findById(2).getSingle(); + expect(row.name, 'bar'); + }); + }); } diff --git a/drift/lib/src/runtime/query_builder/schema/entities.dart b/drift/lib/src/runtime/query_builder/schema/entities.dart index 2fb70a99..11542420 100644 --- a/drift/lib/src/runtime/query_builder/schema/entities.dart +++ b/drift/lib/src/runtime/query_builder/schema/entities.dart @@ -99,6 +99,10 @@ abstract class ResultSetImplementation extends DatabaseSchemaEntity { /// when used in a query. ResultSetImplementation createAlias(String alias) => _AliasResultSet(alias, this); + + /// Gets all [$columns] in this table or view, indexed by their (non-escaped) + /// name. + Map get columnsByName; } class _AliasResultSet extends ResultSetImplementation { @@ -131,6 +135,10 @@ class _AliasResultSet extends ResultSetImplementation { @override Tbl get asDslTable => _inner.asDslTable; + + @override + Map> get columnsByName => + _inner.columnsByName; } /// Extension to generate an alias for a table or a view. diff --git a/drift/lib/src/runtime/query_builder/schema/table_info.dart b/drift/lib/src/runtime/query_builder/schema/table_info.dart index 940358f2..85ec0aaa 100644 --- a/drift/lib/src/runtime/query_builder/schema/table_info.dart +++ b/drift/lib/src/runtime/query_builder/schema/table_info.dart @@ -48,7 +48,7 @@ mixin TableInfo on Table Map? _columnsByName; - /// Gets all [$columns] in this table, indexed by their (non-escaped) name. + @override Map get columnsByName { return _columnsByName ??= { for (final column in $columns) column.$name: column diff --git a/drift/lib/src/runtime/query_builder/schema/view_info.dart b/drift/lib/src/runtime/query_builder/schema/view_info.dart index e44a6954..0b6482f5 100644 --- a/drift/lib/src/runtime/query_builder/schema/view_info.dart +++ b/drift/lib/src/runtime/query_builder/schema/view_info.dart @@ -29,4 +29,13 @@ abstract class ViewInfo /// If this view reads from other views, the [readTables] of that view are /// also included in this [readTables] set. Set get readTables; + + Map? _columnsByName; + + @override + Map get columnsByName { + return _columnsByName ??= { + for (final column in $columns) column.$name: column + }; + } }