LCOV - code coverage report
Current view: top level - lib/src/database - sqflite_box.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 80.0 % 135 108
Test Date: 2025-01-14 13:39:53 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           36 :   BoxCollection(this._db, this.boxNames, this.name);
      16              : 
      17           36 :   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           36 :     if (sqfliteDatabase is! Database) {
      26              :       throw ('You must provide a Database `sqfliteDatabase` for use on native.');
      27              :     }
      28           36 :     final batch = sqfliteDatabase.batch();
      29           72 :     for (final name in boxNames) {
      30           36 :       batch.execute(
      31           36 :         'CREATE TABLE IF NOT EXISTS $name (k TEXT PRIMARY KEY NOT NULL, v TEXT)',
      32              :       );
      33           72 :       batch.execute('CREATE INDEX IF NOT EXISTS k_index ON $name (k)');
      34              :     }
      35           36 :     await batch.commit(noResult: true);
      36           36 :     return BoxCollection(sqfliteDatabase, boxNames, name);
      37              :   }
      38              : 
      39           36 :   Box<V> openBox<V>(String name) {
      40           72 :     if (!boxNames.contains(name)) {
      41            0 :       throw ('Box with name $name is not in the known box names of this collection.');
      42              :     }
      43           36 :     return Box<V>(name, this);
      44              :   }
      45              : 
      46              :   Batch? _activeBatch;
      47              : 
      48           36 :   Future<void> transaction(
      49              :     Future<void> Function() action, {
      50              :     List<String>? boxNames,
      51              :     bool readOnly = false,
      52              :   }) =>
      53           72 :       zoneTransaction(() async {
      54           72 :         final batch = _db.batch();
      55           36 :         _activeBatch = batch;
      56           36 :         await action();
      57           36 :         _activeBatch = null;
      58           36 :         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          130 :   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?> _quickAccessCache = {};
      85              : 
      86              :   /// _quickAccessCachedKeys 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>? _quickAccessCachedKeys;
      92              : 
      93              :   static const Set<Type> allowedValueTypes = {
      94              :     List<dynamic>,
      95              :     Map<dynamic, dynamic>,
      96              :     String,
      97              :     int,
      98              :     double,
      99              :     bool,
     100              :   };
     101              : 
     102           36 :   Box(this.name, this.boxCollection) {
     103          108 :     if (!allowedValueTypes.any((type) => V == type)) {
     104            0 :       throw Exception(
     105            0 :         'Illegal value type for Box: "${V.toString()}". Must be one of $allowedValueTypes',
     106              :       );
     107              :     }
     108              :   }
     109              : 
     110           36 :   String? _toString(V? value) {
     111              :     if (value == null) return null;
     112              :     switch (V) {
     113           36 :       case const (List<dynamic>):
     114           36 :       case const (Map<dynamic, dynamic>):
     115           36 :         return jsonEncode(value);
     116           34 :       case const (String):
     117           32 :       case const (int):
     118           32 :       case const (double):
     119           32 :       case const (bool):
     120              :       default:
     121           34 :         return value.toString();
     122              :     }
     123              :   }
     124              : 
     125           10 :   V? _fromString(Object? value) {
     126              :     if (value == null) return null;
     127           10 :     if (value is! String) {
     128            0 :       throw Exception(
     129            0 :         'Wrong database type! Expected String but got one of type ${value.runtimeType}',
     130              :       );
     131              :     }
     132              :     switch (V) {
     133           10 :       case const (int):
     134            0 :         return int.parse(value) as V;
     135           10 :       case const (double):
     136            0 :         return double.parse(value) as V;
     137           10 :       case const (bool):
     138            1 :         return (value == 'true') as V;
     139           10 :       case const (List<dynamic>):
     140            0 :         return List.unmodifiable(jsonDecode(value)) as V;
     141           10 :       case const (Map<dynamic, dynamic>):
     142           10 :         return Map.unmodifiable(jsonDecode(value)) as V;
     143            5 :       case const (String):
     144              :       default:
     145              :         return value as V;
     146              :     }
     147              :   }
     148              : 
     149           36 :   Future<List<String>> getAllKeys([Transaction? txn]) async {
     150          104 :     if (_quickAccessCachedKeys != null) return _quickAccessCachedKeys!.toList();
     151              : 
     152           72 :     final executor = txn ?? boxCollection._db;
     153              : 
     154          108 :     final result = await executor.query(name, columns: ['k']);
     155          144 :     final keys = result.map((row) => row['k'] as String).toList();
     156              : 
     157           72 :     _quickAccessCachedKeys = keys.toSet();
     158              :     return keys;
     159              :   }
     160              : 
     161           34 :   Future<Map<String, V>> getAllValues([Transaction? txn]) async {
     162           68 :     final executor = txn ?? boxCollection._db;
     163              : 
     164           68 :     final result = await executor.query(name);
     165           34 :     return Map.fromEntries(
     166           34 :       result.map(
     167           18 :         (row) => MapEntry(
     168            9 :           row['k'] as String,
     169           18 :           _fromString(row['v']) as V,
     170              :         ),
     171              :       ),
     172              :     );
     173              :   }
     174              : 
     175           36 :   Future<V?> get(String key, [Transaction? txn]) async {
     176          144 :     if (_quickAccessCache.containsKey(key)) return _quickAccessCache[key];
     177              : 
     178           72 :     final executor = txn ?? boxCollection._db;
     179              : 
     180           36 :     final result = await executor.query(
     181           36 :       name,
     182           36 :       columns: ['v'],
     183              :       where: 'k = ?',
     184           36 :       whereArgs: [key],
     185              :     );
     186              : 
     187           39 :     final value = result.isEmpty ? null : _fromString(result.single['v']);
     188           72 :     _quickAccessCache[key] = value;
     189              :     return value;
     190              :   }
     191              : 
     192           34 :   Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
     193           52 :     if (!keys.any((key) => !_quickAccessCache.containsKey(key))) {
     194           86 :       return keys.map((key) => _quickAccessCache[key]).toList();
     195              :     }
     196              : 
     197              :     // The SQL operation might fail with more than 1000 keys. We define some
     198              :     // buffer here and half the amount of keys recursively for this situation.
     199              :     const getAllMax = 800;
     200            0 :     if (keys.length > getAllMax) {
     201            0 :       final half = keys.length ~/ 2;
     202            0 :       return [
     203            0 :         ...(await getAll(keys.sublist(0, half))),
     204            0 :         ...(await getAll(keys.sublist(half))),
     205              :       ];
     206              :     }
     207              : 
     208            0 :     final executor = txn ?? boxCollection._db;
     209              : 
     210            0 :     final list = <V?>[];
     211              : 
     212            0 :     final result = await executor.query(
     213            0 :       name,
     214            0 :       where: 'k IN (${keys.map((_) => '?').join(',')})',
     215              :       whereArgs: keys,
     216              :     );
     217            0 :     final resultMap = Map<String, V?>.fromEntries(
     218            0 :       result.map((row) => MapEntry(row['k'] as String, _fromString(row['v']))),
     219              :     );
     220              : 
     221              :     // We want to make sure that they values are returnd in the exact same
     222              :     // order than the given keys. That's why we do this instead of just return
     223              :     // `resultMap.values`.
     224            0 :     list.addAll(keys.map((key) => resultMap[key]));
     225              : 
     226            0 :     _quickAccessCache.addAll(resultMap);
     227              : 
     228              :     return list;
     229              :   }
     230              : 
     231           36 :   Future<void> put(String key, V val) async {
     232           72 :     final txn = boxCollection._activeBatch;
     233              : 
     234           36 :     final params = {
     235              :       'k': key,
     236           36 :       'v': _toString(val),
     237              :     };
     238              :     if (txn == null) {
     239          108 :       await boxCollection._db.insert(
     240           36 :         name,
     241              :         params,
     242              :         conflictAlgorithm: ConflictAlgorithm.replace,
     243              :       );
     244              :     } else {
     245           34 :       txn.insert(
     246           34 :         name,
     247              :         params,
     248              :         conflictAlgorithm: ConflictAlgorithm.replace,
     249              :       );
     250              :     }
     251              : 
     252           72 :     _quickAccessCache[key] = val;
     253           70 :     _quickAccessCachedKeys?.add(key);
     254              :     return;
     255              :   }
     256              : 
     257           36 :   Future<void> delete(String key, [Batch? txn]) async {
     258           72 :     txn ??= boxCollection._activeBatch;
     259              : 
     260              :     if (txn == null) {
     261           70 :       await boxCollection._db.delete(name, where: 'k = ?', whereArgs: [key]);
     262              :     } else {
     263          108 :       txn.delete(name, where: 'k = ?', whereArgs: [key]);
     264              :     }
     265              : 
     266              :     // Set to null instead remove() so that inside of transactions null is
     267              :     // returned.
     268           72 :     _quickAccessCache[key] = null;
     269           68 :     _quickAccessCachedKeys?.remove(key);
     270              :     return;
     271              :   }
     272              : 
     273            2 :   Future<void> deleteAll(List<String> keys, [Batch? txn]) async {
     274            4 :     txn ??= boxCollection._activeBatch;
     275              : 
     276            6 :     final placeholder = keys.map((_) => '?').join(',');
     277              :     if (txn == null) {
     278            6 :       await boxCollection._db.delete(
     279            2 :         name,
     280            2 :         where: 'k IN ($placeholder)',
     281              :         whereArgs: keys,
     282              :       );
     283              :     } else {
     284            0 :       txn.delete(
     285            0 :         name,
     286            0 :         where: 'k IN ($placeholder)',
     287              :         whereArgs: keys,
     288              :       );
     289              :     }
     290              : 
     291            4 :     for (final key in keys) {
     292            4 :       _quickAccessCache[key] = null;
     293            2 :       _quickAccessCachedKeys?.removeAll(keys);
     294              :     }
     295              :     return;
     296              :   }
     297              : 
     298           15 :   void clearQuickAccessCache() {
     299           30 :     _quickAccessCache.clear();
     300           15 :     _quickAccessCachedKeys = null;
     301              :   }
     302              : 
     303            8 :   Future<void> clear([Batch? txn]) async {
     304           16 :     txn ??= boxCollection._activeBatch;
     305              : 
     306              :     if (txn == null) {
     307           24 :       await boxCollection._db.delete(name);
     308              :     } else {
     309            6 :       txn.delete(name);
     310              :     }
     311              : 
     312            8 :     clearQuickAccessCache();
     313              :   }
     314              : }
        

Generated by: LCOV version 2.0-1