From e7a76b19bcb1accfcc5983fed4b8aea1f57a29e2 Mon Sep 17 00:00:00 2001 From: Timm Preetz Date: Mon, 2 Dec 2024 17:05:34 +0100 Subject: [PATCH] Add `delete(where:)` --- CHANGELOG.md | 1 + lib/src/index_entity_store.dart | 55 +++++++++++++++++++++-- test/indexed_entity_store_test.dart | 70 +++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 816550a..5486e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Add tests for internal schema migrations * Speed up `writeMany` by defaulting to use a single statement for all inserts (as opposed to a single transactions with many individual inserts) * One can revert to the previous behavior by setting `singleStatement: false` in the call +* Add `delete(where: )` to delete all rows matching a specific index-query ## 2.0.0 diff --git a/lib/src/index_entity_store.dart b/lib/src/index_entity_store.dart index 2e8f858..60baafc 100644 --- a/lib/src/index_entity_store.dart +++ b/lib/src/index_entity_store.dart @@ -321,7 +321,7 @@ class IndexedEntityStore { final Iterable? entities, final K? key, final Iterable? keys, - // TODO(tp): QueryBuilder? where, + final QueryBuilder? where, final bool? all, }) { assert( @@ -329,22 +329,35 @@ class IndexedEntityStore { entities != null || key != null || keys != null || + where != null || all != null, ); assert( all == null || - (entity == null && entities == null && key == null && keys == null), + (entity == null && + entities == null && + key == null && + keys == null && + where == null), ); if (all == true) { _deleteAll(); } else { - _deleteManyByKey({ + final combinedKeys = { if (entity != null) _connector.getPrimaryKey(entity), ...?entities?.map(_connector.getPrimaryKey), if (key != null) key, ...?keys, - }); + }; + + if (combinedKeys.isNotEmpty) { + _deleteManyByKey(combinedKeys); + } + + if (where != null) { + _deleteWhere(where); + } } } @@ -386,6 +399,40 @@ class IndexedEntityStore { _handleUpdate(keys); } + /// Deletes entities matching the query + void _deleteWhere(QueryBuilder query) { + final Set keys; + + try { + _database.execute('BEGIN'); + + final whereClause = query(_indexColumns)._entityKeysQuery(); + + final result = _database.select( + 'DELETE FROM `entity` ' + ' WHERE `type` = ? ' + ' AND `entity`.`key` IN ( ${whereClause.$1} ) ' + ' RETURNING `key`', + [ + _entityKey, + ...whereClause.$2, + ], + ); + + keys = { + for (final row in result) row['key']! as K, + }; + + _database.execute('COMMIT'); + } catch (e) { + _database.execute('ROLLBACK'); + + rethrow; + } + + _handleUpdate(keys); + } + void _assertNoMoreIndexEntries(K key) { assert( _database.select( diff --git a/test/indexed_entity_store_test.dart b/test/indexed_entity_store_test.dart index b427fef..4d4e781 100644 --- a/test/indexed_entity_store_test.dart +++ b/test/indexed_entity_store_test.dart @@ -1173,6 +1173,71 @@ void main() { expect(listSubscription.value, isEmpty); expect(fooStore.queryOnce(), isEmpty); }); + + test( + 'delete(where: )', + () async { + final path = + '/tmp/index_entity_store_test_${FlutterTimeline.now}.sqlite3'; + + final db = IndexedEntityDabase.open(path); + + final valueWrappingConnector = + IndexedEntityConnector<_ValueWrapper, int, String>( + entityKey: 'value_wrapper', + getPrimaryKey: (f) => f.key, + getIndices: (index) { + index((e) => e.value, as: 'value'); + }, + serialize: (f) => jsonEncode(f.toJSON()), + deserialize: (s) => _ValueWrapper.fromJSON( + jsonDecode(s) as Map, + ), + ); + + final valueStore = db.entityStore(valueWrappingConnector); + + valueStore.writeMany( + [ + for (var i = 0; i < 10; i++) + _ValueWrapper(FlutterTimeline.now + i, '$i'), + ], + singleStatement: false, + ); + + final allEntities = valueStore.query(); + final entityValue2 = valueStore.single( + where: (cols) => cols['value'].equals('2'), + ); + + expect(allEntities.value, hasLength(10)); + expect(entityValue2.value, isNotNull); + + // no match + valueStore.delete(where: (cols) => cols['value'].equals('11')); + expect(allEntities.value, hasLength(10)); + expect(entityValue2.value, isNotNull); + + // no match for complete query + valueStore.delete( + where: (cols) => cols['value'].equals('11') & cols['value'].equals('2'), + ); + expect(allEntities.value, hasLength(10)); + expect(entityValue2.value, isNotNull); + + // match delete 2 entries + valueStore.delete( + where: (cols) => cols['value'].equals('2') | cols['value'].equals('7'), + ); + expect(allEntities.value, hasLength(8)); + expect(entityValue2.value, isNull); + + allEntities.dispose(); + entityValue2.dispose(); + + expect(valueStore.subscriptionCount, 0); + }, + ); } class _FooEntity { @@ -1399,4 +1464,9 @@ class _ValueWrapper { json['value'], ); } + + @override + String toString() { + return '_ValueWrapper($key, $value)'; + } }