From 70bcefdaf32f85fae8578dc53771aa04f1b1094a Mon Sep 17 00:00:00 2001 From: westito Date: Fri, 12 Nov 2021 19:38:45 +0100 Subject: [PATCH 01/22] Rename View to ViewInfo --- drift/lib/src/runtime/query_builder/migration.dart | 4 ++-- drift/lib/src/runtime/query_builder/schema/entities.dart | 4 ++-- drift/test/data/tables/custom_tables.g.dart | 2 +- drift_dev/lib/src/writer/tables/view_writer.dart | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/migration.dart b/drift/lib/src/runtime/query_builder/migration.dart index 764c6846..071cadc0 100644 --- a/drift/lib/src/runtime/query_builder/migration.dart +++ b/drift/lib/src/runtime/query_builder/migration.dart @@ -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'); @@ -305,7 +305,7 @@ class Migrator { } /// Executes a `CREATE VIEW` statement to create the [view]. - Future createView(View view) { + Future createView(ViewInfo view) { return _issueCustomQuery(view.createViewStmt, const []); } diff --git a/drift/lib/src/runtime/query_builder/schema/entities.dart b/drift/lib/src/runtime/query_builder/schema/entities.dart index 0d8c04ad..9f7439ed 100644 --- a/drift/lib/src/runtime/query_builder/schema/entities.dart +++ b/drift/lib/src/runtime/query_builder/schema/entities.dart @@ -56,7 +56,7 @@ class Index extends DatabaseSchemaEntity { /// /// [sqlite-docs]: https://www.sqlite.org/lang_createview.html /// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/ -abstract class View extends ResultSetImplementation +abstract class ViewInfo extends ResultSetImplementation implements HasResultSet { @override final String entityName; @@ -66,7 +66,7 @@ abstract class View extends ResultSetImplementation /// Creates an view model by the [createViewStmt] and its [entityName]. /// Mainly used by generated code. - View(this.entityName, this.createViewStmt); + ViewInfo(this.entityName, this.createViewStmt); } /// An internal schema entity to run an sql statement when the database is diff --git a/drift/test/data/tables/custom_tables.g.dart b/drift/test/data/tables/custom_tables.g.dart index a8e502ea..82b3000e 100644 --- a/drift/test/data/tables/custom_tables.g.dart +++ b/drift/test/data/tables/custom_tables.g.dart @@ -1536,7 +1536,7 @@ class MyViewData extends DataClass { other.syncStateImplicit == this.syncStateImplicit); } -class MyView extends View { +class MyView extends ViewInfo { MyView() : super('my_view', 'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2'); diff --git a/drift_dev/lib/src/writer/tables/view_writer.dart b/drift_dev/lib/src/writer/tables/view_writer.dart index 4126bdb1..a534b9dc 100644 --- a/drift_dev/lib/src/writer/tables/view_writer.dart +++ b/drift_dev/lib/src/writer/tables/view_writer.dart @@ -29,7 +29,7 @@ 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)}>'); From 649549a21853cea83a3afe8fe38dd13e5241fbe5 Mon Sep 17 00:00:00 2001 From: westito Date: Fri, 12 Nov 2021 22:20:50 +0100 Subject: [PATCH 02/22] Add View DSL syntax --- drift/example/main.dart | 56 +++++++- drift/example/main.g.dart | 240 ++++++++++++++++++++++++++++++-- drift/lib/src/dsl/database.dart | 4 + drift/lib/src/dsl/table.dart | 140 ++++++++++++------- 4 files changed, 372 insertions(+), 68 deletions(-) diff --git a/drift/example/main.dart b/drift/example/main.dart index e5584937..76246d23 100644 --- a/drift/example/main.dart +++ b/drift/example/main.dart @@ -5,13 +5,54 @@ import 'package:drift/native.dart'; part 'main.g.dart'; +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)(); } -@DriftDatabase(tables: [TodoItems]) +class TodoCategoryItemCount extends View { + late $TodoItemsTable todoItems; + late TodoCategories todoCategories; + + IntColumn get itemCount => integer().generatedAs(todoItems.id.count())(); + + @override + Query as() => + select([todoCategories.name, itemCount]).from(todoCategories).join([ + innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id)) + ]); +} + +class TodoItemWithCategoryName extends View { + late TodoItems todoItems; + late $TodoCategoriesTable todoCategories; + + TextColumn get title => text().generatedAs(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, + TodoItemWithCategoryName, +]) class Database extends _$Database { Database(QueryExecutor e) : super(e); @@ -27,11 +68,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 +97,14 @@ Future 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)); // Delete all todo items await db.delete(db.todoItems).go(); diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index f3adf86f..27ab1975 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -7,11 +7,191 @@ part of 'main.dart'; // ************************************************************************** // ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this +class TodoCategorie extends DataClass implements Insertable { + final int id; + final String name; + TodoCategorie({required this.id, required this.name}); + factory TodoCategorie.fromData(Map data, {String? prefix}) { + final effectivePrefix = prefix ?? ''; + return TodoCategorie( + id: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, + name: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}name'])!, + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + return map; + } + + TodoCategoriesCompanion toCompanion(bool nullToAbsent) { + return TodoCategoriesCompanion( + id: Value(id), + name: Value(name), + ); + } + + factory TodoCategorie.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoCategorie( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + ); + } + factory TodoCategorie.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + TodoCategorie.fromJson( + DataClass.parseJson(encodedJson) as Map, + serializer: serializer); + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + }; + } + + TodoCategorie copyWith({int? id, String? name}) => TodoCategorie( + id: id ?? this.id, + name: name ?? this.name, + ); + @override + String toString() { + return (StringBuffer('TodoCategorie(') + ..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 TodoCategorie && + other.id == this.id && + other.name == this.name); +} + +class TodoCategoriesCompanion extends UpdateCompanion { + final Value id; + final Value 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 custom({ + Expression? id, + Expression? name, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + }); + } + + TodoCategoriesCompanion copyWith({Value? id, Value? name}) { + return TodoCategoriesCompanion( + id: id ?? this.id, + name: name ?? this.name, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(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, TodoCategorie> { + final GeneratedDatabase _db; + final String? _alias; + $TodoCategoriesTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: const IntType(), + requiredDuringInsert: false, + defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); + final VerificationMeta _nameMeta = const VerificationMeta('name'); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: const StringType(), requiredDuringInsert: true); + @override + List get $columns => [id, name]; + @override + String get aliasedName => _alias ?? 'todo_categories'; + @override + String get actualTableName => 'todo_categories'; + @override + VerificationContext validateIntegrity(Insertable 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 get $primaryKey => {id}; + @override + TodoCategorie map(Map data, {String? tablePrefix}) { + return TodoCategorie.fromData(data, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + @override + $TodoCategoriesTable createAlias(String alias) { + return $TodoCategoriesTable(_db, alias); + } +} + class TodoItem extends DataClass implements Insertable { final int id; final String title; final String? content; - TodoItem({required this.id, required this.title, this.content}); + final int categoryId; + TodoItem( + {required this.id, + required this.title, + this.content, + required this.categoryId}); factory TodoItem.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return TodoItem( @@ -21,6 +201,8 @@ class TodoItem extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}title'])!, content: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}content']), + categoryId: const IntType() + .mapFromDatabaseResponse(data['${effectivePrefix}category_id'])!, ); } @override @@ -31,6 +213,7 @@ class TodoItem extends DataClass implements Insertable { if (!nullToAbsent || content != null) { map['content'] = Variable(content); } + map['category_id'] = Variable(categoryId); return map; } @@ -41,6 +224,7 @@ class TodoItem extends DataClass implements Insertable { content: content == null && nullToAbsent ? const Value.absent() : Value(content), + categoryId: Value(categoryId), ); } @@ -51,6 +235,7 @@ class TodoItem extends DataClass implements Insertable { id: serializer.fromJson(json['id']), title: serializer.fromJson(json['title']), content: serializer.fromJson(json['content']), + categoryId: serializer.fromJson(json['categoryId']), ); } factory TodoItem.fromJsonString(String encodedJson, @@ -65,71 +250,86 @@ class TodoItem extends DataClass implements Insertable { 'id': serializer.toJson(id), 'title': serializer.toJson(title), 'content': serializer.toJson(content), + 'categoryId': serializer.toJson(categoryId), }; } TodoItem copyWith( {int? id, String? title, - Value content = const Value.absent()}) => + Value content = const Value.absent(), + int? categoryId}) => TodoItem( id: id ?? this.id, title: title ?? this.title, content: content.present ? content.value : this.content, + categoryId: categoryId ?? this.categoryId, ); @override String toString() { return (StringBuffer('TodoItem(') ..write('id: $id, ') ..write('title: $title, ') - ..write('content: $content') + ..write('content: $content, ') + ..write('categoryId: $categoryId') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, title, content); + int get hashCode => Object.hash(id, title, content, categoryId); @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); } class TodoItemsCompanion extends UpdateCompanion { final Value id; final Value title; final Value content; + final Value 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 custom({ Expression? id, Expression? title, Expression? content, + Expression? 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? id, Value? title, Value? content}) { + {Value? id, + Value? title, + Value? content, + Value? categoryId}) { return TodoItemsCompanion( id: id ?? this.id, title: title ?? this.title, content: content ?? this.content, + categoryId: categoryId ?? this.categoryId, ); } @@ -145,6 +345,9 @@ class TodoItemsCompanion extends UpdateCompanion { if (content.present) { map['content'] = Variable(content.value); } + if (categoryId.present) { + map['category_id'] = Variable(categoryId.value); + } return map; } @@ -153,7 +356,8 @@ class TodoItemsCompanion extends UpdateCompanion { return (StringBuffer('TodoItemsCompanion(') ..write('id: $id, ') ..write('title: $title, ') - ..write('content: $content') + ..write('content: $content, ') + ..write('categoryId: $categoryId') ..write(')')) .toString(); } @@ -178,8 +382,14 @@ class $TodoItemsTable extends TodoItems late final GeneratedColumn content = GeneratedColumn( 'content', aliasedName, true, type: const StringType(), requiredDuringInsert: false); + final VerificationMeta _categoryIdMeta = const VerificationMeta('categoryId'); + late final GeneratedColumn categoryId = GeneratedColumn( + 'category_id', aliasedName, false, + type: const IntType(), + requiredDuringInsert: true, + defaultConstraints: 'REFERENCES todo_categories (id)'); @override - List get $columns => [id, title, content]; + List get $columns => [id, title, content, categoryId]; @override String get aliasedName => _alias ?? 'todo_items'; @override @@ -202,6 +412,14 @@ 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); + } return context; } @@ -222,9 +440,11 @@ class $TodoItemsTable extends TodoItems 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); @override Iterable get allTables => allSchemaEntities.whereType(); @override - List get allSchemaEntities => [todoItems]; + List get allSchemaEntities => + [todoCategories, todoItems]; } diff --git a/drift/lib/src/dsl/database.dart b/drift/lib/src/dsl/database.dart index f80e5462..78b7c420 100644 --- a/drift/lib/src/dsl/database.dart +++ b/drift/lib/src/dsl/database.dart @@ -19,6 +19,9 @@ class DriftDatabase { /// The tables to include in the database final List tables; + /// The views to include in the database + final List 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 {}, diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index 95b3caad..b7e3432f 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -6,59 +6,10 @@ abstract class HasResultSet { const HasResultSet(); } -/// Subclasses represent a table in a database generated by drift. -abstract class Table extends HasResultSet { - /// Defines a table to be used with drift. - const Table(); - - /// The sql table name to be used. By default, drift will use the snake_case - /// representation of your class name as the sql table name. For instance, a - /// [Table] class named `LocalSettings` will be called `local_settings` by - /// default. - /// You can change that behavior by overriding this method to use a custom - /// name. Please note that you must directly return a string literal by using - /// a getter. For instance `@override String get tableName => 'my_table';` is - /// valid, whereas `@override final String tableName = 'my_table';` or - /// `@override String get tableName => createMyTableName();` is not. - @visibleForOverriding - String? get tableName => null; - - /// Whether to append a `WITHOUT ROWID` clause in the `CREATE TABLE` - /// statement. This is intended to be used by generated code only. - bool get withoutRowId => false; - - /// Drift will write some table constraints automatically, for instance when - /// you override [primaryKey]. You can turn this behavior off if you want to. - /// This is intended to be used by generated code only. - bool get dontWriteConstraints => false; - - /// Override this to specify custom primary keys: - /// ```dart - /// class IngredientInRecipes extends Table { - /// @override - /// Set get primaryKey => {recipe, ingredient}; - /// - /// IntColumn get recipe => integer()(); - /// IntColumn get ingredient => integer()(); - /// - /// IntColumn get amountInGrams => integer().named('amount')(); - ///} - /// ``` - /// The getter must return a set literal using the `=>` syntax so that the - /// drift generator can understand the code. - /// Also, please note that it's an error to have an - /// [BuildIntColumn.autoIncrement] column and a custom primary key. - /// As an auto-incremented `IntColumn` is recognized by drift to be the - /// primary key, doing so will result in an exception thrown at runtime. - @visibleForOverriding - Set? get primaryKey => null; - - /// Custom table constraints that should be added to the table. - /// - /// See also: - /// - https://www.sqlite.org/syntax/table-constraint.html, which defines what - /// table constraints are supported. - List get customConstraints => []; +/// Base class for dsl [Table]s and [View]s. +abstract class ColumnDefinition extends HasResultSet { + /// Default constant constructor. + const ColumnDefinition(); /// Use this as the body of a getter to declare a column that holds integers. /// Example (inside the body of a table class): @@ -118,6 +69,89 @@ abstract class Table extends HasResultSet { ColumnBuilder real() => _isGenerated(); } +/// Subclasses represent a table in a database generated by drift. +abstract class Table extends ColumnDefinition { + /// Defines a table to be used with drift. + const Table(); + + /// The sql table name to be used. By default, drift will use the snake_case + /// representation of your class name as the sql table name. For instance, a + /// [Table] class named `LocalSettings` will be called `local_settings` by + /// default. + /// You can change that behavior by overriding this method to use a custom + /// name. Please note that you must directly return a string literal by using + /// a getter. For instance `@override String get tableName => 'my_table';` is + /// valid, whereas `@override final String tableName = 'my_table';` or + /// `@override String get tableName => createMyTableName();` is not. + @visibleForOverriding + String? get tableName => null; + + /// Whether to append a `WITHOUT ROWID` clause in the `CREATE TABLE` + /// statement. This is intended to be used by generated code only. + bool get withoutRowId => false; + + /// Drift will write some table constraints automatically, for instance when + /// you override [primaryKey]. You can turn this behavior off if you want to. + /// This is intended to be used by generated code only. + bool get dontWriteConstraints => false; + + /// Override this to specify custom primary keys: + /// ```dart + /// class IngredientInRecipes extends Table { + /// @override + /// Set get primaryKey => {recipe, ingredient}; + /// + /// IntColumn get recipe => integer()(); + /// IntColumn get ingredient => integer()(); + /// + /// IntColumn get amountInGrams => integer().named('amount')(); + ///} + /// ``` + /// The getter must return a set literal using the `=>` syntax so that the + /// drift generator can understand the code. + /// Also, please note that it's an error to have an + /// [BuildIntColumn.autoIncrement] column and a custom primary key. + /// As an auto-incremented `IntColumn` is recognized by drift to be the + /// primary key, doing so will result in an exception thrown at runtime. + @visibleForOverriding + Set? get primaryKey => null; + + /// Custom table constraints that should be added to the table. + /// + /// See also: + /// - https://www.sqlite.org/syntax/table-constraint.html, which defines what + /// table constraints are supported. + List get customConstraints => []; +} + +/// Subclasses represent a view in a database generated by drift. +abstract class View extends ColumnDefinition { + /// Defines a view to be used with drift. + const View(); + + /// 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 `LocalSettings` will be called `local_settings` by + /// default. + /// You can change that behavior by overriding this method to use a custom + /// name. Please note that you must directly return a string literal by using + /// a getter. For instance `@override String get viewName => 'my_view';` is + /// valid, whereas `@override final String viewName = 'my_view';` or + /// `@override String get viewName => createMyViewName();` is not. + @visibleForOverriding + String? get viewName => null; + + /// + View select(List columns) => _isGenerated(); + + /// + SimpleSelectStatement from(Table table) => _isGenerated(); + + /// + @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. From ea6dc029645a4ac79fcb4a35192d4975b1be8763 Mon Sep 17 00:00:00 2001 From: westito Date: Sat, 13 Nov 2021 12:27:07 +0100 Subject: [PATCH 03/22] Refactor drift views --- drift/example/main.dart | 14 +++--- .../runtime/query_builder/query_builder.dart | 1 + .../query_builder/schema/entities.dart | 22 ---------- .../query_builder/schema/view_info.dart | 22 ++++++++++ drift/test/data/tables/custom_tables.g.dart | 24 +++++++--- .../integration_tests/cancellation_test.dart | 2 +- drift_dev/lib/src/writer/database_writer.dart | 8 ++-- .../lib/src/writer/tables/view_writer.dart | 44 ++++++++++++++++--- 8 files changed, 92 insertions(+), 45 deletions(-) create mode 100644 drift/lib/src/runtime/query_builder/schema/view_info.dart diff --git a/drift/example/main.dart b/drift/example/main.dart index 76246d23..ea3a641e 100644 --- a/drift/example/main.dart +++ b/drift/example/main.dart @@ -17,21 +17,23 @@ class TodoItems extends Table { IntColumn get categoryId => integer().references(TodoCategories, #id)(); } -class TodoCategoryItemCount extends View { - late $TodoItemsTable todoItems; - late TodoCategories todoCategories; +abstract class TodoCategoryItemCount extends View { + $TodoItemsTable get todoItems; + $TodoCategoriesTable get todoCategories; IntColumn get itemCount => integer().generatedAs(todoItems.id.count())(); @override - Query as() => - select([todoCategories.name, itemCount]).from(todoCategories).join([ + Query as() => select([ + todoCategories.name, + itemCount, + ]).from(todoCategories).join([ innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id)) ]); } class TodoItemWithCategoryName extends View { - late TodoItems todoItems; + late $TodoItemsTable todoItems; late $TodoCategoriesTable todoCategories; TextColumn get title => text().generatedAs(todoItems.title + diff --git a/drift/lib/src/runtime/query_builder/query_builder.dart b/drift/lib/src/runtime/query_builder/query_builder.dart index 5cee9e7c..1acaf0a6 100644 --- a/drift/lib/src/runtime/query_builder/query_builder.dart +++ b/drift/lib/src/runtime/query_builder/query_builder.dart @@ -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'; diff --git a/drift/lib/src/runtime/query_builder/schema/entities.dart b/drift/lib/src/runtime/query_builder/schema/entities.dart index 9f7439ed..a3c181c9 100644 --- a/drift/lib/src/runtime/query_builder/schema/entities.dart +++ b/drift/lib/src/runtime/query_builder/schema/entities.dart @@ -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 ViewInfo extends ResultSetImplementation - 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. - ViewInfo(this.entityName, this.createViewStmt); -} - /// An internal schema entity to run an sql statement when the database is /// created. /// diff --git a/drift/lib/src/runtime/query_builder/schema/view_info.dart b/drift/lib/src/runtime/query_builder/schema/view_info.dart new file mode 100644 index 00000000..845d9a52 --- /dev/null +++ b/drift/lib/src/runtime/query_builder/schema/view_info.dart @@ -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/ +mixin ViewInfo on View + implements ResultSetImplementation { + /// + String get actualViewName; + + @override + String get entityName => actualViewName; + + /// The `CREATE VIEW` sql statement that can be used to create this view. + String get createViewStmt; +} diff --git a/drift/test/data/tables/custom_tables.g.dart b/drift/test/data/tables/custom_tables.g.dart index 82b3000e..dcb90fd9 100644 --- a/drift/test/data/tables/custom_tables.g.dart +++ b/drift/test/data/tables/custom_tables.g.dart @@ -1536,14 +1536,21 @@ class MyViewData extends DataClass { other.syncStateImplicit == this.syncStateImplicit); } -class MyView extends ViewInfo { - MyView() - : super('my_view', - 'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2'); +class MyView extends View with ViewInfo { + final _$CustomTablesDb _db; + final String? _alias; + MyView(this._db, [this._alias]); @override List get $columns => [configKey, configValue, syncState, syncStateImplicit]; @override + String get aliasedName => _alias ?? actualViewName; + @override + String get actualViewName => '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 data, {String? tablePrefix}) { @@ -1566,6 +1573,13 @@ class MyView extends ViewInfo { 'sync_state_implicit', aliasedName, true, type: const IntType()) .withConverter(ConfigTable.$converter1); + @override + MyView createAlias(String alias) { + return MyView(_db, alias); + } + + @override + Query as() => _db.select(_db.myView); } 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); diff --git a/drift/test/integration_tests/cancellation_test.dart b/drift/test/integration_tests/cancellation_test.dart index 52f118ca..3abc2cfb 100644 --- a/drift/test/integration_tests/cancellation_test.dart +++ b/drift/test/integration_tests/cancellation_test.dart @@ -105,7 +105,7 @@ void main() { for (var i = 0; i < 4; i++) { filter.add(i); - await pumpEventQueue(times: 10); + await pumpEventQueue(times: 5); } final values = await db diff --git a/drift_dev/lib/src/writer/database_writer.dart b/drift_dev/lib/src/writer/database_writer.dart index bba6caf3..691a3b82 100644 --- a/drift_dev/lib/src/writer/database_writer.dart +++ b/drift_dev/lib/src/writer/database_writer.dart @@ -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, ); } diff --git a/drift_dev/lib/src/writer/tables/view_writer.dart b/drift_dev/lib/src/writer/tables/view_writer.dart index a534b9dc..bba9c1d3 100644 --- a/drift_dev/lib/src/writer/tables/view_writer.dart +++ b/drift_dev/lib/src/writer/tables/view_writer.dart @@ -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,22 +31,30 @@ class ViewWriter extends TableOrViewWriter { void _writeViewInfoClass() { buffer = scope.leaf(); - buffer.write('class ${view.entityInfoName} extends ViewInfo'); + buffer.write('class ${view.entityInfoName} extends View with ViewInfo'); if (scope.generationOptions.writeDataClasses) { buffer.write('<${view.entityInfoName}, ' '${view.dartTypeCode(scope.generationOptions)}>'); } else { buffer.write('<${view.entityInfoName}, Never>'); } + 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'); writeGetColumnsOverride(); + buffer + ..write('@override\nString get aliasedName => ' + '_alias ?? actualViewName;\n') + ..write('@override\n String get actualViewName =>' + ' ${asDartLiteral(view.name)};\n') + ..write('@override\n String get createViewStmt =>' + ' ${asDartLiteral(view.createSql(scope.options))};\n'); + writeAsDslTable(); writeMappingMethod(scope); @@ -52,6 +62,26 @@ class ViewWriter extends TableOrViewWriter { writeColumnGetter(column, scope.generationOptions, false); } + _writeAliasGenerator(); + _writeAs(); + 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 _writeAs() { + buffer + ..write('@override\n') + ..write('Query<${view.entityInfoName}, ${view.dartTypeName}> as() =>\n') + ..write('_db.select(_db.${view.dbGetterName});\n'); + } } From 876f6850f573e59744efafbb34e165dbf52c44af Mon Sep 17 00:00:00 2001 From: westito Date: Mon, 15 Nov 2021 00:06:48 +0100 Subject: [PATCH 04/22] Add dsl view generator --- drift/example/main.dart | 27 +- drift/example/main.g.dart | 254 +++++++++++++++++- drift/lib/src/dsl/database.dart | 4 + drift/lib/src/dsl/table.dart | 47 +++- .../query_builder/generation_context.dart | 4 + .../src/runtime/query_builder/migration.dart | 13 +- .../query_builder/schema/column_impl.dart | 14 +- .../query_builder/schema/view_info.dart | 7 +- .../statements/select/select_with_join.dart | 6 +- drift/test/data/tables/custom_tables.g.dart | 4 +- drift_dev/lib/src/analyzer/dart/parser.dart | 8 + .../lib/src/analyzer/dart/use_dao_parser.dart | 9 + .../src/analyzer/dart/use_moor_parser.dart | 10 +- .../lib/src/analyzer/dart/view_parser.dart | 237 ++++++++++++++++ .../analyzer/runner/steps/analyze_dart.dart | 23 ++ .../src/analyzer/runner/steps/parse_dart.dart | 41 ++- .../lib/src/analyzer/view/view_analyzer.dart | 11 +- drift_dev/lib/src/model/column.dart | 6 + drift_dev/lib/src/model/database.dart | 17 +- .../lib/src/model/declarations/views.dart | 17 ++ drift_dev/lib/src/model/view.dart | 18 +- .../src/writer/tables/data_class_writer.dart | 45 ++-- .../lib/src/writer/tables/view_writer.dart | 43 ++- 23 files changed, 787 insertions(+), 78 deletions(-) create mode 100644 drift_dev/lib/src/analyzer/dart/view_parser.dart diff --git a/drift/example/main.dart b/drift/example/main.dart index ea3a641e..94cf6922 100644 --- a/drift/example/main.dart +++ b/drift/example/main.dart @@ -15,6 +15,9 @@ class TodoItems extends Table { 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(')'))(); } abstract class TodoCategoryItemCount extends View { @@ -28,23 +31,26 @@ abstract class TodoCategoryItemCount extends View { todoCategories.name, itemCount, ]).from(todoCategories).join([ - innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id)) + innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id), + useColumns: false) ]); } -class TodoItemWithCategoryName extends View { - late $TodoItemsTable todoItems; - late $TodoCategoriesTable todoCategories; +@DriftView(name: 'customViewName') +abstract class TodoItemWithCategoryNameView extends View { + $TodoItemsTable get todoItems; + $TodoCategoriesTable get todoCategories; TextColumn get title => text().generatedAs(todoItems.title + - const Constant(' (') + + const Constant('(') + todoCategories.name + const Constant(')'))(); @override Query as() => select([todoItems.id, title]).from(todoItems).join([ innerJoin( - todoCategories, todoCategories.id.equalsExp(todoItems.categoryId)) + todoCategories, todoCategories.id.equalsExp(todoItems.categoryId), + useColumns: false) ]); } @@ -53,13 +59,13 @@ class TodoItemWithCategoryName extends View { TodoCategories, ], views: [ TodoCategoryItemCount, - TodoItemWithCategoryName, + TodoItemWithCategoryNameView, ]) class Database extends _$Database { Database(QueryExecutor e) : super(e); @override - int get schemaVersion => 1; + int get schemaVersion => 2; @override MigrationStrategy get migration { @@ -70,7 +76,7 @@ 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', categoryId: 0), + TodoItemsCompanion.insert(title: 'Aasd first entry', categoryId: 0), TodoItemsCompanion.insert( title: 'Todo: Checkout drift', content: const Value('Drift is a persistence library for Dart ' @@ -108,6 +114,9 @@ Future main() async { 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(); } diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index 27ab1975..f2a2a6de 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -187,11 +187,13 @@ class TodoItem extends DataClass implements Insertable { final String title; final String? content; final int categoryId; + final String? generatedText; TodoItem( {required this.id, required this.title, this.content, - required this.categoryId}); + required this.categoryId, + this.generatedText}); factory TodoItem.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return TodoItem( @@ -203,6 +205,8 @@ class TodoItem extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}content']), categoryId: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}category_id'])!, + generatedText: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}generated_text']), ); } @override @@ -236,6 +240,7 @@ class TodoItem extends DataClass implements Insertable { title: serializer.fromJson(json['title']), content: serializer.fromJson(json['content']), categoryId: serializer.fromJson(json['categoryId']), + generatedText: serializer.fromJson(json['generatedText']), ); } factory TodoItem.fromJsonString(String encodedJson, @@ -251,6 +256,7 @@ class TodoItem extends DataClass implements Insertable { 'title': serializer.toJson(title), 'content': serializer.toJson(content), 'categoryId': serializer.toJson(categoryId), + 'generatedText': serializer.toJson(generatedText), }; } @@ -258,12 +264,15 @@ class TodoItem extends DataClass implements Insertable { {int? id, String? title, Value content = const Value.absent(), - int? categoryId}) => + int? categoryId, + Value 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() { @@ -271,13 +280,15 @@ class TodoItem extends DataClass implements Insertable { ..write('id: $id, ') ..write('title: $title, ') ..write('content: $content, ') - ..write('categoryId: $categoryId') + ..write('categoryId: $categoryId, ') + ..write('generatedText: $generatedText') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, title, content, categoryId); + int get hashCode => + Object.hash(id, title, content, categoryId, generatedText); @override bool operator ==(Object other) => identical(this, other) || @@ -285,7 +296,8 @@ class TodoItem extends DataClass implements Insertable { other.id == this.id && other.title == this.title && other.content == this.content && - other.categoryId == this.categoryId); + other.categoryId == this.categoryId && + other.generatedText == this.generatedText); } class TodoItemsCompanion extends UpdateCompanion { @@ -388,8 +400,17 @@ class $TodoItemsTable extends TodoItems type: const IntType(), requiredDuringInsert: true, defaultConstraints: 'REFERENCES todo_categories (id)'); + final VerificationMeta _generatedTextMeta = + const VerificationMeta('generatedText'); + late final GeneratedColumn generatedText = GeneratedColumn( + 'generated_text', aliasedName, true, + type: const StringType(), + requiredDuringInsert: false, + generatedAs: GeneratedAs( + title + const Constant(' (') + content + const Constant(')'), false)); @override - List get $columns => [id, title, content, categoryId]; + List get $columns => + [id, title, content, categoryId, generatedText]; @override String get aliasedName => _alias ?? 'todo_items'; @override @@ -420,6 +441,12 @@ class $TodoItemsTable extends TodoItems } else if (isInserting) { context.missing(_categoryIdMeta); } + if (data.containsKey('generated_text')) { + context.handle( + _generatedTextMeta, + generatedText.isAcceptableOrUnknown( + data['generated_text']!, _generatedTextMeta)); + } return context; } @@ -437,14 +464,227 @@ 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 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 json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoCategoryItemCountData( + name: serializer.fromJson(json['name']), + itemCount: serializer.fromJson(json['itemCount']), + ); + } + factory TodoCategoryItemCountData.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + TodoCategoryItemCountData.fromJson( + DataClass.parseJson(encodedJson) as Map, + serializer: serializer); + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'itemCount': serializer.toJson(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 get $columns => [todoCategories.name, itemCount]; + @override + String get aliasedName => _alias ?? actualViewName; + @override + String get actualViewName => 'todo_category_item_count'; + @override + String? get createViewStmt => null; + @override + $TodoCategoryItemCountView get asDslTable => this; + @override + TodoCategoryItemCountData map(Map data, + {String? tablePrefix}) { + return TodoCategoryItemCountData.fromData(data, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + late final GeneratedColumn itemCount = GeneratedColumn( + '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)..addColumns($columns)).join([ + innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id), + useColumns: false) + ]); +} + +class TodoItemWithCategoryNameViewData extends DataClass { + final int id; + final String title; + TodoItemWithCategoryNameViewData({required this.id, required this.title}); + factory TodoItemWithCategoryNameViewData.fromData(Map 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 json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoItemWithCategoryNameViewData( + id: serializer.fromJson(json['id']), + title: serializer.fromJson(json['title']), + ); + } + factory TodoItemWithCategoryNameViewData.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + TodoItemWithCategoryNameViewData.fromJson( + DataClass.parseJson(encodedJson) as Map, + serializer: serializer); + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'title': serializer.toJson(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 get $columns => [todoItems.id, title]; + @override + String get aliasedName => _alias ?? actualViewName; + @override + String get actualViewName => 'customViewName'; + @override + String? get createViewStmt => null; + @override + $TodoItemWithCategoryNameViewView get asDslTable => this; + @override + TodoItemWithCategoryNameViewData map(Map data, + {String? tablePrefix}) { + return TodoItemWithCategoryNameViewData.fromData(data, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + late final GeneratedColumn title = GeneratedColumn( + '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)..addColumns($columns)).join([ + innerJoin( + todoCategories, todoCategories.id.equalsExp(todoItems.categoryId), + useColumns: false) + ]); +} + 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 get allTables => allSchemaEntities.whereType(); @override List get allSchemaEntities => - [todoCategories, todoItems]; + [todoCategories, todoItems, todoCategoryItemCount, customViewName]; } diff --git a/drift/lib/src/dsl/database.dart b/drift/lib/src/dsl/database.dart index 78b7c420..2163fee9 100644 --- a/drift/lib/src/dsl/database.dart +++ b/drift/lib/src/dsl/database.dart @@ -95,6 +95,9 @@ class DriftAccessor { /// The tables accessed by this DAO. final List tables; + /// The views to include in the database + final List views; + /// {@macro drift_compile_queries_param} final Map queries; @@ -105,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 {}, }); diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index b7e3432f..4466776a 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -129,22 +129,12 @@ abstract class View extends ColumnDefinition { /// Defines a view to be used with drift. const View(); - /// 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 `LocalSettings` will be called `local_settings` by - /// default. - /// You can change that behavior by overriding this method to use a custom - /// name. Please note that you must directly return a string literal by using - /// a getter. For instance `@override String get viewName => 'my_view';` is - /// valid, whereas `@override final String viewName = 'my_view';` or - /// `@override String get viewName => createMyViewName();` is not. - @visibleForOverriding - String? get viewName => null; - /// + @protected View select(List columns) => _isGenerated(); /// + @protected SimpleSelectStatement from(Table table) => _isGenerated(); /// @@ -203,3 +193,36 @@ 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}); +} + +/// +@Target({TargetKind.getter}) +class Reference { + /// + final Type type; + + /// + const Reference(this.type); +} diff --git a/drift/lib/src/runtime/query_builder/generation_context.dart b/drift/lib/src/runtime/query_builder/generation_context.dart index 755c770e..8a7c90ac 100644 --- a/drift/lib/src/runtime/query_builder/generation_context.dart +++ b/drift/lib/src/runtime/query_builder/generation_context.dart @@ -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 watchedTables = []; diff --git a/drift/lib/src/runtime/query_builder/migration.dart b/drift/lib/src/runtime/query_builder/migration.dart index 071cadc0..0395f5e0 100644 --- a/drift/lib/src/runtime/query_builder/migration.dart +++ b/drift/lib/src/runtime/query_builder/migration.dart @@ -305,8 +305,17 @@ class Migrator { } /// Executes a `CREATE VIEW` statement to create the [view]. - Future createView(ViewInfo view) { - return _issueCustomQuery(view.createViewStmt, const []); + Future 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.actualViewName; + context.buffer.write('CREATE VIEW ${view.actualViewName} AS '); + view.query!.writeInto(context); + await _issueCustomQuery(context.sql, const []); + } } /// Drops a table, trigger or index. diff --git a/drift/lib/src/runtime/query_builder/schema/column_impl.dart b/drift/lib/src/runtime/query_builder/schema/column_impl.dart index b133ff54..d24c0c0d 100644 --- a/drift/lib/src/runtime/query_builder/schema/column_impl.dart +++ b/drift/lib/src/runtime/query_builder/schema/column_impl.dart @@ -153,12 +153,16 @@ class GeneratedColumn extends Column { @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 diff --git a/drift/lib/src/runtime/query_builder/schema/view_info.dart b/drift/lib/src/runtime/query_builder/schema/view_info.dart index 845d9a52..a31253c4 100644 --- a/drift/lib/src/runtime/query_builder/schema/view_info.dart +++ b/drift/lib/src/runtime/query_builder/schema/view_info.dart @@ -9,7 +9,7 @@ part of '../query_builder.dart'; /// /// [sqlite-docs]: https://www.sqlite.org/lang_createview.html /// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/ -mixin ViewInfo on View +abstract class ViewInfo implements ResultSetImplementation { /// String get actualViewName; @@ -18,5 +18,8 @@ mixin ViewInfo on View String get entityName => actualViewName; /// The `CREATE VIEW` sql statement that can be used to create this view. - String get createViewStmt; + String? get createViewStmt; + + /// Predefined query from View.as() + Query? get query; } diff --git a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart index 8ebcd462..0ae0ba5e 100644 --- a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart +++ b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart @@ -86,7 +86,11 @@ class JoinedSelectStatement 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'; } diff --git a/drift/test/data/tables/custom_tables.g.dart b/drift/test/data/tables/custom_tables.g.dart index dcb90fd9..808ea3cf 100644 --- a/drift/test/data/tables/custom_tables.g.dart +++ b/drift/test/data/tables/custom_tables.g.dart @@ -1536,7 +1536,7 @@ class MyViewData extends DataClass { other.syncStateImplicit == this.syncStateImplicit); } -class MyView extends View with ViewInfo { +class MyView extends ViewInfo implements HasResultSet { final _$CustomTablesDb _db; final String? _alias; MyView(this._db, [this._alias]); @@ -1579,7 +1579,7 @@ class MyView extends View with ViewInfo { } @override - Query as() => _db.select(_db.myView); + Query? get query => null; } abstract class _$CustomTablesDb extends GeneratedDatabase { diff --git a/drift_dev/lib/src/analyzer/dart/parser.dart b/drift_dev/lib/src/analyzer/dart/parser.dart index af9f9917..7e0e4edc 100644 --- a/drift_dev/lib/src/analyzer/dart/parser.dart +++ b/drift_dev/lib/src/analyzer/dart/parser.dart @@ -20,6 +20,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 +29,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 parseTable(ClassElement classElement) { return _tableParser.parseTable(classElement); } + Future parseView( + ClassElement classElement, List 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 diff --git a/drift_dev/lib/src/analyzer/dart/use_dao_parser.dart b/drift_dev/lib/src/analyzer/dart/use_dao_parser.dart index 39c594d6..483fee15 100644 --- a/drift_dev/lib/src/analyzer/dart/use_dao_parser.dart +++ b/drift_dev/lib/src/analyzer/dart/use_dao_parser.dart @@ -40,6 +40,13 @@ class UseDaoParser { const []; final queryStrings = annotation.peek('queries')?.mapValue ?? {}; + final viewTypes = annotation + .peek('views') + ?.listValue + .map((obj) => obj.toTypeValue()) + .whereType() ?? + 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, ); diff --git a/drift_dev/lib/src/analyzer/dart/use_moor_parser.dart b/drift_dev/lib/src/analyzer/dart/use_moor_parser.dart index 1fc3388e..8a3679e7 100644 --- a/drift_dev/lib/src/analyzer/dart/use_moor_parser.dart +++ b/drift_dev/lib/src/analyzer/dart/use_moor_parser.dart @@ -23,6 +23,13 @@ class UseMoorParser { )); } + final viewTypes = annotation + .peek('views') + ?.listValue + .map((obj) => obj.toTypeValue()) + .whereType() ?? + 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, diff --git a/drift_dev/lib/src/analyzer/dart/view_parser.dart b/drift_dev/lib/src/analyzer/dart/view_parser.dart new file mode 100644 index 00000000..104737b8 --- /dev/null +++ b/drift_dev/lib/src/analyzer/dart/view_parser.dart @@ -0,0 +1,237 @@ +part of 'parser.dart'; + +/// Parses a [MoorView] from a Dart class. +class ViewParser { + final MoorDartParser base; + + ViewParser(this.base); + + Future parseView( + ClassElement element, List 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, tables, columns); + + final view = MoorView( + declaration: DartViewDeclaration(element, base.step.file), + name: name, + dartTypeName: dataClassInfo.enforcedName, + existingRowClass: dataClassInfo.existingClass, + entityInfoName: '\$${element.name}View', + staticReferences: staticReferences, + viewQuery: query, + ); + + view.columns = columns; + return view; + } + + _DataClassInformation _readDataClassInformation( + List 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.errors); + return _DataClassInformation(name, verified); + } + + Future _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> _parseColumns(ClassElement element) async { + final columnNames = element.allSupertypes + .map((t) => t.element) + .followedBy([element]) + .expand((e) => e.fields) + .where((field) => + isColumn(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; + }); + + final results = await Future.wait(fields.map((field) async { + final node = + await base.loadElementDeclaration(field.getter!) as MethodDeclaration; + + return await base.parseColumn(node, field.getter!); + })); + + return results.whereType(); + } + + Future> _parseStaticReferences( + ClassElement element, List 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() + .toList(); + } + + Future _getStaticReference( + FieldElement field, List 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.entityInfoName == node.returnType.toString()); + if (type != null) { + final name = node.name.toString(); + return '${node.returnType} get $name => _db.${type.dbGetterName};'; + } + } + } catch (_) {} + } + return null; + } + + Future _parseQuery(ClassElement element, + List tables, List 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 _throwError( + 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 table = + tables.firstWhere((tbl) => tbl.dbGetterName == parts[0]); + final column = table.columns + .firstWhere((col) => col.dartGetterName == parts[1]); + column.table = table; + return column; + } + return columns.firstWhere((col) => col.dartGetterName == parts[0]); + }); + + target = target.parent as MethodInvocation; + if (target.methodName.toString() != 'from') { + throw _throwError( + 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(columnList.toList(), from, query); + } catch (e) { + print(e); + throw _throwError(element, 'Failed to parse view `as()` query'); + } + } + + throw _throwError(element, 'Missing `as()` query declaration'); + } + + Exception _throwError(ClassElement element, String message) { + final error = ErrorInDartCode( + message: message, + severity: Severity.criticalError, + affectedElement: element, + ); + base.step.reportError(error); + return Exception(error.toString()); + } +} diff --git a/drift_dev/lib/src/analyzer/runner/steps/analyze_dart.dart b/drift_dev/lib/src/analyzer/runner/steps/analyze_dart.dart index e309a4a1..001cfc40 100644 --- a/drift_dev/lib/src/analyzer/runner/steps/analyze_dart.dart +++ b/drift_dev/lib/src/analyzer/runner/steps/analyze_dart.dart @@ -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 @@ -36,6 +42,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? availableEntities; try { diff --git a/drift_dev/lib/src/analyzer/runner/steps/parse_dart.dart b/drift_dev/lib/src/analyzer/runner/steps/parse_dart.dart index 1f6b5530..fd9ea35c 100644 --- a/drift_dev/lib/src/analyzer/runner/steps/parse_dart.dart +++ b/drift_dev/lib/src/analyzer/runner/steps/parse_dart.dart @@ -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 _tables = {}; + final Map _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 _parseView( + ClassElement element, List 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> parseTables( Iterable 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> parseViews( + Iterable types, Element initializedBy, List 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 readDeclaredQueries(Map obj) { return obj.entries.map((entry) { final key = entry.key.toStringValue()!; diff --git a/drift_dev/lib/src/analyzer/view/view_analyzer.dart b/drift_dev/lib/src/analyzer/view/view_analyzer.dart index 0535f4a9..e5b129a3 100644 --- a/drift_dev/lib/src/analyzer/view/view_analyzer.dart +++ b/drift_dev/lib/src/analyzer/view/view_analyzer.dart @@ -22,10 +22,13 @@ class ViewAnalyzer extends BaseAnalyzer { Future resolve(Iterable 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(); } } } diff --git a/drift_dev/lib/src/model/column.dart b/drift_dev/lib/src/model/column.dart index 4bbc7cc1..cd82ef01 100644 --- a/drift_dev/lib/src/model/column.dart +++ b/drift_dev/lib/src/model/column.dart @@ -51,6 +51,9 @@ class MoorColumn implements HasDeclaration, HasType { /// and in the generated data class that will be generated for each table. final String dartGetterName; + String get getterNameWithTable => + table == null ? dartGetterName : '${table!.dbGetterName}.$dartGetterName'; + /// The declaration of this column, contains information about where this /// column was created in source code. @override @@ -111,6 +114,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`. diff --git a/drift_dev/lib/src/model/database.dart b/drift_dev/lib/src/model/database.dart index df036932..4488a677 100644 --- a/drift_dev/lib/src/model/database.dart +++ b/drift_dev/lib/src/model/database.dart @@ -22,6 +22,13 @@ abstract class BaseMoorAccessor implements HasDeclaration { /// that. final List 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 declaredViews; + /// The `includes` field from the `UseMoor` or `UseDao` annotation. final List declaredIncludes; @@ -48,7 +55,7 @@ abstract class BaseMoorAccessor implements HasDeclaration { /// Resolved imports from this file. List? 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 declaredTables = const [], + List declaredViews = const [], List declaredIncludes = const [], List 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 declaredTables, + List declaredViews = const [], required List declaredIncludes, required List declaredQueries, - }) : super._(declaration, declaredTables, declaredIncludes, declaredQueries); + }) : super._(declaration, declaredTables, declaredViews, declaredIncludes, + declaredQueries); } diff --git a/drift_dev/lib/src/model/declarations/views.dart b/drift_dev/lib/src/model/declarations/views.dart index c79b5536..02c240d6 100644 --- a/drift_dev/lib/src/model/declarations/views.dart +++ b/drift_dev/lib/src/model/declarations/views.dart @@ -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 diff --git a/drift_dev/lib/src/model/view.dart b/drift_dev/lib/src/model/view.dart index 66bb2087..80da182c 100644 --- a/drift_dev/lib/src/model/view.dart +++ b/drift_dev/lib/src/model/view.dart @@ -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 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 List columns; + final String from; + final String query; + + ViewQueryInformation(this.columns, this.from, this.query); +} diff --git a/drift_dev/lib/src/writer/tables/data_class_writer.dart b/drift_dev/lib/src/writer/tables/data_class_writer.dart index c28a766a..25bcda30 100644 --- a/drift_dev/lib/src/writer/tables/data_class_writer.dart +++ b/drift_dev/lib/src/writer/tables/data_class_writer.dart @@ -6,6 +6,7 @@ import 'package:drift_dev/writer.dart'; class DataClassWriter { final MoorEntityWithResultSet table; final Scope scope; + final columns = []; bool get isInsertable => table is MoorTable; @@ -31,8 +32,16 @@ class DataClassWriter { _buffer.writeln('{'); } + // write view columns + final view = table; + if (view is MoorView && view.viewQuery != null) { + columns.addAll(view.viewQuery!.columns); + } 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'); } @@ -45,7 +54,7 @@ class DataClassWriter { _buffer ..write(table.dartTypeName) ..write('({') - ..write(table.columns.map((column) { + ..write(columns.map((column) { if (column.nullable) { return 'this.${column.dartGetterName}'; } else { @@ -74,8 +83,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('}'); @@ -97,7 +106,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, ); @@ -117,7 +126,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); @@ -143,7 +152,7 @@ class DataClassWriter { 'serializer ??= $_runtimeOptions.defaultSerializer;\n' 'return {\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'; @@ -161,9 +170,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); @@ -187,7 +196,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 @@ -210,7 +219,7 @@ class DataClassWriter { '(bool nullToAbsent) {\n') ..write('final map = {};'); - for (final column in table.columns) { + for (final column in columns) { // Generated column - cannot be used for inserts or updates if (column.isGenerated) continue; @@ -268,7 +277,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; @@ -297,7 +306,7 @@ class DataClassWriter { void _writeToString() { overrideToString( table.dartTypeName, - [for (final column in table.columns) column.dartGetterName], + [for (final column in columns) column.dartGetterName], _buffer, ); } @@ -305,7 +314,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(';'); } @@ -323,7 +332,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)'; diff --git a/drift_dev/lib/src/writer/tables/view_writer.dart b/drift_dev/lib/src/writer/tables/view_writer.dart index bba9c1d3..ad2797dc 100644 --- a/drift_dev/lib/src/writer/tables/view_writer.dart +++ b/drift_dev/lib/src/writer/tables/view_writer.dart @@ -31,13 +31,14 @@ class ViewWriter extends TableOrViewWriter { void _writeViewInfoClass() { buffer = scope.leaf(); - buffer.write('class ${view.entityInfoName} extends View with ViewInfo'); + 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') @@ -46,14 +47,32 @@ class ViewWriter extends TableOrViewWriter { ..write('final ${scope.nullableType('String')} _alias;\n') ..write('${view.entityInfoName}(this._db, [this._alias]);\n'); - writeGetColumnsOverride(); + for (final ref in view.staticReferences) { + buffer.write('$ref\n'); + } + + if (view.viewQuery == null) { + writeGetColumnsOverride(); + } else { + final columns = view.viewQuery!.columns + .map((col) => col.getterNameWithTable) + .join(', '); + buffer.write('@override\nList get \$columns => ' + '[$columns];\n'); + } + buffer ..write('@override\nString get aliasedName => ' '_alias ?? actualViewName;\n') ..write('@override\n String get actualViewName =>' - ' ${asDartLiteral(view.name)};\n') - ..write('@override\n String get createViewStmt =>' + ' ${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'); + } writeAsDslTable(); writeMappingMethod(scope); @@ -63,7 +82,7 @@ class ViewWriter extends TableOrViewWriter { } _writeAliasGenerator(); - _writeAs(); + _writeQuery(); buffer.writeln('}'); } @@ -78,10 +97,14 @@ class ViewWriter extends TableOrViewWriter { ..write('}'); } - void _writeAs() { - buffer - ..write('@override\n') - ..write('Query<${view.entityInfoName}, ${view.dartTypeName}> as() =>\n') - ..write('_db.select(_db.${view.dbGetterName});\n'); + void _writeQuery() { + buffer.write('@override\nQuery? get query => '); + final query = view.viewQuery; + if (query != null) { + buffer.write('(_db.selectOnly(${query.from})..addColumns(\$columns))' + '${query.query};'); + } else { + buffer.write('null;\n'); + } } } From 47f0465795080944c48d135155bba05cafb67cf3 Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 16 Nov 2021 09:04:43 +0100 Subject: [PATCH 05/22] Make Join accept bare Table type --- drift/example/main.dart | 8 +++--- drift/example/main.g.dart | 4 --- .../query_builder/components/join.dart | 25 +++++++++++-------- .../statements/select/select_with_join.dart | 4 +-- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/drift/example/main.dart b/drift/example/main.dart index 94cf6922..d548a7d0 100644 --- a/drift/example/main.dart +++ b/drift/example/main.dart @@ -21,8 +21,8 @@ class TodoItems extends Table { } abstract class TodoCategoryItemCount extends View { - $TodoItemsTable get todoItems; - $TodoCategoriesTable get todoCategories; + TodoItems get todoItems; + TodoCategories get todoCategories; IntColumn get itemCount => integer().generatedAs(todoItems.id.count())(); @@ -38,8 +38,8 @@ abstract class TodoCategoryItemCount extends View { @DriftView(name: 'customViewName') abstract class TodoItemWithCategoryNameView extends View { - $TodoItemsTable get todoItems; - $TodoCategoriesTable get todoCategories; + TodoItems get todoItems; + TodoCategories get todoCategories; TextColumn get title => text().generatedAs(todoItems.title + const Constant('(') + diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index f2a2a6de..a91746a7 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -530,8 +530,6 @@ class $TodoCategoryItemCountView final _$Database _db; final String? _alias; $TodoCategoryItemCountView(this._db, [this._alias]); - $TodoItemsTable get todoItems => _db.todoItems; - $TodoCategoriesTable get todoCategories => _db.todoCategories; @override List get $columns => [todoCategories.name, itemCount]; @override @@ -632,8 +630,6 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo< final _$Database _db; final String? _alias; $TodoItemWithCategoryNameViewView(this._db, [this._alias]); - $TodoItemsTable get todoItems => _db.todoItems; - $TodoCategoriesTable get todoCategories => _db.todoCategories; @override List get $columns => [todoItems.id, title]; @override diff --git a/drift/lib/src/runtime/query_builder/components/join.dart b/drift/lib/src/runtime/query_builder/components/join.dart index 3745ff2d..425dddba 100644 --- a/drift/lib/src/runtime/query_builder/components/join.dart +++ b/drift/lib/src/runtime/query_builder/components/join.dart @@ -27,7 +27,7 @@ class Join extends Component { final _JoinType type; /// The [TableInfo] that will be added to the query - final ResultSetImplementation table; + final Table table; /// For joins that aren't [_JoinType.cross], contains an additional predicate /// that must be matched for the join. @@ -42,15 +42,23 @@ class Join extends Component { /// 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; + : includeInResult = includeInResult ?? true { + if (table is! ResultSetImplementation) { + 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; + context.buffer.write(resultSet.tableWithAlias); + context.watchedTables.add(resultSet); if (type != _JoinType.cross) { context.buffer.write(' ON '); @@ -70,8 +78,7 @@ class Join extends Component { /// See also: /// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins /// - http://www.sqlitetutorial.net/sqlite-inner-join/ -Join innerJoin( - ResultSetImplementation other, Expression on, +Join innerJoin(Table other, Expression on, {bool? useColumns}) { return Join._(_JoinType.inner, other, on, includeInResult: useColumns); } @@ -84,8 +91,7 @@ Join innerJoin( /// See also: /// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins /// - http://www.sqlitetutorial.net/sqlite-left-join/ -Join leftOuterJoin( - ResultSetImplementation other, Expression on, +Join leftOuterJoin(Table other, Expression on, {bool? useColumns}) { return Join._(_JoinType.leftOuter, other, on, includeInResult: useColumns); } @@ -98,7 +104,6 @@ Join leftOuterJoin( /// See also: /// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins /// - http://www.sqlitetutorial.net/sqlite-cross-join/ -Join crossJoin(ResultSetImplementation other, - {bool? useColumns}) { +Join crossJoin(Table other, {bool? useColumns}) { return Join._(_JoinType.cross, other, null, includeInResult: useColumns); } diff --git a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart index 0ae0ba5e..4af4525d 100644 --- a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart +++ b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart @@ -44,7 +44,7 @@ class JoinedSelectStatement int get _returnedColumnCount { return _joins.fold(_selectedColumns.length, (prev, join) { if (join.includeInResult) { - return prev + join.table.$columns.length; + return prev + (join.table as ResultSetImplementation).$columns.length; } return prev; }); @@ -63,7 +63,7 @@ class JoinedSelectStatement for (final join in _joins) { if (onlyResults && !join.includeInResult) continue; - yield join.table; + yield join.table as ResultSetImplementation; } } From a6bd75d7800c5d6783c3e70b93646dd9f1824d5f Mon Sep 17 00:00:00 2001 From: westito Date: Fri, 26 Nov 2021 14:15:54 +0100 Subject: [PATCH 06/22] fix post-merge issues --- drift/example/main.g.dart | 4 ++-- drift_dev/lib/src/analyzer/dart/view_parser.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index a91746a7..076bf5ba 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -467,7 +467,7 @@ class $TodoItemsTable extends TodoItems class TodoCategoryItemCountData extends DataClass { final String name; final int itemCount; - TodoCategoryItemCountData({required this.name, required this.itemCount}); + TodoCategoryItemCountData({required this.itemCount}); factory TodoCategoryItemCountData.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; @@ -567,7 +567,7 @@ class $TodoCategoryItemCountView class TodoItemWithCategoryNameViewData extends DataClass { final int id; final String title; - TodoItemWithCategoryNameViewData({required this.id, required this.title}); + TodoItemWithCategoryNameViewData({required this.title}); factory TodoItemWithCategoryNameViewData.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; diff --git a/drift_dev/lib/src/analyzer/dart/view_parser.dart b/drift_dev/lib/src/analyzer/dart/view_parser.dart index 104737b8..1a0e256c 100644 --- a/drift_dev/lib/src/analyzer/dart/view_parser.dart +++ b/drift_dev/lib/src/analyzer/dart/view_parser.dart @@ -80,7 +80,7 @@ class ViewParser { final verified = existingClass == null ? null : validateExistingClass(columns, existingClass, - constructorInExistingClass!, generateInsertable!, base.step.errors); + constructorInExistingClass!, generateInsertable!, base.step); return _DataClassInformation(name, verified); } From 810d9f03a5af2f222a073365aa3d39dffbdcca25 Mon Sep 17 00:00:00 2001 From: westito Date: Sat, 27 Nov 2021 18:22:04 +0100 Subject: [PATCH 07/22] Modify DSL view to accept expression instead of column --- drift/example/main.dart | 10 +-- drift/example/main.g.dart | 58 +++++++++------- drift/lib/src/dsl/table.dart | 2 +- .../src/runtime/query_builder/migration.dart | 4 +- .../query_builder/schema/view_info.dart | 5 +- drift/test/data/tables/custom_tables.g.dart | 4 +- drift_dev/lib/src/analyzer/dart/parser.dart | 2 + .../lib/src/analyzer/dart/view_parser.dart | 68 +++++++++++++------ drift_dev/lib/src/utils/exception.dart | 14 ++++ drift_dev/lib/src/utils/type_utils.dart | 6 ++ .../src/writer/tables/data_class_writer.dart | 2 +- .../lib/src/writer/tables/view_writer.dart | 6 +- 12 files changed, 119 insertions(+), 62 deletions(-) create mode 100644 drift_dev/lib/src/utils/exception.dart diff --git a/drift/example/main.dart b/drift/example/main.dart index d548a7d0..7fafbbd5 100644 --- a/drift/example/main.dart +++ b/drift/example/main.dart @@ -5,6 +5,7 @@ import 'package:drift/native.dart'; part 'main.g.dart'; +@DataClassName('TodoCategory') class TodoCategories extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); @@ -24,7 +25,7 @@ abstract class TodoCategoryItemCount extends View { TodoItems get todoItems; TodoCategories get todoCategories; - IntColumn get itemCount => integer().generatedAs(todoItems.id.count())(); + Expression get itemCount => todoItems.id.count(); @override Query as() => select([ @@ -41,10 +42,11 @@ abstract class TodoItemWithCategoryNameView extends View { TodoItems get todoItems; TodoCategories get todoCategories; - TextColumn get title => text().generatedAs(todoItems.title + + Expression get title => + todoItems.title + const Constant('(') + todoCategories.name + - const Constant(')'))(); + const Constant(')'); @override Query as() => select([todoItems.id, title]).from(todoItems).join([ @@ -76,7 +78,7 @@ class Database extends _$Database { // Add a bunch of default items in a batch await batch((b) { b.insertAll(todoItems, [ - TodoItemsCompanion.insert(title: 'Aasd first entry', categoryId: 0), + TodoItemsCompanion.insert(title: 'A first entry', categoryId: 0), TodoItemsCompanion.insert( title: 'Todo: Checkout drift', content: const Value('Drift is a persistence library for Dart ' diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index 076bf5ba..6bed600e 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -7,13 +7,13 @@ part of 'main.dart'; // ************************************************************************** // ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this -class TodoCategorie extends DataClass implements Insertable { +class TodoCategory extends DataClass implements Insertable { final int id; final String name; - TodoCategorie({required this.id, required this.name}); - factory TodoCategorie.fromData(Map data, {String? prefix}) { + TodoCategory({required this.id, required this.name}); + factory TodoCategory.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; - return TodoCategorie( + return TodoCategory( id: const IntType() .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, name: const StringType() @@ -35,17 +35,17 @@ class TodoCategorie extends DataClass implements Insertable { ); } - factory TodoCategorie.fromJson(Map json, + factory TodoCategory.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return TodoCategorie( + return TodoCategory( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), ); } - factory TodoCategorie.fromJsonString(String encodedJson, + factory TodoCategory.fromJsonString(String encodedJson, {ValueSerializer? serializer}) => - TodoCategorie.fromJson( + TodoCategory.fromJson( DataClass.parseJson(encodedJson) as Map, serializer: serializer); @override @@ -57,13 +57,13 @@ class TodoCategorie extends DataClass implements Insertable { }; } - TodoCategorie copyWith({int? id, String? name}) => TodoCategorie( + TodoCategory copyWith({int? id, String? name}) => TodoCategory( id: id ?? this.id, name: name ?? this.name, ); @override String toString() { - return (StringBuffer('TodoCategorie(') + return (StringBuffer('TodoCategory(') ..write('id: $id, ') ..write('name: $name') ..write(')')) @@ -75,12 +75,10 @@ class TodoCategorie extends DataClass implements Insertable { @override bool operator ==(Object other) => identical(this, other) || - (other is TodoCategorie && - other.id == this.id && - other.name == this.name); + (other is TodoCategory && other.id == this.id && other.name == this.name); } -class TodoCategoriesCompanion extends UpdateCompanion { +class TodoCategoriesCompanion extends UpdateCompanion { final Value id; final Value name; const TodoCategoriesCompanion({ @@ -91,7 +89,7 @@ class TodoCategoriesCompanion extends UpdateCompanion { this.id = const Value.absent(), required String name, }) : name = Value(name); - static Insertable custom({ + static Insertable custom({ Expression? id, Expression? name, }) { @@ -131,7 +129,7 @@ class TodoCategoriesCompanion extends UpdateCompanion { } class $TodoCategoriesTable extends TodoCategories - with TableInfo<$TodoCategoriesTable, TodoCategorie> { + with TableInfo<$TodoCategoriesTable, TodoCategory> { final GeneratedDatabase _db; final String? _alias; $TodoCategoriesTable(this._db, [this._alias]); @@ -152,7 +150,7 @@ class $TodoCategoriesTable extends TodoCategories @override String get actualTableName => 'todo_categories'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); @@ -171,8 +169,8 @@ class $TodoCategoriesTable extends TodoCategories @override Set get $primaryKey => {id}; @override - TodoCategorie map(Map data, {String? tablePrefix}) { - return TodoCategorie.fromData(data, + TodoCategory map(Map data, {String? tablePrefix}) { + return TodoCategory.fromData(data, prefix: tablePrefix != null ? '$tablePrefix.' : null); } @@ -467,7 +465,7 @@ class $TodoItemsTable extends TodoItems class TodoCategoryItemCountData extends DataClass { final String name; final int itemCount; - TodoCategoryItemCountData({required this.itemCount}); + TodoCategoryItemCountData({required this.name, required this.itemCount}); factory TodoCategoryItemCountData.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; @@ -530,12 +528,14 @@ class $TodoCategoryItemCountView final _$Database _db; final String? _alias; $TodoCategoryItemCountView(this._db, [this._alias]); + $TodoItemsTable get todoItems => _db.todoItems; + $TodoCategoriesTable get todoCategories => _db.todoCategories; @override List get $columns => [todoCategories.name, itemCount]; @override - String get aliasedName => _alias ?? actualViewName; + String get aliasedName => _alias ?? entityName; @override - String get actualViewName => 'todo_category_item_count'; + String get entityName => 'todo_category_item_count'; @override String? get createViewStmt => null; @override @@ -547,6 +547,9 @@ class $TodoCategoryItemCountView prefix: tablePrefix != null ? '$tablePrefix.' : null); } + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: const StringType()); late final GeneratedColumn itemCount = GeneratedColumn( 'item_count', aliasedName, false, type: const IntType(), @@ -567,7 +570,7 @@ class $TodoCategoryItemCountView class TodoItemWithCategoryNameViewData extends DataClass { final int id; final String title; - TodoItemWithCategoryNameViewData({required this.title}); + TodoItemWithCategoryNameViewData({required this.id, required this.title}); factory TodoItemWithCategoryNameViewData.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; @@ -630,12 +633,14 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo< final _$Database _db; final String? _alias; $TodoItemWithCategoryNameViewView(this._db, [this._alias]); + $TodoItemsTable get todoItems => _db.todoItems; + $TodoCategoriesTable get todoCategories => _db.todoCategories; @override List get $columns => [todoItems.id, title]; @override - String get aliasedName => _alias ?? actualViewName; + String get aliasedName => _alias ?? entityName; @override - String get actualViewName => 'customViewName'; + String get entityName => 'customViewName'; @override String? get createViewStmt => null; @override @@ -647,6 +652,9 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo< prefix: tablePrefix != null ? '$tablePrefix.' : null); } + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: const IntType(), defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); late final GeneratedColumn title = GeneratedColumn( 'title', aliasedName, false, type: const StringType(), diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index 4466776a..b21db636 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -131,7 +131,7 @@ abstract class View extends ColumnDefinition { /// @protected - View select(List columns) => _isGenerated(); + View select(List columns) => _isGenerated(); /// @protected diff --git a/drift/lib/src/runtime/query_builder/migration.dart b/drift/lib/src/runtime/query_builder/migration.dart index 0395f5e0..8a7598ab 100644 --- a/drift/lib/src/runtime/query_builder/migration.dart +++ b/drift/lib/src/runtime/query_builder/migration.dart @@ -311,8 +311,8 @@ class Migrator { await _issueCustomQuery(stmt, const []); } else if (view.query != null) { final context = GenerationContext.fromDb(_db); - context.generatingForView = view.actualViewName; - context.buffer.write('CREATE VIEW ${view.actualViewName} AS '); + context.generatingForView = view.entityName; + context.buffer.write('CREATE VIEW ${view.entityName} AS '); view.query!.writeInto(context); await _issueCustomQuery(context.sql, const []); } diff --git a/drift/lib/src/runtime/query_builder/schema/view_info.dart b/drift/lib/src/runtime/query_builder/schema/view_info.dart index a31253c4..9043b8b3 100644 --- a/drift/lib/src/runtime/query_builder/schema/view_info.dart +++ b/drift/lib/src/runtime/query_builder/schema/view_info.dart @@ -11,11 +11,8 @@ part of '../query_builder.dart'; /// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/ abstract class ViewInfo implements ResultSetImplementation { - /// - String get actualViewName; - @override - String get entityName => actualViewName; + String get entityName; /// The `CREATE VIEW` sql statement that can be used to create this view. String? get createViewStmt; diff --git a/drift/test/data/tables/custom_tables.g.dart b/drift/test/data/tables/custom_tables.g.dart index 808ea3cf..551048c0 100644 --- a/drift/test/data/tables/custom_tables.g.dart +++ b/drift/test/data/tables/custom_tables.g.dart @@ -1544,9 +1544,9 @@ class MyView extends ViewInfo implements HasResultSet { List get $columns => [configKey, configValue, syncState, syncStateImplicit]; @override - String get aliasedName => _alias ?? actualViewName; + String get aliasedName => _alias ?? entityName; @override - String get actualViewName => 'my_view'; + String get entityName => 'my_view'; @override String get createViewStmt => 'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2'; diff --git a/drift_dev/lib/src/analyzer/dart/parser.dart b/drift_dev/lib/src/analyzer/dart/parser.dart index 7e0e4edc..21b39fde 100644 --- a/drift_dev/lib/src/analyzer/dart/parser.dart +++ b/drift_dev/lib/src/analyzer/dart/parser.dart @@ -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'; diff --git a/drift_dev/lib/src/analyzer/dart/view_parser.dart b/drift_dev/lib/src/analyzer/dart/view_parser.dart index 1a0e256c..f24383c0 100644 --- a/drift_dev/lib/src/analyzer/dart/view_parser.dart +++ b/drift_dev/lib/src/analyzer/dart/view_parser.dart @@ -107,7 +107,7 @@ class ViewParser { .followedBy([element]) .expand((e) => e.fields) .where((field) => - isColumn(field.type) && + isExpression(field.type) && field.getter != null && !field.getter!.isSynthetic) .map((field) => field.name) @@ -117,18 +117,52 @@ class ViewParser { 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 await base.parseColumn(node, field.getter!); - })); + 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> _parseStaticReferences( ClassElement element, List tables) async { return await Stream.fromIterable(element.allSupertypes @@ -147,10 +181,11 @@ class ViewParser { final node = await base.loadElementDeclaration(field.getter!); if (node is MethodDeclaration && node.body is EmptyFunctionBody) { final type = tables.firstWhereOrNull( - (tbl) => tbl.entityInfoName == node.returnType.toString()); + (tbl) => tbl.fromClass!.name == node.returnType.toString()); if (type != null) { final name = node.name.toString(); - return '${node.returnType} get $name => _db.${type.dbGetterName};'; + return '${type.entityInfoName} get $name => ' + '_db.${type.dbGetterName};'; } } } catch (_) {} @@ -177,7 +212,8 @@ class ViewParser { } if (target.methodName.toString() != 'select') { - throw _throwError( + throw analysisError( + base.step, element, 'The `as()` query declaration must be started ' 'with `select(columns).from(table)'); @@ -201,7 +237,8 @@ class ViewParser { target = target.parent as MethodInvocation; if (target.methodName.toString() != 'from') { - throw _throwError( + throw analysisError( + base.step, element, 'The `as()` query declaration must be started ' 'with `select(columns).from(table)'); @@ -218,20 +255,11 @@ class ViewParser { return ViewQueryInformation(columnList.toList(), from, query); } catch (e) { print(e); - throw _throwError(element, 'Failed to parse view `as()` query'); + throw analysisError( + base.step, element, 'Failed to parse view `as()` query'); } } - throw _throwError(element, 'Missing `as()` query declaration'); - } - - Exception _throwError(ClassElement element, String message) { - final error = ErrorInDartCode( - message: message, - severity: Severity.criticalError, - affectedElement: element, - ); - base.step.reportError(error); - return Exception(error.toString()); + throw analysisError(base.step, element, 'Missing `as()` query declaration'); } } diff --git a/drift_dev/lib/src/utils/exception.dart b/drift_dev/lib/src/utils/exception.dart new file mode 100644 index 00000000..15477416 --- /dev/null +++ b/drift_dev/lib/src/utils/exception.dart @@ -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()); +} diff --git a/drift_dev/lib/src/utils/type_utils.dart b/drift_dev/lib/src/utils/type_utils.dart index 7b5c1d61..e50bb2ad 100644 --- a/drift_dev/lib/src/utils/type_utils.dart +++ b/drift_dev/lib/src/utils/type_utils.dart @@ -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); diff --git a/drift_dev/lib/src/writer/tables/data_class_writer.dart b/drift_dev/lib/src/writer/tables/data_class_writer.dart index 1daf244e..9b5c12d6 100644 --- a/drift_dev/lib/src/writer/tables/data_class_writer.dart +++ b/drift_dev/lib/src/writer/tables/data_class_writer.dart @@ -55,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 diff --git a/drift_dev/lib/src/writer/tables/view_writer.dart b/drift_dev/lib/src/writer/tables/view_writer.dart index ad2797dc..6c9a4a47 100644 --- a/drift_dev/lib/src/writer/tables/view_writer.dart +++ b/drift_dev/lib/src/writer/tables/view_writer.dart @@ -63,8 +63,8 @@ class ViewWriter extends TableOrViewWriter { buffer ..write('@override\nString get aliasedName => ' - '_alias ?? actualViewName;\n') - ..write('@override\n String get actualViewName =>' + '_alias ?? entityName;\n') + ..write('@override\n String get entityName=>' ' ${asDartLiteral(view.name)};\n'); if (view.declaration is MoorViewDeclaration) { @@ -77,7 +77,7 @@ class ViewWriter extends TableOrViewWriter { writeAsDslTable(); writeMappingMethod(scope); - for (final column in view.columns) { + for (final column in view.viewQuery?.columns ?? view.columns) { writeColumnGetter(column, scope.generationOptions, false); } From b5af3edf737cc2f8904b46889d698d91d1ae2039 Mon Sep 17 00:00:00 2001 From: westito Date: Sun, 28 Nov 2021 19:29:17 +0100 Subject: [PATCH 08/22] Remove unnecessary generics declaration --- drift/lib/src/runtime/query_builder/components/join.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/components/join.dart b/drift/lib/src/runtime/query_builder/components/join.dart index 425dddba..9b6405ec 100644 --- a/drift/lib/src/runtime/query_builder/components/join.dart +++ b/drift/lib/src/runtime/query_builder/components/join.dart @@ -78,8 +78,7 @@ class Join extends Component { /// See also: /// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins /// - http://www.sqlitetutorial.net/sqlite-inner-join/ -Join innerJoin(Table other, Expression on, - {bool? useColumns}) { +Join innerJoin(Table other, Expression on, {bool? useColumns}) { return Join._(_JoinType.inner, other, on, includeInResult: useColumns); } @@ -91,8 +90,7 @@ Join innerJoin(Table other, Expression on, /// See also: /// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins /// - http://www.sqlitetutorial.net/sqlite-left-join/ -Join leftOuterJoin(Table other, Expression on, - {bool? useColumns}) { +Join leftOuterJoin(Table other, Expression on, {bool? useColumns}) { return Join._(_JoinType.leftOuter, other, on, includeInResult: useColumns); } @@ -104,6 +102,6 @@ Join leftOuterJoin(Table other, Expression on, /// See also: /// - https://drift.simonbinder.eu/docs/advanced-features/joins/#joins /// - http://www.sqlitetutorial.net/sqlite-cross-join/ -Join crossJoin(Table other, {bool? useColumns}) { +Join crossJoin(Table other, {bool? useColumns}) { return Join._(_JoinType.cross, other, null, includeInResult: useColumns); } From 9e443dba2aa6a7b9ddcfd55b1be4667634f6fcee Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 30 Nov 2021 14:06:13 +0100 Subject: [PATCH 09/22] Revert table declaration class --- drift/lib/src/dsl/table.dart | 114 +++++++++++++++++------------------ 1 file changed, 54 insertions(+), 60 deletions(-) diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index b21db636..9eba7c86 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -6,10 +6,59 @@ abstract class HasResultSet { const HasResultSet(); } -/// Base class for dsl [Table]s and [View]s. -abstract class ColumnDefinition extends HasResultSet { - /// Default constant constructor. - const ColumnDefinition(); +/// Subclasses represent a table in a database generated by drift. +abstract class Table extends HasResultSet { + /// Defines a table to be used with drift. + const Table(); + + /// The sql table name to be used. By default, drift will use the snake_case + /// representation of your class name as the sql table name. For instance, a + /// [Table] class named `LocalSettings` will be called `local_settings` by + /// default. + /// You can change that behavior by overriding this method to use a custom + /// name. Please note that you must directly return a string literal by using + /// a getter. For instance `@override String get tableName => 'my_table';` is + /// valid, whereas `@override final String tableName = 'my_table';` or + /// `@override String get tableName => createMyTableName();` is not. + @visibleForOverriding + String? get tableName => null; + + /// Whether to append a `WITHOUT ROWID` clause in the `CREATE TABLE` + /// statement. This is intended to be used by generated code only. + bool get withoutRowId => false; + + /// Drift will write some table constraints automatically, for instance when + /// you override [primaryKey]. You can turn this behavior off if you want to. + /// This is intended to be used by generated code only. + bool get dontWriteConstraints => false; + + /// Override this to specify custom primary keys: + /// ```dart + /// class IngredientInRecipes extends Table { + /// @override + /// Set get primaryKey => {recipe, ingredient}; + /// + /// IntColumn get recipe => integer()(); + /// IntColumn get ingredient => integer()(); + /// + /// IntColumn get amountInGrams => integer().named('amount')(); + ///} + /// ``` + /// The getter must return a set literal using the `=>` syntax so that the + /// drift generator can understand the code. + /// Also, please note that it's an error to have an + /// [BuildIntColumn.autoIncrement] column and a custom primary key. + /// As an auto-incremented `IntColumn` is recognized by drift to be the + /// primary key, doing so will result in an exception thrown at runtime. + @visibleForOverriding + Set? get primaryKey => null; + + /// Custom table constraints that should be added to the table. + /// + /// See also: + /// - https://www.sqlite.org/syntax/table-constraint.html, which defines what + /// table constraints are supported. + List get customConstraints => []; /// Use this as the body of a getter to declare a column that holds integers. /// Example (inside the body of a table class): @@ -69,63 +118,8 @@ abstract class ColumnDefinition extends HasResultSet { ColumnBuilder real() => _isGenerated(); } -/// Subclasses represent a table in a database generated by drift. -abstract class Table extends ColumnDefinition { - /// Defines a table to be used with drift. - const Table(); - - /// The sql table name to be used. By default, drift will use the snake_case - /// representation of your class name as the sql table name. For instance, a - /// [Table] class named `LocalSettings` will be called `local_settings` by - /// default. - /// You can change that behavior by overriding this method to use a custom - /// name. Please note that you must directly return a string literal by using - /// a getter. For instance `@override String get tableName => 'my_table';` is - /// valid, whereas `@override final String tableName = 'my_table';` or - /// `@override String get tableName => createMyTableName();` is not. - @visibleForOverriding - String? get tableName => null; - - /// Whether to append a `WITHOUT ROWID` clause in the `CREATE TABLE` - /// statement. This is intended to be used by generated code only. - bool get withoutRowId => false; - - /// Drift will write some table constraints automatically, for instance when - /// you override [primaryKey]. You can turn this behavior off if you want to. - /// This is intended to be used by generated code only. - bool get dontWriteConstraints => false; - - /// Override this to specify custom primary keys: - /// ```dart - /// class IngredientInRecipes extends Table { - /// @override - /// Set get primaryKey => {recipe, ingredient}; - /// - /// IntColumn get recipe => integer()(); - /// IntColumn get ingredient => integer()(); - /// - /// IntColumn get amountInGrams => integer().named('amount')(); - ///} - /// ``` - /// The getter must return a set literal using the `=>` syntax so that the - /// drift generator can understand the code. - /// Also, please note that it's an error to have an - /// [BuildIntColumn.autoIncrement] column and a custom primary key. - /// As an auto-incremented `IntColumn` is recognized by drift to be the - /// primary key, doing so will result in an exception thrown at runtime. - @visibleForOverriding - Set? get primaryKey => null; - - /// Custom table constraints that should be added to the table. - /// - /// See also: - /// - https://www.sqlite.org/syntax/table-constraint.html, which defines what - /// table constraints are supported. - List get customConstraints => []; -} - /// Subclasses represent a view in a database generated by drift. -abstract class View extends ColumnDefinition { +abstract class View extends HasResultSet { /// Defines a view to be used with drift. const View(); From 0e61940d705db38b1282ff689906e2ac910dde7f Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 30 Nov 2021 15:20:19 +0100 Subject: [PATCH 10/22] add join useColumns option to selectOnly --- drift/lib/src/runtime/api/connection_user.dart | 15 +++++++++++---- .../runtime/query_builder/components/join.dart | 4 +++- .../statements/select/select_with_join.dart | 10 +++++++--- drift_dev/lib/src/writer/tables/view_writer.dart | 3 ++- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/drift/lib/src/runtime/api/connection_user.dart b/drift/lib/src/runtime/api/connection_user.dart index e997309e..3b1b1528 100644 --- a/drift/lib/src/runtime/api/connection_user.dart +++ b/drift/lib/src/runtime/api/connection_user.dart @@ -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 selectOnly( - ResultSetImplementation table, { - bool distinct = false, - }) { + ResultSetImplementation table, + {bool distinct = false, + bool includeJoinedTableColumns = true}) { return JoinedSelectStatement( - resolvedEngine, table, [], distinct, false); + resolvedEngine, table, [], distinct, false, includeJoinedTableColumns); } /// Starts a [DeleteStatement] that can be used to delete rows from a table. diff --git a/drift/lib/src/runtime/query_builder/components/join.dart b/drift/lib/src/runtime/query_builder/components/join.dart index 9b6405ec..8fb3bcbf 100644 --- a/drift/lib/src/runtime/query_builder/components/join.dart +++ b/drift/lib/src/runtime/query_builder/components/join.dart @@ -34,10 +34,12 @@ class Join extends Component { final Expression? 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]. diff --git a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart index 4af4525d..2ba86d40 100644 --- a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart +++ b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart @@ -12,13 +12,16 @@ class JoinedSelectStatement /// instead. JoinedSelectStatement(DatabaseConnectionUser database, ResultSetImplementation 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 _joins; /// All columns that we're selecting from. @@ -43,7 +46,7 @@ class JoinedSelectStatement @override int get _returnedColumnCount { return _joins.fold(_selectedColumns.length, (prev, join) { - if (join.includeInResult) { + if (join.includeInResult ?? _includeJoinedTablesInResult) { return prev + (join.table as ResultSetImplementation).$columns.length; } return prev; @@ -61,7 +64,8 @@ class JoinedSelectStatement } for (final join in _joins) { - if (onlyResults && !join.includeInResult) continue; + if (onlyResults && + !(join.includeInResult ?? _includeJoinedTablesInResult)) continue; yield join.table as ResultSetImplementation; } diff --git a/drift_dev/lib/src/writer/tables/view_writer.dart b/drift_dev/lib/src/writer/tables/view_writer.dart index 6c9a4a47..c736853b 100644 --- a/drift_dev/lib/src/writer/tables/view_writer.dart +++ b/drift_dev/lib/src/writer/tables/view_writer.dart @@ -101,7 +101,8 @@ class ViewWriter extends TableOrViewWriter { buffer.write('@override\nQuery? get query => '); final query = view.viewQuery; if (query != null) { - buffer.write('(_db.selectOnly(${query.from})..addColumns(\$columns))' + buffer.write('(_db.selectOnly(${query.from}, ' + 'includeJoinedTableColumns: false)..addColumns(\$columns))' '${query.query};'); } else { buffer.write('null;\n'); From f3e9fe56da138df039cc10570db90b3d1c7bd564 Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 30 Nov 2021 20:42:20 +0100 Subject: [PATCH 11/22] Fix parsing view references --- .../lib/src/analyzer/dart/view_parser.dart | 48 +++++++++++++------ drift_dev/lib/src/model/column.dart | 3 -- drift_dev/lib/src/model/view.dart | 2 +- .../src/writer/tables/data_class_writer.dart | 2 +- .../lib/src/writer/tables/view_writer.dart | 6 +-- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/drift_dev/lib/src/analyzer/dart/view_parser.dart b/drift_dev/lib/src/analyzer/dart/view_parser.dart index f24383c0..7681d7db 100644 --- a/drift_dev/lib/src/analyzer/dart/view_parser.dart +++ b/drift_dev/lib/src/analyzer/dart/view_parser.dart @@ -13,7 +13,7 @@ class ViewParser { final staticReferences = (await _parseStaticReferences(element, tables)).toList(); final dataClassInfo = _readDataClassInformation(columns, element); - final query = await _parseQuery(element, tables, columns); + final query = await _parseQuery(element, staticReferences, columns); final view = MoorView( declaration: DartViewDeclaration(element, base.step.file), @@ -21,7 +21,7 @@ class ViewParser { dartTypeName: dataClassInfo.enforcedName, existingRowClass: dataClassInfo.existingClass, entityInfoName: '\$${element.name}View', - staticReferences: staticReferences, + staticReferences: staticReferences.map((ref) => ref.declaration).toList(), viewQuery: query, ); @@ -163,18 +163,18 @@ class ViewParser { }[name]; } - Future> _parseStaticReferences( + Future> _parseStaticReferences( ClassElement element, List 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() + .cast<_TableReference>() .toList(); } - Future _getStaticReference( + Future<_TableReference?> _getStaticReference( FieldElement field, List tables) async { if (field.getter != null) { try { @@ -184,8 +184,9 @@ class ViewParser { (tbl) => tbl.fromClass!.name == node.returnType.toString()); if (type != null) { final name = node.name.toString(); - return '${type.entityInfoName} get $name => ' + final declaration = '${type.entityInfoName} get $name => ' '_db.${type.dbGetterName};'; + return _TableReference(type, name, declaration); } } } catch (_) {} @@ -194,7 +195,7 @@ class ViewParser { } Future _parseQuery(ClassElement element, - List tables, List columns) async { + List<_TableReference> references, List columns) async { final as = element.methods.where((method) => method.name == 'as').firstOrNull; @@ -225,15 +226,26 @@ class ViewParser { columnListLiteral.elements.map((col) => col.toString()).map((col) { final parts = col.split('.'); if (parts.length > 1) { - final table = - tables.firstWhere((tbl) => tbl.dbGetterName == parts[0]); - final column = table.columns + 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 = table; - return column; + column.table = reference.table; + return MapEntry( + '${reference.name}.${column.dartGetterName}', column); } - return columns.firstWhere((col) => col.dartGetterName == parts[0]); + 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') { @@ -252,7 +264,7 @@ class ViewParser { query = target.toString().substring(target.target!.toString().length); } - return ViewQueryInformation(columnList.toList(), from, query); + return ViewQueryInformation(columnMap, from, query); } catch (e) { print(e); throw analysisError( @@ -263,3 +275,11 @@ class ViewParser { throw analysisError(base.step, element, 'Missing `as()` query declaration'); } } + +class _TableReference { + MoorTable table; + String name; + String declaration; + + _TableReference(this.table, this.name, this.declaration); +} diff --git a/drift_dev/lib/src/model/column.dart b/drift_dev/lib/src/model/column.dart index cd82ef01..f5f5edaf 100644 --- a/drift_dev/lib/src/model/column.dart +++ b/drift_dev/lib/src/model/column.dart @@ -51,9 +51,6 @@ class MoorColumn implements HasDeclaration, HasType { /// and in the generated data class that will be generated for each table. final String dartGetterName; - String get getterNameWithTable => - table == null ? dartGetterName : '${table!.dbGetterName}.$dartGetterName'; - /// The declaration of this column, contains information about where this /// column was created in source code. @override diff --git a/drift_dev/lib/src/model/view.dart b/drift_dev/lib/src/model/view.dart index 80da182c..970f0442 100644 --- a/drift_dev/lib/src/model/view.dart +++ b/drift_dev/lib/src/model/view.dart @@ -102,7 +102,7 @@ class MoorView extends MoorEntityWithResultSet { } class ViewQueryInformation { - final List columns; + final Map columns; final String from; final String query; diff --git a/drift_dev/lib/src/writer/tables/data_class_writer.dart b/drift_dev/lib/src/writer/tables/data_class_writer.dart index 9b5c12d6..bd3e0499 100644 --- a/drift_dev/lib/src/writer/tables/data_class_writer.dart +++ b/drift_dev/lib/src/writer/tables/data_class_writer.dart @@ -36,7 +36,7 @@ class DataClassWriter { // write view columns final view = table; if (view is MoorView && view.viewQuery != null) { - columns.addAll(view.viewQuery!.columns); + columns.addAll(view.viewQuery!.columns.values); } else { columns.addAll(table.columns); } diff --git a/drift_dev/lib/src/writer/tables/view_writer.dart b/drift_dev/lib/src/writer/tables/view_writer.dart index c736853b..90785143 100644 --- a/drift_dev/lib/src/writer/tables/view_writer.dart +++ b/drift_dev/lib/src/writer/tables/view_writer.dart @@ -54,9 +54,7 @@ class ViewWriter extends TableOrViewWriter { if (view.viewQuery == null) { writeGetColumnsOverride(); } else { - final columns = view.viewQuery!.columns - .map((col) => col.getterNameWithTable) - .join(', '); + final columns = view.viewQuery!.columns.keys.join(', '); buffer.write('@override\nList get \$columns => ' '[$columns];\n'); } @@ -77,7 +75,7 @@ class ViewWriter extends TableOrViewWriter { writeAsDslTable(); writeMappingMethod(scope); - for (final column in view.viewQuery?.columns ?? view.columns) { + for (final column in view.viewQuery?.columns.values ?? view.columns) { writeColumnGetter(column, scope.generationOptions, false); } From 823e06494df2784861d4b5b02d893beb6190d3b1 Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 30 Nov 2021 21:27:19 +0100 Subject: [PATCH 12/22] Add drop view to migration --- .../lib/src/runtime/query_builder/migration.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/drift/lib/src/runtime/query_builder/migration.dart b/drift/lib/src/runtime/query_builder/migration.dart index 8a7598ab..303485e5 100644 --- a/drift/lib/src/runtime/query_builder/migration.dart +++ b/drift/lib/src/runtime/query_builder/migration.dart @@ -74,11 +74,22 @@ class Migrator { } else if (entity is OnCreateQuery) { await _issueCustomQuery(entity.sql, const []); } else if (entity is ViewInfo) { - await createView(entity); + // Skip } else { throw AssertionError('Unknown entity: $entity'); } } + // Create views after all other entities are created + await recreateAllViews(); + } + + /// Drop and recreate all views. You should call it on every upgrade + Future recreateAllViews() async { + for (final entity in _db.allSchemaEntities) { + if (entity is ViewInfo) { + await createView(entity); + } + } } GenerationContext _createContext() { @@ -314,6 +325,8 @@ class Migrator { context.generatingForView = view.entityName; context.buffer.write('CREATE VIEW ${view.entityName} AS '); view.query!.writeInto(context); + await _issueCustomQuery( + 'DROP VIEW IF EXISTS ${view.entityName}', const []); await _issueCustomQuery(context.sql, const []); } } From bda374ff7e02e7a199f3aeb9aab8a3151289e806 Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 30 Nov 2021 21:31:26 +0100 Subject: [PATCH 13/22] Add view schema test --- drift/example/main.dart | 6 +- drift/example/main.g.dart | 15 +- drift/test/data/tables/todos.dart | 26 ++++ drift/test/data/tables/todos.g.dart | 217 +++++++++++++++++++++++++++- drift/test/schema_test.dart | 33 +++++ 5 files changed, 286 insertions(+), 11 deletions(-) diff --git a/drift/example/main.dart b/drift/example/main.dart index 7fafbbd5..e61aec4c 100644 --- a/drift/example/main.dart +++ b/drift/example/main.dart @@ -32,8 +32,7 @@ abstract class TodoCategoryItemCount extends View { todoCategories.name, itemCount, ]).from(todoCategories).join([ - innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id), - useColumns: false) + innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id)) ]); } @@ -51,8 +50,7 @@ abstract class TodoItemWithCategoryNameView extends View { @override Query as() => select([todoItems.id, title]).from(todoItems).join([ innerJoin( - todoCategories, todoCategories.id.equalsExp(todoItems.categoryId), - useColumns: false) + todoCategories, todoCategories.id.equalsExp(todoItems.categoryId)) ]); } diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index 6bed600e..7eb9f182 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -561,9 +561,10 @@ class $TodoCategoryItemCountView @override Query? get query => - (_db.selectOnly(todoCategories)..addColumns($columns)).join([ - innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id), - useColumns: false) + (_db.selectOnly(todoCategories, includeJoinedTableColumns: false) + ..addColumns($columns)) + .join([ + innerJoin(todoItems, todoItems.categoryId.equalsExp(todoCategories.id)) ]); } @@ -670,10 +671,12 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo< } @override - Query? get query => (_db.selectOnly(todoItems)..addColumns($columns)).join([ + Query? get query => + (_db.selectOnly(todoItems, includeJoinedTableColumns: false) + ..addColumns($columns)) + .join([ innerJoin( - todoCategories, todoCategories.id.equalsExp(todoItems.categoryId), - useColumns: false) + todoCategories, todoCategories.id.equalsExp(todoItems.categoryId)) ]); } diff --git a/drift/test/data/tables/todos.dart b/drift/test/data/tables/todos.dart index c8b9a5b2..acfe4530 100644 --- a/drift/test/data/tables/todos.dart +++ b/drift/test/data/tables/todos.dart @@ -117,6 +117,28 @@ class CustomConverter extends TypeConverter { } } +abstract class CategoryTodoCountView extends View { + TodosTable get todos; + Categories get categories; + + Expression 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 { TableWithoutPK, PureDefaults, ], + views: [ + CategoryTodoCountView, + TodoWithCategoryView, + ], daos: [SomeDao], queries: { 'allTodosWithCategory': 'SELECT t.*, c.id as catId, c."desc" as catDesc ' diff --git a/drift/test/data/tables/todos.g.dart b/drift/test/data/tables/todos.g.dart index c2882629..33d2a56e 100644 --- a/drift/test/data/tables/todos.g.dart +++ b/drift/test/data/tables/todos.g.dart @@ -1326,6 +1326,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 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 json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CategoryTodoCountViewData( + description: serializer.fromJson(json['description']), + itemCount: serializer.fromJson(json['itemCount']), + ); + } + factory CategoryTodoCountViewData.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + CategoryTodoCountViewData.fromJson( + DataClass.parseJson(encodedJson) as Map, + serializer: serializer); + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'description': serializer.toJson(description), + 'itemCount': serializer.toJson(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 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 data, + {String? tablePrefix}) { + return CategoryTodoCountViewData.fromData(data, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + late final GeneratedColumn description = GeneratedColumn( + 'desc', aliasedName, false, + type: const StringType(), $customConstraints: 'NOT NULL UNIQUE'); + late final GeneratedColumn itemCount = GeneratedColumn( + '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 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 json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoWithCategoryViewData( + title: serializer.fromJson(json['title']), + description: serializer.fromJson(json['description']), + ); + } + factory TodoWithCategoryViewData.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + TodoWithCategoryViewData.fromJson( + DataClass.parseJson(encodedJson) as Map, + serializer: serializer); + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'title': serializer.toJson(title), + 'description': serializer.toJson(description), + }; + } + + TodoWithCategoryViewData copyWith( + {Value 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 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 data, + {String? tablePrefix}) { + return TodoWithCategoryViewData.fromData(data, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + late final GeneratedColumn title = GeneratedColumn( + 'title', aliasedName, true, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 4, maxTextLength: 16), + type: const StringType()); + late final GeneratedColumn description = GeneratedColumn( + '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 +1544,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 allTodosWithCategory() { return customSelect( @@ -1412,7 +1625,9 @@ abstract class _$TodoDb extends GeneratedDatabase { users, sharedTodos, tableWithoutPK, - pureDefaults + pureDefaults, + categoryTodoCountView, + todoWithCategoryView ]; } diff --git a/drift/test/schema_test.dart b/drift/test/schema_test.dart index 999c2f99..34ab507b 100644 --- a/drift/test/schema_test.dart +++ b/drift/test/schema_test.dart @@ -64,6 +64,39 @@ void main() { 'custom TEXT NOT NULL' ');', [])); + + verify(mockExecutor + .runCustom('DROP VIEW IF EXISTS category_todo_count_view', [])); + + verify(mockExecutor.runCustom( + 'CREATE VIEW category_todo_count_view AS SELECT ' + 'todos.id AS "todos.id", todos.title AS "todos.title", ' + 'todos.content AS "todos.content", ' + 'todos.target_date AS "todos.target_date", ' + 'todos.category AS "todos.category", ' + 'categories."desc" AS "categories.desc", ' + 'COUNT(todos.id) AS "item_count" ' + 'FROM categories ' + 'INNER JOIN todos ' + 'ON todos.category = categories.id', + [])); + + verify(mockExecutor + .runCustom('DROP VIEW IF EXISTS todo_with_category_view', [])); + + verify(mockExecutor.runCustom( + 'CREATE VIEW todo_with_category_view AS SELECT ' + 'categories.id AS "categories.id", ' + 'categories."desc" AS "categories.desc", ' + 'categories.priority AS "categories.priority", ' + 'categories.description_in_upper_case ' + 'AS "categories.description_in_upper_case", ' + '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 { From 3e9a920a4f434292ec36cff7d4d899af208c7f24 Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 30 Nov 2021 22:23:35 +0100 Subject: [PATCH 14/22] Regenerate files --- drift/example/main.g.dart | 7 +++++++ drift/test/data/tables/todos.g.dart | 20 +++++++++++++++++++ .../benchmarks/lib/src/moor/database.g.dart | 2 ++ .../tests/lib/database/database.g.dart | 8 ++++++++ extras/migrations_example/lib/database.g.dart | 2 ++ 5 files changed, 39 insertions(+) diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index 7eb9f182..cf0b9d37 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -134,12 +134,14 @@ class $TodoCategoriesTable extends TodoCategories final String? _alias; $TodoCategoriesTable(this._db, [this._alias]); final VerificationMeta _idMeta = const VerificationMeta('id'); + @override late final GeneratedColumn id = GeneratedColumn( 'id', aliasedName, false, type: const IntType(), requiredDuringInsert: false, defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); final VerificationMeta _nameMeta = const VerificationMeta('name'); + @override late final GeneratedColumn name = GeneratedColumn( 'name', aliasedName, false, type: const StringType(), requiredDuringInsert: true); @@ -379,20 +381,24 @@ class $TodoItemsTable extends TodoItems final String? _alias; $TodoItemsTable(this._db, [this._alias]); final VerificationMeta _idMeta = const VerificationMeta('id'); + @override late final GeneratedColumn id = GeneratedColumn( 'id', aliasedName, false, type: const IntType(), requiredDuringInsert: false, defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); final VerificationMeta _titleMeta = const VerificationMeta('title'); + @override late final GeneratedColumn title = GeneratedColumn( 'title', aliasedName, false, type: const StringType(), requiredDuringInsert: true); final VerificationMeta _contentMeta = const VerificationMeta('content'); + @override late final GeneratedColumn content = GeneratedColumn( 'content', aliasedName, true, type: const StringType(), requiredDuringInsert: false); final VerificationMeta _categoryIdMeta = const VerificationMeta('categoryId'); + @override late final GeneratedColumn categoryId = GeneratedColumn( 'category_id', aliasedName, false, type: const IntType(), @@ -400,6 +406,7 @@ class $TodoItemsTable extends TodoItems defaultConstraints: 'REFERENCES todo_categories (id)'); final VerificationMeta _generatedTextMeta = const VerificationMeta('generatedText'); + @override late final GeneratedColumn generatedText = GeneratedColumn( 'generated_text', aliasedName, true, type: const StringType(), diff --git a/drift/test/data/tables/todos.g.dart b/drift/test/data/tables/todos.g.dart index 33d2a56e..f7291aff 100644 --- a/drift/test/data/tables/todos.g.dart +++ b/drift/test/data/tables/todos.g.dart @@ -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 id = GeneratedColumn( '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 description = GeneratedColumn( 'desc', aliasedName, false, type: const StringType(), requiredDuringInsert: true, $customConstraints: 'NOT NULL UNIQUE'); final VerificationMeta _priorityMeta = const VerificationMeta('priority'); + @override late final GeneratedColumnWithTypeConverter priority = GeneratedColumn('priority', aliasedName, false, type: const IntType(), @@ -205,6 +208,7 @@ class $CategoriesTable extends Categories .withConverter($CategoriesTable.$converter0); final VerificationMeta _descriptionInUpperCaseMeta = const VerificationMeta('descriptionInUpperCase'); + @override late final GeneratedColumn descriptionInUpperCase = GeneratedColumn('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 id = GeneratedColumn( 'id', aliasedName, false, type: const IntType(), requiredDuringInsert: false, defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); final VerificationMeta _titleMeta = const VerificationMeta('title'); + @override late final GeneratedColumn title = GeneratedColumn( '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 content = GeneratedColumn( 'content', aliasedName, false, type: const StringType(), requiredDuringInsert: true); final VerificationMeta _targetDateMeta = const VerificationMeta('targetDate'); + @override late final GeneratedColumn targetDate = GeneratedColumn( 'target_date', aliasedName, true, type: const IntType(), requiredDuringInsert: false); final VerificationMeta _categoryMeta = const VerificationMeta('category'); + @override late final GeneratedColumn category = GeneratedColumn( '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 id = GeneratedColumn( 'id', aliasedName, false, type: const IntType(), requiredDuringInsert: false, defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); final VerificationMeta _nameMeta = const VerificationMeta('name'); + @override late final GeneratedColumn name = GeneratedColumn( '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 isAwesome = GeneratedColumn( '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 profilePicture = GeneratedColumn('profile_picture', aliasedName, false, type: const BlobType(), requiredDuringInsert: true); final VerificationMeta _creationTimeMeta = const VerificationMeta('creationTime'); + @override late final GeneratedColumn creationTime = GeneratedColumn('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 todo = GeneratedColumn( 'todo', aliasedName, false, type: const IntType(), requiredDuringInsert: true); final VerificationMeta _userMeta = const VerificationMeta('user'); + @override late final GeneratedColumn user = GeneratedColumn( '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 notReallyAnId = GeneratedColumn( 'not_really_an_id', aliasedName, false, type: const IntType(), requiredDuringInsert: true); final VerificationMeta _someFloatMeta = const VerificationMeta('someFloat'); + @override late final GeneratedColumn someFloat = GeneratedColumn( 'some_float', aliasedName, false, type: const RealType(), requiredDuringInsert: true); final VerificationMeta _customMeta = const VerificationMeta('custom'); + @override late final GeneratedColumnWithTypeConverter custom = GeneratedColumn('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 txt = GeneratedColumn( 'insert', aliasedName, true, type: const StringType(), requiredDuringInsert: false); diff --git a/extras/benchmarks/lib/src/moor/database.g.dart b/extras/benchmarks/lib/src/moor/database.g.dart index fb4e5d6c..e6788d89 100644 --- a/extras/benchmarks/lib/src/moor/database.g.dart +++ b/extras/benchmarks/lib/src/moor/database.g.dart @@ -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 key = GeneratedColumn( 'key', aliasedName, false, type: const StringType(), requiredDuringInsert: true); final VerificationMeta _valueMeta = const VerificationMeta('value'); + @override late final GeneratedColumn value = GeneratedColumn( 'value', aliasedName, false, type: const StringType(), requiredDuringInsert: true); diff --git a/extras/integration_tests/tests/lib/database/database.g.dart b/extras/integration_tests/tests/lib/database/database.g.dart index 36dff255..9cb7ea9e 100644 --- a/extras/integration_tests/tests/lib/database/database.g.dart +++ b/extras/integration_tests/tests/lib/database/database.g.dart @@ -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 id = GeneratedColumn( 'id', aliasedName, false, type: const IntType(), requiredDuringInsert: false, defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); final VerificationMeta _nameMeta = const VerificationMeta('name'); + @override late final GeneratedColumn name = GeneratedColumn( 'name', aliasedName, false, type: const StringType(), requiredDuringInsert: true); final VerificationMeta _birthDateMeta = const VerificationMeta('birthDate'); + @override late final GeneratedColumn birthDate = GeneratedColumn( 'birth_date', aliasedName, false, type: const IntType(), requiredDuringInsert: true); final VerificationMeta _profilePictureMeta = const VerificationMeta('profilePicture'); + @override late final GeneratedColumn profilePicture = GeneratedColumn('profile_picture', aliasedName, true, type: const BlobType(), requiredDuringInsert: false); final VerificationMeta _preferencesMeta = const VerificationMeta('preferences'); + @override late final GeneratedColumnWithTypeConverter preferences = GeneratedColumn('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 firstUser = GeneratedColumn( 'first_user', aliasedName, false, type: const IntType(), requiredDuringInsert: true); final VerificationMeta _secondUserMeta = const VerificationMeta('secondUser'); + @override late final GeneratedColumn secondUser = GeneratedColumn( 'second_user', aliasedName, false, type: const IntType(), requiredDuringInsert: true); final VerificationMeta _reallyGoodFriendsMeta = const VerificationMeta('reallyGoodFriends'); + @override late final GeneratedColumn reallyGoodFriends = GeneratedColumn( 'really_good_friends', aliasedName, false, type: const BoolType(), diff --git a/extras/migrations_example/lib/database.g.dart b/extras/migrations_example/lib/database.g.dart index 93218352..7bf0f7ee 100644 --- a/extras/migrations_example/lib/database.g.dart +++ b/extras/migrations_example/lib/database.g.dart @@ -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 id = GeneratedColumn( 'id', aliasedName, false, type: const IntType(), requiredDuringInsert: false, defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); final VerificationMeta _nameMeta = const VerificationMeta('name'); + @override late final GeneratedColumn name = GeneratedColumn( 'name', aliasedName, false, type: const StringType(), From 37de03085c54c094f124795151bd8056001709f7 Mon Sep 17 00:00:00 2001 From: westito Date: Tue, 30 Nov 2021 22:24:16 +0100 Subject: [PATCH 15/22] Modify cancellation switchMap test --- drift/test/integration_tests/cancellation_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drift/test/integration_tests/cancellation_test.dart b/drift/test/integration_tests/cancellation_test.dart index 3abc2cfb..52f118ca 100644 --- a/drift/test/integration_tests/cancellation_test.dart +++ b/drift/test/integration_tests/cancellation_test.dart @@ -105,7 +105,7 @@ void main() { for (var i = 0; i < 4; i++) { filter.add(i); - await pumpEventQueue(times: 5); + await pumpEventQueue(times: 10); } final values = await db From 5fcdbed46fc2c2c002d6817d96a456b296c951ea Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 1 Dec 2021 17:05:58 +0100 Subject: [PATCH 16/22] Add docs --- .../Getting started/advanced_dart_tables.md | 63 +++++++++++++++++++ drift/lib/src/dsl/database.dart | 2 +- drift/lib/src/dsl/table.dart | 12 +--- .../query_builder/schema/view_info.dart | 4 +- drift_dev/pubspec.yaml | 2 +- 5 files changed, 69 insertions(+), 14 deletions(-) diff --git a/docs/pages/docs/Getting started/advanced_dart_tables.md b/docs/pages/docs/Getting started/advanced_dart_tables.md index 37b4592c..a68dccc2 100644 --- a/docs/pages/docs/Getting started/advanced_dart_tables.md +++ b/docs/pages/docs/Getting started/advanced_dart_tables.md @@ -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 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 { +``` diff --git a/drift/lib/src/dsl/database.dart b/drift/lib/src/dsl/database.dart index 2163fee9..babb6ac1 100644 --- a/drift/lib/src/dsl/database.dart +++ b/drift/lib/src/dsl/database.dart @@ -95,7 +95,7 @@ class DriftAccessor { /// The tables accessed by this DAO. final List tables; - /// The views to include in the database + /// The views to make accessible in this DAO. final List views; /// {@macro drift_compile_queries_param} diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index 9eba7c86..620e59c7 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -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(); @@ -210,13 +212,3 @@ class DriftView { /// Customize view name and data class name const DriftView({this.name, this.dataClassName}); } - -/// -@Target({TargetKind.getter}) -class Reference { - /// - final Type type; - - /// - const Reference(this.type); -} diff --git a/drift/lib/src/runtime/query_builder/schema/view_info.dart b/drift/lib/src/runtime/query_builder/schema/view_info.dart index 9043b8b3..a976d9de 100644 --- a/drift/lib/src/runtime/query_builder/schema/view_info.dart +++ b/drift/lib/src/runtime/query_builder/schema/view_info.dart @@ -9,7 +9,7 @@ part of '../query_builder.dart'; /// /// [sqlite-docs]: https://www.sqlite.org/lang_createview.html /// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/ -abstract class ViewInfo +abstract class ViewInfo implements ResultSetImplementation { @override String get entityName; @@ -17,6 +17,6 @@ abstract class ViewInfo /// The `CREATE VIEW` sql statement that can be used to create this view. String? get createViewStmt; - /// Predefined query from View.as() + /// Predefined query from `View.as()` Query? get query; } diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index 29f8dcc1..cfcdbbe4 100644 --- a/drift_dev/pubspec.yaml +++ b/drift_dev/pubspec.yaml @@ -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 From 8f7788ae8f4c00d9b934bb891c330d3aea3141b5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 1 Dec 2021 17:09:56 +0100 Subject: [PATCH 17/22] More documentation --- drift/lib/src/dsl/table.dart | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index 620e59c7..c0ed6752 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -121,19 +121,41 @@ abstract class Table extends HasResultSet { } /// 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 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 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(); } From f383eef2ee8b9c209cd96264022977d6b7c84cd6 Mon Sep 17 00:00:00 2001 From: westito Date: Thu, 2 Dec 2021 08:54:59 +0100 Subject: [PATCH 18/22] Remove drop view --- drift/lib/src/runtime/query_builder/migration.dart | 8 +++----- drift/test/schema_test.dart | 6 ------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/migration.dart b/drift/lib/src/runtime/query_builder/migration.dart index 303485e5..26619ad1 100644 --- a/drift/lib/src/runtime/query_builder/migration.dart +++ b/drift/lib/src/runtime/query_builder/migration.dart @@ -74,19 +74,19 @@ class Migrator { } else if (entity is OnCreateQuery) { await _issueCustomQuery(entity.sql, const []); } else if (entity is ViewInfo) { - // Skip + await createView(entity); } else { throw AssertionError('Unknown entity: $entity'); } } - // Create views after all other entities are created - await recreateAllViews(); } /// Drop and recreate all views. You should call it on every upgrade Future recreateAllViews() async { for (final entity in _db.allSchemaEntities) { if (entity is ViewInfo) { + await _issueCustomQuery( + 'DROP VIEW IF EXISTS ${entity.entityName}', const []); await createView(entity); } } @@ -325,8 +325,6 @@ class Migrator { context.generatingForView = view.entityName; context.buffer.write('CREATE VIEW ${view.entityName} AS '); view.query!.writeInto(context); - await _issueCustomQuery( - 'DROP VIEW IF EXISTS ${view.entityName}', const []); await _issueCustomQuery(context.sql, const []); } } diff --git a/drift/test/schema_test.dart b/drift/test/schema_test.dart index 34ab507b..d641bb64 100644 --- a/drift/test/schema_test.dart +++ b/drift/test/schema_test.dart @@ -65,9 +65,6 @@ void main() { ');', [])); - verify(mockExecutor - .runCustom('DROP VIEW IF EXISTS category_todo_count_view', [])); - verify(mockExecutor.runCustom( 'CREATE VIEW category_todo_count_view AS SELECT ' 'todos.id AS "todos.id", todos.title AS "todos.title", ' @@ -81,9 +78,6 @@ void main() { 'ON todos.category = categories.id', [])); - verify(mockExecutor - .runCustom('DROP VIEW IF EXISTS todo_with_category_view', [])); - verify(mockExecutor.runCustom( 'CREATE VIEW todo_with_category_view AS SELECT ' 'categories.id AS "categories.id", ' From ffe24bb17a629255b6208704aa0f9979c61cb1b7 Mon Sep 17 00:00:00 2001 From: westito Date: Thu, 2 Dec 2021 09:04:17 +0100 Subject: [PATCH 19/22] Fix includeJoinedTableColumns and add test --- .../query_builder/components/join.dart | 3 +- drift/test/join_test.dart | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/components/join.dart b/drift/lib/src/runtime/query_builder/components/join.dart index 8fb3bcbf..cf3925f8 100644 --- a/drift/lib/src/runtime/query_builder/components/join.dart +++ b/drift/lib/src/runtime/query_builder/components/join.dart @@ -43,8 +43,7 @@ class Join extends Component { /// 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) { throw ArgumentError( 'Invalid table parameter. You must provide the table reference from ' diff --git a/drift/test/join_test.dart b/drift/test/join_test.dart index 83ee7c71..09b4ba2e 100644 --- a/drift/test/join_test.dart +++ b/drift/test/join_test.dart @@ -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')); From fb087078721354683af0e86074fce0b63bed595b Mon Sep 17 00:00:00 2001 From: westito Date: Thu, 2 Dec 2021 20:24:34 +0100 Subject: [PATCH 20/22] Revert ViewInfo generic bound --- drift/lib/src/runtime/query_builder/schema/view_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drift/lib/src/runtime/query_builder/schema/view_info.dart b/drift/lib/src/runtime/query_builder/schema/view_info.dart index a976d9de..7f435366 100644 --- a/drift/lib/src/runtime/query_builder/schema/view_info.dart +++ b/drift/lib/src/runtime/query_builder/schema/view_info.dart @@ -9,7 +9,7 @@ part of '../query_builder.dart'; /// /// [sqlite-docs]: https://www.sqlite.org/lang_createview.html /// [sql-tut]: https://www.sqlitetutorial.net/sqlite-create-view/ -abstract class ViewInfo +abstract class ViewInfo implements ResultSetImplementation { @override String get entityName; From 4fe95bc01a045ffb0271b89dfad5d63951ee9b53 Mon Sep 17 00:00:00 2001 From: westito Date: Thu, 2 Dec 2021 20:24:46 +0100 Subject: [PATCH 21/22] Fix view schema test --- drift/test/schema_test.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/drift/test/schema_test.dart b/drift/test/schema_test.dart index d641bb64..cee45c10 100644 --- a/drift/test/schema_test.dart +++ b/drift/test/schema_test.dart @@ -67,10 +67,6 @@ void main() { verify(mockExecutor.runCustom( 'CREATE VIEW category_todo_count_view AS SELECT ' - 'todos.id AS "todos.id", todos.title AS "todos.title", ' - 'todos.content AS "todos.content", ' - 'todos.target_date AS "todos.target_date", ' - 'todos.category AS "todos.category", ' 'categories."desc" AS "categories.desc", ' 'COUNT(todos.id) AS "item_count" ' 'FROM categories ' @@ -80,11 +76,6 @@ void main() { verify(mockExecutor.runCustom( 'CREATE VIEW todo_with_category_view AS SELECT ' - 'categories.id AS "categories.id", ' - 'categories."desc" AS "categories.desc", ' - 'categories.priority AS "categories.priority", ' - 'categories.description_in_upper_case ' - 'AS "categories.description_in_upper_case", ' 'todos.title AS "todos.title", ' 'categories."desc" AS "categories.desc" ' 'FROM todos ' From aa929690b79d29b1a5d4de1af0e4cf090275be5f Mon Sep 17 00:00:00 2001 From: westito Date: Thu, 2 Dec 2021 20:27:44 +0100 Subject: [PATCH 22/22] Modify cancellation test --- drift/test/integration_tests/cancellation_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drift/test/integration_tests/cancellation_test.dart b/drift/test/integration_tests/cancellation_test.dart index 52f118ca..52908006 100644 --- a/drift/test/integration_tests/cancellation_test.dart +++ b/drift/test/integration_tests/cancellation_test.dart @@ -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('r')) .getSingle(); - expect(values, anyOf('0,3', '3')); + expect(values, anyOf('0,3', '3', '1,3')); }); }