Add docs on schema reflection

This commit is contained in:
Simon Binder 2023-02-17 17:56:48 +01:00
parent 99884c55ca
commit 039ff942f7
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 135 additions and 9 deletions

View File

@ -0,0 +1,36 @@
import 'package:drift/drift.dart';
import 'drift/example.drift.dart';
// #docregion findById
extension FindById<Table extends HasResultSet, Row>
on ResultSetImplementation<Table, Row> {
Selectable<Row> 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<Todo> findTodoEntryById(int id) {
return select(todos)..where((row) => row.id.equals(id));
}
// #enddocregion findTodoEntryById
}

View File

@ -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<Todos, Todo>` (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<Row>` 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

View File

@ -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.
</p>
<a class="btn btn-lg btn-primary mr-3 mb-4" href="{{ 'docs/index' | pageUrl }}">
Learn more <i class="fas fa-arrow-alt-circle-right ml-2"></i>
</a>
<a class="btn btn-lg btn-secondary mr-3 mb-4" href="https://pub.dev/packages/drift">
<a class="btn btn-lg btn-secondary mr-3 mb-4" href="{{ 'docs/Getting started/index.md' | pageUrl }}">
Get started <i class="fas fa-code ml-2 "></i>
</a>
</div>

View File

@ -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;

View File

@ -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');
});
});
}

View File

@ -99,6 +99,10 @@ abstract class ResultSetImplementation<Tbl, Row> extends DatabaseSchemaEntity {
/// when used in a query.
ResultSetImplementation<Tbl, Row> createAlias(String alias) =>
_AliasResultSet(alias, this);
/// Gets all [$columns] in this table or view, indexed by their (non-escaped)
/// name.
Map<String, GeneratedColumn> get columnsByName;
}
class _AliasResultSet<Tbl, Row> extends ResultSetImplementation<Tbl, Row> {
@ -131,6 +135,10 @@ class _AliasResultSet<Tbl, Row> extends ResultSetImplementation<Tbl, Row> {
@override
Tbl get asDslTable => _inner.asDslTable;
@override
Map<String, GeneratedColumn<Object>> get columnsByName =>
_inner.columnsByName;
}
/// Extension to generate an alias for a table or a view.

View File

@ -48,7 +48,7 @@ mixin TableInfo<TableDsl extends Table, D> on Table
Map<String, GeneratedColumn>? _columnsByName;
/// Gets all [$columns] in this table, indexed by their (non-escaped) name.
@override
Map<String, GeneratedColumn> get columnsByName {
return _columnsByName ??= {
for (final column in $columns) column.$name: column

View File

@ -29,4 +29,13 @@ abstract class ViewInfo<Self extends HasResultSet, Row>
/// If this view reads from other views, the [readTables] of that view are
/// also included in this [readTables] set.
Set<String> get readTables;
Map<String, GeneratedColumn>? _columnsByName;
@override
Map<String, GeneratedColumn> get columnsByName {
return _columnsByName ??= {
for (final column in $columns) column.$name: column
};
}
}