FFI: Ability to override loading behavior, async api

This commit is contained in:
Simon Binder 2019-09-22 11:10:32 +02:00
parent 22dee72680
commit d6913af380
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
10 changed files with 294 additions and 179 deletions

View File

@ -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;

View File

@ -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';

View File

@ -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();
}

View File

@ -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> {

View File

@ -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>>(

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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) {}
}

View File

@ -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();
}
}