diff --git a/moor_ffi/lib/database.dart b/moor_ffi/lib/database.dart new file mode 100644 index 00000000..9b6157eb --- /dev/null +++ b/moor_ffi/lib/database.dart @@ -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; diff --git a/moor_ffi/lib/moor_ffi.dart b/moor_ffi/lib/moor_ffi.dart index 632b3f81..7954e761 100644 --- a/moor_ffi/lib/moor_ffi.dart +++ b/moor_ffi/lib/moor_ffi.dart @@ -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'; diff --git a/moor_ffi/lib/src/api/database.dart b/moor_ffi/lib/src/api/database.dart index 96633e2a..bd54c1a1 100644 --- a/moor_ffi/lib/src/api/database.dart +++ b/moor_ffi/lib/src/api/database.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 _db; - final List _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>.allocate(); - final pathC = CBlob.allocateString(fileName); - - final resultCode = - bindings.sqlite3_open_v2(pathC, dbOut, _openingFlags, nullptr.cast()); - final dbPointer = dbOut.load>(); - - 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 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>.allocate(); - - final result = - bindings.sqlite3_exec(_db, sqlPtr, nullptr, nullptr, errorOut); - - sqlPtr.free(); - - final errorPtr = errorOut.load>(); - errorOut.free(); - - String errorMsg; - if (!isNullPointer(errorPtr)) { - errorMsg = errorPtr.load().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 execute(String sql); /// Prepares the [sql] statement. - PreparedStatement prepare(String sql) { - _ensureOpen(); - - final stmtOut = Pointer>.allocate(); - final sqlPtr = CBlob.allocateString(sql); - - final resultCode = - bindings.sqlite3_prepare_v2(_db, sqlPtr, -1, stmtOut, nullptr.cast()); - sqlPtr.free(); - - final stmt = stmtOut.load>(); - 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 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 userVersion(); /// Update the application defined version of this database. - set userVersion(int version) { - execute('PRAGMA user_version = $version'); - } + FutureOr 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 getUpdatedRows(); /// Returns the row-id of the last inserted row. - int get lastInsertId { - _ensureOpen(); - return bindings.sqlite3_last_insert_rowid(_db); - } + FutureOr 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 select([List args]); + + /// Executes this prepared statement. + FutureOr execute([List params]); + + /// Closes this prepared statement and releases its resources. + FutureOr close(); } diff --git a/moor_ffi/lib/src/api/result.dart b/moor_ffi/lib/src/api/result.dart index 3204124a..9c90cd2d 100644 --- a/moor_ffi/lib/src/api/result.dart +++ b/moor_ffi/lib/src/api/result.dart @@ -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 { diff --git a/moor_ffi/lib/src/bindings/bindings.dart b/moor_ffi/lib/src/bindings/bindings.dart index 7008699d..4b397358 100644 --- a/moor_ffi/lib/src/bindings/bindings.dart +++ b/moor_ffi/lib/src/bindings/bindings.dart @@ -94,7 +94,7 @@ class _SQLiteBindings { int Function(Pointer statement, int columnIndex) sqlite3_bind_null; _SQLiteBindings() { - sqlite = moorSqliteOpener(); + sqlite = open.openSqlite(); sqlite3_bind_double = sqlite .lookup>( diff --git a/moor_ffi/lib/src/impl/database.dart b/moor_ffi/lib/src/impl/database.dart new file mode 100644 index 00000000..1878013b --- /dev/null +++ b/moor_ffi/lib/src/impl/database.dart @@ -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 _db; + final List _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>.allocate(); + final pathC = CBlob.allocateString(fileName); + + final resultCode = + bindings.sqlite3_open_v2(pathC, dbOut, _openingFlags, nullptr.cast()); + final dbPointer = dbOut.load>(); + + 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>.allocate(); + + final result = + bindings.sqlite3_exec(_db, sqlPtr, nullptr, nullptr, errorOut); + + sqlPtr.free(); + + final errorPtr = errorOut.load>(); + errorOut.free(); + + String errorMsg; + if (!isNullPointer(errorPtr)) { + errorMsg = errorPtr.load().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>.allocate(); + final sqlPtr = CBlob.allocateString(sql); + + final resultCode = + bindings.sqlite3_prepare_v2(_db, sqlPtr, -1, stmtOut, nullptr.cast()); + sqlPtr.free(); + + final stmt = stmtOut.load>(); + 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); + } +} diff --git a/moor_ffi/lib/src/api/errors.dart b/moor_ffi/lib/src/impl/errors.dart similarity index 100% rename from moor_ffi/lib/src/api/errors.dart rename to moor_ffi/lib/src/impl/errors.dart diff --git a/moor_ffi/lib/src/api/prepared_statement.dart b/moor_ffi/lib/src/impl/prepared_statement.dart similarity index 95% rename from moor_ffi/lib/src/api/prepared_statement.dart rename to moor_ffi/lib/src/impl/prepared_statement.dart index 5b06d476..01ff6ffa 100644 --- a/moor_ffi/lib/src/api/prepared_statement.dart +++ b/moor_ffi/lib/src/impl/prepared_statement.dart @@ -1,6 +1,6 @@ part of 'database.dart'; -class PreparedStatement { +class PreparedStatement implements BasePreparedStatement { final Pointer _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 params]) { _ensureNotFinalized(); _reset(); @@ -75,7 +75,7 @@ class PreparedStatement { } } - /// Executes this prepared statement. + @override void execute([List params]) { _ensureNotFinalized(); _reset(); diff --git a/moor_ffi/lib/src/load_library.dart b/moor_ffi/lib/src/load_library.dart index c3bdaea4..db3108e0 100644 --- a/moor_ffi/lib/src/load_library.dart +++ b/moor_ffi/lib/src/load_library.dart @@ -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 _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) {} +} diff --git a/moor_ffi/lib/src/vm_database.dart b/moor_ffi/lib/src/vm_database.dart index c73a8a5e..4699c898 100644 --- a/moor_ffi/lib/src/vm_database.dart +++ b/moor_ffi/lib/src/vm_database.dart @@ -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 runBatched(List statements) { + Future runBatched(List 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 args) { + Future _runWithArgs(String statement, List 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 runCustom(String statement, List args) { - _runWithArgs(statement, args); - return Future.value(); + Future runCustom(String statement, List args) async { + await _runWithArgs(statement, args); } @override - Future runInsert(String statement, List args) { - _runWithArgs(statement, args); - return Future.value(_db.lastInsertId); + Future runInsert(String statement, List args) async { + await _runWithArgs(statement, args); + return await _db.getLastInsertId(); } @override - Future runUpdate(String statement, List args) { - _runWithArgs(statement, args); - return Future.value(_db.updatedRows); + Future runUpdate(String statement, List args) async { + await _runWithArgs(statement, args); + return await _db.getUpdatedRows(); } @override - Future runSelect(String statement, List args) { - final stmt = _db.prepare(statement); - final result = stmt.select(args); - stmt.close(); + Future 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 get schemaVersion => Future.value(database.userVersion); + Future get schemaVersion => Future.value(database.userVersion()); @override - Future setSchemaVersion(int version) { - database.userVersion = version; + Future setSchemaVersion(int version) async { + await database.setUserVersion(version); return Future.value(); } }