Support multiple upsert clauses

This commit is contained in:
Simon Binder 2021-06-30 21:31:29 +02:00
parent dcb1c4776b
commit a1c9a5933d
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
6 changed files with 70 additions and 12 deletions

View File

@ -37,7 +37,7 @@ class Batch {
/// See also:
/// - [InsertStatement.insert], which would be used outside a [Batch].
void insert<T extends Table, D>(TableInfo<T, D> table, Insertable<D> row,
{InsertMode? mode, DoUpdate<T, D>? onConflict}) {
{InsertMode? mode, UpsertClause<T, D>? onConflict}) {
_addUpdate(table, UpdateKind.insert);
final actualMode = mode ?? InsertMode.insert;
final context = InsertStatement<Table, D>(_user, table)
@ -59,7 +59,7 @@ class Batch {
/// support it. For details and examples, see [InsertStatement.insert].
void insertAll<T extends Table, D>(
TableInfo<T, D> table, List<Insertable<D>> rows,
{InsertMode? mode, DoUpdate<T, D>? onConflict}) {
{InsertMode? mode, UpsertClause<T, D>? onConflict}) {
for (final row in rows) {
insert<T, D>(table, row, mode: mode, onConflict: onConflict);
}

View File

@ -61,7 +61,7 @@ class InsertStatement<T extends Table, D> {
Future<int> insert(
Insertable<D> entity, {
InsertMode? mode,
DoUpdate<T, D>? onConflict,
UpsertClause<T, D>? onConflict,
}) async {
final ctx = createContext(entity, mode ?? InsertMode.insert,
onConflict: onConflict);
@ -81,7 +81,7 @@ class InsertStatement<T extends Table, D> {
/// this method, make sure that you have a recent sqlite3 version available.
/// This is the case with `sqlite3_flutter_libs`.
Future<D> insertReturning(Insertable<D> entity,
{InsertMode? mode, DoUpdate<T, D>? onConflict}) async {
{InsertMode? mode, UpsertClause<T, D>? onConflict}) async {
final ctx = createContext(entity, mode ?? InsertMode.insert,
onConflict: onConflict, returning: true);
@ -113,7 +113,7 @@ class InsertStatement<T extends Table, D> {
///
/// This method is used internally by moor. Consider using [insert] instead.
GenerationContext createContext(Insertable<D> entry, InsertMode mode,
{DoUpdate<T, D>? onConflict, bool returning = false}) {
{UpsertClause<T, D>? onConflict, bool returning = false}) {
_validateIntegrity(entry);
final rawValues = entry.toColumns(true);
@ -148,7 +148,7 @@ class InsertStatement<T extends Table, D> {
writeInsertable(ctx, map);
}
if (onConflict != null) {
void writeDoUpdate(DoUpdate<T, D> onConflict) {
final upsertInsertable = onConflict._createInsertable(table.asDslTable);
if (!identical(entry, upsertInsertable)) {
@ -194,6 +194,12 @@ class InsertStatement<T extends Table, D> {
}
}
if (onConflict is DoUpdate<T, D>) {
writeDoUpdate(onConflict);
} else if (onConflict is UpsertMultiple<T, D>) {
onConflict.clauses.forEach(writeDoUpdate);
}
if (returning) {
ctx.buffer.write(' RETURNING *');
}
@ -278,11 +284,17 @@ const _insertKeywords = <InsertMode, String>{
InsertMode.insertOrIgnore: 'INSERT OR IGNORE',
};
/// A upsert clause controls how to behave when a uniqueness constraint is
/// violated during an insert.
///
/// Typically, one would use [DoUpdate] to run an update instead in this case.
abstract class UpsertClause<T extends Table, D> {}
/// 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> {
class DoUpdate<T extends Table, D> extends UpsertClause<T, D> {
final Insertable<D> Function(T old) _creator;
/// An optional list of columns to serve as an "conflict target", which
@ -299,3 +311,19 @@ class DoUpdate<T extends Table, D> {
return _creator(table);
}
}
/// Upsert clause that consists of multiple [clauses].
///
/// The first [DoUpdate.target] matched by this upsert will be run.
class UpsertMultiple<T extends Table, D> extends UpsertClause<T, D> {
/// All [DoUpdate] clauses that are part of this upsert.
///
/// The first clause with a matching [DoUpdate.target] will be considered.
final List<DoUpdate<T, D>> clauses;
/// Creates an upsert consisting of multiple [DoUpdate] clauses.
///
/// This requires a fairly recent sqlite3 version (3.35.0, released on 2021-
/// 03-12).
UpsertMultiple(this.clauses);
}

View File

@ -4,7 +4,6 @@ import 'package:test/test.dart';
import 'data/tables/todos.dart';
import 'data/utils/mocks.dart';
import 'skips.dart';
void main() {
late TodoDb db;
@ -93,7 +92,7 @@ void main() {
{const TableUpdate('users', kind: UpdateKind.insert)}));
expect(user.id, 5);
}, skip: onNoReturningSupport());
});
group('enforces integrity', () {
test('for regular inserts', () async {
@ -206,6 +205,37 @@ void main() {
));
});
test(
'can use multiple upsert targets',
() async {
await db
.into(db.todosTable)
.insert(TodosTableCompanion.insert(content: 'my content'),
onConflict: UpsertMultiple([
DoUpdate(
(old) {
return TodosTableCompanion.custom(
content: const Variable('important: ') + old.content);
},
),
DoUpdate(
(old) {
return TodosTableCompanion.custom(
content: const Variable('second: ') + old.content);
},
target: [db.todosTable.content],
),
]));
verify(executor.runInsert(
'INSERT INTO todos (content) VALUES (?) '
'ON CONFLICT(id) DO UPDATE SET content = ? || content '
'ON CONFLICT(content) DO UPDATE SET content = ? || content',
argThat(equals(['my content', 'important: ', 'second: '])),
));
},
);
test('can use a custom conflict clause', () async {
await db.into(db.todosTable).insert(
TodosTableCompanion.insert(content: 'my content'),

View File

@ -38,5 +38,5 @@ void main() {
priority: CategoryPriority.low,
),
);
}, skip: onNoReturningSupport());
}, skip: ifOlderThanSqlite335());
}

View File

@ -118,5 +118,5 @@ void main() {
),
);
});
}, skip: onNoReturningSupport());
}, skip: ifOlderThanSqlite335());
}

View File

@ -1,5 +1,5 @@
import 'package:sqlite3/sqlite3.dart';
String? onNoReturningSupport() => sqlite3.version.versionNumber > 3035000
String? ifOlderThanSqlite335() => sqlite3.version.versionNumber > 3035000
? null
: 'RETURNING not supported by sqlite version ${sqlite3.version.libVersion}';