Implement insertOrReplace method for insert statements

This commit is contained in:
Simon Binder 2019-03-15 12:56:22 +01:00
parent e7cb0cb2e5
commit e7ece27528
No known key found for this signature in database
GPG Key ID: B807FDF954BA00CF
7 changed files with 161 additions and 7 deletions

View File

@ -1,5 +1,6 @@
## 1.2.0
- Blob data type
- `insertOrReplace` method for insert statements
## 1.1.0
- Transactions

View File

@ -18,6 +18,13 @@ class Category {
description: stringType.mapFromDatabaseResponse(data['description']),
);
}
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'recipe': recipe,
'ingredient': ingredient,
'amountInGrams': amountInGrams,
};
}
IngredientInRecipe copyWith(
{int recipe, int ingredient, int amountInGrams}) =>
IngredientInRecipe(

View File

@ -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<DataClass> {
@protected
@ -10,8 +11,17 @@ class InsertStatement<DataClass> {
@protected
final TableInfo<dynamic, DataClass> 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<void> insert(DataClass entity) async {
if (!table.validateIntegrity(entity, true)) {
throw InvalidDataException(
@ -23,7 +33,9 @@ class InsertStatement<DataClass> {
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<DataClass> {
// 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<void> insertOrReplace(DataClass entity) async {
_orReplace = true;
await insert(entity);
}
}

View File

@ -43,17 +43,17 @@ class UpdateStatement<T, D> extends Query<T, D> {
}
/// 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<int> write(D entity) async {
if (!table.validateIntegrity(entity, false)) {
throw InvalidDataException(
@ -81,6 +81,12 @@ class UpdateStatement<T, D> extends Query<T, D> {
/// 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<bool> 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

View File

@ -26,6 +26,16 @@ class TodoEntry {
category: intType.mapFromDatabaseResponse(data['category']),
);
}
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'todo': todo,
'user': user,
};
}
SharedTodo copyWith({int todo, int user}) => SharedTodo(
todo: todo ?? this.todo,
user: user ?? this.user,

View File

@ -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<InvalidDataException>()),
);
});
}

View File

@ -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<InvalidDataException>()));
});