Support additional insert modes

This commit is contained in:
Simon Binder 2019-10-21 17:14:58 +02:00
parent 82477d9325
commit ccf208b329
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
2 changed files with 93 additions and 15 deletions

View File

@ -24,17 +24,26 @@ class InsertStatement<D extends DataClass> {
/// must be set and non-null. Otherwise, an [InvalidDataException] will be /// must be set and non-null. Otherwise, an [InvalidDataException] will be
/// thrown. /// thrown.
/// ///
/// If [orReplace] is true and a row with the same primary key already exists, /// By default, an exception will be thrown if another row with the same
/// the columns of that row will be updated and no new row will be written. /// primary key already exists. This behavior can be overridden with [mode],
/// Otherwise, an exception will be thrown. /// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore].
/// ///
/// If the table contains an auto-increment column, the generated value will /// 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 /// 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 /// return value, but the future will resolve to an error when the insert
/// fails. /// fails.
Future<int> insert(Insertable<D> entity, {bool orReplace = false}) async { Future<int> insert(
Insertable<D> 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); _validateIntegrity(entity);
final ctx = _createContext(entity, orReplace); final ctx = _createContext(entity, _resolveMode(mode, orReplace));
return await database.executor.doWhenOpened((e) async { return await database.executor.doWhenOpened((e) async {
final id = await database.executor.runInsert(ctx.sql, ctx.boundVariables); final id = await database.executor.runInsert(ctx.sql, ctx.boundVariables);
@ -48,11 +57,19 @@ class InsertStatement<D extends DataClass> {
/// All fields in a row that don't have a default value or auto-increment /// 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 /// must be set and non-null. Otherwise, an [InvalidDataException] will be
/// thrown. /// thrown.
/// When a row with the same primary or unique key already exists in the /// By default, an exception will be thrown if another row with the same
/// database, the insert will fail. Use [orReplace] to replace rows that /// primary key already exists. This behavior can be overridden with [mode],
/// already exist. /// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore].
Future<void> insertAll(List<Insertable<D>> rows, Future<void> insertAll(
{bool orReplace = false}) async { List<Insertable<D>> 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 = <String, List<GenerationContext>>{}; final statements = <String, List<GenerationContext>>{};
// Not every insert has the same sql, as fields which are set to null are // Not every insert has the same sql, as fields which are set to null are
@ -61,7 +78,7 @@ class InsertStatement<D extends DataClass> {
for (var row in rows) { for (var row in rows) {
_validateIntegrity(row); _validateIntegrity(row);
final ctx = _createContext(row, orReplace); final ctx = _createContext(row, _resolveMode(mode, orReplace));
statements.putIfAbsent(ctx.sql, () => []).add(ctx); statements.putIfAbsent(ctx.sql, () => []).add(ctx);
} }
@ -76,15 +93,14 @@ class InsertStatement<D extends DataClass> {
database.markTablesUpdated({table}); database.markTablesUpdated({table});
} }
GenerationContext _createContext(Insertable<D> entry, bool replace) { GenerationContext _createContext(Insertable<D> entry, InsertMode mode) {
final map = table.entityToSql(entry.createCompanion(true)) final map = table.entityToSql(entry.createCompanion(true))
..removeWhere((_, value) => value == null); ..removeWhere((_, value) => value == null);
final ctx = GenerationContext.fromDb(database); final ctx = GenerationContext.fromDb(database);
ctx.buffer ctx.buffer
..write('INSERT ') ..write(_insertKeywords[mode])
..write(replace ? 'OR REPLACE ' : '') ..write(' INTO ')
..write('INTO ')
..write(table.$tableName) ..write(table.$tableName)
..write(' '); ..write(' ');
@ -113,6 +129,11 @@ class InsertStatement<D extends DataClass> {
return ctx; return ctx;
} }
InsertMode _resolveMode(InsertMode mode, bool orReplace) {
return mode ??
(orReplace == true ? InsertMode.insertOrReplace : InsertMode.insert);
}
void _validateIntegrity(Insertable<D> d) { void _validateIntegrity(Insertable<D> d) {
if (d == null) { if (d == null) {
throw InvalidDataException( throw InvalidDataException(
@ -124,3 +145,46 @@ class InsertStatement<D extends DataClass> {
.throwIfInvalid(d); .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, String>{
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',
};

View File

@ -37,6 +37,20 @@ void main() {
}); });
test('generates insert or replace statements', () async { 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( await db.into(db.todosTable).insert(
TodoEntry( TodoEntry(
id: 113, id: 113,