Generate selectable for custom queries

This makes is easier to use getSingle() and watchSingle()
on them.
Fixes #120
This commit is contained in:
Simon Binder 2019-08-27 18:44:34 +02:00
parent 5d2149d727
commit 0860b6645a
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 216 additions and 106 deletions

View File

@ -8,6 +8,7 @@ void transactionTests(TestExecutor executor) {
test('transactions write data', () async {
final db = Database(executor.createExecutor());
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
await db.transaction((_) async {
final florianId = await db.writeUser(People.florian);
@ -30,6 +31,7 @@ void transactionTests(TestExecutor executor) {
final db = Database(executor.createExecutor());
try {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
await db.transaction((_) async {
final florianId = await db.writeUser(People.florian);

View File

@ -832,22 +832,23 @@ abstract class _$Database extends GeneratedDatabase {
);
}
Selectable<TotalWeightResult> _totalWeightQuery(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery(
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
variables: [],
readsFrom: {recipes, ingredientInRecipes}).map(_rowToTotalWeightResult);
}
Future<List<TotalWeightResult>> _totalWeight(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
variables: []).then((rows) => rows.map(_rowToTotalWeightResult).toList());
return _totalWeightQuery(operateOn: operateOn).get();
}
Stream<List<TotalWeightResult>> _watchTotalWeight() {
return customSelectStream(
' SELECT r.title, SUM(ir.amount) AS total_weight\n FROM recipes r\n INNER JOIN recipe_ingredients ir ON ir.recipe = r.id\n GROUP BY r.id\n ',
variables: [],
readsFrom: {
recipes,
ingredientInRecipes
}).map((rows) => rows.map(_rowToTotalWeightResult).toList());
return _totalWeightQuery().watch();
}
@override

View File

@ -111,6 +111,7 @@ mixin QueryEngine on DatabaseConnectionUser {
/// although it is very likely that the user meant to call it on the
/// [Transaction] t. We can detect this by calling the function passed to
/// `transaction` in a forked [Zone] storing the transaction in
@protected
bool get topLevel => false;
/// We can detect when a user called methods on the wrong [QueryEngine]
@ -169,6 +170,8 @@ mixin QueryEngine on DatabaseConnectionUser {
/// You can use the [updates] parameter so that moor knows which tables are
/// affected by your query. All select streams that depend on a table
/// specified there will then issue another query.
@protected
@visibleForTesting
Future<int> customUpdate(String query,
{List<Variable> variables = const [], Set<TableInfo> updates}) async {
final engine = _resolvedEngine;
@ -190,11 +193,12 @@ mixin QueryEngine on DatabaseConnectionUser {
/// Executes a custom select statement once. To use the variables, mark them
/// with a "?" in your [query]. They will then be changed to the appropriate
/// value.
@protected
@visibleForTesting
@Deprecated('use customSelectQuery(...).get() instead')
Future<List<QueryRow>> customSelect(String query,
{List<Variable> variables = const []}) async {
return CustomSelectStatement(
query, variables, <TableInfo>{}, _resolvedEngine)
.get();
return customSelectQuery(query, variables: variables).get();
}
/// Creates a stream from a custom select statement.To use the variables, mark
@ -202,15 +206,36 @@ mixin QueryEngine on DatabaseConnectionUser {
/// appropriate value. The stream will re-emit items when any table in
/// [readsFrom] changes, so be sure to set it to the set of tables your query
/// reads data from.
@protected
@visibleForTesting
@Deprecated('use customSelectQuery(...).watch() instead')
Stream<List<QueryRow>> customSelectStream(String query,
{List<Variable> variables = const [], Set<TableInfo> readsFrom}) {
final tables = readsFrom ?? <TableInfo>{};
final statement =
CustomSelectStatement(query, variables, tables, _resolvedEngine);
return statement.watch();
return customSelectQuery(query, variables: variables, readsFrom: readsFrom)
.watch();
}
/// Creates a custom select statement from the given sql [query]. To run the
/// query once, use [Selectable.get]. For an auto-updating streams, set the
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
/// know the query will never emit more than one row, you can also use
/// [Selectable.getSingle] and [Selectable.watchSingle] which return the item
/// directly or wrapping it into a list.
///
/// If you use variables in your query (for instance with "?"), they will be
/// bound to the [variables] you specify on this query.
@protected
@visibleForTesting
Selectable<QueryRow> customSelectQuery(String query,
{List<Variable> variables = const [],
Set<TableInfo> readsFrom = const {}}) {
readsFrom ??= {};
return CustomSelectStatement(query, variables, readsFrom, _resolvedEngine);
}
/// Executes the custom sql [statement] on the database.
@protected
@visibleForTesting
Future<void> customStatement(String statement) {
return _resolvedEngine.executor.runCustom(statement);
}
@ -226,6 +251,8 @@ mixin QueryEngine on DatabaseConnectionUser {
/// might be different than that of the "global" database instance.
/// 2. Nested transactions are not supported. Creating another transaction
/// inside a transaction returns the parent transaction.
@protected
@visibleForTesting
Future transaction(Future Function(QueryEngine transaction) action) async {
final resolved = _resolvedEngine;
if (resolved is Transaction) {

View File

@ -120,6 +120,33 @@ abstract class Selectable<T> {
Stream<T> watchSingle() {
return watch().transform(singleElements());
}
/// Maps this selectable by using [mapper].
///
/// Each entry emitted by this [Selectable] will be transformed by the
/// [mapper] and then emitted to the selectable returned.
Selectable<N> map<N>(N Function(T) mapper) {
return _MappedSelectable<T, N>(this, mapper);
}
}
class _MappedSelectable<S, T> extends Selectable<T> {
final Selectable<S> _source;
final T Function(S) _mapper;
_MappedSelectable(this._source, this._mapper);
@override
Future<List<T>> get() {
return _source.get().then(_mapResults);
}
@override
Stream<List<T>> watch() {
return _source.watch().map(_mapResults);
}
List<T> _mapResults(List<S> results) => results.map(_mapper).toList();
}
mixin SingleTableQueryMixin<T extends Table, D extends DataClass>

View File

@ -1303,22 +1303,26 @@ abstract class _$TodoDb extends GeneratedDatabase {
);
}
Future<List<AllTodosWithCategoryResult>> allTodosWithCategory(
Selectable<AllTodosWithCategoryResult> allTodosWithCategoryQuery(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT t.*, c.id as catId, c."desc" as catDesc FROM todos t INNER JOIN categories c ON c.id = t.category',
variables: []).then((rows) => rows.map(_rowToAllTodosWithCategoryResult).toList());
}
Stream<List<AllTodosWithCategoryResult>> watchAllTodosWithCategory() {
return customSelectStream(
return (operateOn ?? this).customSelectQuery(
'SELECT t.*, c.id as catId, c."desc" as catDesc FROM todos t INNER JOIN categories c ON c.id = t.category',
variables: [],
readsFrom: {
categories,
todosTable
}).map((rows) => rows.map(_rowToAllTodosWithCategoryResult).toList());
}).map(_rowToAllTodosWithCategoryResult);
}
Future<List<AllTodosWithCategoryResult>> allTodosWithCategory(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return allTodosWithCategoryQuery(operateOn: operateOn).get();
}
Stream<List<AllTodosWithCategoryResult>> watchAllTodosWithCategory() {
return allTodosWithCategoryQuery().watch();
}
Future<int> deleteTodoById(
@ -1344,7 +1348,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
);
}
Future<List<TodoEntry>> withIn(
Selectable<TodoEntry> withInQuery(
String var1,
String var2,
List<int> var3,
@ -1353,21 +1357,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
var $highestIndex = 3;
final expandedvar3 = $expandVar($highestIndex, var3.length);
$highestIndex += var3.length;
return (operateOn ?? this).customSelect(
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
variables: [
Variable.withString(var1),
Variable.withString(var2),
for (var $ in var3) Variable.withInt($),
]).then((rows) => rows.map(_rowToTodoEntry).toList());
}
Stream<List<TodoEntry>> watchWithIn(
String var1, String var2, List<int> var3) {
var $highestIndex = 3;
final expandedvar3 = $expandVar($highestIndex, var3.length);
$highestIndex += var3.length;
return customSelectStream(
return (operateOn ?? this).customSelectQuery(
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
variables: [
Variable.withString(var1),
@ -1376,29 +1366,46 @@ abstract class _$TodoDb extends GeneratedDatabase {
],
readsFrom: {
todosTable
}).map((rows) => rows.map(_rowToTodoEntry).toList());
}).map(_rowToTodoEntry);
}
Future<List<TodoEntry>> withIn(
String var1,
String var2,
List<int> var3,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return withInQuery(var1, var2, var3, operateOn: operateOn).get();
}
Stream<List<TodoEntry>> watchWithIn(
String var1, String var2, List<int> var3) {
return withInQuery(var1, var2, var3).watch();
}
Selectable<TodoEntry> searchQuery(
int id,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery(
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
variables: [
Variable.withInt(id),
],
readsFrom: {
todosTable
}).map(_rowToTodoEntry);
}
Future<List<TodoEntry>> search(
int id,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
variables: [
Variable.withInt(id),
]).then((rows) => rows.map(_rowToTodoEntry).toList());
return searchQuery(id, operateOn: operateOn).get();
}
Stream<List<TodoEntry>> watchSearch(int id) {
return customSelectStream(
'SELECT * FROM todos WHERE CASE WHEN -1 = :id THEN 1 ELSE id = :id END',
variables: [
Variable.withInt(id),
],
readsFrom: {
todosTable
}).map((rows) => rows.map(_rowToTodoEntry).toList());
return searchQuery(id).watch();
}
FindCustomResult _rowToFindCustomResult(QueryRow row) {
@ -1408,20 +1415,23 @@ abstract class _$TodoDb extends GeneratedDatabase {
);
}
Selectable<FindCustomResult> findCustomQuery(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery(
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
variables: [],
readsFrom: {tableWithoutPK}).map(_rowToFindCustomResult);
}
Future<List<FindCustomResult>> findCustom(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
variables: []).then((rows) => rows.map(_rowToFindCustomResult).toList());
return findCustomQuery(operateOn: operateOn).get();
}
Stream<List<FindCustomResult>> watchFindCustom() {
return customSelectStream(
'SELECT custom FROM table_without_p_k WHERE some_float < 10',
variables: [],
readsFrom: {tableWithoutPK})
.map((rows) => rows.map(_rowToFindCustomResult).toList());
return findCustomQuery().watch();
}
@override
@ -1453,19 +1463,11 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
);
}
Future<List<TodoEntry>> todosForUser(
Selectable<TodoEntry> todosForUserQuery(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT t.* FROM todos t INNER JOIN shared_todos st ON st.todo = t.id INNER JOIN users u ON u.id = st.user WHERE u.id = :user',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToTodoEntry).toList());
}
Stream<List<TodoEntry>> watchTodosForUser(int user) {
return customSelectStream(
return (operateOn ?? this).customSelectQuery(
'SELECT t.* FROM todos t INNER JOIN shared_todos st ON st.todo = t.id INNER JOIN users u ON u.id = st.user WHERE u.id = :user',
variables: [
Variable.withInt(user),
@ -1474,6 +1476,17 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
todosTable,
sharedTodos,
users
}).map((rows) => rows.map(_rowToTodoEntry).toList());
}).map(_rowToTodoEntry);
}
Future<List<TodoEntry>> todosForUser(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return todosForUserQuery(user, operateOn: operateOn).get();
}
Stream<List<TodoEntry>> watchTodosForUser(int user) {
return todosForUserQuery(user).watch();
}
}

View File

@ -91,14 +91,14 @@ class Database extends _$Database {
Stream<List<CategoryWithCount>> categoriesWithCount() {
// select all categories and load how many associated entries there are for
// each category
return customSelectStream(
return customSelectQuery(
'SELECT c.id, c.desc, '
'(SELECT COUNT(*) FROM todos WHERE category = c.id) AS amount '
'FROM categories c '
'UNION ALL SELECT null, null, '
'(SELECT COUNT(*) FROM todos WHERE category IS NULL)',
readsFrom: {todos, categories},
).map((rows) {
).watch().map((rows) {
// when we have the result set, map each row to the data class
return rows.map((row) {
final hasId = row.data['id'] != null;

View File

@ -1,5 +1,6 @@
import 'package:moor_generator/src/model/specified_column.dart';
import 'package:moor_generator/src/model/specified_table.dart';
import 'package:moor_generator/src/model/used_type_converter.dart';
import 'package:moor_generator/src/parser/sql/type_mapping.dart';
import 'package:moor_generator/src/utils/names.dart';
import 'package:moor_generator/src/utils/string_escaper.dart';
@ -33,6 +34,8 @@ class CreateTable {
/// The AST of this `CREATE TABLE` statement.
final ParseResult ast;
CreateTable(this.ast);
SpecifiedTable extractTable(TypeMapper mapper) {
final table =
SchemaFromCreateTable().read(ast.rootNode as CreateTableStatement);
@ -47,6 +50,7 @@ class CreateTable {
final dartName = ReCase(sqlName).camelCase;
final constraintWriter = StringBuffer();
final moorType = mapper.resolvedToMoor(column.type);
UsedTypeConverter converter;
String defaultValue;
for (var constraint in column.constraints) {
@ -65,6 +69,12 @@ class CreateTable {
defaultValue = '$expressionName(${asDartLiteral(sqlDefault)})';
}
if (constraint is MappedBy) {
converter = _readTypeConverter(constraint);
// don't write MAPPED BY constraints when creating the table
continue;
}
if (constraintWriter.isNotEmpty) {
constraintWriter.write(' ');
}
@ -79,6 +89,7 @@ class CreateTable {
features: features,
customConstraints: constraintWriter.toString(),
defaultArgument: defaultValue,
typeConverter: converter,
);
foundColumns[column.name] = parsed;
@ -114,5 +125,8 @@ class CreateTable {
);
}
CreateTable(this.ast);
UsedTypeConverter _readTypeConverter(MappedBy mapper) {
// todo we need to somehow parse the dart expression and check types
return null;
}
}

View File

@ -46,6 +46,7 @@ class QueryWriter {
void _writeSelect(StringBuffer buffer) {
_writeMapping(buffer);
_writeSelectStatementCreator(buffer);
_writeOneTimeReader(buffer);
_writeStreamReader(buffer);
}
@ -54,6 +55,10 @@ class QueryWriter {
return '_rowTo${_select.resultClassName}';
}
String _nameOfCreationMethod() {
return '${_select.name}Query';
}
/// Writes a mapping method that turns a "QueryRow" into the desired custom
/// return type.
void _writeMapping(StringBuffer buffer) {
@ -87,27 +92,50 @@ class QueryWriter {
}
}
/// Writes a method returning a `Selectable<T>`, where `T` is the return type
/// of the custom query.
void _writeSelectStatementCreator(StringBuffer buffer) {
final returnType = 'Selectable<${_select.resultClassName}>';
final methodName = _nameOfCreationMethod();
buffer.write('$returnType $methodName(');
_writeParameters(buffer);
buffer.write(') {\n');
_writeExpandedDeclarations(buffer);
buffer
..write('return (operateOn ?? this).')
..write('customSelectQuery(${_queryCode()}, ');
_writeVariables(buffer);
buffer.write(', ');
_writeReadsFrom(buffer);
buffer.write(').map(');
buffer.write(_nameOfMappingMethod());
buffer.write(');\n}\n');
}
/*
Future<List<AllTodosWithCategoryResult>> allTodos(String name,
{QueryEngine overrideEngine}) {
return _allTodosWithCategoryQuery(name, engine: overrideEngine).get();
}
*/
void _writeOneTimeReader(StringBuffer buffer) {
buffer.write('Future<List<${_select.resultClassName}>> ${query.name}(');
_writeParameters(buffer);
buffer.write(') {\n');
_writeExpandedDeclarations(buffer);
buffer
..write('return (operateOn ?? this).') // use custom engine, if set
..write('customSelect(${_queryCode()},');
_writeVariables(buffer);
buffer
..write(')')
..write(
'.then((rows) => rows.map(${_nameOfMappingMethod()}).toList());\n')
..write('\n}\n');
buffer..write(') {\n')..write('return ${_nameOfCreationMethod()}(');
_writeUseParameters(buffer);
buffer.write(').get();\n}\n');
}
void _writeStreamReader(StringBuffer buffer) {
// turning the query name into pascal case will remove underscores
final upperQueryName = ReCase(query.name).pascalCase;
String methodName;
// turning the query name into pascal case will remove underscores, add the
// "private" modifier back in if needed
if (session.options.fixPrivateWatchMethods && query.name.startsWith('_')) {
methodName = '_watch$upperQueryName';
} else {
@ -115,23 +143,10 @@ class QueryWriter {
}
buffer.write('Stream<List<${_select.resultClassName}>> $methodName(');
// don't supply an engine override parameter because select streams cannot
// be used in transaction or similar context, only on the main database
// engine.
_writeParameters(buffer, dontOverrideEngine: true);
buffer.write(') {\n');
_writeExpandedDeclarations(buffer);
buffer..write('return customSelectStream(${_queryCode()},');
_writeVariables(buffer);
buffer.write(',');
_writeReadsFrom(buffer);
buffer
..write(')')
..write('.map((rows) => rows.map(${_nameOfMappingMethod()}).toList());\n')
..write('\n}\n');
buffer..write(') {\n')..write('return ${_nameOfCreationMethod()}(');
_writeUseParameters(buffer, dontUseEngine: true);
buffer.write(').watch();\n}\n');
}
void _writeUpdatingQuery(StringBuffer buffer) {
@ -177,6 +192,17 @@ class QueryWriter {
}
}
/// Writes code that uses the parameters as declared by [_writeParameters],
/// assuming that for each parameter, a variable with the same name exists
/// in the current scope.
void _writeUseParameters(StringBuffer into, {bool dontUseEngine = false}) {
into.write(query.variables.map((v) => v.dartParameterName).join(', '));
if (!dontUseEngine) {
if (query.variables.isNotEmpty) into.write(', ');
into.write('operateOn: operateOn');
}
}
// Some notes on parameters and generating query code:
// We expand array parameters to multiple variables at runtime (see the
// documentation of FoundVariable and SqlQuery for further discussion).