diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index 7601e299..b89212e2 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.2.0 - Blob data type +- `insertOrReplace` method for insert statements ## 1.1.0 - Transactions diff --git a/moor/example/example.g.dart b/moor/example/example.g.dart index 08b9c3a4..5c81c7d5 100644 --- a/moor/example/example.g.dart +++ b/moor/example/example.g.dart @@ -18,6 +18,13 @@ class Category { description: stringType.mapFromDatabaseResponse(data['description']), ); } + Map toJson() { + return { + 'id': id, + 'description': description, + }; + } + Category copyWith({int id, String description}) => Category( id: id ?? this.id, description: description ?? this.description, @@ -99,6 +106,15 @@ class Recipe { category: intType.mapFromDatabaseResponse(data['category']), ); } + Map toJson() { + return { + 'id': id, + 'title': title, + 'instructions': instructions, + 'category': category, + }; + } + Recipe copyWith({int id, String title, String instructions, int category}) => Recipe( id: id ?? this.id, @@ -207,6 +223,14 @@ class Ingredient { caloriesPer100g: intType.mapFromDatabaseResponse(data['calories']), ); } + Map toJson() { + return { + 'id': id, + 'name': name, + 'caloriesPer100g': caloriesPer100g, + }; + } + Ingredient copyWith({int id, String name, int caloriesPer100g}) => Ingredient( id: id ?? this.id, name: name ?? this.name, @@ -303,6 +327,14 @@ class IngredientInRecipe { amountInGrams: intType.mapFromDatabaseResponse(data['amount']), ); } + Map toJson() { + return { + 'recipe': recipe, + 'ingredient': ingredient, + 'amountInGrams': amountInGrams, + }; + } + IngredientInRecipe copyWith( {int recipe, int ingredient, int amountInGrams}) => IngredientInRecipe( diff --git a/moor/lib/src/runtime/statements/insert.dart b/moor/lib/src/runtime/statements/insert.dart index 8cfb96f3..9745d62d 100644 --- a/moor/lib/src/runtime/statements/insert.dart +++ b/moor/lib/src/runtime/statements/insert.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:moor/moor.dart'; import 'package:moor/src/runtime/components/component.dart'; +import 'update.dart'; class InsertStatement { @protected @@ -10,8 +11,17 @@ class InsertStatement { @protected final TableInfo table; + bool _orReplace = false; + InsertStatement(this.database, this.table); + /// Inserts a row constructed from the fields in [entity]. + /// + /// All fields in the entity that don't have a default value or auto-increment + /// must be set and non-null. Otherwise, an [InvalidDataException] will be + /// thrown. An insert will also fail if another row with the same primary key + /// or unique constraints already exists. If you want to override data in that + /// case, use [insertOrReplace] instead. Future insert(DataClass entity) async { if (!table.validateIntegrity(entity, true)) { throw InvalidDataException( @@ -23,7 +33,9 @@ class InsertStatement { final ctx = GenerationContext(database); ctx.buffer - ..write('INSERT INTO ') + ..write('INSERT ') + ..write(_orReplace ? 'OR REPLACE ' : '') + ..write('INTO ') ..write(table.$tableName) ..write(' (') ..write(map.keys.join(', ')) @@ -50,4 +62,16 @@ class InsertStatement { // TODO insert multiple values + /// Updates the row with the same primary key in the database or creates one + /// if it doesn't exist. + /// + /// Behaves similar to [UpdateStatement.replace], meaning that all fields from + /// [entity] will be written to override rows with the same primary key, which + /// includes setting columns with null values back to null. + /// + /// However, if no such row exists, a new row will be written instead. + Future insertOrReplace(DataClass entity) async { + _orReplace = true; + await insert(entity); + } } diff --git a/moor/lib/src/runtime/statements/update.dart b/moor/lib/src/runtime/statements/update.dart index 4bc3fbe7..8c39c1b9 100644 --- a/moor/lib/src/runtime/statements/update.dart +++ b/moor/lib/src/runtime/statements/update.dart @@ -43,17 +43,17 @@ class UpdateStatement extends Query { } /// Writes all non-null fields from [entity] into the columns of all rows - /// that match the set [where] and limit constraints. Warning: That also - /// means that, when you're not setting a where or limit expression - /// explicitly, this method will update all rows in the specific table. + /// that match the [where] clause. Warning: That also means that, when you're + /// not setting a where clause explicitly, this method will update all rows in + /// the [table]. /// /// The fields that are null on the [entity] object will not be changed by - /// this operation. + /// this operation, they will be ignored. /// /// Returns the amount of rows that have been affected by this operation. /// /// See also: [replace], which does not require [where] statements and - /// supports setting fields to null. + /// supports setting fields back to null. Future write(D entity) async { if (!table.validateIntegrity(entity, false)) { throw InvalidDataException( @@ -81,6 +81,12 @@ class UpdateStatement extends Query { /// null fields. /// /// Returns true if a row was affected by this operation. + /// + /// See also: + /// - [write], which doesn't apply a [where] statement itself and ignores + /// null values in the entity. + /// - [InsertStatement.insertOrReplace], which behaves similar to this method + /// but creates a new row if none exists. Future replace(D entity) async { // We set isInserting to true here although we're in an update. This is // because all the fields from the entity will be written (as opposed to a diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index 37df5a68..060538e8 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -26,6 +26,16 @@ class TodoEntry { category: intType.mapFromDatabaseResponse(data['category']), ); } + Map toJson() { + return { + 'id': id, + 'title': title, + 'content': content, + 'targetDate': targetDate, + 'category': category, + }; + } + TodoEntry copyWith( {int id, String title, @@ -154,6 +164,13 @@ class Category { description: stringType.mapFromDatabaseResponse(data['`desc`']), ); } + Map toJson() { + return { + 'id': id, + 'description': description, + }; + } + Category copyWith({int id, String description}) => Category( id: id ?? this.id, description: description ?? this.description, @@ -238,6 +255,15 @@ class User { uint8ListType.mapFromDatabaseResponse(data['profile_picture']), ); } + Map toJson() { + return { + 'id': id, + 'name': name, + 'isAwesome': isAwesome, + 'profilePicture': profilePicture, + }; + } + User copyWith( {int id, String name, bool isAwesome, Uint8List profilePicture}) => User( @@ -344,6 +370,13 @@ class SharedTodo { user: intType.mapFromDatabaseResponse(data['user']), ); } + Map toJson() { + return { + 'todo': todo, + 'user': user, + }; + } + SharedTodo copyWith({int todo, int user}) => SharedTodo( todo: todo ?? this.todo, user: user ?? this.user, diff --git a/moor/test/insert_test.dart b/moor/test/insert_test.dart new file mode 100644 index 00000000..ceeaecf1 --- /dev/null +++ b/moor/test/insert_test.dart @@ -0,0 +1,58 @@ +import 'package:moor/moor.dart'; +import 'package:test_api/test_api.dart'; + +import 'data/tables/todos.dart'; +import 'data/utils/mocks.dart'; + +void main() { + TodoDb db; + MockExecutor executor; + MockStreamQueries streamQueries; + + setUp(() { + executor = MockExecutor(); + streamQueries = MockStreamQueries(); + db = TodoDb(executor)..streamQueries = streamQueries; + }); + + test('generates insert statements', () async { + await db.into(db.todosTable).insert(TodoEntry( + content: 'Implement insert statements', + )); + + verify(executor.runInsert('INSERT INTO todos (content) VALUES (?)', + ['Implement insert statements'])); + }); + + test('generates insert or replace statements', () async { + await db.into(db.todosTable).insertOrReplace(TodoEntry( + id: 113, + content: 'Done', + )); + + verify(executor.runInsert( + 'INSERT OR REPLACE INTO todos (id, content) VALUES (?, ?)', + [113, 'Done'])); + }); + + test('notifies stream queries on inserts', () async { + await db.into(db.users).insert(User( + name: 'User McUserface', + isAwesome: true, + profilePicture: Uint8List(0), + )); + + verify(streamQueries.handleTableUpdates({'users'})); + }); + + test('enforces data integrety', () { + expect( + db.into(db.todosTable).insert( + TodoEntry( + content: null, // not declared as nullable in table definition + ), + ), + throwsA(const TypeMatcher()), + ); + }); +} diff --git a/moor/test/update_test.dart b/moor/test/update_test.dart index 09082464..0d4385e7 100644 --- a/moor/test/update_test.dart +++ b/moor/test/update_test.dart @@ -55,7 +55,7 @@ void main() { // The length of a title must be between 4 and 16 chars expect(() async { - await db.into(db.todosTable).insert(TodoEntry(title: 'lol')); + await db.update(db.todosTable).write(TodoEntry(title: 'lol')); }, throwsA(const TypeMatcher())); });