LCOV - code coverage report
Current view: top level - lib/src/database - sqflite_box.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 107 134 79.9 %
Date: 2025-01-06 12:44:40 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:convert';
       3             : 
       4             : import 'package:sqflite_common/sqflite.dart';
       5             : 
       6             : import 'package:matrix/src/database/zone_transaction_mixin.dart';
       7             : 
       8             : /// Key-Value store abstraction over Sqflite so that the sdk database can use
       9             : /// a single interface for all platforms. API is inspired by Hive.
      10             : class BoxCollection with ZoneTransactionMixin {
      11             :   final Database _db;
      12             :   final Set<String> boxNames;
      13             :   final String name;
      14             : 
      15          38 :   BoxCollection(this._db, this.boxNames, this.name);
      16             : 
      17          38 :   static Future<BoxCollection> open(
      18             :     String name,
      19             :     Set<String> boxNames, {
      20             :     Object? sqfliteDatabase,
      21             :     DatabaseFactory? sqfliteFactory,
      22             :     dynamic idbFactory,
      23             :     int version = 1,
      24             :   }) async {
      25          38 :     if (sqfliteDatabase is! Database) {
      26             :       throw ('You must provide a Database `sqfliteDatabase` for use on native.');
      27             :     }
      28          38 :     final batch = sqfliteDatabase.batch();
      29          76 :     for (final name in boxNames) {
      30          38 :       batch.execute(
      31          38 :         'CREATE TABLE IF NOT EXISTS $name (k TEXT PRIMARY KEY NOT NULL, v TEXT)',
      32             :       );
      33          76 :       batch.execute('CREATE INDEX IF NOT EXISTS k_index ON $name (k)');
      34             :     }
      35          38 :     await batch.commit(noResult: true);
      36          38 :     return BoxCollection(sqfliteDatabase, boxNames, name);
      37             :   }
      38             : 
      39          38 :   Box<V> openBox<V>(String name) {
      40          76 :     if (!boxNames.contains(name)) {
      41           0 :       throw ('Box with name $name is not in the known box names of this collection.');
      42             :     }
      43          38 :     return Box<V>(name, this);
      44             :   }
      45             : 
      46             :   Batch? _activeBatch;
      47             : 
      48          38 :   Future<void> transaction(
      49             :     Future<void> Function() action, {
      50             :     List<String>? boxNames,
      51             :     bool readOnly = false,
      52             :   }) =>
      53          76 :       zoneTransaction(() async {
      54          76 :         final batch = _db.batch();
      55          38 :         _activeBatch = batch;
      56          38 :         await action();
      57          38 :         _activeBatch = null;
      58          38 :         await batch.commit(noResult: true);
      59             :       });
      60             : 
      61          18 :   Future<void> clear() => transaction(
      62           9 :         () async {
      63          18 :           for (final name in boxNames) {
      64          18 :             await _db.delete(name);
      65             :           }
      66             :         },
      67             :       );
      68             : 
      69         140 :   Future<void> close() => zoneTransaction(() => _db.close());
      70             : 
      71           0 :   @Deprecated('use collection.deleteDatabase now')
      72             :   static Future<void> delete(String path, [dynamic factory]) =>
      73           0 :       (factory ?? databaseFactory).deleteDatabase(path);
      74             : 
      75          11 :   Future<void> deleteDatabase(String path, [dynamic factory]) async {
      76          11 :     await close();
      77          11 :     await (factory ?? databaseFactory).deleteDatabase(path);
      78             :   }
      79             : }
      80             : 
      81             : class Box<V> {
      82             :   final String name;
      83             :   final BoxCollection boxCollection;
      84             :   final Map<String, V?> _cache = {};
      85             : 
      86             :   /// _cachedKeys is only used to make sure that if you fetch all keys from a
      87             :   /// box, you do not need to have an expensive read operation twice. There is
      88             :   /// no other usage for this at the moment. So the cache is never partial.
      89             :   /// Once the keys are cached, they need to be updated when changed in put and
      90             :   /// delete* so that the cache does not become outdated.
      91             :   Set<String>? _cachedKeys;
      92          76 :   bool get _keysCached => _cachedKeys != null;
      93             : 
      94             :   static const Set<Type> allowedValueTypes = {
      95             :     List<dynamic>,
      96             :     Map<dynamic, dynamic>,
      97             :     String,
      98             :     int,
      99             :     double,
     100             :     bool,
     101             :   };
     102             : 
     103          38 :   Box(this.name, this.boxCollection) {
     104         114 :     if (!allowedValueTypes.any((type) => V == type)) {
     105           0 :       throw Exception(
     106           0 :         'Illegal value type for Box: "${V.toString()}". Must be one of $allowedValueTypes',
     107             :       );
     108             :     }
     109             :   }
     110             : 
     111          38 :   String? _toString(V? value) {
     112             :     if (value == null) return null;
     113             :     switch (V) {
     114          38 :       case const (List<dynamic>):
     115          38 :       case const (Map<dynamic, dynamic>):
     116          38 :         return jsonEncode(value);
     117          36 :       case const (String):
     118          34 :       case const (int):
     119          34 :       case const (double):
     120          34 :       case const (bool):
     121             :       default:
     122          36 :         return value.toString();
     123             :     }
     124             :   }
     125             : 
     126          10 :   V? _fromString(Object? value) {
     127             :     if (value == null) return null;
     128          10 :     if (value is! String) {
     129           0 :       throw Exception(
     130           0 :         'Wrong database type! Expected String but got one of type ${value.runtimeType}',
     131             :       );
     132             :     }
     133             :     switch (V) {
     134          10 :       case const (int):
     135           0 :         return int.parse(value) as V;
     136          10 :       case const (double):
     137           0 :         return double.parse(value) as V;
     138          10 :       case const (bool):
     139           1 :         return (value == 'true') as V;
     140          10 :       case const (List<dynamic>):
     141           0 :         return List.unmodifiable(jsonDecode(value)) as V;
     142          10 :       case const (Map<dynamic, dynamic>):
     143          10 :         return Map.unmodifiable(jsonDecode(value)) as V;
     144           5 :       case const (String):
     145             :       default:
     146             :         return value as V;
     147             :     }
     148             :   }
     149             : 
     150          38 :   Future<List<String>> getAllKeys([Transaction? txn]) async {
     151         110 :     if (_keysCached) return _cachedKeys!.toList();
     152             : 
     153          76 :     final executor = txn ?? boxCollection._db;
     154             : 
     155         114 :     final result = await executor.query(name, columns: ['k']);
     156         152 :     final keys = result.map((row) => row['k'] as String).toList();
     157             : 
     158          76 :     _cachedKeys = keys.toSet();
     159             :     return keys;
     160             :   }
     161             : 
     162          36 :   Future<Map<String, V>> getAllValues([Transaction? txn]) async {
     163          72 :     final executor = txn ?? boxCollection._db;
     164             : 
     165          72 :     final result = await executor.query(name);
     166          36 :     return Map.fromEntries(
     167          36 :       result.map(
     168          18 :         (row) => MapEntry(
     169           9 :           row['k'] as String,
     170          18 :           _fromString(row['v']) as V,
     171             :         ),
     172             :       ),
     173             :     );
     174             :   }
     175             : 
     176          38 :   Future<V?> get(String key, [Transaction? txn]) async {
     177         152 :     if (_cache.containsKey(key)) return _cache[key];
     178             : 
     179          76 :     final executor = txn ?? boxCollection._db;
     180             : 
     181          38 :     final result = await executor.query(
     182          38 :       name,
     183          38 :       columns: ['v'],
     184             :       where: 'k = ?',
     185          38 :       whereArgs: [key],
     186             :     );
     187             : 
     188          41 :     final value = result.isEmpty ? null : _fromString(result.single['v']);
     189          76 :     _cache[key] = value;
     190             :     return value;
     191             :   }
     192             : 
     193          36 :   Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
     194          54 :     if (!keys.any((key) => !_cache.containsKey(key))) {
     195          90 :       return keys.map((key) => _cache[key]).toList();
     196             :     }
     197             : 
     198             :     // The SQL operation might fail with more than 1000 keys. We define some
     199             :     // buffer here and half the amount of keys recursively for this situation.
     200             :     const getAllMax = 800;
     201           0 :     if (keys.length > getAllMax) {
     202           0 :       final half = keys.length ~/ 2;
     203           0 :       return [
     204           0 :         ...(await getAll(keys.sublist(0, half))),
     205           0 :         ...(await getAll(keys.sublist(half))),
     206             :       ];
     207             :     }
     208             : 
     209           0 :     final executor = txn ?? boxCollection._db;
     210             : 
     211           0 :     final list = <V?>[];
     212             : 
     213           0 :     final result = await executor.query(
     214           0 :       name,
     215           0 :       where: 'k IN (${keys.map((_) => '?').join(',')})',
     216             :       whereArgs: keys,
     217             :     );
     218           0 :     final resultMap = Map<String, V?>.fromEntries(
     219           0 :       result.map((row) => MapEntry(row['k'] as String, _fromString(row['v']))),
     220             :     );
     221             : 
     222             :     // We want to make sure that they values are returnd in the exact same
     223             :     // order than the given keys. That's why we do this instead of just return
     224             :     // `resultMap.values`.
     225           0 :     list.addAll(keys.map((key) => resultMap[key]));
     226             : 
     227           0 :     _cache.addAll(resultMap);
     228             : 
     229             :     return list;
     230             :   }
     231             : 
     232          38 :   Future<void> put(String key, V val) async {
     233          76 :     final txn = boxCollection._activeBatch;
     234             : 
     235          38 :     final params = {
     236             :       'k': key,
     237          38 :       'v': _toString(val),
     238             :     };
     239             :     if (txn == null) {
     240         114 :       await boxCollection._db.insert(
     241          38 :         name,
     242             :         params,
     243             :         conflictAlgorithm: ConflictAlgorithm.replace,
     244             :       );
     245             :     } else {
     246          36 :       txn.insert(
     247          36 :         name,
     248             :         params,
     249             :         conflictAlgorithm: ConflictAlgorithm.replace,
     250             :       );
     251             :     }
     252             : 
     253          76 :     _cache[key] = val;
     254          74 :     _cachedKeys?.add(key);
     255             :     return;
     256             :   }
     257             : 
     258          38 :   Future<void> delete(String key, [Batch? txn]) async {
     259          76 :     txn ??= boxCollection._activeBatch;
     260             : 
     261             :     if (txn == null) {
     262          70 :       await boxCollection._db.delete(name, where: 'k = ?', whereArgs: [key]);
     263             :     } else {
     264         114 :       txn.delete(name, where: 'k = ?', whereArgs: [key]);
     265             :     }
     266             : 
     267             :     // Set to null instead remove() so that inside of transactions null is
     268             :     // returned.
     269          76 :     _cache[key] = null;
     270          72 :     _cachedKeys?.remove(key);
     271             :     return;
     272             :   }
     273             : 
     274           2 :   Future<void> deleteAll(List<String> keys, [Batch? txn]) async {
     275           4 :     txn ??= boxCollection._activeBatch;
     276             : 
     277           6 :     final placeholder = keys.map((_) => '?').join(',');
     278             :     if (txn == null) {
     279           6 :       await boxCollection._db.delete(
     280           2 :         name,
     281           2 :         where: 'k IN ($placeholder)',
     282             :         whereArgs: keys,
     283             :       );
     284             :     } else {
     285           0 :       txn.delete(
     286           0 :         name,
     287           0 :         where: 'k IN ($placeholder)',
     288             :         whereArgs: keys,
     289             :       );
     290             :     }
     291             : 
     292           4 :     for (final key in keys) {
     293           4 :       _cache[key] = null;
     294           2 :       _cachedKeys?.removeAll(keys);
     295             :     }
     296             :     return;
     297             :   }
     298             : 
     299           8 :   Future<void> clear([Batch? txn]) async {
     300          16 :     txn ??= boxCollection._activeBatch;
     301             : 
     302             :     if (txn == null) {
     303          24 :       await boxCollection._db.delete(name);
     304             :     } else {
     305           6 :       txn.delete(name);
     306             :     }
     307             : 
     308          16 :     _cache.clear();
     309           8 :     _cachedKeys = null;
     310             :     return;
     311             :   }
     312             : }

Generated by: LCOV version 1.14