diff --git a/moor/lib/src/runtime/database.dart b/moor/lib/src/runtime/database.dart index 832ee2b5..3e92ab96 100644 --- a/moor/lib/src/runtime/database.dart +++ b/moor/lib/src/runtime/database.dart @@ -156,17 +156,43 @@ mixin QueryEngine on DatabaseConnectionUser { TableInfo table) => UpdateStatement(_resolvedEngine, table); - /// Starts a query on the given table. Queries can be limited with an limit - /// or a where clause and can either return a current snapshot or a continuous - /// stream of data + /// Starts a query on the given table. + /// + /// In moor, queries are commonly used as a builder by chaining calls on them + /// using the `..` syntax from Dart. For instance, to load the 10 oldest users + /// with an 'S' in their name, you could use: + /// ```dart + /// Future> oldestUsers() { + /// return ( + /// select(users) + /// ..where((u) => u.name.like('%S%')) + /// ..orderBy([(u) => OrderingTerm( + /// expression: u.id, + /// mode: OrderingMode.asc + /// )]) + /// ..limit(10) + /// ).get(); + /// } + /// ``` + /// + /// The [distinct] parameter (defaults to false) can be used to remove + /// duplicate rows from the result set. + /// + /// For more information on queries, see the + /// [documentation](https://moor.simonbinder.eu/docs/getting-started/writing_queries/). @protected @visibleForTesting SimpleSelectStatement select( - TableInfo table) { - return SimpleSelectStatement(_resolvedEngine, table); + TableInfo table, + {bool distinct = false}) { + return SimpleSelectStatement(_resolvedEngine, table, + distinct: distinct); } /// Starts a [DeleteStatement] that can be used to delete rows from a table. + /// + /// See the [documentation](https://moor.simonbinder.eu/docs/getting-started/writing_queries/#updates-and-deletes) + /// for more details and example on how delete statements work. @protected @visibleForTesting DeleteStatement delete( diff --git a/moor/lib/src/runtime/statements/query.dart b/moor/lib/src/runtime/statements/query.dart index d5f074ce..0063ce5a 100644 --- a/moor/lib/src/runtime/statements/query.dart +++ b/moor/lib/src/runtime/statements/query.dart @@ -161,6 +161,28 @@ class _MappedSelectable extends Selectable { mixin SingleTableQueryMixin on Query { + /// Makes this statement only include rows that match the [filter]. + /// + /// For instance, if you have a table users with an id column, you could + /// select a user with a specific id by using + /// ```dart + /// (select(users)..where((u) => u.id.equals(42))).watchSingle() + /// ``` + /// + /// Please note that this [where] call is different to [Iterable.where] and + /// [Stream.where] in the sense that [filter] will NOT be called for each + /// row. Instead, it will only be called once (with the underlying table as + /// parameter). The result [Expression] will be written as a SQL string and + /// sent to the underlying database engine. The filtering does not happen in + /// Dart. + /// If a where condition has already been set before, the resulting filter + /// will be the conjunction of both calls. + /// + /// For more information, see: + /// - The docs on [expressions](https://moor.simonbinder.eu/docs/getting-started/expressions/), + /// which explains how to express most SQL expressions in Dart. + /// If you want to remove duplicate rows from a query, use the `distinct` + /// parameter on [QueryEngine.select]. void where(Expression filter(T tbl)) { final predicate = filter(table.asDslTable); @@ -212,6 +234,7 @@ mixin SingleTableQueryMixin } } +/// Mixin to provide the high-level [limit] methods for users. mixin LimitContainerMixin on Query { /// Limits the amount of rows returned by capping them at [limit]. If [offset] /// is provided as well, the first [offset] rows will be skipped and not diff --git a/moor/lib/src/runtime/statements/select.dart b/moor/lib/src/runtime/statements/select.dart index 60b75b10..997ce1cd 100644 --- a/moor/lib/src/runtime/statements/select.dart +++ b/moor/lib/src/runtime/statements/select.dart @@ -19,10 +19,15 @@ typedef OrderingTerm OrderClauseGenerator(T tbl); class JoinedSelectStatement extends Query with LimitContainerMixin, Selectable { + /// Whether to generate a `SELECT DISTINCT` query that will remove duplicate + /// rows from the result set. + final bool distinct; + /// Used internally by moor, users should use [SimpleSelectStatement.join] /// instead. JoinedSelectStatement( - QueryEngine database, TableInfo table, this._joins) + QueryEngine database, TableInfo table, this._joins, + [this.distinct = false]) : super(database, table); final List _joins; @@ -38,7 +43,7 @@ class JoinedSelectStatement @override void writeStartPart(GenerationContext ctx) { ctx.hasMultipleTables = true; - ctx.buffer.write('SELECT '); + ctx.buffer..write(_beginOfSelect(distinct))..write(' '); var isFirst = true; for (var table in _tables) { @@ -176,9 +181,14 @@ class JoinedSelectStatement class SimpleSelectStatement extends Query with SingleTableQueryMixin, LimitContainerMixin, Selectable { + /// Whether duplicate rows should be eliminated from the result (this is a + /// `SELECT DISTINCT` statement in sql). Defaults to false. + final bool distinct; + /// Used internally by moor, users will want to call [QueryEngine.select] /// instead. - SimpleSelectStatement(QueryEngine database, TableInfo table) + SimpleSelectStatement(QueryEngine database, TableInfo table, + {this.distinct = false}) : super(database, table); /// The tables this select statement reads from. @@ -187,7 +197,9 @@ class SimpleSelectStatement @override void writeStartPart(GenerationContext ctx) { - ctx.buffer.write('SELECT * FROM ${table.tableWithAlias}'); + ctx.buffer + ..write(_beginOfSelect(distinct)) + ..write(' * FROM ${table.tableWithAlias}'); } @override @@ -226,7 +238,7 @@ class SimpleSelectStatement /// - [DatabaseConnectionUser.alias], which can be used to build statements /// that refer to the same table multiple times. JoinedSelectStatement join(List joins) { - final statement = JoinedSelectStatement(database, table, joins); + final statement = JoinedSelectStatement(database, table, joins, distinct); if (whereExpr != null) { statement.where(whereExpr.predicate); @@ -269,6 +281,10 @@ class SimpleSelectStatement } } +String _beginOfSelect(bool distinct) { + return distinct ? 'SELECT DISTINCT' : 'SELECT'; +} + /// A select statement that is constructed with a raw sql prepared statement /// instead of the high-level moor api. class CustomSelectStatement with Selectable { diff --git a/moor/test/join_test.dart b/moor/test/join_test.dart index b6faa1e0..6c5cbee7 100644 --- a/moor/test/join_test.dart +++ b/moor/test/join_test.dart @@ -48,7 +48,7 @@ void main() { ]); }); - final result = await db.select(todos).join([ + final result = await db.select(todos, distinct: true).join([ leftOuterJoin(categories, categories.id.equalsExp(todos.category)) ]).get(); @@ -67,6 +67,8 @@ void main() { expect( row.readTable(categories), Category(id: 3, description: 'description')); + + verify(executor.runSelect(argThat(contains('DISTINCT')), any)); }); test('reports null when no data is available', () async { diff --git a/moor/test/select_test.dart b/moor/test/select_test.dart index a774b2c9..9f2d46ec 100644 --- a/moor/test/select_test.dart +++ b/moor/test/select_test.dart @@ -31,8 +31,9 @@ void main() { group('SELECT statements are generated', () { test('for simple statements', () { - db.select(db.users).get(); - verify(executor.runSelect('SELECT * FROM users;', argThat(isEmpty))); + db.select(db.users, distinct: true).get(); + verify(executor.runSelect( + 'SELECT DISTINCT * FROM users;', argThat(isEmpty))); }); test('with limit statements', () {