drift/moor/lib/src/runtime/query_builder/statements/insert.dart

267 lines
8.9 KiB
Dart

part of '../query_builder.dart';
/// Represents an insert statement
class InsertStatement<T extends Table, D extends DataClass> {
/// The database to use then executing this statement
@protected
final QueryEngine database;
/// The table we're inserting into
@protected
final TableInfo<T, D> table;
/// Constructs an insert statement from the database and the table. Used
/// internally by moor.
InsertStatement(this.database, this.table);
/// Inserts a row constructed from the fields in [entity].
///
/// All fields in the entity that don't have a default value or auto-increment
/// must be set and non-null. Otherwise, an [InvalidDataException] 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].
///
/// 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.
/// Be aware that upsert clauses and [onConflict] are not available on older
/// sqlite versions.
///
/// Returns the `rowid` of the inserted row. For tables with an auto-increment
/// column, the `rowid` is the generated value of that column. The returned
/// value can be inaccurate when [onConflict] is set and the insert behaved
/// like an update.
///
/// If the table doesn't have a `rowid`, you can't rely on the return value.
/// Still, the future will always complete with an error if the insert fails.
Future<int> insert(
Insertable<D> entity, {
InsertMode mode,
DoUpdate<T, D> onConflict,
}) async {
final ctx = createContext(entity, mode ?? InsertMode.insert,
onConflict: onConflict);
return await database.doWhenOpened((e) async {
final id = await e.runInsert(ctx.sql, ctx.boundVariables);
database
.notifyUpdates({TableUpdate.onTable(table, kind: UpdateKind.insert)});
return id;
});
}
/// Attempts to [insert] [entity] into the database. If the insert would
/// violate a primary key or uniqueness constraint, updates the columns that
/// are present on [entity].
///
/// Note that this is subtly different from [InsertMode.replace]! When using
/// [InsertMode.replace], the old row will be deleted and replaced with the
/// new row. With [insertOnConflictUpdate], columns from the old row that are
/// not present on [entity] are unchanged, and no row will be deleted.
///
/// Be aware that [insertOnConflictUpdate] uses an upsert clause, which is not
/// available on older sqlite implementations.
Future<int> insertOnConflictUpdate(Insertable<D> entity) {
return insert(entity, onConflict: DoUpdate((_) => entity));
}
/// Creates a [GenerationContext] which contains the sql necessary to run an
/// insert statement fro the [entry] with the [mode].
///
/// This method is used internally by moor. Consider using [insert] instead.
GenerationContext createContext(Insertable<D> entry, InsertMode mode,
{DoUpdate<T, D> onConflict}) {
_validateIntegrity(entry);
final rawValues = entry.toColumns(true);
// apply default values for columns that have one
final map = <String, Expression>{};
for (final column in table.$columns) {
final columnName = column.$name;
if (rawValues.containsKey(columnName)) {
map[columnName] = rawValues[columnName];
} else {
if (column.clientDefault != null) {
map[columnName] = column._evaluateClientDefault();
}
}
// column not set, and doesn't have a client default. So just don't
// include this column
}
final ctx = GenerationContext.fromDb(database);
ctx.buffer
..write(_insertKeywords[mode])
..write(' INTO ')
..write(table.$tableName)
..write(' ');
if (map.isEmpty) {
ctx.buffer.write('DEFAULT VALUES');
} else {
final columns = map.keys.map(escapeIfNeeded);
ctx.buffer
..write('(')
..write(columns.join(', '))
..write(') ')
..write('VALUES (');
var first = true;
for (final variable in map.values) {
if (!first) {
ctx.buffer.write(', ');
}
first = false;
variable.writeInto(ctx);
}
ctx.buffer.write(')');
}
if (onConflict != null) {
final upsertInsertable = onConflict._createInsertable(table.asDslTable);
if (!identical(entry, upsertInsertable)) {
// We run a ON CONFLICT DO UPDATE, so make sure upsertInsertable is
// valid for updates.
// the identical check is a performance optimization - for the most
// common call (insertOnConflictUpdate) we don't have to check twice.
table
.validateIntegrity(upsertInsertable, isInserting: false)
.throwIfInvalid(upsertInsertable);
}
final updateSet = upsertInsertable.toColumns(true);
ctx.buffer.write(' ON CONFLICT(');
final conflictTarget = onConflict.target ?? table.$primaryKey.toList();
var first = true;
for (final target in conflictTarget) {
if (!first) ctx.buffer.write(', ');
target.writeInto(ctx);
first = false;
}
ctx.buffer.write(') DO UPDATE SET ');
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;
}
void _validateIntegrity(Insertable<D> d) {
if (d == null) {
throw InvalidDataException(
'Cannot write null row into ${table.$tableName}');
}
table.validateIntegrity(d, isInserting: true).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',
};
/// 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;
/// An optional list of columns to serve as an "conflict target", which
/// specifies the uniqueness constraint that will trigger the upsert.
///
/// By default, the primary key of the table will be used.
final List<Column> /*?*/ target;
/// For an example, see [InsertStatement.insert].
DoUpdate(Insertable<D> Function(T old) update, {this.target})
: _creator = update;
Insertable<D> _createInsertable(T table) {
return _creator(table);
}
}