From e872901ee2acf0540d503eda04714f72fddcd4f2 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 26 Sep 2025 16:32:30 +0200 Subject: [PATCH] support for riverpod persistence --- .../features/user/data/database/database.dart | 8 +- .../user/data/database/database.drift.dart | 4 + .../user/data/database/database.steps.dart | 214 +++++ .../user/data/database/definitions.drift | 7 + .../user/data/database/definitions.drift.dart | 432 +++++++++ .../user/data/database/riverpod_storage.dart | 57 ++ app/lib/features/user/data/providers.dart | 8 + app/lib/features/user/data/providers.g.dart | 48 + app/test/drift/user/generated/schema.dart | 23 + app/test/drift/user/generated/schema_v1.dart | 651 +++++++++++++ app/test/drift/user/generated/schema_v2.dart | 902 ++++++++++++++++++ app/test/drift/user/migration_test.dart | 84 ++ 12 files changed, 2437 insertions(+), 1 deletion(-) create mode 100644 app/lib/features/user/data/database/database.steps.dart create mode 100644 app/lib/features/user/data/database/riverpod_storage.dart create mode 100644 app/test/drift/user/generated/schema.dart create mode 100644 app/test/drift/user/generated/schema_v1.dart create mode 100644 app/test/drift/user/generated/schema_v2.dart create mode 100644 app/test/drift/user/migration_test.dart diff --git a/app/lib/features/user/data/database/database.dart b/app/lib/features/user/data/database/database.dart index 4819e9a3..564ebc13 100644 --- a/app/lib/features/user/data/database/database.dart +++ b/app/lib/features/user/data/database/database.dart @@ -22,6 +22,7 @@ import 'package:weblibre/features/user/data/database/daos/cache.dart'; import 'package:weblibre/features/user/data/database/daos/onboarding.dart'; import 'package:weblibre/features/user/data/database/daos/setting.dart'; import 'package:weblibre/features/user/data/database/database.drift.dart'; +import 'package:weblibre/features/user/data/database/database.steps.dart'; @DriftDatabase( include: {'definitions.drift'}, @@ -29,13 +30,18 @@ import 'package:weblibre/features/user/data/database/database.drift.dart'; ) class UserDatabase extends $UserDatabase { @override - final int schemaVersion = 1; + final int schemaVersion = 2; @override MigrationStrategy get migration => MigrationStrategy( beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON;'); }, + onUpgrade: stepByStep( + from1To2: (m, schema) async { + await m.createTable(schema.riverpod); + }, + ), ); UserDatabase(super.e); diff --git a/app/lib/features/user/data/database/database.drift.dart b/app/lib/features/user/data/database/database.drift.dart index c50931d8..710a9363 100644 --- a/app/lib/features/user/data/database/database.drift.dart +++ b/app/lib/features/user/data/database/database.drift.dart @@ -17,6 +17,7 @@ abstract class $UserDatabase extends i0.GeneratedDatabase { late final i1.Setting setting = i1.Setting(this); late final i1.IconCache iconCache = i1.IconCache(this); late final i1.Onboarding onboarding = i1.Onboarding(this); + late final i1.Riverpod riverpod = i1.Riverpod(this); late final i2.SettingDao settingDao = i2.SettingDao(this as i3.UserDatabase); late final i4.CacheDao cacheDao = i4.CacheDao(this as i3.UserDatabase); late final i5.OnboardingDao onboardingDao = i5.OnboardingDao( @@ -33,6 +34,7 @@ abstract class $UserDatabase extends i0.GeneratedDatabase { setting, iconCache, onboarding, + riverpod, ]; } @@ -45,6 +47,8 @@ class $UserDatabaseManager { i1.$IconCacheTableManager(_db, _db.iconCache); i1.$OnboardingTableManager get onboarding => i1.$OnboardingTableManager(_db, _db.onboarding); + i1.$RiverpodTableManager get riverpod => + i1.$RiverpodTableManager(_db, _db.riverpod); } extension DefineFunctions on i7.CommonDatabase { diff --git a/app/lib/features/user/data/database/database.steps.dart b/app/lib/features/user/data/database/database.steps.dart new file mode 100644 index 00000000..b1a0732a --- /dev/null +++ b/app/lib/features/user/data/database/database.steps.dart @@ -0,0 +1,214 @@ +// dart format width=80 +import 'package:drift/internal/versioned_schema.dart' as i0; +import 'package:drift/drift.dart' as i1; +import 'dart:typed_data' as i2; +import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import + +// GENERATED BY drift_dev, DO NOT MODIFY. +final class Schema2 extends i0.VersionedSchema { + Schema2({required super.database}) : super(version: 2); + @override + late final List entities = [ + setting, + iconCache, + onboarding, + riverpod, + ]; + late final Shape0 setting = Shape0( + source: i0.VersionedTable( + entityName: 'setting', + withoutRowId: false, + isStrict: true, + tableConstraints: [], + columns: [_column_0, _column_1, _column_2], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape1 iconCache = Shape1( + source: i0.VersionedTable( + entityName: 'icon_cache', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_3, _column_4, _column_5], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape2 onboarding = Shape2( + source: i0.VersionedTable( + entityName: 'onboarding', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_6, _column_7], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 riverpod = Shape3( + source: i0.VersionedTable( + entityName: 'riverpod', + withoutRowId: true, + isStrict: false, + tableConstraints: [], + columns: [_column_0, _column_8, _column_9, _column_10], + attachedDatabase: database, + ), + alias: null, + ); +} + +class Shape0 extends i0.VersionedTable { + Shape0({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get key => + columnsByName['key']! as i1.GeneratedColumn; + i1.GeneratedColumn get partitionKey => + columnsByName['partition_key']! as i1.GeneratedColumn; + i1.GeneratedColumn get value => + columnsByName['value']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_0(String aliasedName) => + i1.GeneratedColumn( + 'key', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); +i1.GeneratedColumn _column_1(String aliasedName) => + i1.GeneratedColumn( + 'partition_key', + aliasedName, + true, + type: i1.DriftSqlType.string, + $customConstraints: '', + ); +i1.GeneratedColumn _column_2(String aliasedName) => + i1.GeneratedColumn( + 'value', + aliasedName, + true, + type: i1.DriftSqlType.any, + $customConstraints: '', + ); + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get origin => + columnsByName['origin']! as i1.GeneratedColumn; + i1.GeneratedColumn get iconData => + columnsByName['icon_data']! as i1.GeneratedColumn; + i1.GeneratedColumn get fetchDate => + columnsByName['fetch_date']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_3(String aliasedName) => + i1.GeneratedColumn( + 'origin', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); +i1.GeneratedColumn _column_4(String aliasedName) => + i1.GeneratedColumn( + 'icon_data', + aliasedName, + false, + type: i1.DriftSqlType.blob, + $customConstraints: 'NOT NULL', + ); +i1.GeneratedColumn _column_5(String aliasedName) => + i1.GeneratedColumn( + 'fetch_date', + aliasedName, + false, + type: i1.DriftSqlType.dateTime, + $customConstraints: 'NOT NULL', + ); + +class Shape2 extends i0.VersionedTable { + Shape2({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get revision => + columnsByName['revision']! as i1.GeneratedColumn; + i1.GeneratedColumn get completionDate => + columnsByName['completion_date']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_6(String aliasedName) => + i1.GeneratedColumn( + 'revision', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL', + ); +i1.GeneratedColumn _column_7(String aliasedName) => + i1.GeneratedColumn( + 'completion_date', + aliasedName, + false, + type: i1.DriftSqlType.dateTime, + $customConstraints: 'NOT NULL', + ); + +class Shape3 extends i0.VersionedTable { + Shape3({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get key => + columnsByName['key']! as i1.GeneratedColumn; + i1.GeneratedColumn get json => + columnsByName['json']! as i1.GeneratedColumn; + i1.GeneratedColumn get expireAt => + columnsByName['expireAt']! as i1.GeneratedColumn; + i1.GeneratedColumn get destroyKey => + columnsByName['destroyKey']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_8(String aliasedName) => + i1.GeneratedColumn( + 'json', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL', + ); +i1.GeneratedColumn _column_9(String aliasedName) => + i1.GeneratedColumn( + 'expireAt', + aliasedName, + true, + type: i1.DriftSqlType.dateTime, + $customConstraints: '', + ); +i1.GeneratedColumn _column_10(String aliasedName) => + i1.GeneratedColumn( + 'destroyKey', + aliasedName, + true, + type: i1.DriftSqlType.string, + $customConstraints: '', + ); +i0.MigrationStepWithVersion migrationSteps({ + required Future Function(i1.Migrator m, Schema2 schema) from1To2, +}) { + return (currentVersion, database) async { + switch (currentVersion) { + case 1: + final schema = Schema2(database: database); + final migrator = i1.Migrator(database, schema); + await from1To2(migrator, schema); + return 2; + default: + throw ArgumentError.value('Unknown migration from $currentVersion'); + } + }; +} + +i1.OnUpgrade stepByStep({ + required Future Function(i1.Migrator m, Schema2 schema) from1To2, +}) => i0.VersionedSchema.stepByStepHelper( + step: migrationSteps(from1To2: from1To2), +); diff --git a/app/lib/features/user/data/database/definitions.drift b/app/lib/features/user/data/database/definitions.drift index af338803..67d5251e 100644 --- a/app/lib/features/user/data/database/definitions.drift +++ b/app/lib/features/user/data/database/definitions.drift @@ -15,6 +15,13 @@ CREATE TABLE onboarding ( completion_date DATETIME NOT NULL ); +CREATE TABLE riverpod ( + "key" TEXT PRIMARY KEY NOT NULL, + json TEXT NOT NULL, + expireAt DATETIME, + destroyKey TEXT +) WITHOUT ROWID; + evictCacheEntries: DELETE FROM icon_cache WHERE rowid IN ( diff --git a/app/lib/features/user/data/database/definitions.drift.dart b/app/lib/features/user/data/database/definitions.drift.dart index 669ae043..63a4b753 100644 --- a/app/lib/features/user/data/database/definitions.drift.dart +++ b/app/lib/features/user/data/database/definitions.drift.dart @@ -489,6 +489,187 @@ typedef $OnboardingProcessedTableManager = i1.OnboardingData, i0.PrefetchHooks Function() >; +typedef $RiverpodCreateCompanionBuilder = + i1.RiverpodCompanion Function({ + required String key, + required String json, + i0.Value expireAt, + i0.Value destroyKey, + }); +typedef $RiverpodUpdateCompanionBuilder = + i1.RiverpodCompanion Function({ + i0.Value key, + i0.Value json, + i0.Value expireAt, + i0.Value destroyKey, + }); + +class $RiverpodFilterComposer + extends i0.Composer { + $RiverpodFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get key => $composableBuilder( + column: $table.key, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get json => $composableBuilder( + column: $table.json, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get expireAt => $composableBuilder( + column: $table.expireAt, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get destroyKey => $composableBuilder( + column: $table.destroyKey, + builder: (column) => i0.ColumnFilters(column), + ); +} + +class $RiverpodOrderingComposer + extends i0.Composer { + $RiverpodOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get key => $composableBuilder( + column: $table.key, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get json => $composableBuilder( + column: $table.json, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get expireAt => $composableBuilder( + column: $table.expireAt, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get destroyKey => $composableBuilder( + column: $table.destroyKey, + builder: (column) => i0.ColumnOrderings(column), + ); +} + +class $RiverpodAnnotationComposer + extends i0.Composer { + $RiverpodAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get key => + $composableBuilder(column: $table.key, builder: (column) => column); + + i0.GeneratedColumn get json => + $composableBuilder(column: $table.json, builder: (column) => column); + + i0.GeneratedColumn get expireAt => + $composableBuilder(column: $table.expireAt, builder: (column) => column); + + i0.GeneratedColumn get destroyKey => $composableBuilder( + column: $table.destroyKey, + builder: (column) => column, + ); +} + +class $RiverpodTableManager + extends + i0.RootTableManager< + i0.GeneratedDatabase, + i1.Riverpod, + i1.RiverpodData, + i1.$RiverpodFilterComposer, + i1.$RiverpodOrderingComposer, + i1.$RiverpodAnnotationComposer, + $RiverpodCreateCompanionBuilder, + $RiverpodUpdateCompanionBuilder, + ( + i1.RiverpodData, + i0.BaseReferences< + i0.GeneratedDatabase, + i1.Riverpod, + i1.RiverpodData + >, + ), + i1.RiverpodData, + i0.PrefetchHooks Function() + > { + $RiverpodTableManager(i0.GeneratedDatabase db, i1.Riverpod table) + : super( + i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$RiverpodFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$RiverpodOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$RiverpodAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + i0.Value key = const i0.Value.absent(), + i0.Value json = const i0.Value.absent(), + i0.Value expireAt = const i0.Value.absent(), + i0.Value destroyKey = const i0.Value.absent(), + }) => i1.RiverpodCompanion( + key: key, + json: json, + expireAt: expireAt, + destroyKey: destroyKey, + ), + createCompanionCallback: + ({ + required String key, + required String json, + i0.Value expireAt = const i0.Value.absent(), + i0.Value destroyKey = const i0.Value.absent(), + }) => i1.RiverpodCompanion.insert( + key: key, + json: json, + expireAt: expireAt, + destroyKey: destroyKey, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $RiverpodProcessedTableManager = + i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.Riverpod, + i1.RiverpodData, + i1.$RiverpodFilterComposer, + i1.$RiverpodOrderingComposer, + i1.$RiverpodAnnotationComposer, + $RiverpodCreateCompanionBuilder, + $RiverpodUpdateCompanionBuilder, + ( + i1.RiverpodData, + i0.BaseReferences, + ), + i1.RiverpodData, + i0.PrefetchHooks Function() + >; class Setting extends i0.Table with i0.TableInfo { @override @@ -1126,6 +1307,257 @@ class OnboardingCompanion extends i0.UpdateCompanion { } } +class Riverpod extends i0.Table with i0.TableInfo { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + Riverpod(this.attachedDatabase, [this._alias]); + late final i0.GeneratedColumn key = i0.GeneratedColumn( + 'key', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final i0.GeneratedColumn json = i0.GeneratedColumn( + 'json', + aliasedName, + false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final i0.GeneratedColumn expireAt = + i0.GeneratedColumn( + 'expireAt', + aliasedName, + true, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final i0.GeneratedColumn destroyKey = i0.GeneratedColumn( + 'destroyKey', + aliasedName, + true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, json, expireAt, destroyKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'riverpod'; + @override + Set get $primaryKey => {key}; + @override + i1.RiverpodData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RiverpodData( + key: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + json: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}json'], + )!, + expireAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}expireAt'], + ), + destroyKey: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}destroyKey'], + ), + ); + } + + @override + Riverpod createAlias(String alias) { + return Riverpod(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get dontWriteConstraints => true; +} + +class RiverpodData extends i0.DataClass + implements i0.Insertable { + final String key; + final String json; + final DateTime? expireAt; + final String? destroyKey; + const RiverpodData({ + required this.key, + required this.json, + this.expireAt, + this.destroyKey, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = i0.Variable(key); + map['json'] = i0.Variable(json); + if (!nullToAbsent || expireAt != null) { + map['expireAt'] = i0.Variable(expireAt); + } + if (!nullToAbsent || destroyKey != null) { + map['destroyKey'] = i0.Variable(destroyKey); + } + return map; + } + + factory RiverpodData.fromJson( + Map json, { + i0.ValueSerializer? serializer, + }) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RiverpodData( + key: serializer.fromJson(json['key']), + json: serializer.fromJson(json['json']), + expireAt: serializer.fromJson(json['expireAt']), + destroyKey: serializer.fromJson(json['destroyKey']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'json': serializer.toJson(json), + 'expireAt': serializer.toJson(expireAt), + 'destroyKey': serializer.toJson(destroyKey), + }; + } + + i1.RiverpodData copyWith({ + String? key, + String? json, + i0.Value expireAt = const i0.Value.absent(), + i0.Value destroyKey = const i0.Value.absent(), + }) => i1.RiverpodData( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt.present ? expireAt.value : this.expireAt, + destroyKey: destroyKey.present ? destroyKey.value : this.destroyKey, + ); + RiverpodData copyWithCompanion(i1.RiverpodCompanion data) { + return RiverpodData( + key: data.key.present ? data.key.value : this.key, + json: data.json.present ? data.json.value : this.json, + expireAt: data.expireAt.present ? data.expireAt.value : this.expireAt, + destroyKey: data.destroyKey.present + ? data.destroyKey.value + : this.destroyKey, + ); + } + + @override + String toString() { + return (StringBuffer('RiverpodData(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, json, expireAt, destroyKey); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RiverpodData && + other.key == this.key && + other.json == this.json && + other.expireAt == this.expireAt && + other.destroyKey == this.destroyKey); +} + +class RiverpodCompanion extends i0.UpdateCompanion { + final i0.Value key; + final i0.Value json; + final i0.Value expireAt; + final i0.Value destroyKey; + const RiverpodCompanion({ + this.key = const i0.Value.absent(), + this.json = const i0.Value.absent(), + this.expireAt = const i0.Value.absent(), + this.destroyKey = const i0.Value.absent(), + }); + RiverpodCompanion.insert({ + required String key, + required String json, + this.expireAt = const i0.Value.absent(), + this.destroyKey = const i0.Value.absent(), + }) : key = i0.Value(key), + json = i0.Value(json); + static i0.Insertable custom({ + i0.Expression? key, + i0.Expression? json, + i0.Expression? expireAt, + i0.Expression? destroyKey, + }) { + return i0.RawValuesInsertable({ + if (key != null) 'key': key, + if (json != null) 'json': json, + if (expireAt != null) 'expireAt': expireAt, + if (destroyKey != null) 'destroyKey': destroyKey, + }); + } + + i1.RiverpodCompanion copyWith({ + i0.Value? key, + i0.Value? json, + i0.Value? expireAt, + i0.Value? destroyKey, + }) { + return i1.RiverpodCompanion( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt ?? this.expireAt, + destroyKey: destroyKey ?? this.destroyKey, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = i0.Variable(key.value); + } + if (json.present) { + map['json'] = i0.Variable(json.value); + } + if (expireAt.present) { + map['expireAt'] = i0.Variable(expireAt.value); + } + if (destroyKey.present) { + map['destroyKey'] = i0.Variable(destroyKey.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RiverpodCompanion(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } +} + class DefinitionsDrift extends i3.ModularAccessor { DefinitionsDrift(i0.GeneratedDatabase db) : super(db); Future evictCacheEntries({required int limit}) { diff --git a/app/lib/features/user/data/database/riverpod_storage.dart b/app/lib/features/user/data/database/riverpod_storage.dart new file mode 100644 index 00000000..71176671 --- /dev/null +++ b/app/lib/features/user/data/database/riverpod_storage.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:nullability/nullability.dart'; +import 'package:riverpod/experimental/persist.dart'; +import 'package:weblibre/features/user/data/database/database.dart'; +import 'package:weblibre/features/user/data/database/definitions.drift.dart'; + +final class RiverpodStorage extends Storage { + final UserDatabase _db; + + RiverpodStorage(this._db); + + @override + Future delete(String key) { + return _db.riverpod.deleteWhere((x) => x.key.equals(key)); + } + + @override + Future deleteOutOfDate() { + return _db.riverpod.deleteWhere( + (x) => x.expireAt.isSmallerThanValue(DateTime.now()), + ); + } + + @override + Future?> read(String key) async { + final query = _db.riverpod.select()..where((x) => x.key.equals(key)); + + final data = await query.getSingleOrNull(); + + return data.mapNotNull( + (data) => PersistedData( + data.json, + destroyKey: data.destroyKey, + expireAt: data.expireAt, + ), + ); + } + + @override + Future write(String key, String value, StorageOptions options) { + return _db.riverpod.insertOne( + RiverpodCompanion.insert( + key: key, + json: value, + destroyKey: Value(options.destroyKey), + expireAt: Value( + options.cacheTime.duration.mapNotNull( + (duration) => DateTime.now().add(duration), + ), + ), + ), + mode: InsertMode.insertOrReplace, + ); + } +} diff --git a/app/lib/features/user/data/providers.dart b/app/lib/features/user/data/providers.dart index 391b32b5..8d04ace6 100644 --- a/app/lib/features/user/data/providers.dart +++ b/app/lib/features/user/data/providers.dart @@ -21,11 +21,13 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:riverpod/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; import 'package:universal_io/io.dart'; import 'package:weblibre/features/user/data/database/database.dart'; +import 'package:weblibre/features/user/data/database/riverpod_storage.dart'; part 'providers.g.dart'; @@ -60,3 +62,9 @@ UserDatabase userDatabase(Ref ref) { return db; } + +@Riverpod(keepAlive: true) +Storage riverpodDatabaseStorage(Ref ref) { + final db = ref.watch(userDatabaseProvider); + return RiverpodStorage(db); +} diff --git a/app/lib/features/user/data/providers.g.dart b/app/lib/features/user/data/providers.g.dart index 96b3418c..426de194 100644 --- a/app/lib/features/user/data/providers.g.dart +++ b/app/lib/features/user/data/providers.g.dart @@ -49,3 +49,51 @@ final class UserDatabaseProvider } String _$userDatabaseHash() => r'b925780435806f0241d7ddb635e301e9fc8baf6e'; + +@ProviderFor(riverpodDatabaseStorage) +const riverpodDatabaseStorageProvider = RiverpodDatabaseStorageProvider._(); + +final class RiverpodDatabaseStorageProvider + extends + $FunctionalProvider< + Storage, + Storage, + Storage + > + with $Provider> { + const RiverpodDatabaseStorageProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'riverpodDatabaseStorageProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$riverpodDatabaseStorageHash(); + + @$internal + @override + $ProviderElement> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + Storage create(Ref ref) { + return riverpodDatabaseStorage(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Storage value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$riverpodDatabaseStorageHash() => + r'e60613538dcb1f9cf48d87c029d8493335a5c3e8'; diff --git a/app/test/drift/user/generated/schema.dart b/app/test/drift/user/generated/schema.dart new file mode 100644 index 00000000..b2b7404b --- /dev/null +++ b/app/test/drift/user/generated/schema.dart @@ -0,0 +1,23 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; +import 'package:drift/internal/migrations.dart'; +import 'schema_v1.dart' as v1; +import 'schema_v2.dart' as v2; + +class GeneratedHelper implements SchemaInstantiationHelper { + @override + GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { + switch (version) { + case 1: + return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); + default: + throw MissingSchemaException(version, versions); + } + } + + static const versions = const [1, 2]; +} diff --git a/app/test/drift/user/generated/schema_v1.dart b/app/test/drift/user/generated/schema_v1.dart new file mode 100644 index 00000000..24f36374 --- /dev/null +++ b/app/test/drift/user/generated/schema_v1.dart @@ -0,0 +1,651 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn partitionKey = GeneratedColumn( + 'partition_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + true, + type: DriftSqlType.any, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, partitionKey, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + partitionKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}partition_key'], + ), + value: attachedDatabase.typeMapping.read( + DriftSqlType.any, + data['${effectivePrefix}value'], + ), + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } + + @override + bool get isStrict => true; + @override + bool get dontWriteConstraints => true; +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String? partitionKey; + final DriftAny? value; + const SettingData({required this.key, this.partitionKey, this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + if (!nullToAbsent || partitionKey != null) { + map['partition_key'] = Variable(partitionKey); + } + if (!nullToAbsent || value != null) { + map['value'] = Variable(value); + } + return map; + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + partitionKey: serializer.fromJson(json['partitionKey']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'partitionKey': serializer.toJson(partitionKey), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({ + String? key, + Value partitionKey = const Value.absent(), + Value value = const Value.absent(), + }) => SettingData( + key: key ?? this.key, + partitionKey: partitionKey.present ? partitionKey.value : this.partitionKey, + value: value.present ? value.value : this.value, + ); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + partitionKey: data.partitionKey.present + ? data.partitionKey.value + : this.partitionKey, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, partitionKey, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.partitionKey == this.partitionKey && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value partitionKey; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }) : key = Value(key); + static Insertable custom({ + Expression? key, + Expression? partitionKey, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (partitionKey != null) 'partition_key': partitionKey, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? partitionKey, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + partitionKey: partitionKey ?? this.partitionKey, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (partitionKey.present) { + map['partition_key'] = Variable(partitionKey.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class IconCache extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + IconCache(this.attachedDatabase, [this._alias]); + late final GeneratedColumn origin = GeneratedColumn( + 'origin', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn iconData = + GeneratedColumn( + 'icon_data', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn fetchDate = GeneratedColumn( + 'fetch_date', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [origin, iconData, fetchDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'icon_cache'; + @override + Set get $primaryKey => {origin}; + @override + IconCacheData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return IconCacheData( + origin: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin'], + )!, + iconData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}icon_data'], + )!, + fetchDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}fetch_date'], + )!, + ); + } + + @override + IconCache createAlias(String alias) { + return IconCache(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class IconCacheData extends DataClass implements Insertable { + final String origin; + final i2.Uint8List iconData; + final DateTime fetchDate; + const IconCacheData({ + required this.origin, + required this.iconData, + required this.fetchDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['origin'] = Variable(origin); + map['icon_data'] = Variable(iconData); + map['fetch_date'] = Variable(fetchDate); + return map; + } + + factory IconCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return IconCacheData( + origin: serializer.fromJson(json['origin']), + iconData: serializer.fromJson(json['iconData']), + fetchDate: serializer.fromJson(json['fetchDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'origin': serializer.toJson(origin), + 'iconData': serializer.toJson(iconData), + 'fetchDate': serializer.toJson(fetchDate), + }; + } + + IconCacheData copyWith({ + String? origin, + i2.Uint8List? iconData, + DateTime? fetchDate, + }) => IconCacheData( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + ); + IconCacheData copyWithCompanion(IconCacheCompanion data) { + return IconCacheData( + origin: data.origin.present ? data.origin.value : this.origin, + iconData: data.iconData.present ? data.iconData.value : this.iconData, + fetchDate: data.fetchDate.present ? data.fetchDate.value : this.fetchDate, + ); + } + + @override + String toString() { + return (StringBuffer('IconCacheData(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(origin, $driftBlobEquality.hash(iconData), fetchDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is IconCacheData && + other.origin == this.origin && + $driftBlobEquality.equals(other.iconData, this.iconData) && + other.fetchDate == this.fetchDate); +} + +class IconCacheCompanion extends UpdateCompanion { + final Value origin; + final Value iconData; + final Value fetchDate; + final Value rowid; + const IconCacheCompanion({ + this.origin = const Value.absent(), + this.iconData = const Value.absent(), + this.fetchDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + IconCacheCompanion.insert({ + required String origin, + required i2.Uint8List iconData, + required DateTime fetchDate, + this.rowid = const Value.absent(), + }) : origin = Value(origin), + iconData = Value(iconData), + fetchDate = Value(fetchDate); + static Insertable custom({ + Expression? origin, + Expression? iconData, + Expression? fetchDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (origin != null) 'origin': origin, + if (iconData != null) 'icon_data': iconData, + if (fetchDate != null) 'fetch_date': fetchDate, + if (rowid != null) 'rowid': rowid, + }); + } + + IconCacheCompanion copyWith({ + Value? origin, + Value? iconData, + Value? fetchDate, + Value? rowid, + }) { + return IconCacheCompanion( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (origin.present) { + map['origin'] = Variable(origin.value); + } + if (iconData.present) { + map['icon_data'] = Variable(iconData.value); + } + if (fetchDate.present) { + map['fetch_date'] = Variable(fetchDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('IconCacheCompanion(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Onboarding extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Onboarding(this.attachedDatabase, [this._alias]); + late final GeneratedColumn revision = GeneratedColumn( + 'revision', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn completionDate = + GeneratedColumn( + 'completion_date', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [revision, completionDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'onboarding'; + @override + Set get $primaryKey => const {}; + @override + OnboardingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OnboardingData( + revision: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}revision'], + )!, + completionDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}completion_date'], + )!, + ); + } + + @override + Onboarding createAlias(String alias) { + return Onboarding(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class OnboardingData extends DataClass implements Insertable { + final int revision; + final DateTime completionDate; + const OnboardingData({required this.revision, required this.completionDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['revision'] = Variable(revision); + map['completion_date'] = Variable(completionDate); + return map; + } + + factory OnboardingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OnboardingData( + revision: serializer.fromJson(json['revision']), + completionDate: serializer.fromJson(json['completionDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'revision': serializer.toJson(revision), + 'completionDate': serializer.toJson(completionDate), + }; + } + + OnboardingData copyWith({int? revision, DateTime? completionDate}) => + OnboardingData( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + ); + OnboardingData copyWithCompanion(OnboardingCompanion data) { + return OnboardingData( + revision: data.revision.present ? data.revision.value : this.revision, + completionDate: data.completionDate.present + ? data.completionDate.value + : this.completionDate, + ); + } + + @override + String toString() { + return (StringBuffer('OnboardingData(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(revision, completionDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OnboardingData && + other.revision == this.revision && + other.completionDate == this.completionDate); +} + +class OnboardingCompanion extends UpdateCompanion { + final Value revision; + final Value completionDate; + final Value rowid; + const OnboardingCompanion({ + this.revision = const Value.absent(), + this.completionDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + OnboardingCompanion.insert({ + required int revision, + required DateTime completionDate, + this.rowid = const Value.absent(), + }) : revision = Value(revision), + completionDate = Value(completionDate); + static Insertable custom({ + Expression? revision, + Expression? completionDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (revision != null) 'revision': revision, + if (completionDate != null) 'completion_date': completionDate, + if (rowid != null) 'rowid': rowid, + }); + } + + OnboardingCompanion copyWith({ + Value? revision, + Value? completionDate, + Value? rowid, + }) { + return OnboardingCompanion( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (revision.present) { + map['revision'] = Variable(revision.value); + } + if (completionDate.present) { + map['completion_date'] = Variable(completionDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OnboardingCompanion(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV1 extends GeneratedDatabase { + DatabaseAtV1(QueryExecutor e) : super(e); + late final Setting setting = Setting(this); + late final IconCache iconCache = IconCache(this); + late final Onboarding onboarding = Onboarding(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + setting, + iconCache, + onboarding, + ]; + @override + int get schemaVersion => 1; +} diff --git a/app/test/drift/user/generated/schema_v2.dart b/app/test/drift/user/generated/schema_v2.dart new file mode 100644 index 00000000..b70d507c --- /dev/null +++ b/app/test/drift/user/generated/schema_v2.dart @@ -0,0 +1,902 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class Setting extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Setting(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn partitionKey = GeneratedColumn( + 'partition_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + true, + type: DriftSqlType.any, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, partitionKey, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'setting'; + @override + Set get $primaryKey => {key}; + @override + SettingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SettingData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + partitionKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}partition_key'], + ), + value: attachedDatabase.typeMapping.read( + DriftSqlType.any, + data['${effectivePrefix}value'], + ), + ); + } + + @override + Setting createAlias(String alias) { + return Setting(attachedDatabase, alias); + } + + @override + bool get isStrict => true; + @override + bool get dontWriteConstraints => true; +} + +class SettingData extends DataClass implements Insertable { + final String key; + final String? partitionKey; + final DriftAny? value; + const SettingData({required this.key, this.partitionKey, this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + if (!nullToAbsent || partitionKey != null) { + map['partition_key'] = Variable(partitionKey); + } + if (!nullToAbsent || value != null) { + map['value'] = Variable(value); + } + return map; + } + + factory SettingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SettingData( + key: serializer.fromJson(json['key']), + partitionKey: serializer.fromJson(json['partitionKey']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'partitionKey': serializer.toJson(partitionKey), + 'value': serializer.toJson(value), + }; + } + + SettingData copyWith({ + String? key, + Value partitionKey = const Value.absent(), + Value value = const Value.absent(), + }) => SettingData( + key: key ?? this.key, + partitionKey: partitionKey.present ? partitionKey.value : this.partitionKey, + value: value.present ? value.value : this.value, + ); + SettingData copyWithCompanion(SettingCompanion data) { + return SettingData( + key: data.key.present ? data.key.value : this.key, + partitionKey: data.partitionKey.present + ? data.partitionKey.value + : this.partitionKey, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('SettingData(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, partitionKey, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SettingData && + other.key == this.key && + other.partitionKey == this.partitionKey && + other.value == this.value); +} + +class SettingCompanion extends UpdateCompanion { + final Value key; + final Value partitionKey; + final Value value; + final Value rowid; + const SettingCompanion({ + this.key = const Value.absent(), + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + SettingCompanion.insert({ + required String key, + this.partitionKey = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }) : key = Value(key); + static Insertable custom({ + Expression? key, + Expression? partitionKey, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (partitionKey != null) 'partition_key': partitionKey, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + SettingCompanion copyWith({ + Value? key, + Value? partitionKey, + Value? value, + Value? rowid, + }) { + return SettingCompanion( + key: key ?? this.key, + partitionKey: partitionKey ?? this.partitionKey, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (partitionKey.present) { + map['partition_key'] = Variable(partitionKey.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SettingCompanion(') + ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class IconCache extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + IconCache(this.attachedDatabase, [this._alias]); + late final GeneratedColumn origin = GeneratedColumn( + 'origin', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn iconData = + GeneratedColumn( + 'icon_data', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn fetchDate = GeneratedColumn( + 'fetch_date', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [origin, iconData, fetchDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'icon_cache'; + @override + Set get $primaryKey => {origin}; + @override + IconCacheData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return IconCacheData( + origin: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}origin'], + )!, + iconData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}icon_data'], + )!, + fetchDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}fetch_date'], + )!, + ); + } + + @override + IconCache createAlias(String alias) { + return IconCache(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class IconCacheData extends DataClass implements Insertable { + final String origin; + final i2.Uint8List iconData; + final DateTime fetchDate; + const IconCacheData({ + required this.origin, + required this.iconData, + required this.fetchDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['origin'] = Variable(origin); + map['icon_data'] = Variable(iconData); + map['fetch_date'] = Variable(fetchDate); + return map; + } + + factory IconCacheData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return IconCacheData( + origin: serializer.fromJson(json['origin']), + iconData: serializer.fromJson(json['iconData']), + fetchDate: serializer.fromJson(json['fetchDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'origin': serializer.toJson(origin), + 'iconData': serializer.toJson(iconData), + 'fetchDate': serializer.toJson(fetchDate), + }; + } + + IconCacheData copyWith({ + String? origin, + i2.Uint8List? iconData, + DateTime? fetchDate, + }) => IconCacheData( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + ); + IconCacheData copyWithCompanion(IconCacheCompanion data) { + return IconCacheData( + origin: data.origin.present ? data.origin.value : this.origin, + iconData: data.iconData.present ? data.iconData.value : this.iconData, + fetchDate: data.fetchDate.present ? data.fetchDate.value : this.fetchDate, + ); + } + + @override + String toString() { + return (StringBuffer('IconCacheData(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(origin, $driftBlobEquality.hash(iconData), fetchDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is IconCacheData && + other.origin == this.origin && + $driftBlobEquality.equals(other.iconData, this.iconData) && + other.fetchDate == this.fetchDate); +} + +class IconCacheCompanion extends UpdateCompanion { + final Value origin; + final Value iconData; + final Value fetchDate; + final Value rowid; + const IconCacheCompanion({ + this.origin = const Value.absent(), + this.iconData = const Value.absent(), + this.fetchDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + IconCacheCompanion.insert({ + required String origin, + required i2.Uint8List iconData, + required DateTime fetchDate, + this.rowid = const Value.absent(), + }) : origin = Value(origin), + iconData = Value(iconData), + fetchDate = Value(fetchDate); + static Insertable custom({ + Expression? origin, + Expression? iconData, + Expression? fetchDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (origin != null) 'origin': origin, + if (iconData != null) 'icon_data': iconData, + if (fetchDate != null) 'fetch_date': fetchDate, + if (rowid != null) 'rowid': rowid, + }); + } + + IconCacheCompanion copyWith({ + Value? origin, + Value? iconData, + Value? fetchDate, + Value? rowid, + }) { + return IconCacheCompanion( + origin: origin ?? this.origin, + iconData: iconData ?? this.iconData, + fetchDate: fetchDate ?? this.fetchDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (origin.present) { + map['origin'] = Variable(origin.value); + } + if (iconData.present) { + map['icon_data'] = Variable(iconData.value); + } + if (fetchDate.present) { + map['fetch_date'] = Variable(fetchDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('IconCacheCompanion(') + ..write('origin: $origin, ') + ..write('iconData: $iconData, ') + ..write('fetchDate: $fetchDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Onboarding extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Onboarding(this.attachedDatabase, [this._alias]); + late final GeneratedColumn revision = GeneratedColumn( + 'revision', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn completionDate = + GeneratedColumn( + 'completion_date', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [revision, completionDate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'onboarding'; + @override + Set get $primaryKey => const {}; + @override + OnboardingData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return OnboardingData( + revision: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}revision'], + )!, + completionDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}completion_date'], + )!, + ); + } + + @override + Onboarding createAlias(String alias) { + return Onboarding(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class OnboardingData extends DataClass implements Insertable { + final int revision; + final DateTime completionDate; + const OnboardingData({required this.revision, required this.completionDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['revision'] = Variable(revision); + map['completion_date'] = Variable(completionDate); + return map; + } + + factory OnboardingData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return OnboardingData( + revision: serializer.fromJson(json['revision']), + completionDate: serializer.fromJson(json['completionDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'revision': serializer.toJson(revision), + 'completionDate': serializer.toJson(completionDate), + }; + } + + OnboardingData copyWith({int? revision, DateTime? completionDate}) => + OnboardingData( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + ); + OnboardingData copyWithCompanion(OnboardingCompanion data) { + return OnboardingData( + revision: data.revision.present ? data.revision.value : this.revision, + completionDate: data.completionDate.present + ? data.completionDate.value + : this.completionDate, + ); + } + + @override + String toString() { + return (StringBuffer('OnboardingData(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(revision, completionDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is OnboardingData && + other.revision == this.revision && + other.completionDate == this.completionDate); +} + +class OnboardingCompanion extends UpdateCompanion { + final Value revision; + final Value completionDate; + final Value rowid; + const OnboardingCompanion({ + this.revision = const Value.absent(), + this.completionDate = const Value.absent(), + this.rowid = const Value.absent(), + }); + OnboardingCompanion.insert({ + required int revision, + required DateTime completionDate, + this.rowid = const Value.absent(), + }) : revision = Value(revision), + completionDate = Value(completionDate); + static Insertable custom({ + Expression? revision, + Expression? completionDate, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (revision != null) 'revision': revision, + if (completionDate != null) 'completion_date': completionDate, + if (rowid != null) 'rowid': rowid, + }); + } + + OnboardingCompanion copyWith({ + Value? revision, + Value? completionDate, + Value? rowid, + }) { + return OnboardingCompanion( + revision: revision ?? this.revision, + completionDate: completionDate ?? this.completionDate, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (revision.present) { + map['revision'] = Variable(revision.value); + } + if (completionDate.present) { + map['completion_date'] = Variable(completionDate.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('OnboardingCompanion(') + ..write('revision: $revision, ') + ..write('completionDate: $completionDate, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Riverpod extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Riverpod(this.attachedDatabase, [this._alias]); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL', + ); + late final GeneratedColumn json = GeneratedColumn( + 'json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn expireAt = GeneratedColumn( + 'expireAt', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumn destroyKey = GeneratedColumn( + 'destroyKey', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + @override + List get $columns => [key, json, expireAt, destroyKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'riverpod'; + @override + Set get $primaryKey => {key}; + @override + RiverpodData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RiverpodData( + key: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}key'], + )!, + json: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}json'], + )!, + expireAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}expireAt'], + ), + destroyKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}destroyKey'], + ), + ); + } + + @override + Riverpod createAlias(String alias) { + return Riverpod(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get dontWriteConstraints => true; +} + +class RiverpodData extends DataClass implements Insertable { + final String key; + final String json; + final DateTime? expireAt; + final String? destroyKey; + const RiverpodData({ + required this.key, + required this.json, + this.expireAt, + this.destroyKey, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + map['json'] = Variable(json); + if (!nullToAbsent || expireAt != null) { + map['expireAt'] = Variable(expireAt); + } + if (!nullToAbsent || destroyKey != null) { + map['destroyKey'] = Variable(destroyKey); + } + return map; + } + + factory RiverpodData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RiverpodData( + key: serializer.fromJson(json['key']), + json: serializer.fromJson(json['json']), + expireAt: serializer.fromJson(json['expireAt']), + destroyKey: serializer.fromJson(json['destroyKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'json': serializer.toJson(json), + 'expireAt': serializer.toJson(expireAt), + 'destroyKey': serializer.toJson(destroyKey), + }; + } + + RiverpodData copyWith({ + String? key, + String? json, + Value expireAt = const Value.absent(), + Value destroyKey = const Value.absent(), + }) => RiverpodData( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt.present ? expireAt.value : this.expireAt, + destroyKey: destroyKey.present ? destroyKey.value : this.destroyKey, + ); + RiverpodData copyWithCompanion(RiverpodCompanion data) { + return RiverpodData( + key: data.key.present ? data.key.value : this.key, + json: data.json.present ? data.json.value : this.json, + expireAt: data.expireAt.present ? data.expireAt.value : this.expireAt, + destroyKey: data.destroyKey.present + ? data.destroyKey.value + : this.destroyKey, + ); + } + + @override + String toString() { + return (StringBuffer('RiverpodData(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, json, expireAt, destroyKey); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RiverpodData && + other.key == this.key && + other.json == this.json && + other.expireAt == this.expireAt && + other.destroyKey == this.destroyKey); +} + +class RiverpodCompanion extends UpdateCompanion { + final Value key; + final Value json; + final Value expireAt; + final Value destroyKey; + const RiverpodCompanion({ + this.key = const Value.absent(), + this.json = const Value.absent(), + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }); + RiverpodCompanion.insert({ + required String key, + required String json, + this.expireAt = const Value.absent(), + this.destroyKey = const Value.absent(), + }) : key = Value(key), + json = Value(json); + static Insertable custom({ + Expression? key, + Expression? json, + Expression? expireAt, + Expression? destroyKey, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (json != null) 'json': json, + if (expireAt != null) 'expireAt': expireAt, + if (destroyKey != null) 'destroyKey': destroyKey, + }); + } + + RiverpodCompanion copyWith({ + Value? key, + Value? json, + Value? expireAt, + Value? destroyKey, + }) { + return RiverpodCompanion( + key: key ?? this.key, + json: json ?? this.json, + expireAt: expireAt ?? this.expireAt, + destroyKey: destroyKey ?? this.destroyKey, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (json.present) { + map['json'] = Variable(json.value); + } + if (expireAt.present) { + map['expireAt'] = Variable(expireAt.value); + } + if (destroyKey.present) { + map['destroyKey'] = Variable(destroyKey.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RiverpodCompanion(') + ..write('key: $key, ') + ..write('json: $json, ') + ..write('expireAt: $expireAt, ') + ..write('destroyKey: $destroyKey') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV2 extends GeneratedDatabase { + DatabaseAtV2(QueryExecutor e) : super(e); + late final Setting setting = Setting(this); + late final IconCache iconCache = IconCache(this); + late final Onboarding onboarding = Onboarding(this); + late final Riverpod riverpod = Riverpod(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + setting, + iconCache, + onboarding, + riverpod, + ]; + @override + int get schemaVersion => 2; +} diff --git a/app/test/drift/user/migration_test.dart b/app/test/drift/user/migration_test.dart new file mode 100644 index 00000000..0ead8ba9 --- /dev/null +++ b/app/test/drift/user/migration_test.dart @@ -0,0 +1,84 @@ +// dart format width=80 +// ignore_for_file: unused_local_variable, unused_import +import 'package:drift/drift.dart'; +import 'package:drift_dev/api/migrations_native.dart'; +import 'package:weblibre/features/user/data/database/database.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'generated/schema.dart'; + +import 'generated/schema_v1.dart' as v1; +import 'generated/schema_v2.dart' as v2; + +void main() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + late SchemaVerifier verifier; + + setUpAll(() { + verifier = SchemaVerifier(GeneratedHelper()); + }); + + group('simple database migrations', () { + // These simple tests verify all possible schema updates with a simple (no + // data) migration. This is a quick way to ensure that written database + // migrations properly alter the schema. + const versions = GeneratedHelper.versions; + for (final (i, fromVersion) in versions.indexed) { + group('from $fromVersion', () { + for (final toVersion in versions.skip(i + 1)) { + test('to $toVersion', () async { + final schema = await verifier.schemaAt(fromVersion); + final db = UserDatabase(schema.newConnection()); + await verifier.migrateAndValidate(db, toVersion); + await db.close(); + }); + } + }); + } + }); + + // The following template shows how to write tests ensuring your migrations + // preserve existing data. + // Testing this can be useful for migrations that change existing columns + // (e.g. by alterating their type or constraints). Migrations that only add + // tables or columns typically don't need these advanced tests. For more + // information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity + // TODO: This generated template shows how these tests could be written. Adopt + // it to your own needs when testing migrations with data integrity. + test('migration from v1 to v2 does not corrupt data', () async { + // Add data to insert into the old database, and the expected rows after the + // migration. + // TODO: Fill these lists + final oldSettingData = []; + final expectedNewSettingData = []; + + final oldIconCacheData = []; + final expectedNewIconCacheData = []; + + final oldOnboardingData = []; + final expectedNewOnboardingData = []; + + await verifier.testWithDataIntegrity( + oldVersion: 1, + newVersion: 2, + createOld: v1.DatabaseAtV1.new, + createNew: v2.DatabaseAtV2.new, + openTestedDatabase: UserDatabase.new, + createItems: (batch, oldDb) { + batch.insertAll(oldDb.setting, oldSettingData); + batch.insertAll(oldDb.iconCache, oldIconCacheData); + batch.insertAll(oldDb.onboarding, oldOnboardingData); + }, + validateItems: (newDb) async { + expect(expectedNewSettingData, await newDb.select(newDb.setting).get()); + expect( + expectedNewIconCacheData, + await newDb.select(newDb.iconCache).get(), + ); + expect( + expectedNewOnboardingData, + await newDb.select(newDb.onboarding).get(), + ); + }, + ); + }); +}