Files
mugiten/lib/database/database.dart

244 lines
6.4 KiB
Dart

import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:jadb/search.dart';
import 'package:mugiten/models/verify_tables.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
export 'package:sqflite/sqlite_api.dart';
const int expectedDatabaseVersion = 2;
Database db() => GetIt.instance.get<Database>();
/// Returns the directory where mugiten's database file is stored.
Future<Directory> _databaseDir() async {
final Directory appDocDir = await getApplicationDocumentsDirectory();
if (!appDocDir.existsSync()) appDocDir.createSync(recursive: true);
return appDocDir;
}
/// Returns the expected path to mugiten's database file.
Future<String> databasePath() async {
return join((await _databaseDir()).path, 'mugiten.sqlite');
}
Future<bool> databaseNeedsInitialization() async {
final String dbPath = await databasePath();
if (!await File(dbPath).exists()) {
return true;
}
final Database database = await openDatabase(
dbPath,
readOnly: true,
singleInstance: true,
);
final databaseVersion = await database.getVersion();
await database.close();
if (databaseVersion < expectedDatabaseVersion) {
return true;
}
return false;
}
Future<void> quickInitializeDatabase() async {
// TODO: Create more lightweight solution
await setupDatabase();
}
/// Migration logic and heavy initialization
class DatabaseMigration {
final String path;
final String content;
const DatabaseMigration({
required this.path,
required this.content,
});
int get version {
final String fileName = basenameWithoutExtension(path);
return int.parse(fileName.split('_')[0]);
}
@override
String toString() {
return 'DatabaseMigration(path: $path, content: ${content.length} chars)';
}
}
Future<List<DatabaseMigration>> readMigrationsFromAssets() async {
log('Reading migrations from assets...');
final String assetManifest =
await rootBundle.loadString('AssetManifest.json');
final List<String> migrations =
(jsonDecode(assetManifest) as Map<String, Object?>)
.keys
.where(
(assetPath) =>
RegExp(r'^migrations\/\d{4}.*\.sql$').hasMatch(assetPath),
)
.toList();
migrations.sort();
log('Found ${migrations.length} migration files:');
for (final migration in migrations) {
log(' - $migration');
}
return Future.wait(
migrations.map(
(migration) async {
final content = await rootBundle.loadString(migration, cache: false);
return DatabaseMigration(path: migration, content: content);
},
),
);
}
/// Migrates the database from version [oldVersion] to [newVersion].
Future<void> migrate(Database db, List<DatabaseMigration> migrations) async {
for (final migration in migrations) {
log('Running migration ${migration.version} from ${migration.path}');
migration.content
.split(';')
.map(
(s) => s
.split('\n')
.where((l) => !l.startsWith(RegExp(r'\s*--')))
.join('\n')
.trim(),
)
.where((s) => s != '')
.forEach(db.execute);
}
}
Future<Database> openDatabaseWithoutMigrations(
String dbPath, {
bool readOnly = false,
bool verifyTables = true,
}) async {
log('Opening database at $dbPath');
final Database database = await openDatabase(
dbPath,
version: expectedDatabaseVersion,
readOnly: readOnly,
onConfigure: (db) async {
// Enable foreign key constraints
await db.execute('PRAGMA foreign_keys=ON');
},
onOpen: (db) async {
if (verifyTables) {
log('Verifying jadb tables...');
db.jadbVerifyTables();
log('Verifying mugiten tables...');
verifyMugitenTablesWithDbConnection(db);
log('Database tables verified successfully');
}
},
);
return database;
}
Future<Database> openAndMigrateDatabase(
String dbPath,
List<DatabaseMigration> migrations,
) async {
log('Opening database at $dbPath');
final Database database = await openDatabase(
dbPath,
version: expectedDatabaseVersion,
readOnly: false,
onUpgrade: (db, oldVersion, newVersion) async {
log('Migrating database from v$oldVersion to v$newVersion...');
final migrationsToRun = migrations
.where((migration) =>
migration.version > oldVersion && migration.version <= newVersion)
.toList();
await migrate(db, migrationsToRun);
},
onConfigure: (db) async {
// Enable foreign key constraints
await db.execute('PRAGMA foreign_keys=ON');
},
onOpen: (db) async {
log('Verifying jadb tables...');
db.jadbVerifyTables();
log('Verifying jadb tables...');
verifyMugitenTablesWithDbConnection(db);
log('Database tables verified successfully');
},
);
return database;
}
/// Sets up the database, creating it if it does not exist.
Future<void> setupDatabase() async {
log('Setting up database...');
final String dbPath = await databasePath();
assert(
await File(dbPath).exists(), 'Database file should exist at this point');
final database = await openDatabaseWithoutMigrations(
dbPath,
readOnly: false,
verifyTables: true,
);
assert(await database.getVersion() == expectedDatabaseVersion,
'Database version should be $expectedDatabaseVersion');
log('Registering database in GetIt...');
GetIt.instance.registerSingleton<Database>(database);
}
/// Resets the database by closing it, deleting the file, and setting it up again.
Future<void> resetDatabase() async {
log('Closing database...');
await db().close();
log('Deleting mugiten.sqlite file...');
File(await databasePath()).deleteSync();
log('Unregistering database from GetIt...');
GetIt.instance.unregister<Database>();
log('Setting up database again...');
await setupDatabase();
}
/// Extracts the jadb.sqlite file from the assets into a writable directory
/// and returns its path.
Future<void> extractJadbFromAssets(String path) async {
final File jadbFile = File(path);
if (!await jadbFile.exists()) {
jadbFile.createSync();
}
ByteData data = await rootBundle.load('assets/jadb.sqlite');
await jadbFile.writeAsBytes(
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes));
}