Support upsert clauses from Dart DSL (#367)

This commit is contained in:
Simon Binder 2020-04-17 22:14:27 +02:00
parent a7ac6db55d
commit ca0fe1ef55
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
4 changed files with 86 additions and 8 deletions

View File

@ -34,8 +34,8 @@ class Batch {
{InsertMode mode}) { {InsertMode mode}) {
_addUpdate(table, UpdateKind.insert); _addUpdate(table, UpdateKind.insert);
final actualMode = mode ?? InsertMode.insert; final actualMode = mode ?? InsertMode.insert;
final context = final context = InsertStatement<Table, D>(_engine, table)
InsertStatement<D>(_engine, table).createContext(row, actualMode); .createContext(row, actualMode);
_addContext(context); _addContext(context);
} }

View File

@ -102,8 +102,10 @@ mixin QueryEngine on DatabaseConnectionUser {
/// to write data into the [table] by using [InsertStatement.insert]. /// to write data into the [table] by using [InsertStatement.insert].
@protected @protected
@visibleForTesting @visibleForTesting
InsertStatement<T> into<T extends DataClass>(TableInfo<Table, T> table) => InsertStatement<T, D> into<T extends Table, D extends DataClass>(
InsertStatement<T>(_resolvedEngine, table); TableInfo<T, D> table) {
return InsertStatement<T, D>(_resolvedEngine, table);
}
/// Starts an [UpdateStatement] for the given table. You can use that /// Starts an [UpdateStatement] for the given table. You can use that
/// statement to update individual rows in that table by setting a where /// statement to update individual rows in that table by setting a where

View File

@ -1,14 +1,14 @@
part of '../query_builder.dart'; part of '../query_builder.dart';
/// Represents an insert statements /// Represents an insert statements
class InsertStatement<D extends DataClass> { class InsertStatement<T extends Table, D extends DataClass> {
/// The database to use then executing this statement /// The database to use then executing this statement
@protected @protected
final QueryEngine database; final QueryEngine database;
/// The table we're inserting into /// The table we're inserting into
@protected @protected
final TableInfo<Table, D> table; final TableInfo<T, D> table;
/// Constructs an insert statement from the database and the table. Used /// Constructs an insert statement from the database and the table. Used
/// internally by moor. /// internally by moor.
@ -24,6 +24,31 @@ class InsertStatement<D extends DataClass> {
/// primary key already exists. This behavior can be overridden with [mode], /// primary key already exists. This behavior can be overridden with [mode],
/// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore]. /// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore].
/// ///
/// To apply a partial or custom update in case of a conflict, you can also
/// use an [upsert clause](https://sqlite.org/lang_UPSERT.html) by using
/// [onConflict].
/// For instance, you could increase a counter whenever a conflict occurs:
///
/// ```dart
/// class Words extends Table {
/// TextColumn get word => text()();
/// IntColumn get occurrences => integer()();
/// }
///
/// Future<void> addWord(String word) async {
/// await into(words).insert(
/// WordsCompanion.insert(word: word, occurrences: 1),
/// onConflict: DoUpdate((old) => WordsCompanion.custom(
/// occurrences: old.occurrences + Constant(1),
/// )),
/// );
/// }
/// ```
///
/// When calling `addWord` with a word not yet saved, the regular insert will
/// write it with one occurrence. If it already exists however, the insert
/// behaves like an update incrementing occurrences by one.
///
/// 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 complete with an error if the insert /// return value, but the future will complete with an error if the insert
@ -31,8 +56,10 @@ class InsertStatement<D extends DataClass> {
Future<int> insert( Future<int> insert(
Insertable<D> entity, { Insertable<D> entity, {
InsertMode mode, InsertMode mode,
DoUpdate<T, D> onConflict,
}) async { }) async {
final ctx = createContext(entity, mode ?? InsertMode.insert); final ctx = createContext(entity, mode ?? InsertMode.insert,
onConflict: onConflict);
return await database.doWhenOpened((e) async { return await database.doWhenOpened((e) async {
final id = await e.runInsert(ctx.sql, ctx.boundVariables); final id = await e.runInsert(ctx.sql, ctx.boundVariables);
@ -46,7 +73,8 @@ class InsertStatement<D extends DataClass> {
/// insert statement fro the [entry] with the [mode]. /// insert statement fro the [entry] with the [mode].
/// ///
/// This method is used internally by moor. Consider using [insert] instead. /// This method is used internally by moor. Consider using [insert] instead.
GenerationContext createContext(Insertable<D> entry, InsertMode mode) { GenerationContext createContext(Insertable<D> entry, InsertMode mode,
{DoUpdate onConflict}) {
_validateIntegrity(entry); _validateIntegrity(entry);
final rawValues = entry.toColumns(true); final rawValues = entry.toColumns(true);
@ -99,6 +127,23 @@ class InsertStatement<D extends DataClass> {
ctx.buffer.write(')'); ctx.buffer.write(')');
} }
if (onConflict != null) {
final updateSet = onConflict._createInsertable(table).toColumns(true);
ctx.buffer.write(' ON CONFLICT DO UPDATE SET ');
var first = true;
for (final update in updateSet.entries) {
final column = escapeIfNeeded(update.key);
if (!first) ctx.buffer.write(', ');
ctx.buffer.write('$column = ');
update.value.writeInto(ctx);
first = false;
}
}
return ctx; return ctx;
} }
@ -154,3 +199,18 @@ const _insertKeywords = <InsertMode, String>{
InsertMode.insertOrFail: 'INSERT OR FAIL', InsertMode.insertOrFail: 'INSERT OR FAIL',
InsertMode.insertOrIgnore: 'INSERT OR IGNORE', InsertMode.insertOrIgnore: 'INSERT OR IGNORE',
}; };
/// A [DoUpdate] upsert clause can be used to insert or update a custom
/// companion when the underlying companion already exists.
///
/// For an example, see [InsertStatement.insert].
class DoUpdate<T extends Table, D extends DataClass> {
final Insertable<D> Function(T old) _creator;
/// For an example, see [InsertStatement.insert].
DoUpdate(Insertable<D> Function(T old) update) : _creator = update;
Insertable<D> _createInsertable(T table) {
return _creator(table);
}
}

View File

@ -145,4 +145,20 @@ void main() {
), ),
); );
}); });
test('can use an upsert clause', () async {
await db.into(db.todosTable).insert(
TodosTableCompanion.insert(content: 'my content'),
onConflict: DoUpdate((old) {
return TodosTableCompanion.custom(
content: const Variable('important: ') + old.content);
}),
);
verify(executor.runInsert(
'INSERT INTO todos (content) VALUES (?) '
'ON CONFLICT DO UPDATE SET content = ? || content',
argThat(equals(['my content', 'important: '])),
));
});
} }