diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fca98..118a00a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.4.6 + +* Re-enable foreign key constraint for every session + * Clean up unused indices which were not automatically removed before (but will be going forward) + ## 1.4.5 * try/catch with `ROLLBACK` in case a transaction fails, so as to not leave the database in a locked state diff --git a/example/pubspec.lock b/example/pubspec.lock index 82db360..5ea3386 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -105,7 +105,7 @@ packages: path: ".." relative: true source: path - version: "1.4.5" + version: "1.4.6" leak_tracker: dependency: transitive description: diff --git a/lib/src/index_entity_store.dart b/lib/src/index_entity_store.dart index 5758fc3..bd0fcf0 100644 --- a/lib/src/index_entity_store.dart +++ b/lib/src/index_entity_store.dart @@ -268,6 +268,7 @@ class IndexedEntityStore { _entityInsertStatement.execute( [_entityKey, _connector.getPrimaryKey(e), _connector.serialize(e)], ); + _assertNoMoreIndexEntries(_connector.getPrimaryKey(e)); _updateIndexInternal(e); @@ -311,19 +312,12 @@ class IndexedEntityStore { _handleUpdate(keys); } - late final _deleteIndexStatement = _database.prepare( - 'DELETE FROM `index` WHERE `type` = ? AND `entity` = ?', - persistent: true, - ); - late final _insertIndexStatement = _database.prepare( 'INSERT INTO `index` (`type`, `entity`, `field`, `value`) VALUES (?, ?, ?, ?)', persistent: true, ); void _updateIndexInternal(T e) { - _deleteIndexStatement.execute([_entityKey, _connector.getPrimaryKey(e)]); - for (final indexColumn in _indexColumns._indexColumns.values) { _insertIndexStatement.execute( [ @@ -376,11 +370,22 @@ class IndexedEntityStore { 'DELETE FROM `entity` WHERE `type` = ? AND `key` = ?', [_entityKey, key], ); + + _assertNoMoreIndexEntries(key); } _handleUpdate(keys); } + void _assertNoMoreIndexEntries(K key) { + assert( + _database.select( + 'SELECT * FROM `index` WHERE `type` = ? and `entity` = ?', + [_entityKey, key], + ).isEmpty, + ); + } + void _ensureIndexIsUpToDate() { final currentlyIndexedFields = _database .select( diff --git a/lib/src/indexed_entity_database.dart b/lib/src/indexed_entity_database.dart index b3247c3..e6d72c9 100644 --- a/lib/src/indexed_entity_database.dart +++ b/lib/src/indexed_entity_database.dart @@ -14,13 +14,26 @@ class IndexedEntityDabase { _initialDBSetup(); _v2Migration(); + _v3Migration(); } else if (_dbVersion == 1) { debugPrint('Migrating DB to v2'); _v2Migration(); + _v3Migration(); + } else if (_dbVersion == 2) { + _v3Migration(); } - assert(_dbVersion == 2); + assert(_dbVersion == 3); + + // Foreign keys need to be re-enable on every open (session) + // https://www.sqlite.org/foreignkeys.html#fk_enable + _database.execute('PRAGMA foreign_keys = ON'); + + // Ensure that the library used actually supports foreign keys + assert( + _database.select('PRAGMA foreign_keys').first.values.first as int == 1, + ); } void _initialDBSetup() { @@ -68,6 +81,20 @@ class IndexedEntityDabase { ); } + void _v3Migration() { + final res = _database.select( + 'DELETE FROM `index` WHERE NOT EXISTS (SELECT COUNT(*) FROM `entity` WHERE `entity`.`type` = type AND `entity`.`key` = entity) RETURNING `index`.`type`', + ); + if (res.isNotEmpty) { + debugPrint('Cleaned up ${res.length} unused indices'); + } + + _database.execute( + 'UPDATE `metadata` SET `value` = ? WHERE `key` = ?', + [3, 'version'], + ); + } + factory IndexedEntityDabase.open(String path) { return IndexedEntityDabase._(path); } diff --git a/pubspec.yaml b/pubspec.yaml index e8a5c65..18e7292 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: indexed_entity_store description: A fast, simple, and synchronous entity store for Flutter applications. -version: 1.4.5 +version: 1.4.6 repository: https://github.com/LunaONE/indexed_entity_store environment: diff --git a/test/indexed_entity_store_test.dart b/test/indexed_entity_store_test.dart index 14457fc..cc28dd6 100644 --- a/test/indexed_entity_store_test.dart +++ b/test/indexed_entity_store_test.dart @@ -606,6 +606,66 @@ void main() { }, ); + test( + 'Foreign key constraint persists across sessions', + () async { + final path = + '/tmp/index_entity_store_test_${FlutterTimeline.now}.sqlite3'; + + final db = IndexedEntityDabase.open(path); + + final indexedEntityConnector = + IndexedEntityConnector<_AllSupportedIndexTypes, String, String>( + entityKey: 'indexed_entity', + getPrimaryKey: (e) => e.string, + getIndices: (index) { + index((e) => e.string, as: 'string'); + }, + serialize: (f) => jsonEncode(f.toJSON()), + deserialize: (s) => _AllSupportedIndexTypes.fromJSON( + jsonDecode(s) as Map, + ), + ); + + final store = db.entityStore(indexedEntityConnector); + + expect(store.getAllOnce(), isEmpty); + + final e = _AllSupportedIndexTypes.defaultIfNull(string: 'default'); + + store.insert(e); + + db.dispose(); + + // now open again, ensuring foreign keys are still on and thus `index` + // will be cleaned up with entity removals & overwrites + + // delete & insert + { + final db = IndexedEntityDabase.open(path); + + final store = db.entityStore(indexedEntityConnector); + + store.delete('default'); + expect(store.getAllOnce(), isEmpty); + store.insert(e); + + db.dispose(); + } + + // second insert (overwrite) + { + final db = IndexedEntityDabase.open(path); + + final store = db.entityStore(indexedEntityConnector); + + store.insert(e); + + db.dispose(); + } + }, + ); + test('Query operations', () async { final path = '/tmp/index_entity_store_test_${FlutterTimeline.now}.sqlite3';