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

Generated by: LCOV version 1.14