Support excluded in upserts on the Dart API

This commit is contained in:
Simon Binder 2021-09-22 17:08:37 +02:00
parent 9fffebbae6
commit 90e179643c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 90 additions and 11 deletions

View File

@ -1,3 +1,7 @@
## 4.6.0-dev
- Add `DoUpdate.withExcluded` to refer to the excluded row in an upsert clause.
## 4.5.0 ## 4.5.0
- Add `moorRuntimeOptions.debugPrint` option to control which `print` method is used by moor. - Add `moorRuntimeOptions.debugPrint` option to control which `print` method is used by moor.

View File

@ -5,6 +5,15 @@ part of 'dsl.dart';
abstract class Column<T> extends Expression<T> { abstract class Column<T> extends Expression<T> {
@override @override
final Precedence precedence = Precedence.primary; final Precedence precedence = Precedence.primary;
/// The (unescaped) name of this column.
///
/// Use [escapedName] to access a name that's escaped in double quotes if
/// needed.
String get name;
/// [name], but escaped if it's an sql keyword.
String get escapedName => escapeIfNeeded(name);
} }
/// A column that stores int values. /// A column that stores int values.

View File

@ -3,6 +3,7 @@ import 'dart:typed_data' show Uint8List;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:meta/meta_meta.dart'; import 'package:meta/meta_meta.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:moor/sqlite_keywords.dart';
part 'columns.dart'; part 'columns.dart';
part 'database.dart'; part 'database.dart';

View File

@ -7,10 +7,7 @@ const VerificationResult _invalidNull = VerificationResult.failure(
/// Implementation for a [Column] declared on a table. /// Implementation for a [Column] declared on a table.
class GeneratedColumn<T> extends Column<T> { class GeneratedColumn<T> extends Column<T> {
/// The sql name of this column. /// The sql name of this column.
final String $name; final String $name; // todo: Remove, replace with `name`
/// [$name], but escaped if it's an sql keyword.
String get escapedName => escapeIfNeeded($name);
/// The name of the table that contains this column /// The name of the table that contains this column
final String tableName; final String tableName;
@ -55,6 +52,9 @@ class GeneratedColumn<T> extends Column<T> {
bool get hasAutoIncrement => bool get hasAutoIncrement =>
_defaultConstraints?.contains('AUTOINCREMENT') == true; _defaultConstraints?.contains('AUTOINCREMENT') == true;
@override
String get name => $name;
/// Used by generated code. /// Used by generated code.
GeneratedColumn( GeneratedColumn(
this.$name, this.$name,

View File

@ -149,7 +149,10 @@ class InsertStatement<T extends Table, D> {
} }
void writeDoUpdate(DoUpdate<T, D> onConflict) { void writeDoUpdate(DoUpdate<T, D> onConflict) {
final upsertInsertable = onConflict._createInsertable(table.asDslTable); if (onConflict._usesExcludedTable) {
ctx.hasMultipleTables = true;
}
final upsertInsertable = onConflict._createInsertable(table);
if (!identical(entry, upsertInsertable)) { if (!identical(entry, upsertInsertable)) {
// We run a ON CONFLICT DO UPDATE, so make sure upsertInsertable is // We run a ON CONFLICT DO UPDATE, so make sure upsertInsertable is
@ -176,7 +179,9 @@ class InsertStatement<T extends Table, D> {
for (final target in conflictTarget) { for (final target in conflictTarget) {
if (!first) ctx.buffer.write(', '); if (!first) ctx.buffer.write(', ');
target.writeInto(ctx); // Writing the escaped name directly because it should not have a table
// name in front of it.
ctx.buffer.write(target.escapedName);
first = false; first = false;
} }
@ -295,7 +300,8 @@ abstract class UpsertClause<T extends Table, D> {}
/// ///
/// For an example, see [InsertStatement.insert]. /// For an example, see [InsertStatement.insert].
class DoUpdate<T extends Table, D> extends UpsertClause<T, D> { class DoUpdate<T extends Table, D> extends UpsertClause<T, D> {
final Insertable<D> Function(T old) _creator; final Insertable<D> Function(T old, T excluded) _creator;
final bool _usesExcludedTable;
/// An optional list of columns to serve as an "conflict target", which /// An optional list of columns to serve as an "conflict target", which
/// specifies the uniqueness constraint that will trigger the upsert. /// specifies the uniqueness constraint that will trigger the upsert.
@ -303,12 +309,32 @@ class DoUpdate<T extends Table, D> extends UpsertClause<T, D> {
/// By default, the primary key of the table will be used. /// By default, the primary key of the table will be used.
final List<Column>? target; final List<Column>? target;
/// Creates a `DO UPDATE` clause.
///
/// The [update] function will be used to construct an [Insertable] used to
/// update an old row that prevented an insert.
/// If you need to refer to both the old row and the row that would have
/// been inserted, use [DoUpdate.withExcluded].
///
/// For an example, see [InsertStatement.insert]. /// For an example, see [InsertStatement.insert].
DoUpdate(Insertable<D> Function(T old) update, {this.target}) DoUpdate(Insertable<D> Function(T old) update, {this.target})
: _creator = update; : _creator = ((old, _) => update(old)),
_usesExcludedTable = false;
Insertable<D> _createInsertable(T table) { /// Creates a `DO UPDATE` clause.
return _creator(table); ///
/// The [update] function will be used to construct an [Insertable] used to
/// update an old row that prevented an insert.
/// It can refer to the values from the old row in the first parameter and
/// to columns in the row that couldn't be inserted with the `excluded`
/// parameter.
///
/// For an example, see [InsertStatement.insert].
DoUpdate.withExcluded(this._creator, {this.target})
: _usesExcludedTable = true;
Insertable<D> _createInsertable(TableInfo<T, D> table) {
return _creator(table.asDslTable, table.createAlias('excluded').asDslTable);
} }
} }

View File

@ -1,6 +1,6 @@
name: moor name: moor
description: Moor is a safe and reactive persistence library for Dart applications description: Moor is a safe and reactive persistence library for Dart applications
version: 4.5.0 version: 4.6.0-dev
repository: https://github.com/simolus3/moor repository: https://github.com/simolus3/moor
homepage: https://moor.simonbinder.eu/ homepage: https://moor.simonbinder.eu/
issue_tracker: https://github.com/simolus3/moor/issues issue_tracker: https://github.com/simolus3/moor/issues

View File

@ -266,6 +266,24 @@ void main() {
expect(id, 3); expect(id, 3);
}); });
test('can access excluded row in upsert', () async {
await db.into(db.todosTable).insert(
TodosTableCompanion.insert(content: 'content'),
onConflict: DoUpdate.withExcluded(
(old, excluded) => TodosTableCompanion.custom(
content: old.content + excluded.content,
),
),
);
verify(executor.runInsert(
'INSERT INTO todos (content) VALUES (?) '
'ON CONFLICT(id) DO UPDATE '
'SET content = todos.content || excluded.content',
['content'],
));
});
test('applies implicit type converter', () async { test('applies implicit type converter', () async {
await db.into(db.categories).insert(CategoriesCompanion.insert( await db.into(db.categories).insert(CategoriesCompanion.insert(
description: 'description', description: 'description',

View File

@ -26,6 +26,27 @@ void main() {
expect(row.description, 'changed description'); expect(row.description, 'changed description');
}); });
test('insert with DoUpdate and excluded row', () async {
await db.into(db.categories).insert(
CategoriesCompanion.insert(description: 'original description'));
var row = await db.select(db.categories).getSingle();
await db.into(db.categories).insert(
CategoriesCompanion(
id: Value(row.id),
description: const Value('new description'),
),
onConflict: DoUpdate.withExcluded(
(old, excluded) => CategoriesCompanion.custom(
description:
old.description + const Constant(' ') + excluded.description),
));
row = await db.select(db.categories).getSingle();
expect(row.description, 'original description new description');
});
test('returning', () async { test('returning', () async {
final entry = await db.into(db.categories).insertReturning( final entry = await db.into(db.categories).insertReturning(
CategoriesCompanion.insert(description: 'Description')); CategoriesCompanion.insert(description: 'Description'));