Merge pull request #1541 from westito/views

Dsl views
This commit is contained in:
Simon Binder 2021-12-02 22:47:32 +01:00 committed by GitHub
commit 18206c8510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1682 additions and 120 deletions

View File

@ -167,3 +167,66 @@ Applying a `customConstraint` will override all other constraints that would be
particular, that means that we need to also include the `NOT NULL` constraint again.
You can also add table-wide constraints by overriding the `customConstraints` getter in your table class.
## References
[Foreign key references](https://www.sqlite.org/foreignkeys.html) can be expressed
in Dart tables with the `references()` method when building a column:
```dart
class Todos extends Table {
// ...
IntColumn get category => integer().nullable().references(Categories, #id)();
}
@DataClassName("Category")
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
// and more columns...
}
```
The first parameter to `references` points to the table on which a reference should be created.
The second parameter is a [symbol](https://dart.dev/guides/language/language-tour#symbols) of the column to use for the reference.
Optionally, the `onUpdate` and `onDelete` parameters can be used to describe what
should happen when the target row gets updated or deleted.
Be aware that, in sqlite3, foreign key references aren't enabled by default.
They need to be enabled with `PRAGMA foreign_keys = ON`.
A suitable place to issue that pragma with drift is in a [post-migration callback]({{ '../Advanced Features/migrations.md#post-migration-callbacks' | pageUrl }}).
## Views
It is also possible to define [SQL views](https://www.sqlite.org/lang_createview.html)
as Dart classes.
To do so, write an abstract class extending `View`. This example declares a view reading
the amount of todo-items added to a category in the schema from [the example]({{ 'index.md' | pageUrl }}):
```dart
abstract class CategoryTodoCount extends View {
TodosTable get todos;
Categories get categories;
Expression<int> get itemCount => todos.id.count();
@override
Query as() => select([categories.description, itemCount])
.from(categories)
.join([innerJoin(todos, todos.category.equalsExp(categories.id))]);
}
```
Inside a Dart view, use
- abstract getters to declare tables that you'll read from (e.g. `TodosTable get todos`)
- `Expression` getters to add columns: (e.g. `itemCount => todos.id.count()`);
- the overridden `as` method to define the select statement backing the view
Finally, a view needs to be added to a database or accessor by including it in the
`views` parameter:
```dart
@DriftDatabase(tables: [Todos, Categories], views: [CategoryTodoCount])
class MyDatabase extends _$MyDatabase {
```

View File

@ -5,18 +5,67 @@ import 'package:drift/native.dart';
part 'main.g.dart';
@DataClassName('TodoCategory')
class TodoCategories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
class TodoItems extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text()();
TextColumn get content => text().nullable()();
IntColumn get categoryId => integer().references(TodoCategories, #id)();
TextColumn get generatedText => text().nullable().generatedAs(
title + const Constant(' (') + content + const Constant(')'))();
}
@DriftDatabase(tables: [TodoItems])
abstract class TodoCategoryItemCount extends View {
TodoItems get todoItems;
TodoCategories get todoCategories;
Expression<int> get itemCount => todoItems.id.count();
@override
Query as() => select([
todoCategories.name,
itemCount,
]).from(todoCategories).join([
innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id))
]);
}
@DriftView(name: 'customViewName')
abstract class TodoItemWithCategoryNameView extends View {
TodoItems get todoItems;
TodoCategories get todoCategories;
Expression<String> get title =>
todoItems.title +
const Constant('(') +
todoCategories.name +
const Constant(')');
@override
Query as() => select([todoItems.id, title]).from(todoItems).join([
innerJoin(
todoCategories, todoCategories.id.equalsExp(todoItems.categoryId))
]);
}
@DriftDatabase(tables: [
TodoItems,
TodoCategories,
], views: [
TodoCategoryItemCount,
TodoItemWithCategoryNameView,
])
class Database extends _$Database {
Database(QueryExecutor e) : super(e);
@override
int get schemaVersion => 1;
int get schemaVersion => 2;
@override
MigrationStrategy get migration {
@ -27,11 +76,12 @@ class Database extends _$Database {
// Add a bunch of default items in a batch
await batch((b) {
b.insertAll(todoItems, [
TodoItemsCompanion.insert(title: 'A first entry'),
TodoItemsCompanion.insert(title: 'A first entry', categoryId: 0),
TodoItemsCompanion.insert(
title: 'Todo: Checkout drift',
content: const Value('Drift is a persistence library for Dart '
'and Flutter applications.'),
categoryId: 0,
),
]);
});
@ -55,10 +105,17 @@ Future<void> main() async {
print('Todo-item in database: $event');
});
// Add category
final categoryId = await db
.into(db.todoCategories)
.insert(TodoCategoriesCompanion.insert(name: 'Category'));
// Add another entry
await db
.into(db.todoItems)
.insert(TodoItemsCompanion.insert(title: 'Another entry added later'));
await db.into(db.todoItems).insert(TodoItemsCompanion.insert(
title: 'Another entry added later', categoryId: categoryId));
(await db.select(db.customViewName).get()).forEach(print);
(await db.select(db.todoCategoryItemCount).get()).forEach(print);
// Delete all todo items
await db.delete(db.todoItems).go();

View File

@ -7,11 +7,193 @@ part of 'main.dart';
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class TodoCategory extends DataClass implements Insertable<TodoCategory> {
final int id;
final String name;
TodoCategory({required this.id, required this.name});
factory TodoCategory.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return TodoCategory(
id: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}id'])!,
name: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}name'])!,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['name'] = Variable<String>(name);
return map;
}
TodoCategoriesCompanion toCompanion(bool nullToAbsent) {
return TodoCategoriesCompanion(
id: Value(id),
name: Value(name),
);
}
factory TodoCategory.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TodoCategory(
id: serializer.fromJson<int>(json['id']),
name: serializer.fromJson<String>(json['name']),
);
}
factory TodoCategory.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
TodoCategory.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'name': serializer.toJson<String>(name),
};
}
TodoCategory copyWith({int? id, String? name}) => TodoCategory(
id: id ?? this.id,
name: name ?? this.name,
);
@override
String toString() {
return (StringBuffer('TodoCategory(')
..write('id: $id, ')
..write('name: $name')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, name);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TodoCategory && other.id == this.id && other.name == this.name);
}
class TodoCategoriesCompanion extends UpdateCompanion<TodoCategory> {
final Value<int> id;
final Value<String> name;
const TodoCategoriesCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
});
TodoCategoriesCompanion.insert({
this.id = const Value.absent(),
required String name,
}) : name = Value(name);
static Insertable<TodoCategory> custom({
Expression<int>? id,
Expression<String>? name,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
});
}
TodoCategoriesCompanion copyWith({Value<int>? id, Value<String>? name}) {
return TodoCategoriesCompanion(
id: id ?? this.id,
name: name ?? this.name,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('TodoCategoriesCompanion(')
..write('id: $id, ')
..write('name: $name')
..write(')'))
.toString();
}
}
class $TodoCategoriesTable extends TodoCategories
with TableInfo<$TodoCategoriesTable, TodoCategory> {
final GeneratedDatabase _db;
final String? _alias;
$TodoCategoriesTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String?> name = GeneratedColumn<String?>(
'name', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [id, name];
@override
String get aliasedName => _alias ?? 'todo_categories';
@override
String get actualTableName => 'todo_categories';
@override
VerificationContext validateIntegrity(Insertable<TodoCategory> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('name')) {
context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
} else if (isInserting) {
context.missing(_nameMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
TodoCategory map(Map<String, dynamic> data, {String? tablePrefix}) {
return TodoCategory.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@override
$TodoCategoriesTable createAlias(String alias) {
return $TodoCategoriesTable(_db, alias);
}
}
class TodoItem extends DataClass implements Insertable<TodoItem> {
final int id;
final String title;
final String? content;
TodoItem({required this.id, required this.title, this.content});
final int categoryId;
final String? generatedText;
TodoItem(
{required this.id,
required this.title,
this.content,
required this.categoryId,
this.generatedText});
factory TodoItem.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? '';
return TodoItem(
@ -21,6 +203,10 @@ class TodoItem extends DataClass implements Insertable<TodoItem> {
.mapFromDatabaseResponse(data['${effectivePrefix}title'])!,
content: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}content']),
categoryId: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}category_id'])!,
generatedText: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}generated_text']),
);
}
@override
@ -31,6 +217,7 @@ class TodoItem extends DataClass implements Insertable<TodoItem> {
if (!nullToAbsent || content != null) {
map['content'] = Variable<String?>(content);
}
map['category_id'] = Variable<int>(categoryId);
return map;
}
@ -41,6 +228,7 @@ class TodoItem extends DataClass implements Insertable<TodoItem> {
content: content == null && nullToAbsent
? const Value.absent()
: Value(content),
categoryId: Value(categoryId),
);
}
@ -51,6 +239,8 @@ class TodoItem extends DataClass implements Insertable<TodoItem> {
id: serializer.fromJson<int>(json['id']),
title: serializer.fromJson<String>(json['title']),
content: serializer.fromJson<String?>(json['content']),
categoryId: serializer.fromJson<int>(json['categoryId']),
generatedText: serializer.fromJson<String?>(json['generatedText']),
);
}
factory TodoItem.fromJsonString(String encodedJson,
@ -65,71 +255,93 @@ class TodoItem extends DataClass implements Insertable<TodoItem> {
'id': serializer.toJson<int>(id),
'title': serializer.toJson<String>(title),
'content': serializer.toJson<String?>(content),
'categoryId': serializer.toJson<int>(categoryId),
'generatedText': serializer.toJson<String?>(generatedText),
};
}
TodoItem copyWith(
{int? id,
String? title,
Value<String?> content = const Value.absent()}) =>
Value<String?> content = const Value.absent(),
int? categoryId,
Value<String?> generatedText = const Value.absent()}) =>
TodoItem(
id: id ?? this.id,
title: title ?? this.title,
content: content.present ? content.value : this.content,
categoryId: categoryId ?? this.categoryId,
generatedText:
generatedText.present ? generatedText.value : this.generatedText,
);
@override
String toString() {
return (StringBuffer('TodoItem(')
..write('id: $id, ')
..write('title: $title, ')
..write('content: $content')
..write('content: $content, ')
..write('categoryId: $categoryId, ')
..write('generatedText: $generatedText')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, title, content);
int get hashCode =>
Object.hash(id, title, content, categoryId, generatedText);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TodoItem &&
other.id == this.id &&
other.title == this.title &&
other.content == this.content);
other.content == this.content &&
other.categoryId == this.categoryId &&
other.generatedText == this.generatedText);
}
class TodoItemsCompanion extends UpdateCompanion<TodoItem> {
final Value<int> id;
final Value<String> title;
final Value<String?> content;
final Value<int> categoryId;
const TodoItemsCompanion({
this.id = const Value.absent(),
this.title = const Value.absent(),
this.content = const Value.absent(),
this.categoryId = const Value.absent(),
});
TodoItemsCompanion.insert({
this.id = const Value.absent(),
required String title,
this.content = const Value.absent(),
}) : title = Value(title);
required int categoryId,
}) : title = Value(title),
categoryId = Value(categoryId);
static Insertable<TodoItem> custom({
Expression<int>? id,
Expression<String>? title,
Expression<String?>? content,
Expression<int>? categoryId,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (title != null) 'title': title,
if (content != null) 'content': content,
if (categoryId != null) 'category_id': categoryId,
});
}
TodoItemsCompanion copyWith(
{Value<int>? id, Value<String>? title, Value<String?>? content}) {
{Value<int>? id,
Value<String>? title,
Value<String?>? content,
Value<int>? categoryId}) {
return TodoItemsCompanion(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
categoryId: categoryId ?? this.categoryId,
);
}
@ -145,6 +357,9 @@ class TodoItemsCompanion extends UpdateCompanion<TodoItem> {
if (content.present) {
map['content'] = Variable<String?>(content.value);
}
if (categoryId.present) {
map['category_id'] = Variable<int>(categoryId.value);
}
return map;
}
@ -153,7 +368,8 @@ class TodoItemsCompanion extends UpdateCompanion<TodoItem> {
return (StringBuffer('TodoItemsCompanion(')
..write('id: $id, ')
..write('title: $title, ')
..write('content: $content')
..write('content: $content, ')
..write('categoryId: $categoryId')
..write(')'))
.toString();
}
@ -165,21 +381,41 @@ class $TodoItemsTable extends TodoItems
final String? _alias;
$TodoItemsTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _titleMeta = const VerificationMeta('title');
@override
late final GeneratedColumn<String?> title = GeneratedColumn<String?>(
'title', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _contentMeta = const VerificationMeta('content');
@override
late final GeneratedColumn<String?> content = GeneratedColumn<String?>(
'content', aliasedName, true,
type: const StringType(), requiredDuringInsert: false);
final VerificationMeta _categoryIdMeta = const VerificationMeta('categoryId');
@override
List<GeneratedColumn> get $columns => [id, title, content];
late final GeneratedColumn<int?> categoryId = GeneratedColumn<int?>(
'category_id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: true,
defaultConstraints: 'REFERENCES todo_categories (id)');
final VerificationMeta _generatedTextMeta =
const VerificationMeta('generatedText');
@override
late final GeneratedColumn<String?> generatedText = GeneratedColumn<String?>(
'generated_text', aliasedName, true,
type: const StringType(),
requiredDuringInsert: false,
generatedAs: GeneratedAs(
title + const Constant(' (') + content + const Constant(')'), false));
@override
List<GeneratedColumn> get $columns =>
[id, title, content, categoryId, generatedText];
@override
String get aliasedName => _alias ?? 'todo_items';
@override
@ -202,6 +438,20 @@ class $TodoItemsTable extends TodoItems
context.handle(_contentMeta,
content.isAcceptableOrUnknown(data['content']!, _contentMeta));
}
if (data.containsKey('category_id')) {
context.handle(
_categoryIdMeta,
categoryId.isAcceptableOrUnknown(
data['category_id']!, _categoryIdMeta));
} else if (isInserting) {
context.missing(_categoryIdMeta);
}
if (data.containsKey('generated_text')) {
context.handle(
_generatedTextMeta,
generatedText.isAcceptableOrUnknown(
data['generated_text']!, _generatedTextMeta));
}
return context;
}
@ -219,12 +469,236 @@ class $TodoItemsTable extends TodoItems
}
}
class TodoCategoryItemCountData extends DataClass {
final String name;
final int itemCount;
TodoCategoryItemCountData({required this.name, required this.itemCount});
factory TodoCategoryItemCountData.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return TodoCategoryItemCountData(
name: const StringType().mapFromDatabaseResponse(
data['${effectivePrefix}todo_categories.name'])!,
itemCount: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}item_count'])!,
);
}
factory TodoCategoryItemCountData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TodoCategoryItemCountData(
name: serializer.fromJson<String>(json['name']),
itemCount: serializer.fromJson<int>(json['itemCount']),
);
}
factory TodoCategoryItemCountData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
TodoCategoryItemCountData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'name': serializer.toJson<String>(name),
'itemCount': serializer.toJson<int>(itemCount),
};
}
TodoCategoryItemCountData copyWith({String? name, int? itemCount}) =>
TodoCategoryItemCountData(
name: name ?? this.name,
itemCount: itemCount ?? this.itemCount,
);
@override
String toString() {
return (StringBuffer('TodoCategoryItemCountData(')
..write('name: $name, ')
..write('itemCount: $itemCount')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(name, itemCount);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TodoCategoryItemCountData &&
other.name == this.name &&
other.itemCount == this.itemCount);
}
class $TodoCategoryItemCountView
extends ViewInfo<$TodoCategoryItemCountView, TodoCategoryItemCountData>
implements HasResultSet {
final _$Database _db;
final String? _alias;
$TodoCategoryItemCountView(this._db, [this._alias]);
$TodoItemsTable get todoItems => _db.todoItems;
$TodoCategoriesTable get todoCategories => _db.todoCategories;
@override
List<GeneratedColumn> get $columns => [todoCategories.name, itemCount];
@override
String get aliasedName => _alias ?? entityName;
@override
String get entityName => 'todo_category_item_count';
@override
String? get createViewStmt => null;
@override
$TodoCategoryItemCountView get asDslTable => this;
@override
TodoCategoryItemCountData map(Map<String, dynamic> data,
{String? tablePrefix}) {
return TodoCategoryItemCountData.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
late final GeneratedColumn<String?> name = GeneratedColumn<String?>(
'name', aliasedName, false,
type: const StringType());
late final GeneratedColumn<int?> itemCount = GeneratedColumn<int?>(
'item_count', aliasedName, false,
type: const IntType(),
generatedAs: GeneratedAs(todoItems.id.count(), false));
@override
$TodoCategoryItemCountView createAlias(String alias) {
return $TodoCategoryItemCountView(_db, alias);
}
@override
Query? get query =>
(_db.selectOnly(todoCategories, includeJoinedTableColumns: false)
..addColumns($columns))
.join([
innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id))
]);
}
class TodoItemWithCategoryNameViewData extends DataClass {
final int id;
final String title;
TodoItemWithCategoryNameViewData({required this.id, required this.title});
factory TodoItemWithCategoryNameViewData.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return TodoItemWithCategoryNameViewData(
id: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}todo_items.id'])!,
title: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}title'])!,
);
}
factory TodoItemWithCategoryNameViewData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TodoItemWithCategoryNameViewData(
id: serializer.fromJson<int>(json['id']),
title: serializer.fromJson<String>(json['title']),
);
}
factory TodoItemWithCategoryNameViewData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
TodoItemWithCategoryNameViewData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'title': serializer.toJson<String>(title),
};
}
TodoItemWithCategoryNameViewData copyWith({int? id, String? title}) =>
TodoItemWithCategoryNameViewData(
id: id ?? this.id,
title: title ?? this.title,
);
@override
String toString() {
return (StringBuffer('TodoItemWithCategoryNameViewData(')
..write('id: $id, ')
..write('title: $title')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, title);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TodoItemWithCategoryNameViewData &&
other.id == this.id &&
other.title == this.title);
}
class $TodoItemWithCategoryNameViewView extends ViewInfo<
$TodoItemWithCategoryNameViewView,
TodoItemWithCategoryNameViewData> implements HasResultSet {
final _$Database _db;
final String? _alias;
$TodoItemWithCategoryNameViewView(this._db, [this._alias]);
$TodoItemsTable get todoItems => _db.todoItems;
$TodoCategoriesTable get todoCategories => _db.todoCategories;
@override
List<GeneratedColumn> get $columns => [todoItems.id, title];
@override
String get aliasedName => _alias ?? entityName;
@override
String get entityName => 'customViewName';
@override
String? get createViewStmt => null;
@override
$TodoItemWithCategoryNameViewView get asDslTable => this;
@override
TodoItemWithCategoryNameViewData map(Map<String, dynamic> data,
{String? tablePrefix}) {
return TodoItemWithCategoryNameViewData.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(), defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
late final GeneratedColumn<String?> title = GeneratedColumn<String?>(
'title', aliasedName, false,
type: const StringType(),
generatedAs: GeneratedAs(
todoItems.title +
const Constant('(') +
todoCategories.name +
const Constant(')'),
false));
@override
$TodoItemWithCategoryNameViewView createAlias(String alias) {
return $TodoItemWithCategoryNameViewView(_db, alias);
}
@override
Query? get query =>
(_db.selectOnly(todoItems, includeJoinedTableColumns: false)
..addColumns($columns))
.join([
innerJoin(
todoCategories, todoCategories.id.equalsExp(todoItems.categoryId))
]);
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
_$Database.connect(DatabaseConnection c) : super.connect(c);
late final $TodoCategoriesTable todoCategories = $TodoCategoriesTable(this);
late final $TodoItemsTable todoItems = $TodoItemsTable(this);
late final $TodoCategoryItemCountView todoCategoryItemCount =
$TodoCategoryItemCountView(this);
late final $TodoItemWithCategoryNameViewView customViewName =
$TodoItemWithCategoryNameViewView(this);
@override
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [todoItems];
List<DatabaseSchemaEntity> get allSchemaEntities =>
[todoCategories, todoItems, todoCategoryItemCount, customViewName];
}

View File

@ -19,6 +19,9 @@ class DriftDatabase {
/// The tables to include in the database
final List<Type> tables;
/// The views to include in the database
final List<Type> views;
/// Optionally, the list of daos to use. A dao can also make queries like a
/// regular database class, making is suitable to extract parts of your
/// database logic into smaller components.
@ -58,6 +61,7 @@ class DriftDatabase {
/// class should be generated using the specified [DriftDatabase.tables].
const DriftDatabase({
this.tables = const [],
this.views = const [],
this.daos = const [],
this.queries = const {},
this.include = const {},
@ -91,6 +95,9 @@ class DriftAccessor {
/// The tables accessed by this DAO.
final List<Type> tables;
/// The views to make accessible in this DAO.
final List<Type> views;
/// {@macro drift_compile_queries_param}
final Map<String, String> queries;
@ -101,6 +108,7 @@ class DriftAccessor {
/// the referenced documentation on how to use daos with drift.
const DriftAccessor({
this.tables = const [],
this.views = const [],
this.queries = const {},
this.include = const {},
});

View File

@ -7,6 +7,8 @@ abstract class HasResultSet {
}
/// Subclasses represent a table in a database generated by drift.
///
/// For more information on how to write tables, see [the documentation](https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/)
abstract class Table extends HasResultSet {
/// Defines a table to be used with drift.
const Table();
@ -118,6 +120,46 @@ abstract class Table extends HasResultSet {
ColumnBuilder<double> real() => _isGenerated();
}
/// Subclasses represent a view in a database generated by drift.
///
/// For more information on how to define views in Dart, see
/// [the documentation](https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#views)
abstract class View extends HasResultSet {
/// Defines a view to be used with drift.
const View();
/// The select method can be used in [as] to define the select query backing
/// this view.
///
/// The select statement should select all columns defined on this view.
@protected
View select(List<Expression> columns) => _isGenerated();
/// This method should be called on [select] to define the main table of this
/// view:
///
/// ```dart
/// abstract class CategoryTodoCount extends View {
/// TodosTable get todos;
/// Categories get categories;
///
/// Expression<int> get itemCount => todos.id.count();
///
/// @override
/// Query as() => select([categories.description, itemCount])
/// .from(categories)
/// .join([innerJoin(todos, todos.category.equalsExp(categories.id))]);
/// }
/// ```
@protected
SimpleSelectStatement from(Table table) => _isGenerated();
/// This method is overridden by Dart-defined views to declare the right
/// query to run.
@visibleForOverriding
Query as();
}
/// A class to be used as an annotation on [Table] classes to customize the
/// name for the data class that will be generated for the table class. The data
/// class is a dart object that will be used to represent a row in the table.
@ -169,3 +211,26 @@ class UseRowClass {
const UseRowClass(this.type,
{this.constructor = '', this.generateInsertable = false});
}
/// An annotation specifying view properties
@Target({TargetKind.classType})
class DriftView {
/// The sql view name to be used. By default, drift will use the snake_case
/// representation of your class name as the sql view name. For instance, a
/// [View] class named `UserView` will be called `user_view` by
/// default.
final String? name;
/// The name for the data class that will be generated for the view class.
/// The data class is a dart object that will be used to represent a result of
/// the view.
/// {@template drift_custom_data_class}
/// By default, drift will attempt to use the view name followed by "Data"
/// when naming data classes (e.g. a view named "UserView" will generate a
/// data class called "UserViewData").
/// {@macro drift_custom_data_class}
final String? dataClassName;
/// Customize view name and data class name
const DriftView({this.name, this.dataClassName});
}

View File

@ -223,17 +223,24 @@ 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:
/// - the documentation on [aggregate expressions](https://drift.simonbinder.eu/docs/getting-started/expressions/#aggregate)
/// - 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,
}) {
ResultSetImplementation<T, R> table,
{bool distinct = false,
bool includeJoinedTableColumns = true}) {
return JoinedSelectStatement<T, R>(
resolvedEngine, table, [], distinct, false);
resolvedEngine, table, [], distinct, false, includeJoinedTableColumns);
}
/// Starts a [DeleteStatement] that can be used to delete rows from a table.

View File

@ -27,30 +27,39 @@ class Join<T extends HasResultSet, D> extends Component {
final _JoinType type;
/// The [TableInfo] that will be added to the query
final ResultSetImplementation<T, D> table;
final Table table;
/// For joins that aren't [_JoinType.cross], contains an additional predicate
/// that must be matched for the join.
final Expression<bool?>? on;
/// Whether [table] should appear in the result set (defaults to true).
/// Default value can be changed by `includeJoinedTableColumns` in
/// `selectOnly` statements.
///
/// It can be useful to exclude some tables. Sometimes, tables are used in a
/// join only to run aggregate functions on them.
final bool includeInResult;
final bool? includeInResult;
/// Constructs a [Join] by providing the relevant fields. [on] is optional for
/// [_JoinType.cross].
Join._(this.type, this.table, this.on, {bool? includeInResult})
: includeInResult = includeInResult ?? true;
Join._(this.type, this.table, this.on, {this.includeInResult}) {
if (table is! ResultSetImplementation<T, D>) {
throw ArgumentError(
'Invalid table parameter. You must provide the table reference from '
'generated database object.',
'table');
}
}
@override
void writeInto(GenerationContext context) {
context.buffer.write(_joinKeywords[type]);
context.buffer.write(' JOIN ');
context.buffer.write(table.tableWithAlias);
context.watchedTables.add(table);
final resultSet = table as ResultSetImplementation<T, D>;
context.buffer.write(resultSet.tableWithAlias);
context.watchedTables.add(resultSet);
if (type != _JoinType.cross) {
context.buffer.write(' ON ');
@ -70,9 +79,7 @@ class Join<T extends HasResultSet, D> extends Component {
/// See also:
/// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins
/// - http://www.sqlitetutorial.net/sqlite-inner-join/
Join innerJoin<T extends HasResultSet, D>(
ResultSetImplementation<T, D> other, Expression<bool?> on,
{bool? useColumns}) {
Join innerJoin(Table other, Expression<bool?> on, {bool? useColumns}) {
return Join._(_JoinType.inner, other, on, includeInResult: useColumns);
}
@ -84,9 +91,7 @@ Join innerJoin<T extends HasResultSet, D>(
/// See also:
/// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins
/// - http://www.sqlitetutorial.net/sqlite-left-join/
Join leftOuterJoin<T extends HasResultSet, D>(
ResultSetImplementation<T, D> other, Expression<bool?> on,
{bool? useColumns}) {
Join leftOuterJoin(Table other, Expression<bool?> on, {bool? useColumns}) {
return Join._(_JoinType.leftOuter, other, on, includeInResult: useColumns);
}
@ -98,7 +103,6 @@ Join leftOuterJoin<T extends HasResultSet, D>(
/// See also:
/// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins
/// - http://www.sqlitetutorial.net/sqlite-cross-join/
Join crossJoin<T extends HasResultSet, D>(ResultSetImplementation<T, D> other,
{bool? useColumns}) {
Join crossJoin(Table other, {bool? useColumns}) {
return Join._(_JoinType.cross, other, null, includeInResult: useColumns);
}

View File

@ -12,6 +12,10 @@ class GenerationContext {
/// explicit indices starting at [explicitVariableIndex].
int? explicitVariableIndex;
/// When set to an entity name (view or table), generated column in that
/// entity definition will written into query as expression
String? generatingForView;
/// All tables that the generated query reads from.
final List<ResultSetImplementation> watchedTables = [];

View File

@ -73,7 +73,7 @@ class Migrator {
await createIndex(entity);
} else if (entity is OnCreateQuery) {
await _issueCustomQuery(entity.sql, const []);
} else if (entity is View) {
} else if (entity is ViewInfo) {
await createView(entity);
} else {
throw AssertionError('Unknown entity: $entity');
@ -81,6 +81,17 @@ class Migrator {
}
}
/// Drop and recreate all views. You should call it on every upgrade
Future<void> recreateAllViews() async {
for (final entity in _db.allSchemaEntities) {
if (entity is ViewInfo) {
await _issueCustomQuery(
'DROP VIEW IF EXISTS ${entity.entityName}', const []);
await createView(entity);
}
}
}
GenerationContext _createContext() {
return GenerationContext.fromDb(_db);
}
@ -305,8 +316,17 @@ class Migrator {
}
/// Executes a `CREATE VIEW` statement to create the [view].
Future<void> createView(View view) {
return _issueCustomQuery(view.createViewStmt, const []);
Future<void> createView(ViewInfo view) async {
final stmt = view.createViewStmt;
if (stmt != null) {
await _issueCustomQuery(stmt, const []);
} else if (view.query != null) {
final context = GenerationContext.fromDb(_db);
context.generatingForView = view.entityName;
context.buffer.write('CREATE VIEW ${view.entityName} AS ');
view.query!.writeInto(context);
await _issueCustomQuery(context.sql, const []);
}
}
/// Drops a table, trigger or index.

View File

@ -33,6 +33,7 @@ part 'expressions/variables.dart';
part 'schema/column_impl.dart';
part 'schema/entities.dart';
part 'schema/table_info.dart';
part 'schema/view_info.dart';
part 'statements/select/custom_select.dart';
part 'statements/select/select.dart';

View File

@ -153,12 +153,16 @@ class GeneratedColumn<T> extends Column<T> {
@override
void writeInto(GenerationContext context, {bool ignoreEscape = false}) {
if (context.hasMultipleTables) {
context.buffer
..write(tableName)
..write('.');
if (generatedAs != null && context.generatingForView == tableName) {
generatedAs!.generatedAs.writeInto(context);
} else {
if (context.hasMultipleTables) {
context.buffer
..write(tableName)
..write('.');
}
context.buffer.write(ignoreEscape ? $name : escapedName);
}
context.buffer.write(ignoreEscape ? $name : escapedName);
}
/// Checks whether the given value fits into this column. The default

View File

@ -47,28 +47,6 @@ class Index extends DatabaseSchemaEntity {
Index(this.entityName, this.createIndexStmt);
}
/// A sqlite view.
///
/// In drift, views can only be declared in `.drift` files.
///
/// For more information on views, see the [CREATE VIEW][sqlite-docs]
/// documentation from sqlite, or the [entry on sqlitetutorial.net][sql-tut].
///
/// [sqlite-docs]: https://www.sqlite.org/lang_createview.html
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/
abstract class View<Self, Row> extends ResultSetImplementation<Self, Row>
implements HasResultSet {
@override
final String entityName;
/// The `CREATE VIEW` sql statement that can be used to create this view.
final String createViewStmt;
/// Creates an view model by the [createViewStmt] and its [entityName].
/// Mainly used by generated code.
View(this.entityName, this.createViewStmt);
}
/// An internal schema entity to run an sql statement when the database is
/// created.
///

View File

@ -0,0 +1,22 @@
part of '../query_builder.dart';
/// A sqlite view.
///
/// In drift, views can only be declared in `.drift` files.
///
/// For more information on views, see the [CREATE VIEW][sqlite-docs]
/// documentation from sqlite, or the [entry on sqlitetutorial.net][sql-tut].
///
/// [sqlite-docs]: https://www.sqlite.org/lang_createview.html
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/
abstract class ViewInfo<Self extends HasResultSet, Row>
implements ResultSetImplementation<Self, Row> {
@override
String get entityName;
/// The `CREATE VIEW` sql statement that can be used to create this view.
String? get createViewStmt;
/// Predefined query from `View.as()`
Query? get query;
}

View File

@ -12,13 +12,16 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
/// instead.
JoinedSelectStatement(DatabaseConnectionUser database,
ResultSetImplementation<FirstT, FirstD> table, this._joins,
[this.distinct = false, this._includeMainTableInResult = true])
[this.distinct = false,
this._includeMainTableInResult = true,
this._includeJoinedTablesInResult = true])
: super(database, table);
/// Whether to generate a `SELECT DISTINCT` query that will remove duplicate
/// rows from the result set.
final bool distinct;
final bool _includeMainTableInResult;
final bool _includeJoinedTablesInResult;
final List<Join> _joins;
/// All columns that we're selecting from.
@ -43,8 +46,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
@override
int get _returnedColumnCount {
return _joins.fold(_selectedColumns.length, (prev, join) {
if (join.includeInResult) {
return prev + join.table.$columns.length;
if (join.includeInResult ?? _includeJoinedTablesInResult) {
return prev + (join.table as ResultSetImplementation).$columns.length;
}
return prev;
});
@ -61,9 +64,10 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
}
for (final join in _joins) {
if (onlyResults && !join.includeInResult) continue;
if (onlyResults &&
!(join.includeInResult ?? _includeJoinedTablesInResult)) continue;
yield join.table;
yield join.table as ResultSetImplementation;
}
}
@ -86,7 +90,11 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
final column = _selectedColumns[i];
String chosenAlias;
if (column is GeneratedColumn) {
chosenAlias = '${column.tableName}.${column.$name}';
if (ctx.generatingForView == column.tableName) {
chosenAlias = '${column.$name}';
} else {
chosenAlias = '${column.tableName}.${column.$name}';
}
} else {
chosenAlias = 'c$i';
}

View File

@ -1536,14 +1536,21 @@ class MyViewData extends DataClass {
other.syncStateImplicit == this.syncStateImplicit);
}
class MyView extends View<MyView, MyViewData> {
MyView()
: super('my_view',
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2');
class MyView extends ViewInfo<MyView, MyViewData> implements HasResultSet {
final _$CustomTablesDb _db;
final String? _alias;
MyView(this._db, [this._alias]);
@override
List<GeneratedColumn> get $columns =>
[configKey, configValue, syncState, syncStateImplicit];
@override
String get aliasedName => _alias ?? entityName;
@override
String get entityName => 'my_view';
@override
String get createViewStmt =>
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2';
@override
MyView get asDslTable => this;
@override
MyViewData map(Map<String, dynamic> data, {String? tablePrefix}) {
@ -1566,6 +1573,13 @@ class MyView extends View<MyView, MyViewData> {
'sync_state_implicit', aliasedName, true,
type: const IntType())
.withConverter<SyncType?>(ConfigTable.$converter1);
@override
MyView createAlias(String alias) {
return MyView(_db, alias);
}
@override
Query? get query => null;
}
abstract class _$CustomTablesDb extends GeneratedDatabase {
@ -1578,7 +1592,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
late final Trigger myTrigger = Trigger(
'CREATE TRIGGER my_trigger AFTER INSERT ON config BEGIN INSERT INTO with_defaults VALUES (new.config_key, LENGTH(new.config_value));END',
'my_trigger');
late final MyView myView = MyView();
late final MyView myView = MyView(this);
late final NoIds noIds = NoIds(this);
late final WithConstraints withConstraints = WithConstraints(this);
late final Mytable mytable = Mytable(this);

View File

@ -117,6 +117,28 @@ class CustomConverter extends TypeConverter<MyCustomObject, String> {
}
}
abstract class CategoryTodoCountView extends View {
TodosTable get todos;
Categories get categories;
Expression<int> get itemCount => todos.id.count();
@override
Query as() => select([categories.description, itemCount])
.from(categories)
.join([innerJoin(todos, todos.category.equalsExp(categories.id))]);
}
abstract class TodoWithCategoryView extends View {
TodosTable get todos;
Categories get categories;
@override
Query as() => select([todos.title, categories.description])
.from(todos)
.join([innerJoin(categories, categories.id.equalsExp(todos.category))]);
}
@DriftDatabase(
tables: [
TodosTable,
@ -126,6 +148,10 @@ class CustomConverter extends TypeConverter<MyCustomObject, String> {
TableWithoutPK,
PureDefaults,
],
views: [
CategoryTodoCountView,
TodoWithCategoryView,
],
daos: [SomeDao],
queries: {
'allTodosWithCategory': 'SELECT t.*, c.id as catId, c."desc" as catDesc '

View File

@ -184,6 +184,7 @@ class $CategoriesTable extends Categories
final String? _alias;
$CategoriesTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
@ -191,12 +192,14 @@ class $CategoriesTable extends Categories
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _descriptionMeta =
const VerificationMeta('description');
@override
late final GeneratedColumn<String?> description = GeneratedColumn<String?>(
'desc', aliasedName, false,
type: const StringType(),
requiredDuringInsert: true,
$customConstraints: 'NOT NULL UNIQUE');
final VerificationMeta _priorityMeta = const VerificationMeta('priority');
@override
late final GeneratedColumnWithTypeConverter<CategoryPriority, int?> priority =
GeneratedColumn<int?>('priority', aliasedName, false,
type: const IntType(),
@ -205,6 +208,7 @@ class $CategoriesTable extends Categories
.withConverter<CategoryPriority>($CategoriesTable.$converter0);
final VerificationMeta _descriptionInUpperCaseMeta =
const VerificationMeta('descriptionInUpperCase');
@override
late final GeneratedColumn<String?> descriptionInUpperCase =
GeneratedColumn<String?>('description_in_upper_case', aliasedName, false,
type: const StringType(),
@ -474,12 +478,14 @@ class $TodosTableTable extends TodosTable
final String? _alias;
$TodosTableTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _titleMeta = const VerificationMeta('title');
@override
late final GeneratedColumn<String?> title = GeneratedColumn<String?>(
'title', aliasedName, true,
additionalChecks:
@ -487,14 +493,17 @@ class $TodosTableTable extends TodosTable
type: const StringType(),
requiredDuringInsert: false);
final VerificationMeta _contentMeta = const VerificationMeta('content');
@override
late final GeneratedColumn<String?> content = GeneratedColumn<String?>(
'content', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _targetDateMeta = const VerificationMeta('targetDate');
@override
late final GeneratedColumn<DateTime?> targetDate = GeneratedColumn<DateTime?>(
'target_date', aliasedName, true,
type: const IntType(), requiredDuringInsert: false);
final VerificationMeta _categoryMeta = const VerificationMeta('category');
@override
late final GeneratedColumn<int?> category = GeneratedColumn<int?>(
'category', aliasedName, true,
type: const IntType(),
@ -757,12 +766,14 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
final String? _alias;
$UsersTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String?> name = GeneratedColumn<String?>(
'name', aliasedName, false,
additionalChecks:
@ -770,6 +781,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
type: const StringType(),
requiredDuringInsert: true);
final VerificationMeta _isAwesomeMeta = const VerificationMeta('isAwesome');
@override
late final GeneratedColumn<bool?> isAwesome = GeneratedColumn<bool?>(
'is_awesome', aliasedName, false,
type: const BoolType(),
@ -778,11 +790,13 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
defaultValue: const Constant(true));
final VerificationMeta _profilePictureMeta =
const VerificationMeta('profilePicture');
@override
late final GeneratedColumn<Uint8List?> profilePicture =
GeneratedColumn<Uint8List?>('profile_picture', aliasedName, false,
type: const BlobType(), requiredDuringInsert: true);
final VerificationMeta _creationTimeMeta =
const VerificationMeta('creationTime');
@override
late final GeneratedColumn<DateTime?> creationTime =
GeneratedColumn<DateTime?>('creation_time', aliasedName, false,
type: const IntType(),
@ -974,10 +988,12 @@ class $SharedTodosTable extends SharedTodos
final String? _alias;
$SharedTodosTable(this._db, [this._alias]);
final VerificationMeta _todoMeta = const VerificationMeta('todo');
@override
late final GeneratedColumn<int?> todo = GeneratedColumn<int?>(
'todo', aliasedName, false,
type: const IntType(), requiredDuringInsert: true);
final VerificationMeta _userMeta = const VerificationMeta('user');
@override
late final GeneratedColumn<int?> user = GeneratedColumn<int?>(
'user', aliasedName, false,
type: const IntType(), requiredDuringInsert: true);
@ -1114,14 +1130,17 @@ class $TableWithoutPKTable extends TableWithoutPK
$TableWithoutPKTable(this._db, [this._alias]);
final VerificationMeta _notReallyAnIdMeta =
const VerificationMeta('notReallyAnId');
@override
late final GeneratedColumn<int?> notReallyAnId = GeneratedColumn<int?>(
'not_really_an_id', aliasedName, false,
type: const IntType(), requiredDuringInsert: true);
final VerificationMeta _someFloatMeta = const VerificationMeta('someFloat');
@override
late final GeneratedColumn<double?> someFloat = GeneratedColumn<double?>(
'some_float', aliasedName, false,
type: const RealType(), requiredDuringInsert: true);
final VerificationMeta _customMeta = const VerificationMeta('custom');
@override
late final GeneratedColumnWithTypeConverter<MyCustomObject, String?> custom =
GeneratedColumn<String?>('custom', aliasedName, false,
type: const StringType(),
@ -1291,6 +1310,7 @@ class $PureDefaultsTable extends PureDefaults
final String? _alias;
$PureDefaultsTable(this._db, [this._alias]);
final VerificationMeta _txtMeta = const VerificationMeta('txt');
@override
late final GeneratedColumn<String?> txt = GeneratedColumn<String?>(
'insert', aliasedName, true,
type: const StringType(), requiredDuringInsert: false);
@ -1326,6 +1346,215 @@ class $PureDefaultsTable extends PureDefaults
}
}
class CategoryTodoCountViewData extends DataClass {
final String description;
final int itemCount;
CategoryTodoCountViewData(
{required this.description, required this.itemCount});
factory CategoryTodoCountViewData.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return CategoryTodoCountViewData(
description: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}categories.desc'])!,
itemCount: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}item_count'])!,
);
}
factory CategoryTodoCountViewData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return CategoryTodoCountViewData(
description: serializer.fromJson<String>(json['description']),
itemCount: serializer.fromJson<int>(json['itemCount']),
);
}
factory CategoryTodoCountViewData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
CategoryTodoCountViewData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'description': serializer.toJson<String>(description),
'itemCount': serializer.toJson<int>(itemCount),
};
}
CategoryTodoCountViewData copyWith({String? description, int? itemCount}) =>
CategoryTodoCountViewData(
description: description ?? this.description,
itemCount: itemCount ?? this.itemCount,
);
@override
String toString() {
return (StringBuffer('CategoryTodoCountViewData(')
..write('description: $description, ')
..write('itemCount: $itemCount')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(description, itemCount);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is CategoryTodoCountViewData &&
other.description == this.description &&
other.itemCount == this.itemCount);
}
class $CategoryTodoCountViewView
extends ViewInfo<$CategoryTodoCountViewView, CategoryTodoCountViewData>
implements HasResultSet {
final _$TodoDb _db;
final String? _alias;
$CategoryTodoCountViewView(this._db, [this._alias]);
$TodosTableTable get todos => _db.todosTable;
$CategoriesTable get categories => _db.categories;
@override
List<GeneratedColumn> get $columns => [categories.description, itemCount];
@override
String get aliasedName => _alias ?? entityName;
@override
String get entityName => 'category_todo_count_view';
@override
String? get createViewStmt => null;
@override
$CategoryTodoCountViewView get asDslTable => this;
@override
CategoryTodoCountViewData map(Map<String, dynamic> data,
{String? tablePrefix}) {
return CategoryTodoCountViewData.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
late final GeneratedColumn<String?> description = GeneratedColumn<String?>(
'desc', aliasedName, false,
type: const StringType(), $customConstraints: 'NOT NULL UNIQUE');
late final GeneratedColumn<int?> itemCount = GeneratedColumn<int?>(
'item_count', aliasedName, false,
type: const IntType(), generatedAs: GeneratedAs(todos.id.count(), false));
@override
$CategoryTodoCountViewView createAlias(String alias) {
return $CategoryTodoCountViewView(_db, alias);
}
@override
Query? get query =>
(_db.selectOnly(categories, includeJoinedTableColumns: false)
..addColumns($columns))
.join([innerJoin(todos, todos.category.equalsExp(categories.id))]);
}
class TodoWithCategoryViewData extends DataClass {
final String? title;
final String description;
TodoWithCategoryViewData({this.title, required this.description});
factory TodoWithCategoryViewData.fromData(Map<String, dynamic> data,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return TodoWithCategoryViewData(
title: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}todos.title']),
description: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}categories.desc'])!,
);
}
factory TodoWithCategoryViewData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TodoWithCategoryViewData(
title: serializer.fromJson<String?>(json['title']),
description: serializer.fromJson<String>(json['description']),
);
}
factory TodoWithCategoryViewData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
TodoWithCategoryViewData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'title': serializer.toJson<String?>(title),
'description': serializer.toJson<String>(description),
};
}
TodoWithCategoryViewData copyWith(
{Value<String?> title = const Value.absent(), String? description}) =>
TodoWithCategoryViewData(
title: title.present ? title.value : this.title,
description: description ?? this.description,
);
@override
String toString() {
return (StringBuffer('TodoWithCategoryViewData(')
..write('title: $title, ')
..write('description: $description')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(title, description);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TodoWithCategoryViewData &&
other.title == this.title &&
other.description == this.description);
}
class $TodoWithCategoryViewView
extends ViewInfo<$TodoWithCategoryViewView, TodoWithCategoryViewData>
implements HasResultSet {
final _$TodoDb _db;
final String? _alias;
$TodoWithCategoryViewView(this._db, [this._alias]);
$TodosTableTable get todos => _db.todosTable;
$CategoriesTable get categories => _db.categories;
@override
List<GeneratedColumn> get $columns => [todos.title, categories.description];
@override
String get aliasedName => _alias ?? entityName;
@override
String get entityName => 'todo_with_category_view';
@override
String? get createViewStmt => null;
@override
$TodoWithCategoryViewView get asDslTable => this;
@override
TodoWithCategoryViewData map(Map<String, dynamic> data,
{String? tablePrefix}) {
return TodoWithCategoryViewData.fromData(data,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
late final GeneratedColumn<String?> title = GeneratedColumn<String?>(
'title', aliasedName, true,
additionalChecks:
GeneratedColumn.checkTextLength(minTextLength: 4, maxTextLength: 16),
type: const StringType());
late final GeneratedColumn<String?> description = GeneratedColumn<String?>(
'desc', aliasedName, false,
type: const StringType(), $customConstraints: 'NOT NULL UNIQUE');
@override
$TodoWithCategoryViewView createAlias(String alias) {
return $TodoWithCategoryViewView(_db, alias);
}
@override
Query? get query => (_db.selectOnly(todos, includeJoinedTableColumns: false)
..addColumns($columns))
.join([innerJoin(categories, categories.id.equalsExp(todos.category))]);
}
abstract class _$TodoDb extends GeneratedDatabase {
_$TodoDb(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
_$TodoDb.connect(DatabaseConnection c) : super.connect(c);
@ -1335,6 +1564,10 @@ abstract class _$TodoDb extends GeneratedDatabase {
late final $SharedTodosTable sharedTodos = $SharedTodosTable(this);
late final $TableWithoutPKTable tableWithoutPK = $TableWithoutPKTable(this);
late final $PureDefaultsTable pureDefaults = $PureDefaultsTable(this);
late final $CategoryTodoCountViewView categoryTodoCountView =
$CategoryTodoCountViewView(this);
late final $TodoWithCategoryViewView todoWithCategoryView =
$TodoWithCategoryViewView(this);
late final SomeDao someDao = SomeDao(this as TodoDb);
Selectable<AllTodosWithCategoryResult> allTodosWithCategory() {
return customSelect(
@ -1412,7 +1645,9 @@ abstract class _$TodoDb extends GeneratedDatabase {
users,
sharedTodos,
tableWithoutPK,
pureDefaults
pureDefaults,
categoryTodoCountView,
todoWithCategoryView
];
}

View File

@ -105,7 +105,7 @@ void main() {
for (var i = 0; i < 4; i++) {
filter.add(i);
await pumpEventQueue(times: 10);
await pumpEventQueue();
}
final values = await db
@ -113,6 +113,6 @@ void main() {
.map((row) => row.read<String>('r'))
.getSingle();
expect(values, anyOf('0,3', '3'));
expect(values, anyOf('0,3', '3', '1,3'));
});
}

View File

@ -401,6 +401,43 @@ void main() {
expect(result.read(todos.id.count()), equals(10));
});
test('use selectOnly(includeJoinedTableColumns) instead of useColumns',
() async {
final categories = db.categories;
final todos = db.todosTable;
final query =
db.selectOnly(categories, includeJoinedTableColumns: false).join([
innerJoin(
todos,
todos.category.equalsExp(categories.id),
)
]);
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'));

View File

@ -64,6 +64,24 @@ void main() {
'custom TEXT NOT NULL'
');',
[]));
verify(mockExecutor.runCustom(
'CREATE VIEW category_todo_count_view AS SELECT '
'categories."desc" AS "categories.desc", '
'COUNT(todos.id) AS "item_count" '
'FROM categories '
'INNER JOIN todos '
'ON todos.category = categories.id',
[]));
verify(mockExecutor.runCustom(
'CREATE VIEW todo_with_category_view AS SELECT '
'todos.title AS "todos.title", '
'categories."desc" AS "categories.desc" '
'FROM todos '
'INNER JOIN categories '
'ON categories.id = todos.category',
[]));
});
test('creates individual tables', () async {

View File

@ -1,6 +1,7 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'package:drift/sqlite_keywords.dart';
@ -9,6 +10,7 @@ import 'package:drift_dev/src/analyzer/errors.dart';
import 'package:drift_dev/src/analyzer/runner/steps.dart';
import 'package:drift_dev/src/model/declarations/declaration.dart';
import 'package:drift_dev/src/model/used_type_converter.dart';
import 'package:drift_dev/src/utils/exception.dart';
import 'package:drift_dev/src/utils/names.dart';
import 'package:drift_dev/src/utils/type_utils.dart';
import 'package:meta/meta.dart';
@ -20,6 +22,7 @@ import '../custom_row_class.dart';
part 'column_parser.dart';
part 'table_parser.dart';
part 'view_parser.dart';
part 'use_dao_parser.dart';
part 'use_moor_parser.dart';
@ -28,16 +31,23 @@ class MoorDartParser {
late ColumnParser _columnParser;
late TableParser _tableParser;
late ViewParser _viewParser;
MoorDartParser(this.step) {
_columnParser = ColumnParser(this);
_tableParser = TableParser(this);
_viewParser = ViewParser(this);
}
Future<MoorTable?> parseTable(ClassElement classElement) {
return _tableParser.parseTable(classElement);
}
Future<MoorView?> parseView(
ClassElement classElement, List<MoorTable> tables) {
return _viewParser.parseView(classElement, tables);
}
/// Attempts to parse the column created from the Dart getter.
///
/// When the column is invalid, an error will be logged and `null` is

View File

@ -40,6 +40,13 @@ class UseDaoParser {
const [];
final queryStrings = annotation.peek('queries')?.mapValue ?? {};
final viewTypes = annotation
.peek('views')
?.listValue
.map((obj) => obj.toTypeValue())
.whereType<DartType>() ??
const [];
final includes = annotation
.read('include')
.objectValue
@ -50,12 +57,14 @@ class UseDaoParser {
[];
final parsedTables = await step.parseTables(tableTypes, element);
final parsedViews = await step.parseViews(viewTypes, element, parsedTables);
final parsedQueries = step.readDeclaredQueries(queryStrings.cast());
return Dao(
declaration: DatabaseOrDaoDeclaration(element, step.file),
dbClass: dbImpl,
declaredTables: parsedTables,
declaredViews: parsedViews,
declaredIncludes: includes,
declaredQueries: parsedQueries,
);

View File

@ -23,6 +23,13 @@ class UseMoorParser {
));
}
final viewTypes = annotation
.peek('views')
?.listValue
.map((obj) => obj.toTypeValue())
.whereType<DartType>() ??
const [];
final tableTypes = tablesOrNull ?? [];
final queryStrings = annotation.peek('queries')?.mapValue ?? {};
final includes = annotation
@ -34,13 +41,14 @@ class UseMoorParser {
[];
final parsedTables = await step.parseTables(tableTypes, element);
final parsedViews = await step.parseViews(viewTypes, element, parsedTables);
final parsedQueries = step.readDeclaredQueries(queryStrings.cast());
final daoTypes = _readDaoTypes(annotation);
return Database(
declaration: DatabaseOrDaoDeclaration(element, step.file),
declaredTables: parsedTables,
declaredViews: parsedViews,
daos: daoTypes,
declaredIncludes: includes,
declaredQueries: parsedQueries,

View File

@ -0,0 +1,285 @@
part of 'parser.dart';
/// Parses a [MoorView] from a Dart class.
class ViewParser {
final MoorDartParser base;
ViewParser(this.base);
Future<MoorView?> parseView(
ClassElement element, List<MoorTable> tables) async {
final name = await _parseViewName(element);
final columns = (await _parseColumns(element)).toList();
final staticReferences =
(await _parseStaticReferences(element, tables)).toList();
final dataClassInfo = _readDataClassInformation(columns, element);
final query = await _parseQuery(element, staticReferences, columns);
final view = MoorView(
declaration: DartViewDeclaration(element, base.step.file),
name: name,
dartTypeName: dataClassInfo.enforcedName,
existingRowClass: dataClassInfo.existingClass,
entityInfoName: '\$${element.name}View',
staticReferences: staticReferences.map((ref) => ref.declaration).toList(),
viewQuery: query,
);
view.columns = columns;
return view;
}
_DataClassInformation _readDataClassInformation(
List<MoorColumn> columns, ClassElement element) {
DartObject? useRowClass;
String? dataClassName;
for (final annotation in element.metadata) {
final computed = annotation.computeConstantValue();
final annotationClass = computed!.type!.element!.name;
if (annotationClass == 'DriftView') {
dataClassName = computed.getField('dataClassName')?.toStringValue();
} else if (annotationClass == 'UseRowClass') {
useRowClass = computed;
}
}
if (dataClassName != null && useRowClass != null) {
base.step.reportError(ErrorInDartCode(
message: "A table can't be annotated with both @DataClassName and "
'@UseRowClass',
affectedElement: element,
));
}
FoundDartClass? existingClass;
String? constructorInExistingClass;
bool? generateInsertable;
var name = dataClassName ?? dataClassNameForClassName(element.name);
if (useRowClass != null) {
final type = useRowClass.getField('type')!.toTypeValue();
constructorInExistingClass =
useRowClass.getField('constructor')!.toStringValue()!;
generateInsertable =
useRowClass.getField('generateInsertable')!.toBoolValue()!;
if (type is InterfaceType) {
existingClass = FoundDartClass(type.element, type.typeArguments);
name = type.element.name;
} else {
base.step.reportError(ErrorInDartCode(
message: 'The @UseRowClass annotation must be used with a class',
affectedElement: element,
));
}
}
final verified = existingClass == null
? null
: validateExistingClass(columns, existingClass,
constructorInExistingClass!, generateInsertable!, base.step);
return _DataClassInformation(name, verified);
}
Future<String> _parseViewName(ClassElement element) async {
for (final annotation in element.metadata) {
final computed = annotation.computeConstantValue();
final annotationClass = computed!.type!.element!.name;
if (annotationClass == 'DriftView') {
final name = computed.getField('name')?.toStringValue();
if (name != null) {
return name;
}
break;
}
}
return element.name.snakeCase;
}
Future<Iterable<MoorColumn>> _parseColumns(ClassElement element) async {
final columnNames = element.allSupertypes
.map((t) => t.element)
.followedBy([element])
.expand((e) => e.fields)
.where((field) =>
isExpression(field.type) &&
field.getter != null &&
!field.getter!.isSynthetic)
.map((field) => field.name)
.toSet();
final fields = columnNames.map((name) {
final getter = element.getGetter(name) ??
element.lookUpInheritedConcreteGetter(name, element.library);
return getter!.variable;
}).toList();
final results = await Future.wait(fields.map((field) async {
final dartType = (field.type as InterfaceType).typeArguments[0];
final typeName = dartType.element!.name!;
final sqlType = _dartTypeToColumnType(typeName);
if (sqlType == null) {
final String errorMessage;
if (typeName == 'dynamic') {
errorMessage = 'You must specify Expression<?> type argument';
} else {
errorMessage =
'Invalid Expression<?> type argument `$typeName` found. '
'Must be one of: '
'bool, String, int, DateTime, Uint8List, double';
}
throw analysisError(base.step, field, errorMessage);
}
final node =
await base.loadElementDeclaration(field.getter!) as MethodDeclaration;
final expression = (node.body as ExpressionFunctionBody).expression;
return MoorColumn(
type: sqlType,
dartGetterName: field.name,
name: ColumnName.implicitly(ReCase(field.name).snakeCase),
nullable: dartType.nullabilitySuffix == NullabilitySuffix.question,
generatedAs: ColumnGeneratedAs(expression.toString(), false));
}).toList());
return results.whereType();
}
ColumnType? _dartTypeToColumnType(String name) {
return const {
'bool': ColumnType.boolean,
'String': ColumnType.text,
'int': ColumnType.integer,
'DateTime': ColumnType.datetime,
'Uint8List': ColumnType.blob,
'double': ColumnType.real,
}[name];
}
Future<List<_TableReference>> _parseStaticReferences(
ClassElement element, List<MoorTable> tables) async {
return await Stream.fromIterable(element.allSupertypes
.map((t) => t.element)
.followedBy([element]).expand((e) => e.fields))
.asyncMap((field) => _getStaticReference(field, tables))
.where((ref) => ref != null)
.cast<_TableReference>()
.toList();
}
Future<_TableReference?> _getStaticReference(
FieldElement field, List<MoorTable> tables) async {
if (field.getter != null) {
try {
final node = await base.loadElementDeclaration(field.getter!);
if (node is MethodDeclaration && node.body is EmptyFunctionBody) {
final type = tables.firstWhereOrNull(
(tbl) => tbl.fromClass!.name == node.returnType.toString());
if (type != null) {
final name = node.name.toString();
final declaration = '${type.entityInfoName} get $name => '
'_db.${type.dbGetterName};';
return _TableReference(type, name, declaration);
}
}
} catch (_) {}
}
return null;
}
Future<ViewQueryInformation> _parseQuery(ClassElement element,
List<_TableReference> references, List<MoorColumn> columns) async {
final as =
element.methods.where((method) => method.name == 'as').firstOrNull;
if (as != null) {
try {
final node = await base.loadElementDeclaration(as);
var target =
((node as MethodDeclaration).body as ExpressionFunctionBody)
.expression as MethodInvocation;
for (;;) {
if (target.target == null) break;
target = target.target as MethodInvocation;
}
if (target.methodName.toString() != 'select') {
throw analysisError(
base.step,
element,
'The `as()` query declaration must be started '
'with `select(columns).from(table)');
}
final columnListLiteral =
target.argumentList.arguments[0] as ListLiteral;
final columnList =
columnListLiteral.elements.map((col) => col.toString()).map((col) {
final parts = col.split('.');
if (parts.length > 1) {
final reference =
references.firstWhereOrNull((ref) => ref.name == parts[0]);
if (reference == null) {
throw analysisError(
base.step,
element,
'Table named `${parts[0]}` not found! Maybe not included in '
'@DriftDatabase or not belongs to this database');
}
final column = reference.table.columns
.firstWhere((col) => col.dartGetterName == parts[1]);
column.table = reference.table;
return MapEntry(
'${reference.name}.${column.dartGetterName}', column);
}
final column =
columns.firstWhere((col) => col.dartGetterName == parts[0]);
return MapEntry('${column.dartGetterName}', column);
});
final columnMap = Map.fromEntries(columnList);
target = target.parent as MethodInvocation;
if (target.methodName.toString() != 'from') {
throw analysisError(
base.step,
element,
'The `as()` query declaration must be started '
'with `select(columns).from(table)');
}
final from = target.argumentList.arguments[0].toString();
var query = '';
if (target.parent is MethodInvocation) {
target = target.parent as MethodInvocation;
query = target.toString().substring(target.target!.toString().length);
}
return ViewQueryInformation(columnMap, from, query);
} catch (e) {
print(e);
throw analysisError(
base.step, element, 'Failed to parse view `as()` query');
}
}
throw analysisError(base.step, element, 'Missing `as()` query declaration');
}
}
class _TableReference {
MoorTable table;
String name;
String declaration;
_TableReference(this.table, this.name, this.declaration);
}

View File

@ -18,6 +18,12 @@ class AnalyzeDartStep extends AnalyzingStep {
(entry.declaration as DartTableDeclaration).element: entry
};
final viewDartClasses = {
for (final entry in unsortedEntities)
if (entry.declaration is DartViewDeclaration)
(entry.declaration as DartViewDeclaration).element: entry
};
for (final declaredHere in accessor.declaredTables) {
// See issue #447: The table added to an accessor might already be
// included through a transitive moor file. In that case, we just ignore
@ -39,6 +45,23 @@ class AnalyzeDartStep extends AnalyzingStep {
_resolveDartColumnReferences(tableDartClasses);
}
for (final declaredHere in accessor.declaredViews) {
// See issue #447: The view added to an accessor might already be
// included through a transitive moor file. In that case, we just ignore
// it to avoid duplicates.
final declaration = declaredHere.declaration;
if (declaration is DartViewDeclaration &&
viewDartClasses.containsKey(declaration.element)) {
continue;
}
// Not a Dart view that we already included - add it now
unsortedEntities.add(declaredHere);
if (declaration is DartViewDeclaration) {
viewDartClasses[declaration.element] = declaredHere;
}
}
List<MoorSchemaEntity>? availableEntities;
try {

View File

@ -8,6 +8,7 @@ part of '../steps.dart';
/// Notably, this step does not analyze defined queries.
class ParseDartStep extends Step {
static const _tableTypeChecker = TypeChecker.fromRuntime(Table);
static const _viewTypeChecker = TypeChecker.fromRuntime(View);
static const _generatedInfoChecker = TypeChecker.fromRuntime(TableInfo);
static const _useMoorChecker = TypeChecker.fromRuntime(DriftDatabase);
static const _useDaoChecker = TypeChecker.fromRuntime(DriftAccessor);
@ -18,6 +19,7 @@ class ParseDartStep extends Step {
MoorDartParser get parser => _parser;
final Map<ClassElement, MoorTable> _tables = {};
final Map<ClassElement, MoorView> _views = {};
ParseDartStep(Task task, FoundFile file, this.library) : super(task, file) {
_parser = MoorDartParser(this);
@ -66,6 +68,18 @@ class ParseDartStep extends Step {
return _tables[element];
}
Future<MoorView?> _parseView(
ClassElement element, List<MoorTable> tables) async {
if (!_views.containsKey(element)) {
final view = await parser.parseView(element, tables);
if (view != null) {
_views[element] = view;
}
}
return _views[element];
}
void _lintDartTable(MoorTable table, ClassElement from) {
if (table.primaryKey != null) {
final hasAdditional = table.columns.any((c) {
@ -101,8 +115,8 @@ class ParseDartStep extends Step {
/// Resolves a [MoorTable] for the class of each [DartType] in [types].
/// The [initializedBy] element should be the piece of code that caused the
/// parsing (e.g. the database class that is annotated with `@UseMoor`). This
/// will allow for more descriptive error messages.
/// parsing (e.g. the database class that is annotated with `@DriftDatabase`).
/// This will allow for more descriptive error messages.
Future<List<MoorTable>> parseTables(
Iterable<DartType> types, Element initializedBy) {
return Future.wait(types.map((type) {
@ -122,6 +136,29 @@ class ParseDartStep extends Step {
});
}
/// Resolves a [MoorView] for the class of each [DartType] in [types].
/// The [initializedBy] element should be the piece of code that caused the
/// parsing (e.g. the database class that is annotated with `@DriftDatabase`).
/// This will allow for more descriptive error messages.
Future<List<MoorView>> parseViews(
Iterable<DartType> types, Element initializedBy, List<MoorTable> tables) {
return Future.wait(types.map((type) {
if (!_viewTypeChecker.isAssignableFrom(type.element!)) {
reportError(ErrorInDartCode(
severity: Severity.criticalError,
message: 'The type $type is not a drift view',
affectedElement: initializedBy,
));
return Future.value(null);
} else {
return _parseView(type.element as ClassElement, tables);
}
})).then((list) {
// only keep tables that were resolved successfully
return List.from(list.where((t) => t != null));
});
}
List<DeclaredQuery> readDeclaredQueries(Map<DartObject, DartObject> obj) {
return obj.entries.map((entry) {
final key = entry.key.toStringValue()!;

View File

@ -22,10 +22,13 @@ class ViewAnalyzer extends BaseAnalyzer {
Future<void> resolve(Iterable<MoorView> viewsToAnalyze) async {
// Going through the topologically sorted list and analyzing each view.
for (final view in viewsToAnalyze) {
final ctx = engine.analyzeNode(
view.declaration!.node, view.file!.parseResult.sql);
if (view.declaration is! MoorViewDeclaration) continue;
final viewDeclaration = view.declaration as MoorViewDeclaration;
final ctx =
engine.analyzeNode(viewDeclaration.node, view.file!.parseResult.sql);
lintContext(ctx, view.name);
final declaration = view.declaration!.creatingStatement;
final declaration = viewDeclaration.creatingStatement;
final parserView = view.parserView =
const SchemaFromCreateTable(moorExtensions: true)
@ -76,7 +79,7 @@ class ViewAnalyzer extends BaseAnalyzer {
}
engine.registerView(mapper.extractView(view));
view.references = findReferences(view.declaration!.node).toList();
view.references = findReferences(viewDeclaration.node).toList();
}
}
}

View File

@ -111,6 +111,9 @@ class MoorColumn implements HasDeclaration, HasType {
bool get isGenerated => generatedAs != null;
/// Parent table
MoorTable? table;
/// The column type from the dsl library. For instance, if a table has
/// declared an `IntColumn`, the matching dsl column name would also be an
/// `IntColumn`.

View File

@ -22,6 +22,13 @@ abstract class BaseMoorAccessor implements HasDeclaration {
/// that.
final List<MoorTable> declaredTables;
/// All views that have been declared on this accessor directly.
///
/// This contains the `views` field from a `DriftDatabase` or `UseDao`
/// annotation, but not tables that are declared in imported moor files.
/// Use [views] for that.
final List<MoorView> declaredViews;
/// The `includes` field from the `UseMoor` or `UseDao` annotation.
final List<String> declaredIncludes;
@ -48,7 +55,7 @@ abstract class BaseMoorAccessor implements HasDeclaration {
/// Resolved imports from this file.
List<FoundFile>? imports = [];
BaseMoorAccessor._(this.declaration, this.declaredTables,
BaseMoorAccessor._(this.declaration, this.declaredTables, this.declaredViews,
this.declaredIncludes, this.declaredQueries);
}
@ -60,9 +67,11 @@ class Database extends BaseMoorAccessor {
this.daos = const [],
DatabaseOrDaoDeclaration? declaration,
List<MoorTable> declaredTables = const [],
List<MoorView> declaredViews = const [],
List<String> declaredIncludes = const [],
List<DeclaredQuery> declaredQueries = const [],
}) : super._(declaration, declaredTables, declaredIncludes, declaredQueries);
}) : super._(declaration, declaredTables, declaredViews, declaredIncludes,
declaredQueries);
}
/// A dao, declared via an `UseDao` annotation on a Dart class.
@ -74,7 +83,9 @@ class Dao extends BaseMoorAccessor {
required this.dbClass,
DatabaseOrDaoDeclaration? declaration,
required List<MoorTable> declaredTables,
List<MoorView> declaredViews = const [],
required List<String> declaredIncludes,
required List<DeclaredQuery> declaredQueries,
}) : super._(declaration, declaredTables, declaredIncludes, declaredQueries);
}) : super._(declaration, declaredTables, declaredViews, declaredIncludes,
declaredQueries);
}

View File

@ -10,6 +10,23 @@ abstract class ViewDeclarationWithSql implements ViewDeclaration {
CreateViewStatement get creatingStatement;
}
class DartViewDeclaration implements ViewDeclaration, DartDeclaration {
@override
final SourceRange declaration;
@override
final ClassElement element;
DartViewDeclaration._(this.declaration, this.element);
factory DartViewDeclaration(ClassElement element, FoundFile file) {
return DartViewDeclaration._(
SourceRange.fromElementAndFile(element, file),
element,
);
}
}
class MoorViewDeclaration
implements ViewDeclaration, MoorDeclaration, ViewDeclarationWithSql {
@override

View File

@ -13,7 +13,7 @@ import 'model.dart';
/// A parsed view
class MoorView extends MoorEntityWithResultSet {
@override
final MoorViewDeclaration? declaration;
final ViewDeclaration? declaration;
/// The associated view to use for the sqlparser package when analyzing
/// sql queries. Note that this field is set lazily.
@ -38,12 +38,18 @@ class MoorView extends MoorEntityWithResultSet {
@override
ExistingRowClass? existingRowClass;
final List<String> staticReferences;
final ViewQueryInformation? viewQuery;
MoorView({
this.declaration,
required this.name,
required this.dartTypeName,
required this.entityInfoName,
this.existingRowClass,
this.staticReferences = const [],
this.viewQuery,
});
@override
@ -80,7 +86,7 @@ class MoorView extends MoorEntityWithResultSet {
/// The `CREATE VIEW` statement that can be used to create this view.
String createSql(MoorOptions options) {
final decl = declaration;
final decl = declaration as MoorViewDeclaration?;
if (decl == null) {
throw StateError('Cannot show SQL for views without a declaration');
}
@ -94,3 +100,11 @@ class MoorView extends MoorEntityWithResultSet {
@override
String get displayName => name;
}
class ViewQueryInformation {
final Map<String, MoorColumn> columns;
final String from;
final String query;
ViewQueryInformation(this.columns, this.from, this.query);
}

View File

@ -0,0 +1,14 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/exception/exception.dart';
import 'package:drift_dev/src/analyzer/errors.dart';
import 'package:drift_dev/src/analyzer/runner/steps.dart';
Exception analysisError(Step step, Element element, String message) {
final error = ErrorInDartCode(
message: message,
severity: Severity.criticalError,
affectedElement: element,
);
step.reportError(error);
return AnalysisException(error.toString());
}

View File

@ -18,6 +18,12 @@ bool isColumn(DartType type) {
!name.contains('Builder');
}
bool isExpression(DartType type) {
final name = type.element?.name ?? '';
return isFromMoor(type) && name.startsWith('Expression');
}
extension TypeUtils on DartType {
String get userVisibleName => getDisplayString(withNullability: true);

View File

@ -18,7 +18,7 @@ class DatabaseWriter {
DatabaseWriter(this.db, this.scope);
String get _dbClassName {
String get dbClassName {
if (scope.generationOptions.isGeneratingForSchema) {
return 'DatabaseAtV${scope.generationOptions.forSchema}';
}
@ -32,13 +32,13 @@ class DatabaseWriter {
TableWriter(table, scope.child()).writeInto();
}
for (final view in db.views) {
ViewWriter(view, scope.child()).write();
ViewWriter(view, scope.child(), this).write();
}
// Write the database class
final dbScope = scope.child();
final className = _dbClassName;
final className = dbClassName;
final firstLeaf = dbScope.leaf();
final isAbstract = !scope.generationOptions.isGeneratingForSchema;
if (isAbstract) {
@ -95,7 +95,7 @@ class DatabaseWriter {
buffer: dbScope.leaf(),
getterName: entity.dbGetterName,
returnType: entity.entityInfoName,
code: '${entity.entityInfoName}()',
code: '${entity.entityInfoName}(this)',
options: scope.generationOptions,
);
}

View File

@ -7,6 +7,7 @@ import 'package:drift_dev/writer.dart';
class DataClassWriter {
final MoorEntityWithResultSet table;
final Scope scope;
final columns = <MoorColumn>[];
bool get isInsertable => table is MoorTable;
@ -32,8 +33,16 @@ class DataClassWriter {
_buffer.writeln('{');
}
// write view columns
final view = table;
if (view is MoorView && view.viewQuery != null) {
columns.addAll(view.viewQuery!.columns.values);
} else {
columns.addAll(table.columns);
}
// write individual fields
for (final column in table.columns) {
for (final column in columns) {
if (column.documentationComment != null) {
_buffer.write('${column.documentationComment}\n');
}
@ -46,7 +55,7 @@ class DataClassWriter {
_buffer
..write(table.dartTypeName)
..write('({')
..write(table.columns.map((column) {
..write(columns.map((column) {
final nullableDartType = column.typeConverter != null &&
scope.options.nullAwareTypeConverters
? column.typeConverter!.hasNullableDartType
@ -80,8 +89,8 @@ class DataClassWriter {
_writeToString();
_writeHashCode();
overrideEquals(table.columns.map((c) => c.dartGetterName),
table.dartTypeName, _buffer);
overrideEquals(
columns.map((c) => c.dartGetterName), table.dartTypeName, _buffer);
// finish class declaration
_buffer.write('}');
@ -103,7 +112,7 @@ class DataClassWriter {
final writer = RowMappingWriter(
const [],
{for (final column in table.columns) column: column.dartGetterName},
{for (final column in columns) column: column.dartGetterName},
table,
scope.generationOptions,
scope.options,
@ -124,7 +133,7 @@ class DataClassWriter {
..write('serializer ??= $_runtimeOptions.defaultSerializer;\n')
..write('return $dataClassName(');
for (final column in table.columns) {
for (final column in columns) {
final getter = column.dartGetterName;
final jsonKey = column.getJsonKey(scope.options);
final type = column.dartTypeCode(scope.generationOptions);
@ -150,7 +159,7 @@ class DataClassWriter {
'serializer ??= $_runtimeOptions.defaultSerializer;\n'
'return <String, dynamic>{\n');
for (final column in table.columns) {
for (final column in columns) {
final name = column.getJsonKey(scope.options);
final getter = column.dartGetterName;
final needsThis = getter == 'serializer';
@ -168,9 +177,9 @@ class DataClassWriter {
final wrapNullableInValue = scope.options.generateValuesInCopyWith;
_buffer.write('$dataClassName copyWith({');
for (var i = 0; i < table.columns.length; i++) {
final column = table.columns[i];
final last = i == table.columns.length - 1;
for (var i = 0; i < columns.length; i++) {
final column = columns[i];
final last = i == columns.length - 1;
final isNullable = column.nullableInDart;
final typeName = column.dartTypeCode(scope.generationOptions);
@ -194,7 +203,7 @@ class DataClassWriter {
_buffer.write('}) => $dataClassName(');
for (final column in table.columns) {
for (final column in columns) {
// We also have a method parameter called like the getter, so we can use
// field: field ?? this.field. If we wrapped the parameter in a `Value`,
// we can use field.present ? field.value : this.field
@ -217,7 +226,7 @@ class DataClassWriter {
'(bool nullToAbsent) {\n')
..write('final map = <String, Expression> {};');
for (final column in table.columns) {
for (final column in columns) {
// Generated column - cannot be used for inserts or updates
if (column.isGenerated) continue;
@ -275,7 +284,7 @@ class DataClassWriter {
..write(asTable.getNameForCompanionClass(scope.options))
..write('(');
for (final column in table.columns) {
for (final column in columns) {
// Generated columns are not parts of companions.
if (column.isGenerated) continue;
@ -304,7 +313,7 @@ class DataClassWriter {
void _writeToString() {
overrideToString(
table.dartTypeName,
[for (final column in table.columns) column.dartGetterName],
[for (final column in columns) column.dartGetterName],
_buffer,
);
}
@ -312,7 +321,7 @@ class DataClassWriter {
void _writeHashCode() {
_buffer.write('@override\n int get hashCode => ');
final fields = table.columns.map((c) => c.dartGetterName).toList();
final fields = columns.map((c) => c.dartGetterName).toList();
const HashCodeWriter().writeHashCode(fields, _buffer);
_buffer.write(';');
}
@ -332,7 +341,11 @@ class RowMappingWriter {
void writeArguments(StringBuffer buffer) {
String readAndMap(MoorColumn column) {
final rawData = "data['\${effectivePrefix}${column.name.name}']";
var columnName = column.name.name;
if (column.table != null && column.table != table) {
columnName = '${column.table!.sqlName}.${column.name.name}';
}
final rawData = "data['\${effectivePrefix}$columnName']";
final sqlType = 'const ${sqlTypes[column.type]}()';
var loadType = '$sqlType.mapFromDatabaseResponse($rawData)';

View File

@ -1,6 +1,7 @@
import 'package:drift_dev/moor_generator.dart';
import 'package:drift_dev/src/utils/string_escaper.dart';
import '../database_writer.dart';
import '../writer.dart';
import 'data_class_writer.dart';
import 'table_writer.dart';
@ -8,6 +9,7 @@ import 'table_writer.dart';
class ViewWriter extends TableOrViewWriter {
final MoorView view;
final Scope scope;
final DatabaseWriter databaseWriter;
@override
late StringBuffer buffer;
@ -15,7 +17,7 @@ class ViewWriter extends TableOrViewWriter {
@override
MoorView get tableOrView => view;
ViewWriter(this.view, this.scope);
ViewWriter(this.view, this.scope, this.databaseWriter);
void write() {
if (scope.generationOptions.writeDataClasses &&
@ -29,29 +31,79 @@ class ViewWriter extends TableOrViewWriter {
void _writeViewInfoClass() {
buffer = scope.leaf();
buffer.write('class ${view.entityInfoName} extends View');
buffer.write('class ${view.entityInfoName} extends ViewInfo');
if (scope.generationOptions.writeDataClasses) {
buffer.write('<${view.entityInfoName}, '
'${view.dartTypeCode(scope.generationOptions)}>');
} else {
buffer.write('<${view.entityInfoName}, Never>');
}
buffer.write(' implements HasResultSet');
buffer
..write('{\n')
..write('${view.entityInfoName}(): super(')
..write(asDartLiteral(view.name))
..write(',')
..write(asDartLiteral(view.createSql(scope.options)))
..write(');');
// write the generated database reference that is set in the constructor
..write('final ${databaseWriter.dbClassName} _db;\n')
..write('final ${scope.nullableType('String')} _alias;\n')
..write('${view.entityInfoName}(this._db, [this._alias]);\n');
for (final ref in view.staticReferences) {
buffer.write('$ref\n');
}
if (view.viewQuery == null) {
writeGetColumnsOverride();
} else {
final columns = view.viewQuery!.columns.keys.join(', ');
buffer.write('@override\nList<GeneratedColumn> get \$columns => '
'[$columns];\n');
}
buffer
..write('@override\nString get aliasedName => '
'_alias ?? entityName;\n')
..write('@override\n String get entityName=>'
' ${asDartLiteral(view.name)};\n');
if (view.declaration is MoorViewDeclaration) {
buffer.write('@override\n String get createViewStmt =>'
' ${asDartLiteral(view.createSql(scope.options))};\n');
} else {
buffer.write('@override\n String? get createViewStmt => null;\n');
}
writeGetColumnsOverride();
writeAsDslTable();
writeMappingMethod(scope);
for (final column in view.columns) {
for (final column in view.viewQuery?.columns.values ?? view.columns) {
writeColumnGetter(column, scope.generationOptions, false);
}
_writeAliasGenerator();
_writeQuery();
buffer.writeln('}');
}
void _writeAliasGenerator() {
final typeName = view.entityInfoName;
buffer
..write('@override\n')
..write('$typeName createAlias(String alias) {\n')
..write('return $typeName(_db, alias);')
..write('}');
}
void _writeQuery() {
buffer.write('@override\nQuery? get query => ');
final query = view.viewQuery;
if (query != null) {
buffer.write('(_db.selectOnly(${query.from}, '
'includeJoinedTableColumns: false)..addColumns(\$columns))'
'${query.query};');
} else {
buffer.write('null;\n');
}
}
}

View File

@ -25,7 +25,7 @@ dependencies:
io: ^1.0.3
# Moor-specific analysis and apis
drift: ^1.1.0-dev
drift: '>=1.1.0-dev <1.2.0'
sqlite3: '>=0.1.6 <2.0.0'
sqlparser: ^0.18.0

View File

@ -130,10 +130,12 @@ class $KeyValuesTable extends KeyValues
final String? _alias;
$KeyValuesTable(this._db, [this._alias]);
final VerificationMeta _keyMeta = const VerificationMeta('key');
@override
late final GeneratedColumn<String?> key = GeneratedColumn<String?>(
'key', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _valueMeta = const VerificationMeta('value');
@override
late final GeneratedColumn<String?> value = GeneratedColumn<String?>(
'value', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);

View File

@ -237,26 +237,31 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
final String? _alias;
$UsersTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String?> name = GeneratedColumn<String?>(
'name', aliasedName, false,
type: const StringType(), requiredDuringInsert: true);
final VerificationMeta _birthDateMeta = const VerificationMeta('birthDate');
@override
late final GeneratedColumn<DateTime?> birthDate = GeneratedColumn<DateTime?>(
'birth_date', aliasedName, false,
type: const IntType(), requiredDuringInsert: true);
final VerificationMeta _profilePictureMeta =
const VerificationMeta('profilePicture');
@override
late final GeneratedColumn<Uint8List?> profilePicture =
GeneratedColumn<Uint8List?>('profile_picture', aliasedName, true,
type: const BlobType(), requiredDuringInsert: false);
final VerificationMeta _preferencesMeta =
const VerificationMeta('preferences');
@override
late final GeneratedColumnWithTypeConverter<Preferences, String?>
preferences = GeneratedColumn<String?>('preferences', aliasedName, true,
type: const StringType(), requiredDuringInsert: false)
@ -468,15 +473,18 @@ class $FriendshipsTable extends Friendships
final String? _alias;
$FriendshipsTable(this._db, [this._alias]);
final VerificationMeta _firstUserMeta = const VerificationMeta('firstUser');
@override
late final GeneratedColumn<int?> firstUser = GeneratedColumn<int?>(
'first_user', aliasedName, false,
type: const IntType(), requiredDuringInsert: true);
final VerificationMeta _secondUserMeta = const VerificationMeta('secondUser');
@override
late final GeneratedColumn<int?> secondUser = GeneratedColumn<int?>(
'second_user', aliasedName, false,
type: const IntType(), requiredDuringInsert: true);
final VerificationMeta _reallyGoodFriendsMeta =
const VerificationMeta('reallyGoodFriends');
@override
late final GeneratedColumn<bool?> reallyGoodFriends = GeneratedColumn<bool?>(
'really_good_friends', aliasedName, false,
type: const BoolType(),

View File

@ -128,12 +128,14 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
final String? _alias;
$UsersTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int?> id = GeneratedColumn<int?>(
'id', aliasedName, false,
type: const IntType(),
requiredDuringInsert: false,
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
final VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String?> name = GeneratedColumn<String?>(
'name', aliasedName, false,
type: const StringType(),