diff --git a/packages/drift_sqlite_async/lib/src/executor.dart b/packages/drift_sqlite_async/lib/src/executor.dart index 127462e..711c886 100644 --- a/packages/drift_sqlite_async/lib/src/executor.dart +++ b/packages/drift_sqlite_async/lib/src/executor.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:drift/backends.dart'; import 'package:drift/drift.dart'; -import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite3/common.dart'; import 'package:sqlite_async/sqlite_async.dart'; // Ends with " RETURNING *", or starts with insert/update/delete. diff --git a/packages/drift_sqlite_async/pubspec.yaml b/packages/drift_sqlite_async/pubspec.yaml index c78b97f..ee10a66 100644 --- a/packages/drift_sqlite_async/pubspec.yaml +++ b/packages/drift_sqlite_async/pubspec.yaml @@ -17,6 +17,7 @@ environment: dependencies: drift: ">=2.28.0 <3.0.0" + sqlite3: ^3.2.0 sqlite_async: ^0.14.0-wip dev_dependencies: @@ -24,7 +25,6 @@ dev_dependencies: drift_dev: ">=2.28.0 <3.0.0" glob: ^2.1.2 lints: ^6.0.0 - sqlite3: ^3.2.0 test: ^1.25.2 test_api: ^0.7.0 diff --git a/packages/drift_sqlite_async/test/utils/test_utils.dart b/packages/drift_sqlite_async/test/utils/test_utils.dart index e2fc447..3cd2dd4 100644 --- a/packages/drift_sqlite_async/test/utils/test_utils.dart +++ b/packages/drift_sqlite_async/test/utils/test_utils.dart @@ -1,31 +1,11 @@ import 'dart:io'; -import 'package:glob/glob.dart'; -import 'package:glob/list_local_fs.dart'; -import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test_api/src/backend/invoker.dart'; -class TestSqliteOpenFactory extends DefaultSqliteOpenFactory { - TestSqliteOpenFactory({ - required super.path, - super.sqliteOptions, - }); - - @override - CommonDatabase open(SqliteOpenOptions options) { - final db = super.open(options); - - return db; - } -} - -DefaultSqliteOpenFactory testFactory({String? path}) { - return TestSqliteOpenFactory(path: path ?? dbPath()); -} - Future setupDatabase({String? path}) async { - final db = SqliteDatabase.withFactory(testFactory(path: path)); + final db = + SqliteDatabase.withFactory(SqliteOpenFactory(path: path ?? dbPath())); await db.initialize(); return db; } @@ -48,15 +28,6 @@ Future cleanDb({required String path}) async { } } -List findSqliteLibraries() { - var glob = Glob('sqlite-*/.libs/libsqlite3.so'); - List sqlites = [ - 'libsqlite3.so.0', - for (var sqlite in glob.listSync()) sqlite.path - ]; - return sqlites; -} - String dbPath() { final test = Invoker.current!.liveTest; var testName = test.test.name; diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index e532bfd..a1709c8 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -5,6 +5,16 @@ - __Breaking__: Rewrite the native connection pool implementation. - Remove isolate connection factories. Simply open the same database on another isolate, it's safe to do so now. - It is no longer possible to register user-defined functions in Dart. Extensions providing functions in native code can still be used. +- __Breaking__: Remove `AbstractDefaultSqliteOpenFactory` and `DefaultSqliteOpenFactory`. Use `SqliteOpenFactory` instead. + To provide a custom open factory, import `NativeSqliteOpenFactory` or `WebSqliteOpenFactory` with a platform-specific + import and extend those classes. +- __Breaking__: Remove the `maxReaders` parameter on `SqliteDatabase`. Set that parameter on `SqliteOptions` instead. +- __Breaking__: Remove libraries exporting the `sqlite3` package: + - Instead of `package:sqlite_async/sqlite3.dart`, import `package:sqlite3/sqlite3.dart`. + - Instead of `package:sqlite_async/sqlite3_common.dart`, import `package:sqlite3/common.dart`. + - Instead of `package:sqlite_async/sqlite3_wasm.dart`, import `package:sqlite3/wasm.dart`. + - Instead of `package:sqlite_async/sqlite3_web.dart`, import `package:sqlite3_web/sqlite3_web.dart`. + ## 0.13.1 diff --git a/packages/sqlite_async/example/web/worker.dart b/packages/sqlite_async/example/web/worker.dart index 481455d..9dfeff4 100644 --- a/packages/sqlite_async/example/web/worker.dart +++ b/packages/sqlite_async/example/web/worker.dart @@ -1,4 +1,4 @@ -import 'package:sqlite_async/sqlite3_web.dart'; +import 'package:sqlite3_web/sqlite3_web.dart'; import 'package:sqlite_async/sqlite3_web_worker.dart'; void main() { diff --git a/packages/sqlite_async/lib/native.dart b/packages/sqlite_async/lib/native.dart new file mode 100644 index 0000000..89dbb1e --- /dev/null +++ b/packages/sqlite_async/lib/native.dart @@ -0,0 +1 @@ +export 'src/native/native_sqlite_open_factory.dart'; diff --git a/packages/sqlite_async/lib/sqlite3.dart b/packages/sqlite_async/lib/sqlite3.dart deleted file mode 100644 index b62b481..0000000 --- a/packages/sqlite_async/lib/sqlite3.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// Re-exports [sqlite3](https://pub.dev/packages/sqlite3) to expose sqlite3 without -/// adding it as a direct dependency. -library; - -export 'package:sqlite3/sqlite3.dart'; diff --git a/packages/sqlite_async/lib/sqlite3_common.dart b/packages/sqlite_async/lib/sqlite3_common.dart deleted file mode 100644 index eae3c6f..0000000 --- a/packages/sqlite_async/lib/sqlite3_common.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Exports common Sqlite3 exports which are available in different environments. -export 'package:sqlite3/common.dart'; diff --git a/packages/sqlite_async/lib/sqlite3_wasm.dart b/packages/sqlite_async/lib/sqlite3_wasm.dart deleted file mode 100644 index 0c30793..0000000 --- a/packages/sqlite_async/lib/sqlite3_wasm.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// Re-exports [sqlite3 WASM](https://pub.dev/packages/sqlite3) to expose sqlite3 without -/// adding it as a direct dependency. -library; - -export 'package:sqlite3/wasm.dart'; diff --git a/packages/sqlite_async/lib/sqlite3_web.dart b/packages/sqlite_async/lib/sqlite3_web.dart deleted file mode 100644 index b08d46e..0000000 --- a/packages/sqlite_async/lib/sqlite3_web.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// Re-exports [sqlite3_web](https://pub.dev/packages/sqlite3_web) to expose sqlite3_web without -/// adding it as a direct dependency. -library; - -export 'package:sqlite3_web/sqlite3_web.dart'; diff --git a/packages/sqlite_async/lib/sqlite_async.dart b/packages/sqlite_async/lib/sqlite_async.dart index 1b250c8..c6668c9 100644 --- a/packages/sqlite_async/lib/sqlite_async.dart +++ b/packages/sqlite_async/lib/sqlite_async.dart @@ -3,14 +3,12 @@ /// See [SqliteDatabase] as a starting point. library; -export 'src/common/abstract_open_factory.dart'; +export 'src/common/abstract_open_factory.dart' hide InternalOpenFactory; export 'src/common/connection/sync_sqlite_connection.dart'; export 'src/common/mutex.dart'; -export 'src/common/sqlite_database.dart'; +export 'src/common/sqlite_database.dart' hide SqliteDatabaseImpl; export 'src/sqlite_connection.dart'; -export 'src/sqlite_database.dart'; export 'src/sqlite_migrations.dart'; -export 'src/sqlite_open_factory.dart'; export 'src/sqlite_options.dart'; export 'src/sqlite_queries.dart'; export 'src/update_notification.dart'; diff --git a/packages/sqlite_async/lib/src/common/abstract_open_factory.dart b/packages/sqlite_async/lib/src/common/abstract_open_factory.dart index 61cc8da..4b1349c 100644 --- a/packages/sqlite_async/lib/src/common/abstract_open_factory.dart +++ b/packages/sqlite_async/lib/src/common/abstract_open_factory.dart @@ -1,48 +1,80 @@ -import 'dart:async'; +/// @docImport '../native/native_sqlite_open_factory.dart'; +/// @docImport '../web/web_sqlite_open_factory.dart'; +library; + import 'package:meta/meta.dart'; +import 'package:sqlite3/common.dart' as sqlite; + +import '../impl/platform.dart' as platform; -import 'package:sqlite_async/sqlite3_common.dart' as sqlite; -import 'package:sqlite_async/src/common/mutex.dart'; -import 'package:sqlite_async/src/sqlite_connection.dart'; -import 'package:sqlite_async/src/sqlite_options.dart'; -import 'package:sqlite_async/src/update_notification.dart'; +import '../sqlite_options.dart'; /// Factory to create new SQLite database connections. /// /// Since connections are opened in dedicated background isolates, this class /// must be safe to pass to different isolates. -abstract class SqliteOpenFactory { - String get path; +/// +/// How databases are opened is platform specific. For this reason, this class +/// can't be extended directly. To customize how databases are opened across +/// platforms, prefer using [SqliteOptions]. If that class doesn't provide the +/// degree of customization you need, you can also subclass platform-specific +/// connection factory implementations: +/// +/// - On native platforms, extend [NativeSqliteOpenFactory]. +/// - When compiling for the web, extend [WebSqliteOpenFactory]. +sealed class SqliteOpenFactory { + final String path; + final SqliteOptions sqliteOptions; + + SqliteOpenFactory._({ + required this.path, + this.sqliteOptions = const SqliteOptions(), + }); + + /// Creates a default open factory opening databases at the given path with + /// specified options. + /// + /// This will return a [NativeSqliteOpenFactory] on native platforms and a + /// [WebSqliteOpenFactory] when compiling for the web. + factory SqliteOpenFactory({ + required String path, + SqliteOptions options = const SqliteOptions(), + }) { + return platform.createDefaultOpenFactory(path, options); + } - /// Opens a direct connection to the SQLite database - Database open(SqliteOpenOptions options); + /// Pragma statements to run on newly opened connections to configure them. + /// + /// On native platforms, these configure WAL mode for instance. This can also + /// be useed to configure an encryption key if SQLite3MultipleCiphers is used. + List pragmaStatements(SqliteOpenOptions options); +} - /// Opens an asynchronous [SqliteConnection] - FutureOr openConnection(SqliteOpenOptions options); +/// The superclass for all connection factories. +/// +/// By keeping this class internal, we can safely assert that all connection +/// factories on native and web platforms extend [NativeSqliteOpenFactory] and +/// [WebSqliteOpenFactory], respectively. +@internal +abstract base class InternalOpenFactory extends SqliteOpenFactory { + InternalOpenFactory({required super.path, super.sqliteOptions}) : super._(); } -class SqliteOpenOptions { +final class SqliteOpenOptions { /// Whether this is the primary write connection for the database. final bool primaryConnection; /// Whether this connection is read-only. final bool readOnly; - /// Mutex to use in [SqliteConnection]s - final Mutex? mutex; - /// Name used in debug logs final String? debugName; - /// Stream of external update notifications - final Stream? updates; - - const SqliteOpenOptions( - {required this.primaryConnection, - required this.readOnly, - this.mutex, - this.debugName, - this.updates}); + const SqliteOpenOptions({ + required this.primaryConnection, + required this.readOnly, + this.debugName, + }); sqlite.OpenMode get openMode { if (primaryConnection) { @@ -54,60 +86,3 @@ class SqliteOpenOptions { } } } - -/// The default database factory. -/// -/// This takes care of opening the database, and running PRAGMA statements -/// to configure the connection. -/// -/// Override the [open] method to customize the process. -abstract class AbstractDefaultSqliteOpenFactory< - Database extends sqlite.CommonDatabase> - implements SqliteOpenFactory { - @override - final String path; - final SqliteOptions sqliteOptions; - - const AbstractDefaultSqliteOpenFactory( - {required this.path, - this.sqliteOptions = const SqliteOptions.defaults()}); - - List pragmaStatements(SqliteOpenOptions options); - - @protected - - /// Opens a direct connection to a SQLite database connection - Database openDB(SqliteOpenOptions options); - - @override - - /// Opens a direct connection to a SQLite database connection - /// and executes setup pragma statements to initialize the DB - Database open(SqliteOpenOptions options) { - var db = openDB(options); - - // Pragma statements don't have the same BUSY_TIMEOUT behavior as normal statements. - // We add a manual retry loop for those. - for (var statement in pragmaStatements(options)) { - for (var tries = 0; tries < 30; tries++) { - try { - db.execute(statement); - break; - } on sqlite.SqliteException catch (e) { - if (e.resultCode == sqlite.SqlError.SQLITE_BUSY && tries < 29) { - continue; - } else { - rethrow; - } - } - } - } - return db; - } - - @override - - /// Opens an asynchronous [SqliteConnection] to a SQLite database - /// and executes setup pragma statements to initialize the DB - FutureOr openConnection(SqliteOpenOptions options); -} diff --git a/packages/sqlite_async/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart index b8a5f17..93bde03 100644 --- a/packages/sqlite_async/lib/src/common/sqlite_database.dart +++ b/packages/sqlite_async/lib/src/common/sqlite_database.dart @@ -3,12 +3,13 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:sqlite_async/src/common/abstract_open_factory.dart'; import 'package:sqlite_async/src/impl/single_connection_database.dart'; -import 'package:sqlite_async/src/impl/sqlite_database_impl.dart'; import 'package:sqlite_async/src/sqlite_options.dart'; import 'package:sqlite_async/src/sqlite_queries.dart'; import 'package:sqlite_async/src/update_notification.dart'; import 'package:sqlite_async/src/sqlite_connection.dart'; +import '../impl/platform.dart' as platform; + mixin SqliteDatabaseMixin implements SqliteConnection, SqliteQueries { /// Maximum number of concurrent read transactions. int get maxReaders; @@ -18,7 +19,7 @@ mixin SqliteDatabaseMixin implements SqliteConnection, SqliteQueries { /// This must be safe to pass to different isolates. /// /// Use a custom class for this to customize the open process. - AbstractDefaultSqliteOpenFactory get openFactory; + SqliteOpenFactory get openFactory; /// Use this stream to subscribe to notifications of updates to tables. @override @@ -47,11 +48,10 @@ mixin SqliteDatabaseMixin implements SqliteConnection, SqliteQueries { /// /// Use one instance per database file. If multiple instances are used, update /// notifications may not trigger, and calls may fail with "SQLITE_BUSY" errors. -abstract class SqliteDatabase +abstract base class SqliteDatabase with SqliteQueries, SqliteDatabaseMixin implements SqliteConnection { - /// The maximum number of concurrent read transactions if not explicitly specified. - static const int defaultMaxReaders = 5; + SqliteDatabase._(); /// Open a SqliteDatabase. /// @@ -61,14 +61,11 @@ abstract class SqliteDatabase /// transactions, and a single concurrent write transaction. Write transactions /// do not block read transactions, and read transactions will see the state /// from the last committed write transaction. - /// - /// A maximum of [maxReaders] concurrent read transactions are allowed. factory SqliteDatabase( - {required String path, - int maxReaders = SqliteDatabase.defaultMaxReaders, - SqliteOptions options = const SqliteOptions.defaults()}) { - return SqliteDatabaseImpl( - path: path, maxReaders: maxReaders, options: options); + {required String path, SqliteOptions options = const SqliteOptions()}) { + return SqliteDatabase.withFactory( + SqliteOpenFactory(path: path, options: options), + ); } /// Advanced: Open a database with a specified factory. @@ -80,10 +77,8 @@ abstract class SqliteDatabase /// 2. Running additional per-connection PRAGMA statements on each connection. /// 3. Creating custom SQLite functions. /// 4. Creating temporary views or triggers. - factory SqliteDatabase.withFactory( - AbstractDefaultSqliteOpenFactory openFactory, - {int maxReaders = SqliteDatabase.defaultMaxReaders}) { - return SqliteDatabaseImpl.withFactory(openFactory, maxReaders: maxReaders); + factory SqliteDatabase.withFactory(SqliteOpenFactory openFactory) { + return platform.openDatabaseWithFactory(openFactory); } /// Opens a [SqliteDatabase] that only wraps an underlying connection. @@ -106,3 +101,9 @@ abstract class SqliteDatabase return SingleConnectionDatabase(connection); } } + +/// Internal superclass for all [SqliteDatabase] implementations. +@internal +abstract base class SqliteDatabaseImpl extends SqliteDatabase { + SqliteDatabaseImpl() : super._(); +} diff --git a/packages/sqlite_async/lib/src/impl/open_factory_impl.dart b/packages/sqlite_async/lib/src/impl/open_factory_impl.dart deleted file mode 100644 index a7a27ac..0000000 --- a/packages/sqlite_async/lib/src/impl/open_factory_impl.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'stub_sqlite_open_factory.dart' - // ignore: uri_does_not_exist - if (dart.library.io) '../native/native_sqlite_open_factory.dart' - // ignore: uri_does_not_exist - if (dart.library.js_interop) '../web/web_sqlite_open_factory.dart'; diff --git a/packages/sqlite_async/lib/src/impl/platform.dart b/packages/sqlite_async/lib/src/impl/platform.dart new file mode 100644 index 0000000..befbab5 --- /dev/null +++ b/packages/sqlite_async/lib/src/impl/platform.dart @@ -0,0 +1,3 @@ +export 'platform_stub.dart' + if (dart.library.js_interop) 'platform_web.dart' + if (dart.library.io) 'platform_native.dart'; diff --git a/packages/sqlite_async/lib/src/impl/platform_native.dart b/packages/sqlite_async/lib/src/impl/platform_native.dart new file mode 100644 index 0000000..f7b5977 --- /dev/null +++ b/packages/sqlite_async/lib/src/impl/platform_native.dart @@ -0,0 +1,15 @@ +import '../common/abstract_open_factory.dart'; +import '../native/database/native_sqlite_database.dart'; +import '../native/native_sqlite_open_factory.dart'; +import '../sqlite_options.dart'; + +NativeSqliteOpenFactory createDefaultOpenFactory( + String path, SqliteOptions options) { + return NativeSqliteOpenFactory(path: path, sqliteOptions: options); +} + +NativeSqliteDatabaseImpl openDatabaseWithFactory(SqliteOpenFactory factory) { + // It's safe to cast here, SqliteOpenFactory can only be implemented by + // NativeSqliteOpenFactory on native platforms (the class is sealed). + return NativeSqliteDatabaseImpl(factory as NativeSqliteOpenFactory); +} diff --git a/packages/sqlite_async/lib/src/impl/platform_stub.dart b/packages/sqlite_async/lib/src/impl/platform_stub.dart new file mode 100644 index 0000000..5b186a5 --- /dev/null +++ b/packages/sqlite_async/lib/src/impl/platform_stub.dart @@ -0,0 +1,11 @@ +import '../common/abstract_open_factory.dart'; +import '../common/sqlite_database.dart'; +import '../sqlite_options.dart'; + +SqliteOpenFactory createDefaultOpenFactory(String path, SqliteOptions options) { + throw UnsupportedError('Unsupported platform for sqlite_async package.'); +} + +SqliteDatabase openDatabaseWithFactory(SqliteOpenFactory factory) { + throw UnsupportedError('Unsupported platform for sqlite_async package.'); +} diff --git a/packages/sqlite_async/lib/src/impl/platform_web.dart b/packages/sqlite_async/lib/src/impl/platform_web.dart new file mode 100644 index 0000000..2062640 --- /dev/null +++ b/packages/sqlite_async/lib/src/impl/platform_web.dart @@ -0,0 +1,15 @@ +import '../common/abstract_open_factory.dart'; +import '../sqlite_options.dart'; +import '../web/database/async_web_database.dart'; +import '../web/web_sqlite_open_factory.dart'; + +WebSqliteOpenFactory createDefaultOpenFactory( + String path, SqliteOptions options) { + return WebSqliteOpenFactory(path: path, sqliteOptions: options); +} + +AsyncWebDatabaseImpl openDatabaseWithFactory(SqliteOpenFactory factory) { + // It's safe to cast here, SqliteOpenFactory can only be implemented by + // WebSqliteOpenFactory when compiling to the web (the class is sealed). + return AsyncWebDatabaseImpl(factory as WebSqliteOpenFactory); +} diff --git a/packages/sqlite_async/lib/src/impl/single_connection_database.dart b/packages/sqlite_async/lib/src/impl/single_connection_database.dart index 4f4294a..a794e13 100644 --- a/packages/sqlite_async/lib/src/impl/single_connection_database.dart +++ b/packages/sqlite_async/lib/src/impl/single_connection_database.dart @@ -1,15 +1,14 @@ -import 'package:sqlite3/common.dart'; import 'package:sqlite_async/sqlite_async.dart'; +import '../common/sqlite_database.dart'; + /// A database implementation that delegates everything to a single connection. /// /// This doesn't provide an automatic connection pool or the web worker /// management, but it can still be useful in cases like unit tests where those /// features might not be necessary. Since only a single sqlite connection is /// used internally, this also allows using in-memory databases. -final class SingleConnectionDatabase - with SqliteQueries, SqliteDatabaseMixin - implements SqliteDatabase { +final class SingleConnectionDatabase extends SqliteDatabaseImpl { final SqliteConnection connection; SingleConnectionDatabase(this.connection); @@ -30,8 +29,7 @@ final class SingleConnectionDatabase int get maxReaders => 1; @override - AbstractDefaultSqliteOpenFactory get openFactory => - throw UnimplementedError(); + SqliteOpenFactory get openFactory => throw UnimplementedError(); @override Future readLock(Future Function(SqliteReadContext tx) callback, diff --git a/packages/sqlite_async/lib/src/impl/sqlite_database_impl.dart b/packages/sqlite_async/lib/src/impl/sqlite_database_impl.dart deleted file mode 100644 index cf1dfbb..0000000 --- a/packages/sqlite_async/lib/src/impl/sqlite_database_impl.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'stub_sqlite_database.dart' - // ignore: uri_does_not_exist - if (dart.library.io) '../native/database/native_sqlite_database.dart' - // ignore: uri_does_not_exist - if (dart.library.js_interop) '../web/database/web_sqlite_database.dart'; diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart deleted file mode 100644 index 695729b..0000000 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:sqlite_async/src/common/abstract_open_factory.dart'; -import 'package:sqlite_async/src/common/sqlite_database.dart'; -import 'package:sqlite_async/src/sqlite_connection.dart'; -import 'package:sqlite_async/src/sqlite_options.dart'; -import 'package:sqlite_async/src/sqlite_queries.dart'; -import 'package:sqlite_async/src/update_notification.dart'; - -class SqliteDatabaseImpl - with SqliteQueries, SqliteDatabaseMixin - implements SqliteDatabase { - @override - bool get closed => throw UnimplementedError(); - - @override - AbstractDefaultSqliteOpenFactory openFactory; - - @override - int maxReaders; - - factory SqliteDatabaseImpl( - {required String path, - int maxReaders = SqliteDatabase.defaultMaxReaders, - SqliteOptions options = const SqliteOptions.defaults()}) { - throw UnimplementedError(); - } - - SqliteDatabaseImpl.withFactory(this.openFactory, - {this.maxReaders = SqliteDatabase.defaultMaxReaders}) { - throw UnimplementedError(); - } - - @override - @protected - Future get isInitialized => throw UnimplementedError(); - - @override - Stream get updates => throw UnimplementedError(); - - @override - Future readLock(Future Function(SqliteReadContext tx) callback, - {Duration? lockTimeout, String? debugContext}) { - throw UnimplementedError(); - } - - @override - Future writeLock(Future Function(SqliteWriteContext tx) callback, - {Duration? lockTimeout, String? debugContext}) { - throw UnimplementedError(); - } - - @override - Future close() { - throw UnimplementedError(); - } - - @override - Future getAutoCommit() { - throw UnimplementedError(); - } - - @override - Future withAllConnections( - Future Function( - SqliteWriteContext writer, List readers) - block) { - throw UnimplementedError(); - } -} diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_open_factory.dart deleted file mode 100644 index 1108752..0000000 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_open_factory.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:async'; - -import 'package:sqlite_async/sqlite3_common.dart'; -import 'package:sqlite_async/src/common/abstract_open_factory.dart'; -import 'package:sqlite_async/src/sqlite_connection.dart'; -import 'package:sqlite_async/src/sqlite_options.dart'; - -class DefaultSqliteOpenFactory extends AbstractDefaultSqliteOpenFactory { - const DefaultSqliteOpenFactory( - {required super.path, - super.sqliteOptions = const SqliteOptions.defaults()}); - - @override - CommonDatabase openDB(SqliteOpenOptions options) { - throw UnimplementedError(); - } - - @override - List pragmaStatements(SqliteOpenOptions options) { - throw UnimplementedError(); - } - - @override - FutureOr openConnection(SqliteOpenOptions options) { - throw UnimplementedError(); - } -} diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart index 0380e6f..4411fa6 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart @@ -11,8 +11,7 @@ import 'package:sqlite_async/src/common/abstract_open_factory.dart'; import 'package:sqlite_async/src/common/sqlite_database.dart'; import 'package:sqlite_async/src/native/native_sqlite_open_factory.dart'; import 'package:sqlite_async/src/sqlite_connection.dart'; -import 'package:sqlite_async/src/sqlite_options.dart'; -import 'package:sqlite_async/src/sqlite_queries.dart'; + import 'package:sqlite_async/src/update_notification.dart'; import '../../common/mutex.dart'; @@ -28,18 +27,15 @@ import 'worker.dart'; /// shared between instances. This also works if the instances are opened on /// different isolates or Dart/Flutter engines in the same process without prior /// coordination. -class SqliteDatabaseImpl - with SqliteQueries, SqliteDatabaseMixin - implements SqliteDatabase { +final class NativeSqliteDatabaseImpl extends SqliteDatabaseImpl { @override - final DefaultSqliteOpenFactory openFactory; - late final Future _pool = - _openNativePool(openFactory, maxReaders); + final NativeSqliteOpenFactory openFactory; + late final Future _pool = _openNativePool(openFactory); bool _isClosed = false; final _lockGuard = Object(); @override - final int maxReaders; + int get maxReaders => openFactory.sqliteOptions.maxReaders; @override @protected @@ -52,38 +48,11 @@ class SqliteDatabaseImpl .asyncExpand((pool) => pool.updatedTables .map((changedTables) => UpdateNotification(changedTables.toSet()))); - /// Open a SqliteDatabase. - /// - /// A connection pool is used by default, allowing multiple concurrent read - /// transactions, and a single concurrent write transaction. Write transactions - /// do not block read transactions, and read transactions will see the state - /// from the last committed write transaction. - /// - /// A maximum of [maxReaders] concurrent read transactions are allowed. - factory SqliteDatabaseImpl( - {required String path, - int maxReaders = SqliteDatabase.defaultMaxReaders, - SqliteOptions options = const SqliteOptions.defaults()}) { - final factory = - DefaultSqliteOpenFactory(path: path, sqliteOptions: options); - return SqliteDatabaseImpl.withFactory(factory, maxReaders: maxReaders); - } - - /// Advanced: Open a database with a specified factory. - /// - /// The factory is used to open each database connection in background isolates. - /// - /// Use when control is required over the opening process. Examples include: - /// 1. Specifying the path to `libsqlite.so` on Linux. - /// 2. Running additional per-connection PRAGMA statements on each connection. - /// 3. Creating custom SQLite functions. - /// 4. Creating temporary views or triggers. - SqliteDatabaseImpl.withFactory(AbstractDefaultSqliteOpenFactory factory, - {this.maxReaders = SqliteDatabase.defaultMaxReaders}) - : openFactory = factory as DefaultSqliteOpenFactory, + NativeSqliteDatabaseImpl(this.openFactory) + : // When the pool is fully used, we'd have all concurrent readers and a // writer operating on the database. Prepare a queue with that capacity. - _workers = ListQueue(maxReaders + 1); + _workers = ListQueue(openFactory.sqliteOptions.maxReaders + 1); @override bool get closed { @@ -319,19 +288,19 @@ class SqliteDatabaseImpl } static Future _openNativePool( - DefaultSqliteOpenFactory openFactory, - int maxReaders, + NativeSqliteOpenFactory openFactory, ) { // We want to open pools asynchronously since running pragma statements as // part of openFactory.open might do IO. openAsync spawn a temporary isolate // for that. + final maxReaders = openFactory.sqliteOptions.maxReaders; return SqliteConnectionPool.openAsync( name: openFactory.path, openConnections: () { Database openConnection({required bool isWriter}) { - return openFactory.open( + return openFactory.openNativeConnection( SqliteOpenOptions(primaryConnection: isWriter, readOnly: !isWriter), - ) as Database; + ); } return PoolConnections( @@ -348,7 +317,7 @@ class SqliteDatabaseImpl final class _LeasedContext extends UnscopedContext { final AsyncConnection inner; - final SqliteDatabaseImpl pool; + final NativeSqliteDatabaseImpl pool; final TimelineTask? task; final IsolateWorker worker; diff --git a/packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart index 5210851..676c397 100644 --- a/packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart +++ b/packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart @@ -1,22 +1,13 @@ -import 'package:sqlite_async/sqlite3.dart' as sqlite; -import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite3/sqlite3.dart' as sqlite; -import 'package:sqlite_async/src/common/abstract_open_factory.dart'; -import 'package:sqlite_async/src/sqlite_connection.dart'; -import 'package:sqlite_async/src/sqlite_options.dart'; +import '../common/abstract_open_factory.dart'; -/// Native implementation of [AbstractDefaultSqliteOpenFactory] -class DefaultSqliteOpenFactory extends AbstractDefaultSqliteOpenFactory { - const DefaultSqliteOpenFactory( - {required super.path, - super.sqliteOptions = const SqliteOptions.defaults()}); - - @override - CommonDatabase openDB(SqliteOpenOptions options) { - final mode = options.openMode; - var db = sqlite.sqlite3.open(path, mode: mode, mutex: false); - return db; - } +/// [SqliteOpenFactory] implementation for native platforms. +/// +/// This class can be extended to customize how databases are opened on native +/// platforms. +base class NativeSqliteOpenFactory extends InternalOpenFactory { + NativeSqliteOpenFactory({required super.path, super.sqliteOptions}); @override List pragmaStatements(SqliteOpenOptions options) { @@ -45,10 +36,39 @@ class DefaultSqliteOpenFactory extends AbstractDefaultSqliteOpenFactory { return statements; } - @override - SqliteConnection openConnection(SqliteOpenOptions options) { - // TODO: Refactor open factories to remove this method. - throw UnsupportedError( - 'openConnection() is not supported on native platforms, open factories can only open pools.'); + /// Opens a new native [Database] connection and runs pragma statements via + /// [configureConnection]. + sqlite.Database openNativeConnection(SqliteOpenOptions options) { + final mode = options.openMode; + final db = sqlite.sqlite3.open(path, mode: mode, mutex: false); + + try { + configureConnection(db, options); + } on Object { + db.close(); + rethrow; + } + return db; + } + + /// Runs [pragmaStatements] for a freshly opened connection, + void configureConnection( + sqlite.Database database, SqliteOpenOptions options) { + // Pragma statements don't have the same BUSY_TIMEOUT behavior as normal statements. + // We add a manual retry loop for those. + for (var statement in pragmaStatements(options)) { + for (var tries = 0; tries < 30; tries++) { + try { + database.execute(statement); + break; + } on sqlite.SqliteException catch (e) { + if (e.resultCode == sqlite.SqlError.SQLITE_BUSY && tries < 29) { + continue; + } else { + rethrow; + } + } + } + } } } diff --git a/packages/sqlite_async/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart index 328fb3f..5ae4ffc 100644 --- a/packages/sqlite_async/lib/src/sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/sqlite_connection.dart @@ -1,7 +1,10 @@ +/// @docImport 'common/sqlite_database.dart'; +library; + import 'dart:async'; import 'package:sqlite3/common.dart' as sqlite; -import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite3/common.dart'; import 'package:sqlite_async/src/update_notification.dart'; import 'common/connection/sync_sqlite_connection.dart'; diff --git a/packages/sqlite_async/lib/src/sqlite_database.dart b/packages/sqlite_async/lib/src/sqlite_database.dart deleted file mode 100644 index 080cb10..0000000 --- a/packages/sqlite_async/lib/src/sqlite_database.dart +++ /dev/null @@ -1 +0,0 @@ -export 'package:sqlite_async/src/impl/sqlite_database_impl.dart'; diff --git a/packages/sqlite_async/lib/src/sqlite_open_factory.dart b/packages/sqlite_async/lib/src/sqlite_open_factory.dart deleted file mode 100644 index 0fd42cb..0000000 --- a/packages/sqlite_async/lib/src/sqlite_open_factory.dart +++ /dev/null @@ -1 +0,0 @@ -export 'impl/open_factory_impl.dart'; diff --git a/packages/sqlite_async/lib/src/sqlite_options.dart b/packages/sqlite_async/lib/src/sqlite_options.dart index 0489f6b..d722f4e 100644 --- a/packages/sqlite_async/lib/src/sqlite_options.dart +++ b/packages/sqlite_async/lib/src/sqlite_options.dart @@ -1,16 +1,15 @@ -class WebSqliteOptions { +final class WebSqliteOptions { final String workerUri; final String wasmUri; - const WebSqliteOptions.defaults() - : workerUri = 'db_worker.js', - wasmUri = 'sqlite3.wasm'; + @Deprecated('Use default WebSqliteOptions constructor instead') + const factory WebSqliteOptions.defaults() = WebSqliteOptions; const WebSqliteOptions( {this.wasmUri = 'sqlite3.wasm', this.workerUri = 'db_worker.js'}); } -class SqliteOptions { +final class SqliteOptions { /// SQLite journal mode. Defaults to [SqliteJournalMode.wal]. final SqliteJournalMode? journalMode; @@ -37,20 +36,53 @@ class SqliteOptions { /// is enabled by default in debug and profile mode. final bool profileQueries; + /// The maximum amount of concurrent readers allowed on opened database pools. + /// + /// Depending on the target platforms, fewer readers than requested here might + /// be supported. For instance, this package does not currently open + /// additional readers on the web. + final int maxReaders; + + @Deprecated('Use default SqliteOptions constructor instead') const factory SqliteOptions.defaults() = SqliteOptions; const SqliteOptions({ this.journalMode = SqliteJournalMode.wal, this.journalSizeLimit = 6 * 1024 * 1024, this.synchronous = SqliteSynchronous.normal, - this.webSqliteOptions = const WebSqliteOptions.defaults(), + this.webSqliteOptions = const WebSqliteOptions(), this.lockTimeout = const Duration(seconds: 30), this.profileQueries = _profileQueriesByDefault, + this.maxReaders = defaultMaxReaders, }); + /// Creates a new options instance by applying overrides from parameters. + /// + /// Only non-nullable fields can be changed this way. For other fields, create + /// a new instance manually. + SqliteOptions copyWith({ + WebSqliteOptions? webSqliteOptions, + bool? profileQueries, + int? maxReaders, + }) { + return SqliteOptions( + journalMode: journalMode, + synchronous: synchronous, + journalSizeLimit: journalSizeLimit, + webSqliteOptions: webSqliteOptions ?? this.webSqliteOptions, + lockTimeout: lockTimeout, + profileQueries: profileQueries ?? this.profileQueries, + maxReaders: maxReaders ?? this.maxReaders, + ); + } + // https://api.flutter.dev/flutter/foundation/kReleaseMode-constant.html static const _profileQueriesByDefault = !bool.fromEnvironment('dart.vm.product'); + + /// The maximum number of concurrent read transactions if not explicitly + /// specified. + static const int defaultMaxReaders = 5; } /// SQLite journal mode. Set on the primary connection. diff --git a/packages/sqlite_async/lib/src/web/connection.dart b/packages/sqlite_async/lib/src/web/connection.dart new file mode 100644 index 0000000..99da467 --- /dev/null +++ b/packages/sqlite_async/lib/src/web/connection.dart @@ -0,0 +1,102 @@ +import 'package:sqlite3_web/sqlite3_web.dart'; +import 'package:web/web.dart'; + +import '../sqlite_connection.dart'; +import 'database.dart'; +import 'update_notifications.dart'; +import 'web_mutex.dart'; + +/// An endpoint that can be used, by any running JavaScript context in the same +/// website, to connect to an existing [WebSqliteConnection]. +/// +/// These endpoints are created by calling [WebSqliteConnection.exposeEndpoint] +/// and consist of a [MessagePort] and two [String]s internally identifying the +/// connection. Both objects can be transferred over send ports towards another +/// worker or context. That context can then use +/// [WebSqliteConnection.connectToEndpoint] to connect to the port already +/// opened. +typedef WebDatabaseEndpoint = ({ + MessagePort connectPort, + String connectName, + String? lockName, +}); + +/// A [SqliteConnection] interface implemented by opened connections when +/// running on the web. +/// +/// This adds the [exposeEndpoint], which uses `dart:js_interop` types not +/// supported on native Dart platforms. The method can be used to access an +/// opened database across different JavaScript contexts +/// (e.g. document windows and workers). +abstract class WebSqliteConnection implements SqliteConnection { + /// Returns a future that completes when this connection is closed. + /// + /// This usually only happens when calling [close], but on the web + /// specifically, it can also happen when a remote context closes a database + /// accessed via [connectToEndpoint]. + Future get closedFuture; + + /// Returns a [WebDatabaseEndpoint] - a structure that consists only of types + /// that can be transferred across a [MessagePort] in JavaScript. + /// + /// After transferring this endpoint to another JavaScript context (e.g. a + /// worker), the worker can call [connectToEndpoint] to obtain a connection to + /// the same sqlite database. + Future exposeEndpoint(); + + /// Connect to an endpoint obtained through [exposeEndpoint]. + /// + /// The endpoint is transferrable in JavaScript, allowing multiple JavaScript + /// contexts to exchange opened database connections. + static Future connectToEndpoint( + WebDatabaseEndpoint endpoint) async { + final updates = UpdateNotificationStreams(); + final rawSqlite = await WebSqlite.connectToPort( + (endpoint.connectPort, endpoint.connectName), + handleCustomRequest: updates.handleRequest, + ); + + final database = WebDatabase( + rawSqlite, + switch (endpoint.lockName) { + var lock? => WebMutexImpl(identifier: lock), + null => null, + }, + profileQueries: false, + updates: updates.updatesFor(rawSqlite), + ); + return database; + } + + /// Same as [SqliteConnection.writeLock]. + /// + /// Has an additional [flush] (defaults to true). This can be set to false + /// to delay flushing changes to the database file, losing durability guarantees. + /// This only has an effect when IndexedDB storage is used. + /// + /// See [flush] for details. + @override + Future writeLock(Future Function(SqliteWriteContext tx) callback, + {Duration? lockTimeout, String? debugContext, bool? flush}); + + /// Same as [SqliteConnection.writeTransaction]. + /// + /// Has an additional [flush] (defaults to true). This can be set to false + /// to delay flushing changes to the database file, losing durability guarantees. + /// This only has an effect when IndexedDB storage is used. + /// + /// See [flush] for details. + @override + Future writeTransaction( + Future Function(SqliteWriteContext tx) callback, + {Duration? lockTimeout, + bool? flush}); + + /// Flush changes to the underlying storage. + /// + /// When this returns, all changes previously written will be persisted + /// to storage. + /// + /// This only has an effect when IndexedDB storage is used. + Future flush(); +} diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 45fdba4..1f8e0b3 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -2,20 +2,21 @@ import 'dart:async'; import 'dart:developer'; import 'dart:js_interop'; +import 'package:meta/meta.dart'; import 'package:sqlite3/common.dart'; import 'package:sqlite3_web/sqlite3_web.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite_async/src/utils/profiler.dart'; import 'package:sqlite_async/src/web/database/broadcast_updates.dart'; -import 'package:sqlite_async/web.dart'; +import '../common/sqlite_database.dart'; import '../common/timeouts.dart'; import '../impl/context.dart'; +import 'connection.dart'; import 'protocol.dart'; import 'web_mutex.dart'; -class WebDatabase - with SqliteQueries, SqliteDatabaseMixin - implements SqliteDatabase, WebSqliteConnection { +final class WebDatabase extends SqliteDatabaseImpl + implements WebSqliteConnection { final Database _database; final Mutex? _mutex; final bool profileQueries; @@ -30,6 +31,7 @@ class WebDatabase @override bool closed = false; + @internal WebDatabase( this._database, this._mutex, { diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/async_web_database.dart similarity index 66% rename from packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart rename to packages/sqlite_async/lib/src/web/database/async_web_database.dart index 577e81a..c7704ea 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/async_web_database.dart @@ -2,21 +2,18 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:sqlite_async/src/common/abstract_open_factory.dart'; import 'package:sqlite_async/src/common/mutex.dart'; -import 'package:sqlite_async/src/sqlite_queries.dart'; import 'package:sqlite_async/src/common/sqlite_database.dart'; import 'package:sqlite_async/src/sqlite_connection.dart'; -import 'package:sqlite_async/src/sqlite_options.dart'; import 'package:sqlite_async/src/update_notification.dart'; -import 'package:sqlite_async/src/web/web_sqlite_open_factory.dart'; -import 'package:sqlite_async/web.dart'; +import 'package:sqlite_async/web.dart' hide WebDatabaseEndpoint; +import '../connection.dart'; import '../database.dart'; -/// Web implementation of [SqliteDatabase] -/// Uses a web worker for SQLite connection -class SqliteDatabaseImpl - with SqliteQueries, SqliteDatabaseMixin - implements SqliteDatabase, WebSqliteConnection { +/// A [SqliteDatabase] implemented by delegating to a [WebDatabase] opened +/// asynchronously. +final class AsyncWebDatabaseImpl extends SqliteDatabaseImpl + implements WebSqliteConnection { @override bool get closed { return _connection.closed; @@ -32,48 +29,19 @@ class SqliteDatabaseImpl late Stream updates; @override - int maxReaders; + int get maxReaders => openFactory.sqliteOptions.maxReaders; @override @protected late Future isInitialized; @override - AbstractDefaultSqliteOpenFactory openFactory; + WebSqliteOpenFactory openFactory; late final WebDatabase _connection; StreamSubscription? _broadcastUpdatesSubscription; - /// Open a SqliteDatabase. - /// - /// Only a single SqliteDatabase per [path] should be opened at a time. - /// - /// A connection pool is used by default, allowing multiple concurrent read - /// transactions, and a single concurrent write transaction. Write transactions - /// do not block read transactions, and read transactions will see the state - /// from the last committed write transaction. - /// - /// A maximum of [maxReaders] concurrent read transactions are allowed. - factory SqliteDatabaseImpl( - {required String path, - int maxReaders = SqliteDatabase.defaultMaxReaders, - SqliteOptions options = const SqliteOptions.defaults()}) { - final factory = - DefaultSqliteOpenFactory(path: path, sqliteOptions: options); - return SqliteDatabaseImpl.withFactory(factory, maxReaders: maxReaders); - } - - /// Advanced: Open a database with a specified factory. - /// - /// The factory is used to open each database connection in background isolates. - /// - /// Use when control is required over the opening process. Examples include: - /// 1. Specifying the path to `libsqlite.so` on Linux. - /// 2. Running additional per-connection PRAGMA statements on each connection. - /// 3. Creating custom SQLite functions. - /// 4. Creating temporary views or triggers. - SqliteDatabaseImpl.withFactory(this.openFactory, - {this.maxReaders = SqliteDatabase.defaultMaxReaders}) { + AsyncWebDatabaseImpl(this.openFactory) { // This way the `updates` member is available synchronously updates = updatesController.stream; isInitialized = _init(); @@ -81,8 +49,7 @@ class SqliteDatabaseImpl Future _init() async { _connection = await openFactory.openConnection( - SqliteOpenOptions(primaryConnection: true, readOnly: false)) - as WebDatabase; + SqliteOpenOptions(primaryConnection: true, readOnly: false)); final broadcastUpdates = _connection.broadcastUpdates; if (broadcastUpdates == null) { diff --git a/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart index 14e8d92..950ffbe 100644 --- a/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart +++ b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart @@ -1,21 +1,23 @@ import 'dart:async'; +import 'dart:js_interop'; -import 'package:sqlite3/wasm.dart'; import 'package:sqlite3_web/sqlite3_web.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite_async/src/web/database/broadcast_updates.dart'; import 'package:sqlite_async/src/web/web_mutex.dart'; -import 'package:sqlite_async/web.dart'; +import '../common/abstract_open_factory.dart'; import 'database.dart'; +import 'update_notifications.dart'; import 'worker/worker_utils.dart'; +final UpdateNotificationStreams _updateStreams = UpdateNotificationStreams(); Map> _webSQLiteImplementations = {}; -/// Web implementation of [AbstractDefaultSqliteOpenFactory] -class DefaultSqliteOpenFactory - extends AbstractDefaultSqliteOpenFactory - with WebSqliteOpenFactory { +/// [SqliteOpenFactory] implementation for the web. +/// +/// This class can be extended to customize how databases are opened on the web. +base class WebSqliteOpenFactory extends InternalOpenFactory { late final Future _initialized = Future.sync(() { final cacheKey = sqliteOptions.webSqliteOptions.wasmUri + sqliteOptions.webSqliteOptions.workerUri; @@ -29,14 +31,18 @@ class DefaultSqliteOpenFactory return _webSQLiteImplementations[cacheKey]!; }); - DefaultSqliteOpenFactory( - {required super.path, - super.sqliteOptions = const SqliteOptions.defaults()}) { + WebSqliteOpenFactory( + {required super.path, super.sqliteOptions = const SqliteOptions()}) { // Make sure initializer starts running immediately _initialized; } - @override + /// Opens a [WebSqlite] instance for the given [options]. + /// + /// This method can be overriden in scenarios where the way [WebSqlite] is + /// opened needs to be customized. Implementers should be aware that the + /// result of this method is cached and will be re-used by the open factory + /// when provided with the same [options] again. Future openWebSqlite(WebSqliteOptions options) async { return WebSqlite.open( wasmModule: Uri.parse(sqliteOptions.webSqliteOptions.wasmUri), @@ -47,23 +53,40 @@ class DefaultSqliteOpenFactory ); } - /// This is currently not supported on web - @override - CommonDatabase openDB(SqliteOpenOptions options) { - throw UnimplementedError( - 'Direct access to CommonDatabase is not available on web.'); + /// Handles a custom request sent from the worker to the client. + Future handleCustomRequest(JSAny? request) { + return _updateStreams.handleRequest(request); } - @override + /// Uses [WebSqlite] to connects to the recommended database setup for [name]. + /// + /// This typically just calls [WebSqlite.connectToRecommended], but subclasses + /// can customize the behavior where needed. + Future connectToWorker( + WebSqlite sqlite, String name) { + return sqlite.connectToRecommended(name); + } /// Currently this only uses the SQLite Web WASM implementation. /// This provides built in async Web worker functionality /// and automatic persistence storage selection. - /// Due to being asynchronous, the under laying CommonDatabase is not accessible - Future openConnection(SqliteOpenOptions options) async { + /// Due to being asynchronous, the under laying CommonDatabase is not + /// accessible + Future openConnection(SqliteOpenOptions options) async { final workers = await _initialized; final connection = await connectToWorker(workers, path); + final pragmaStatements = this.pragmaStatements(options); + if (pragmaStatements.isNotEmpty) { + // The default implementation doesn't use pragmas on the web, but a + // subclass might. + await connection.database.requestLock((token) async { + for (final stmt in pragmaStatements) { + await connection.database.execute(stmt, token: token); + } + }); + } + // When the database is hosted in a shared worker, we don't need a local // mutex since that worker will hand out leases for us. // Additionally, package:sqlite3_web uses navigator locks internally for @@ -89,7 +112,7 @@ class DefaultSqliteOpenFactory return WebDatabase( connection.database, - options.mutex ?? mutex, + mutex, broadcastUpdates: broadcastUpdates, profileQueries: sqliteOptions.profileQueries, updates: updatesFor(connection.database), @@ -101,4 +124,12 @@ class DefaultSqliteOpenFactory // WAL mode is not supported on Web return []; } + + /// Obtains a stream of [UpdateNotification]s from a [database]. + /// + /// The default implementation uses custom requests to allow workers to + /// debounce the stream on their side to avoid messages where possible. + Stream updatesFor(Database database) { + return _updateStreams.updatesFor(database); + } } diff --git a/packages/sqlite_async/lib/web.dart b/packages/sqlite_async/lib/web.dart index 55c4ea4..42385ed 100644 --- a/packages/sqlite_async/lib/web.dart +++ b/packages/sqlite_async/lib/web.dart @@ -4,154 +4,5 @@ /// workers. library; -import 'dart:js_interop'; - -import 'package:sqlite3_web/sqlite3_web.dart'; -import 'package:web/web.dart'; - -import 'sqlite3_common.dart'; -import 'sqlite_async.dart'; -import 'src/web/database.dart'; -import 'src/web/update_notifications.dart'; -import 'src/web/web_mutex.dart'; - -/// An endpoint that can be used, by any running JavaScript context in the same -/// website, to connect to an existing [WebSqliteConnection]. -/// -/// These endpoints are created by calling [WebSqliteConnection.exposeEndpoint] -/// and consist of a [MessagePort] and two [String]s internally identifying the -/// connection. Both objects can be transferred over send ports towards another -/// worker or context. That context can then use -/// [WebSqliteConnection.connectToEndpoint] to connect to the port already -/// opened. -typedef WebDatabaseEndpoint = ({ - MessagePort connectPort, - String connectName, - String? lockName, -}); - -final UpdateNotificationStreams _updateStreams = UpdateNotificationStreams(); - -/// An additional interface for [SqliteOpenFactory] exposing additional -/// functionality that is only relevant when compiling to the web. -/// -/// The [DefaultSqliteOpenFactory] class implements this interface only when -/// compiling for the web. -abstract mixin class WebSqliteOpenFactory - implements SqliteOpenFactory { - /// Handles a custom request sent from the worker to the client. - Future handleCustomRequest(JSAny? request) { - return _updateStreams.handleRequest(request); - } - - /// Opens a [WebSqlite] instance for the given [options]. - /// - /// This method can be overriden in scenarios where the way [WebSqlite] is - /// opened needs to be customized. Implementers should be aware that the - /// result of this method is cached and will be re-used by the open factory - /// when provided with the same [options] again. - Future openWebSqlite(WebSqliteOptions options) async { - return WebSqlite.open( - workers: WorkerConnector.defaultWorkers(Uri.parse(options.workerUri)), - wasmModule: Uri.parse(options.wasmUri), - handleCustomRequest: handleCustomRequest, - ); - } - - /// Uses [WebSqlite] to connects to the recommended database setup for [name]. - /// - /// This typically just calls [WebSqlite.connectToRecommended], but subclasses - /// can customize the behavior where needed. - Future connectToWorker( - WebSqlite sqlite, String name) { - return sqlite.connectToRecommended(name); - } - - /// Obtains a stream of [UpdateNotification]s from a [database]. - /// - /// The default implementation uses custom requests to allow workers to - /// debounce the stream on their side to avoid messages where possible. - Stream updatesFor(Database database) { - return _updateStreams.updatesFor(database); - } -} - -/// A [SqliteConnection] interface implemented by opened connections when -/// running on the web. -/// -/// This adds the [exposeEndpoint], which uses `dart:js_interop` types not -/// supported on native Dart platforms. The method can be used to access an -/// opened database across different JavaScript contexts -/// (e.g. document windows and workers). -abstract class WebSqliteConnection implements SqliteConnection { - /// Returns a future that completes when this connection is closed. - /// - /// This usually only happens when calling [close], but on the web - /// specifically, it can also happen when a remote context closes a database - /// accessed via [connectToEndpoint]. - Future get closedFuture; - - /// Returns a [WebDatabaseEndpoint] - a structure that consists only of types - /// that can be transferred across a [MessagePort] in JavaScript. - /// - /// After transferring this endpoint to another JavaScript context (e.g. a - /// worker), the worker can call [connectToEndpoint] to obtain a connection to - /// the same sqlite database. - Future exposeEndpoint(); - - /// Connect to an endpoint obtained through [exposeEndpoint]. - /// - /// The endpoint is transferrable in JavaScript, allowing multiple JavaScript - /// contexts to exchange opened database connections. - static Future connectToEndpoint( - WebDatabaseEndpoint endpoint) async { - final updates = UpdateNotificationStreams(); - final rawSqlite = await WebSqlite.connectToPort( - (endpoint.connectPort, endpoint.connectName), - handleCustomRequest: updates.handleRequest, - ); - - final database = WebDatabase( - rawSqlite, - switch (endpoint.lockName) { - var lock? => WebMutexImpl(identifier: lock), - null => null, - }, - profileQueries: false, - updates: updates.updatesFor(rawSqlite), - ); - return database; - } - - /// Same as [SqliteConnection.writeLock]. - /// - /// Has an additional [flush] (defaults to true). This can be set to false - /// to delay flushing changes to the database file, losing durability guarantees. - /// This only has an effect when IndexedDB storage is used. - /// - /// See [flush] for details. - @override - Future writeLock(Future Function(SqliteWriteContext tx) callback, - {Duration? lockTimeout, String? debugContext, bool? flush}); - - /// Same as [SqliteConnection.writeTransaction]. - /// - /// Has an additional [flush] (defaults to true). This can be set to false - /// to delay flushing changes to the database file, losing durability guarantees. - /// This only has an effect when IndexedDB storage is used. - /// - /// See [flush] for details. - @override - Future writeTransaction( - Future Function(SqliteWriteContext tx) callback, - {Duration? lockTimeout, - bool? flush}); - - /// Flush changes to the underlying storage. - /// - /// When this returns, all changes previously written will be persisted - /// to storage. - /// - /// This only has an effect when IndexedDB storage is used. - Future flush(); -} +export 'src/web/connection.dart'; +export 'src/web/web_sqlite_open_factory.dart'; diff --git a/packages/sqlite_async/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart index 1623fdd..1295be3 100644 --- a/packages/sqlite_async/test/basic_test.dart +++ b/packages/sqlite_async/test/basic_test.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite3/common.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; @@ -258,8 +258,7 @@ void main() { }); test('can use raw database instance', () async { - final factory = await testUtils.testFactory(); - final raw = await factory.openDatabaseForSingleConnection(); + final raw = await testUtils.openDatabaseForSingleConnection(); // Creating a fuction ensures that this database is actually used - if // a connection were set up in a background isolate, it wouldn't have this // function. @@ -381,8 +380,8 @@ void main() { final maxReaders = _isWeb ? 0 : 3; final db = SqliteDatabase.withFactory( - await testUtils.testFactory(path: path), - maxReaders: maxReaders, + await testUtils.testFactory( + path: path, options: SqliteOptions(maxReaders: maxReaders)), ); await db.initialize(); await createTables(db); diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index 164e7c7..b1820fd 100644 --- a/packages/sqlite_async/test/native/basic_test.dart +++ b/packages/sqlite_async/test/native/basic_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:sqlite3/common.dart' as sqlite; import 'package:sqlite3_connection_pool/sqlite3_connection_pool.dart'; +import 'package:sqlite_async/native.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; @@ -55,8 +56,9 @@ void main() { // Manually verified test('Concurrency', () async { final db = SqliteDatabase.withFactory( - await testUtils.testFactory(path: path), - maxReaders: 3); + await testUtils.testFactory( + path: path, options: SqliteOptions(maxReaders: 3)), + ); await db.initialize(); await createTables(db); @@ -86,8 +88,9 @@ void main() { test('prevent opening new readers while in withAllConnections', () async { final db = SqliteDatabase.withFactory( - await testUtils.testFactory(path: path), - maxReaders: 3); + await testUtils.testFactory( + path: path, options: SqliteOptions(maxReaders: 3)), + ); await db.initialize(); await createTables(db); @@ -323,7 +326,8 @@ void main() { }); test('lockTimeout', () async { - final db = await testUtils.setupDatabase(path: path, maxReaders: 2); + final db = await testUtils.setupDatabase( + path: path, options: SqliteOptions(maxReaders: 2)); await db.initialize(); final f1 = db.readTransaction((tx) async { @@ -367,8 +371,8 @@ void ignore(Future future) { future.then((_) {}, onError: (_) {}); } -class _InvalidPragmaOnOpenFactory extends DefaultSqliteOpenFactory { - const _InvalidPragmaOnOpenFactory({required super.path}); +final class _InvalidPragmaOnOpenFactory extends NativeSqliteOpenFactory { + _InvalidPragmaOnOpenFactory({required super.path}); @override List pragmaStatements(SqliteOpenOptions options) { diff --git a/packages/sqlite_async/test/native/watch_test.dart b/packages/sqlite_async/test/native/watch_test.dart index e289209..1ae030a 100644 --- a/packages/sqlite_async/test/native/watch_test.dart +++ b/packages/sqlite_async/test/native/watch_test.dart @@ -6,6 +6,7 @@ import 'dart:isolate'; import 'dart:math'; import 'package:sqlite3/common.dart'; +import 'package:sqlite_async/native.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite_async/src/utils/shared_utils.dart'; import 'package:test/test.dart'; @@ -24,18 +25,11 @@ void main() { await testUtils.cleanDb(path: path); }); - generateSourceTableTests(testUtils.findSqliteLibraries(), - (String sqlitePath) async { - final db = - SqliteDatabase.withFactory(await testUtils.testFactory(path: path)); - await db.initialize(); - return db; - }); - test('raw update notifications', () async { - final factory = await testUtils.testFactory(path: path); - final db = factory - .openDB(SqliteOpenOptions(primaryConnection: true, readOnly: false)); + final factory = + (await testUtils.testFactory(path: path)) as NativeSqliteOpenFactory; + final db = factory.openNativeConnection( + SqliteOpenOptions(primaryConnection: true, readOnly: false)); db.execute('CREATE TABLE a (bar INTEGER);'); db.execute('CREATE TABLE b (bar INTEGER);'); diff --git a/packages/sqlite_async/test/utils/abstract_test_utils.dart b/packages/sqlite_async/test/utils/abstract_test_utils.dart index b388c4d..29be184 100644 --- a/packages/sqlite_async/test/utils/abstract_test_utils.dart +++ b/packages/sqlite_async/test/utils/abstract_test_utils.dart @@ -1,45 +1,25 @@ -import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite3/common.dart'; import 'package:sqlite_async/sqlite_async.dart'; -class TestDefaultSqliteOpenFactory extends DefaultSqliteOpenFactory { - final String sqlitePath; - - TestDefaultSqliteOpenFactory( - {required super.path, super.sqliteOptions, this.sqlitePath = ''}); - - Future openDatabaseForSingleConnection() async { - return openDB(SqliteOpenOptions(primaryConnection: true, readOnly: false)); - } -} - abstract class AbstractTestUtils { String dbPath(); - /// Generates a test open factory - Future testFactory( - {String? path, - String sqlitePath = '', - List initStatements = const [], - SqliteOptions options = const SqliteOptions.defaults()}) async { - return TestDefaultSqliteOpenFactory( - path: path ?? dbPath(), - sqliteOptions: options, - ); + Future openDatabaseForSingleConnection(); + + Future testFactory( + {String? path, SqliteOptions options = const SqliteOptions()}) async { + return SqliteOpenFactory(path: path ?? dbPath(), options: options); } /// Creates a SqliteDatabaseConnection - Future setupDatabase( - {String? path, - List initStatements = const [], - int maxReaders = SqliteDatabase.defaultMaxReaders}) async { - final db = SqliteDatabase.withFactory(await testFactory(path: path), - maxReaders: maxReaders); - await db.initialize(); - return db; + Future setupDatabase({ + String? path, + SqliteOptions options = const SqliteOptions(), + }) async { + final factory = await testFactory(path: path, options: options); + return SqliteDatabase.withFactory(factory); } /// Deletes any DB data Future cleanDb({required String path}); - - List findSqliteLibraries(); } diff --git a/packages/sqlite_async/test/utils/native_test_utils.dart b/packages/sqlite_async/test/utils/native_test_utils.dart index 2982d4a..f8d180d 100644 --- a/packages/sqlite_async/test/utils/native_test_utils.dart +++ b/packages/sqlite_async/test/utils/native_test_utils.dart @@ -1,30 +1,14 @@ import 'dart:async'; import 'dart:io'; -import 'package:glob/glob.dart'; -import 'package:glob/list_local_fs.dart'; -import 'package:sqlite_async/sqlite3.dart'; -import 'package:sqlite_async/sqlite3_common.dart'; -import 'package:sqlite_async/sqlite_async.dart'; +import 'package:sqlite3/common.dart'; +import 'package:sqlite3/sqlite3.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; import 'abstract_test_utils.dart'; const defaultSqlitePath = 'libsqlite3.so.0'; -class TestSqliteOpenFactory extends TestDefaultSqliteOpenFactory { - TestSqliteOpenFactory( - {required super.path, - super.sqliteOptions, - super.sqlitePath = defaultSqlitePath, - initStatements}); - - @override - Future openDatabaseForSingleConnection() async { - return sqlite3.openInMemory(); - } -} - class TestUtils extends AbstractTestUtils { @override String dbPath() { @@ -51,25 +35,7 @@ class TestUtils extends AbstractTestUtils { } @override - List findSqliteLibraries() { - var glob = Glob('sqlite-*/.libs/libsqlite3.so'); - List sqlites = [ - 'libsqlite3.so.0', - for (var sqlite in glob.listSync()) sqlite.path - ]; - return sqlites; - } - - @override - Future testFactory( - {String? path, - String sqlitePath = defaultSqlitePath, - List initStatements = const [], - SqliteOptions options = const SqliteOptions.defaults()}) async { - return TestSqliteOpenFactory( - path: path ?? dbPath(), - sqlitePath: sqlitePath, - sqliteOptions: options, - initStatements: initStatements); + Future openDatabaseForSingleConnection() async { + return sqlite3.openInMemory(); } } diff --git a/packages/sqlite_async/test/utils/stub_test_utils.dart b/packages/sqlite_async/test/utils/stub_test_utils.dart index 852009f..dfa5f07 100644 --- a/packages/sqlite_async/test/utils/stub_test_utils.dart +++ b/packages/sqlite_async/test/utils/stub_test_utils.dart @@ -1,3 +1,5 @@ +import 'package:sqlite3/common.dart'; + import 'abstract_test_utils.dart'; class TestUtils extends AbstractTestUtils { @@ -12,7 +14,7 @@ class TestUtils extends AbstractTestUtils { } @override - List findSqliteLibraries() { + Future openDatabaseForSingleConnection() { throw UnimplementedError(); } } diff --git a/packages/sqlite_async/test/utils/web_test_utils.dart b/packages/sqlite_async/test/utils/web_test_utils.dart index 33f7d64..34a4469 100644 --- a/packages/sqlite_async/test/utils/web_test_utils.dart +++ b/packages/sqlite_async/test/utils/web_test_utils.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:math'; -import 'package:sqlite_async/sqlite3_wasm.dart'; +import 'package:sqlite3/wasm.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; import 'package:web/web.dart' show Blob, BlobPart, BlobPropertyBag; @@ -13,23 +13,9 @@ external String _createObjectURL(Blob blob); String? _dbPath; -class TestSqliteOpenFactory extends TestDefaultSqliteOpenFactory { - TestSqliteOpenFactory( - {required super.path, super.sqliteOptions, super.sqlitePath = ''}); - - @override - Future openDatabaseForSingleConnection() async { - final sqlite = await WasmSqlite3.loadFromUrl( - Uri.parse(sqliteOptions.webSqliteOptions.wasmUri)); - sqlite.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true); - - return sqlite.openInMemory(); - } -} - class TestUtils extends AbstractTestUtils { late Future _isInitialized; - late final SqliteOptions webOptions; + late final WebSqliteOptions webOptions; TestUtils() { _isInitialized = _init(); @@ -46,9 +32,8 @@ class TestUtils extends AbstractTestUtils { BlobPropertyBag(type: 'application/javascript')); sqliteUri = _createObjectURL(blob); - webOptions = SqliteOptions( - webSqliteOptions: WebSqliteOptions( - wasmUri: sqliteWasmUri.toString(), workerUri: sqliteUri)); + webOptions = WebSqliteOptions( + wasmUri: sqliteWasmUri.toString(), workerUri: sqliteUri); } @override @@ -70,30 +55,22 @@ class TestUtils extends AbstractTestUtils { Future cleanDb({required String path}) async {} @override - Future testFactory( - {String? path, - String sqlitePath = '', - List initStatements = const [], - SqliteOptions options = const SqliteOptions.defaults()}) async { + Future testFactory({ + String? path, + SqliteOptions options = const SqliteOptions(), + }) async { await _isInitialized; - return TestSqliteOpenFactory( - path: path ?? dbPath(), - sqlitePath: sqlitePath, - sqliteOptions: webOptions, + return super.testFactory( + path: path, + options: options.copyWith(webSqliteOptions: webOptions), ); } @override - Future setupDatabase( - {String? path, - List initStatements = const [], - int maxReaders = SqliteDatabase.defaultMaxReaders}) async { - await _isInitialized; - return super.setupDatabase(path: path); - } + Future openDatabaseForSingleConnection() async { + final sqlite = await WasmSqlite3.loadFromUrl(Uri.parse(webOptions.wasmUri)); + sqlite.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true); - @override - List findSqliteLibraries() { - return ['sqlite3.wasm']; + return sqlite.openInMemory(); } } diff --git a/packages/sqlite_async/test/watch_test.dart b/packages/sqlite_async/test/watch_test.dart index 1597aa0..c78b6de 100644 --- a/packages/sqlite_async/test/watch_test.dart +++ b/packages/sqlite_async/test/watch_test.dart @@ -21,39 +21,6 @@ Future createTables(SqliteDatabase db) async { }); } -// Web and native have different requirements for `sqlitePaths`. -void generateSourceTableTests(List sqlitePaths, - Future Function(String sqlitePath) generateDB) { - for (var sqlite in sqlitePaths) { - test('getSourceTables - $sqlite', () async { - final db = await generateDB(sqlite); - await createTables(db); - - var versionRow = await db.get('SELECT sqlite_version() as version'); - print('Testing SQLite ${versionRow['version']} - $sqlite'); - - final tables = await getSourceTables(db, - 'SELECT * FROM assets INNER JOIN customers ON assets.customer_id = customers.id'); - expect(tables, equals({'assets', 'customers'})); - - final tables2 = await getSourceTables(db, - 'SELECT count() FROM assets INNER JOIN "other_customers" AS oc ON assets.customer_id = oc.id AND assets.make = oc.name'); - expect(tables2, equals({'assets', 'other_customers'})); - - final tables3 = await getSourceTables(db, 'SELECT count() FROM assets'); - expect(tables3, equals({'assets'})); - - final tables4 = - await getSourceTables(db, 'SELECT count() FROM assets_alias'); - expect(tables4, equals({'assets'})); - - final tables5 = - await getSourceTables(db, 'SELECT sqlite_version() as version'); - expect(tables5, equals({})); - }); - } -} - void main() { // Shared tests for watch group('Query Watch Tests', () { @@ -304,5 +271,32 @@ void main() { // [0]: No updates triggered. // [2, 2]: Timing issue? }); + + test('getSourceTables', () async { + final db = await testUtils.setupDatabase(path: path); + await createTables(db); + + var versionRow = await db.get('SELECT sqlite_version() as version'); + print('Testing SQLite ${versionRow['version']}'); + + final tables = await getSourceTables(db, + 'SELECT * FROM assets INNER JOIN customers ON assets.customer_id = customers.id'); + expect(tables, equals({'assets', 'customers'})); + + final tables2 = await getSourceTables(db, + 'SELECT count() FROM assets INNER JOIN "other_customers" AS oc ON assets.customer_id = oc.id AND assets.make = oc.name'); + expect(tables2, equals({'assets', 'other_customers'})); + + final tables3 = await getSourceTables(db, 'SELECT count() FROM assets'); + expect(tables3, equals({'assets'})); + + final tables4 = + await getSourceTables(db, 'SELECT count() FROM assets_alias'); + expect(tables4, equals({'assets'})); + + final tables5 = + await getSourceTables(db, 'SELECT sqlite_version() as version'); + expect(tables5, equals({})); + }); }); } diff --git a/packages/sqlite_async/test/web/watch_test.dart b/packages/sqlite_async/test/web/watch_test.dart deleted file mode 100644 index c6757b9..0000000 --- a/packages/sqlite_async/test/web/watch_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -@TestOn('browser') -library; - -import 'package:sqlite_async/sqlite_async.dart'; -import 'package:test/test.dart'; - -import '../utils/test_utils_impl.dart'; -import '../watch_test.dart'; - -final testUtils = TestUtils(); - -void main() { - // Shared tests for watch - group('Web Query Watch Tests', () { - late String path; - - setUp(() async { - path = testUtils.dbPath(); - await testUtils.cleanDb(path: path); - }); - - /// Can't use testUtils instance here directly (can be in callback) since it requires spawnHybridUri - /// which is not available when declaring tests - generateSourceTableTests(['sqlite3.wasm'], (String sqlitePath) async { - final db = - SqliteDatabase.withFactory(await testUtils.testFactory(path: path)); - await db.initialize(); - return db; - }); - }); -}