diff --git a/moor/lib/src/runtime/api/query_engine.dart b/moor/lib/src/runtime/api/query_engine.dart index a6112241..3b62a272 100644 --- a/moor/lib/src/runtime/api/query_engine.dart +++ b/moor/lib/src/runtime/api/query_engine.dart @@ -133,7 +133,7 @@ mixin QueryEngine on DatabaseConnectionUser { bool distinct = false, }) { return JoinedSelectStatement( - _resolvedEngine, table, const [], distinct, false); + _resolvedEngine, table, [], distinct, false); } /// Starts a [DeleteStatement] that can be used to delete rows from a table. diff --git a/moor/lib/src/runtime/query_builder/components/where.dart b/moor/lib/src/runtime/query_builder/components/where.dart index 09a46af7..de8b6901 100644 --- a/moor/lib/src/runtime/query_builder/components/where.dart +++ b/moor/lib/src/runtime/query_builder/components/where.dart @@ -14,4 +14,13 @@ class Where extends Component { context.buffer.write('WHERE '); predicate.writeInto(context); } + + @override + int get hashCode => predicate.hashCode * 7; + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + other is Where && other.predicate == predicate; + } } diff --git a/moor/lib/src/runtime/query_builder/expressions/aggregate.dart b/moor/lib/src/runtime/query_builder/expressions/aggregate.dart index cbfe0c41..3f2953a0 100644 --- a/moor/lib/src/runtime/query_builder/expressions/aggregate.dart +++ b/moor/lib/src/runtime/query_builder/expressions/aggregate.dart @@ -96,6 +96,26 @@ class _AggregateExpression> extends Expression { context.buffer.write(')'); } } + + @override + int get hashCode { + return $mrjf($mrjc(functionName.hashCode, + $mrjc(distinct.hashCode, $mrjc(parameter.hashCode, filter.hashCode)))); + } + + @override + bool operator ==(dynamic other) { + if (!identical(this, other) && other.runtimeType != runtimeType) { + return false; + } + + // ignore: test_types_in_equals + final typedOther = other as _AggregateExpression; + return typedOther.functionName == functionName && + typedOther.distinct == distinct && + typedOther.parameter == parameter && + typedOther.filter == filter; + } } class _StarFunctionParameter implements FunctionParameter { diff --git a/moor/lib/src/runtime/query_builder/expressions/expression.dart b/moor/lib/src/runtime/query_builder/expressions/expression.dart index 303fc810..7b821a97 100644 --- a/moor/lib/src/runtime/query_builder/expressions/expression.dart +++ b/moor/lib/src/runtime/query_builder/expressions/expression.dart @@ -12,6 +12,9 @@ abstract class FunctionParameter implements Component {} /// Any sql expression that evaluates to some generic value. This does not /// include queries (which might evaluate to multiple values) but individual /// columns, functions and operators. +/// +/// It's important that all subclasses properly implement [hashCode] and +/// [==]. abstract class Expression> implements FunctionParameter { /// Constant constructor so that subclasses can be constant. diff --git a/moor/lib/src/runtime/query_builder/statements/select/select.dart b/moor/lib/src/runtime/query_builder/statements/select/select.dart index 73cfb56f..9a142d7a 100644 --- a/moor/lib/src/runtime/query_builder/statements/select/select.dart +++ b/moor/lib/src/runtime/query_builder/statements/select/select.dart @@ -60,6 +60,7 @@ class SimpleSelectStatement /// ``` /// /// See also: + /// - https://moor.simonbinder.eu/docs/advanced-features/joins/#joins /// - [innerJoin], [leftOuterJoin] and [crossJoin], which can be used to /// construct a [Join]. /// - [DatabaseConnectionUser.alias], which can be used to build statements diff --git a/moor/lib/src/runtime/query_builder/statements/select/select_with_join.dart b/moor/lib/src/runtime/query_builder/statements/select/select_with_join.dart index 93a641bb..9e551184 100644 --- a/moor/lib/src/runtime/query_builder/statements/select/select_with_join.dart +++ b/moor/lib/src/runtime/query_builder/statements/select/select_with_join.dart @@ -12,11 +12,7 @@ class JoinedSelectStatement JoinedSelectStatement( QueryEngine database, TableInfo table, this._joins, [this.distinct = false, this._includeMainTableInResult = true]) - : super(database, table) { - // use all columns across all tables as result column for this query - _selectedColumns.addAll( - _queriedTables(true).expand((t) => t.$columns).cast()); - } + : super(database, table); /// Whether to generate a `SELECT DISTINCT` query that will remove duplicate /// rows from the result set. @@ -60,6 +56,10 @@ class JoinedSelectStatement @override void writeStartPart(GenerationContext ctx) { + // use all columns across all tables as result column for this query + _selectedColumns.insertAll( + 0, _queriedTables(true).expand((t) => t.$columns).cast()); + ctx.hasMultipleTables = true; ctx.buffer..write(_beginOfSelect(distinct))..write(' '); @@ -74,8 +74,8 @@ class JoinedSelectStatement chosenAlias = '${column.tableName}.${column.$name}'; } else { chosenAlias = 'c$i'; - _columnAliases[column] = chosenAlias; } + _columnAliases[column] = chosenAlias; column.writeInto(ctx); ctx.buffer..write(' AS "')..write(chosenAlias)..write('"'); @@ -145,6 +145,23 @@ class JoinedSelectStatement _selectedColumns.addAll(expressions); } + /// Adds more joined tables to this [JoinedSelectStatement]. + /// + /// Always returns the same instance. + /// + /// See also: + /// - https://moor.simonbinder.eu/docs/advanced-features/joins/#joins + /// - [SimpleSelectStatement.join], which is used for the first join + /// - [innerJoin], [leftOuterJoin] and [crossJoin], which can be used to + /// construct a [Join]. + /// - [DatabaseConnectionUser.alias], which can be used to build statements + /// that refer to the same table multiple times. + // ignore: avoid_returning_this + JoinedSelectStatement join(List joins) { + _joins.addAll(joins); + return this; + } + /// Groups the result by values in [expressions]. /// /// An optional [having] attribute can be set to exclude certain groups. diff --git a/moor/test/join_test.dart b/moor/test/join_test.dart index 7faf9578..74d1e39e 100644 --- a/moor/test/join_test.dart +++ b/moor/test/join_test.dart @@ -225,6 +225,42 @@ void main() { expect(result, 3.0); }); + test('join on JoinedSelectStatement', () async { + final categories = db.categories; + final todos = db.todosTable; + + final query = db.selectOnly(categories).join([ + innerJoin( + todos, + todos.category.equalsExp(categories.id), + useColumns: false, + ) + ]); + query + ..addColumns([categories.id, todos.id.count()]) + ..groupBy([categories.id]); + + when(executor.runSelect(any, any)).thenAnswer((_) async { + return [ + { + 'categories.id': 2, + 'c1': 10, + } + ]; + }); + + final result = await query.getSingle(); + + verify(executor.runSelect( + 'SELECT categories.id AS "categories.id", COUNT(todos.id) AS "c1" ' + 'FROM categories INNER JOIN todos ON todos.category = categories.id ' + 'GROUP BY categories.id;', + [])); + + expect(result.read(categories.id), equals(2)); + expect(result.read(todos.id.count()), equals(10)); + }); + test('injects custom error message when a table is used multiple times', () async { when(executor.runSelect(any, any)).thenAnswer((_) => Future.error('nah'));