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
/// 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<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);
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<D extends DataClass> {
/// 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<void> insertAll(List<Insertable<D>> 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<void> insertAll(
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>>{};
// 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) {
_validateIntegrity(row);
final ctx = _createContext(row, orReplace);
final ctx = _createContext(row, _resolveMode(mode, orReplace));
statements.putIfAbsent(ctx.sql, () => []).add(ctx);
}
@ -76,14 +93,13 @@ class InsertStatement<D extends DataClass> {
database.markTablesUpdated({table});
}
GenerationContext _createContext(Insertable<D> entry, bool replace) {
GenerationContext _createContext(Insertable<D> 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(_insertKeywords[mode])
..write(' INTO ')
..write(table.$tableName)
..write(' ');
@ -113,6 +129,11 @@ class InsertStatement<D extends DataClass> {
return ctx;
}
InsertMode _resolveMode(InsertMode mode, bool orReplace) {
return mode ??
(orReplace == true ? InsertMode.insertOrReplace : InsertMode.insert);
}
void _validateIntegrity(Insertable<D> d) {
if (d == null) {
throw InvalidDataException(
@ -124,3 +145,46 @@ class InsertStatement<D extends DataClass> {
.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 {
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,