diff --git a/extras/integration_tests/tests/lib/suite/transactions.dart b/extras/integration_tests/tests/lib/suite/transactions.dart index 93b55eef..9b0e41ad 100644 --- a/extras/integration_tests/tests/lib/suite/transactions.dart +++ b/extras/integration_tests/tests/lib/suite/transactions.dart @@ -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); diff --git a/moor/example/example.g.dart b/moor/example/example.g.dart index cdf8da0b..31184953 100644 --- a/moor/example/example.g.dart +++ b/moor/example/example.g.dart @@ -832,22 +832,23 @@ abstract class _$Database extends GeneratedDatabase { ); } + Selectable _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> _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> _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 diff --git a/moor/lib/src/runtime/database.dart b/moor/lib/src/runtime/database.dart index 61e472a8..034cc53d 100644 --- a/moor/lib/src/runtime/database.dart +++ b/moor/lib/src/runtime/database.dart @@ -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 customUpdate(String query, {List variables = const [], Set 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> customSelect(String query, {List variables = const []}) async { - return CustomSelectStatement( - query, variables, {}, _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> customSelectStream(String query, {List variables = const [], Set readsFrom}) { - final tables = readsFrom ?? {}; - 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 customSelectQuery(String query, + {List variables = const [], + Set readsFrom = const {}}) { + readsFrom ??= {}; + return CustomSelectStatement(query, variables, readsFrom, _resolvedEngine); } /// Executes the custom sql [statement] on the database. + @protected + @visibleForTesting Future 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) { diff --git a/moor/lib/src/runtime/statements/query.dart b/moor/lib/src/runtime/statements/query.dart index 7fc66e03..80bbf8e1 100644 --- a/moor/lib/src/runtime/statements/query.dart +++ b/moor/lib/src/runtime/statements/query.dart @@ -120,6 +120,33 @@ abstract class Selectable { Stream 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 map(N Function(T) mapper) { + return _MappedSelectable(this, mapper); + } +} + +class _MappedSelectable extends Selectable { + final Selectable _source; + final T Function(S) _mapper; + + _MappedSelectable(this._source, this._mapper); + + @override + Future> get() { + return _source.get().then(_mapResults); + } + + @override + Stream> watch() { + return _source.watch().map(_mapResults); + } + + List _mapResults(List results) => results.map(_mapper).toList(); } mixin SingleTableQueryMixin diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index 0caf9e30..5d5bbce7 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -1303,22 +1303,26 @@ abstract class _$TodoDb extends GeneratedDatabase { ); } - Future> allTodosWithCategory( + Selectable 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> 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> allTodosWithCategory( + {@Deprecated('No longer needed with Moor 1.6 - see the changelog for details') + QueryEngine operateOn}) { + return allTodosWithCategoryQuery(operateOn: operateOn).get(); + } + + Stream> watchAllTodosWithCategory() { + return allTodosWithCategoryQuery().watch(); } Future deleteTodoById( @@ -1344,7 +1348,7 @@ abstract class _$TodoDb extends GeneratedDatabase { ); } - Future> withIn( + Selectable withInQuery( String var1, String var2, List 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> watchWithIn( - String var1, String var2, List 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> withIn( + String var1, + String var2, + List 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> watchWithIn( + String var1, String var2, List var3) { + return withInQuery(var1, var2, var3).watch(); + } + + Selectable 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> 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> 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 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> 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> 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 { ); } - Future> todosForUser( + Selectable 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> 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 { todosTable, sharedTodos, users - }).map((rows) => rows.map(_rowToTodoEntry).toList()); + }).map(_rowToTodoEntry); + } + + Future> 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> watchTodosForUser(int user) { + return todosForUserQuery(user).watch(); } } diff --git a/moor_flutter/example/lib/database/database.dart b/moor_flutter/example/lib/database/database.dart index d0639556..e48b539d 100644 --- a/moor_flutter/example/lib/database/database.dart +++ b/moor_flutter/example/lib/database/database.dart @@ -91,14 +91,14 @@ class Database extends _$Database { Stream> 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; diff --git a/moor_generator/lib/src/parser/moor/parsed_moor_file.dart b/moor_generator/lib/src/parser/moor/parsed_moor_file.dart index b9f712cf..aabdae85 100644 --- a/moor_generator/lib/src/parser/moor/parsed_moor_file.dart +++ b/moor_generator/lib/src/parser/moor/parsed_moor_file.dart @@ -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; + } } diff --git a/moor_generator/lib/src/writer/query_writer.dart b/moor_generator/lib/src/writer/query_writer.dart index 7c45fe4e..8c98d325 100644 --- a/moor_generator/lib/src/writer/query_writer.dart +++ b/moor_generator/lib/src/writer/query_writer.dart @@ -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`, 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> allTodos(String name, + {QueryEngine overrideEngine}) { + return _allTodosWithCategoryQuery(name, engine: overrideEngine).get(); + } + */ + void _writeOneTimeReader(StringBuffer buffer) { buffer.write('Future> ${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> $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).