Add common methods as extension on table (#1613)

This commit is contained in:
Simon Binder 2022-01-18 22:11:09 +01:00
parent 7bfa00e8b4
commit 672bb930c8
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
14 changed files with 223 additions and 43 deletions

View File

@ -1,3 +1,11 @@
## 1.4.0
- Most methods to compose statements are now available as an extension on
tables. As an alternative to `update(todos).replace(newEntry)`, you can
now write `todos.replaceOne(newEntry)`.
- Deprecate the `from(table)` API introduced in 1.3.0. Having the methods on
the table instances turned out to be even easier!
## 1.3.0
- Add the `from(table)` method to generated databases. It can be used to write

View File

@ -535,10 +535,11 @@ class $TodoCategoryItemCountView
extends ViewInfo<$TodoCategoryItemCountView, TodoCategoryItemCountData>
implements HasResultSet {
final String? _alias;
$TodoCategoryItemCountView(DatabaseConnectionUser db, [this._alias])
: super(db);
$TodoItemsTable get todoItems => _db.todoItems;
$TodoCategoriesTable get todoCategories => _db.todoCategories;
@override
final _$Database attachedDatabase;
$TodoCategoryItemCountView(this.attachedDatabase, [this._alias]);
$TodoItemsTable get todoItems => attachedDatabase.todoItems;
$TodoCategoriesTable get todoCategories => attachedDatabase.todoCategories;
@override
List<GeneratedColumn> get $columns => [todoCategories.name, itemCount];
@override
@ -643,10 +644,11 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo<
$TodoItemWithCategoryNameViewView,
TodoItemWithCategoryNameViewData> implements HasResultSet {
final String? _alias;
$TodoItemWithCategoryNameViewView(DatabaseConnectionUser db, [this._alias])
: super(db);
$TodoItemsTable get todoItems => _db.todoItems;
$TodoCategoriesTable get todoCategories => _db.todoCategories;
@override
final _$Database attachedDatabase;
$TodoItemWithCategoryNameViewView(this.attachedDatabase, [this._alias]);
$TodoItemsTable get todoItems => attachedDatabase.todoItems;
$TodoCategoriesTable get todoCategories => attachedDatabase.todoCategories;
@override
List<GeneratedColumn> get $columns => [todoItems.id, title];
@override

View File

@ -159,7 +159,11 @@ abstract class DatabaseConnectionUser {
/// The [TableOrViewOperations] class (or the [TableOperations] extension
/// for tables) provides convenience methods that make common operations
/// easier to write than using the methods from this class directly.
@experimental
///
/// This method is deprecated. For an even easier access, the methods that
/// were made available here are now available on the [table] or view instance
/// directly.
@Deprecated('Experiment ended - use the methods on the [table] direclty')
TableOrViewOperations<T, D> from<T extends HasResultSet, D>(
ResultSetImplementation<T, D> table) {
return TableOrViewOperations._(this, table);

View File

@ -0,0 +1,102 @@
import 'package:drift/drift.dart';
/// Easily-accessible methods to compose common operations or statements on
/// tables or views.
extension TableOrViewStatements<Tbl extends HasResultSet, Row>
on ResultSetImplementation<Tbl, Row> {
/// Composes a `SELECT` statement on the captured table or view.
///
/// This is equivalent to calling [DatabaseConnectionUser.select].
SimpleSelectStatement<Tbl, Row> select({bool distinct = false}) {
return attachedDatabase.select(this, distinct: distinct);
}
/// Composes a `SELECT` statement only selecting a subset of columns.
///
/// This is equivalent to calling [DatabaseConnectionUser.selectOnly].
JoinedSelectStatement<Tbl, Row> selectOnly(
{bool distinct = false, bool includeJoinedTableColumns = true}) {
return attachedDatabase.selectOnly(this,
distinct: distinct,
includeJoinedTableColumns: includeJoinedTableColumns);
}
}
/// Easily-accessible methods to compose common operations or statements on
/// tables.
extension TableStatements<Tbl extends Table, Row> on TableInfo<Tbl, Row> {
/// Creates an insert statment to be used to compose an insert on the table.
///
/// This is equivalent to calling [DatabaseConnectionUser.into] on the
/// captured table. See that method for more information.
InsertStatement<Tbl, Row> insert() => attachedDatabase.into(this);
/// Inserts one row into this table.
///
/// This is equivalent to calling [InsertStatement.insert] - see that method
/// for more information.
Future<int> insertOne(
Insertable<Row> row, {
InsertMode? mode,
UpsertClause<Tbl, Row>? onConflict,
}) {
return insert().insert(row, mode: mode, onConflict: onConflict);
}
/// Inserts one row into this table table, replacing an existing row if it
/// exists already.
///
/// Please note that this method is only available on recent sqlite3 versions.
/// See also [InsertStatement.insertOnConflictUpdate].
Future<int> insertOnConflictUpdate(Insertable<Row> row) {
return insert().insertOnConflictUpdate(row);
}
/// Inserts one row into this table and returns it, along with auto-
/// generated fields.
///
/// Please note that this method is only available on recent sqlite3 versions.
/// See also [InsertStatement.insertReturning].
Future<Row> insertReturning(
Insertable<Row> row, {
InsertMode? mode,
UpsertClause<Tbl, Row>? onConflict,
}) {
return insert().insertReturning(
row,
mode: mode,
onConflict: onConflict,
);
}
/// Creates a statement to compose an `UPDATE` into the database.
///
/// This is equivalent to calling [DatabaseConnectionUser.update] with the
/// captured table.
UpdateStatement<Tbl, Row> update() => attachedDatabase.update(this);
/// Replaces a single row with an update statement.
///
/// See also [UpdateStatement.replace].
Future<void> replaceOne(Insertable<Row> row) {
return update().replace(row);
}
/// Creates a statement to compose a `DELETE` from the database.
///
/// This is equivalent to calling [DatabaseConnectionUser.delete] with the
/// captured table.
DeleteStatement<Tbl, Row> delete() => attachedDatabase.delete(this);
/// Deletes the [row] from the captured table.
Future<bool> deleteOne(Insertable<Row> row) async {
return await (delete()..whereSamePrimaryKey(row)).go() != 0;
}
/// Deletes all rows matching the [filter] from the table.
///
/// See also [SingleTableQueryMixin.where].
Future<int> deleteWhere(Expression<bool?> Function(Tbl tbl) filter) {
return (delete()..where(filter)).go();
}
}

View File

@ -12,6 +12,8 @@ import 'package:meta/meta.dart';
// split up.
import 'expressions/case_when.dart';
export 'on_table.dart';
part 'components/group_by.dart';
part 'components/join.dart';
part 'components/limit.dart';

View File

@ -11,13 +11,6 @@ part of '../query_builder.dart';
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/
abstract class ViewInfo<Self extends HasResultSet, Row>
implements ResultSetImplementation<Self, Row> {
@override
final DatabaseConnectionUser attachedDatabase;
/// Constructor for a view implementation, used by drift-generated code.
@internal
ViewInfo(this.attachedDatabase);
@override
String get entityName;

View File

@ -1547,7 +1547,9 @@ class MyViewData extends DataClass {
class MyView extends ViewInfo<MyView, MyViewData> implements HasResultSet {
final String? _alias;
MyView(DatabaseConnectionUser db, [this._alias]) : super(db);
@override
final _$CustomTablesDb attachedDatabase;
MyView(this.attachedDatabase, [this._alias]);
@override
List<GeneratedColumn> get $columns =>
[configKey, configValue, syncState, syncStateImplicit];

View File

@ -1417,10 +1417,11 @@ class $CategoryTodoCountViewView
extends ViewInfo<$CategoryTodoCountViewView, CategoryTodoCountViewData>
implements HasResultSet {
final String? _alias;
$CategoryTodoCountViewView(DatabaseConnectionUser db, [this._alias])
: super(db);
$TodosTableTable get todos => _db.todosTable;
$CategoriesTable get categories => _db.categories;
@override
final _$TodoDb attachedDatabase;
$CategoryTodoCountViewView(this.attachedDatabase, [this._alias]);
$TodosTableTable get todos => attachedDatabase.todosTable;
$CategoriesTable get categories => attachedDatabase.categories;
@override
List<GeneratedColumn> get $columns => [categories.description, itemCount];
@override
@ -1523,10 +1524,11 @@ class $TodoWithCategoryViewView
extends ViewInfo<$TodoWithCategoryViewView, TodoWithCategoryViewData>
implements HasResultSet {
final String? _alias;
$TodoWithCategoryViewView(DatabaseConnectionUser db, [this._alias])
: super(db);
$TodosTableTable get todos => _db.todosTable;
$CategoriesTable get categories => _db.categories;
@override
final _$TodoDb attachedDatabase;
$TodoWithCategoryViewView(this.attachedDatabase, [this._alias]);
$TodosTableTable get todos => attachedDatabase.todosTable;
$CategoriesTable get categories => attachedDatabase.categories;
@override
List<GeneratedColumn> get $columns => [todos.title, categories.description];
@override

View File

@ -71,23 +71,21 @@ void main() {
});
});
group('delete with from()', () {
group('delete on table instances', () {
test('delete()', () async {
await db.from(db.users).delete().go();
await db.users.delete().go();
verify(executor.runDelete('DELETE FROM users;', []));
});
test('deleteOne()', () async {
await db.from(db.users).deleteOne(const UsersCompanion(id: Value(3)));
await db.users.deleteOne(const UsersCompanion(id: Value(3)));
verify(executor.runDelete('DELETE FROM users WHERE id = ?;', [3]));
});
test('deleteWhere', () async {
await db
.from(db.users)
.deleteWhere((tbl) => tbl.id.isSmallerThanValue(3));
await db.users.deleteWhere((tbl) => tbl.id.isSmallerThanValue(3));
verify(executor.runDelete('DELETE FROM users WHERE id < ?;', [3]));
});

View File

@ -454,4 +454,55 @@ void main() {
));
});
});
group('on table instances', () {
test('insert', () async {
await db.categories
.insert()
.insert(CategoriesCompanion.insert(description: 'description'));
verify(executor.runInsert(
'INSERT INTO categories ("desc") VALUES (?)', ['description']));
});
test('insertOne', () async {
await db.categories.insertOne(
CategoriesCompanion.insert(description: 'description'),
mode: InsertMode.insertOrReplace);
verify(executor.runInsert(
'INSERT OR REPLACE INTO categories ("desc") VALUES (?)',
['description']));
});
test('insertOnConflictUpdate', () async {
when(executor.runSelect(any, any)).thenAnswer(
(_) => Future.value([
{
'id': 1,
'desc': 'description',
'description_in_upper_case': 'DESCRIPTION',
'priority': 1,
},
]),
);
final row = await db.categories.insertReturning(
CategoriesCompanion.insert(description: 'description'));
expect(
row,
Category(
id: 1,
description: 'description',
descriptionInUpperCase: 'DESCRIPTION',
priority: CategoryPriority.medium,
),
);
verify(executor.runSelect(
'INSERT INTO categories ("desc") VALUES (?) RETURNING *',
['description'],
));
});
});
}

View File

@ -180,4 +180,21 @@ void main() {
['new name', 3]));
});
});
group('update on table instances', () {
test('update()', () async {
await db.users.update().write(const UsersCompanion(id: Value(3)));
verify(executor.runUpdate('UPDATE users SET id = ?;', [3]));
});
test('replace', () async {
await db.categories.replace(const CategoriesCompanion(
id: Value(3), description: Value('new name')));
verify(executor.runUpdate(
'UPDATE categories SET "desc" = ?, priority = 0 WHERE id = ?;',
['new name', 3]));
});
});
}

View File

@ -187,9 +187,7 @@ class ViewParser {
(tbl) => tbl.fromClass!.name == node.returnType.toString());
if (type != null) {
final name = node.name.toString();
final declaration = '${type.entityInfoName} get $name => '
'_db.${type.dbGetterName};';
return TableReferenceInDartView(type, name, declaration);
return TableReferenceInDartView(type, name);
}
}
} catch (_) {}

View File

@ -34,9 +34,8 @@ class DartViewDeclaration implements ViewDeclaration, DartDeclaration {
class TableReferenceInDartView {
final MoorTable table;
final String name;
final String declaration;
TableReferenceInDartView(this.table, this.name, this.declaration);
TableReferenceInDartView(this.table, this.name);
}
class MoorViewDeclaration

View File

@ -38,19 +38,21 @@ class ViewWriter extends TableOrViewWriter {
} else {
buffer.write('<${view.entityInfoName}, Never>');
}
buffer.write(' implements HasResultSet');
buffer.writeln(' implements HasResultSet {');
buffer
..write('{\n')
// write the generated database reference that is set in the constructor
..write('final ${scope.nullableType('String')} _alias;\n')
..write('${view.entityInfoName}(DatabaseConnectionUser db, '
'[this._alias]): super(db);\n');
..writeln('final ${scope.nullableType('String')} _alias;')
..writeln(
'@override final ${databaseWriter.dbClassName} attachedDatabase;')
..writeln('${view.entityInfoName}(this.attachedDatabase, '
'[this._alias]);');
final declaration = view.declaration;
if (declaration is DartViewDeclaration) {
for (final ref in declaration.staticReferences) {
buffer.write('${ref.declaration}\n');
final declaration = '${ref.table.entityInfoName} get ${ref.name} => '
'attachedDatabase.${ref.table.dbGetterName};';
buffer.writeln(declaration);
}
}