Skip to content

Commit

Permalink
Merge pull request #3387 from simolus3/manager-order-nulls
Browse files Browse the repository at this point in the history
Add null ordering to manager API
  • Loading branch information
dickermoshe authored Dec 20, 2024
2 parents a1c01d4 + 8453f79 commit 0e2b861
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 21 deletions.
19 changes: 12 additions & 7 deletions docs/docs/dart_api/manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ This will return a record with the entity and a `refs` object which contains the

{{ load_snippet('manager_references','lib/snippets/dart_api/manager.dart.excerpt.json') }}

The problem with the above approach is that it will issue a separate query for each row in the result set. This can be very inefficient if you have a large number of rows.
The problem with the above approach is that it will issue a separate query for each row in the result set. This can be very inefficient if you have a large number of rows.
If there were 1000 todos, this would issue 1000 queries to fetch the category for each todo.

!!! note "Filter on foreign keys"

When filtering on a reference column, drift will apply the filter to the column itself instead of joining the referenced table.
For example, `todos.filter((f) => f.category.id(1))` will filter on the `category` column on the `todos` table, instead of joining the two tables and filtering on the `id` column of the `categories` table.

Expand All @@ -70,7 +70,7 @@ You can filter across references to other tables by using the generated referenc

{{ load_snippet('manager_filter_forward_references','lib/snippets/dart_api/manager.dart.excerpt.json') }}

You can also filter across back references. This is useful when you have a one-to-many relationship and want to filter the parent table based on the child table.
You can also filter across back references. This is useful when you have a one-to-many relationship and want to filter the parent table based on the child table.

{{ load_snippet('manager_filter_back_references','lib/snippets/dart_api/manager.dart.excerpt.json') }}

Expand All @@ -87,8 +87,8 @@ In this example, had we not specified a custom name for the reference, the code


#### Name Clashes
Drift auto-generates filters and orderings based on the names of your tables and fields. However, many times, there will be duplicates.
When this happens, you will see a warning message from the generator.
Drift auto-generates filters and orderings based on the names of your tables and fields. However, many times, there will be duplicates.
When this happens, you will see a warning message from the generator.
To fix this issue, use the `@ReferenceName()` annotation to specify what we should name the filter/orderings.


Expand All @@ -100,6 +100,11 @@ You can also use ordering across multiple tables just like with filters.

{{ load_snippet('manager_ordering','lib/snippets/dart_api/manager.dart.excerpt.json') }}

When including nullable columns in `orderBy`, you might want to control whether `NULL` values are
placed at the start or end of the results. This is possible with the `nulls` parameter on `asc()` and
`desc()`.
For instance, you could write `o.title.asc(nulls: NullsOrder.first)` to request that todo items without
a title appear before those that have one.

### Count and exists
The manager makes it easy to check if a row exists or to count the number of rows that match a certain condition.
Expand Down Expand Up @@ -135,7 +140,7 @@ Any rows that meet the specified condition will be deleted.

## Computed Fields

Manager queries are great when you need to select entire rows from a database table along with their related data. However, there are situations where you might want to perform more complex operations directly within the database for better efficiency.
Manager queries are great when you need to select entire rows from a database table along with their related data. However, there are situations where you might want to perform more complex operations directly within the database for better efficiency.

Drift offers strong support for writing SQL expressions. These expressions can be used to filter data, sort results, and perform various calculations directly within your SQL queries. This means you can leverage the full power of SQL to handle complex logic right in the database, making your queries more efficient and your code cleaner.

Expand All @@ -152,7 +157,7 @@ You can also use [aggregate](./expressions.md#aggregate-functions-like-count-and

{{ load_snippet('aggregated_annotations','lib/snippets/dart_api/manager.dart.excerpt.json') }}

<!--
<!--
This documentation should added once the internal manager APIs are more stable
## Extensions
Expand Down
1 change: 1 addition & 0 deletions drift/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 2.23.0-dev

- Allow building compound select statements in Dart.
- Support `NULLS FIRST` and `NULLS LAST` in manager API.

## 2.22.1

Expand Down
33 changes: 19 additions & 14 deletions drift/lib/src/runtime/manager/ordering.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,44 +19,49 @@ class ColumnOrderings<T extends Object> {
return ComposableOrdering._(orderings);
}

/// Sort this column in ascending order
/// Sort this column in ascending order (1 -> 10 | A -> Z | Jan 1 -> Dec 31).
///
/// 1 -> 10 | A -> Z | Jan 1 -> Dec 31
ComposableOrdering asc() =>
$composableOrdering({OrderingBuilder(OrderingMode.asc, column)});
/// The optional [nulls] parameter can be used to control whether `NULL`
/// values in the column should come for or after non-null values.
ComposableOrdering asc({NullsOrder? nulls}) => $composableOrdering(
{OrderingBuilder(OrderingMode.asc, column, nulls: nulls)});

/// Sort this column in descending order
/// Sort this column in descending order (10 -> 1 | Z -> A | Dec 31 -> Jan 1).
///
/// 10 -> 1 | Z -> A | Dec 31 -> Jan 1
ComposableOrdering desc() =>
$composableOrdering({OrderingBuilder(OrderingMode.desc, column)});
/// The optional [nulls] parameter can be used to control whether `NULL`
/// values in the column should come for or after non-null values.
ComposableOrdering desc({NullsOrder? nulls}) => $composableOrdering(
{OrderingBuilder(OrderingMode.desc, column, nulls: nulls)});
}

/// Defines a class which will hold the information needed to create an ordering
class OrderingBuilder {
final class OrderingBuilder {
/// The mode of the ordering
final OrderingMode mode;

/// The column that the ordering is applied to
final Expression<Object> column;

/// How null values are treated in the ordering.
final NullsOrder? nulls;

/// Create a new ordering builder, will be used by the [TableManagerState] to create [OrderingTerm]s
@internal
OrderingBuilder(this.mode, this.column);
OrderingBuilder(this.mode, this.column, {this.nulls});

@override
bool operator ==(covariant OrderingBuilder other) {
if (identical(this, other)) return true;

return other.mode == mode && other.column == column;
return other.mode == mode && other.column == column && other.nulls == nulls;
}

@override
int get hashCode => mode.hashCode ^ column.hashCode;
int get hashCode => Object.hash(mode, column, nulls);

/// Build the ordering term using the expression and direction
OrderingTerm buildTerm() {
return OrderingTerm(mode: mode, expression: column);
return OrderingTerm(mode: mode, expression: column, nulls: nulls);
}
}

Expand All @@ -65,7 +70,7 @@ class OrderingBuilder {
/// Multiple orderings can be composed together using the `&` operator.
/// The orderings will be executed from left to right.
class ComposableOrdering {
final class ComposableOrdering {
/// The orderings that are being composed
final Set<OrderingBuilder> orderingBuilders;

Expand Down
26 changes: 26 additions & 0 deletions drift/test/manager/manager_order_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,32 @@ void main() {
1);
});

test('nulls first', () async {
await db.managers.todosTable.bulkCreate((o) => [
o(content: 'a'),
o(title: Value('first title'), content: 'b'),
o(title: Value('second title'), content: 'c'),
]);

final entries = await db.managers.todosTable
.orderBy((o) => o.title.asc(nulls: NullsOrder.first))
.get();
expect(entries.map((e) => e.content), ['a', 'b', 'c']);
});

test('nulls last', () async {
await db.managers.todosTable.bulkCreate((o) => [
o(title: Value('second title'), content: 'a'),
o(title: Value('first title'), content: 'b'),
o(content: 'c'),
]);

final entries = await db.managers.todosTable
.orderBy((o) => o.title.desc(nulls: NullsOrder.last))
.get();
expect(entries.map((e) => e.content), ['a', 'b', 'c']);
});

test('manager - order related', () async {
final schoolCategoryId = await db.managers.categories.create((o) =>
o(priority: Value(CategoryPriority.high), description: "School"));
Expand Down

0 comments on commit 0e2b861

Please sign in to comment.