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

Generated by: LCOV version 1.14