LCOV - code coverage report
Current view: top level - lib/src/database - sqflite_box.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 117 134 87.3 %
Date: 2024-09-30 15:57:20 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          35 :   BoxCollection(this._db, this.boxNames, this.name);
      16             : 
      17          35 :   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          35 :     if (sqfliteDatabase is! Database) {
      26             :       throw ('You must provide a Database `sqfliteDatabase` for use on native.');
      27             :     }
      28          35 :     final batch = sqfliteDatabase.batch();
      29          70 :     for (final name in boxNames) {
      30          35 :       batch.execute(
      31          35 :         'CREATE TABLE IF NOT EXISTS $name (k TEXT PRIMARY KEY NOT NULL, v TEXT)',
      32             :       );
      33          70 :       batch.execute('CREATE INDEX IF NOT EXISTS k_index ON $name (k)');
      34             :     }
      35          35 :     await batch.commit(noResult: true);
      36          35 :     return BoxCollection(sqfliteDatabase, boxNames, name);
      37             :   }
      38             : 
      39          35 :   Box<V> openBox<V>(String name) {
      40          70 :     if (!boxNames.contains(name)) {
      41           0 :       throw ('Box with name $name is not in the known box names of this collection.');
      42             :     }
      43          35 :     return Box<V>(name, this);
      44             :   }
      45             : 
      46             :   Batch? _activeBatch;
      47             : 
      48          35 :   Future<void> transaction(
      49             :     Future<void> Function() action, {
      50             :     List<String>? boxNames,
      51             :     bool readOnly = false,
      52             :   }) =>
      53          70 :       zoneTransaction(() async {
      54          70 :         final batch = _db.batch();
      55          35 :         _activeBatch = batch;
      56          35 :         await action();
      57          35 :         _activeBatch = null;
      58          35 :         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         125 :   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          70 :   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          35 :   Box(this.name, this.boxCollection) {
     104         105 :     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          35 :   String? _toString(V? value) {
     112             :     if (value == null) return null;
     113             :     switch (V) {
     114          35 :       case const (List<dynamic>):
     115          35 :       case const (Map<dynamic, dynamic>):
     116          35 :         return jsonEncode(value);
     117          33 :       case const (String):
     118          31 :       case const (int):
     119          31 :       case const (double):
     120          31 :       case const (bool):
     121             :       default:
     122          33 :         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             :     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          35 :   Future<List<String>> getAllKeys([Transaction? txn]) async {
     150         101 :     if (_keysCached) return _cachedKeys!.toList();
     151             : 
     152          70 :     final executor = txn ?? boxCollection._db;
     153             : 
     154         105 :     final result = await executor.query(name, columns: ['k']);
     155         140 :     final keys = result.map((row) => row['k'] as String).toList();
     156             : 
     157          70 :     _cachedKeys = keys.toSet();
     158             :     return keys;
     159             :   }
     160             : 
     161          33 :   Future<Map<String, V>> getAllValues([Transaction? txn]) async {
     162          66 :     final executor = txn ?? boxCollection._db;
     163             : 
     164          66 :     final result = await executor.query(name);
     165          33 :     return Map.fromEntries(
     166          33 :       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          35 :   Future<V?> get(String key, [Transaction? txn]) async {
     176         140 :     if (_cache.containsKey(key)) return _cache[key];
     177             : 
     178          70 :     final executor = txn ?? boxCollection._db;
     179             : 
     180          35 :     final result = await executor.query(
     181          35 :       name,
     182          35 :       columns: ['v'],
     183             :       where: 'k = ?',
     184          35 :       whereArgs: [key],
     185             :     );
     186             : 
     187          38 :     final value = result.isEmpty ? null : _fromString(result.single['v']);
     188          70 :     _cache[key] = value;
     189             :     return value;
     190             :   }
     191             : 
     192          33 :   Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
     193          51 :     if (!keys.any((key) => !_cache.containsKey(key))) {
     194          84 :       return keys.map((key) => _cache[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           8 :     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           8 :     final executor = txn ?? boxCollection._db;
     209             : 
     210           4 :     final list = <V?>[];
     211             : 
     212           4 :     final result = await executor.query(
     213           4 :       name,
     214          16 :       where: 'k IN (${keys.map((_) => '?').join(',')})',
     215             :       whereArgs: keys,
     216             :     );
     217           4 :     final resultMap = Map<String, V?>.fromEntries(
     218          14 :       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          16 :     list.addAll(keys.map((key) => resultMap[key]));
     225             : 
     226           8 :     _cache.addAll(resultMap);
     227             : 
     228             :     return list;
     229             :   }
     230             : 
     231          35 :   Future<void> put(String key, V val) async {
     232          70 :     final txn = boxCollection._activeBatch;
     233             : 
     234          35 :     final params = {
     235             :       'k': key,
     236          35 :       'v': _toString(val),
     237             :     };
     238             :     if (txn == null) {
     239         105 :       await boxCollection._db.insert(
     240          35 :         name,
     241             :         params,
     242             :         conflictAlgorithm: ConflictAlgorithm.replace,
     243             :       );
     244             :     } else {
     245          33 :       txn.insert(
     246          33 :         name,
     247             :         params,
     248             :         conflictAlgorithm: ConflictAlgorithm.replace,
     249             :       );
     250             :     }
     251             : 
     252          70 :     _cache[key] = val;
     253          68 :     _cachedKeys?.add(key);
     254             :     return;
     255             :   }
     256             : 
     257          35 :   Future<void> delete(String key, [Batch? txn]) async {
     258          70 :     txn ??= boxCollection._activeBatch;
     259             : 
     260             :     if (txn == null) {
     261          70 :       await boxCollection._db.delete(name, where: 'k = ?', whereArgs: [key]);
     262             :     } else {
     263         105 :       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          70 :     _cache[key] = null;
     269          66 :     _cachedKeys?.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 :       _cache[key] = null;
     293           2 :       _cachedKeys?.removeAll(keys);
     294             :     }
     295             :     return;
     296             :   }
     297             : 
     298           8 :   Future<void> clear([Batch? txn]) async {
     299          16 :     txn ??= boxCollection._activeBatch;
     300             : 
     301             :     if (txn == null) {
     302          24 :       await boxCollection._db.delete(name);
     303             :     } else {
     304           6 :       txn.delete(name);
     305             :     }
     306             : 
     307          16 :     _cache.clear();
     308           8 :     _cachedKeys = null;
     309             :     return;
     310             :   }
     311             : }

Generated by: LCOV version 1.14