Skip to content

Commit

Permalink
Fix foreign key
Browse files Browse the repository at this point in the history
Foreign keys need to be re-enable with each session

Otherwise e.g. `ON DELETE CASCADE` for the automatic index removal does not work.

Revert explicit delete introduced in 9bcc24d
  • Loading branch information
tp committed Nov 6, 2024
1 parent 9bcc24d commit 359df7a
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 10 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.4.5"
version: "1.4.6"
leak_tracker:
dependency: transitive
description:
Expand Down
19 changes: 12 additions & 7 deletions lib/src/index_entity_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ class IndexedEntityStore<T, K> {
_entityInsertStatement.execute(
[_entityKey, _connector.getPrimaryKey(e), _connector.serialize(e)],
);
_assertNoMoreIndexEntries(_connector.getPrimaryKey(e));

_updateIndexInternal(e);

Expand Down Expand Up @@ -311,19 +312,12 @@ class IndexedEntityStore<T, K> {
_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(
[
Expand Down Expand Up @@ -376,11 +370,22 @@ class IndexedEntityStore<T, K> {
'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(
Expand Down
29 changes: 28 additions & 1 deletion lib/src/indexed_entity_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
60 changes: 60 additions & 0 deletions test/indexed_entity_store_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic>,
),
);

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';

Expand Down

0 comments on commit 359df7a

Please sign in to comment.