From ecf6740cb7661bac068245b32c55b4a9e075b96b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 21 Jun 2019 10:27:14 +0200 Subject: [PATCH] Constraints on type parameters for data classes --- moor/CHANGELOG.md | 4 ++ moor/example/example.g.dart | 25 ++++++------ moor/lib/src/runtime/components/join.dart | 6 +-- moor/lib/src/runtime/data_class.dart | 36 +++-------------- moor/lib/src/runtime/database.dart | 19 ++++----- moor/lib/src/runtime/statements/delete.dart | 2 +- moor/lib/src/runtime/statements/query.dart | 17 ++++---- moor/lib/src/runtime/statements/select.dart | 7 ++-- moor/lib/src/runtime/structure/columns.dart | 40 ++++++++++--------- .../lib/src/runtime/structure/table_info.dart | 21 +++++----- moor/test/data/tables/todos.g.dart | 31 +++++++------- .../lib/src/writer/data_class_writer.dart | 8 ++-- .../src/writer/update_companion_writer.dart | 2 +- 13 files changed, 98 insertions(+), 120 deletions(-) diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index e0f6f028..52712213 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.5.0 +- More consistent and reliable migration and opening callbacks +- TODO: Explain new companions + ## 1.4.0 - Added the `RealColumn`, which stores floating point values - Better configuration for the serializer with the `JsonKey` annotation and the ability to diff --git a/moor/example/example.g.dart b/moor/example/example.g.dart index 0a9deacf..695b10fb 100644 --- a/moor/example/example.g.dart +++ b/moor/example/example.g.dart @@ -7,7 +7,7 @@ part of 'example.dart'; // ************************************************************************** // ignore_for_file: unnecessary_brace_in_string_interps -class Category extends DataClass with DelegatingCompanionMixin { +class Category extends DataClass { final int id; final String description; Category({this.id, this.description}); @@ -39,7 +39,7 @@ class Category extends DataClass with DelegatingCompanionMixin { } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + CategoriesCompanion createCompanion(bool nullToAbsent) { return CategoriesCompanion( id: id == null && nullToAbsent ? const Value.absent() : Value.use(id), description: description == null && nullToAbsent @@ -77,7 +77,7 @@ class CategoriesCompanion implements UpdateCompanion { this.description = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return id.present; @@ -160,7 +160,7 @@ class $CategoriesTable extends Categories } } -class Recipe extends DataClass with DelegatingCompanionMixin { +class Recipe extends DataClass { final int id; final String title; final String instructions; @@ -202,7 +202,7 @@ class Recipe extends DataClass with DelegatingCompanionMixin { } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + RecipesCompanion createCompanion(bool nullToAbsent) { return RecipesCompanion( id: id == null && nullToAbsent ? const Value.absent() : Value.use(id), title: title == null && nullToAbsent @@ -262,7 +262,7 @@ class RecipesCompanion implements UpdateCompanion { this.category = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return id.present; @@ -380,7 +380,7 @@ class $RecipesTable extends Recipes with TableInfo<$RecipesTable, Recipe> { } } -class Ingredient extends DataClass with DelegatingCompanionMixin { +class Ingredient extends DataClass { final int id; final String name; final int caloriesPer100g; @@ -416,7 +416,7 @@ class Ingredient extends DataClass with DelegatingCompanionMixin { } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + IngredientsCompanion createCompanion(bool nullToAbsent) { return IngredientsCompanion( id: id == null && nullToAbsent ? const Value.absent() : Value.use(id), name: @@ -464,7 +464,7 @@ class IngredientsCompanion implements UpdateCompanion { this.caloriesPer100g = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return id.present; @@ -567,8 +567,7 @@ class $IngredientsTable extends Ingredients } } -class IngredientInRecipe extends DataClass - with DelegatingCompanionMixin { +class IngredientInRecipe extends DataClass { final int recipe; final int ingredient; final int amountInGrams; @@ -605,7 +604,7 @@ class IngredientInRecipe extends DataClass } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + IngredientInRecipesCompanion createCompanion(bool nullToAbsent) { return IngredientInRecipesCompanion( recipe: recipe == null && nullToAbsent ? const Value.absent() @@ -660,7 +659,7 @@ class IngredientInRecipesCompanion this.amountInGrams = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return recipe.present; diff --git a/moor/lib/src/runtime/components/join.dart b/moor/lib/src/runtime/components/join.dart index 671682a2..17c2aa86 100644 --- a/moor/lib/src/runtime/components/join.dart +++ b/moor/lib/src/runtime/components/join.dart @@ -10,7 +10,7 @@ const Map _joinKeywords = { JoinType.cross: 'CROSS', }; -class Join extends Component { +class Join extends Component { final JoinType type; final TableInfo table; final Expression on; @@ -35,7 +35,7 @@ class Join extends Component { /// /// See also: /// - http://www.sqlitetutorial.net/sqlite-inner-join/ -Join innerJoin( +Join innerJoin( TableInfo other, Expression on) { return Join(JoinType.inner, other, on); } @@ -45,7 +45,7 @@ Join innerJoin( /// /// See also: /// - http://www.sqlitetutorial.net/sqlite-left-join/ -Join leftOuterJoin( +Join leftOuterJoin( TableInfo other, Expression on) { return Join(JoinType.leftOuter, other, on); } diff --git a/moor/lib/src/runtime/data_class.dart b/moor/lib/src/runtime/data_class.dart index 0cba001c..55b04c8e 100644 --- a/moor/lib/src/runtime/data_class.dart +++ b/moor/lib/src/runtime/data_class.dart @@ -26,6 +26,9 @@ abstract class DataClass { static dynamic parseJson(String jsonString) { return json.decode(jsonString); } + + /// Used internally by moor. + UpdateCompanion createCompanion(bool nullToAbsent); } /// An update companion for a [DataClass] which is used to write data into a @@ -37,37 +40,8 @@ abstract class UpdateCompanion { /// Used internally by moor. /// /// Returns true if the column at the position [index] has been explicitly - /// set to a value. The [assumeNullAsAbsent] parameter exists for backwards - /// compatibility reasons, as [DataClass] implements [UpdateCompanion] but - /// doesn't have value fields. - bool isValuePresent(int index, bool assumeNullAsAbsent); -} - -/// Used internally by moor for generated code. -/// -/// Exists for backwards compatibility so that a [DataClass] can implement -/// [UpdateCompanion]. -mixin DelegatingCompanionMixin - implements UpdateCompanion { - UpdateCompanion _absent; - UpdateCompanion _present; - - @visibleForOverriding - UpdateCompanion createCompanion(bool nullToAbsent); - - UpdateCompanion _resolveDelegate(bool nullToAbsent) { - if (nullToAbsent) { - return _absent ??= createCompanion(true); - } else { - return _present ??= createCompanion(false); - } - } - - @override - bool isValuePresent(int index, bool assumeNullAsAbsent) { - final delegate = _resolveDelegate(assumeNullAsAbsent); - return delegate.isValuePresent(index, assumeNullAsAbsent); - } + /// set to a value. + bool isValuePresent(int index); } /// A wrapper around arbitrary data [T] to indicate presence or absence diff --git a/moor/lib/src/runtime/database.dart b/moor/lib/src/runtime/database.dart index de5b6f84..f5385daf 100644 --- a/moor/lib/src/runtime/database.dart +++ b/moor/lib/src/runtime/database.dart @@ -76,7 +76,8 @@ abstract class DatabaseConnectionUser { /// innerJoin(destination, routes.startPoint.equalsExp(destination.id)), /// ]); /// ``` - T alias(TableInfo table, String alias) { + T alias( + TableInfo table, String alias) { return table.createAlias(alias).asDslTable; } } @@ -96,8 +97,8 @@ mixin QueryEngine on DatabaseConnectionUser { /// clause on that table and then use [UpdateStatement.write]. @protected @visibleForTesting - UpdateStatement update( - TableInfo table) => + UpdateStatement update( + TableInfo table) => UpdateStatement(this, table); /// Starts a query on the given table. Queries can be limited with an limit @@ -105,17 +106,17 @@ mixin QueryEngine on DatabaseConnectionUser { /// stream of data @protected @visibleForTesting - SimpleSelectStatement select( - TableInfo table) { - return SimpleSelectStatement(this, table); + SimpleSelectStatement select( + TableInfo table) { + return SimpleSelectStatement(this, table); } /// Starts a [DeleteStatement] that can be used to delete rows from a table. @protected @visibleForTesting - DeleteStatement delete( - TableInfo table) { - return DeleteStatement(this, table); + DeleteStatement delete( + TableInfo table) { + return DeleteStatement(this, table); } /// Executes a custom delete or update statement and returns the amount of diff --git a/moor/lib/src/runtime/statements/delete.dart b/moor/lib/src/runtime/statements/delete.dart index 7d7e9b84..19e1e7ef 100644 --- a/moor/lib/src/runtime/statements/delete.dart +++ b/moor/lib/src/runtime/statements/delete.dart @@ -5,7 +5,7 @@ import 'package:moor/src/runtime/components/component.dart'; import 'package:moor/src/runtime/statements/query.dart'; import 'package:moor/src/runtime/structure/table_info.dart'; -class DeleteStatement extends Query +class DeleteStatement extends Query with SingleTableQueryMixin { /// This constructor should be called by [GeneratedDatabase.delete] for you. DeleteStatement(QueryEngine database, TableInfo table) diff --git a/moor/lib/src/runtime/statements/query.dart b/moor/lib/src/runtime/statements/query.dart index 94e14a61..0642d4b7 100644 --- a/moor/lib/src/runtime/statements/query.dart +++ b/moor/lib/src/runtime/statements/query.dart @@ -1,23 +1,19 @@ import 'package:meta/meta.dart'; -import 'package:moor/src/dsl/table.dart'; +import 'package:moor/moor.dart'; import 'package:moor/src/runtime/components/component.dart'; import 'package:moor/src/runtime/components/limit.dart'; import 'package:moor/src/runtime/components/order_by.dart'; import 'package:moor/src/runtime/components/where.dart'; -import 'package:moor/src/runtime/database.dart'; -import 'package:moor/src/runtime/expressions/bools.dart'; import 'package:moor/src/runtime/expressions/custom.dart'; import 'package:moor/src/runtime/expressions/expression.dart'; -import 'package:moor/src/types/sql_types.dart'; -import 'package:moor/src/runtime/structure/table_info.dart'; import 'package:moor/src/utils/single_transformer.dart'; /// Statement that operates with data that already exists (select, delete, /// update). -abstract class Query { +abstract class Query { @protected QueryEngine database; - TableInfo table; + TableInfo table; Query(this.database, this.table); @@ -110,7 +106,8 @@ abstract class Selectable { } } -mixin SingleTableQueryMixin on Query { +mixin SingleTableQueryMixin + on Query { void where(Expression filter(T tbl)) { final predicate = filter(table.asDslTable); @@ -123,7 +120,7 @@ mixin SingleTableQueryMixin on Query { /// Applies a [where] statement so that the row with the same primary key as /// [d] will be matched. - void whereSamePrimaryKey(DataClass d) { + void whereSamePrimaryKey(D d) { assert( table.$primaryKey != null && table.$primaryKey.isNotEmpty, 'When using Query.whereSamePrimaryKey, which is also called from ' @@ -162,7 +159,7 @@ mixin SingleTableQueryMixin on Query { } } -mixin LimitContainerMixin on Query { +mixin LimitContainerMixin on Query { /// Limits the amount of rows returned by capping them at [limit]. If [offset] /// is provided as well, the first [offset] rows will be skipped and not /// included in the result. diff --git a/moor/lib/src/runtime/statements/select.dart b/moor/lib/src/runtime/statements/select.dart index 9d13a481..ea7bd5b0 100644 --- a/moor/lib/src/runtime/statements/select.dart +++ b/moor/lib/src/runtime/statements/select.dart @@ -13,7 +13,7 @@ import 'package:moor/src/runtime/structure/table_info.dart'; typedef OrderingTerm OrderClauseGenerator(T tbl); -class JoinedSelectStatement +class JoinedSelectStatement extends Query with LimitContainerMixin, Selectable { JoinedSelectStatement( @@ -142,7 +142,8 @@ class JoinedSelectStatement } /// A select statement that doesn't use joins -class SimpleSelectStatement extends Query +class SimpleSelectStatement + extends Query with SingleTableQueryMixin, LimitContainerMixin, Selectable { SimpleSelectStatement(QueryEngine database, TableInfo table) : super(database, table); @@ -316,7 +317,7 @@ class TypedResult { final QueryRow rawData; /// Reads all data that belongs to the given [table] from this row. - D readTable(TableInfo table) { + D readTable(TableInfo table) { return _parsedData[table] as D; } } diff --git a/moor/lib/src/runtime/structure/columns.dart b/moor/lib/src/runtime/structure/columns.dart index cfdf42cd..e3cebb37 100644 --- a/moor/lib/src/runtime/structure/columns.dart +++ b/moor/lib/src/runtime/structure/columns.dart @@ -83,22 +83,29 @@ abstract class GeneratedColumn> extends Column { } /// Checks whether the given value fits into this column. The default - /// implementation checks whether the value is not null, as null values are - /// only allowed for updates or if the column is nullable. - /// If [duringInsert] is true, the method should check whether the value is - /// suitable for a new row that is being inserted. If it's false, we the - /// method should check whether the value is valid for an update. Null values - /// should always be accepted for updates, as the describe a value that should - /// not be replaced. - VerificationResult isAcceptableValue( - T value, bool duringInsert, VerificationMeta meta) { - final nullOk = !duringInsert || $nullable || defaultValue != null; + /// implementation only checks for nullability, but subclasses might enforce + /// additional checks. For instance, the [GeneratedTextColumn] can verify + /// that a text has a certain length. + /// + /// Note: The behavior of this method was changed in moor 1.5. Before, null + /// values were interpreted as an absent value during updates or if the + /// [defaultValue] is set. Verification was skipped for absent values. + /// This is no longer the case, all null values are assumed to be an sql + /// `NULL`. + VerificationResult isAcceptableValue(T value, VerificationMeta meta) { + final nullOk = $nullable; if (!nullOk && value == null) { return _invalidNull; } else { return const VerificationResult.success(); } } + + /// Returns true if this column needs to be set when writing a new row into + /// a table. + bool get isRequired { + return !$nullable && defaultValue == null; + } } class GeneratedTextColumn extends GeneratedColumn @@ -125,10 +132,9 @@ class GeneratedTextColumn extends GeneratedColumn final String typeName = 'VARCHAR'; @override - VerificationResult isAcceptableValue( - String value, bool duringInsert, VerificationMeta meta) { + VerificationResult isAcceptableValue(String value, VerificationMeta meta) { // handle nullability check in common column - if (value == null) return super.isAcceptableValue(null, duringInsert, meta); + if (value == null) return super.isAcceptableValue(null, meta); final length = value.length; if (minTextLength != null && minTextLength > length) { @@ -189,12 +195,8 @@ class GeneratedIntColumn extends GeneratedColumn } @override - VerificationResult isAcceptableValue( - int value, bool duringInsert, VerificationMeta meta) { - if (hasAutoIncrement) { - return const VerificationResult.success(); - } - return super.isAcceptableValue(value, duringInsert, meta); + bool get isRequired { + return !hasAutoIncrement && super.isRequired; } } diff --git a/moor/lib/src/runtime/structure/table_info.dart b/moor/lib/src/runtime/structure/table_info.dart index 7a71d247..3b38c8fc 100644 --- a/moor/lib/src/runtime/structure/table_info.dart +++ b/moor/lib/src/runtime/structure/table_info.dart @@ -2,9 +2,9 @@ import 'package:moor/moor.dart'; import 'package:moor/src/runtime/expressions/variables.dart'; /// Base class for generated classes. [TableDsl] is the type specified by the -/// user that extends [Table], [DataClass] is the type of the data class +/// user that extends [Table], [D] is the type of the data class /// generated from the table. -mixin TableInfo { +mixin TableInfo { /// Type system sugar. Implementations are likely to inherit from both /// [TableInfo] and [TableDsl] and can thus just return their instance. TableDsl get asDslTable; @@ -36,22 +36,23 @@ mixin TableInfo { /// Validates that the given entity can be inserted into this table, meaning /// that it respects all constraints (nullability, text length, etc.). - /// During insertion mode, fields that have a default value or are - /// auto-incrementing are allowed to be null as they will be set by sqlite. - VerificationContext validateIntegrity(DataClass instance, bool isInserting); + /// + /// The [nullsAreAbsent] parameter exists for backwards-compatibility. See the + /// discussion in the changelog at version 1.5 for details. + VerificationContext validateIntegrity( + UpdateCompanion instance, bool nullsAreAbsent); /// Maps the given data class to a [Map] that can be inserted into sql. The /// keys should represent the column name in sql, the values the corresponding /// values of the field. /// - /// If [includeNulls] is true, fields of the [DataClass] that are null will be + /// If [includeNulls] is true, fields of the [D] that are null will be /// written as a [Variable] with a value of null. Otherwise, these fields will /// not be written into the map at all. - Map entityToSql(DataClass instance, - {bool includeNulls = false}); + Map entityToSql(D instance, {bool includeNulls = false}); /// Maps the given row returned by the database into the fitting data class. - DataClass map(Map data, {String tablePrefix}); + D map(Map data, {String tablePrefix}); - TableInfo createAlias(String alias); + TableInfo createAlias(String alias); } diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index 3ae07ad8..ea56acb7 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -7,7 +7,7 @@ part of 'todos.dart'; // ************************************************************************** // ignore_for_file: unnecessary_brace_in_string_interps -class TodoEntry extends DataClass with DelegatingCompanionMixin { +class TodoEntry extends DataClass { final int id; final String title; final String content; @@ -56,7 +56,7 @@ class TodoEntry extends DataClass with DelegatingCompanionMixin { } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + TodosTableCompanion createCompanion(bool nullToAbsent) { return TodosTableCompanion( id: id == null && nullToAbsent ? const Value.absent() : Value.use(id), title: title == null && nullToAbsent @@ -130,7 +130,7 @@ class TodosTableCompanion implements UpdateCompanion { this.category = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return id.present; @@ -271,7 +271,7 @@ class $TodosTableTable extends TodosTable } } -class Category extends DataClass with DelegatingCompanionMixin { +class Category extends DataClass { final int id; final String description; Category({this.id, this.description}); @@ -303,7 +303,7 @@ class Category extends DataClass with DelegatingCompanionMixin { } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + CategoriesCompanion createCompanion(bool nullToAbsent) { return CategoriesCompanion( id: id == null && nullToAbsent ? const Value.absent() : Value.use(id), description: description == null && nullToAbsent @@ -341,7 +341,7 @@ class CategoriesCompanion implements UpdateCompanion { this.description = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return id.present; @@ -421,7 +421,7 @@ class $CategoriesTable extends Categories } } -class User extends DataClass with DelegatingCompanionMixin { +class User extends DataClass { final int id; final String name; final bool isAwesome; @@ -475,7 +475,7 @@ class User extends DataClass with DelegatingCompanionMixin { } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + UsersCompanion createCompanion(bool nullToAbsent) { return UsersCompanion( id: id == null && nullToAbsent ? const Value.absent() : Value.use(id), name: @@ -549,7 +549,7 @@ class UsersCompanion implements UpdateCompanion { this.creationTime = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return id.present; @@ -686,7 +686,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> { } } -class SharedTodo extends DataClass with DelegatingCompanionMixin { +class SharedTodo extends DataClass { final int todo; final int user; SharedTodo({this.todo, this.user}); @@ -716,7 +716,7 @@ class SharedTodo extends DataClass with DelegatingCompanionMixin { } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + SharedTodosCompanion createCompanion(bool nullToAbsent) { return SharedTodosCompanion( todo: todo == null && nullToAbsent ? const Value.absent() : Value.use(todo), @@ -754,7 +754,7 @@ class SharedTodosCompanion implements UpdateCompanion { this.user = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return todo.present; @@ -838,8 +838,7 @@ class $SharedTodosTable extends SharedTodos } } -class TableWithoutPKData extends DataClass - with DelegatingCompanionMixin { +class TableWithoutPKData extends DataClass { final int notReallyAnId; final double someFloat; TableWithoutPKData({this.notReallyAnId, this.someFloat}); @@ -873,7 +872,7 @@ class TableWithoutPKData extends DataClass } @override - UpdateCompanion createCompanion(bool nullToAbsent) { + TableWithoutPKCompanion createCompanion(bool nullToAbsent) { return TableWithoutPKCompanion( notReallyAnId: notReallyAnId == null && nullToAbsent ? const Value.absent() @@ -917,7 +916,7 @@ class TableWithoutPKCompanion implements UpdateCompanion { this.someFloat = const Value.absent(), }); @override - bool isValuePresent(int index, bool _) { + bool isValuePresent(int index) { switch (index) { case 0: return notReallyAnId.present; diff --git a/moor_generator/lib/src/writer/data_class_writer.dart b/moor_generator/lib/src/writer/data_class_writer.dart index e4d8706e..05118312 100644 --- a/moor_generator/lib/src/writer/data_class_writer.dart +++ b/moor_generator/lib/src/writer/data_class_writer.dart @@ -12,8 +12,7 @@ class DataClassWriter { DataClassWriter(this.table, this.options); void writeInto(StringBuffer buffer) { - buffer.write('class ${table.dartTypeName} extends DataClass ' - 'with DelegatingCompanionMixin<${table.dartTypeName}> {\n'); + buffer.write('class ${table.dartTypeName} extends DataClass {\n'); // write individual fields for (var column in table.columns) { @@ -220,9 +219,10 @@ class DataClassWriter { void _writeCompanionOverride(StringBuffer buffer) { // UpdateCompanion createCompanion(bool nullToAbsent); - buffer.write('@override\nUpdateCompanion<${table.dartTypeName}> ' + final companionClass = table.updateCompanionName; + buffer.write('@override\n$companionClass ' 'createCompanion(bool nullToAbsent) {\n' - 'return ${table.updateCompanionName}('); + 'return $companionClass('); for (var column in table.columns) { final getter = column.dartGetterName; diff --git a/moor_generator/lib/src/writer/update_companion_writer.dart b/moor_generator/lib/src/writer/update_companion_writer.dart index 234973cb..348833f1 100644 --- a/moor_generator/lib/src/writer/update_companion_writer.dart +++ b/moor_generator/lib/src/writer/update_companion_writer.dart @@ -36,7 +36,7 @@ class UpdateCompanionWriter { void _writeIsPresentOverride(StringBuffer buffer) { buffer - ..write('@override\nbool isValuePresent(int index, bool _) {\n') + ..write('@override\nbool isValuePresent(int index) {\n') ..write('switch (index) {'); for (var i = 0; i < table.columns.length; i++) {