From e57b4e2520781946c70986f0b0e8d5ce3854e6e6 Mon Sep 17 00:00:00 2001 From: Jorge Sardina Date: Thu, 12 Sep 2024 13:24:41 +0200 Subject: [PATCH 01/16] WIP --- .DS_Store | Bin 0 -> 6148 bytes .../drift_sqlite_async/lib/src/connection.dart | 4 ++-- .../drift_sqlite_async/lib/src/executor.dart | 10 +++++----- .../lib/src/common/sqlite_database.dart | 2 ++ .../lib/src/impl/stub_sqlite_database.dart | 3 +++ .../lib/src/native/database/connection_pool.dart | 4 ++++ .../database/native_sqlite_connection_impl.dart | 1 + .../native/database/native_sqlite_database.dart | 3 +++ packages/sqlite_async/lib/src/web/database.dart | 3 +++ .../src/web/database/web_sqlite_database.dart | 3 +++ 10 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0{}; for (var tableName in event.tables) { diff --git a/packages/drift_sqlite_async/lib/src/executor.dart b/packages/drift_sqlite_async/lib/src/executor.dart index 91c6f7f..0b6b913 100644 --- a/packages/drift_sqlite_async/lib/src/executor.dart +++ b/packages/drift_sqlite_async/lib/src/executor.dart @@ -9,7 +9,9 @@ class _SqliteAsyncDelegate extends DatabaseDelegate { final SqliteConnection db; bool _closed = false; - _SqliteAsyncDelegate(this.db); + _SqliteAsyncDelegate( + this.db, + ); @override late final DbVersionDelegate versionDelegate = @@ -127,10 +129,8 @@ class _SqliteAsyncVersionDelegate extends DynamicVersionDelegate { /// Extnral update notifications from the [SqliteConnection] are _not_ forwarded /// automatically - use [SqliteAsyncDriftConnection] for that. class SqliteAsyncQueryExecutor extends DelegatedDatabase { - SqliteAsyncQueryExecutor(SqliteConnection db) - : super( - _SqliteAsyncDelegate(db), - ); + SqliteAsyncQueryExecutor(SqliteConnection db, {bool logStatements = false}) + : super(_SqliteAsyncDelegate(db), logStatements: logStatements); /// The underlying SqliteConnection used by drift to send queries. SqliteConnection get db { diff --git a/packages/sqlite_async/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart index f8e0be5..f7bc933 100644 --- a/packages/sqlite_async/lib/src/common/sqlite_database.dart +++ b/packages/sqlite_async/lib/src/common/sqlite_database.dart @@ -50,6 +50,8 @@ abstract class SqliteDatabase /// The maximum number of concurrent read transactions if not explicitly specified. static const int defaultMaxReaders = 5; + int get numConnections; + /// Open a SqliteDatabase. /// /// Only a single SqliteDatabase per [path] should be opened at a time. diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart index 29db641..89b9967 100644 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart @@ -64,4 +64,7 @@ class SqliteDatabaseImpl Future getAutoCommit() { throw UnimplementedError(); } + + @override + int get numConnections => throw UnimplementedError(); } diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 9521b34..475a90a 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -232,6 +232,10 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { await connection.refreshSchema(); } } + + getNumConnections() { + return _allReadConnections.length + (_writeConnection == null ? 0 : 1); + } } typedef ReadCallback = Future Function(SqliteReadContext tx); diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart index b7ef76b..5b2f041 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart @@ -95,6 +95,7 @@ class SqliteConnectionImpl @override Future close() async { + print("Closing native sqlite Connection ${StackTrace.current}"); eventsPort?.close(); await _connectionMutex.lock(() async { if (readOnly) { 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 5cb60f3..b47a614 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 @@ -40,6 +40,9 @@ class SqliteDatabaseImpl late final SqliteConnectionImpl _internalConnection; late final SqliteConnectionPool _pool; + @override + int get numConnections => _pool.getNumConnections(); + final StreamController updatesController = StreamController.broadcast(); diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index b632aa7..03b8ec6 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -129,6 +129,9 @@ class WebDatabase } } } + + @override + int get numConnections => 0; } class _SharedContext implements SqliteReadContext { diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index 522b48e..90f0371 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -139,4 +139,7 @@ class SqliteDatabaseImpl await isInitialized; return _connection.getAutoCommit(); } + + @override + int get numConnections => 0; } From 476a958912c44ba70e163575be46e9b545ac5146 Mon Sep 17 00:00:00 2001 From: David Martos Date: Mon, 23 Sep 2024 20:21:21 +0200 Subject: [PATCH 02/16] Support passing `logStatements` to drift --- packages/drift_sqlite_async/lib/src/connection.dart | 4 ++-- packages/drift_sqlite_async/lib/src/executor.dart | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/drift_sqlite_async/lib/src/connection.dart b/packages/drift_sqlite_async/lib/src/connection.dart index e375795..a1af55a 100644 --- a/packages/drift_sqlite_async/lib/src/connection.dart +++ b/packages/drift_sqlite_async/lib/src/connection.dart @@ -15,8 +15,8 @@ import 'package:sqlite_async/sqlite_async.dart'; class SqliteAsyncDriftConnection extends DatabaseConnection { late StreamSubscription _updateSubscription; - SqliteAsyncDriftConnection(SqliteConnection db) - : super(SqliteAsyncQueryExecutor(db)) { + SqliteAsyncDriftConnection(SqliteConnection db, {bool logStatements = false}) + : super(SqliteAsyncQueryExecutor(db, logStatements: logStatements)) { _updateSubscription = (db as SqliteQueries).updates!.listen((event) { var setUpdates = {}; for (var tableName in event.tables) { diff --git a/packages/drift_sqlite_async/lib/src/executor.dart b/packages/drift_sqlite_async/lib/src/executor.dart index 91c6f7f..b68e2e6 100644 --- a/packages/drift_sqlite_async/lib/src/executor.dart +++ b/packages/drift_sqlite_async/lib/src/executor.dart @@ -127,10 +127,8 @@ class _SqliteAsyncVersionDelegate extends DynamicVersionDelegate { /// Extnral update notifications from the [SqliteConnection] are _not_ forwarded /// automatically - use [SqliteAsyncDriftConnection] for that. class SqliteAsyncQueryExecutor extends DelegatedDatabase { - SqliteAsyncQueryExecutor(SqliteConnection db) - : super( - _SqliteAsyncDelegate(db), - ); + SqliteAsyncQueryExecutor(SqliteConnection db, {bool logStatements = false}) + : super(_SqliteAsyncDelegate(db), logStatements: logStatements); /// The underlying SqliteConnection used by drift to send queries. SqliteConnection get db { From a95b30c35a1c41efa3970a8b32b679ee1ce90367 Mon Sep 17 00:00:00 2001 From: Jorge Sardina Date: Tue, 24 Sep 2024 11:46:11 +0200 Subject: [PATCH 03/16] Get all connections --- .../common/connection/sync_sqlite_connection.dart | 5 +++++ .../lib/src/common/sqlite_database.dart | 1 + .../lib/src/impl/stub_sqlite_database.dart | 10 ++++++++++ .../lib/src/native/database/connection_pool.dart | 13 ++++++++++++- .../database/native_sqlite_connection_impl.dart | 5 +++++ .../src/native/database/native_sqlite_database.dart | 10 ++++++++++ .../sqlite_async/lib/src/sqlite_connection.dart | 2 ++ packages/sqlite_async/lib/src/web/database.dart | 10 ++++++++++ .../lib/src/web/database/web_sqlite_database.dart | 10 ++++++++++ 9 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart index f29520f..697d7eb 100644 --- a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart @@ -50,6 +50,11 @@ class SyncSqliteConnection extends SqliteConnection with SqliteQueries { Future getAutoCommit() async { return db.autocommit; } + + @override + int getNumConnections() { + return -1; + } } class SyncReadContext implements SqliteReadContext { diff --git a/packages/sqlite_async/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart index f7bc933..1a2dfe2 100644 --- a/packages/sqlite_async/lib/src/common/sqlite_database.dart +++ b/packages/sqlite_async/lib/src/common/sqlite_database.dart @@ -51,6 +51,7 @@ abstract class SqliteDatabase static const int defaultMaxReaders = 5; int get numConnections; + List getAllConnections(); /// Open a SqliteDatabase. /// diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart index 89b9967..211f32b 100644 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart @@ -67,4 +67,14 @@ class SqliteDatabaseImpl @override int get numConnections => throw UnimplementedError(); + + @override + int getNumConnections() { + throw UnimplementedError(); + } + + @override + List getAllConnections() { + throw UnimplementedError(); + } } diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 475a90a..50d2dd5 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -233,7 +233,18 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { } } - getNumConnections() { + List getAllConnections() { + final connections = []; + if (_writeConnection != null) { + connections.add(_writeConnection!); + } + connections.addAll(_allReadConnections); + return connections; + } + + int getNumConnections() { + print( + "TESTING READ: ${_allReadConnections.length} WRITE: ${_writeConnection == null ? 0 : 1}"); return _allReadConnections.length + (_writeConnection == null ? 0 : 1); } } diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart index 5b2f041..1a15baa 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart @@ -159,6 +159,11 @@ class SqliteConnectionImpl }); }, timeout: lockTimeout); } + + @override + int getNumConnections() { + return -1; + } } int _nextCtxId = 1; 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 b47a614..365cc81 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 @@ -174,4 +174,14 @@ class SqliteDatabaseImpl Future refreshSchema() { return _pool.refreshSchema(); } + + @override + int getNumConnections() { + return -1; + } + + @override + List getAllConnections() { + return _pool.getAllConnections(); + } } diff --git a/packages/sqlite_async/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart index f1b721a..e10136d 100644 --- a/packages/sqlite_async/lib/src/sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/sqlite_connection.dart @@ -134,6 +134,8 @@ abstract class SqliteConnection extends SqliteWriteContext { /// Queries and watch calls can potentially use outdated schema information after a schema update. Future refreshSchema(); + int getNumConnections(); + /// Returns true if the connection is closed @override bool get closed; diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 03b8ec6..cbbb31e 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -132,6 +132,16 @@ class WebDatabase @override int get numConnections => 0; + + @override + int getNumConnections() { + return -1; + } + + @override + List getAllConnections() { + throw UnimplementedError(); + } } class _SharedContext implements SqliteReadContext { diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index 90f0371..f1989b4 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -142,4 +142,14 @@ class SqliteDatabaseImpl @override int get numConnections => 0; + + @override + int getNumConnections() { + return -1; + } + + @override + List getAllConnections() { + throw UnimplementedError(); + } } From c9f8fa25df6eb688ef87a1df886933b8a7e78248 Mon Sep 17 00:00:00 2001 From: David Martos Date: Fri, 4 Apr 2025 12:24:53 +0200 Subject: [PATCH 04/16] cleanup --- .DS_Store | Bin 6148 -> 0 bytes .../connection/sync_sqlite_connection.dart | 5 ----- .../lib/src/impl/single_connection_database.dart | 5 ----- .../lib/src/impl/stub_sqlite_database.dart | 5 ----- .../lib/src/native/database/connection_pool.dart | 4 ++-- .../database/native_sqlite_connection_impl.dart | 7 +------ .../native/database/native_sqlite_database.dart | 7 +------ .../sqlite_async/lib/src/sqlite_connection.dart | 2 -- packages/sqlite_async/lib/src/web/database.dart | 6 +----- .../src/web/database/web_sqlite_database.dart | 9 ++------- 10 files changed, 7 insertions(+), 43 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 getAutoCommit() async { return db.autocommit; } - - @override - int getNumConnections() { - return -1; - } } class SyncReadContext implements SqliteReadContext { 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 2364102..5645f02 100644 --- a/packages/sqlite_async/lib/src/impl/single_connection_database.dart +++ b/packages/sqlite_async/lib/src/impl/single_connection_database.dart @@ -63,11 +63,6 @@ final class SingleConnectionDatabase return [connection]; } - @override - int getNumConnections() { - return 1; - } - @override int get numConnections => 1; } diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart index 211f32b..0ea2312 100644 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart @@ -68,11 +68,6 @@ class SqliteDatabaseImpl @override int get numConnections => throw UnimplementedError(); - @override - int getNumConnections() { - throw UnimplementedError(); - } - @override List getAllConnections() { throw UnimplementedError(); diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 50d2dd5..01fd1af 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -243,8 +243,8 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { } int getNumConnections() { - print( - "TESTING READ: ${_allReadConnections.length} WRITE: ${_writeConnection == null ? 0 : 1}"); + // print( + // "TESTING READ: ${_allReadConnections.length} WRITE: ${_writeConnection == null ? 0 : 1}"); return _allReadConnections.length + (_writeConnection == null ? 0 : 1); } } diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart index 547a015..6249d52 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart @@ -95,7 +95,7 @@ class SqliteConnectionImpl @override Future close() async { - print("Closing native sqlite Connection ${StackTrace.current}"); + // print("Closing native sqlite Connection ${StackTrace.current}"); eventsPort?.close(); await _connectionMutex.lock(() async { if (readOnly) { @@ -159,11 +159,6 @@ class SqliteConnectionImpl }); }, timeout: lockTimeout); } - - @override - int getNumConnections() { - return -1; - } } int _nextCtxId = 1; 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 365cc81..06d730c 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 @@ -40,9 +40,6 @@ class SqliteDatabaseImpl late final SqliteConnectionImpl _internalConnection; late final SqliteConnectionPool _pool; - @override - int get numConnections => _pool.getNumConnections(); - final StreamController updatesController = StreamController.broadcast(); @@ -176,9 +173,7 @@ class SqliteDatabaseImpl } @override - int getNumConnections() { - return -1; - } + int get numConnections => _pool.getNumConnections(); @override List getAllConnections() { diff --git a/packages/sqlite_async/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart index 84d1233..15f4f6a 100644 --- a/packages/sqlite_async/lib/src/sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/sqlite_connection.dart @@ -157,8 +157,6 @@ abstract class SqliteConnection extends SqliteWriteContext { /// Queries and watch calls can potentially use outdated schema information after a schema update. Future refreshSchema(); - int getNumConnections(); - /// Returns true if the connection is closed @override bool get closed; diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index dfe9120..1901510 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -170,12 +170,8 @@ class WebDatabase return _database.fileSystem.flush(); } @override - int get numConnections => 0; + int get numConnections => throw UnimplementedError(); - @override - int getNumConnections() { - return -1; - } @override List getAllConnections() { diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index fda8510..b3e7ec0 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -181,16 +181,11 @@ class SqliteDatabaseImpl return await _connection.exposeEndpoint(); } @override - int get numConnections => 0; - - @override - int getNumConnections() { - return -1; - } + int get numConnections => 1; @override List getAllConnections() { - throw UnimplementedError(); + return [_connection]; } From 085caad8750724857c7f4e2f9654105ed4cc1973 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 22 May 2025 12:16:26 +0200 Subject: [PATCH 05/16] Prepare release of sqlite_async 0.11.5 --- CHANGELOG.md | 21 +++++++++++++++++++++ packages/sqlite_async/CHANGELOG.md | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aba9311..d23d6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2025-05-22 + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`sqlite_async` - `v0.11.5`](#sqlite_async---v0115) + +--- + +#### `sqlite_async` - `v0.11.5` + +- Allow profiling queries. Queries are profiled by default in debug and profile builds, the runtime + for queries is added to profiling timelines under the `sqlite_async` tag. +- Fix cancelling `watch()` queries sometimes taking longer than necessary. +- Fix web databases not respecting lock timeouts. + ## 2024-11-06 ### Changes diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index 42a5531..bf1fc83 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,6 +1,9 @@ ## 0.11.5 - - Allow profiling queries. +- Allow profiling queries. Queries are profiled by default in debug and profile builds, the runtime + for queries is added to profiling timelines under the `sqlite_async` tag. +- Fix cancelling `watch()` queries sometimes taking longer than necessary. +- Fix web databases not respecting lock timeouts. ## 0.11.4 From e11f506d058d59790fb04128a477ae36cd89a1b7 Mon Sep 17 00:00:00 2001 From: David Martos Date: Wed, 18 Jun 2025 10:45:02 +0200 Subject: [PATCH 06/16] cleanup --- packages/sqlite_async/lib/src/common/sqlite_database.dart | 8 +++++--- .../lib/src/impl/single_connection_database.dart | 3 --- .../sqlite_async/lib/src/impl/stub_sqlite_database.dart | 3 --- .../lib/src/native/database/native_sqlite_database.dart | 5 +---- packages/sqlite_async/lib/src/web/database.dart | 6 +----- .../lib/src/web/database/web_sqlite_database.dart | 4 ---- 6 files changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/sqlite_async/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart index 524b64a..49fe82d 100644 --- a/packages/sqlite_async/lib/src/common/sqlite_database.dart +++ b/packages/sqlite_async/lib/src/common/sqlite_database.dart @@ -51,9 +51,6 @@ abstract class SqliteDatabase /// The maximum number of concurrent read transactions if not explicitly specified. static const int defaultMaxReaders = 5; - int get numConnections; - List getAllConnections(); - /// Open a SqliteDatabase. /// /// Only a single SqliteDatabase per [path] should be opened at a time. @@ -106,4 +103,9 @@ abstract class SqliteDatabase factory SqliteDatabase.singleConnection(SqliteConnection connection) { return SingleConnectionDatabase(connection); } + + /// Returns a list of all the connections (read and write) managed by this database. + /// This can be useful to run the same statement on all connections. For instance, + /// ATTACHing a database, that is expected to be available in all connections. + List getAllConnections(); } 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 5645f02..955b6d5 100644 --- a/packages/sqlite_async/lib/src/impl/single_connection_database.dart +++ b/packages/sqlite_async/lib/src/impl/single_connection_database.dart @@ -62,7 +62,4 @@ final class SingleConnectionDatabase List getAllConnections() { return [connection]; } - - @override - int get numConnections => 1; } diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart index 0ea2312..264e85b 100644 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart @@ -65,9 +65,6 @@ class SqliteDatabaseImpl throw UnimplementedError(); } - @override - int get numConnections => throw UnimplementedError(); - @override List getAllConnections() { 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 06d730c..545dce7 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 @@ -171,10 +171,7 @@ class SqliteDatabaseImpl Future refreshSchema() { return _pool.refreshSchema(); } - - @override - int get numConnections => _pool.getNumConnections(); - + @override List getAllConnections() { return _pool.getAllConnections(); diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index d9c0207..07f997c 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -177,15 +177,11 @@ class WebDatabase await isInitialized; return _database.fileSystem.flush(); } - @override - int get numConnections => throw UnimplementedError(); - @override List getAllConnections() { - throw UnimplementedError(); + return [this]; } - } class _SharedContext implements SqliteReadContext { diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index b3e7ec0..daa7a10 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -180,13 +180,9 @@ class SqliteDatabaseImpl Future exposeEndpoint() async { return await _connection.exposeEndpoint(); } - @override - int get numConnections => 1; @override List getAllConnections() { return [_connection]; } - - } From a5c4f960e802e37867dd94fdf9cc8a8fd517b3c5 Mon Sep 17 00:00:00 2001 From: David Martos Date: Wed, 18 Jun 2025 10:59:24 +0200 Subject: [PATCH 07/16] cleanup --- .../lib/src/native/database/connection_pool.dart | 6 ------ .../src/native/database/native_sqlite_connection_impl.dart | 1 - 2 files changed, 7 deletions(-) diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 01fd1af..963c008 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -241,12 +241,6 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { connections.addAll(_allReadConnections); return connections; } - - int getNumConnections() { - // print( - // "TESTING READ: ${_allReadConnections.length} WRITE: ${_writeConnection == null ? 0 : 1}"); - return _allReadConnections.length + (_writeConnection == null ? 0 : 1); - } } typedef ReadCallback = Future Function(SqliteReadContext tx); diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart index 40299d9..e2df0f3 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart @@ -108,7 +108,6 @@ class SqliteConnectionImpl @override Future close() async { - // print("Closing native sqlite Connection ${StackTrace.current}"); eventsPort?.close(); await _connectionMutex.lock(() async { if (_didOpenSuccessfully) { From 082e7722c6aa23623be67803a83167f9eeb31ecf Mon Sep 17 00:00:00 2001 From: David Martos Date: Wed, 18 Jun 2025 12:14:23 +0200 Subject: [PATCH 08/16] use getter --- packages/sqlite_async/lib/src/common/sqlite_database.dart | 2 +- .../sqlite_async/lib/src/impl/single_connection_database.dart | 2 +- packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart | 2 +- .../sqlite_async/lib/src/native/database/connection_pool.dart | 2 +- .../lib/src/native/database/native_sqlite_database.dart | 4 ++-- packages/sqlite_async/lib/src/web/database.dart | 2 +- .../lib/src/web/database/web_sqlite_database.dart | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sqlite_async/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart index 49fe82d..1c66c6d 100644 --- a/packages/sqlite_async/lib/src/common/sqlite_database.dart +++ b/packages/sqlite_async/lib/src/common/sqlite_database.dart @@ -107,5 +107,5 @@ abstract class SqliteDatabase /// Returns a list of all the connections (read and write) managed by this database. /// This can be useful to run the same statement on all connections. For instance, /// ATTACHing a database, that is expected to be available in all connections. - List getAllConnections(); + List get allConnections; } 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 955b6d5..cd04f3f 100644 --- a/packages/sqlite_async/lib/src/impl/single_connection_database.dart +++ b/packages/sqlite_async/lib/src/impl/single_connection_database.dart @@ -59,7 +59,7 @@ final class SingleConnectionDatabase } @override - List getAllConnections() { + List get allConnections { return [connection]; } } diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart index 264e85b..15d395a 100644 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart @@ -66,7 +66,7 @@ class SqliteDatabaseImpl } @override - List getAllConnections() { + List get allConnections { throw UnimplementedError(); } } diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 963c008..859c4cf 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -233,7 +233,7 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { } } - List getAllConnections() { + List get allConnections { final connections = []; if (_writeConnection != null) { connections.add(_writeConnection!); 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 b718627..64974b3 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 @@ -173,7 +173,7 @@ class SqliteDatabaseImpl } @override - List getAllConnections() { - return _pool.getAllConnections(); + List get allConnections { + return _pool.allConnections; } } diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 508aefb..b52a199 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -173,7 +173,7 @@ class WebDatabase } @override - List getAllConnections() { + List get allConnections { return [this]; } } diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index 78e1363..7e1c8f9 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -180,7 +180,7 @@ class SqliteDatabaseImpl } @override - List getAllConnections() { + List get allConnections { return [_connection]; } } From ea41b34df4d56ee6df6ebc0be48034fa243aad1e Mon Sep 17 00:00:00 2001 From: David Martos Date: Thu, 25 Sep 2025 16:41:26 +0200 Subject: [PATCH 09/16] wip --- .../src/native/database/connection_pool.dart | 49 +++++++++++++++++++ .../sqlite_async/test/native/basic_test.dart | 30 +++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 859c4cf..5a45f5a 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -241,6 +241,55 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { connections.addAll(_allReadConnections); return connections; } + + Future withAllConnections( + Future Function( + SqliteWriteContext writer, List readers) + block) async { + final blockCompleter = Completer(); + final (write, reads) = await _lockAllConns(blockCompleter); + + try { + final res = await block(write, reads); + blockCompleter.complete(res); + return res; + } catch (e, st) { + blockCompleter.completeError(e, st); + rethrow; + } + } + + /// Locks all connections, returning the acquired contexts. + /// We pass a completer that would be called after the locks are taken. + Future<(SqliteWriteContext, List)> _lockAllConns( + Completer lockCompleter) async { + final List> readLockedCompleters = []; + final Completer writeLockedCompleter = Completer(); + + // Take the write lock + writeLock((ctx) { + writeLockedCompleter.complete(ctx); + return lockCompleter.future; + }); + + // Take all the read locks + for (final readConn in _allReadConnections) { + final completer = Completer(); + readLockedCompleters.add(completer); + + readConn.readLock((ctx) { + completer.complete(ctx); + return lockCompleter.future; + }); + } + + // Wait after all locks are taken + final contexts = await Future.wait([ + writeLockedCompleter.future, + ...readLockedCompleters.map((e) => e.future) + ]); + return (contexts.first as SqliteWriteContext, contexts.sublist(1)); + } } typedef ReadCallback = Future Function(SqliteReadContext tx); diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index dec1fed..0a94705 100644 --- a/packages/sqlite_async/test/native/basic_test.dart +++ b/packages/sqlite_async/test/native/basic_test.dart @@ -69,7 +69,35 @@ void main() { } }); - test('Concurrency 2', () async { + test('with all connections', () async { + final db = SqliteDatabase.withFactory( + await testUtils.testFactory(path: path), + maxReaders: 3); + await db.initialize(); + await createTables(db); + + print("${DateTime.now()} start"); + + final withAllConnsFut = () async { + await Future.delayed(const Duration(milliseconds: 20)); + await db.withAllConnections((writer, readers) async { + print("${DateTime.now()} in withAllConnections"); + await Future.delayed(const Duration(seconds: 5)); + }); + }(); + + var readFutures = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => db.get( + 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', + [i, 5 + Random().nextInt(10)])); + + final futures = [...readFutures, withAllConnsFut]; + + await for (var result in Stream.fromFutures(futures)) { + print("${DateTime.now()} $result"); + } + }); + + test('Concurren 2', () async { final db1 = await testUtils.setupDatabase(path: path, maxReaders: 3); final db2 = await testUtils.setupDatabase(path: path, maxReaders: 3); From fe0b71d55b0b98779f7c89b0d4e84372004afa40 Mon Sep 17 00:00:00 2001 From: David Martos Date: Thu, 25 Sep 2025 17:24:41 +0200 Subject: [PATCH 10/16] test --- .../lib/src/common/sqlite_database.dart | 6 ++ .../src/impl/single_connection_database.dart | 5 ++ .../lib/src/impl/stub_sqlite_database.dart | 5 ++ .../src/native/database/connection_pool.dart | 5 +- .../database/native_sqlite_database.dart | 5 ++ .../sqlite_async/lib/src/web/database.dart | 8 +++ .../src/web/database/web_sqlite_database.dart | 8 +++ .../sqlite_async/test/native/basic_test.dart | 63 ++++++++++++++----- 8 files changed, 88 insertions(+), 17 deletions(-) diff --git a/packages/sqlite_async/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart index 1c66c6d..2b2ad6b 100644 --- a/packages/sqlite_async/lib/src/common/sqlite_database.dart +++ b/packages/sqlite_async/lib/src/common/sqlite_database.dart @@ -39,6 +39,12 @@ mixin SqliteDatabaseMixin implements SqliteConnection, SqliteQueries { /// /// Use this to access the database in background isolates. IsolateConnectionFactory isolateConnectionFactory(); + + /// Locks all underlying connections making up this database, and gives [block] access to all of them at once. + Future withAllConnections( + Future Function( + SqliteWriteContext writer, List readers) + block); } /// A SQLite database instance. 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 cd04f3f..d512cde 100644 --- a/packages/sqlite_async/lib/src/impl/single_connection_database.dart +++ b/packages/sqlite_async/lib/src/impl/single_connection_database.dart @@ -62,4 +62,9 @@ final class SingleConnectionDatabase List get allConnections { return [connection]; } + + @override + Future withAllConnections(Future Function(SqliteWriteContext writer, List readers) block) { + return writeLock((_) => block(connection, [])); + } } diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart index 15d395a..a869ac5 100644 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart @@ -69,4 +69,9 @@ class SqliteDatabaseImpl List get allConnections { throw UnimplementedError(); } + + @override + Future withAllConnections(Future Function(SqliteWriteContext writer, List readers) block) { + throw UnimplementedError(); + } } diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 5a45f5a..6159506 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -284,11 +284,12 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { } // Wait after all locks are taken - final contexts = await Future.wait([ + final [writer as SqliteWriteContext, ...readers] = await Future.wait([ writeLockedCompleter.future, ...readLockedCompleters.map((e) => e.future) ]); - return (contexts.first as SqliteWriteContext, contexts.sublist(1)); + + return (writer, readers); } } 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 64974b3..a5823dd 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 @@ -176,4 +176,9 @@ class SqliteDatabaseImpl List get allConnections { return _pool.allConnections; } + + @override + Future withAllConnections(Future Function(SqliteWriteContext writer, List readers) block) { + return _pool.withAllConnections(block); + } } diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index cdfc1c3..c97c9d8 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -176,6 +176,14 @@ class WebDatabase List get allConnections { return [this]; } + + @override + Future withAllConnections( + Future Function( + SqliteWriteContext writer, List readers) + block) { + return writeLock((_) => block(this, [])); + } } final class _UnscopedContext extends UnscopedContext { diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index 7e1c8f9..e4d94ad 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -183,4 +183,12 @@ class SqliteDatabaseImpl List get allConnections { return [_connection]; } + + @override + Future withAllConnections( + Future Function( + SqliteWriteContext writer, List readers) + block) { + return writeLock((_) => block(_connection, [])); + } } diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index 0a94705..34aced4 100644 --- a/packages/sqlite_async/test/native/basic_test.dart +++ b/packages/sqlite_async/test/native/basic_test.dart @@ -4,7 +4,9 @@ library; import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:sqlite3/common.dart' as sqlite; +import 'package:sqlite3/sqlite3.dart' show Row; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; @@ -76,25 +78,56 @@ void main() { await db.initialize(); await createTables(db); - print("${DateTime.now()} start"); - final withAllConnsFut = () async { - await Future.delayed(const Duration(milliseconds: 20)); - await db.withAllConnections((writer, readers) async { - print("${DateTime.now()} in withAllConnections"); - await Future.delayed(const Duration(seconds: 5)); - }); - }(); + Future readWithRandomDelay(SqliteReadContext ctx, int id) async { + return await ctx.get( + 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', + [id, 5 + Random().nextInt(10)]); + } - var readFutures = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => db.get( - 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', - [i, 5 + Random().nextInt(10)])); + // Warm up to spawn the max readers + await Future.wait( + [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readWithRandomDelay(db, i)), + ); - final futures = [...readFutures, withAllConnsFut]; + bool finishedWithAllConns = false; - await for (var result in Stream.fromFutures(futures)) { - print("${DateTime.now()} $result"); - } + late Future readsCalledWhileWithAllConnsRunning; + + print("${DateTime.now()} start"); + await db.withAllConnections((writer, readers) async { + assert(readers.length == 3); + + // Run some reads during the block that they should run after the block finishes and releases + // all locks + readsCalledWhileWithAllConnsRunning = Future.wait( + [1, 2, 3, 4, 5, 6, 7, 8].map((i) async { + final r = await db.readLock((c) async { + expect(finishedWithAllConns, isTrue); + return await readWithRandomDelay(c, i); + }); + print( + "${DateTime.now()} After withAllConnections, started while running $r"); + }), + ); + + await Future.wait([ + writer.execute( + "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *", + [ + 123, + 5 + Random().nextInt(20) + ]).then((value) => + print("${DateTime.now()} withAllConnections writer done $value")), + ...readers + .mapIndexed((i, r) => readWithRandomDelay(r, i).then((results) { + print( + "${DateTime.now()} withAllConnections readers done $results"); + })) + ]); + }).then((_) => finishedWithAllConns = true); + + await readsCalledWhileWithAllConnsRunning; }); test('Concurren 2', () async { From 384f2a7239c5d4bd42a2350e5a3e2f8df24b657e Mon Sep 17 00:00:00 2001 From: David Martos Date: Thu, 25 Sep 2025 17:24:52 +0200 Subject: [PATCH 11/16] remove getter --- .../sqlite_async/lib/src/common/sqlite_database.dart | 7 ++----- .../lib/src/impl/single_connection_database.dart | 10 ++++------ .../lib/src/impl/stub_sqlite_database.dart | 10 ++++------ .../lib/src/native/database/connection_pool.dart | 9 --------- .../src/native/database/native_sqlite_database.dart | 12 +++++------- packages/sqlite_async/lib/src/web/database.dart | 5 ----- .../lib/src/web/database/web_sqlite_database.dart | 5 ----- packages/sqlite_async/test/native/basic_test.dart | 1 - 8 files changed, 15 insertions(+), 44 deletions(-) diff --git a/packages/sqlite_async/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart index 2b2ad6b..3201135 100644 --- a/packages/sqlite_async/lib/src/common/sqlite_database.dart +++ b/packages/sqlite_async/lib/src/common/sqlite_database.dart @@ -41,6 +41,8 @@ mixin SqliteDatabaseMixin implements SqliteConnection, SqliteQueries { IsolateConnectionFactory isolateConnectionFactory(); /// Locks all underlying connections making up this database, and gives [block] access to all of them at once. + /// This can be useful to run the same statement on all connections. For instance, + /// ATTACHing a database, that is expected to be available in all connections. Future withAllConnections( Future Function( SqliteWriteContext writer, List readers) @@ -109,9 +111,4 @@ abstract class SqliteDatabase factory SqliteDatabase.singleConnection(SqliteConnection connection) { return SingleConnectionDatabase(connection); } - - /// Returns a list of all the connections (read and write) managed by this database. - /// This can be useful to run the same statement on all connections. For instance, - /// ATTACHing a database, that is expected to be available in all connections. - List get allConnections; } 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 d512cde..7ca4357 100644 --- a/packages/sqlite_async/lib/src/impl/single_connection_database.dart +++ b/packages/sqlite_async/lib/src/impl/single_connection_database.dart @@ -59,12 +59,10 @@ final class SingleConnectionDatabase } @override - List get allConnections { - return [connection]; - } - - @override - Future withAllConnections(Future Function(SqliteWriteContext writer, List readers) block) { + Future withAllConnections( + Future Function( + SqliteWriteContext writer, List readers) + block) { return writeLock((_) => block(connection, [])); } } diff --git a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart index a869ac5..ee254f3 100644 --- a/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart @@ -66,12 +66,10 @@ class SqliteDatabaseImpl } @override - List get allConnections { - throw UnimplementedError(); - } - - @override - Future withAllConnections(Future Function(SqliteWriteContext writer, List readers) block) { + Future withAllConnections( + Future Function( + SqliteWriteContext writer, List readers) + block) { throw UnimplementedError(); } } diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 6159506..a3576e8 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -233,15 +233,6 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { } } - List get allConnections { - final connections = []; - if (_writeConnection != null) { - connections.add(_writeConnection!); - } - connections.addAll(_allReadConnections); - return connections; - } - Future withAllConnections( Future Function( SqliteWriteContext writer, List readers) 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 a5823dd..22cacf3 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 @@ -171,14 +171,12 @@ class SqliteDatabaseImpl Future refreshSchema() { return _pool.refreshSchema(); } - - @override - List get allConnections { - return _pool.allConnections; - } - + @override - Future withAllConnections(Future Function(SqliteWriteContext writer, List readers) block) { + Future withAllConnections( + Future Function( + SqliteWriteContext writer, List readers) + block) { return _pool.withAllConnections(block); } } diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index c97c9d8..f2dc998 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -172,11 +172,6 @@ class WebDatabase return _database.fileSystem.flush(); } - @override - List get allConnections { - return [this]; - } - @override Future withAllConnections( Future Function( diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index e4d94ad..69f01ab 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -179,11 +179,6 @@ class SqliteDatabaseImpl return await _connection.exposeEndpoint(); } - @override - List get allConnections { - return [_connection]; - } - @override Future withAllConnections( Future Function( diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index 34aced4..cb2a907 100644 --- a/packages/sqlite_async/test/native/basic_test.dart +++ b/packages/sqlite_async/test/native/basic_test.dart @@ -78,7 +78,6 @@ void main() { await db.initialize(); await createTables(db); - Future readWithRandomDelay(SqliteReadContext ctx, int id) async { return await ctx.get( 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', From c9830f6e55cc71051b22c870ed5eb82ccd551579 Mon Sep 17 00:00:00 2001 From: David Martos Date: Thu, 25 Sep 2025 17:26:24 +0200 Subject: [PATCH 12/16] move test --- .../sqlite_async/test/native/basic_test.dart | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index cb2a907..72b3363 100644 --- a/packages/sqlite_async/test/native/basic_test.dart +++ b/packages/sqlite_async/test/native/basic_test.dart @@ -71,6 +71,37 @@ void main() { } }); + test('Concurrency 2', () async { + final db1 = await testUtils.setupDatabase(path: path, maxReaders: 3); + final db2 = await testUtils.setupDatabase(path: path, maxReaders: 3); + + await db1.initialize(); + await createTables(db1); + await db2.initialize(); + print("${DateTime.now()} start"); + + var futures1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) { + return db1.execute( + "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *", + [ + i, + 5 + Random().nextInt(20) + ]).then((value) => print("${DateTime.now()} $value")); + }).toList(); + + var futures2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) { + return db2.execute( + "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 2 ' || datetime() as connection RETURNING *", + [ + i, + 5 + Random().nextInt(20) + ]).then((value) => print("${DateTime.now()} $value")); + }).toList(); + await Future.wait(futures1); + await Future.wait(futures2); + print("${DateTime.now()} done"); + }); + test('with all connections', () async { final db = SqliteDatabase.withFactory( await testUtils.testFactory(path: path), @@ -129,37 +160,6 @@ void main() { await readsCalledWhileWithAllConnsRunning; }); - test('Concurren 2', () async { - final db1 = await testUtils.setupDatabase(path: path, maxReaders: 3); - final db2 = await testUtils.setupDatabase(path: path, maxReaders: 3); - - await db1.initialize(); - await createTables(db1); - await db2.initialize(); - print("${DateTime.now()} start"); - - var futures1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) { - return db1.execute( - "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *", - [ - i, - 5 + Random().nextInt(20) - ]).then((value) => print("${DateTime.now()} $value")); - }).toList(); - - var futures2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) { - return db2.execute( - "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 2 ' || datetime() as connection RETURNING *", - [ - i, - 5 + Random().nextInt(20) - ]).then((value) => print("${DateTime.now()} $value")); - }).toList(); - await Future.wait(futures1); - await Future.wait(futures2); - print("${DateTime.now()} done"); - }); - test('read-only transactions', () async { final db = await testUtils.setupDatabase(path: path); await createTables(db); From dfb1dbe4db2623c470618b6f178e7fb593613209 Mon Sep 17 00:00:00 2001 From: David Martos Date: Thu, 25 Sep 2025 19:14:01 +0200 Subject: [PATCH 13/16] prevent readers from spawning while running withAllConnections --- .../src/native/database/connection_pool.dart | 25 ++++- .../sqlite_async/test/native/basic_test.dart | 91 ++++++++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index a3576e8..2c6cfca 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -31,6 +31,8 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { final MutexImpl mutex; + bool _runningWithAllConnections = false; + @override bool closed = false; @@ -88,6 +90,11 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { return; } + if (_availableReadConnections.isEmpty && _runningWithAllConnections) { + // Wait until withAllConnections is done + return; + } + var nextItem = _queue.removeFirst(); while (nextItem.completer.isCompleted) { // This item already timed out - try the next one if available @@ -237,16 +244,26 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { Future Function( SqliteWriteContext writer, List readers) block) async { - final blockCompleter = Completer(); - final (write, reads) = await _lockAllConns(blockCompleter); + try { + _runningWithAllConnections = true; + + final blockCompleter = Completer(); + final (write, reads) = await _lockAllConns(blockCompleter); try { final res = await block(write, reads); blockCompleter.complete(res); return res; } catch (e, st) { - blockCompleter.completeError(e, st); - rethrow; + blockCompleter.completeError(e, st); + rethrow; + } + } finally { + _runningWithAllConnections = false; + + // Continue processing any pending read requests that may have been queued while + // the block was running. + Timer.run(_nextRead); } } diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index 72b3363..7456d76 100644 --- a/packages/sqlite_async/test/native/basic_test.dart +++ b/packages/sqlite_async/test/native/basic_test.dart @@ -2,14 +2,17 @@ library; import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'package:collection/collection.dart'; +import 'package:path/path.dart' show join; import 'package:sqlite3/common.dart' as sqlite; import 'package:sqlite3/sqlite3.dart' show Row; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; +import '../utils/abstract_test_utils.dart'; import '../utils/test_utils_impl.dart'; final testUtils = TestUtils(); @@ -126,7 +129,7 @@ void main() { print("${DateTime.now()} start"); await db.withAllConnections((writer, readers) async { - assert(readers.length == 3); + expect(readers.length, 3); // Run some reads during the block that they should run after the block finishes and releases // all locks @@ -160,6 +163,64 @@ void main() { await readsCalledWhileWithAllConnsRunning; }); + test('prevent opening new readers while in withAllConnections', () async { + final sharedStateDir = Directory.systemTemp.createTempSync(); + addTearDown(() => sharedStateDir.deleteSync(recursive: true)); + + final File sharedStateFile = + File(join(sharedStateDir.path, 'shared-state.txt')); + + sharedStateFile.writeAsStringSync('initial'); + + final db = SqliteDatabase.withFactory( + _TestSqliteOpenFactoryWithSharedStateFile( + path: path, sharedStateFilePath: sharedStateFile.path), + maxReaders: 3); + await db.initialize(); + await createTables(db); + + // The writer saw 'initial' in the file when opening the connection + expect( + await db + .writeLock((c) => c.get('SELECT file_contents_on_open() AS state')), + {'state': 'initial'}, + ); + + final withAllConnectionsCompleter = Completer(); + + final withAllConnsFut = db.withAllConnections((writer, readers) async { + expect(readers.length, 0); // No readers yet + + // Simulate some work until the file is updated + await Future.delayed(const Duration(milliseconds: 200)); + sharedStateFile.writeAsStringSync('updated'); + + await withAllConnectionsCompleter.future; + }); + + // Start a reader that gets the contents of the shared file + bool readFinished = false; + final someReadFut = + db.get('SELECT file_contents_on_open() AS state', []).then((r) { + readFinished = true; + return r; + }); + + // The withAllConnections should prevent the reader from opening + await Future.delayed(const Duration(milliseconds: 100)); + expect(readFinished, isFalse); + + // Free all the locks + withAllConnectionsCompleter.complete(); + await withAllConnsFut; + + final readerInfo = await someReadFut; + expect(readFinished, isTrue); + // The read should see the updated value in the file. This checks + // that a reader doesn't spawn while running withAllConnections + expect(readerInfo, {'state': 'updated'}); + }); + test('read-only transactions', () async { final db = await testUtils.setupDatabase(path: path); await createTables(db); @@ -439,3 +500,31 @@ class _InvalidPragmaOnOpenFactory extends DefaultSqliteOpenFactory { ]; } } + +class _TestSqliteOpenFactoryWithSharedStateFile + extends TestDefaultSqliteOpenFactory { + final String sharedStateFilePath; + + _TestSqliteOpenFactoryWithSharedStateFile( + {required super.path, required this.sharedStateFilePath}); + + @override + sqlite.CommonDatabase open(SqliteOpenOptions options) { + final File sharedStateFile = File(sharedStateFilePath); + final String sharedState = sharedStateFile.readAsStringSync(); + + final db = super.open(options); + + // Function to return the contents of the shared state file at the time of opening + // so that we know at which point the factory was called. + db.createFunction( + functionName: 'file_contents_on_open', + argumentCount: const sqlite.AllowedArgumentCount(0), + function: (args) { + return sharedState; + }, + ); + + return db; + } +} From b119b13c95ccfe0f7504041e0e9af491455fdd0f Mon Sep 17 00:00:00 2001 From: David Martos Date: Tue, 30 Sep 2025 10:37:52 +0200 Subject: [PATCH 14/16] fix withAllConnections race condition --- .../src/native/database/connection_pool.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 2c6cfca..1ad6a3e 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -31,7 +31,7 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { final MutexImpl mutex; - bool _runningWithAllConnections = false; + int _runningWithAllConnectionsCount = 0; @override bool closed = false; @@ -90,7 +90,8 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { return; } - if (_availableReadConnections.isEmpty && _runningWithAllConnections) { + if (_availableReadConnections.isEmpty && + _runningWithAllConnectionsCount > 0) { // Wait until withAllConnections is done return; } @@ -245,21 +246,21 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { SqliteWriteContext writer, List readers) block) async { try { - _runningWithAllConnections = true; + _runningWithAllConnectionsCount++; final blockCompleter = Completer(); final (write, reads) = await _lockAllConns(blockCompleter); - try { - final res = await block(write, reads); - blockCompleter.complete(res); - return res; - } catch (e, st) { + try { + final res = await block(write, reads); + blockCompleter.complete(res); + return res; + } catch (e, st) { blockCompleter.completeError(e, st); rethrow; } } finally { - _runningWithAllConnections = false; + _runningWithAllConnectionsCount--; // Continue processing any pending read requests that may have been queued while // the block was running. From ab861ec05891d5815c1fe991b1d07f99c2a212db Mon Sep 17 00:00:00 2001 From: David Martos Date: Tue, 30 Sep 2025 11:21:19 +0200 Subject: [PATCH 15/16] review comments --- .../src/native/database/connection_pool.dart | 4 +- packages/sqlite_async/test/basic_test.dart | 64 +++++++++++++++++++ .../sqlite_async/test/native/basic_test.dart | 60 ----------------- 3 files changed, 67 insertions(+), 61 deletions(-) diff --git a/packages/sqlite_async/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart index 1ad6a3e..8dab27e 100644 --- a/packages/sqlite_async/lib/src/native/database/connection_pool.dart +++ b/packages/sqlite_async/lib/src/native/database/connection_pool.dart @@ -92,7 +92,9 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection { if (_availableReadConnections.isEmpty && _runningWithAllConnectionsCount > 0) { - // Wait until withAllConnections is done + // Wait until [withAllConnections] is done. Otherwise we could spawn a new + // reader while the user is configuring all the connections, + // e.g. a global open factory configuration shared across all connections. return; } diff --git a/packages/sqlite_async/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart index 6a315da..abe54a3 100644 --- a/packages/sqlite_async/test/basic_test.dart +++ b/packages/sqlite_async/test/basic_test.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; @@ -301,6 +303,68 @@ void main() { 'Web locks are managed with a shared worker, which does not support timeouts', ) }); + + test('with all connections', () async { + // TODO: Is this right? + final maxReaders = _isDart2Wasm ? 0 : 3; + + final db = SqliteDatabase.withFactory( + await testUtils.testFactory(path: path), + maxReaders: maxReaders, + ); + await db.initialize(); + await createTables(db); + + Future readWithRandomDelay(SqliteReadContext ctx, int id) async { + return await ctx.get( + 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', + [id, 5 + Random().nextInt(10)]); + } + + // Warm up to spawn the max readers + await Future.wait( + [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readWithRandomDelay(db, i)), + ); + + bool finishedWithAllConns = false; + + late Future readsCalledWhileWithAllConnsRunning; + + print("${DateTime.now()} start"); + await db.withAllConnections((writer, readers) async { + expect(readers.length, maxReaders); + + // Run some reads during the block that they should run after the block finishes and releases + // all locks + readsCalledWhileWithAllConnsRunning = Future.wait( + [1, 2, 3, 4, 5, 6, 7, 8].map((i) async { + final r = await db.readLock((c) async { + expect(finishedWithAllConns, isTrue); + return await readWithRandomDelay(c, i); + }); + print( + "${DateTime.now()} After withAllConnections, started while running $r"); + }), + ); + + await Future.wait([ + writer.execute( + "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *", + [ + 123, + 5 + Random().nextInt(20) + ]).then((value) => + print("${DateTime.now()} withAllConnections writer done $value")), + ...readers + .mapIndexed((i, r) => readWithRandomDelay(r, i).then((results) { + print( + "${DateTime.now()} withAllConnections readers done $results"); + })) + ]); + }).then((_) => finishedWithAllConns = true); + + await readsCalledWhileWithAllConnsRunning; + }); }); } diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index 7456d76..c580306 100644 --- a/packages/sqlite_async/test/native/basic_test.dart +++ b/packages/sqlite_async/test/native/basic_test.dart @@ -5,10 +5,8 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:path/path.dart' show join; import 'package:sqlite3/common.dart' as sqlite; -import 'package:sqlite3/sqlite3.dart' show Row; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; @@ -105,64 +103,6 @@ void main() { print("${DateTime.now()} done"); }); - test('with all connections', () async { - final db = SqliteDatabase.withFactory( - await testUtils.testFactory(path: path), - maxReaders: 3); - await db.initialize(); - await createTables(db); - - Future readWithRandomDelay(SqliteReadContext ctx, int id) async { - return await ctx.get( - 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', - [id, 5 + Random().nextInt(10)]); - } - - // Warm up to spawn the max readers - await Future.wait( - [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readWithRandomDelay(db, i)), - ); - - bool finishedWithAllConns = false; - - late Future readsCalledWhileWithAllConnsRunning; - - print("${DateTime.now()} start"); - await db.withAllConnections((writer, readers) async { - expect(readers.length, 3); - - // Run some reads during the block that they should run after the block finishes and releases - // all locks - readsCalledWhileWithAllConnsRunning = Future.wait( - [1, 2, 3, 4, 5, 6, 7, 8].map((i) async { - final r = await db.readLock((c) async { - expect(finishedWithAllConns, isTrue); - return await readWithRandomDelay(c, i); - }); - print( - "${DateTime.now()} After withAllConnections, started while running $r"); - }), - ); - - await Future.wait([ - writer.execute( - "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *", - [ - 123, - 5 + Random().nextInt(20) - ]).then((value) => - print("${DateTime.now()} withAllConnections writer done $value")), - ...readers - .mapIndexed((i, r) => readWithRandomDelay(r, i).then((results) { - print( - "${DateTime.now()} withAllConnections readers done $results"); - })) - ]); - }).then((_) => finishedWithAllConns = true); - - await readsCalledWhileWithAllConnsRunning; - }); - test('prevent opening new readers while in withAllConnections', () async { final sharedStateDir = Directory.systemTemp.createTempSync(); addTearDown(() => sharedStateDir.deleteSync(recursive: true)); From 1d33dc84ca24ffea03df9577944f14e080265c23 Mon Sep 17 00:00:00 2001 From: Jorge Sardina Date: Tue, 7 Oct 2025 16:55:59 +0200 Subject: [PATCH 16/16] add tests --- packages/sqlite_async/test/basic_test.dart | 60 ++++++------------ .../sqlite_async/test/native/basic_test.dart | 63 +++++++++++++++++++ 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/packages/sqlite_async/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart index abe54a3..e2914b3 100644 --- a/packages/sqlite_async/test/basic_test.dart +++ b/packages/sqlite_async/test/basic_test.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:sqlite_async/sqlite3_common.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; @@ -9,6 +7,7 @@ import 'utils/test_utils_impl.dart'; final testUtils = TestUtils(); const _isDart2Wasm = bool.fromEnvironment('dart.tool.dart2wasm'); +const _isWeb = identical(0, 0.0) || _isDart2Wasm; void main() { group('Shared Basic Tests', () { @@ -305,8 +304,7 @@ void main() { }); test('with all connections', () async { - // TODO: Is this right? - final maxReaders = _isDart2Wasm ? 0 : 3; + final maxReaders = _isWeb ? 0 : 3; final db = SqliteDatabase.withFactory( await testUtils.testFactory(path: path), @@ -315,53 +313,35 @@ void main() { await db.initialize(); await createTables(db); - Future readWithRandomDelay(SqliteReadContext ctx, int id) async { - return await ctx.get( - 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', - [id, 5 + Random().nextInt(10)]); - } - // Warm up to spawn the max readers - await Future.wait( - [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readWithRandomDelay(db, i)), - ); + await Future.wait([for (var i = 0; i < 10; i++) db.get('SELECT $i')]); bool finishedWithAllConns = false; late Future readsCalledWhileWithAllConnsRunning; - print("${DateTime.now()} start"); + final parentZone = Zone.current; await db.withAllConnections((writer, readers) async { expect(readers.length, maxReaders); // Run some reads during the block that they should run after the block finishes and releases // all locks - readsCalledWhileWithAllConnsRunning = Future.wait( - [1, 2, 3, 4, 5, 6, 7, 8].map((i) async { - final r = await db.readLock((c) async { - expect(finishedWithAllConns, isTrue); - return await readWithRandomDelay(c, i); - }); - print( - "${DateTime.now()} After withAllConnections, started while running $r"); - }), - ); - - await Future.wait([ - writer.execute( - "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *", - [ - 123, - 5 + Random().nextInt(20) - ]).then((value) => - print("${DateTime.now()} withAllConnections writer done $value")), - ...readers - .mapIndexed((i, r) => readWithRandomDelay(r, i).then((results) { - print( - "${DateTime.now()} withAllConnections readers done $results"); - })) - ]); - }).then((_) => finishedWithAllConns = true); + // Need a root zone here to avoid recursive lock errors. + readsCalledWhileWithAllConnsRunning = + Future(parentZone.bindCallback(() async { + await Future.wait( + [1, 2, 3, 4, 5, 6, 7, 8].map((i) async { + await db.readLock((c) async { + expect(finishedWithAllConns, isTrue); + await Future.delayed(const Duration(milliseconds: 100)); + }); + }), + ); + })); + + await Future.delayed(const Duration(milliseconds: 200)); + finishedWithAllConns = true; + }); await readsCalledWhileWithAllConnsRunning; }); diff --git a/packages/sqlite_async/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart index c580306..3f348e6 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 'dart:io'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:path/path.dart' show join; import 'package:sqlite3/common.dart' as sqlite; import 'package:sqlite_async/sqlite_async.dart'; @@ -161,6 +162,68 @@ void main() { expect(readerInfo, {'state': 'updated'}); }); + test('with all connections', () async { + final maxReaders = 3; + + final db = SqliteDatabase.withFactory( + await testUtils.testFactory(path: path), + maxReaders: maxReaders, + ); + await db.initialize(); + await createTables(db); + + Future readWithRandomDelay( + SqliteReadContext ctx, int id) async { + return await ctx.get( + 'SELECT ? as i, test_sleep(?) as sleep, test_connection_name() as connection', + [id, 5 + Random().nextInt(10)]); + } + + // Warm up to spawn the max readers + await Future.wait( + [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readWithRandomDelay(db, i)), + ); + + bool finishedWithAllConns = false; + + late Future readsCalledWhileWithAllConnsRunning; + + print("${DateTime.now()} start"); + await db.withAllConnections((writer, readers) async { + expect(readers.length, maxReaders); + + // Run some reads during the block that they should run after the block finishes and releases + // all locks + readsCalledWhileWithAllConnsRunning = Future.wait( + [1, 2, 3, 4, 5, 6, 7, 8].map((i) async { + final r = await db.readLock((c) async { + expect(finishedWithAllConns, isTrue); + return await readWithRandomDelay(c, i); + }); + print( + "${DateTime.now()} After withAllConnections, started while running $r"); + }), + ); + + await Future.wait([ + writer.execute( + "INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *", + [ + 123, + 5 + Random().nextInt(20) + ]).then((value) => + print("${DateTime.now()} withAllConnections writer done $value")), + ...readers + .mapIndexed((i, r) => readWithRandomDelay(r, i).then((results) { + print( + "${DateTime.now()} withAllConnections readers done $results"); + })) + ]); + }).then((_) => finishedWithAllConns = true); + + await readsCalledWhileWithAllConnsRunning; + }); + test('read-only transactions', () async { final db = await testUtils.setupDatabase(path: path); await createTables(db);