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-11-12 07:37:08 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?> _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          72 :   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          36 :   Box(this.name, this.boxCollection) {
     104         108 :     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          36 :   String? _toString(V? value) {
     112             :     if (value == null) return null;
     113             :     switch (V) {
     114          36 :       case const (List<dynamic>):
     115          36 :       case const (Map<dynamic, dynamic>):
     116          36 :         return jsonEncode(value);
     117          34 :       case const (String):
     118          32 :       case const (int):
     119          32 :       case const (double):
     120          32 :       case const (bool):
     121             :       default:
     122          34 :         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          36 :   Future<List<String>> getAllKeys([Transaction? txn]) async {
     151         104 :     if (_keysCached) return _cachedKeys!.toList();
     152             : 
     153          72 :     final executor = txn ?? boxCollection._db;
     154             : 
     155         108 :     final result = await executor.query(name, columns: ['k']);
     156         144 :     final keys = result.map((row) => row['k'] as String).toList();
     157             : 
     158          72 :     _cachedKeys = keys.toSet();
     159             :     return keys;
     160             :   }
     161             : 
     162          34 :   Future<Map<String, V>> getAllValues([Transaction? txn]) async {
     163          68 :     final executor = txn ?? boxCollection._db;
     164             : 
     165          68 :     final result = await executor.query(name);
     166          34 :     return Map.fromEntries(
     167          34 :       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          36 :   Future<V?> get(String key, [Transaction? txn]) async {
     177         144 :     if (_cache.containsKey(key)) return _cache[key];
     178             : 
     179          72 :     final executor = txn ?? boxCollection._db;
     180             : 
     181          36 :     final result = await executor.query(
     182          36 :       name,
     183          36 :       columns: ['v'],
     184             :       where: 'k = ?',
     185          36 :       whereArgs: [key],
     186             :     );
     187             : 
     188          39 :     final value = result.isEmpty ? null : _fromString(result.single['v']);
     189          72 :     _cache[key] = value;
     190             :     return value;
     191             :   }
     192             : 
     193          34 :   Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
     194          52 :     if (!keys.any((key) => !_cache.containsKey(key))) {
     195          86 :       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           8 :     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           8 :     final executor = txn ?? boxCollection._db;
     210             : 
     211           4 :     final list = <V?>[];
     212             : 
     213           4 :     final result = await executor.query(
     214           4 :       name,
     215          16 :       where: 'k IN (${keys.map((_) => '?').join(',')})',
     216             :       whereArgs: keys,
     217             :     );
     218           4 :     final resultMap = Map<String, V?>.fromEntries(
     219          14 :       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          16 :     list.addAll(keys.map((key) => resultMap[key]));
     226             : 
     227           8 :     _cache.addAll(resultMap);
     228             : 
     229             :     return list;
     230             :   }
     231             : 
     232          36 :   Future<void> put(String key, V val) async {
     233          72 :     final txn = boxCollection._activeBatch;
     234             : 
     235          36 :     final params = {
     236             :       'k': key,
     237          36 :       'v': _toString(val),
     238             :     };
     239             :     if (txn == null) {
     240         108 :       await boxCollection._db.insert(
     241          36 :         name,
     242             :         params,
     243             :         conflictAlgorithm: ConflictAlgorithm.replace,
     244             :       );
     245             :     } else {
     246          34 :       txn.insert(
     247          34 :         name,
     248             :         params,
     249             :         conflictAlgorithm: ConflictAlgorithm.replace,
     250             :       );
     251             :     }
     252             : 
     253          72 :     _cache[key] = val;
     254          70 :     _cachedKeys?.add(key);
     255             :     return;
     256             :   }
     257             : 
     258          36 :   Future<void> delete(String key, [Batch? txn]) async {
     259          72 :     txn ??= boxCollection._activeBatch;
     260             : 
     261             :     if (txn == null) {
     262          70 :       await boxCollection._db.delete(name, where: 'k = ?', whereArgs: [key]);
     263             :     } else {
     264         108 :       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          72 :     _cache[key] = null;
     270          68 :     _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