diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index 22c70870..a8297c82 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.6.0-dev + +- Add `DoUpdate.withExcluded` to refer to the excluded row in an upsert clause. + ## 4.5.0 - Add `moorRuntimeOptions.debugPrint` option to control which `print` method is used by moor. diff --git a/moor/lib/src/dsl/columns.dart b/moor/lib/src/dsl/columns.dart index 9ebaf431..6460c10b 100644 --- a/moor/lib/src/dsl/columns.dart +++ b/moor/lib/src/dsl/columns.dart @@ -5,6 +5,15 @@ part of 'dsl.dart'; abstract class Column extends Expression { @override 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. diff --git a/moor/lib/src/dsl/dsl.dart b/moor/lib/src/dsl/dsl.dart index 27bb2e7d..34129931 100644 --- a/moor/lib/src/dsl/dsl.dart +++ b/moor/lib/src/dsl/dsl.dart @@ -3,6 +3,7 @@ import 'dart:typed_data' show Uint8List; import 'package:meta/meta.dart'; import 'package:meta/meta_meta.dart'; import 'package:moor/moor.dart'; +import 'package:moor/sqlite_keywords.dart'; part 'columns.dart'; part 'database.dart'; diff --git a/moor/lib/src/runtime/query_builder/schema/column_impl.dart b/moor/lib/src/runtime/query_builder/schema/column_impl.dart index 5c6174dc..33c9ce67 100644 --- a/moor/lib/src/runtime/query_builder/schema/column_impl.dart +++ b/moor/lib/src/runtime/query_builder/schema/column_impl.dart @@ -7,10 +7,7 @@ const VerificationResult _invalidNull = VerificationResult.failure( /// Implementation for a [Column] declared on a table. class GeneratedColumn extends Column { /// The sql name of this column. - final String $name; - - /// [$name], but escaped if it's an sql keyword. - String get escapedName => escapeIfNeeded($name); + final String $name; // todo: Remove, replace with `name` /// The name of the table that contains this column final String tableName; @@ -55,6 +52,9 @@ class GeneratedColumn extends Column { bool get hasAutoIncrement => _defaultConstraints?.contains('AUTOINCREMENT') == true; + @override + String get name => $name; + /// Used by generated code. GeneratedColumn( this.$name, diff --git a/moor/lib/src/runtime/query_builder/statements/insert.dart b/moor/lib/src/runtime/query_builder/statements/insert.dart index 80adea80..12250034 100644 --- a/moor/lib/src/runtime/query_builder/statements/insert.dart +++ b/moor/lib/src/runtime/query_builder/statements/insert.dart @@ -149,7 +149,10 @@ class InsertStatement { } void writeDoUpdate(DoUpdate onConflict) { - final upsertInsertable = onConflict._createInsertable(table.asDslTable); + if (onConflict._usesExcludedTable) { + ctx.hasMultipleTables = true; + } + final upsertInsertable = onConflict._createInsertable(table); if (!identical(entry, upsertInsertable)) { // We run a ON CONFLICT DO UPDATE, so make sure upsertInsertable is @@ -176,7 +179,9 @@ class InsertStatement { for (final target in conflictTarget) { 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; } @@ -295,7 +300,8 @@ abstract class UpsertClause {} /// /// For an example, see [InsertStatement.insert]. class DoUpdate extends UpsertClause { - final Insertable Function(T old) _creator; + final Insertable Function(T old, T excluded) _creator; + final bool _usesExcludedTable; /// An optional list of columns to serve as an "conflict target", which /// specifies the uniqueness constraint that will trigger the upsert. @@ -303,12 +309,32 @@ class DoUpdate extends UpsertClause { /// By default, the primary key of the table will be used. final List? 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]. DoUpdate(Insertable Function(T old) update, {this.target}) - : _creator = update; + : _creator = ((old, _) => update(old)), + _usesExcludedTable = false; - Insertable _createInsertable(T table) { - return _creator(table); + /// 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. + /// 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 _createInsertable(TableInfo table) { + return _creator(table.asDslTable, table.createAlias('excluded').asDslTable); } } diff --git a/moor/pubspec.yaml b/moor/pubspec.yaml index fbff8ccc..fa945370 100644 --- a/moor/pubspec.yaml +++ b/moor/pubspec.yaml @@ -1,6 +1,6 @@ name: moor 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 homepage: https://moor.simonbinder.eu/ issue_tracker: https://github.com/simolus3/moor/issues diff --git a/moor/test/insert_test.dart b/moor/test/insert_test.dart index 5f45d1d3..2e7fb2ac 100644 --- a/moor/test/insert_test.dart +++ b/moor/test/insert_test.dart @@ -266,6 +266,24 @@ void main() { 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 { await db.into(db.categories).insert(CategoriesCompanion.insert( description: 'description', diff --git a/moor/test/integration_tests/insert_integration_test.dart b/moor/test/integration_tests/insert_integration_test.dart index 07e5bd0f..fa044dc9 100644 --- a/moor/test/integration_tests/insert_integration_test.dart +++ b/moor/test/integration_tests/insert_integration_test.dart @@ -26,6 +26,27 @@ void main() { 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 { final entry = await db.into(db.categories).insertReturning( CategoriesCompanion.insert(description: 'Description'));