From c5504237d5b4daf47070768a45620ec4f067c72a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 4 Nov 2022 13:42:06 +0100 Subject: [PATCH] Make drift's tests pass with new analyzer --- drift/example/main.g.dart | 348 ++++++------ .../test/database/statements/schema_test.dart | 8 +- drift/test/generated/custom_tables.g.dart | 92 +++- drift/test/generated/todos.g.dart | 514 +++++++++--------- .../integration_tests/drift_files_test.dart | 6 +- .../lib/src/analysis/custom_result_class.dart | 113 ++++ .../src/analysis/resolver/drift/table.dart | 18 + .../src/analysis/resolver/file_analysis.dart | 3 +- .../src/analysis/results/file_results.dart | 2 +- .../sql_queries/custom_result_class.dart | 127 ----- .../lib/src/backends/build/drift_builder.dart | 14 +- .../services/find_stream_update_rules.dart | 5 +- .../src/utils/entity_reference_sorter.dart | 93 ++-- drift_dev/lib/src/writer/database_writer.dart | 8 +- 14 files changed, 728 insertions(+), 623 deletions(-) create mode 100644 drift_dev/lib/src/analysis/custom_result_class.dart delete mode 100644 drift_dev/lib/src/analyzer/sql_queries/custom_result_class.dart diff --git a/drift/example/main.g.dart b/drift/example/main.g.dart index 1581f5a6..4cc5ab89 100644 --- a/drift/example/main.g.dart +++ b/drift/example/main.g.dart @@ -6,6 +6,178 @@ part of 'main.dart'; // DriftElementId(asset:drift/example/main.dart, todo_items) // DriftElementId(asset:drift/example/main.dart, todo_categories) // DriftElementId(asset:drift/example/main.dart, customViewName) +class TodoCategory extends DataClass implements Insertable { + final int id; + final String name; + const TodoCategory({required this.id, required this.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 TodoCategory.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoCategory( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + ); + } + factory TodoCategory.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + TodoCategory.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), + }; + } + + TodoCategory copyWith({int? id, String? name}) => TodoCategory( + id: id ?? this.id, + name: name ?? this.name, + ); + @override + String toString() { + return (StringBuffer('TodoCategory(') + ..write('id: $id, ') + ..write('name: $name') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodoCategory && other.id == this.id && other.name == this.name); +} + +class TodoCategoriesCompanion extends UpdateCompanion { + 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, TodoCategory> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TodoCategoriesTable(this.attachedDatabase, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); + final VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, 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 + TodoCategory map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodoCategory( + id: attachedDatabase.options.types + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.options.types + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + ); + } + + @override + $TodoCategoriesTable createAlias(String alias) { + return $TodoCategoriesTable(attachedDatabase, alias); + } +} + class TodoItem extends DataClass implements Insertable { final int id; final String title; @@ -290,178 +462,6 @@ class $TodoItemsTable extends TodoItems } } -class TodoCategory extends DataClass implements Insertable { - final int id; - final String name; - const TodoCategory({required this.id, required this.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 TodoCategory.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return TodoCategory( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - ); - } - factory TodoCategory.fromJsonString(String encodedJson, - {ValueSerializer? serializer}) => - TodoCategory.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), - }; - } - - TodoCategory copyWith({int? id, String? name}) => TodoCategory( - id: id ?? this.id, - name: name ?? this.name, - ); - @override - String toString() { - return (StringBuffer('TodoCategory(') - ..write('id: $id, ') - ..write('name: $name') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is TodoCategory && other.id == this.id && other.name == this.name); -} - -class TodoCategoriesCompanion extends UpdateCompanion { - 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, TodoCategory> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $TodoCategoriesTable(this.attachedDatabase, [this._alias]); - final VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); - final VerificationMeta _nameMeta = const VerificationMeta('name'); - @override - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, 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 - TodoCategory map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return TodoCategory( - id: attachedDatabase.options.types - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.options.types - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - ); - } - - @override - $TodoCategoriesTable createAlias(String alias) { - return $TodoCategoriesTable(attachedDatabase, alias); - } -} - class TodoCategoryItemCountData extends DataClass { final String name; final int? itemCount; @@ -680,8 +680,8 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo< abstract class _$Database extends GeneratedDatabase { _$Database(QueryExecutor e) : super(e); _$Database.connect(DatabaseConnection c) : super.connect(c); - late final $TodoItemsTable todoItems = $TodoItemsTable(this); late final $TodoCategoriesTable todoCategories = $TodoCategoriesTable(this); + late final $TodoItemsTable todoItems = $TodoItemsTable(this); late final $TodoCategoryItemCountView todoCategoryItemCount = $TodoCategoryItemCountView(this); late final $TodoItemWithCategoryNameViewView customViewName = @@ -691,5 +691,5 @@ abstract class _$Database extends GeneratedDatabase { allSchemaEntities.whereType>(); @override List get allSchemaEntities => - [todoItems, todoCategories, todoCategoryItemCount, customViewName]; + [todoCategories, todoItems, todoCategoryItemCount, customViewName]; } diff --git a/drift/test/database/statements/schema_test.dart b/drift/test/database/statements/schema_test.dart index d8bddc0c..abfbeb61 100644 --- a/drift/test/database/statements/schema_test.dart +++ b/drift/test/database/statements/schema_test.dart @@ -23,7 +23,7 @@ void main() { 'CREATE TABLE IF NOT EXISTS "todos" ' '("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" TEXT NULL, ' '"content" TEXT NOT NULL, "target_date" INTEGER NULL UNIQUE, ' - '"category" INTEGER NULL REFERENCES "categories" ("id"), ' + '"category" INTEGER NULL REFERENCES categories (id), ' 'UNIQUE ("title", "category"), UNIQUE ("title", "target_date"));', [])); @@ -41,7 +41,7 @@ void main() { 'CREATE TABLE IF NOT EXISTS "users" (' '"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' '"name" TEXT NOT NULL UNIQUE, ' - '"is_awesome" INTEGER NOT NULL DEFAULT 1 CHECK ("is_awesome" IN (0, 1)), ' + '"is_awesome" INTEGER NOT NULL DEFAULT 1 CHECK (is_awesome IN (0, 1)), ' '"profile_picture" BLOB NOT NULL, ' '"creation_time" INTEGER NOT NULL ' "DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)) " @@ -98,7 +98,7 @@ void main() { 'CREATE TABLE IF NOT EXISTS "users" ' '("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' '"name" TEXT NOT NULL UNIQUE, ' - '"is_awesome" INTEGER NOT NULL DEFAULT 1 CHECK ("is_awesome" IN (0, 1)), ' + '"is_awesome" INTEGER NOT NULL DEFAULT 1 CHECK (is_awesome IN (0, 1)), ' '"profile_picture" BLOB NOT NULL, ' '"creation_time" INTEGER NOT NULL ' "DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)) " @@ -145,7 +145,7 @@ void main() { verify(mockExecutor.runCustom('ALTER TABLE "users" ADD COLUMN ' '"is_awesome" INTEGER NOT NULL DEFAULT 1 ' - 'CHECK ("is_awesome" IN (0, 1));')); + 'CHECK (is_awesome IN (0, 1));')); }); test('renames columns', () async { diff --git a/drift/test/generated/custom_tables.g.dart b/drift/test/generated/custom_tables.g.dart index 7d45f4b7..d71ebc78 100644 --- a/drift/test/generated/custom_tables.g.dart +++ b/drift/test/generated/custom_tables.g.dart @@ -52,7 +52,7 @@ class $NoIdsTable extends Table with TableInfo<$NoIdsTable, NoIdRow> { 'payload', aliasedName, false, type: DriftSqlType.blob, requiredDuringInsert: true, - defaultConstraints: 'PRIMARY KEY'); + $customConstraints: 'NOT NULL PRIMARY KEY'); @override List get $columns => [payload]; @override @@ -92,6 +92,8 @@ class $NoIdsTable extends Table with TableInfo<$NoIdsTable, NoIdRow> { @override bool get withoutRowId => true; @override + List get customConstraints => const []; + @override bool get dontWriteConstraints => true; } @@ -225,13 +227,14 @@ class $WithDefaultsTable extends Table 'a', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, + $customConstraints: 'DEFAULT \'something\'', defaultValue: const CustomExpression('\'something\'')); final VerificationMeta _bMeta = const VerificationMeta('b'); late final GeneratedColumn b = GeneratedColumn( 'b', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: 'UNIQUE'); + $customConstraints: 'UNIQUE'); @override List get $columns => [a, b]; @override @@ -270,6 +273,8 @@ class $WithDefaultsTable extends Table return $WithDefaultsTable(attachedDatabase, alias); } + @override + List get customConstraints => const []; @override bool get dontWriteConstraints => true; } @@ -424,15 +429,21 @@ class $WithConstraintsTable extends Table final VerificationMeta _aMeta = const VerificationMeta('a'); late final GeneratedColumn a = GeneratedColumn( 'a', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); final VerificationMeta _bMeta = const VerificationMeta('b'); late final GeneratedColumn b = GeneratedColumn( 'b', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); final VerificationMeta _cMeta = const VerificationMeta('c'); late final GeneratedColumn c = GeneratedColumn( 'c', aliasedName, true, - type: DriftSqlType.double, requiredDuringInsert: false); + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: ''); @override List get $columns => [a, b, c]; @override @@ -478,6 +489,9 @@ class $WithConstraintsTable extends Table return $WithConstraintsTable(attachedDatabase, alias); } + @override + List get customConstraints => + const ['FOREIGN KEY(a, b)REFERENCES with_defaults(a, b)']; @override bool get dontWriteConstraints => true; } @@ -676,23 +690,29 @@ class $ConfigTable extends Table with TableInfo<$ConfigTable, Config> { 'config_key', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, - defaultConstraints: 'PRIMARY KEY'); + $customConstraints: 'NOT NULL PRIMARY KEY'); final VerificationMeta _configValueMeta = const VerificationMeta('configValue'); late final GeneratedColumn configValue = GeneratedColumn( 'config_value', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); final VerificationMeta _syncStateMeta = const VerificationMeta('syncState'); late final GeneratedColumnWithTypeConverter syncState = GeneratedColumn('sync_state', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false) + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '') .withConverter($ConfigTable.$convertersyncStaten); final VerificationMeta _syncStateImplicitMeta = const VerificationMeta('syncStateImplicit'); late final GeneratedColumnWithTypeConverter syncStateImplicit = GeneratedColumn( 'sync_state_implicit', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false) + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: '') .withConverter($ConfigTable.$convertersyncStateImplicitn); @override List get $columns => @@ -758,6 +778,8 @@ class $ConfigTable extends Table with TableInfo<$ConfigTable, Config> { @override bool get isStrict => true; @override + List get customConstraints => const []; + @override bool get dontWriteConstraints => true; } @@ -941,22 +963,28 @@ class $MytableTable extends Table with TableInfo<$MytableTable, MytableData> { final VerificationMeta _someidMeta = const VerificationMeta('someid'); late final GeneratedColumn someid = GeneratedColumn( 'someid', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false); + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL'); final VerificationMeta _sometextMeta = const VerificationMeta('sometext'); late final GeneratedColumn sometext = GeneratedColumn( 'sometext', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); final VerificationMeta _isInsertingMeta = const VerificationMeta('isInserting'); late final GeneratedColumn isInserting = GeneratedColumn( 'is_inserting', aliasedName, true, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: 'CHECK (is_inserting IN (0, 1))'); + $customConstraints: ''); final VerificationMeta _somedateMeta = const VerificationMeta('somedate'); late final GeneratedColumn somedate = GeneratedColumn( 'somedate', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + $customConstraints: ''); @override List get $columns => [someid, sometext, isInserting, somedate]; @@ -1013,6 +1041,8 @@ class $MytableTable extends Table with TableInfo<$MytableTable, MytableData> { return $MytableTable(attachedDatabase, alias); } + @override + List get customConstraints => const ['PRIMARY KEY(someid DESC)']; @override bool get dontWriteConstraints => true; } @@ -1160,15 +1190,21 @@ class $EmailTable extends Table final VerificationMeta _senderMeta = const VerificationMeta('sender'); late final GeneratedColumn sender = GeneratedColumn( 'sender', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: ''); final VerificationMeta _titleMeta = const VerificationMeta('title'); late final GeneratedColumn title = GeneratedColumn( 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: ''); final VerificationMeta _bodyMeta = const VerificationMeta('body'); late final GeneratedColumn body = GeneratedColumn( 'body', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: ''); @override List get $columns => [sender, title, body]; @override @@ -1221,6 +1257,8 @@ class $EmailTable extends Table return $EmailTable(attachedDatabase, alias); } + @override + List get customConstraints => const []; @override bool get dontWriteConstraints => true; @override @@ -1352,11 +1390,15 @@ class $WeirdTableTable extends Table final VerificationMeta _sqlClassMeta = const VerificationMeta('sqlClass'); late final GeneratedColumn sqlClass = GeneratedColumn( 'class', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); final VerificationMeta _textColumnMeta = const VerificationMeta('textColumn'); late final GeneratedColumn textColumn = GeneratedColumn( 'text', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); @override List get $columns => [sqlClass, textColumn]; @override @@ -1401,6 +1443,8 @@ class $WeirdTableTable extends Table return $WeirdTableTable(attachedDatabase, alias); } + @override + List get customConstraints => const []; @override bool get dontWriteConstraints => true; } @@ -1815,6 +1859,18 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { 'INSERT INTO config (config_key, config_value) VALUES (\'key\', \'values\')') ]; @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( + [ + WritePropagation( + on: TableUpdateQuery.onTableName('config', + limitUpdateKind: UpdateKind.insert), + result: [ + TableUpdate('with_defaults', kind: UpdateKind.insert), + ], + ), + ], + ); + @override DriftDatabaseOptions get options => const DriftDatabaseOptions(storeDateTimeAsText: true); } diff --git a/drift/test/generated/todos.g.dart b/drift/test/generated/todos.g.dart index cde32f52..3243d14f 100644 --- a/drift/test/generated/todos.g.dart +++ b/drift/test/generated/todos.g.dart @@ -6,6 +6,260 @@ part of 'todos.dart'; // DriftElementId(asset:drift/test/generated/todos.dart, todos) // DriftElementId(asset:drift/test/generated/todos.dart, categories) // DriftElementId(asset:drift/test/generated/todos.dart, todo_with_category_view) +class Category extends DataClass implements Insertable { + final int id; + final String description; + final CategoryPriority priority; + final String descriptionInUpperCase; + const Category( + {required this.id, + required this.description, + required this.priority, + required this.descriptionInUpperCase}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['desc'] = Variable(description); + { + final converter = $CategoriesTable.$converterpriority; + map['priority'] = Variable(converter.toSql(priority)); + } + return map; + } + + CategoriesCompanion toCompanion(bool nullToAbsent) { + return CategoriesCompanion( + id: Value(id), + description: Value(description), + priority: Value(priority), + ); + } + + factory Category.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Category( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + priority: serializer.fromJson(json['priority']), + descriptionInUpperCase: + serializer.fromJson(json['descriptionInUpperCase']), + ); + } + factory Category.fromJsonString(String encodedJson, + {ValueSerializer? serializer}) => + Category.fromJson( + DataClass.parseJson(encodedJson) as Map, + serializer: serializer); + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + 'priority': serializer.toJson(priority), + 'descriptionInUpperCase': + serializer.toJson(descriptionInUpperCase), + }; + } + + Category copyWith( + {int? id, + String? description, + CategoryPriority? priority, + String? descriptionInUpperCase}) => + Category( + id: id ?? this.id, + description: description ?? this.description, + priority: priority ?? this.priority, + descriptionInUpperCase: + descriptionInUpperCase ?? this.descriptionInUpperCase, + ); + @override + String toString() { + return (StringBuffer('Category(') + ..write('id: $id, ') + ..write('description: $description, ') + ..write('priority: $priority, ') + ..write('descriptionInUpperCase: $descriptionInUpperCase') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, description, priority, descriptionInUpperCase); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Category && + other.id == this.id && + other.description == this.description && + other.priority == this.priority && + other.descriptionInUpperCase == this.descriptionInUpperCase); +} + +class CategoriesCompanion extends UpdateCompanion { + final Value id; + final Value description; + final Value priority; + const CategoriesCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + this.priority = const Value.absent(), + }); + CategoriesCompanion.insert({ + this.id = const Value.absent(), + required String description, + this.priority = const Value.absent(), + }) : description = Value(description); + static Insertable custom({ + Expression? id, + Expression? description, + Expression? priority, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (description != null) 'desc': description, + if (priority != null) 'priority': priority, + }); + } + + CategoriesCompanion copyWith( + {Value? id, + Value? description, + Value? priority}) { + return CategoriesCompanion( + id: id ?? this.id, + description: description ?? this.description, + priority: priority ?? this.priority, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (description.present) { + map['desc'] = Variable(description.value); + } + if (priority.present) { + final converter = $CategoriesTable.$converterpriority; + map['priority'] = Variable(converter.toSql(priority.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CategoriesCompanion(') + ..write('id: $id, ') + ..write('description: $description, ') + ..write('priority: $priority') + ..write(')')) + .toString(); + } +} + +class $CategoriesTable extends Categories + with TableInfo<$CategoriesTable, Category> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CategoriesTable(this.attachedDatabase, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); + final VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'desc', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL UNIQUE'); + final VerificationMeta _priorityMeta = const VerificationMeta('priority'); + @override + late final GeneratedColumnWithTypeConverter priority = + GeneratedColumn('priority', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)) + .withConverter($CategoriesTable.$converterpriority); + final VerificationMeta _descriptionInUpperCaseMeta = + const VerificationMeta('descriptionInUpperCase'); + @override + late final GeneratedColumn descriptionInUpperCase = + GeneratedColumn('description_in_upper_case', aliasedName, false, + generatedAs: GeneratedAs(description.upper(), false), + type: DriftSqlType.string, + requiredDuringInsert: false); + @override + List get $columns => + [id, description, priority, descriptionInUpperCase]; + @override + String get aliasedName => _alias ?? 'categories'; + @override + String get actualTableName => '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('desc')) { + context.handle(_descriptionMeta, + description.isAcceptableOrUnknown(data['desc']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + context.handle(_priorityMeta, const VerificationResult.success()); + if (data.containsKey('description_in_upper_case')) { + context.handle( + _descriptionInUpperCaseMeta, + descriptionInUpperCase.isAcceptableOrUnknown( + data['description_in_upper_case']!, _descriptionInUpperCaseMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + Category map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Category( + id: attachedDatabase.options.types + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + description: attachedDatabase.options.types + .read(DriftSqlType.string, data['${effectivePrefix}desc'])!, + priority: $CategoriesTable.$converterpriority.fromSql(attachedDatabase + .options.types + .read(DriftSqlType.int, data['${effectivePrefix}priority'])!), + descriptionInUpperCase: attachedDatabase.options.types.read( + DriftSqlType.string, + data['${effectivePrefix}description_in_upper_case'])!, + ); + } + + @override + $CategoriesTable createAlias(String alias) { + return $CategoriesTable(attachedDatabase, alias); + } + + static TypeConverter $converterpriority = + const EnumIndexConverter(CategoryPriority.values); +} + class TodoEntry extends DataClass implements Insertable { final int id; final String? title; @@ -309,260 +563,6 @@ class $TodosTableTable extends TodosTable } } -class Category extends DataClass implements Insertable { - final int id; - final String description; - final CategoryPriority priority; - final String descriptionInUpperCase; - const Category( - {required this.id, - required this.description, - required this.priority, - required this.descriptionInUpperCase}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['desc'] = Variable(description); - { - final converter = $CategoriesTable.$converterpriority; - map['priority'] = Variable(converter.toSql(priority)); - } - return map; - } - - CategoriesCompanion toCompanion(bool nullToAbsent) { - return CategoriesCompanion( - id: Value(id), - description: Value(description), - priority: Value(priority), - ); - } - - factory Category.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return Category( - id: serializer.fromJson(json['id']), - description: serializer.fromJson(json['description']), - priority: serializer.fromJson(json['priority']), - descriptionInUpperCase: - serializer.fromJson(json['descriptionInUpperCase']), - ); - } - factory Category.fromJsonString(String encodedJson, - {ValueSerializer? serializer}) => - Category.fromJson( - DataClass.parseJson(encodedJson) as Map, - serializer: serializer); - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'description': serializer.toJson(description), - 'priority': serializer.toJson(priority), - 'descriptionInUpperCase': - serializer.toJson(descriptionInUpperCase), - }; - } - - Category copyWith( - {int? id, - String? description, - CategoryPriority? priority, - String? descriptionInUpperCase}) => - Category( - id: id ?? this.id, - description: description ?? this.description, - priority: priority ?? this.priority, - descriptionInUpperCase: - descriptionInUpperCase ?? this.descriptionInUpperCase, - ); - @override - String toString() { - return (StringBuffer('Category(') - ..write('id: $id, ') - ..write('description: $description, ') - ..write('priority: $priority, ') - ..write('descriptionInUpperCase: $descriptionInUpperCase') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, description, priority, descriptionInUpperCase); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is Category && - other.id == this.id && - other.description == this.description && - other.priority == this.priority && - other.descriptionInUpperCase == this.descriptionInUpperCase); -} - -class CategoriesCompanion extends UpdateCompanion { - final Value id; - final Value description; - final Value priority; - const CategoriesCompanion({ - this.id = const Value.absent(), - this.description = const Value.absent(), - this.priority = const Value.absent(), - }); - CategoriesCompanion.insert({ - this.id = const Value.absent(), - required String description, - this.priority = const Value.absent(), - }) : description = Value(description); - static Insertable custom({ - Expression? id, - Expression? description, - Expression? priority, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (description != null) 'desc': description, - if (priority != null) 'priority': priority, - }); - } - - CategoriesCompanion copyWith( - {Value? id, - Value? description, - Value? priority}) { - return CategoriesCompanion( - id: id ?? this.id, - description: description ?? this.description, - priority: priority ?? this.priority, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (description.present) { - map['desc'] = Variable(description.value); - } - if (priority.present) { - final converter = $CategoriesTable.$converterpriority; - map['priority'] = Variable(converter.toSql(priority.value)); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CategoriesCompanion(') - ..write('id: $id, ') - ..write('description: $description, ') - ..write('priority: $priority') - ..write(')')) - .toString(); - } -} - -class $CategoriesTable extends Categories - with TableInfo<$CategoriesTable, Category> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $CategoriesTable(this.attachedDatabase, [this._alias]); - final VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); - final VerificationMeta _descriptionMeta = - const VerificationMeta('description'); - @override - late final GeneratedColumn description = GeneratedColumn( - 'desc', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL UNIQUE'); - final VerificationMeta _priorityMeta = const VerificationMeta('priority'); - @override - late final GeneratedColumnWithTypeConverter priority = - GeneratedColumn('priority', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)) - .withConverter($CategoriesTable.$converterpriority); - final VerificationMeta _descriptionInUpperCaseMeta = - const VerificationMeta('descriptionInUpperCase'); - @override - late final GeneratedColumn descriptionInUpperCase = - GeneratedColumn('description_in_upper_case', aliasedName, false, - generatedAs: GeneratedAs(description.upper(), false), - type: DriftSqlType.string, - requiredDuringInsert: false); - @override - List get $columns => - [id, description, priority, descriptionInUpperCase]; - @override - String get aliasedName => _alias ?? 'categories'; - @override - String get actualTableName => '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('desc')) { - context.handle(_descriptionMeta, - description.isAcceptableOrUnknown(data['desc']!, _descriptionMeta)); - } else if (isInserting) { - context.missing(_descriptionMeta); - } - context.handle(_priorityMeta, const VerificationResult.success()); - if (data.containsKey('description_in_upper_case')) { - context.handle( - _descriptionInUpperCaseMeta, - descriptionInUpperCase.isAcceptableOrUnknown( - data['description_in_upper_case']!, _descriptionInUpperCaseMeta)); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - Category map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return Category( - id: attachedDatabase.options.types - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - description: attachedDatabase.options.types - .read(DriftSqlType.string, data['${effectivePrefix}desc'])!, - priority: $CategoriesTable.$converterpriority.fromSql(attachedDatabase - .options.types - .read(DriftSqlType.int, data['${effectivePrefix}priority'])!), - descriptionInUpperCase: attachedDatabase.options.types.read( - DriftSqlType.string, - data['${effectivePrefix}description_in_upper_case'])!, - ); - } - - @override - $CategoriesTable createAlias(String alias) { - return $CategoriesTable(attachedDatabase, alias); - } - - static TypeConverter $converterpriority = - const EnumIndexConverter(CategoryPriority.values); -} - class User extends DataClass implements Insertable { final int id; final String name; @@ -1590,8 +1590,8 @@ class $TodoWithCategoryViewView abstract class _$TodoDb extends GeneratedDatabase { _$TodoDb(QueryExecutor e) : super(e); _$TodoDb.connect(DatabaseConnection c) : super.connect(c); - late final $TodosTableTable todosTable = $TodosTableTable(this); late final $CategoriesTable categories = $CategoriesTable(this); + late final $TodosTableTable todosTable = $TodosTableTable(this); late final $UsersTable users = $UsersTable(this); late final $SharedTodosTable sharedTodos = $SharedTodosTable(this); late final $TableWithoutPKTable tableWithoutPK = $TableWithoutPKTable(this); @@ -1673,8 +1673,8 @@ abstract class _$TodoDb extends GeneratedDatabase { allSchemaEntities.whereType>(); @override List get allSchemaEntities => [ - todosTable, categories, + todosTable, users, sharedTodos, tableWithoutPK, @@ -1738,10 +1738,10 @@ class AllTodosWithCategoryResult extends CustomResultSet { mixin _$SomeDaoMixin on DatabaseAccessor { $UsersTable get users => attachedDatabase.users; $SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos; + $CategoriesTable get categories => attachedDatabase.categories; $TodosTableTable get todosTable => attachedDatabase.todosTable; $TodoWithCategoryViewView get todoWithCategoryView => attachedDatabase.todoWithCategoryView; - $CategoriesTable get categories => attachedDatabase.categories; Selectable todosForUser({required int user}) { return customSelect( 'SELECT t.* FROM todos AS t INNER JOIN shared_todos AS st ON st.todo = t.id INNER JOIN users AS u ON u.id = st.user WHERE u.id = ?1', diff --git a/drift/test/integration_tests/drift_files_test.dart b/drift/test/integration_tests/drift_files_test.dart index 4215d07a..d3e8d214 100644 --- a/drift/test/integration_tests/drift_files_test.dart +++ b/drift/test/integration_tests/drift_files_test.dart @@ -15,11 +15,11 @@ const _createWithDefaults = 'CREATE TABLE IF NOT EXISTS "with_defaults" (' const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS "with_constraints" (' '"a" TEXT, "b" INTEGER NOT NULL, "c" REAL, ' - 'FOREIGN KEY (a, b) REFERENCES with_defaults (a, b)' + 'FOREIGN KEY(a, b)REFERENCES with_defaults(a, b)' ');'; const _createConfig = 'CREATE TABLE IF NOT EXISTS "config" (' - '"config_key" TEXT not null primary key, ' + '"config_key" TEXT NOT NULL PRIMARY KEY, ' '"config_value" TEXT, ' '"sync_state" INTEGER, ' '"sync_state_implicit" INTEGER) STRICT;'; @@ -29,7 +29,7 @@ const _createMyTable = 'CREATE TABLE IF NOT EXISTS "mytable" (' '"sometext" TEXT, ' '"is_inserting" INTEGER, ' '"somedate" TEXT, ' - 'PRIMARY KEY (someid DESC)' + 'PRIMARY KEY(someid DESC)' ');'; const _createEmail = 'CREATE VIRTUAL TABLE IF NOT EXISTS "email" USING ' diff --git a/drift_dev/lib/src/analysis/custom_result_class.dart b/drift_dev/lib/src/analysis/custom_result_class.dart new file mode 100644 index 00000000..63803fb6 --- /dev/null +++ b/drift_dev/lib/src/analysis/custom_result_class.dart @@ -0,0 +1,113 @@ +import 'results/results.dart'; + +/// Transforms queries given in [inputs] so that their result sets respect +/// custom result class names specified by the user. +/// +/// The "custom result class name" feature can be used to change the name of a +/// result class and to generate the same result class for multiple custom +/// queries. +/// +/// Merging result classes of queries will always happen from the point of a +/// database class or dao. This means that incompatible queries can have the +/// same result class name as long as they're not imported into the same moor +/// accessor. +/// +/// This feature doesn't work when we apply other simplifications to query, so +/// we report an error if the query returns a single column or if it has a +/// matching table. This restriction might be lifted in the future, but it makes +/// the implementation easier. +Map transformCustomResultClasses( + Iterable inputs, + void Function(String) reportError, +) { + // A group of queries sharing a common result class name. + final queryGroups = >{}; + + // Find and group queries with the same result class name + for (final query in inputs) { + if (query is! SqlSelectQuery) continue; + final selectQuery = query; + + // Doesn't use a custom result class, so it's not affected by this + if (selectQuery.requestedResultClass == null) continue; + + // Alright, the query wants a custom result class, but is it allowed to + // have one? + if (selectQuery.resultSet.singleColumn) { + reportError("The query ${selectQuery.name} can't have a custom name as " + 'it only returns one column.'); + continue; + } + if (selectQuery.resultSet.matchingTable != null) { + reportError("The query ${selectQuery.name} can't have a custom name as " + 'it returns a single table data class.'); + continue; + } + + if (selectQuery.requestedResultClass != null) { + queryGroups + .putIfAbsent(selectQuery.requestedResultClass!, () => []) + .add(selectQuery); + } + } + + final replacements = {}; + + for (final group in queryGroups.entries) { + final resultSetName = group.key; + final queries = group.value; + + if (!_resultSetsCompatible(queries.map((e) => e.resultSet))) { + reportError( + 'Could not merge result sets to $resultSetName: The queries ' + 'have different columns and types.', + ); + continue; + } + + final referenceResult = queries.first.resultSet; + final dartNames = { + for (final column in referenceResult.columns) + column: referenceResult.dartNameFor(column), + }; + + var isFirst = true; + for (final query in queries) { + final newResultSet = InferredResultSet( + null, + query.resultSet.columns, + resultClassName: resultSetName, + nestedResults: query.resultSet.nestedResults, + // Only generate a result class for the first query in the group + dontGenerateResultClass: !isFirst, + ); + + // Make sure compatible columns in the two result sets have the same + // Dart name. + newResultSet.forceDartNames({ + for (final entry in dartNames.entries) + newResultSet.columns.singleWhere((e) => e.compatibleTo(entry.key)): + entry.value, + }); + + final newQuery = query.replaceResultSet(newResultSet); + replacements[query] = newQuery; + isFirst = false; + } + } + + return replacements; +} + +bool _resultSetsCompatible(Iterable resultSets) { + InferredResultSet? last; + + for (final current in resultSets) { + if (last != null && !last.isCompatibleTo(current)) { + return false; + } + + last = current; + } + return true; +} diff --git a/drift_dev/lib/src/analysis/resolver/drift/table.dart b/drift_dev/lib/src/analysis/resolver/drift/table.dart index a016417a..6b68c7ac 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/table.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/table.dart @@ -84,10 +84,16 @@ class DriftTableResolver extends LocalElementResolver { // can't read the constraints. final sqlConstraints = column.hasDefinition ? column.constraints : const []; + final customConstraintsForDrift = StringBuffer(); + for (final constraint in sqlConstraints) { + var writeIntoTable = true; + if (constraint is DriftDartName) { overriddenDartName = constraint.dartName; + writeIntoTable = false; } else if (constraint is MappedBy) { + writeIntoTable = false; if (converter != null) { reportError(DriftAnalysisError.inDriftFile( constraint, @@ -150,6 +156,13 @@ class DriftTableResolver extends LocalElementResolver { } else if (constraint is sql.UniqueColumn) { constraints.add(UniqueColumn()); } + + if (writeIntoTable) { + if (customConstraintsForDrift.isNotEmpty) { + customConstraintsForDrift.write(' '); + } + customConstraintsForDrift.write(constraint.toSql()); + } } columns.add(DriftColumn( @@ -160,6 +173,7 @@ class DriftTableResolver extends LocalElementResolver { constraints: constraints, typeConverter: converter, defaultArgument: defaultArgument, + customConstraints: customConstraintsForDrift.toString(), declaration: DriftDeclaration.driftFile( column.definition?.nameToken ?? stmt, state.ownId.libraryUri, @@ -168,9 +182,12 @@ class DriftTableResolver extends LocalElementResolver { } VirtualTableData? virtualTableData; + final sqlTableConstraints = []; if (stmt is CreateTableStatement) { for (final constraint in stmt.tableConstraints) { + sqlTableConstraints.add(constraint.toSql()); + if (constraint is ForeignKeyTableConstraint) { final otherTable = await resolveSqlReferenceOrReportError( constraint.clause.foreignTable.tableName, @@ -325,6 +342,7 @@ class DriftTableResolver extends LocalElementResolver { tableConstraints: tableConstraints, virtualTableData: virtualTableData, writeDefaultConstraints: false, + overrideTableConstraints: sqlTableConstraints, ); } diff --git a/drift_dev/lib/src/analysis/resolver/file_analysis.dart b/drift_dev/lib/src/analysis/resolver/file_analysis.dart index c4f2dded..e5710ca7 100644 --- a/drift_dev/lib/src/analysis/resolver/file_analysis.dart +++ b/drift_dev/lib/src/analysis/resolver/file_analysis.dart @@ -1,5 +1,6 @@ import 'package:sqlparser/sqlparser.dart'; +import '../../utils/entity_reference_sorter.dart'; import '../driver/driver.dart'; import '../driver/error.dart'; import '../driver/state.dart'; @@ -45,7 +46,7 @@ class FileAnalyzer { .whereType() .followedBy(element.references) .transitiveClosureUnderReferences() - .toList(); + .sortTopologicallyOrElse(driver.backend.log.severe); for (final query in element.declaredQueries) { final engine = diff --git a/drift_dev/lib/src/analysis/results/file_results.dart b/drift_dev/lib/src/analysis/results/file_results.dart index d183f23b..c2b51bbf 100644 --- a/drift_dev/lib/src/analysis/results/file_results.dart +++ b/drift_dev/lib/src/analysis/results/file_results.dart @@ -11,7 +11,7 @@ class FileAnalysisResult { } class ResolvedDatabaseAccessor { - final Map definedQueries; + Map definedQueries; final List knownImports; final List availableElements; diff --git a/drift_dev/lib/src/analyzer/sql_queries/custom_result_class.dart b/drift_dev/lib/src/analyzer/sql_queries/custom_result_class.dart deleted file mode 100644 index c0b550fc..00000000 --- a/drift_dev/lib/src/analyzer/sql_queries/custom_result_class.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:drift_dev/moor_generator.dart'; -import 'package:drift_dev/src/analyzer/errors.dart'; - -/// Transforms queries accessible to the [accessor] so that they use custom -/// result names. -/// -/// The "custom result class name" feature can be used to change the name of a -/// result class and to generate the same result class for multiple custom -/// queries. -/// -/// Merging result classes of queries will always happen from the point of a -/// database class or dao. This means that incompatible queries can have the -/// same result class name as long as they're not imported into the same moor -/// accessor. -/// -/// This feature doesn't work when we apply other simplifications to query, so -/// we report an error if the query returns a single column or if it has a -/// matching table. This restriction might be lifted in the future, but it makes -/// the implementation easier. -class CustomResultClassTransformer { - final BaseDriftAccessor accessor; - - CustomResultClassTransformer(this.accessor); - - void transform(ErrorSink errors) { - // For efficient replacing later on - final indexOfOldQueries = {}; - final queryGroups = >{}; - - // Find and group queries with the same result class name - var index = 0; - for (final query in accessor.queries ?? const []) { - final indexOfQuery = index++; - - if (query is! SqlSelectQuery) continue; - final selectQuery = query; - - // Doesn't use a custom result class, so it's not affected by this - if (selectQuery.requestedResultClass == null) continue; - - // Alright, the query wants a custom result class, but is it allowed to - // have one? - if (selectQuery.resultSet.singleColumn) { - errors.report(ErrorInDartCode( - message: "The query ${selectQuery.name} can't have a custom name as " - 'it only returns one column.', - affectedElement: accessor.declaration?.element, - )); - continue; - } - if (selectQuery.resultSet.matchingTable != null) { - errors.report(ErrorInDartCode( - message: "The query ${selectQuery.name} can't have a custom name as " - 'it returns a single table data class.', - affectedElement: accessor.declaration?.element, - )); - continue; - } - - // query will be replaced, save index for fast replacement later on - indexOfOldQueries[selectQuery] = indexOfQuery; - if (selectQuery.requestedResultClass != null) { - queryGroups - .putIfAbsent(selectQuery.requestedResultClass!, () => []) - .add(selectQuery); - } - } - - for (final group in queryGroups.entries) { - final resultSetName = group.key; - final queries = group.value; - - if (!_resultSetsCompatible(queries.map((e) => e.resultSet))) { - errors.report(ErrorInDartCode( - message: 'Could not merge result sets to $resultSetName: The queries ' - 'have different columns and types.', - affectedElement: accessor.declaration?.element, - )); - continue; - } - - final referenceResult = queries.first.resultSet; - final dartNames = { - for (final column in referenceResult.columns) - column: referenceResult.dartNameFor(column), - }; - - var isFirst = true; - for (final query in queries) { - final newResultSet = InferredResultSet( - null, - query.resultSet.columns, - resultClassName: resultSetName, - nestedResults: query.resultSet.nestedResults, - // Only generate a result class for the first query in the group - dontGenerateResultClass: !isFirst, - ); - - // Make sure compatible columns in the two result sets have the same - // Dart name. - newResultSet.forceDartNames({ - for (final entry in dartNames.entries) - newResultSet.columns.singleWhere((e) => e.compatibleTo(entry.key)): - entry.value, - }); - - final newQuery = query.replaceResultSet(newResultSet); - accessor.queries![indexOfOldQueries[query]!] = newQuery; - - isFirst = false; - } - } - } - - bool _resultSetsCompatible(Iterable resultSets) { - InferredResultSet? last; - - for (final current in resultSets) { - if (last != null && !last.isCompatibleTo(current)) { - return false; - } - - last = current; - } - return true; - } -} diff --git a/drift_dev/lib/src/backends/build/drift_builder.dart b/drift_dev/lib/src/backends/build/drift_builder.dart index aaee70f0..31d3194f 100644 --- a/drift_dev/lib/src/backends/build/drift_builder.dart +++ b/drift_dev/lib/src/backends/build/drift_builder.dart @@ -1,7 +1,9 @@ import 'package:build/build.dart'; import 'package:dart_style/dart_style.dart'; +import '../../analysis/custom_result_class.dart'; import '../../analysis/driver/driver.dart'; +import '../../analysis/driver/error.dart'; import '../../analysis/driver/state.dart'; import '../../analysis/results/results.dart'; import '../../analyzer/options.dart'; @@ -107,7 +109,7 @@ class DriftBuilder extends Builder { if (result is BaseDriftAccessor) { final resolved = fileResult.fileAnalysis!.resolvedDatabases[result.id]!; - final importedQueries = {}; + var importedQueries = {}; for (final query in resolved.availableElements.whereType()) { @@ -120,6 +122,16 @@ class DriftBuilder extends Builder { } } + // Apply custom result classes + final mappedQueries = transformCustomResultClasses( + resolved.definedQueries.values.followedBy(importedQueries.values), + (message) => log.warning('For accessor ${result.id.name}: $message'), + ); + importedQueries = + importedQueries.map((k, v) => MapEntry(k, mappedQueries[v] ?? v)); + resolved.definedQueries = resolved.definedQueries + .map((k, v) => MapEntry(k, mappedQueries[v] ?? v)); + if (result is DriftDatabase) { final input = DatabaseGenerationInput(result, resolved, importedQueries); diff --git a/drift_dev/lib/src/services/find_stream_update_rules.dart b/drift_dev/lib/src/services/find_stream_update_rules.dart index ee14a93b..7138f8bd 100644 --- a/drift_dev/lib/src/services/find_stream_update_rules.dart +++ b/drift_dev/lib/src/services/find_stream_update_rules.dart @@ -1,17 +1,18 @@ import 'package:drift/drift.dart' hide DriftDatabase; import 'package:sqlparser/sqlparser.dart'; +import '../analysis/results/file_results.dart'; import '../analysis/results/results.dart'; class FindStreamUpdateRules { - final DriftDatabase db; + final ResolvedDatabaseAccessor db; FindStreamUpdateRules(this.db); StreamQueryUpdateRules identifyRules() { final rules = []; - for (final entity in db.references) { + for (final entity in db.availableElements) { if (entity is DriftTrigger) { _writeRulesForTrigger(entity, rules); } else if (entity is DriftTable) { diff --git a/drift_dev/lib/src/utils/entity_reference_sorter.dart b/drift_dev/lib/src/utils/entity_reference_sorter.dart index ef49245b..bce47b80 100644 --- a/drift_dev/lib/src/utils/entity_reference_sorter.dart +++ b/drift_dev/lib/src/utils/entity_reference_sorter.dart @@ -1,42 +1,71 @@ import '../analysis/results/results.dart'; -/// Topologically sorts a list of [DriftElement]s by their -/// [DriftElement.references] relationship: Tables appearing first in the -/// output have to be created first so the table creation script doesn't crash -/// because of tables not existing. -/// -/// If there is a circular reference between [DriftTable]s, an error will -/// be added that contains the name of the tables in question. Self-references -/// in tables are allowed. -List sortEntitiesTopologically(Iterable tables) { - final run = _SortRun(); +extension SortTopologically on Iterable { + /// Topologically sorts a list of [DriftElement]s by their + /// [DriftElement.references] relationship: Tables appearing first in the + /// output have to be created first so the table creation script doesn't crash + /// because of tables not existing. + /// + /// If there is a circular reference between [DriftTable]s, an error will + /// be added that contains the name of the tables in question. + /// + /// Note that self-references (e.g. a foreign column in a table referencing + /// itself or another column in the same table) are _not_ included in + /// [DriftElement.references]. For example, an element created for the + /// statement `CREATE TABLE pairs (id INTEGER PRIMARY KEY, partner INTEGER + /// REFERENCES pairs (id))` has no references in the drift element model. + List sortTopologically() { + final run = _SortRun(); - for (final entity in tables) { - if (!run.didVisitAlready(entity)) { - run.previous[entity] = null; - _visit(entity, run); + for (final entity in this) { + if (!run.didVisitAlready(entity)) { + run.previous[entity] = null; + _visit(entity, run); + } + } + + return run.result; + } + + /// Sorts elements topologically (like [sortTopologically]). + /// + /// Unlike throwing an exception for circular references, the [reportError] + /// callback is invoked and the elements are returned unchanged. + List sortTopologicallyOrElse( + void Function(String) reportError) { + try { + return sortTopologically(); + } on CircularReferenceException catch (e) { + final joined = e.affected.map((e) => e.id.name).join('->'); + final last = e.affected.last.id.name; + final message = + 'Illegal circular reference. This is likely a bug in drift, ' + 'generated code may be invalid. Invalid cycle from $joined->$last.'; + reportError(message); + + return toList(); } } - return run.result; -} + static void _visit(DriftElement entity, _SortRun run) { + for (final reference in entity.references) { + assert(reference != entity, 'Illegal self-reference in $entity'); -void _visit(DriftElement entity, _SortRun run) { - for (final reference in entity.references) { - if (run.result.contains(reference) || reference == entity) { - // When the target entity has already been added there's nothing to do. - // We also ignore self-references - } else if (run.previous.containsKey(reference)) { - // that's a circular reference, report - run.throwCircularException(entity, reference); - } else { - run.previous[reference] = entity; - _visit(reference, run); + if (run.result.contains(reference)) { + // When the target entity has already been added there's nothing to do. + // We also ignore self-references + } else if (run.previous.containsKey(reference)) { + // that's a circular reference, report + run.throwCircularException(entity, reference); + } else { + run.previous[reference] = entity; + _visit(reference, run); + } } - } - // now that everything this table references is written, add the table itself - run.result.add(entity); + // now that everything this table references is written, add the table itself + run.result.add(entity); + } } class _SortRun { @@ -73,8 +102,8 @@ class _SortRun { } } -/// Thrown by [sortEntitiesTopologically] when the graph formed by -/// [DriftElement.references] is not acyclic except for self-references. +/// Thrown by [SortTopologically] when the graph formed by +/// [DriftElement.references] is not acyclic. class CircularReferenceException implements Exception { /// The list of entities forming a circular reference, so that the first /// entity in this list references the second one and so on. The last entity diff --git a/drift_dev/lib/src/writer/database_writer.dart b/drift_dev/lib/src/writer/database_writer.dart index a054019e..804b2e92 100644 --- a/drift_dev/lib/src/writer/database_writer.dart +++ b/drift_dev/lib/src/writer/database_writer.dart @@ -158,7 +158,8 @@ class DatabaseWriter { // close list literal and allSchemaEntities getter ..write('];\n'); - final updateRules = FindStreamUpdateRules(db).identifyRules(); + final updateRules = + FindStreamUpdateRules(input.resolvedAccessor).identifyRules(); if (updateRules.rules.isNotEmpty) { schemaScope ..write('@override\nStreamQueryUpdateRules get streamUpdateRules => ') @@ -240,13 +241,14 @@ extension on drift.TableUpdate { void writeConstructor(TextEmitter emitter) { emitter ..writeDriftRef('TableUpdate') - ..write('(${asDartLiteral(table)})'); + ..write('(${asDartLiteral(table)}'); if (kind == null) { emitter.write(')'); } else { emitter.write(', kind: '); kind!.write(emitter); + emitter.write(')'); } } } @@ -262,7 +264,7 @@ extension on drift.TableUpdateQuery { emitter.write('.onTableName(${asDartLiteral(query.table)} '); if (query.limitUpdateKind != null) { - emitter.write(', '); + emitter.write(', limitUpdateKind: '); query.limitUpdateKind!.write(emitter); } emitter.write(')');