mirror of https://github.com/AMT-Cheif/drift.git
FFI: Ability to override loading behavior, async api
This commit is contained in:
parent
22dee72680
commit
d6913af380
|
@ -0,0 +1,6 @@
|
|||
/// Exports the raw []
|
||||
library database;
|
||||
|
||||
export 'src/api/database.dart';
|
||||
export 'src/api/result.dart';
|
||||
export 'src/impl/database.dart' show SqliteException, Database;
|
|
@ -3,7 +3,7 @@ import 'dart:io';
|
|||
|
||||
import 'package:moor/backends.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor_ffi/src/api/database.dart';
|
||||
import 'package:moor_ffi/database.dart';
|
||||
|
||||
part 'src/vm_database.dart';
|
||||
part 'src/load_library.dart';
|
||||
|
|
|
@ -1,161 +1,44 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moor_ffi/src/bindings/constants.dart';
|
||||
import 'package:moor_ffi/src/bindings/types.dart' as types;
|
||||
import 'package:moor_ffi/src/bindings/bindings.dart';
|
||||
import 'package:moor_ffi/src/ffi/blob.dart';
|
||||
import 'package:moor_ffi/src/ffi/utils.dart';
|
||||
|
||||
part 'errors.dart';
|
||||
part 'prepared_statement.dart';
|
||||
part 'result.dart';
|
||||
|
||||
const _openingFlags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE;
|
||||
|
||||
class Database {
|
||||
final Pointer<types.Database> _db;
|
||||
final List<PreparedStatement> _preparedStmt = [];
|
||||
bool _isClosed = false;
|
||||
|
||||
Database._(this._db);
|
||||
|
||||
/// Opens the [file] as a sqlite3 database. The file will be created if it
|
||||
/// doesn't exist.
|
||||
factory Database.openFile(File file) => Database.open(file.absolute.path);
|
||||
|
||||
/// Opens an in-memory sqlite3 database.
|
||||
factory Database.memory() => Database.open(':memory:');
|
||||
|
||||
/// Opens an sqlite3 database from a filename.
|
||||
factory Database.open(String fileName) {
|
||||
final dbOut = Pointer<Pointer<types.Database>>.allocate();
|
||||
final pathC = CBlob.allocateString(fileName);
|
||||
|
||||
final resultCode =
|
||||
bindings.sqlite3_open_v2(pathC, dbOut, _openingFlags, nullptr.cast());
|
||||
final dbPointer = dbOut.load<Pointer<types.Database>>();
|
||||
|
||||
dbOut.free();
|
||||
pathC.free();
|
||||
|
||||
if (resultCode == Errors.SQLITE_OK) {
|
||||
return Database._(dbPointer);
|
||||
} else {
|
||||
throw SqliteException._fromErrorCode(dbPointer, resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureOpen() {
|
||||
if (_isClosed) {
|
||||
throw Exception('This database has already been closed');
|
||||
}
|
||||
}
|
||||
import 'package:moor_ffi/database.dart';
|
||||
|
||||
/// A opened sqlite database.
|
||||
abstract class BaseDatabase {
|
||||
/// Closes this database connection and releases the resources it uses. If
|
||||
/// an error occurs while closing the database, an exception will be thrown.
|
||||
/// The allocated memory will be freed either way.
|
||||
void close() {
|
||||
final code = bindings.sqlite3_close_v2(_db);
|
||||
SqliteException exception;
|
||||
if (code != Errors.SQLITE_OK) {
|
||||
exception = SqliteException._fromErrorCode(_db, code);
|
||||
}
|
||||
_isClosed = true;
|
||||
|
||||
for (var stmt in _preparedStmt) {
|
||||
stmt.close();
|
||||
}
|
||||
_db.free();
|
||||
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStmtFinalized(PreparedStatement stmt) {
|
||||
if (!_isClosed) {
|
||||
_preparedStmt.remove(stmt);
|
||||
}
|
||||
}
|
||||
FutureOr<void> close();
|
||||
|
||||
/// Executes the [sql] statement and ignores the result. Will throw if an
|
||||
/// error occurs while executing.
|
||||
void execute(String sql) {
|
||||
_ensureOpen();
|
||||
final sqlPtr = CBlob.allocateString(sql);
|
||||
final errorOut = Pointer<Pointer<CBlob>>.allocate();
|
||||
|
||||
final result =
|
||||
bindings.sqlite3_exec(_db, sqlPtr, nullptr, nullptr, errorOut);
|
||||
|
||||
sqlPtr.free();
|
||||
|
||||
final errorPtr = errorOut.load<Pointer<CBlob>>();
|
||||
errorOut.free();
|
||||
|
||||
String errorMsg;
|
||||
if (!isNullPointer(errorPtr)) {
|
||||
errorMsg = errorPtr.load<CBlob>().readString();
|
||||
// the message was allocated from sqlite, we need to free it
|
||||
bindings.sqlite3_free(errorPtr.cast());
|
||||
}
|
||||
|
||||
if (result != Errors.SQLITE_OK) {
|
||||
throw SqliteException(errorMsg);
|
||||
}
|
||||
}
|
||||
FutureOr<void> execute(String sql);
|
||||
|
||||
/// Prepares the [sql] statement.
|
||||
PreparedStatement prepare(String sql) {
|
||||
_ensureOpen();
|
||||
|
||||
final stmtOut = Pointer<Pointer<types.Statement>>.allocate();
|
||||
final sqlPtr = CBlob.allocateString(sql);
|
||||
|
||||
final resultCode =
|
||||
bindings.sqlite3_prepare_v2(_db, sqlPtr, -1, stmtOut, nullptr.cast());
|
||||
sqlPtr.free();
|
||||
|
||||
final stmt = stmtOut.load<Pointer<types.Statement>>();
|
||||
stmtOut.free();
|
||||
|
||||
if (resultCode != Errors.SQLITE_OK) {
|
||||
// we don't need to worry about freeing the statement. If preparing the
|
||||
// statement was unsuccessful, stmtOut.load() will be null
|
||||
throw SqliteException._fromErrorCode(_db, resultCode);
|
||||
}
|
||||
|
||||
return PreparedStatement._(stmt, this);
|
||||
}
|
||||
FutureOr<BasePreparedStatement> prepare(String sql);
|
||||
|
||||
/// Get the application defined version of this database.
|
||||
int get userVersion {
|
||||
final stmt = prepare('PRAGMA user_version');
|
||||
final result = stmt.select();
|
||||
stmt.close();
|
||||
|
||||
return result.first.columnAt(0) as int;
|
||||
}
|
||||
FutureOr<int> userVersion();
|
||||
|
||||
/// Update the application defined version of this database.
|
||||
set userVersion(int version) {
|
||||
execute('PRAGMA user_version = $version');
|
||||
}
|
||||
FutureOr<void> setUserVersion(int version);
|
||||
|
||||
/// Returns the amount of rows affected by the last INSERT, UPDATE or DELETE
|
||||
/// statement.
|
||||
int get updatedRows {
|
||||
_ensureOpen();
|
||||
return bindings.sqlite3_changes(_db);
|
||||
}
|
||||
FutureOr<int> getUpdatedRows();
|
||||
|
||||
/// Returns the row-id of the last inserted row.
|
||||
int get lastInsertId {
|
||||
_ensureOpen();
|
||||
return bindings.sqlite3_last_insert_rowid(_db);
|
||||
}
|
||||
FutureOr<int> getLastInsertId();
|
||||
}
|
||||
|
||||
/// A prepared statement that can be executed multiple times.
|
||||
abstract class BasePreparedStatement {
|
||||
/// Executes this prepared statement as a select statement. The returned rows
|
||||
/// will be returned.
|
||||
FutureOr<Result> select([List<dynamic> args]);
|
||||
|
||||
/// Executes this prepared statement.
|
||||
FutureOr<void> execute([List<dynamic> params]);
|
||||
|
||||
/// Closes this prepared statement and releases its resources.
|
||||
FutureOr<void> close();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
part of 'database.dart';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
/// Stores the result of a select statement.
|
||||
class Result extends Iterable<Row> {
|
||||
|
|
|
@ -94,7 +94,7 @@ class _SQLiteBindings {
|
|||
int Function(Pointer<Statement> statement, int columnIndex) sqlite3_bind_null;
|
||||
|
||||
_SQLiteBindings() {
|
||||
sqlite = moorSqliteOpener();
|
||||
sqlite = open.openSqlite();
|
||||
|
||||
sqlite3_bind_double = sqlite
|
||||
.lookup<NativeFunction<sqlite3_bind_double_native>>(
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:moor_ffi/database.dart';
|
||||
import 'package:moor_ffi/src/api/result.dart';
|
||||
import 'package:moor_ffi/src/bindings/constants.dart';
|
||||
import 'package:moor_ffi/src/bindings/types.dart' as types;
|
||||
import 'package:moor_ffi/src/bindings/bindings.dart';
|
||||
import 'package:moor_ffi/src/ffi/blob.dart';
|
||||
import 'package:moor_ffi/src/ffi/utils.dart';
|
||||
|
||||
part 'errors.dart';
|
||||
part 'prepared_statement.dart';
|
||||
|
||||
const _openingFlags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE;
|
||||
|
||||
class Database implements BaseDatabase {
|
||||
final Pointer<types.Database> _db;
|
||||
final List<PreparedStatement> _preparedStmt = [];
|
||||
bool _isClosed = false;
|
||||
|
||||
Database._(this._db);
|
||||
|
||||
/// Opens the [file] as a sqlite3 database. The file will be created if it
|
||||
/// doesn't exist.
|
||||
factory Database.openFile(File file) => Database.open(file.absolute.path);
|
||||
|
||||
/// Opens an in-memory sqlite3 database.
|
||||
factory Database.memory() => Database.open(':memory:');
|
||||
|
||||
/// Opens an sqlite3 database from a filename.
|
||||
factory Database.open(String fileName) {
|
||||
final dbOut = Pointer<Pointer<types.Database>>.allocate();
|
||||
final pathC = CBlob.allocateString(fileName);
|
||||
|
||||
final resultCode =
|
||||
bindings.sqlite3_open_v2(pathC, dbOut, _openingFlags, nullptr.cast());
|
||||
final dbPointer = dbOut.load<Pointer<types.Database>>();
|
||||
|
||||
dbOut.free();
|
||||
pathC.free();
|
||||
|
||||
if (resultCode == Errors.SQLITE_OK) {
|
||||
return Database._(dbPointer);
|
||||
} else {
|
||||
throw SqliteException._fromErrorCode(dbPointer, resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureOpen() {
|
||||
if (_isClosed) {
|
||||
throw Exception('This database has already been closed');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
final code = bindings.sqlite3_close_v2(_db);
|
||||
SqliteException exception;
|
||||
if (code != Errors.SQLITE_OK) {
|
||||
exception = SqliteException._fromErrorCode(_db, code);
|
||||
}
|
||||
_isClosed = true;
|
||||
|
||||
for (var stmt in _preparedStmt) {
|
||||
stmt.close();
|
||||
}
|
||||
_db.free();
|
||||
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStmtFinalized(PreparedStatement stmt) {
|
||||
if (!_isClosed) {
|
||||
_preparedStmt.remove(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void execute(String sql) {
|
||||
_ensureOpen();
|
||||
final sqlPtr = CBlob.allocateString(sql);
|
||||
final errorOut = Pointer<Pointer<CBlob>>.allocate();
|
||||
|
||||
final result =
|
||||
bindings.sqlite3_exec(_db, sqlPtr, nullptr, nullptr, errorOut);
|
||||
|
||||
sqlPtr.free();
|
||||
|
||||
final errorPtr = errorOut.load<Pointer<CBlob>>();
|
||||
errorOut.free();
|
||||
|
||||
String errorMsg;
|
||||
if (!isNullPointer(errorPtr)) {
|
||||
errorMsg = errorPtr.load<CBlob>().readString();
|
||||
// the message was allocated from sqlite, we need to free it
|
||||
bindings.sqlite3_free(errorPtr.cast());
|
||||
}
|
||||
|
||||
if (result != Errors.SQLITE_OK) {
|
||||
throw SqliteException(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
PreparedStatement prepare(String sql) {
|
||||
_ensureOpen();
|
||||
|
||||
final stmtOut = Pointer<Pointer<types.Statement>>.allocate();
|
||||
final sqlPtr = CBlob.allocateString(sql);
|
||||
|
||||
final resultCode =
|
||||
bindings.sqlite3_prepare_v2(_db, sqlPtr, -1, stmtOut, nullptr.cast());
|
||||
sqlPtr.free();
|
||||
|
||||
final stmt = stmtOut.load<Pointer<types.Statement>>();
|
||||
stmtOut.free();
|
||||
|
||||
if (resultCode != Errors.SQLITE_OK) {
|
||||
// we don't need to worry about freeing the statement. If preparing the
|
||||
// statement was unsuccessful, stmtOut.load() will be null
|
||||
throw SqliteException._fromErrorCode(_db, resultCode);
|
||||
}
|
||||
|
||||
return PreparedStatement._(stmt, this);
|
||||
}
|
||||
|
||||
@override
|
||||
int userVersion() {
|
||||
final stmt = prepare('PRAGMA user_version');
|
||||
final result = stmt.select();
|
||||
stmt.close();
|
||||
|
||||
return result.first.columnAt(0) as int;
|
||||
}
|
||||
|
||||
@override
|
||||
void setUserVersion(int version) {
|
||||
execute('PRAGMA user_version = $version');
|
||||
}
|
||||
|
||||
@override
|
||||
int getUpdatedRows() {
|
||||
_ensureOpen();
|
||||
return bindings.sqlite3_changes(_db);
|
||||
}
|
||||
|
||||
@override
|
||||
int getLastInsertId() {
|
||||
_ensureOpen();
|
||||
return bindings.sqlite3_last_insert_rowid(_db);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
part of 'database.dart';
|
||||
|
||||
class PreparedStatement {
|
||||
class PreparedStatement implements BasePreparedStatement {
|
||||
final Pointer<types.Statement> _stmt;
|
||||
final Database _db;
|
||||
bool _closed = false;
|
||||
|
@ -10,6 +10,7 @@ class PreparedStatement {
|
|||
|
||||
PreparedStatement._(this._stmt, this._db);
|
||||
|
||||
@override
|
||||
void close() {
|
||||
if (!_closed) {
|
||||
_reset();
|
||||
|
@ -25,8 +26,7 @@ class PreparedStatement {
|
|||
}
|
||||
}
|
||||
|
||||
/// Executes this prepared statement as a select statement. The returned rows
|
||||
/// will be returned.
|
||||
@override
|
||||
Result select([List<dynamic> params]) {
|
||||
_ensureNotFinalized();
|
||||
_reset();
|
||||
|
@ -75,7 +75,7 @@ class PreparedStatement {
|
|||
}
|
||||
}
|
||||
|
||||
/// Executes this prepared statement.
|
||||
@override
|
||||
void execute([List<dynamic> params]) {
|
||||
_ensureNotFinalized();
|
||||
_reset();
|
|
@ -4,11 +4,19 @@ part of 'package:moor_ffi/moor_ffi.dart';
|
|||
/// use.
|
||||
typedef OpenLibrary = DynamicLibrary Function();
|
||||
|
||||
/// The [OpenLibrary] function that will be used for the first time the native
|
||||
/// library is requested. This can be overridden, but won't have an effect after
|
||||
/// the library has been opened once (which happens when a `VmDatabase` is
|
||||
/// instantiated).
|
||||
OpenLibrary moorSqliteOpener = _defaultOpen;
|
||||
enum OperatingSystem {
|
||||
android,
|
||||
linux,
|
||||
iOS,
|
||||
macOS,
|
||||
windows,
|
||||
fuchsia,
|
||||
}
|
||||
|
||||
/// The instance managing different approaches to load the [DynamicLibrary] for
|
||||
/// sqlite when needed. See the documentation for [OpenDynamicLibrary] to learn
|
||||
/// how the default opening behavior can be overridden.
|
||||
final OpenDynamicLibrary open = OpenDynamicLibrary._();
|
||||
|
||||
DynamicLibrary _defaultOpen() {
|
||||
if (Platform.isLinux || Platform.isAndroid) {
|
||||
|
@ -26,3 +34,64 @@ DynamicLibrary _defaultOpen() {
|
|||
throw UnsupportedError(
|
||||
'moor_ffi does not support ${Platform.operatingSystem} yet');
|
||||
}
|
||||
|
||||
/// Manages functions that define how to load the [DynamicLibrary] for sqlite.
|
||||
///
|
||||
/// The default behavior will use `DynamicLibrary.open('libsqlite3.so')` on
|
||||
/// Linux and Android, `DynamicLibrary.open('libsqlite3.dylib')` on iOS and
|
||||
/// macOS and `DynamicLibrary.open('sqlite3.dll')` on Windows.
|
||||
///
|
||||
/// The default behavior can be overridden for a specific OS by using
|
||||
/// [overrideFor]. To override the behavior on all platforms, use
|
||||
/// [overrideForAll].
|
||||
class OpenDynamicLibrary {
|
||||
final Map<OperatingSystem, OpenLibrary> _overriddenPlatforms = {};
|
||||
OpenLibrary _overriddenForAll;
|
||||
|
||||
OpenDynamicLibrary._();
|
||||
|
||||
/// Returns the current [OperatingSystem] as read from the [Platform] getters.
|
||||
OperatingSystem get os {
|
||||
if (Platform.isAndroid) return OperatingSystem.android;
|
||||
if (Platform.isLinux) return OperatingSystem.linux;
|
||||
if (Platform.isIOS) return OperatingSystem.iOS;
|
||||
if (Platform.isMacOS) return OperatingSystem.macOS;
|
||||
if (Platform.isWindows) return OperatingSystem.windows;
|
||||
if (Platform.isFuchsia) return OperatingSystem.fuchsia;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Opens the [DynamicLibrary] from which `moor_ffi` is going to
|
||||
/// [DynamicLibrary.lookup] sqlite's methods that will be used. This method is
|
||||
/// meant to be called by `moor_ffi` only.
|
||||
DynamicLibrary openSqlite() {
|
||||
if (_overriddenForAll != null) {
|
||||
return _overriddenForAll();
|
||||
}
|
||||
|
||||
final forPlatform = _overriddenPlatforms[os];
|
||||
if (forPlatform != null) {
|
||||
return forPlatform();
|
||||
}
|
||||
|
||||
return _defaultOpen();
|
||||
}
|
||||
|
||||
/// Makes `moor_ffi` use the [open] function when running on the specified
|
||||
/// [os]. This can be used to override the loading behavior on some platforms.
|
||||
/// To override that behavior on all platforms, consider using
|
||||
/// [overrideForAll].
|
||||
/// This method must be called before opening any database.
|
||||
///
|
||||
/// When using the asynchronous API over isolates, [open] __must be__ a top-
|
||||
/// level function or a static method.
|
||||
void overrideFor(OperatingSystem os, OpenLibrary open) {}
|
||||
|
||||
/// Makes `moor_ffi` use the [OpenLibrary] function for all Dart platforms.
|
||||
/// If this method has been called, it takes precedence over [overrideFor].
|
||||
/// This method must be called before opening any database.
|
||||
///
|
||||
/// When using the asynchronous API over isolates, [open] __must be__ a top-
|
||||
/// level function or a static method.
|
||||
void overrideForAll(OpenLibrary open) {}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class VmDatabase extends DelegatedDatabase {
|
|||
}
|
||||
|
||||
class _VmDelegate extends DatabaseDelegate {
|
||||
Database _db;
|
||||
BaseDatabase _db;
|
||||
|
||||
final File file;
|
||||
|
||||
|
@ -45,12 +45,12 @@ class _VmDelegate extends DatabaseDelegate {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(List<BatchedStatement> statements) {
|
||||
Future<void> runBatched(List<BatchedStatement> statements) async {
|
||||
for (var stmt in statements) {
|
||||
final prepared = _db.prepare(stmt.sql);
|
||||
final prepared = await _db.prepare(stmt.sql);
|
||||
|
||||
for (var boundVars in stmt.variables) {
|
||||
prepared.execute(boundVars);
|
||||
await prepared.execute(boundVars);
|
||||
}
|
||||
|
||||
prepared.close();
|
||||
|
@ -59,55 +59,54 @@ class _VmDelegate extends DatabaseDelegate {
|
|||
return Future.value();
|
||||
}
|
||||
|
||||
void _runWithArgs(String statement, List<dynamic> args) {
|
||||
Future _runWithArgs(String statement, List<dynamic> args) async {
|
||||
if (args.isEmpty) {
|
||||
_db.execute(statement);
|
||||
await _db.execute(statement);
|
||||
} else {
|
||||
_db.prepare(statement)
|
||||
..execute(args)
|
||||
..close();
|
||||
final stmt = await _db.prepare(statement);
|
||||
await stmt.execute(args);
|
||||
await stmt.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, List args) {
|
||||
_runWithArgs(statement, args);
|
||||
return Future.value();
|
||||
Future<void> runCustom(String statement, List args) async {
|
||||
await _runWithArgs(statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List args) {
|
||||
_runWithArgs(statement, args);
|
||||
return Future.value(_db.lastInsertId);
|
||||
Future<int> runInsert(String statement, List args) async {
|
||||
await _runWithArgs(statement, args);
|
||||
return await _db.getLastInsertId();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List args) {
|
||||
_runWithArgs(statement, args);
|
||||
return Future.value(_db.updatedRows);
|
||||
Future<int> runUpdate(String statement, List args) async {
|
||||
await _runWithArgs(statement, args);
|
||||
return await _db.getUpdatedRows();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<QueryResult> runSelect(String statement, List args) {
|
||||
final stmt = _db.prepare(statement);
|
||||
final result = stmt.select(args);
|
||||
stmt.close();
|
||||
Future<QueryResult> runSelect(String statement, List args) async {
|
||||
final stmt = await _db.prepare(statement);
|
||||
final result = await stmt.select(args);
|
||||
await stmt.close();
|
||||
|
||||
return Future.value(QueryResult(result.columnNames, result.rows));
|
||||
}
|
||||
}
|
||||
|
||||
class _VmVersionDelegate extends DynamicVersionDelegate {
|
||||
final Database database;
|
||||
final BaseDatabase database;
|
||||
|
||||
_VmVersionDelegate(this.database);
|
||||
|
||||
@override
|
||||
Future<int> get schemaVersion => Future.value(database.userVersion);
|
||||
Future<int> get schemaVersion => Future.value(database.userVersion());
|
||||
|
||||
@override
|
||||
Future<void> setSchemaVersion(int version) {
|
||||
database.userVersion = version;
|
||||
Future<void> setSchemaVersion(int version) async {
|
||||
await database.setUserVersion(version);
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue