From ccf208b3297669723cdb82529b13b668b207813d Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 21 Oct 2019 17:14:58 +0200 Subject: [PATCH] Support additional insert modes --- moor/lib/src/runtime/statements/insert.dart | 94 +++++++++++++++++---- moor/test/insert_test.dart | 14 +++ 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/moor/lib/src/runtime/statements/insert.dart b/moor/lib/src/runtime/statements/insert.dart index 605bdfa6..1073d916 100644 --- a/moor/lib/src/runtime/statements/insert.dart +++ b/moor/lib/src/runtime/statements/insert.dart @@ -24,17 +24,26 @@ class InsertStatement { /// must be set and non-null. Otherwise, an [InvalidDataException] will be /// thrown. /// - /// If [orReplace] is true and a row with the same primary key already exists, - /// the columns of that row will be updated and no new row will be written. - /// Otherwise, an exception will be thrown. + /// By default, an exception will be thrown if another row with the same + /// primary key already exists. This behavior can be overridden with [mode], + /// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore]. /// /// If the table contains an auto-increment column, the generated value will /// be returned. If there is no auto-increment column, you can't rely on the /// return value, but the future will resolve to an error when the insert /// fails. - Future insert(Insertable entity, {bool orReplace = false}) async { + Future insert( + Insertable entity, { + @Deprecated('Use mode: InsertMode.replace instead') bool orReplace = false, + InsertMode mode, + }) async { + assert( + mode == null || (orReplace != true), + 'If the mode parameter is set on insertAll, orReplace must be null or ' + 'false', + ); _validateIntegrity(entity); - final ctx = _createContext(entity, orReplace); + final ctx = _createContext(entity, _resolveMode(mode, orReplace)); return await database.executor.doWhenOpened((e) async { final id = await database.executor.runInsert(ctx.sql, ctx.boundVariables); @@ -48,11 +57,19 @@ class InsertStatement { /// All fields in a row that don't have a default value or auto-increment /// must be set and non-null. Otherwise, an [InvalidDataException] will be /// thrown. - /// When a row with the same primary or unique key already exists in the - /// database, the insert will fail. Use [orReplace] to replace rows that - /// already exist. - Future insertAll(List> rows, - {bool orReplace = false}) async { + /// By default, an exception will be thrown if another row with the same + /// primary key already exists. This behavior can be overridden with [mode], + /// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore]. + Future insertAll( + List> rows, { + @Deprecated('Use mode: InsertMode.replace instead') bool orReplace = false, + InsertMode mode, + }) async { + assert( + mode == null || (orReplace != true), + 'If the mode parameter is set on insertAll, orReplace must be null or ' + 'false', + ); final statements = >{}; // Not every insert has the same sql, as fields which are set to null are @@ -61,7 +78,7 @@ class InsertStatement { for (var row in rows) { _validateIntegrity(row); - final ctx = _createContext(row, orReplace); + final ctx = _createContext(row, _resolveMode(mode, orReplace)); statements.putIfAbsent(ctx.sql, () => []).add(ctx); } @@ -76,15 +93,14 @@ class InsertStatement { database.markTablesUpdated({table}); } - GenerationContext _createContext(Insertable entry, bool replace) { + GenerationContext _createContext(Insertable entry, InsertMode mode) { final map = table.entityToSql(entry.createCompanion(true)) ..removeWhere((_, value) => value == null); final ctx = GenerationContext.fromDb(database); ctx.buffer - ..write('INSERT ') - ..write(replace ? 'OR REPLACE ' : '') - ..write('INTO ') + ..write(_insertKeywords[mode]) + ..write(' INTO ') ..write(table.$tableName) ..write(' '); @@ -113,6 +129,11 @@ class InsertStatement { return ctx; } + InsertMode _resolveMode(InsertMode mode, bool orReplace) { + return mode ?? + (orReplace == true ? InsertMode.insertOrReplace : InsertMode.insert); + } + void _validateIntegrity(Insertable d) { if (d == null) { throw InvalidDataException( @@ -124,3 +145,46 @@ class InsertStatement { .throwIfInvalid(d); } } + +/// Enumeration of different insert behaviors. See the documentation on the +/// individual fields for details. +enum InsertMode { + /// A regular `INSERT INTO` statement. When a row with the same primary or + /// unique key already exists, the insert statement will fail and an exception + /// will be thrown. If the exception is caught, previous statements made in + /// the same transaction will NOT be reverted. + insert, + + /// Identical to [InsertMode.insertOrReplace], included for the sake of + /// completeness. + replace, + + /// Like [insert], but if a row with the same primary or unique key already + /// exists, it will be deleted and re-created with the row being inserted. + insertOrReplace, + + /// Similar to [InsertMode.insertOrAbort], but it will revert the surrounding + /// transaction if a constraint is violated, even if the thrown exception is + /// caught. + insertOrRollback, + + /// Identical to [insert], included for the sake of completeness. + insertOrAbort, + + /// Like [insert], but if multiple values are inserted with the same insert + /// statement and one of them fails, the others will still be completed. + insertOrFail, + + /// Like [insert], but failures will be ignored. + insertOrIgnore, +} + +const _insertKeywords = { + InsertMode.insert: 'INSERT', + InsertMode.replace: 'REPLACE', + InsertMode.insertOrReplace: 'INSERT OR REPLACE', + InsertMode.insertOrRollback: 'INSERT OR ROLLBACK', + InsertMode.insertOrAbort: 'INSERT OR ABORT', + InsertMode.insertOrFail: 'INSERT OR FAIL', + InsertMode.insertOrIgnore: 'INSERT OR IGNORE', +}; diff --git a/moor/test/insert_test.dart b/moor/test/insert_test.dart index d3b54b1d..84f7f86a 100644 --- a/moor/test/insert_test.dart +++ b/moor/test/insert_test.dart @@ -37,6 +37,20 @@ void main() { }); test('generates insert or replace statements', () async { + await db.into(db.todosTable).insert( + TodoEntry( + id: 113, + content: 'Done', + ), + mode: InsertMode.insertOrReplace); + + verify(executor.runInsert( + 'INSERT OR REPLACE INTO todos (id, content) VALUES (?, ?)', + [113, 'Done'])); + }); + + test('generates insert or replace statements with legacy parameter', + () async { await db.into(db.todosTable).insert( TodoEntry( id: 113,