Merge branch 'develop' into refactor-type-system

This commit is contained in:
Simon Binder 2022-07-20 22:35:58 +02:00
commit 3744fa0601
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
34 changed files with 374 additions and 145 deletions

2
.gitignore vendored
View File

@ -16,3 +16,5 @@ flutter_export_environment.sh
.DS_Store
docs/**/*.g.dart
*/build/

View File

@ -5,14 +5,14 @@ _Note: Moor has been renamed to drift_
[![Build Status](https://api.cirrus-ci.com/github/simolus3/moor.svg)](https://github.com/simolus3/drift/actions/workflows/main.yml/badge.svg)
[![Chat on Gitter](https://img.shields.io/gitter/room/moor-dart/community)](https://gitter.im/moor-dart/community)
## Proudly Sponsored by [Stream 💙](https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor)
## Proudly Sponsored by [Stream 💙](https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22)
<p align="center">
<table>
<tbody>
<tr>
<td align="center">
<a href="https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor" target="_blank"><img width="250px" src="https://stream-blog.s3.amazonaws.com/blog/wp-content/uploads/fc148f0fc75d02841d017bb36e14e388/Stream-logo-with-background-.png"/></a><br/><span><a href="https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor" target="_blank">Try the Flutter Chat Tutorial &nbsp💬</a></span>
<a href="https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22" target="_blank"><img width="250px" src="https://stream-blog.s3.amazonaws.com/blog/wp-content/uploads/fc148f0fc75d02841d017bb36e14e388/Stream-logo-with-background-.png"/></a><br/><span><a href="https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22" target="_blank">Try the Flutter Chat Tutorial &nbsp💬</a></span>
</td>
</tr>
</tbody>

View File

@ -0,0 +1,74 @@
import 'package:drift/drift.dart';
import '../tables/filename.dart';
part 'custom_queries.g.dart';
// #docregion manual
class CategoryWithCount {
final Category category;
final int count; // amount of entries in this category
CategoryWithCount({required this.category, required this.count});
}
// #enddocregion manual
// #docregion setup
@DriftDatabase(
tables: [Todos, Categories],
queries: {
'categoriesWithCount': 'SELECT *, '
'(SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" '
'FROM categories c;'
},
)
class MyDatabase extends _$MyDatabase {
// rest of class stays the same
// #enddocregion setup
@override
int get schemaVersion => 1;
MyDatabase(QueryExecutor e) : super(e);
// #docregion run
Future<void> useGeneratedQuery() async {
// The generated query can be run once as a future:
await categoriesWithCount().get();
// Or multiple times as a stream
await for (final snapshot in categoriesWithCount().watch()) {
print('Found ${snapshot.length} category results');
}
}
// #enddocregion run
// #docregion manual
// then, in the database class:
Stream<List<CategoryWithCount>> allCategoriesWithCount() {
// select all categories and load how many associated entries there are for
// each category
return customSelect(
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount"'
' FROM categories c;',
// used for the stream: the stream will update when either table changes
readsFrom: {todos, categories},
).watch().map((rows) {
// we get list of rows here. We just have to turn the raw data from the
// row into a CategoryWithCount instnace. As we defined the Category table
// earlier, drift knows how to parse a category. The only thing left to do
// manually is extracting the amount.
return rows
.map((row) => CategoryWithCount(
category: categories.map(row.data),
count: row.read<int>('amount'),
))
.toList();
});
}
// #enddocregion manual
// #docregion setup
}
// #enddocregion setup

View File

@ -2,45 +2,56 @@
data:
title: "Custom queries"
weight: 10
description: Let drift generate Dart from your SQL statements
description: Write SQL for advanced queries that drift can't express in Dart yet.
aliases:
- /queries/custom
template: layouts/docs/single
---
{% assign snippets = "package:drift_docs/snippets/drift_files/custom_queries.dart.excerpt.json" | readString | json_decode %}
Although drift includes a fluent api that can be used to model most statements, advanced
features like `WITH` clauses or subqueries aren't supported yet. You can
use these features with custom statements. You don't have to miss out on other benefits
drift brings, though: Drift helps you parse the result rows and custom queries also
support auto-updating streams.
features like `WITH` clauses or some subqueries aren't supported yet.
However, you can use methods like `customSelect` and `customStatement` to run advanced
statements on the database by writing the SQL manually.
For most custom queries, drift can analyze their SQL at compile time, make sure they're valid
and generate a type-safe API for them.
This approach can be much safer than writing custom SQL at runtime.
This page describes both approaches: The first section introduces methods generated by drift,
the second section gives an example for a custom query defined at runtime.
## Statements with a generated api
You can instruct drift to automatically generate a typesafe
API for your select, update and delete statements. Of course, you can still write custom
sql manually. See the sections below for details.
sql manually. See the sections below for details.
To use this feature, all you need to is define your queries in your `DriftDatabase` annotation:
```dart
@DriftDatabase(
tables: [Todos, Categories],
queries: {
'categoriesWithCount':
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" FROM categories c;'
},
)
class MyDatabase extends _$MyDatabase {
// rest of class stays the same
}
```
After running the build step again, drift will have written the `CategoriesWithCountResult` class for you -
it will hold the result of your query. Also, the `_$MyDatabase` class from which you inherit will have the
methods `categoriesWithCount` (which runs the query once) and `watchCategoriesWithCount` (which returns
an auto-updating stream).
Queries can have parameters in them by using the `?` or `:name` syntax. When your queries contains parameters,
drift will figure out an appropriate type for them and include them in the generated methods. For instance,
{% include "blocks/snippet" snippets = snippets name = "setup" %}
After running the build step again, drift will have written the `CategoriesWithCountResult` class for you -
it will hold the result of your query. Also, the `_$MyDatabase` class from which you inherit will have a
`Selectable<CategoriesWithCountResult> categoriesWithCount()` method which can be used to run the query.
Like all `Selectable`s in drift, you can use `get()` to run the query once or `watch()` to get an auto-updating
stream of results:
{% block "blocks/alert" title="Better support for custom queries in drift files" %}
Defining SQL in the `@DriftDatabase` annotation is a great way to define a few custom queries. For apps that
use lots of custom queries, extracting them into separate files may be more manageable.
[Drift files]({{ "drift_files.md" | pageUrl }}), which can be included into the database, are a really great fit for this, and may be easier
to use.
{% endblock %}
{% include "blocks/snippet" snippets = snippets name = "run" %}
Queries can have parameters in them by using the `?` or `:name` syntax. For parameters in queries,
drift will figure out an appropriate type and include them in the generated methods. For instance,
`'categoryById': 'SELECT * FROM categories WHERE id = :id'` will generate the method `categoryById(int id)`.
Drift also supports additional convenience features in custom queries, like embededding Dart expressions in
SQL. For more details, see the documentation on [drift files]({{ 'drift_files.md' | pageUrl }}).
{% block "blocks/alert" title="On table names" color="info" %}
To use this feature, it's helpful to know how Dart tables are named in sql. For tables that don't
@ -63,31 +74,9 @@ still send custom queries by calling `customSelect` for a one-time query or
the underlying data changes. Using the todo example introduced in the
[getting started guide]({{ "../Getting started/index.md" | pageUrl }}), we can
write this query which will load the amount of todo entries in each category:
```dart
class CategoryWithCount {
final Category category;
final int count; // amount of entries in this category
CategoryWithCount(this.category, this.count);
}
{% include "blocks/snippet" snippets = snippets name = "manual" %}
// then, in the database class:
Stream<List<CategoryWithCount>> categoriesWithCount() {
// select all categories and load how many associated entries there are for
// each category
return customSelect(
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" FROM categories c;',
readsFrom: {todos, categories}, // used for the stream: the stream will update when either table changes
).watch().map((rows) {
// we get list of rows here. We just have to turn the raw data from the row into a
// CategoryWithCount. As we defined the Category table earlier, drift knows how to parse
// a category. The only thing left to do manually is extracting the amount
return rows
.map((row) => CategoryWithCount(Category.fromData(row.data, this), row.read<int>('amount')))
.toList();
});
}
```
For custom selects, you should use the `readsFrom` parameter to specify from which tables the query is
reading. When using a `Stream`, drift will be able to know after which updates the stream should emit
items.

View File

@ -9,6 +9,14 @@
- To describe the type a column has, use the `DriftSqlType` enum
- To map a value from Dart to SQL and vice-versa, use an instance of `SqlTypes`,
reachable via `database.options.types`.
- __Breaking__: `Expression`s (including `Column`s) always have a non-nullable type
parameter now. They are implicitly nullable, so `TypedResult.read` now returns a
nullable value.
- __Breaking__: `QueryRow.read` can only read non-nullable values now. To read nullable
values, use `readNullable`.
- __Breaking__: Remove the `includeJoinedTableColumns` parameter on `selectOnly()`.
The method now behaves as if that parameter was turned off. To use columns from a
joined table, add them with `addColumns`.
- Consistently handle transaction errors like a failing `BEGIN` or `COMMIT`
across database implementations.
- Support nested transactions.

View File

@ -1,13 +1,13 @@
# Drift
## Proudly Sponsored by [Stream 💙](https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor)
## Proudly Sponsored by [Stream 💙](https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22)
<p align="center">
<table>
<tbody>
<tr>
<td align="center">
<a href="https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor" target="_blank"><img width="250px" src="https://stream-blog.s3.amazonaws.com/blog/wp-content/uploads/fc148f0fc75d02841d017bb36e14e388/Stream-logo-with-background-.png"/></a><br/><span><a href="https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor" target="_blank">Try the Flutter Chat Tutorial &nbsp💬</a></span>
<a href="https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22" target="_blank"><img width="250px" src="https://stream-blog.s3.amazonaws.com/blog/wp-content/uploads/fc148f0fc75d02841d017bb36e14e388/Stream-logo-with-background-.png"/></a><br/><span><a href="https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22" target="_blank">Try the Flutter Chat Tutorial &nbsp💬</a></span>
</td>
</tr>
</tbody>

View File

@ -10,7 +10,7 @@ part of 'main.dart';
class TodoCategory extends DataClass implements Insertable<TodoCategory> {
final int id;
final String name;
TodoCategory({required this.id, required this.name});
const TodoCategory({required this.id, required this.name});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@ -185,7 +185,7 @@ class TodoItem extends DataClass implements Insertable<TodoItem> {
final String? content;
final int categoryId;
final String? generatedText;
TodoItem(
const TodoItem(
{required this.id,
required this.title,
this.content,
@ -466,7 +466,8 @@ class $TodoItemsTable extends TodoItems
class TodoCategoryItemCountData extends DataClass {
final String name;
final int itemCount;
TodoCategoryItemCountData({required this.name, required this.itemCount});
const TodoCategoryItemCountData(
{required this.name, required this.itemCount});
factory TodoCategoryItemCountData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
@ -558,10 +559,8 @@ class $TodoCategoryItemCountView
}
@override
Query? get query => (attachedDatabase.selectOnly(todoCategories,
includeJoinedTableColumns: false)
..addColumns($columns))
.join([
Query? get query =>
(attachedDatabase.selectOnly(todoCategories)..addColumns($columns)).join([
innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id))
]);
@override
@ -571,7 +570,8 @@ class $TodoCategoryItemCountView
class TodoItemWithCategoryNameViewData extends DataClass {
final int id;
final String title;
TodoItemWithCategoryNameViewData({required this.id, required this.title});
const TodoItemWithCategoryNameViewData(
{required this.id, required this.title});
factory TodoItemWithCategoryNameViewData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
@ -668,9 +668,7 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo<
@override
Query? get query =>
(attachedDatabase.selectOnly(todoItems, includeJoinedTableColumns: false)
..addColumns($columns))
.join([
(attachedDatabase.selectOnly(todoItems)..addColumns($columns)).join([
innerJoin(
todoCategories, todoCategories.id.equalsExp(todoItems.categoryId))
]);

View File

@ -242,13 +242,6 @@ abstract class DatabaseConnectionUser {
/// The [distinct] parameter (defaults to false) can be used to remove
/// duplicate rows from the result set.
///
/// The [includeJoinedTableColumns] parameter (defaults to true) can be used
/// to determinate join statement's `useColumns` parameter default value. Set
/// it to false if you don't want to include joined table columns by default.
/// If you leave it on true and don't set `useColumns` parameter to false in
/// join declarations, all columns of joined table will be included in query
/// by default.
///
/// For simple queries, use [select].
///
/// See also:
@ -256,10 +249,9 @@ abstract class DatabaseConnectionUser {
/// - the documentation on [group by](https://drift.simonbinder.eu/docs/advanced-features/joins/#group-by)
JoinedSelectStatement<T, R> selectOnly<T extends HasResultSet, R>(
ResultSetImplementation<T, R> table,
{bool distinct = false,
bool includeJoinedTableColumns = true}) {
{bool distinct = false}) {
return JoinedSelectStatement<T, R>(
resolvedEngine, table, [], distinct, false, includeJoinedTableColumns);
resolvedEngine, table, [], distinct, false, false);
}
/// Starts a [DeleteStatement] that can be used to delete rows from a table.
@ -625,11 +617,8 @@ class TableOrViewOperations<Tbl extends HasResultSet, Row> {
/// Composes a `SELECT` statement only selecting a subset of columns.
///
/// This is equivalent to calling [DatabaseConnectionUser.selectOnly].
JoinedSelectStatement<Tbl, Row> selectOnly(
{bool distinct = false, bool includeJoinedTableColumns = true}) {
return _user.selectOnly(_sourceSet,
distinct: distinct,
includeJoinedTableColumns: includeJoinedTableColumns);
JoinedSelectStatement<Tbl, Row> selectOnly({bool distinct = false}) {
return _user.selectOnly(_sourceSet, distinct: distinct);
}
}

View File

@ -102,8 +102,8 @@ class Migrator {
}
}
GenerationContext _createContext() {
return GenerationContext.fromDb(_db);
GenerationContext _createContext({bool supportsVariables = false}) {
return GenerationContext.fromDb(_db, supportsVariables: supportsVariables);
}
/// Creates the given table if it doesn't exist
@ -197,7 +197,7 @@ class Migrator {
await createTable(temporaryTable);
// Step 5: Transfer old content into the new table
final context = _createContext();
final context = _createContext(supportsVariables: true);
final expressionsForSelect = <Expression>[];
context.buffer.write('INSERT INTO $temporaryName (');

View File

@ -14,11 +14,8 @@ extension TableOrViewStatements<Tbl extends HasResultSet, Row>
/// Composes a `SELECT` statement only selecting a subset of columns.
///
/// This is equivalent to calling [DatabaseConnectionUser.selectOnly].
JoinedSelectStatement<Tbl, Row> selectOnly(
{bool distinct = false, bool includeJoinedTableColumns = true}) {
return attachedDatabase.selectOnly(this,
distinct: distinct,
includeJoinedTableColumns: includeJoinedTableColumns);
JoinedSelectStatement<Tbl, Row> selectOnly({bool distinct = false}) {
return attachedDatabase.selectOnly(this, distinct: distinct);
}
}

View File

@ -189,7 +189,7 @@ void main() {
test('updates stream queries', () async {
await db.batch((b) {
b.insert(db.todosTable, TodoEntry(id: 3, content: 'content'));
b.insert(db.todosTable, const TodoEntry(id: 3, content: 'content'));
b.update(db.users, const UsersCompanion(name: Value('new user name')));
b.replace(

View File

@ -77,7 +77,7 @@ void main() {
});
test('generated data classes can be converted to companions', () {
final entry = Category(
const entry = Category(
id: 3,
description: 'description',
priority: CategoryPriority.low,
@ -97,7 +97,7 @@ void main() {
});
test('data classes can be converted to companions with null to absent', () {
final entry = PureDefault(txt: null);
const entry = PureDefault(txt: null);
expect(entry.toCompanion(false),
const PureDefaultsCompanion(txt: Value(null)));

View File

@ -33,14 +33,18 @@ void main() {
.go();
verify(executor.runDelete(
'DELETE FROM users WHERE NOT is_awesome OR id < ?;', [100]));
'DELETE FROM users WHERE NOT is_awesome OR id < ?;', const [100]));
});
test('to delete an entity via a dataclasss', () async {
await db.delete(db.sharedTodos).delete(SharedTodo(todo: 3, user: 2));
await db
.delete(db.sharedTodos)
.delete(const SharedTodo(todo: 3, user: 2));
verify(executor.runDelete(
'DELETE FROM shared_todos WHERE todo = ? AND user = ?;', [3, 2]));
'DELETE FROM shared_todos WHERE todo = ? AND user = ?;',
const [3, 2],
));
});
});
@ -75,19 +79,19 @@ void main() {
test('delete()', () async {
await db.users.delete().go();
verify(executor.runDelete('DELETE FROM users;', []));
verify(executor.runDelete('DELETE FROM users;', const []));
});
test('deleteOne()', () async {
await db.users.deleteOne(const UsersCompanion(id: Value(3)));
verify(executor.runDelete('DELETE FROM users WHERE id = ?;', [3]));
verify(executor.runDelete('DELETE FROM users WHERE id = ?;', const [3]));
});
test('deleteWhere', () async {
await db.users.deleteWhere((tbl) => tbl.id.isSmallerThanValue(3));
verify(executor.runDelete('DELETE FROM users WHERE id < ?;', [3]));
verify(executor.runDelete('DELETE FROM users WHERE id < ?;', const [3]));
});
});
}

View File

@ -55,7 +55,7 @@ void main() {
test('generates insert or replace statements', () async {
await db.into(db.todosTable).insert(
TodoEntry(
const TodoEntry(
id: 113,
content: 'Done',
),
@ -452,7 +452,7 @@ void main() {
CategoriesCompanion.insert(description: 'description'));
expect(
row,
Category(
const Category(
id: 1,
description: 'description',
descriptionInUpperCase: 'DESCRIPTION',
@ -503,7 +503,7 @@ void main() {
CategoriesCompanion.insert(description: 'description'));
expect(
row,
Category(
const Category(
id: 1,
description: 'description',
descriptionInUpperCase: 'DESCRIPTION',

View File

@ -72,7 +72,7 @@ void main() {
expect(
row.readTable(categories),
Category(
const Category(
id: 3,
description: 'description',
priority: CategoryPriority.high,
@ -107,7 +107,7 @@ void main() {
expect(() => row.readTable(db.categories), throwsArgumentError);
expect(
row.readTable(db.todosTable),
TodoEntry(
const TodoEntry(
id: 5,
title: 'title',
content: 'content',
@ -224,7 +224,7 @@ void main() {
expect(
result.readTable(categories),
equals(
Category(
const Category(
id: 3,
description: 'Description',
descriptionInUpperCase: 'DESCRIPTION',
@ -274,7 +274,7 @@ void main() {
expect(
result.readTable(categories),
equals(
Category(
const Category(
id: 3,
description: 'Description',
descriptionInUpperCase: 'DESCRIPTION',
@ -330,7 +330,7 @@ void main() {
expect(result.readTableOrNull(todos), isNull);
expect(
result.readTable(categories),
Category(
const Category(
id: 3,
description: 'desc',
descriptionInUpperCase: 'DESC',
@ -406,8 +406,7 @@ void main() {
final categories = db.categories;
final todos = db.todosTable;
final query =
db.selectOnly(categories, includeJoinedTableColumns: false).join([
final query = db.selectOnly(categories).join([
innerJoin(
todos,
todos.category.equalsExp(categories.id),

View File

@ -187,6 +187,28 @@ void main() {
verify(executor.transactions.runCustom(any, any));
verifyNever(executor.runCustom(any, any));
});
test('removes variables in `CREATE TABLE` statements', () async {
final executor = MockExecutor();
final db = _DefaultDb(executor);
late GeneratedColumn<int> column;
column = GeneratedColumn<int>(
'foo',
'foo',
true,
type: DriftSqlType.int,
check: () => column.isSmallerThan(const Variable(3)),
);
final table = CustomTable('foo', db, [column]);
await db.createMigrator().createTable(table);
await db.close();
// This should not attempt to generate a parameter (`?`)
// https://github.com/simolus3/drift/discussions/1936
verify(executor.runCustom(argThat(contains('CHECK(foo < 3)')), []));
});
}
class _DefaultDb extends GeneratedDatabase {

View File

@ -14,7 +14,7 @@ final _dataOfTodoEntry = {
'category': 3
};
final _todoEntry = TodoEntry(
const _todoEntry = TodoEntry(
id: 10,
title: 'A todo title',
content: 'Content',
@ -123,7 +123,7 @@ void main() {
'category': null,
}
];
final resolved = TodoEntry(
const resolved = TodoEntry(
id: 10,
title: null,
content: 'Content',
@ -193,7 +193,7 @@ void main() {
expect(
category,
Category(
const Category(
id: 1,
description: 'description',
descriptionInUpperCase: 'DESCRIPTION',

View File

@ -52,7 +52,7 @@ void main() {
group('generates replace statements', () {
test('regular', () async {
await db.update(db.todosTable).replace(TodoEntry(
await db.update(db.todosTable).replace(const TodoEntry(
id: 3,
title: 'Title',
content: 'Updated content',

View File

@ -43,7 +43,7 @@ void main() {
final user = db.sharedTodos.mapFromCompanion(companion, db);
expect(
user,
SharedTodo(todo: 3, user: 4),
const SharedTodo(todo: 3, user: 4),
);
});
@ -58,7 +58,7 @@ void main() {
final todo = db.todosTable.mapFromRowOrNull(QueryRow(rowData, db));
expect(
todo,
TodoEntry(
const TodoEntry(
id: 1,
title: 'some title',
content: 'do this',

View File

@ -12,7 +12,7 @@ class Config extends DataClass implements Insertable<Config> {
final String? configValue;
final SyncType? syncState;
final SyncType? syncStateImplicit;
Config(
const Config(
{required this.configKey,
this.configValue,
this.syncState,
@ -292,7 +292,7 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
class WithDefault extends DataClass implements Insertable<WithDefault> {
final String? a;
final int? b;
WithDefault({this.a, this.b});
const WithDefault({this.a, this.b});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@ -565,7 +565,7 @@ class WithConstraint extends DataClass implements Insertable<WithConstraint> {
final String? a;
final int b;
final double? c;
WithConstraint({this.a, required this.b, this.c});
const WithConstraint({this.a, required this.b, this.c});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@ -783,7 +783,7 @@ class MytableData extends DataClass implements Insertable<MytableData> {
final String? sometext;
final bool? isInserting;
final DateTime? somedate;
MytableData(
const MytableData(
{required this.someid, this.sometext, this.isInserting, this.somedate});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
@ -1046,7 +1046,7 @@ class EMail extends DataClass implements Insertable<EMail> {
final String sender;
final String title;
final String body;
EMail({required this.sender, required this.title, required this.body});
const EMail({required this.sender, required this.title, required this.body});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@ -1261,7 +1261,7 @@ class Email extends Table
class WeirdData extends DataClass implements Insertable<WeirdData> {
final int sqlClass;
final String textColumn;
WeirdData({required this.sqlClass, required this.textColumn});
const WeirdData({required this.sqlClass, required this.textColumn});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@ -1444,7 +1444,7 @@ class MyViewData extends DataClass {
final String? configValue;
final SyncType? syncState;
final SyncType? syncStateImplicit;
MyViewData(
const MyViewData(
{required this.configKey,
this.configValue,
this.syncState,

View File

@ -12,7 +12,7 @@ class Category extends DataClass implements Insertable<Category> {
final String description;
final CategoryPriority priority;
final String descriptionInUpperCase;
Category(
const Category(
{required this.id,
required this.description,
required this.priority,
@ -267,7 +267,7 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
final String content;
final DateTime? targetDate;
final int? category;
TodoEntry(
const TodoEntry(
{required this.id,
this.title,
required this.content,
@ -570,7 +570,7 @@ class User extends DataClass implements Insertable<User> {
final bool isAwesome;
final Uint8List profilePicture;
final DateTime creationTime;
User(
const User(
{required this.id,
required this.name,
required this.isAwesome,
@ -863,7 +863,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
class SharedTodo extends DataClass implements Insertable<SharedTodo> {
final int todo;
final int user;
SharedTodo({required this.todo, required this.user});
const SharedTodo({required this.todo, required this.user});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@ -1229,7 +1229,7 @@ class $TableWithoutPKTable extends TableWithoutPK
class PureDefault extends DataClass implements Insertable<PureDefault> {
final MyCustomObject? txt;
PureDefault({this.txt});
const PureDefault({this.txt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@ -1381,7 +1381,7 @@ class $PureDefaultsTable extends PureDefaults
class CategoryTodoCountViewData extends DataClass {
final String description;
final int itemCount;
CategoryTodoCountViewData(
const CategoryTodoCountViewData(
{required this.description, required this.itemCount});
factory CategoryTodoCountViewData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
@ -1477,8 +1477,7 @@ class $CategoryTodoCountViewView
@override
Query? get query =>
(attachedDatabase.selectOnly(categories, includeJoinedTableColumns: false)
..addColumns($columns))
(attachedDatabase.selectOnly(categories)..addColumns($columns))
.join([innerJoin(todos, todos.category.equalsExp(categories.id))])
..groupBy([categories.id]);
@override
@ -1488,7 +1487,7 @@ class $CategoryTodoCountViewView
class TodoWithCategoryViewData extends DataClass {
final String? title;
final String description;
TodoWithCategoryViewData({this.title, required this.description});
const TodoWithCategoryViewData({this.title, required this.description});
factory TodoWithCategoryViewData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
@ -1580,9 +1579,7 @@ class $TodoWithCategoryViewView
}
@override
Query? get query => (attachedDatabase.selectOnly(todos,
includeJoinedTableColumns: false)
..addColumns($columns))
Query? get query => (attachedDatabase.selectOnly(todos)..addColumns($columns))
.join([innerJoin(categories, categories.id.equalsExp(todos.category))]);
@override
Set<String> get readTables => const {'todos', 'categories'};

View File

@ -69,7 +69,7 @@ void main() {
test('can be used in a query stream', () async {
final stream = db.readView().watch();
final entry = Config(
const entry = Config(
configKey: 'another_key',
configValue: 'value',
syncState: SyncType.synchronized,
@ -141,7 +141,7 @@ void main() {
expect(result, hasLength(1));
expect(
result.single,
Config(
const Config(
configKey: 'key2',
configValue: 'val',
syncState: SyncType.locallyCreated,

View File

@ -124,7 +124,7 @@ void main() {
verify(
mock.runSelect('SELECT * FROM config WHERE config_key = ?1', ['key']));
expect(parsed, Config(configKey: 'key', configValue: 'value'));
expect(parsed, const Config(configKey: 'key', configValue: 'value'));
});
test('applies default parameter expressions when not set', () async {
@ -167,7 +167,7 @@ void main() {
row: QueryRow(row, db),
a: 'text for a',
b: 42,
c: WithConstraint(a: 'text', b: 1337, c: 18.7),
c: const WithConstraint(a: 'text', b: 1337, c: 18.7),
),
);
});
@ -216,7 +216,7 @@ void main() {
final entry = await db.readConfig('key').getSingle();
expect(
entry,
Config(
const Config(
configKey: 'key',
configValue: 'value',
syncState: SyncType.locallyUpdated,

View File

@ -104,7 +104,7 @@ void main() {
expect(
entry,
Category(
const Category(
id: 1,
description: 'Description',
priority: CategoryPriority.low,

View File

@ -14,7 +14,12 @@ void main() {
db.select(db.myView).watch(),
emitsInOrder([
isEmpty,
[MyViewData(configKey: 'another', syncState: SyncType.synchronized)]
[
const MyViewData(
configKey: 'another',
syncState: SyncType.synchronized,
),
]
]),
);

View File

@ -40,7 +40,7 @@ void main() {
final result = await db.todoWithCategoryView.select().getSingle();
expect(
result,
TodoWithCategoryViewData(
const TodoWithCategoryViewData(
description: 'category description', title: 'title'));
});
}

View File

@ -171,7 +171,10 @@ void _runTests(FutureOr<DriftIsolate> Function() spawner, bool terminateIsolate,
await expectLater(stream, emits(null));
await database.into(database.todosTable).insert(initialCompanion);
await expectLater(stream, emits(TodoEntry(id: 1, content: 'my content')));
await expectLater(
stream,
emits(const TodoEntry(id: 1, content: 'my content')),
);
});
test('can start transactions', () async {

View File

@ -29,3 +29,29 @@ class _NullExecutor extends Fake implements QueryExecutor {
@override
SqlDialect get dialect => SqlDialect.sqlite;
}
class CustomTable extends Table with TableInfo<CustomTable, Null> {
@override
final String actualTableName;
@override
final DatabaseConnectionUser attachedDatabase;
final List<GeneratedColumn<Object>> columns;
final String? _alias;
CustomTable(this.actualTableName, this.attachedDatabase, this.columns,
[this._alias]);
@override
List<GeneratedColumn<Object>> get $columns => columns;
@override
String get aliasedName => _alias ?? actualTableName;
@override
CustomTable createAlias(String alias) {
return CustomTable(actualTableName, attachedDatabase, columns, alias);
}
@override
Null map(Map<String, dynamic> data, {String? tablePrefix}) => null;
}

View File

@ -53,6 +53,10 @@ class DataClassWriter {
}
// write constructor with named optional fields
if (!scope.options.generateMutableClasses) {
_buffer.write('const ');
}
_buffer
..write(table.dartTypeName)
..write('({')

View File

@ -132,8 +132,8 @@ class ViewWriter extends TableOrViewWriter {
buffer.write('@override\nQuery? get query => ');
final query = view.viewQuery;
if (query != null) {
buffer.write('(attachedDatabase.selectOnly(${query.from}, '
'includeJoinedTableColumns: false)..addColumns(\$columns))'
buffer.write('(attachedDatabase.selectOnly(${query.from})'
'..addColumns(\$columns))'
'${query.query};');
} else {
buffer.write('null;\n');

View File

@ -2,7 +2,7 @@ import 'package:drift_dev/src/model/model.dart';
import 'package:test/test.dart';
void main() {
test('removes leaading numbers', () {
test('removes leading numbers', () {
expect(dartNameForSqlColumn('foo'), 'foo');
expect(dartNameForSqlColumn('123a'), 'a');
});

View File

@ -0,0 +1,111 @@
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:drift_dev/src/backends/build/drift_builder.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
const _testInput = r'''
import 'package:drift/drift.dart';
part 'main.moor.dart';
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
@DriftDatabase(
tables: [Users],
)
class Database extends _$Database {}
''';
void main() {
test(
'generates const constructor for data classes can companion classes',
() async {
await testBuilder(
DriftPartBuilder(const BuilderOptions({})),
const {'a|lib/main.dart': _testInput},
reader: await PackageAssetReader.currentIsolate(),
outputs: const {
'a|lib/main.moor.dart': _GeneratesConstDataClasses(
{'User', 'UsersCompanion'},
),
},
);
},
tags: 'analyzer',
);
}
class _GeneratesConstDataClasses extends Matcher {
final Set<String> expectedWithConstConstructor;
const _GeneratesConstDataClasses(this.expectedWithConstConstructor);
@override
Description describe(Description description) {
return description.add('generates classes $expectedWithConstConstructor '
'const constructor.');
}
@override
bool matches(dynamic desc, Map matchState) {
// Parse the file, assure we don't have final fields in data classes.
final resourceProvider = MemoryResourceProvider();
if (desc is List<int>) {
resourceProvider.newFileWithBytes('/foo.dart', desc);
} else if (desc is String) {
resourceProvider.newFile('/foo.dart', desc);
} else {
desc['desc'] = 'Neither a List<int> or String - cannot be parsed';
return false;
}
final parsed = parseFile(
path: '/foo.dart',
featureSet: FeatureSet.fromEnableFlags2(
sdkLanguageVersion: Version(2, 12, 0),
flags: const [],
),
resourceProvider: resourceProvider,
throwIfDiagnostics: true,
).unit;
final remaining = expectedWithConstConstructor.toSet();
final definedClasses = parsed.declarations.whereType<ClassDeclaration>();
for (final definedClass in definedClasses) {
if (expectedWithConstConstructor.contains(definedClass.name.name)) {
final constructor = definedClass.getConstructor(null);
if (constructor?.constKeyword == null) {
matchState['desc'] = 'Constructor ${definedClass.name.name} is not '
'const.';
return false;
}
remaining.remove(definedClass.name.name);
}
}
// Also ensure that all expected classes were generated.
if (remaining.isNotEmpty) {
matchState['desc'] = 'Did not generate $remaining classes';
return false;
}
return true;
}
@override
Description describeMismatch(dynamic item, Description mismatchDescription,
Map matchState, bool verbose) {
return mismatchDescription
.add((matchState['desc'] as String?) ?? 'Had syntax errors');
}
}

View File

@ -76,8 +76,9 @@ class AppDatabase extends _$AppDatabase {
/// Returns an auto-updating stream of all todo entries in a given category
/// id.
Stream<List<TodoEntryWithCategory>> entriesInCategory(int? categoryId) {
final query = select(todoEntries).join(
[leftOuterJoin(categories, categories.id.equalsExp(todoEntries.id))]);
final query = select(todoEntries).join([
leftOuterJoin(categories, categories.id.equalsExp(todoEntries.category))
]);
if (categoryId != null) {
query.where(categories.id.equals(categoryId));

View File

@ -7,14 +7,14 @@ Thank you for understanding!
__To start using Drift, read our detailed [docs](https://drift.simonbinder.eu/docs/getting-started/).__
## Proudly Sponsored by [Stream 💙](https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor)
## Proudly Sponsored by [Stream 💙](https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22)
<p align="center">
<table>
<tbody>
<tr>
<td align="center">
<a href="https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor" target="_blank"><img width="250px" src="https://stream-blog.s3.amazonaws.com/blog/wp-content/uploads/fc148f0fc75d02841d017bb36e14e388/Stream-logo-with-background-.png"/></a><br/><span><a href="https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=moor" target="_blank">Try the Flutter Chat Tutorial &nbsp💬</a></span>
<a href="https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22" target="_blank"><img width="250px" src="https://stream-blog.s3.amazonaws.com/blog/wp-content/uploads/fc148f0fc75d02841d017bb36e14e388/Stream-logo-with-background-.png"/></a><br/><span><a href="https://getstream.io/chat/sdk/android/?utm_source=Moor&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Moor_July2022_AndroidChatSDK_klmh22" target="_blank">Try the Flutter Chat Tutorial &nbsp💬</a></span>
</td>
</tr>
</tbody>