From 7cb433d1650b2f93e5b686afb434dd58fbdc9cb7 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Thu, 4 Jul 2024 18:04:36 +0200 Subject: [PATCH 01/17] move sqlite_async to packages --- .../sqlite_async/CHANGELOG.md | 0 packages/sqlite_async/LICENSE | 19 +++++++++++++++++++ README.md => packages/sqlite_async/README.md | 0 .../sqlite_async/example}/README.md | 0 .../sqlite_async/example}/basic_example.dart | 0 .../example}/custom_functions_example.dart | 0 .../sqlite_async/example}/json_example.dart | 0 .../example}/linux_cli_example.dart | 0 .../example}/migration_example.dart | 0 .../sqlite_async/example}/watch_example.dart | 0 {lib => packages/sqlite_async/lib}/mutex.dart | 0 .../sqlite_async/lib}/sqlite3.dart | 0 .../sqlite_async/lib}/sqlite3_common.dart | 0 .../sqlite_async/lib}/sqlite3_wasm.dart | 0 .../sqlite_async/lib}/sqlite3_web.dart | 0 .../sqlite_async/lib}/sqlite3_web_worker.dart | 0 .../sqlite_async/lib}/sqlite_async.dart | 0 .../src/common/abstract_open_factory.dart | 0 .../connection/sync_sqlite_connection.dart | 0 .../common/isolate_connection_factory.dart | 0 .../sqlite_async/lib}/src/common/mutex.dart | 0 .../lib}/src/common/port_channel.dart | 0 .../lib}/src/common/sqlite_database.dart | 0 .../impl/isolate_connection_factory_impl.dart | 0 .../lib}/src/impl/mutex_impl.dart | 0 .../lib}/src/impl/open_factory_impl.dart | 0 .../lib}/src/impl/sqlite_database_impl.dart | 0 .../impl/stub_isolate_connection_factory.dart | 0 .../lib}/src/impl/stub_mutex.dart | 0 .../lib}/src/impl/stub_sqlite_database.dart | 0 .../src/impl/stub_sqlite_open_factory.dart | 0 .../lib}/src/isolate_connection_factory.dart | 0 .../sqlite_async/lib}/src/mutex.dart | 0 .../src/native/database/connection_pool.dart | 0 .../native_sqlite_connection_impl.dart | 0 .../database/native_sqlite_database.dart | 0 .../src/native/database/upstream_updates.dart | 0 .../native_isolate_connection_factory.dart | 0 .../lib}/src/native/native_isolate_mutex.dart | 0 .../native/native_sqlite_open_factory.dart | 0 .../lib}/src/sqlite_connection.dart | 0 .../lib}/src/sqlite_database.dart | 0 .../lib}/src/sqlite_migrations.dart | 0 .../lib}/src/sqlite_open_factory.dart | 0 .../sqlite_async/lib}/src/sqlite_options.dart | 0 .../sqlite_async/lib}/src/sqlite_queries.dart | 0 .../lib}/src/update_notification.dart | 0 .../sqlite_async/lib}/src/utils.dart | 0 .../lib}/src/utils/database_utils.dart | 0 .../lib}/src/utils/native_database_utils.dart | 0 .../lib}/src/utils/shared_utils.dart | 0 .../sqlite_async/lib}/src/web/database.dart | 0 .../src/web/database/web_sqlite_database.dart | 0 .../sqlite_async/lib}/src/web/protocol.dart | 0 .../web/web_isolate_connection_factory.dart | 0 .../sqlite_async/lib}/src/web/web_mutex.dart | 0 .../lib}/src/web/web_sqlite_open_factory.dart | 0 .../lib}/src/web/worker/worker.dart | 0 .../lib}/src/web/worker/worker_utils.dart | 0 {lib => packages/sqlite_async/lib}/utils.dart | 0 .../sqlite_async/pubspec.yaml | 0 .../sqlite_async/test}/basic_test.dart | 0 .../sqlite_async/test}/close_test.dart | 0 .../sqlite_async/test}/isolate_test.dart | 0 .../sqlite_async/test}/json1_test.dart | 0 .../sqlite_async/test}/migration_test.dart | 0 .../sqlite_async/test}/mutex_test.dart | 0 .../sqlite_async/test}/native/basic_test.dart | 0 .../sqlite_async/test}/native/watch_test.dart | 0 .../test}/server/asset_server.dart | 0 .../test}/server/worker_server.dart | 0 .../test}/utils/abstract_test_utils.dart | 0 .../test}/utils/native_test_utils.dart | 0 .../test}/utils/stub_test_utils.dart | 0 .../test}/utils/test_utils_impl.dart | 0 .../test}/utils/web_test_utils.dart | 0 .../sqlite_async/test}/watch_test.dart | 0 .../sqlite_async/test}/web/watch_test.dart | 0 78 files changed, 19 insertions(+) rename CHANGELOG.md => packages/sqlite_async/CHANGELOG.md (100%) create mode 100644 packages/sqlite_async/LICENSE rename README.md => packages/sqlite_async/README.md (100%) rename {example => packages/sqlite_async/example}/README.md (100%) rename {example => packages/sqlite_async/example}/basic_example.dart (100%) rename {example => packages/sqlite_async/example}/custom_functions_example.dart (100%) rename {example => packages/sqlite_async/example}/json_example.dart (100%) rename {example => packages/sqlite_async/example}/linux_cli_example.dart (100%) rename {example => packages/sqlite_async/example}/migration_example.dart (100%) rename {example => packages/sqlite_async/example}/watch_example.dart (100%) rename {lib => packages/sqlite_async/lib}/mutex.dart (100%) rename {lib => packages/sqlite_async/lib}/sqlite3.dart (100%) rename {lib => packages/sqlite_async/lib}/sqlite3_common.dart (100%) rename {lib => packages/sqlite_async/lib}/sqlite3_wasm.dart (100%) rename {lib => packages/sqlite_async/lib}/sqlite3_web.dart (100%) rename {lib => packages/sqlite_async/lib}/sqlite3_web_worker.dart (100%) rename {lib => packages/sqlite_async/lib}/sqlite_async.dart (100%) rename {lib => packages/sqlite_async/lib}/src/common/abstract_open_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/common/connection/sync_sqlite_connection.dart (100%) rename {lib => packages/sqlite_async/lib}/src/common/isolate_connection_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/common/mutex.dart (100%) rename {lib => packages/sqlite_async/lib}/src/common/port_channel.dart (100%) rename {lib => packages/sqlite_async/lib}/src/common/sqlite_database.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/isolate_connection_factory_impl.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/mutex_impl.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/open_factory_impl.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/sqlite_database_impl.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/stub_isolate_connection_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/stub_mutex.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/stub_sqlite_database.dart (100%) rename {lib => packages/sqlite_async/lib}/src/impl/stub_sqlite_open_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/isolate_connection_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/mutex.dart (100%) rename {lib => packages/sqlite_async/lib}/src/native/database/connection_pool.dart (100%) rename {lib => packages/sqlite_async/lib}/src/native/database/native_sqlite_connection_impl.dart (100%) rename {lib => packages/sqlite_async/lib}/src/native/database/native_sqlite_database.dart (100%) rename {lib => packages/sqlite_async/lib}/src/native/database/upstream_updates.dart (100%) rename {lib => packages/sqlite_async/lib}/src/native/native_isolate_connection_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/native/native_isolate_mutex.dart (100%) rename {lib => packages/sqlite_async/lib}/src/native/native_sqlite_open_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/sqlite_connection.dart (100%) rename {lib => packages/sqlite_async/lib}/src/sqlite_database.dart (100%) rename {lib => packages/sqlite_async/lib}/src/sqlite_migrations.dart (100%) rename {lib => packages/sqlite_async/lib}/src/sqlite_open_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/sqlite_options.dart (100%) rename {lib => packages/sqlite_async/lib}/src/sqlite_queries.dart (100%) rename {lib => packages/sqlite_async/lib}/src/update_notification.dart (100%) rename {lib => packages/sqlite_async/lib}/src/utils.dart (100%) rename {lib => packages/sqlite_async/lib}/src/utils/database_utils.dart (100%) rename {lib => packages/sqlite_async/lib}/src/utils/native_database_utils.dart (100%) rename {lib => packages/sqlite_async/lib}/src/utils/shared_utils.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/database.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/database/web_sqlite_database.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/protocol.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/web_isolate_connection_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/web_mutex.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/web_sqlite_open_factory.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/worker/worker.dart (100%) rename {lib => packages/sqlite_async/lib}/src/web/worker/worker_utils.dart (100%) rename {lib => packages/sqlite_async/lib}/utils.dart (100%) rename pubspec.yaml => packages/sqlite_async/pubspec.yaml (100%) rename {test => packages/sqlite_async/test}/basic_test.dart (100%) rename {test => packages/sqlite_async/test}/close_test.dart (100%) rename {test => packages/sqlite_async/test}/isolate_test.dart (100%) rename {test => packages/sqlite_async/test}/json1_test.dart (100%) rename {test => packages/sqlite_async/test}/migration_test.dart (100%) rename {test => packages/sqlite_async/test}/mutex_test.dart (100%) rename {test => packages/sqlite_async/test}/native/basic_test.dart (100%) rename {test => packages/sqlite_async/test}/native/watch_test.dart (100%) rename {test => packages/sqlite_async/test}/server/asset_server.dart (100%) rename {test => packages/sqlite_async/test}/server/worker_server.dart (100%) rename {test => packages/sqlite_async/test}/utils/abstract_test_utils.dart (100%) rename {test => packages/sqlite_async/test}/utils/native_test_utils.dart (100%) rename {test => packages/sqlite_async/test}/utils/stub_test_utils.dart (100%) rename {test => packages/sqlite_async/test}/utils/test_utils_impl.dart (100%) rename {test => packages/sqlite_async/test}/utils/web_test_utils.dart (100%) rename {test => packages/sqlite_async/test}/watch_test.dart (100%) rename {test => packages/sqlite_async/test}/web/watch_test.dart (100%) diff --git a/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/sqlite_async/CHANGELOG.md diff --git a/packages/sqlite_async/LICENSE b/packages/sqlite_async/LICENSE new file mode 100644 index 0000000..53316b9 --- /dev/null +++ b/packages/sqlite_async/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Journey Mobile, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/packages/sqlite_async/README.md similarity index 100% rename from README.md rename to packages/sqlite_async/README.md diff --git a/example/README.md b/packages/sqlite_async/example/README.md similarity index 100% rename from example/README.md rename to packages/sqlite_async/example/README.md diff --git a/example/basic_example.dart b/packages/sqlite_async/example/basic_example.dart similarity index 100% rename from example/basic_example.dart rename to packages/sqlite_async/example/basic_example.dart diff --git a/example/custom_functions_example.dart b/packages/sqlite_async/example/custom_functions_example.dart similarity index 100% rename from example/custom_functions_example.dart rename to packages/sqlite_async/example/custom_functions_example.dart diff --git a/example/json_example.dart b/packages/sqlite_async/example/json_example.dart similarity index 100% rename from example/json_example.dart rename to packages/sqlite_async/example/json_example.dart diff --git a/example/linux_cli_example.dart b/packages/sqlite_async/example/linux_cli_example.dart similarity index 100% rename from example/linux_cli_example.dart rename to packages/sqlite_async/example/linux_cli_example.dart diff --git a/example/migration_example.dart b/packages/sqlite_async/example/migration_example.dart similarity index 100% rename from example/migration_example.dart rename to packages/sqlite_async/example/migration_example.dart diff --git a/example/watch_example.dart b/packages/sqlite_async/example/watch_example.dart similarity index 100% rename from example/watch_example.dart rename to packages/sqlite_async/example/watch_example.dart diff --git a/lib/mutex.dart b/packages/sqlite_async/lib/mutex.dart similarity index 100% rename from lib/mutex.dart rename to packages/sqlite_async/lib/mutex.dart diff --git a/lib/sqlite3.dart b/packages/sqlite_async/lib/sqlite3.dart similarity index 100% rename from lib/sqlite3.dart rename to packages/sqlite_async/lib/sqlite3.dart diff --git a/lib/sqlite3_common.dart b/packages/sqlite_async/lib/sqlite3_common.dart similarity index 100% rename from lib/sqlite3_common.dart rename to packages/sqlite_async/lib/sqlite3_common.dart diff --git a/lib/sqlite3_wasm.dart b/packages/sqlite_async/lib/sqlite3_wasm.dart similarity index 100% rename from lib/sqlite3_wasm.dart rename to packages/sqlite_async/lib/sqlite3_wasm.dart diff --git a/lib/sqlite3_web.dart b/packages/sqlite_async/lib/sqlite3_web.dart similarity index 100% rename from lib/sqlite3_web.dart rename to packages/sqlite_async/lib/sqlite3_web.dart diff --git a/lib/sqlite3_web_worker.dart b/packages/sqlite_async/lib/sqlite3_web_worker.dart similarity index 100% rename from lib/sqlite3_web_worker.dart rename to packages/sqlite_async/lib/sqlite3_web_worker.dart diff --git a/lib/sqlite_async.dart b/packages/sqlite_async/lib/sqlite_async.dart similarity index 100% rename from lib/sqlite_async.dart rename to packages/sqlite_async/lib/sqlite_async.dart diff --git a/lib/src/common/abstract_open_factory.dart b/packages/sqlite_async/lib/src/common/abstract_open_factory.dart similarity index 100% rename from lib/src/common/abstract_open_factory.dart rename to packages/sqlite_async/lib/src/common/abstract_open_factory.dart diff --git a/lib/src/common/connection/sync_sqlite_connection.dart b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart similarity index 100% rename from lib/src/common/connection/sync_sqlite_connection.dart rename to packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart diff --git a/lib/src/common/isolate_connection_factory.dart b/packages/sqlite_async/lib/src/common/isolate_connection_factory.dart similarity index 100% rename from lib/src/common/isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/common/isolate_connection_factory.dart diff --git a/lib/src/common/mutex.dart b/packages/sqlite_async/lib/src/common/mutex.dart similarity index 100% rename from lib/src/common/mutex.dart rename to packages/sqlite_async/lib/src/common/mutex.dart diff --git a/lib/src/common/port_channel.dart b/packages/sqlite_async/lib/src/common/port_channel.dart similarity index 100% rename from lib/src/common/port_channel.dart rename to packages/sqlite_async/lib/src/common/port_channel.dart diff --git a/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart similarity index 100% rename from lib/src/common/sqlite_database.dart rename to packages/sqlite_async/lib/src/common/sqlite_database.dart diff --git a/lib/src/impl/isolate_connection_factory_impl.dart b/packages/sqlite_async/lib/src/impl/isolate_connection_factory_impl.dart similarity index 100% rename from lib/src/impl/isolate_connection_factory_impl.dart rename to packages/sqlite_async/lib/src/impl/isolate_connection_factory_impl.dart diff --git a/lib/src/impl/mutex_impl.dart b/packages/sqlite_async/lib/src/impl/mutex_impl.dart similarity index 100% rename from lib/src/impl/mutex_impl.dart rename to packages/sqlite_async/lib/src/impl/mutex_impl.dart diff --git a/lib/src/impl/open_factory_impl.dart b/packages/sqlite_async/lib/src/impl/open_factory_impl.dart similarity index 100% rename from lib/src/impl/open_factory_impl.dart rename to packages/sqlite_async/lib/src/impl/open_factory_impl.dart diff --git a/lib/src/impl/sqlite_database_impl.dart b/packages/sqlite_async/lib/src/impl/sqlite_database_impl.dart similarity index 100% rename from lib/src/impl/sqlite_database_impl.dart rename to packages/sqlite_async/lib/src/impl/sqlite_database_impl.dart diff --git a/lib/src/impl/stub_isolate_connection_factory.dart b/packages/sqlite_async/lib/src/impl/stub_isolate_connection_factory.dart similarity index 100% rename from lib/src/impl/stub_isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/impl/stub_isolate_connection_factory.dart diff --git a/lib/src/impl/stub_mutex.dart b/packages/sqlite_async/lib/src/impl/stub_mutex.dart similarity index 100% rename from lib/src/impl/stub_mutex.dart rename to packages/sqlite_async/lib/src/impl/stub_mutex.dart diff --git a/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart similarity index 100% rename from lib/src/impl/stub_sqlite_database.dart rename to packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart diff --git a/lib/src/impl/stub_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_open_factory.dart similarity index 100% rename from lib/src/impl/stub_sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/impl/stub_sqlite_open_factory.dart diff --git a/lib/src/isolate_connection_factory.dart b/packages/sqlite_async/lib/src/isolate_connection_factory.dart similarity index 100% rename from lib/src/isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/isolate_connection_factory.dart diff --git a/lib/src/mutex.dart b/packages/sqlite_async/lib/src/mutex.dart similarity index 100% rename from lib/src/mutex.dart rename to packages/sqlite_async/lib/src/mutex.dart diff --git a/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart similarity index 100% rename from lib/src/native/database/connection_pool.dart rename to packages/sqlite_async/lib/src/native/database/connection_pool.dart diff --git a/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart similarity index 100% rename from lib/src/native/database/native_sqlite_connection_impl.dart rename to packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart diff --git a/lib/src/native/database/native_sqlite_database.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart similarity index 100% rename from lib/src/native/database/native_sqlite_database.dart rename to packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart diff --git a/lib/src/native/database/upstream_updates.dart b/packages/sqlite_async/lib/src/native/database/upstream_updates.dart similarity index 100% rename from lib/src/native/database/upstream_updates.dart rename to packages/sqlite_async/lib/src/native/database/upstream_updates.dart diff --git a/lib/src/native/native_isolate_connection_factory.dart b/packages/sqlite_async/lib/src/native/native_isolate_connection_factory.dart similarity index 100% rename from lib/src/native/native_isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/native/native_isolate_connection_factory.dart diff --git a/lib/src/native/native_isolate_mutex.dart b/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart similarity index 100% rename from lib/src/native/native_isolate_mutex.dart rename to packages/sqlite_async/lib/src/native/native_isolate_mutex.dart diff --git a/lib/src/native/native_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart similarity index 100% rename from lib/src/native/native_sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart diff --git a/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart similarity index 100% rename from lib/src/sqlite_connection.dart rename to packages/sqlite_async/lib/src/sqlite_connection.dart diff --git a/lib/src/sqlite_database.dart b/packages/sqlite_async/lib/src/sqlite_database.dart similarity index 100% rename from lib/src/sqlite_database.dart rename to packages/sqlite_async/lib/src/sqlite_database.dart diff --git a/lib/src/sqlite_migrations.dart b/packages/sqlite_async/lib/src/sqlite_migrations.dart similarity index 100% rename from lib/src/sqlite_migrations.dart rename to packages/sqlite_async/lib/src/sqlite_migrations.dart diff --git a/lib/src/sqlite_open_factory.dart b/packages/sqlite_async/lib/src/sqlite_open_factory.dart similarity index 100% rename from lib/src/sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/sqlite_open_factory.dart diff --git a/lib/src/sqlite_options.dart b/packages/sqlite_async/lib/src/sqlite_options.dart similarity index 100% rename from lib/src/sqlite_options.dart rename to packages/sqlite_async/lib/src/sqlite_options.dart diff --git a/lib/src/sqlite_queries.dart b/packages/sqlite_async/lib/src/sqlite_queries.dart similarity index 100% rename from lib/src/sqlite_queries.dart rename to packages/sqlite_async/lib/src/sqlite_queries.dart diff --git a/lib/src/update_notification.dart b/packages/sqlite_async/lib/src/update_notification.dart similarity index 100% rename from lib/src/update_notification.dart rename to packages/sqlite_async/lib/src/update_notification.dart diff --git a/lib/src/utils.dart b/packages/sqlite_async/lib/src/utils.dart similarity index 100% rename from lib/src/utils.dart rename to packages/sqlite_async/lib/src/utils.dart diff --git a/lib/src/utils/database_utils.dart b/packages/sqlite_async/lib/src/utils/database_utils.dart similarity index 100% rename from lib/src/utils/database_utils.dart rename to packages/sqlite_async/lib/src/utils/database_utils.dart diff --git a/lib/src/utils/native_database_utils.dart b/packages/sqlite_async/lib/src/utils/native_database_utils.dart similarity index 100% rename from lib/src/utils/native_database_utils.dart rename to packages/sqlite_async/lib/src/utils/native_database_utils.dart diff --git a/lib/src/utils/shared_utils.dart b/packages/sqlite_async/lib/src/utils/shared_utils.dart similarity index 100% rename from lib/src/utils/shared_utils.dart rename to packages/sqlite_async/lib/src/utils/shared_utils.dart diff --git a/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart similarity index 100% rename from lib/src/web/database.dart rename to packages/sqlite_async/lib/src/web/database.dart diff --git a/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart similarity index 100% rename from lib/src/web/database/web_sqlite_database.dart rename to packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart diff --git a/lib/src/web/protocol.dart b/packages/sqlite_async/lib/src/web/protocol.dart similarity index 100% rename from lib/src/web/protocol.dart rename to packages/sqlite_async/lib/src/web/protocol.dart diff --git a/lib/src/web/web_isolate_connection_factory.dart b/packages/sqlite_async/lib/src/web/web_isolate_connection_factory.dart similarity index 100% rename from lib/src/web/web_isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/web/web_isolate_connection_factory.dart diff --git a/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart similarity index 100% rename from lib/src/web/web_mutex.dart rename to packages/sqlite_async/lib/src/web/web_mutex.dart diff --git a/lib/src/web/web_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart similarity index 100% rename from lib/src/web/web_sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart diff --git a/lib/src/web/worker/worker.dart b/packages/sqlite_async/lib/src/web/worker/worker.dart similarity index 100% rename from lib/src/web/worker/worker.dart rename to packages/sqlite_async/lib/src/web/worker/worker.dart diff --git a/lib/src/web/worker/worker_utils.dart b/packages/sqlite_async/lib/src/web/worker/worker_utils.dart similarity index 100% rename from lib/src/web/worker/worker_utils.dart rename to packages/sqlite_async/lib/src/web/worker/worker_utils.dart diff --git a/lib/utils.dart b/packages/sqlite_async/lib/utils.dart similarity index 100% rename from lib/utils.dart rename to packages/sqlite_async/lib/utils.dart diff --git a/pubspec.yaml b/packages/sqlite_async/pubspec.yaml similarity index 100% rename from pubspec.yaml rename to packages/sqlite_async/pubspec.yaml diff --git a/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart similarity index 100% rename from test/basic_test.dart rename to packages/sqlite_async/test/basic_test.dart diff --git a/test/close_test.dart b/packages/sqlite_async/test/close_test.dart similarity index 100% rename from test/close_test.dart rename to packages/sqlite_async/test/close_test.dart diff --git a/test/isolate_test.dart b/packages/sqlite_async/test/isolate_test.dart similarity index 100% rename from test/isolate_test.dart rename to packages/sqlite_async/test/isolate_test.dart diff --git a/test/json1_test.dart b/packages/sqlite_async/test/json1_test.dart similarity index 100% rename from test/json1_test.dart rename to packages/sqlite_async/test/json1_test.dart diff --git a/test/migration_test.dart b/packages/sqlite_async/test/migration_test.dart similarity index 100% rename from test/migration_test.dart rename to packages/sqlite_async/test/migration_test.dart diff --git a/test/mutex_test.dart b/packages/sqlite_async/test/mutex_test.dart similarity index 100% rename from test/mutex_test.dart rename to packages/sqlite_async/test/mutex_test.dart diff --git a/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart similarity index 100% rename from test/native/basic_test.dart rename to packages/sqlite_async/test/native/basic_test.dart diff --git a/test/native/watch_test.dart b/packages/sqlite_async/test/native/watch_test.dart similarity index 100% rename from test/native/watch_test.dart rename to packages/sqlite_async/test/native/watch_test.dart diff --git a/test/server/asset_server.dart b/packages/sqlite_async/test/server/asset_server.dart similarity index 100% rename from test/server/asset_server.dart rename to packages/sqlite_async/test/server/asset_server.dart diff --git a/test/server/worker_server.dart b/packages/sqlite_async/test/server/worker_server.dart similarity index 100% rename from test/server/worker_server.dart rename to packages/sqlite_async/test/server/worker_server.dart diff --git a/test/utils/abstract_test_utils.dart b/packages/sqlite_async/test/utils/abstract_test_utils.dart similarity index 100% rename from test/utils/abstract_test_utils.dart rename to packages/sqlite_async/test/utils/abstract_test_utils.dart diff --git a/test/utils/native_test_utils.dart b/packages/sqlite_async/test/utils/native_test_utils.dart similarity index 100% rename from test/utils/native_test_utils.dart rename to packages/sqlite_async/test/utils/native_test_utils.dart diff --git a/test/utils/stub_test_utils.dart b/packages/sqlite_async/test/utils/stub_test_utils.dart similarity index 100% rename from test/utils/stub_test_utils.dart rename to packages/sqlite_async/test/utils/stub_test_utils.dart diff --git a/test/utils/test_utils_impl.dart b/packages/sqlite_async/test/utils/test_utils_impl.dart similarity index 100% rename from test/utils/test_utils_impl.dart rename to packages/sqlite_async/test/utils/test_utils_impl.dart diff --git a/test/utils/web_test_utils.dart b/packages/sqlite_async/test/utils/web_test_utils.dart similarity index 100% rename from test/utils/web_test_utils.dart rename to packages/sqlite_async/test/utils/web_test_utils.dart diff --git a/test/watch_test.dart b/packages/sqlite_async/test/watch_test.dart similarity index 100% rename from test/watch_test.dart rename to packages/sqlite_async/test/watch_test.dart diff --git a/test/web/watch_test.dart b/packages/sqlite_async/test/web/watch_test.dart similarity index 100% rename from test/web/watch_test.dart rename to packages/sqlite_async/test/web/watch_test.dart From fe85a70f004c9ebc13476fc0d14920643fd79dd4 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Thu, 4 Jul 2024 18:40:49 +0200 Subject: [PATCH 02/17] monorepo updates --- .github/workflows/release.yml | 8 ++-- .github/workflows/test.yaml | 13 +++--- .gitignore | 1 + DEVELOPING.md | 8 ---- README.md | 13 ++++++ melos.yaml | 40 +++++++++++++++++++ packages/sqlite_async/pubspec.yaml | 1 + .../sqlite_async/scripts}/benchmark.dart | 0 .../test/server/worker_server.dart | 24 +++-------- pubspec.yaml | 6 +++ scripts/sqlite3_wasm_download.dart | 33 +++++++++++++++ 11 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 README.md create mode 100644 melos.yaml rename {scripts => packages/sqlite_async/scripts}/benchmark.dart (100%) create mode 100644 pubspec.yaml create mode 100644 scripts/sqlite3_wasm_download.dart diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a098a3..be70530 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,11 +21,11 @@ jobs: - uses: dart-lang/setup-dart@v1 - - name: Install dependencies - run: dart pub get + - name: Install Melos + run: flutter pub global activate melos - - name: Compile WebWorker - run: dart compile js -o assets/db_worker.js -O0 lib/src/web/worker/worker.dart + - name: Install dependencies + run: melos prepare - name: Set tag name id: tag diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f805f55..7f05a8d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -51,18 +51,17 @@ jobs: with: sdk: ${{ matrix.dart_sdk }} + - name: Install Melos + run: flutter pub global activate melos + - name: Install dependencies - run: dart pub get + run: melos prepare - name: Install SQLite run: | ./scripts/install_sqlite.sh ${{ matrix.sqlite_version }} ${{ matrix.sqlite_url }} - mkdir -p assets && curl -LJ https://github.com/simolus3/sqlite3.dart/releases/download/sqlite3-2.4.3/sqlite3.wasm -o assets/sqlite3.wasm - - - name: Compile WebWorker - run: dart compile js -o assets/db_worker.js -O0 lib/src/web/worker/worker.dart - name: Run Tests run: | - export LD_LIBRARY_PATH=./sqlite-autoconf-${{ matrix.sqlite_version }}/.libs - dart test -p vm,chrome + export LD_LIBRARY_PATH=$(pwd)/sqlite-autoconf-${{ matrix.sqlite_version }}/.libs + melos test diff --git a/.gitignore b/.gitignore index 271119a..71ce878 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ assets test-db sqlite-autoconf-* doc +*.iml build diff --git a/DEVELOPING.md b/DEVELOPING.md index d66a664..ff12dec 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -1,13 +1,5 @@ # Developing Instructions -## Testing - -Running tests for the `web` platform requires some preparation to be executed. The `sqlite3.wasm` and `db_worker.js` files need to be available in the Git ignored `./assets` folder. - -See the [test action](./.github/workflows/test.yaml) for the latest steps. - -On your local machine run the commands from the `Install SQLite`, `Compile WebWorker` and `Run Tests` steps. - ## Releases Web worker files are compiled and uploaded to draft Github releases whenever tags matching `v*` are pushed. These tags are created when versioning. Releases should be manually finalized and published when releasing new package versions. diff --git a/README.md b/README.md new file mode 100644 index 0000000..57391ad --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# sqlite_async + +High-performance asynchronous interface for SQLite on Dart & Flutter. + +## Getting Started + +This monorepo uses [melos](https://melos.invertase.dev/) to handle command and package management. + +To configure the monorepo for development run `melos prepare` after cloning + +## Packages + +See [./packages/sqlite_async](./packages/sqlite_async) for details. diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..d70f26f --- /dev/null +++ b/melos.yaml @@ -0,0 +1,40 @@ +name: sqlite_async_monorepo + +packages: + - packages/** + +scripts: + prepare: melos bootstrap && melos prepare:compile:webworker && melos prepare:sqlite:wasm + + prepare:compile:webworker: + description: Compile Javascript web worker distributable + run: dart compile js -o assets/db_worker.js -O0 packages/sqlite_async/lib/src/web/worker/worker.dart + + prepare:sqlite:wasm: + description: Download SQLite3 WASM binary + run: dart run ./scripts/sqlite3_wasm_download.dart + + format: + description: Format Dart code. + run: dart format . + + format:check:packages: + description: Check formatting of Dart code in packages. + run: dart format --output none --set-exit-if-changed packages + + analyze:packages: + description: Analyze Dart code in packages. + run: dart analyze packages --fatal-infos + + test: + description: Run tests in a specific package. + run: dart test -p chrome,vm + exec: + concurrency: 1 + packageFilters: + dirExists: + - test + # This tells Melos tests to ignore env variables passed to tests from `melos run test` + # as they could change the behaviour of how tests filter packages. + env: + MELOS_TEST: true diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 2e041ba..4523ae7 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: meta: ^1.10.0 dev_dependencies: + dcli: ^4.0.0 js: ^0.6.7 lints: ^3.0.0 test: ^1.21.0 diff --git a/scripts/benchmark.dart b/packages/sqlite_async/scripts/benchmark.dart similarity index 100% rename from scripts/benchmark.dart rename to packages/sqlite_async/scripts/benchmark.dart diff --git a/packages/sqlite_async/test/server/worker_server.dart b/packages/sqlite_async/test/server/worker_server.dart index 313c414..30cffe9 100644 --- a/packages/sqlite_async/test/server/worker_server.dart +++ b/packages/sqlite_async/test/server/worker_server.dart @@ -1,44 +1,30 @@ import 'dart:io'; +import 'package:dcli/dcli.dart'; import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; import 'package:shelf_static/shelf_static.dart'; import 'package:stream_channel/stream_channel.dart'; -import 'package:test/test.dart'; import 'asset_server.dart'; Future hybridMain(StreamChannel channel) async { - final directory = Directory('./assets'); + final directory = p.normalize( + p.join(DartScript.self.pathToScriptDirectory, '../../../../assets')); - final sqliteOutputPath = p.join(directory.path, 'sqlite3.wasm'); + final sqliteOutputPath = p.join(directory, 'sqlite3.wasm'); if (!(await File(sqliteOutputPath).exists())) { throw AssertionError( 'sqlite3.wasm file should be present in the ./assets folder'); } - final workerPath = p.join(directory.path, 'db_worker.js'); - if (!(await File(workerPath).exists())) { - final process = await Process.run(Platform.executable, [ - 'compile', - 'js', - '-o', - workerPath, - '-O0', - 'lib/src/web/worker/worker.dart', - ]); - if (process.exitCode != 0) { - fail('Could not compile worker'); - } - } - final server = await HttpServer.bind('localhost', 0); final handler = const Pipeline() .addMiddleware(cors()) - .addHandler(createStaticHandler(directory.path)); + .addHandler(createStaticHandler(directory)); io.serveRequests(server, handler); channel.sink.add(server.port); diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..b00adf8 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,6 @@ +name: drift_sqlite_async_monorepo + +environment: + sdk: ">=3.4.0 <4.0.0" +dev_dependencies: + melos: ^4.1.0 diff --git a/scripts/sqlite3_wasm_download.dart b/scripts/sqlite3_wasm_download.dart new file mode 100644 index 0000000..28d91b4 --- /dev/null +++ b/scripts/sqlite3_wasm_download.dart @@ -0,0 +1,33 @@ +/// Downloads sqlite3.wasm +import 'dart:io'; + +final sqliteUrl = + 'https://github.com/simolus3/sqlite3.dart/releases/download/sqlite3-2.4.3/sqlite3.wasm'; + +void main() async { + // Create assets directory if it doesn't exist + final assetsDir = Directory('assets'); + if (!await assetsDir.exists()) { + await assetsDir.create(); + } + + final sqliteFilename = 'sqlite3.wasm'; + final sqlitePath = 'assets/$sqliteFilename'; + + // Download sqlite3.wasm + await downloadFile(sqliteUrl, sqlitePath); +} + +Future downloadFile(String url, String savePath) async { + print('Downloading: $url'); + var httpClient = HttpClient(); + var request = await httpClient.getUrl(Uri.parse(url)); + var response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + var file = File(savePath); + await response.pipe(file.openWrite()); + } else { + print( + 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); + } +} From d434ad3562ec761940f6355824a1613346d009cc Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Thu, 4 Jul 2024 18:50:40 +0200 Subject: [PATCH 03/17] move drift package --- README.md | 3 +- packages/drift_sqlite_async/CHANGELOG.md | 4 + packages/drift_sqlite_async/LICENSE | 21 ++ packages/drift_sqlite_async/README.md | 62 +++++ packages/drift_sqlite_async/build.yaml | 6 + packages/drift_sqlite_async/example/main.dart | 49 ++++ .../drift_sqlite_async/example/main.g.dart | 189 ++++++++++++++++ .../example/with_migrations.dart | 58 +++++ .../example/with_migrations.g.dart | 189 ++++++++++++++++ .../lib/drift_sqlite_async.dart | 4 + .../lib/src/connection.dart | 34 +++ .../drift_sqlite_async/lib/src/executor.dart | 147 ++++++++++++ .../lib/src/transaction_executor.dart | 193 ++++++++++++++++ packages/drift_sqlite_async/pubspec.yaml | 18 ++ .../drift_sqlite_async/pubspec_overrides.yaml | 4 + .../drift_sqlite_async/test/basic_test.dart | 212 ++++++++++++++++++ packages/drift_sqlite_async/test/db_test.dart | 95 ++++++++ .../test/generated/database.dart | 21 ++ .../test/generated/database.g.dart | 189 ++++++++++++++++ .../test/utils/test_utils.dart | 98 ++++++++ 20 files changed, 1595 insertions(+), 1 deletion(-) create mode 100644 packages/drift_sqlite_async/CHANGELOG.md create mode 100644 packages/drift_sqlite_async/LICENSE create mode 100644 packages/drift_sqlite_async/README.md create mode 100644 packages/drift_sqlite_async/build.yaml create mode 100644 packages/drift_sqlite_async/example/main.dart create mode 100644 packages/drift_sqlite_async/example/main.g.dart create mode 100644 packages/drift_sqlite_async/example/with_migrations.dart create mode 100644 packages/drift_sqlite_async/example/with_migrations.g.dart create mode 100644 packages/drift_sqlite_async/lib/drift_sqlite_async.dart create mode 100644 packages/drift_sqlite_async/lib/src/connection.dart create mode 100644 packages/drift_sqlite_async/lib/src/executor.dart create mode 100644 packages/drift_sqlite_async/lib/src/transaction_executor.dart create mode 100644 packages/drift_sqlite_async/pubspec.yaml create mode 100644 packages/drift_sqlite_async/pubspec_overrides.yaml create mode 100644 packages/drift_sqlite_async/test/basic_test.dart create mode 100644 packages/drift_sqlite_async/test/db_test.dart create mode 100644 packages/drift_sqlite_async/test/generated/database.dart create mode 100644 packages/drift_sqlite_async/test/generated/database.g.dart create mode 100644 packages/drift_sqlite_async/test/utils/test_utils.dart diff --git a/README.md b/README.md index 57391ad..3029b5c 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,5 @@ To configure the monorepo for development run `melos prepare` after cloning ## Packages -See [./packages/sqlite_async](./packages/sqlite_async) for details. +- [./packages/sqlite_async](./packages/sqlite_async) Creates asynchronous SQLite connections +- [./packages/drify_sqlite_async](./packages/drift_sqlite_async/README.md) A Drift wrapper for `sqlite_async` diff --git a/packages/drift_sqlite_async/CHANGELOG.md b/packages/drift_sqlite_async/CHANGELOG.md new file mode 100644 index 0000000..a998fef --- /dev/null +++ b/packages/drift_sqlite_async/CHANGELOG.md @@ -0,0 +1,4 @@ + +## 0.1.0-alpha.1 + +Initial release. diff --git a/packages/drift_sqlite_async/LICENSE b/packages/drift_sqlite_async/LICENSE new file mode 100644 index 0000000..2d3030a --- /dev/null +++ b/packages/drift_sqlite_async/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Journey Mobile, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/drift_sqlite_async/README.md b/packages/drift_sqlite_async/README.md new file mode 100644 index 0000000..c732652 --- /dev/null +++ b/packages/drift_sqlite_async/README.md @@ -0,0 +1,62 @@ +# drift_sqlite_async + +`drift_sqlite_async` allows using drift on an sqlite_async database - the APIs from both can be seamlessly used together in the same application. + +Supported functionality: +1. All queries including select, insert, update, delete. +2. Transactions and nested transactions. +3. Table updates are propagated between sqlite_async and Drift - watching queries works using either API. +4. Select queries can run concurrently with writes and other select statements. + + +## Usage + +Use `SqliteAsyncDriftConnection` to create a DatabaseConnection / QueryExecutor for Drift from the sqlite_async SqliteDatabase: + +```dart +@DriftDatabase(tables: [TodoItems]) +class AppDatabase extends _$AppDatabase { + AppDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; +} + +Future main() async { + // The sqlite_async db + final db = SqliteDatabase(path: 'example.db'); + // The Drift db + final appdb = AppDatabase(db); +} +``` + +A full example is in the `examples/` folder. + +For details on table definitions and using the database, see the [Drift documentation](https://drift.simonbinder.eu/). + +## Transactions and concurrency + +sqlite_async uses WAL mode and multiple read connections by default, and this +is also exposed when using the database with Drift. + +Drift's transactions use sqlite_async's `writeTransaction`. The same locks are used +for both, preventing conflicts. + +Read-only transactions are not currently supported in Drift. + +Drift's nested transactions are supported, implemented using SAVEPOINT. + +Select statements in Drift use read operations (`getAll()`) in sqlite_async, +and can run concurrently with writes. + +## Update notifications + +sqlite_async uses SQLite's update_hook to detect changes for watching queries, +and will automatically pick up changes made using Drift. This also includes any updates from custom queries in Drift. + +Changes from sqlite_async are automatically propagated to Drift when using SqliteAsyncDriftConnection. +These events are only sent while no write transaction is active. + +Within Drift's transactions, Drift's own update notifications will still apply for watching queries within that transaction. + +Note: There is a possibility of events being duplicated. This should not have a significant impact on most applications. \ No newline at end of file diff --git a/packages/drift_sqlite_async/build.yaml b/packages/drift_sqlite_async/build.yaml new file mode 100644 index 0000000..e1151bf --- /dev/null +++ b/packages/drift_sqlite_async/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + drift_dev: + options: + fatal_warnings: true diff --git a/packages/drift_sqlite_async/example/main.dart b/packages/drift_sqlite_async/example/main.dart new file mode 100644 index 0000000..6972643 --- /dev/null +++ b/packages/drift_sqlite_async/example/main.dart @@ -0,0 +1,49 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +part 'main.g.dart'; + +class TodoItems extends Table { + @override + String get tableName => 'todos'; + + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +@DriftDatabase(tables: [TodoItems]) +class AppDatabase extends _$AppDatabase { + AppDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; +} + +Future main() async { + final db = SqliteDatabase(path: 'example.db'); + + // Example where the schema is managed manually + await db.execute( + 'CREATE TABLE IF NOT EXISTS todos(id integer primary key, description text)'); + + final appdb = AppDatabase(db); + + // Watch a query on the Drift database + appdb.select(appdb.todoItems).watch().listen((todos) { + print('Todos: $todos'); + }); + + // Insert using the Drift database + await appdb + .into(appdb.todoItems) + .insert(TodoItemsCompanion.insert(description: 'Test Drift')); + + // Insert using the sqlite_async database + await db.execute('INSERT INTO todos(description) VALUES(?)', ['Test Direct']); + + await Future.delayed(const Duration(milliseconds: 100)); + + await appdb.close(); + await db.close(); +} diff --git a/packages/drift_sqlite_async/example/main.g.dart b/packages/drift_sqlite_async/example/main.g.dart new file mode 100644 index 0000000..576157c --- /dev/null +++ b/packages/drift_sqlite_async/example/main.g.dart @@ -0,0 +1,189 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main.dart'; + +// ignore_for_file: type=lint +class $TodoItemsTable extends TodoItems + with TableInfo<$TodoItemsTable, TodoItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TodoItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, description]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'todos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TodoItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodoItem( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + ); + } + + @override + $TodoItemsTable createAlias(String alias) { + return $TodoItemsTable(attachedDatabase, alias); + } +} + +class TodoItem extends DataClass implements Insertable { + final int id; + final String description; + const TodoItem({required this.id, required this.description}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['description'] = Variable(description); + return map; + } + + TodoItemsCompanion toCompanion(bool nullToAbsent) { + return TodoItemsCompanion( + id: Value(id), + description: Value(description), + ); + } + + factory TodoItem.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoItem( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + }; + } + + TodoItem copyWith({int? id, String? description}) => TodoItem( + id: id ?? this.id, + description: description ?? this.description, + ); + @override + String toString() { + return (StringBuffer('TodoItem(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, description); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodoItem && + other.id == this.id && + other.description == this.description); +} + +class TodoItemsCompanion extends UpdateCompanion { + final Value id; + final Value description; + const TodoItemsCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + }); + TodoItemsCompanion.insert({ + this.id = const Value.absent(), + required String description, + }) : description = Value(description); + static Insertable custom({ + Expression? id, + Expression? description, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (description != null) 'description': description, + }); + } + + TodoItemsCompanion copyWith({Value? id, Value? description}) { + return TodoItemsCompanion( + id: id ?? this.id, + description: description ?? this.description, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodoItemsCompanion(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + late final $TodoItemsTable todoItems = $TodoItemsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todoItems]; +} diff --git a/packages/drift_sqlite_async/example/with_migrations.dart b/packages/drift_sqlite_async/example/with_migrations.dart new file mode 100644 index 0000000..2bd4a87 --- /dev/null +++ b/packages/drift_sqlite_async/example/with_migrations.dart @@ -0,0 +1,58 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +part 'with_migrations.g.dart'; + +class TodoItems extends Table { + @override + String get tableName => 'todos'; + + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +@DriftDatabase(tables: [TodoItems]) +class AppDatabase extends _$AppDatabase { + AppDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (m) async { + // In this example, the schema is managed by Drift + await m.createAll(); + }, + ); + } +} + +Future main() async { + final db = SqliteDatabase(path: 'with_migrations.db'); + + await db.execute( + 'CREATE TABLE IF NOT EXISTS todos(id integer primary key, description text)'); + + final appdb = AppDatabase(db); + + // Watch a query on the Drift database + appdb.select(appdb.todoItems).watch().listen((todos) { + print('Todos: $todos'); + }); + + // Insert using the Drift database + await appdb + .into(appdb.todoItems) + .insert(TodoItemsCompanion.insert(description: 'Test Drift')); + + // Insert using the sqlite_async database + await db.execute('INSERT INTO todos(description) VALUES(?)', ['Test Direct']); + + await Future.delayed(const Duration(milliseconds: 100)); + + await appdb.close(); + await db.close(); +} diff --git a/packages/drift_sqlite_async/example/with_migrations.g.dart b/packages/drift_sqlite_async/example/with_migrations.g.dart new file mode 100644 index 0000000..67ce020 --- /dev/null +++ b/packages/drift_sqlite_async/example/with_migrations.g.dart @@ -0,0 +1,189 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'with_migrations.dart'; + +// ignore_for_file: type=lint +class $TodoItemsTable extends TodoItems + with TableInfo<$TodoItemsTable, TodoItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TodoItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, description]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'todos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TodoItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodoItem( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + ); + } + + @override + $TodoItemsTable createAlias(String alias) { + return $TodoItemsTable(attachedDatabase, alias); + } +} + +class TodoItem extends DataClass implements Insertable { + final int id; + final String description; + const TodoItem({required this.id, required this.description}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['description'] = Variable(description); + return map; + } + + TodoItemsCompanion toCompanion(bool nullToAbsent) { + return TodoItemsCompanion( + id: Value(id), + description: Value(description), + ); + } + + factory TodoItem.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoItem( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + }; + } + + TodoItem copyWith({int? id, String? description}) => TodoItem( + id: id ?? this.id, + description: description ?? this.description, + ); + @override + String toString() { + return (StringBuffer('TodoItem(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, description); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodoItem && + other.id == this.id && + other.description == this.description); +} + +class TodoItemsCompanion extends UpdateCompanion { + final Value id; + final Value description; + const TodoItemsCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + }); + TodoItemsCompanion.insert({ + this.id = const Value.absent(), + required String description, + }) : description = Value(description); + static Insertable custom({ + Expression? id, + Expression? description, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (description != null) 'description': description, + }); + } + + TodoItemsCompanion copyWith({Value? id, Value? description}) { + return TodoItemsCompanion( + id: id ?? this.id, + description: description ?? this.description, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodoItemsCompanion(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + late final $TodoItemsTable todoItems = $TodoItemsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todoItems]; +} diff --git a/packages/drift_sqlite_async/lib/drift_sqlite_async.dart b/packages/drift_sqlite_async/lib/drift_sqlite_async.dart new file mode 100644 index 0000000..f842c5b --- /dev/null +++ b/packages/drift_sqlite_async/lib/drift_sqlite_async.dart @@ -0,0 +1,4 @@ +library drift_sqlite_async; + +export './src/connection.dart'; +export './src/executor.dart'; diff --git a/packages/drift_sqlite_async/lib/src/connection.dart b/packages/drift_sqlite_async/lib/src/connection.dart new file mode 100644 index 0000000..e375795 --- /dev/null +++ b/packages/drift_sqlite_async/lib/src/connection.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/src/executor.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +/// Wraps a sqlite_async [SqliteConnection] as a Drift [DatabaseConnection]. +/// +/// The SqliteConnection must be instantiated before constructing this, and +/// is not closed when [SqliteAsyncDriftConnection.close] is called. +/// +/// This class handles delegating Drift's queries and transactions to the +/// [SqliteConnection], and passes on any table updates from the +/// [SqliteConnection] to Drift. +class SqliteAsyncDriftConnection extends DatabaseConnection { + late StreamSubscription _updateSubscription; + + SqliteAsyncDriftConnection(SqliteConnection db) + : super(SqliteAsyncQueryExecutor(db)) { + _updateSubscription = (db as SqliteQueries).updates!.listen((event) { + var setUpdates = {}; + for (var tableName in event.tables) { + setUpdates.add(TableUpdate(tableName)); + } + super.streamQueries.handleTableUpdates(setUpdates); + }); + } + + @override + Future close() async { + await _updateSubscription.cancel(); + await super.close(); + } +} diff --git a/packages/drift_sqlite_async/lib/src/executor.dart b/packages/drift_sqlite_async/lib/src/executor.dart new file mode 100644 index 0000000..a106b91 --- /dev/null +++ b/packages/drift_sqlite_async/lib/src/executor.dart @@ -0,0 +1,147 @@ +import 'dart:async'; + +import 'package:drift/backends.dart'; +import 'package:drift_sqlite_async/src/transaction_executor.dart'; +import 'package:sqlite_async/sqlite3.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +class _SqliteAsyncDelegate extends DatabaseDelegate { + final SqliteConnection db; + bool _closed = false; + + _SqliteAsyncDelegate(this.db); + + @override + late final DbVersionDelegate versionDelegate = + _SqliteAsyncVersionDelegate(db); + + // Not used - we override beginTransaction() with SqliteAsyncTransactionExecutor for more control. + @override + late final TransactionDelegate transactionDelegate = + const NoTransactionDelegate(); + + @override + bool get isOpen => !db.closed && !_closed; + + // Ends with " RETURNING *", or starts with insert/update/delete. + // Drift-generated queries will always have the RETURNING *. + // The INSERT/UPDATE/DELETE check is for custom queries, and is not exhaustive. + final _returningCheck = RegExp( + r'( RETURNING \*;?$)|(^(INSERT|UPDATE|DELETE))', + caseSensitive: false); + + @override + Future open(QueryExecutorUser user) async { + // Workaround - this ensures the db is open + await db.get('SELECT 1'); + } + + @override + Future close() async { + // We don't own the underlying SqliteConnection - don't close it. + _closed = true; + } + + @override + Future runBatched(BatchedStatements statements) async { + return db.writeLock((tx) async { + // sqlite_async's batch functionality doesn't have enough flexibility to support + // this with prepared statements yet. + for (final arg in statements.arguments) { + await tx.execute( + statements.statements[arg.statementIndex], arg.arguments); + } + }); + } + + @override + Future runCustom(String statement, List args) { + return db.execute(statement, args); + } + + @override + Future runInsert(String statement, List args) async { + return db.writeLock((tx) async { + await tx.execute(statement, args); + final row = await tx.get('SELECT last_insert_rowid() as row_id'); + return row['row_id']; + }); + } + + @override + Future runSelect(String statement, List args) async { + ResultSet result; + if (_returningCheck.hasMatch(statement)) { + // Could be "INSERT INTO ... RETURNING *" (or update or delete), + // so we need to use execute() instead of getAll(). + // This takes write lock, so we want to avoid it for plain select statements. + // This is not an exhaustive check, but should cover all Drift-generated queries using + // `runSelect()`. + result = await db.execute(statement, args); + } else { + // Plain SELECT statement - use getAll() to avoid using a write lock. + result = await db.getAll(statement, args); + } + return QueryResult(result.columnNames, result.rows); + } + + @override + Future runUpdate(String statement, List args) { + return db.writeLock((tx) async { + await tx.execute(statement, args); + final row = await tx.get('SELECT changes() as changes'); + return row['changes']; + }); + } +} + +class _SqliteAsyncVersionDelegate extends DynamicVersionDelegate { + final SqliteConnection _db; + + _SqliteAsyncVersionDelegate(this._db); + + @override + Future get schemaVersion async { + final result = await _db.get('PRAGMA user_version;'); + return result['user_version']; + } + + @override + Future setSchemaVersion(int version) async { + await _db.execute('PRAGMA user_version = $version;'); + } +} + +/// A query executor that uses sqlite_async internally. +/// In most cases, SqliteAsyncConnection should be used instead, as it handles +/// stream queries automatically. +/// +/// Wraps a sqlite_async [SqliteConnection] as a Drift [QueryExecutor]. +/// +/// The SqliteConnection must be instantiated before constructing this, and +/// is not closed when [SqliteAsyncQueryExecutor.close] is called. +/// +/// This class handles delegating Drift's queries and transactions to the +/// [SqliteConnection]. +/// +/// Extnral update notifications from the [SqliteConnection] are _not_ forwarded +/// automatically - use [SqliteAsyncDriftConnection] for that. +class SqliteAsyncQueryExecutor extends DelegatedDatabase { + SqliteAsyncQueryExecutor(SqliteConnection db) + : super( + _SqliteAsyncDelegate(db), + ); + + /// The underlying SqliteConnection used by drift to send queries. + SqliteConnection get db { + return (delegate as _SqliteAsyncDelegate).db; + } + + @override + bool get isSequential => false; + + @override + TransactionExecutor beginTransaction() { + return SqliteAsyncTransactionExecutor(db); + } +} diff --git a/packages/drift_sqlite_async/lib/src/transaction_executor.dart b/packages/drift_sqlite_async/lib/src/transaction_executor.dart new file mode 100644 index 0000000..ee4db9c --- /dev/null +++ b/packages/drift_sqlite_async/lib/src/transaction_executor.dart @@ -0,0 +1,193 @@ +import 'dart:async'; + +import 'package:drift/backends.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +/// Based on Drift's _WrappingTransactionExecutor, which is private. +/// Extended to support nested transactions. +/// +/// The outer SqliteAsyncTransactionExecutor uses sqlite_async's writeTransaction, which +/// does BEGIN/COMMIT/ROLLBACK. +/// +/// Nested transactions use SqliteAsyncNestedTransactionExecutor to implement SAVEPOINT / ROLLBACK. +class SqliteAsyncTransactionExecutor extends TransactionExecutor + with _TransactionQueryMixin { + final SqliteConnection _db; + static final _artificialRollback = + Exception('artificial exception to rollback the transaction'); + final Zone _createdIn = Zone.current; + final Completer _completerForCallback = Completer(); + Completer? _opened, _finished; + + /// Whether this executor has explicitly been closed. + bool _closed = false; + + @override + late SqliteWriteContext ctx; + + SqliteAsyncTransactionExecutor(this._db); + + void _checkCanOpen() { + if (_closed) { + throw StateError( + "A tranaction was used after being closed. Please check that you're " + 'awaiting all database operations inside a `transaction` block.'); + } + } + + @override + Future ensureOpen(QueryExecutorUser user) { + _checkCanOpen(); + var opened = _opened; + + if (opened == null) { + _opened = opened = Completer(); + _createdIn.run(() async { + final result = _db.writeTransaction((innerCtx) async { + opened!.complete(); + ctx = innerCtx; + await _completerForCallback.future; + }); + + _finished = Completer() + ..complete( + // ignore: void_checks + result + // Ignore the exception caused by [rollback] which may be + // rethrown by startTransaction + .onError((error, stackTrace) => null, + test: (e) => e == _artificialRollback) + // Consider this transaction closed after the call completes + // This may happen without send/rollback being called in + // case there's an exception when opening the transaction. + .whenComplete(() => _closed = true), + ); + }); + } + + // The opened completer is never completed if `startTransaction` throws + // before our callback is invoked (probably becaue `BEGIN` threw an + // exception). In that case, _finished will complete with that error though. + return Future.any([opened.future, if (_finished != null) _finished!.future]) + .then((value) => true); + } + + @override + Future send() async { + // don't do anything if the transaction completes before it was opened + if (_opened == null || _closed) return; + + _completerForCallback.complete(); + _closed = true; + await _finished?.future; + } + + @override + Future rollback() async { + // Note: This may be called after send() if send() throws (that is, the + // transaction can't be completed). But if completing fails, we assume that + // the transaction will implicitly be rolled back the underlying connection + // (it's not like we could explicitly roll it back, we only have one + // callback to implement). + if (_opened == null || _closed) return; + + _completerForCallback.completeError(_artificialRollback); + _closed = true; + await _finished?.future; + } + + @override + TransactionExecutor beginTransaction() { + return SqliteAsyncNestedTransactionExecutor(ctx, 1); + } + + @override + SqlDialect get dialect => SqlDialect.sqlite; + + @override + bool get supportsNestedTransactions => true; +} + +class SqliteAsyncNestedTransactionExecutor extends TransactionExecutor + with _TransactionQueryMixin { + @override + final SqliteWriteContext ctx; + + int depth; + + SqliteAsyncNestedTransactionExecutor(this.ctx, this.depth); + + @override + Future ensureOpen(QueryExecutorUser user) async { + await ctx.execute('SAVEPOINT tx$depth'); + return true; + } + + @override + Future send() async { + await ctx.execute('RELEASE SAVEPOINT tx$depth'); + } + + @override + Future rollback() async { + await ctx.execute('ROLLBACK TO SAVEPOINT tx$depth'); + } + + @override + TransactionExecutor beginTransaction() { + return SqliteAsyncNestedTransactionExecutor(ctx, depth + 1); + } + + @override + SqlDialect get dialect => SqlDialect.sqlite; + + @override + bool get supportsNestedTransactions => true; +} + +abstract class _QueryDelegate { + SqliteWriteContext get ctx; +} + +mixin _TransactionQueryMixin implements QueryExecutor, _QueryDelegate { + @override + Future runBatched(BatchedStatements statements) async { + // sqlite_async's batch functionality doesn't have enough flexibility to support + // this with prepared statements yet. + for (final arg in statements.arguments) { + await ctx.execute( + statements.statements[arg.statementIndex], arg.arguments); + } + } + + @override + Future runCustom(String statement, [List? args]) { + return ctx.execute(statement, args ?? const []); + } + + @override + Future runInsert(String statement, List args) async { + await ctx.execute(statement, args); + final row = await ctx.get('SELECT last_insert_rowid() as row_id'); + return row['row_id']; + } + + @override + Future>> runSelect( + String statement, List args) async { + final result = await ctx.execute(statement, args); + return QueryResult(result.columnNames, result.rows).asMap.toList(); + } + + @override + Future runUpdate(String statement, List args) async { + await ctx.execute(statement, args); + final row = await ctx.get('SELECT changes() as changes'); + return row['changes']; + } + + @override + Future runDelete(String statement, List args) { + return runUpdate(statement, args); + } +} diff --git a/packages/drift_sqlite_async/pubspec.yaml b/packages/drift_sqlite_async/pubspec.yaml new file mode 100644 index 0000000..54e1641 --- /dev/null +++ b/packages/drift_sqlite_async/pubspec.yaml @@ -0,0 +1,18 @@ +name: drift_sqlite_async +version: 0.1.0-alpha.1 +homepage: https://github.com/powersync-ja/drift_sqlite_async/ +repository: https://github.com/powersync-ja/drift_sqlite_async/ +description: Use Drift with a sqlite_async database, allowing both to be used in the same application. + +environment: + sdk: ">=3.0.0 <4.0.0" +dependencies: + drift: ^2.15.0 + sqlite_async: ^0.8.0 +dev_dependencies: + build_runner: ^2.4.8 + drift_dev: ^2.15.0 + glob: ^2.1.2 + sqlite3: ^2.4.0 + test: ^1.25.2 + test_api: ^0.7.0 diff --git a/packages/drift_sqlite_async/pubspec_overrides.yaml b/packages/drift_sqlite_async/pubspec_overrides.yaml new file mode 100644 index 0000000..6048d57 --- /dev/null +++ b/packages/drift_sqlite_async/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: sqlite_async +dependency_overrides: + sqlite_async: + path: ../sqlite_async diff --git a/packages/drift_sqlite_async/test/basic_test.dart b/packages/drift_sqlite_async/test/basic_test.dart new file mode 100644 index 0000000..d3d35f1 --- /dev/null +++ b/packages/drift_sqlite_async/test/basic_test.dart @@ -0,0 +1,212 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test/test.dart'; + +import './utils/test_utils.dart'; + +class EmptyDatabase extends GeneratedDatabase { + EmptyDatabase(super.executor); + + @override + Iterable> get allTables => []; + + @override + int get schemaVersion => 1; +} + +void main() { + group('Basic Tests', () { + late String path; + late SqliteDatabase db; + late SqliteAsyncDriftConnection connection; + late EmptyDatabase dbu; + + createTables(SqliteDatabase db) async { + await db.writeTransaction((tx) async { + await tx.execute( + 'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT)'); + }); + } + + setUp(() async { + path = dbPath(); + await cleanDb(path: path); + + db = await setupDatabase(path: path); + connection = SqliteAsyncDriftConnection(db); + dbu = EmptyDatabase(connection); + await createTables(db); + }); + + tearDown(() async { + await dbu.close(); + await db.close(); + + await cleanDb(path: path); + }); + + test('INSERT/SELECT', () async { + final insertRowId = await dbu.customInsert( + 'INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + expect(insertRowId, greaterThanOrEqualTo(1)); + + final result = await dbu + .customSelect('SELECT description FROM test_data') + .getSingle(); + expect(result.data, equals({'description': 'Test Data'})); + }); + + test('INSERT RETURNING', () async { + final row = await dbu.customSelect( + 'INSERT INTO test_data(description) VALUES(?) RETURNING *', + variables: [Variable('Test Data')]).getSingle(); + expect(row.data['description'], equals('Test Data')); + }); + + test('Flat transaction', () async { + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + + // This runs outside the transaction - should not see the insert + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + + // This runs in the transaction - should see the insert + expect( + (await dbu + .customSelect('select count(*) as count from test_data') + .getSingle()) + .data, + equals({'count': 1})); + }); + + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 1})); + }); + + test('Flat transaction rollback', () async { + final testException = Exception('abort'); + + try { + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + + throw testException; + }); + + // ignore: dead_code + throw Exception('Exception expected'); + } catch (e) { + expect(e, equals(testException)); + } + + // Rolled back - no data persisted + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + }); + + test('Nested transaction', () async { + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 1')]); + + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 2')]); + }); + + // This runs outside the transaction + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + }); + + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 2})); + }); + + test('Nested transaction rollback', () async { + final testException = Exception('abort'); + + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 1')]); + + try { + await dbu.transaction(() async { + await dbu.customInsert( + 'INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 2')]); + + throw testException; + }); + + // ignore: dead_code + throw Exception('Exception expected'); + } catch (e) { + expect(e, equals(testException)); + } + + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 3')]); + + // This runs outside the transaction + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + }); + + expect( + await db + .getAll('select description from test_data order by description'), + equals([ + {'description': 'Test 1'}, + {'description': 'Test 3'} + ])); + }); + + test('Concurrent select', () async { + var completer1 = Completer(); + var completer2 = Completer(); + + final tx1 = dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + + completer2.complete(); + + // Stay in the transaction until the check below completed. + await completer1.future; + }); + + await completer2.future; + try { + // This times out if concurrent select is not supported + expect( + (await dbu + .customSelect('select count(*) as count from test_data') + .getSingle() + .timeout(const Duration(milliseconds: 500))) + .data, + equals({'count': 0})); + } finally { + completer1.complete(); + } + await tx1; + + expect( + (await dbu + .customSelect('select count(*) as count from test_data') + .getSingle()) + .data, + equals({'count': 1})); + }); + }); +} diff --git a/packages/drift_sqlite_async/test/db_test.dart b/packages/drift_sqlite_async/test/db_test.dart new file mode 100644 index 0000000..0b3dfef --- /dev/null +++ b/packages/drift_sqlite_async/test/db_test.dart @@ -0,0 +1,95 @@ +import 'package:drift/drift.dart'; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test/test.dart'; + +import './utils/test_utils.dart'; +import 'generated/database.dart'; + +void main() { + group('Generated DB tests', () { + late String path; + late SqliteDatabase db; + late TodoDatabase dbu; + + createTables(SqliteDatabase db) async { + await db.writeTransaction((tx) async { + await tx.execute( + 'CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT)'); + }); + } + + setUp(() async { + path = dbPath(); + await cleanDb(path: path); + + db = await setupDatabase(path: path); + dbu = TodoDatabase(db); + await createTables(db); + }); + + tearDown(() async { + await dbu.close(); + await db.close(); + + await cleanDb(path: path); + }); + + test('INSERT/SELECT', () async { + var insertRowId = await dbu + .into(dbu.todoItems) + .insert(TodoItemsCompanion.insert(description: 'Test 1')); + expect(insertRowId, greaterThanOrEqualTo(1)); + + final result = await dbu.select(dbu.todoItems).getSingle(); + expect(result.description, equals('Test 1')); + }); + + test('watch', () async { + var stream = dbu.select(dbu.todoItems).watch(); + var resultsPromise = + stream.distinct().skipWhile((e) => e.isEmpty).take(3).toList(); + + await dbu.into(dbu.todoItems).insert( + TodoItemsCompanion.insert(id: Value(1), description: 'Test 1')); + + await Future.delayed(Duration(milliseconds: 100)); + await (dbu.update(dbu.todoItems)) + .write(TodoItemsCompanion(description: Value('Test 1B'))); + + await Future.delayed(Duration(milliseconds: 100)); + await (dbu.delete(dbu.todoItems).go()); + + var results = await resultsPromise.timeout(Duration(milliseconds: 500)); + expect( + results, + equals([ + [TodoItem(id: 1, description: 'Test 1')], + [TodoItem(id: 1, description: 'Test 1B')], + [] + ])); + }); + + test('watch with external updates', () async { + var stream = dbu.select(dbu.todoItems).watch(); + var resultsPromise = + stream.distinct().skipWhile((e) => e.isEmpty).take(3).toList(); + + await db.execute( + 'INSERT INTO todos(id, description) VALUES(?, ?)', [1, 'Test 1']); + await Future.delayed(Duration(milliseconds: 100)); + await db.execute( + 'UPDATE todos SET description = ? WHERE id = ?', ['Test 1B', 1]); + await Future.delayed(Duration(milliseconds: 100)); + await db.execute('DELETE FROM todos WHERE id = 1'); + + var results = await resultsPromise.timeout(Duration(milliseconds: 500)); + expect( + results, + equals([ + [TodoItem(id: 1, description: 'Test 1')], + [TodoItem(id: 1, description: 'Test 1B')], + [] + ])); + }); + }); +} diff --git a/packages/drift_sqlite_async/test/generated/database.dart b/packages/drift_sqlite_async/test/generated/database.dart new file mode 100644 index 0000000..e955c3d --- /dev/null +++ b/packages/drift_sqlite_async/test/generated/database.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +part 'database.g.dart'; + +class TodoItems extends Table { + @override + String get tableName => 'todos'; + + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +@DriftDatabase(tables: [TodoItems]) +class TodoDatabase extends _$TodoDatabase { + TodoDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; +} diff --git a/packages/drift_sqlite_async/test/generated/database.g.dart b/packages/drift_sqlite_async/test/generated/database.g.dart new file mode 100644 index 0000000..2572c32 --- /dev/null +++ b/packages/drift_sqlite_async/test/generated/database.g.dart @@ -0,0 +1,189 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $TodoItemsTable extends TodoItems + with TableInfo<$TodoItemsTable, TodoItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TodoItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, description]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'todos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TodoItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodoItem( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + ); + } + + @override + $TodoItemsTable createAlias(String alias) { + return $TodoItemsTable(attachedDatabase, alias); + } +} + +class TodoItem extends DataClass implements Insertable { + final int id; + final String description; + const TodoItem({required this.id, required this.description}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['description'] = Variable(description); + return map; + } + + TodoItemsCompanion toCompanion(bool nullToAbsent) { + return TodoItemsCompanion( + id: Value(id), + description: Value(description), + ); + } + + factory TodoItem.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoItem( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + }; + } + + TodoItem copyWith({int? id, String? description}) => TodoItem( + id: id ?? this.id, + description: description ?? this.description, + ); + @override + String toString() { + return (StringBuffer('TodoItem(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, description); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodoItem && + other.id == this.id && + other.description == this.description); +} + +class TodoItemsCompanion extends UpdateCompanion { + final Value id; + final Value description; + const TodoItemsCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + }); + TodoItemsCompanion.insert({ + this.id = const Value.absent(), + required String description, + }) : description = Value(description); + static Insertable custom({ + Expression? id, + Expression? description, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (description != null) 'description': description, + }); + } + + TodoItemsCompanion copyWith({Value? id, Value? description}) { + return TodoItemsCompanion( + id: id ?? this.id, + description: description ?? this.description, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodoItemsCompanion(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } +} + +abstract class _$TodoDatabase extends GeneratedDatabase { + _$TodoDatabase(QueryExecutor e) : super(e); + late final $TodoItemsTable todoItems = $TodoItemsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todoItems]; +} diff --git a/packages/drift_sqlite_async/test/utils/test_utils.dart b/packages/drift_sqlite_async/test/utils/test_utils.dart new file mode 100644 index 0000000..1128c91 --- /dev/null +++ b/packages/drift_sqlite_async/test/utils/test_utils.dart @@ -0,0 +1,98 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:glob/glob.dart'; +import 'package:glob/list_local_fs.dart'; +import 'package:sqlite3/open.dart' as sqlite_open; +import 'package:sqlite_async/sqlite3.dart' as sqlite; +import 'package:sqlite_async/sqlite3_common.dart'; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test_api/src/backend/invoker.dart'; + +const defaultSqlitePath = 'libsqlite3.so.0'; +// const defaultSqlitePath = './sqlite-autoconf-3410100/.libs/libsqlite3.so.0'; + +class TestSqliteOpenFactory extends DefaultSqliteOpenFactory { + String sqlitePath; + + TestSqliteOpenFactory( + {required super.path, + super.sqliteOptions, + this.sqlitePath = defaultSqlitePath}); + + @override + CommonDatabase open(SqliteOpenOptions options) { + sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.linux, () { + return DynamicLibrary.open(sqlitePath); + }); + final db = super.open(options); + + db.createFunction( + functionName: 'test_sleep', + argumentCount: const sqlite.AllowedArgumentCount(1), + function: (args) { + final millis = args[0] as int; + sleep(Duration(milliseconds: millis)); + return millis; + }, + ); + + db.createFunction( + functionName: 'test_connection_name', + argumentCount: const sqlite.AllowedArgumentCount(0), + function: (args) { + return Isolate.current.debugName; + }, + ); + + return db; + } +} + +DefaultSqliteOpenFactory testFactory({String? path}) { + return TestSqliteOpenFactory(path: path ?? dbPath()); +} + +Future setupDatabase({String? path}) async { + final db = SqliteDatabase.withFactory(testFactory(path: path)); + await db.initialize(); + return db; +} + +Future cleanDb({required String path}) async { + try { + await File(path).delete(); + } on PathNotFoundException { + // Not an issue + } + try { + await File("$path-shm").delete(); + } on PathNotFoundException { + // Not an issue + } + try { + await File("$path-wal").delete(); + } on PathNotFoundException { + // Not an issue + } +} + +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; + var testShortName = + testName.replaceAll(RegExp(r'[\s\./]'), '_').toLowerCase(); + var dbName = "test-db/$testShortName.db"; + Directory("test-db").createSync(recursive: false); + return dbName; +} From 6fe8765e8f08ab789b373a39041305088e8ec249 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Thu, 4 Jul 2024 18:58:17 +0200 Subject: [PATCH 04/17] fix actions --- .github/workflows/release.yml | 2 +- .github/workflows/test.yaml | 16 ++++++++-------- melos.yaml | 6 ++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be70530..356835d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - uses: dart-lang/setup-dart@v1 - name: Install Melos - run: flutter pub global activate melos + run: dart pub global activate melos - name: Install dependencies run: melos prepare diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7f05a8d..769fe0e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,18 +12,18 @@ jobs: - uses: actions/checkout@v3 - uses: dart-lang/setup-dart@v1 + - name: Install Melos + run: dart pub global activate melos - name: Install dependencies - run: dart pub get + run: melos prepare - name: Check formatting - run: dart format --output=none --set-exit-if-changed . + run: melos format:check:packages - name: Lint - run: dart analyze + run: melos analyze:packages - name: Publish dry-run - run: dart pub publish --dry-run + run: melos publish --dry-run --yes - name: Check publish score - run: | - dart pub global activate pana - dart pub global run pana --no-warning --exit-code-threshold 0 + run: melos analyze:packages:pana test: runs-on: ubuntu-latest @@ -52,7 +52,7 @@ jobs: sdk: ${{ matrix.dart_sdk }} - name: Install Melos - run: flutter pub global activate melos + run: dart pub global activate melos - name: Install dependencies run: melos prepare diff --git a/melos.yaml b/melos.yaml index d70f26f..19d45ad 100644 --- a/melos.yaml +++ b/melos.yaml @@ -26,6 +26,12 @@ scripts: description: Analyze Dart code in packages. run: dart analyze packages --fatal-infos + analyze:packages:pana: + description: Analyze Dart packages with Pana + exec: dart pub global run pana --no-warning --exit-code-threshold 0 + packageFilters: + noPrivate: true + test: description: Run tests in a specific package. run: dart test -p chrome,vm From 5d0fd6478c535efbaaaa2bb9fd8a4ea998baba48 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Thu, 4 Jul 2024 19:03:58 +0200 Subject: [PATCH 05/17] more test fixes --- .github/workflows/test.yaml | 4 +++- packages/drift_sqlite_async/test/basic_test.dart | 2 ++ packages/drift_sqlite_async/test/db_test.dart | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 769fe0e..85c9c02 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,9 @@ jobs: - name: Publish dry-run run: melos publish --dry-run --yes - name: Check publish score - run: melos analyze:packages:pana + run: | + dart pub global activate pana + melos analyze:packages:pana test: runs-on: ubuntu-latest diff --git a/packages/drift_sqlite_async/test/basic_test.dart b/packages/drift_sqlite_async/test/basic_test.dart index d3d35f1..503604f 100644 --- a/packages/drift_sqlite_async/test/basic_test.dart +++ b/packages/drift_sqlite_async/test/basic_test.dart @@ -1,3 +1,5 @@ +// TODO +@TestOn('!browser') import 'dart:async'; import 'package:drift/drift.dart'; diff --git a/packages/drift_sqlite_async/test/db_test.dart b/packages/drift_sqlite_async/test/db_test.dart index 0b3dfef..ed901cf 100644 --- a/packages/drift_sqlite_async/test/db_test.dart +++ b/packages/drift_sqlite_async/test/db_test.dart @@ -1,3 +1,5 @@ +// TODO +@TestOn('!browser') import 'package:drift/drift.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; From 394c406859de781b97931e44d9ef23292dc35784 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 08:51:46 +0200 Subject: [PATCH 06/17] feat: use navigator locks --- .../sqlite_async/lib/src/common/mutex.dart | 8 +- .../sqlite_async/lib/src/impl/stub_mutex.dart | 4 + .../sqlite_async/lib/src/web/web_mutex.dart | 104 +++++++++++++++- packages/sqlite_async/pubspec.yaml | 1 + packages/sqlite_async/test/mutex_test.dart | 113 ++++++++++-------- .../test/native/native_mutex_test.dart | 67 +++++++++++ .../sqlite_async/test/web/web_mutex_test.dart | 29 +++++ 7 files changed, 268 insertions(+), 58 deletions(-) create mode 100644 packages/sqlite_async/test/native/native_mutex_test.dart create mode 100644 packages/sqlite_async/test/web/web_mutex_test.dart diff --git a/packages/sqlite_async/lib/src/common/mutex.dart b/packages/sqlite_async/lib/src/common/mutex.dart index ccdcc49..689f2b7 100644 --- a/packages/sqlite_async/lib/src/common/mutex.dart +++ b/packages/sqlite_async/lib/src/common/mutex.dart @@ -1,8 +1,12 @@ import 'package:sqlite_async/src/impl/mutex_impl.dart'; abstract class Mutex { - factory Mutex() { - return MutexImpl(); + factory Mutex( + { + /// An optional identifier for this Mutex instance. + /// This could be used for platform specific logic or debugging purposes. + String? identifier}) { + return MutexImpl(identifier: identifier); } /// timeout is a timeout for acquiring the lock, not for the callback diff --git a/packages/sqlite_async/lib/src/impl/stub_mutex.dart b/packages/sqlite_async/lib/src/impl/stub_mutex.dart index 1e700fa..aefb9e6 100644 --- a/packages/sqlite_async/lib/src/impl/stub_mutex.dart +++ b/packages/sqlite_async/lib/src/impl/stub_mutex.dart @@ -1,6 +1,10 @@ import 'package:sqlite_async/src/common/mutex.dart'; class MutexImpl implements Mutex { + String? identifier; + + MutexImpl({this.identifier}); + @override Future close() { throw UnimplementedError(); diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index b5722c5..e583be0 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -1,13 +1,36 @@ +import 'dart:async'; +import 'dart:js_interop_unsafe'; +import 'dart:math'; + import 'package:mutex/mutex.dart' as mutex; import 'package:sqlite_async/src/common/mutex.dart'; +import 'dart:js_interop'; +import 'package:web/web.dart'; + +@JS('navigator') +external Navigator get _navigator; + +@JS('AbortController') +external AbortController get _abortController; /// Web implementation of [Mutex] -/// This should use `navigator.locks` in future class MutexImpl implements Mutex { - late final mutex.Mutex m; + late final mutex.Mutex fallback; + String? identifier; + String _resolvedIdentifier; + + MutexImpl({this.identifier}) - MutexImpl() { - m = mutex.Mutex(); + /// On web a lock name is required for Navigator locks. + /// Having exclusive Mutex instances requires a somewhat unique lock name. + /// This provides a best effort unique identifier, if no identifier is provided. + /// This should be fine for most use cases: + /// - The uuid package could be added for better uniqueness if required. + /// - This would add another package dependency to `sqlite_async` which is potentially unnecessary at this point. + /// An identifier should be supplied for better exclusion. + : _resolvedIdentifier = identifier ?? + "${DateTime.now().microsecondsSinceEpoch}-${Random().nextDouble()}" { + fallback = mutex.Mutex(); } @override @@ -17,8 +40,77 @@ class MutexImpl implements Mutex { @override Future lock(Future Function() callback, {Duration? timeout}) { - // Note this lock is only valid in a single web tab - return m.protect(callback); + if ((_navigator as JSObject).hasProperty('locks'.toJS).toDart) { + return _webLock(callback, timeout: timeout); + } else { + return _fallbackLock(callback, timeout: timeout); + } + } + + Future _fallbackLock(Future Function() callback, + {Duration? timeout}) { + final completer = Completer(); + // Need to implement timeout manually for this + bool isTimedOut = false; + bool lockObtained = false; + if (timeout != null) { + Future.delayed(timeout, () { + isTimedOut = true; + if (lockObtained == false) { + completer.completeError(LockError('Timeout reached')); + } + }); + } + + fallback.protect(() async { + try { + if (isTimedOut) { + // Don't actually run logic + return; + } + lockObtained = true; + final result = await callback(); + completer.complete(result); + } catch (ex) { + completer.completeError(ex); + } + }); + + return completer.future; + } + + Future _webLock(Future Function() callback, {Duration? timeout}) { + final completer = Completer(); + // Navigator locks can be timed out by using an AbortSignal + final controller = AbortController(); + + bool lockAcquired = false; + if (timeout != null) { + // Can't really abort the `delayed` call easily :( + Future.delayed(timeout, () { + if (lockAcquired == true) { + return; + } + completer.completeError(LockError('Timeout reached')); + controller.abort('Timeout'.toJS); + }); + } + + JSPromise jsCallback(JSAny lock) { + lockAcquired = true; + callback().then((value) { + completer.complete(value); + }).catchError((error) { + completer.completeError(error); + }); + return completer.future.toJS; + } + + final lockOptions = JSObject(); + lockOptions['signal'] = controller.signal; + _navigator.locks.request(_resolvedIdentifier, lockOptions, jsCallback.toJS); + + return completer.future; } @override diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 4523ae7..89ab26e 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: collection: ^1.17.0 mutex: ^3.1.0 meta: ^1.10.0 + web: ^0.5.1 dev_dependencies: dcli: ^4.0.0 diff --git a/packages/sqlite_async/test/mutex_test.dart b/packages/sqlite_async/test/mutex_test.dart index 699a877..1a89c75 100644 --- a/packages/sqlite_async/test/mutex_test.dart +++ b/packages/sqlite_async/test/mutex_test.dart @@ -1,67 +1,80 @@ -@TestOn('!browser') -import 'dart:isolate'; +import 'dart:math'; -import 'package:sqlite_async/src/native/native_isolate_mutex.dart'; +import 'package:sqlite_async/sqlite_async.dart'; import 'package:test/test.dart'; +import 'utils/test_utils_impl.dart'; + +final testUtils = TestUtils(); + void main() { - group('Mutex Tests', () { - test('Closing', () async { - // Test that locks are properly released when calling SharedMutex.close() - // in in Isolate. - // A timeout in this test indicates a likely error. - for (var i = 0; i < 50; i++) { - final mutex = SimpleMutex(); - final serialized = mutex.shared; - - final result = await Isolate.run(() async { - return _lockInIsolate(serialized); + group('Shared Mutex Tests', () { + test('Queue exclusive operations', () async { + final m = Mutex(); + final collection = List.generate(10, (index) => index); + final results = []; + + final futures = collection.map((element) async { + return m.lock(() async { + // Simulate some asynchronous work + await Future.delayed(Duration(milliseconds: Random().nextInt(100))); + results.add(element); + return element; }); + }).toList(); - await mutex.lock(() async {}); + // Await all the promises + await Future.wait(futures); - expect(result, equals(5)); - } + // Check if the results are in ascending order + expect(results, equals(collection)); }); + }); - test('Re-use after closing', () async { - // Test that shared locks can be opened and closed multiple times. - final mutex = SimpleMutex(); - final serialized = mutex.shared; + test('Timeout should throw a LockError', () async { + final m = Mutex(); + m.lock(() async { + await Future.delayed(Duration(milliseconds: 300)); + }); - final result = await Isolate.run(() async { - return _lockInIsolate(serialized); - }); + await expectLater( + m.lock(() async { + print('This should not get executed'); + }, timeout: Duration(milliseconds: 200)), + throwsA((e) => e is LockError && e.message.contains('Timeout'))); + }); - final result2 = await Isolate.run(() async { - return _lockInIsolate(serialized); - }); + test('In-time timeout should function normally', () async { + final m = Mutex(); + final results = []; + m.lock(() async { + await Future.delayed(Duration(milliseconds: 100)); + results.add(1); + }); - await mutex.lock(() async {}); + await m.lock(() async { + results.add(2); + }, timeout: Duration(milliseconds: 200)); - expect(result, equals(5)); - expect(result2, equals(5)); - }); - }, timeout: const Timeout(Duration(milliseconds: 5000))); -} + expect(results, equals([1, 2])); + }); -Future _lockInIsolate( - SerializedMutex smutex, -) async { - final mutex = smutex.open(); - // Start a "thread" that repeatedly takes a lock - _infiniteLock(mutex).ignore(); - await Future.delayed(const Duration(milliseconds: 10)); - // Then close the mutex while the above loop is running. - await mutex.close(); - - return 5; -} + test('Different Mutex instances should cause separate locking', () async { + final m1 = Mutex(); + final m2 = Mutex(); -Future _infiniteLock(SharedMutex mutex) async { - while (true) { - await mutex.lock(() async { - await Future.delayed(const Duration(milliseconds: 1)); + final results = []; + final p1 = m1.lock(() async { + await Future.delayed(Duration(milliseconds: 300)); + results.add(1); }); - } + + final p2 = m2.lock(() async { + results.add(2); + }); + + await p1; + await p2; + expect(results, equals([2, 1])); + }); } diff --git a/packages/sqlite_async/test/native/native_mutex_test.dart b/packages/sqlite_async/test/native/native_mutex_test.dart new file mode 100644 index 0000000..699a877 --- /dev/null +++ b/packages/sqlite_async/test/native/native_mutex_test.dart @@ -0,0 +1,67 @@ +@TestOn('!browser') +import 'dart:isolate'; + +import 'package:sqlite_async/src/native/native_isolate_mutex.dart'; +import 'package:test/test.dart'; + +void main() { + group('Mutex Tests', () { + test('Closing', () async { + // Test that locks are properly released when calling SharedMutex.close() + // in in Isolate. + // A timeout in this test indicates a likely error. + for (var i = 0; i < 50; i++) { + final mutex = SimpleMutex(); + final serialized = mutex.shared; + + final result = await Isolate.run(() async { + return _lockInIsolate(serialized); + }); + + await mutex.lock(() async {}); + + expect(result, equals(5)); + } + }); + + test('Re-use after closing', () async { + // Test that shared locks can be opened and closed multiple times. + final mutex = SimpleMutex(); + final serialized = mutex.shared; + + final result = await Isolate.run(() async { + return _lockInIsolate(serialized); + }); + + final result2 = await Isolate.run(() async { + return _lockInIsolate(serialized); + }); + + await mutex.lock(() async {}); + + expect(result, equals(5)); + expect(result2, equals(5)); + }); + }, timeout: const Timeout(Duration(milliseconds: 5000))); +} + +Future _lockInIsolate( + SerializedMutex smutex, +) async { + final mutex = smutex.open(); + // Start a "thread" that repeatedly takes a lock + _infiniteLock(mutex).ignore(); + await Future.delayed(const Duration(milliseconds: 10)); + // Then close the mutex while the above loop is running. + await mutex.close(); + + return 5; +} + +Future _infiniteLock(SharedMutex mutex) async { + while (true) { + await mutex.lock(() async { + await Future.delayed(const Duration(milliseconds: 1)); + }); + } +} diff --git a/packages/sqlite_async/test/web/web_mutex_test.dart b/packages/sqlite_async/test/web/web_mutex_test.dart new file mode 100644 index 0000000..8eeefcf --- /dev/null +++ b/packages/sqlite_async/test/web/web_mutex_test.dart @@ -0,0 +1,29 @@ +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test/test.dart'; + +import '../utils/test_utils_impl.dart'; + +final testUtils = TestUtils(); + +void main() { + group('Web Mutex Tests', () { + test('Web should share locking with identical identifiers', () async { + final m1 = Mutex(identifier: 'sync'); + final m2 = Mutex(identifier: 'sync'); + + final results = []; + final p1 = m1.lock(() async { + results.add(1); + }); + + final p2 = m2.lock(() async { + results.add(2); + }); + + await p1; + await p2; + // It should be correctly ordered as if it was the same mutex + expect(results, equals([1, 2])); + }); + }); +} From 9e152657aa60020de02756337b608b957794f852 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 11:05:53 +0200 Subject: [PATCH 07/17] comments --- .../sqlite_async/lib/src/web/web_mutex.dart | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index e583be0..d2806b8 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -1,10 +1,12 @@ import 'dart:async'; -import 'dart:js_interop_unsafe'; import 'dart:math'; +import 'package:meta/meta.dart'; import 'package:mutex/mutex.dart' as mutex; import 'package:sqlite_async/src/common/mutex.dart'; import 'dart:js_interop'; +// This allows for checking things like hasProperty without the need for depending on the `js` package +import 'dart:js_interop_unsafe'; import 'package:web/web.dart'; @JS('navigator') @@ -17,7 +19,7 @@ external AbortController get _abortController; class MutexImpl implements Mutex { late final mutex.Mutex fallback; String? identifier; - String _resolvedIdentifier; + final String _resolvedIdentifier; MutexImpl({this.identifier}) @@ -26,7 +28,7 @@ class MutexImpl implements Mutex { /// This provides a best effort unique identifier, if no identifier is provided. /// This should be fine for most use cases: /// - The uuid package could be added for better uniqueness if required. - /// - This would add another package dependency to `sqlite_async` which is potentially unnecessary at this point. + /// This would add another package dependency to `sqlite_async` which is potentially unnecessary at this point. /// An identifier should be supplied for better exclusion. : _resolvedIdentifier = identifier ?? "${DateTime.now().microsecondsSinceEpoch}-${Random().nextDouble()}" { @@ -47,6 +49,7 @@ class MutexImpl implements Mutex { } } + /// Locks the callback with a standard Mutex from the `mutex` package Future _fallbackLock(Future Function() callback, {Duration? timeout}) { final completer = Completer(); @@ -79,8 +82,25 @@ class MutexImpl implements Mutex { return completer.future; } - Future _webLock(Future Function() callback, {Duration? timeout}) { - final completer = Completer(); + /// Locks the callback with web Navigator locks + Future _webLock(Future Function() callback, + {Duration? timeout}) async { + final lock = await _getWebLock(timeout); + try { + final result = await callback(); + return result; + } finally { + lock.release(); + } + } + + /// Passing the Dart callback directly to the JS Navigator can cause some weird + /// context related bugs. Instead the JS lock callback will return a hold on the lock + /// which is represented as a [HeldLock]. This hold can be used when wrapping the Dart + /// callback to manage the JS lock. + /// This is inspired and adapted from https://github.com/simolus3/sqlite3.dart/blob/7bdca77afd7be7159dbef70fd1ac5aa4996211a9/sqlite3_web/lib/src/locks.dart#L6 + Future _getWebLock(Duration? timeout) { + final gotLock = Completer.sync(); // Navigator locks can be timed out by using an AbortSignal final controller = AbortController(); @@ -91,26 +111,27 @@ class MutexImpl implements Mutex { if (lockAcquired == true) { return; } - completer.completeError(LockError('Timeout reached')); + gotLock.completeError(LockError('Timeout reached')); controller.abort('Timeout'.toJS); }); } + // If timeout occurred before the lock is available, then this callback should not be called. JSPromise jsCallback(JSAny lock) { + // Mark that if the timeout occurs after this point then nothing should be done lockAcquired = true; - callback().then((value) { - completer.complete(value); - }).catchError((error) { - completer.completeError(error); - }); - return completer.future.toJS; + + // Give the Held lock something to mark this Navigator lock as completed + final jsCompleter = Completer.sync(); + gotLock.complete(HeldLock._(jsCompleter)); + return jsCompleter.future.toJS; } final lockOptions = JSObject(); lockOptions['signal'] = controller.signal; _navigator.locks.request(_resolvedIdentifier, lockOptions, jsCallback.toJS); - return completer.future; + return gotLock.future; } @override @@ -118,3 +139,15 @@ class MutexImpl implements Mutex { return this; } } + +/// This represents a hold on an active Navigator lock. +/// This is created inside the Navigator lock callback function and is used to release the lock +/// from an external source. +@internal +class HeldLock { + final Completer _completer; + + HeldLock._(this._completer); + + void release() => _completer.complete(); +} From e05ce933c3466ca9dc42e49c4ba5466075dce928 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 11:28:33 +0200 Subject: [PATCH 08/17] cleanup --- README.md | 10 +++++----- packages/drift_sqlite_async/pubspec_overrides.yaml | 4 ---- packages/sqlite_async/lib/src/web/web_mutex.dart | 3 --- .../lib/src/web/web_sqlite_open_factory.dart | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 packages/drift_sqlite_async/pubspec_overrides.yaml diff --git a/README.md b/README.md index c6d60e6..e3e15bf 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ High-performance asynchronous interface for SQLite on Dart & Flutter. -| package | build | pub | likes | popularity | pub points | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| sqlite_async | [![build](https://github.com/powersync-ja/sqlite_async.dart/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/powersync-ja/sqlite_async.dart/actions?query=workflow%3Atest) | [![pub package](https://img.shields.io/pub/v/sqlite_async.svg)](https://pub.dev/packages/sqlite_async) | [![likes](https://img.shields.io/pub/likes/powersync?logo=dart)](https://pub.dev/packages/sqlite_async/score) | [![popularity](https://img.shields.io/pub/popularity/sqlite_async?logo=dart)](https://pub.dev/packages/sqlite_async/score) | [![pub points](https://img.shields.io/pub/points/sqlite_async?logo=dart)](https://pub.dev/packages/sqlite_async/score) | -| drift_sqlite_async | [![build](https://github.com/powersync-ja/sqlite_async.dart/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/powersync-ja/sqlite_async/actions?query=workflow%3Atest) | [![pub package](https://img.shields.io/pub/v/drift_sqlite_async.svg)](https://pub.dev/packages/drift_sqlite_async) | [![likes](https://img.shields.io/pub/likes/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) | [![popularity](https://img.shields.io/pub/popularity/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) | [![pub points](https://img.shields.io/pub/points/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) | +| package | build | pub | likes | popularity | pub points | +|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| ------- | ------- | +| sqlite_async | [![build](https://github.com/powersync-ja/sqlite_async.dart/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/powersync-ja/sqlite_async.dart/actions?query=workflow%3Atest) | [![pub package](https://img.shields.io/pub/v/sqlite_async.svg)](https://pub.dev/packages/sqlite_async) | [![likes](https://img.shields.io/pub/likes/powersync?logo=dart)](https://pub.dev/packages/sqlite_async/score) | [![popularity](https://img.shields.io/pub/popularity/sqlite_async?logo=dart)](https://pub.dev/packages/sqlite_async/score) | [![pub points](https://img.shields.io/pub/points/sqlite_async?logo=dart)](https://pub.dev/packages/sqlite_async/score) +| drift_sqlite_async | [![build](https://github.com/powersync-ja/sqlite_async.dart/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/powersync-ja/sqlite_async/actions?query=workflow%3Atest) | [![pub package](https://img.shields.io/pub/v/drift_sqlite_async.svg)](https://pub.dev/packages/drift_sqlite_async) | [![likes](https://img.shields.io/pub/likes/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) | [![popularity](https://img.shields.io/pub/popularity/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) | [![pub points](https://img.shields.io/pub/points/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) ## Getting Started @@ -13,4 +13,4 @@ This monorepo uses [melos](https://melos.invertase.dev/) to handle command and p To configure the monorepo for development run `melos prepare` after cloning. -For detailed usage, check out the inner [sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/sqlite_async) and [drift_sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/drift_sqlite_async) packages. +For detailed usage, check out the inner [sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/sqlite_async) and [drift_sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/drift_sqlite_async) packages. \ No newline at end of file diff --git a/packages/drift_sqlite_async/pubspec_overrides.yaml b/packages/drift_sqlite_async/pubspec_overrides.yaml deleted file mode 100644 index 6048d57..0000000 --- a/packages/drift_sqlite_async/pubspec_overrides.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# melos_managed_dependency_overrides: sqlite_async -dependency_overrides: - sqlite_async: - path: ../sqlite_async diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index d2806b8..1086ae8 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -12,9 +12,6 @@ import 'package:web/web.dart'; @JS('navigator') external Navigator get _navigator; -@JS('AbortController') -external AbortController get _abortController; - /// Web implementation of [Mutex] class MutexImpl implements Mutex { late final mutex.Mutex fallback; 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 ce7e0c9..521320b 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 @@ -55,7 +55,7 @@ class DefaultSqliteOpenFactory // cases, we need to implement a mutex locally. final mutex = connection.access == AccessMode.throughSharedWorker ? null - : MutexImpl(); + : MutexImpl(identifier: path); // Use the DB path as a mutex identifier return WebDatabase(connection.database, options.mutex ?? mutex); } From 82b83c1c9657b2b8b21b74d1553c5fc22fc07160 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 11:30:38 +0200 Subject: [PATCH 09/17] more cleanup --- packages/sqlite_async/lib/src/web/web_mutex.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index 1086ae8..42d232a 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -3,12 +3,13 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'package:mutex/mutex.dart' as mutex; -import 'package:sqlite_async/src/common/mutex.dart'; import 'dart:js_interop'; // This allows for checking things like hasProperty without the need for depending on the `js` package import 'dart:js_interop_unsafe'; import 'package:web/web.dart'; +import 'package:sqlite_async/src/common/mutex.dart'; + @JS('navigator') external Navigator get _navigator; From 588d92f3dc98ab2629d2b420ad607639b835214b Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 11:41:46 +0200 Subject: [PATCH 10/17] fix tests --- .../sqlite_async/lib/src/native/native_isolate_mutex.dart | 5 +++-- packages/sqlite_async/lib/src/web/web_mutex.dart | 6 ++++-- packages/sqlite_async/test/mutex_test.dart | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart b/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart index 23e5443..c42b8b8 100644 --- a/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart +++ b/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart @@ -7,7 +7,7 @@ import 'package:sqlite_async/src/common/mutex.dart'; import 'package:sqlite_async/src/common/port_channel.dart'; abstract class MutexImpl implements Mutex { - factory MutexImpl() { + factory MutexImpl({String? identifier}) { return SimpleMutex(); } } @@ -19,12 +19,13 @@ class SimpleMutex implements MutexImpl { // Adapted from https://github.com/tekartik/synchronized.dart/blob/master/synchronized/lib/src/basic_lock.dart Future? last; + String? identifier; // Hack to make sure the Mutex is not copied to another isolate. // ignore: unused_field final Finalizer _f = Finalizer((_) {}); - SimpleMutex(); + SimpleMutex({this.identifier}); bool get locked => last != null; diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index 42d232a..8d1805e 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -58,7 +58,8 @@ class MutexImpl implements Mutex { Future.delayed(timeout, () { isTimedOut = true; if (lockObtained == false) { - completer.completeError(LockError('Timeout reached')); + completer.completeError( + TimeoutException('Failed to acquire lock', timeout)); } }); } @@ -109,7 +110,8 @@ class MutexImpl implements Mutex { if (lockAcquired == true) { return; } - gotLock.completeError(LockError('Timeout reached')); + gotLock + .completeError(TimeoutException('Failed to acquire lock', timeout)); controller.abort('Timeout'.toJS); }); } diff --git a/packages/sqlite_async/test/mutex_test.dart b/packages/sqlite_async/test/mutex_test.dart index 1a89c75..b7c7448 100644 --- a/packages/sqlite_async/test/mutex_test.dart +++ b/packages/sqlite_async/test/mutex_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:sqlite_async/sqlite_async.dart'; @@ -41,7 +42,9 @@ void main() { m.lock(() async { print('This should not get executed'); }, timeout: Duration(milliseconds: 200)), - throwsA((e) => e is LockError && e.message.contains('Timeout'))); + throwsA((e) => + e is TimeoutException && + e.message!.contains('Failed to acquire lock'))); }); test('In-time timeout should function normally', () async { From e30ebf8096c9b1a492de7f40e3b7a0ce0545804c Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 16:10:59 +0200 Subject: [PATCH 11/17] fix unhandled exception --- packages/sqlite_async/lib/src/web/web_mutex.dart | 8 ++++++-- packages/sqlite_async/test/mutex_test.dart | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index 8d1805e..90375a2 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'package:mutex/mutex.dart' as mutex; import 'dart:js_interop'; +import 'dart:js_util' as js_util; // This allows for checking things like hasProperty without the need for depending on the `js` package import 'dart:js_interop_unsafe'; import 'package:web/web.dart'; @@ -129,8 +130,11 @@ class MutexImpl implements Mutex { final lockOptions = JSObject(); lockOptions['signal'] = controller.signal; - _navigator.locks.request(_resolvedIdentifier, lockOptions, jsCallback.toJS); - + final promise = _navigator.locks + .request(_resolvedIdentifier, lockOptions, jsCallback.toJS); + // A timeout abort will throw an exception which needs to be handled. + // There should not be any other unhandled lock errors. + js_util.promiseToFuture(promise).catchError((error) {}); return gotLock.future; } diff --git a/packages/sqlite_async/test/mutex_test.dart b/packages/sqlite_async/test/mutex_test.dart index b7c7448..3f492d6 100644 --- a/packages/sqlite_async/test/mutex_test.dart +++ b/packages/sqlite_async/test/mutex_test.dart @@ -32,7 +32,7 @@ void main() { }); }); - test('Timeout should throw a LockError', () async { + test('Timeout should throw a TimeoutException', () async { final m = Mutex(); m.lock(() async { await Future.delayed(Duration(milliseconds: 300)); From 4ba51e5a5bc4b6e47c4a21b658776fdce30a0548 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 16:42:06 +0200 Subject: [PATCH 12/17] cleanup --- packages/sqlite_async/lib/src/web/web_mutex.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index 90375a2..6ec7900 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -57,8 +57,8 @@ class MutexImpl implements Mutex { bool lockObtained = false; if (timeout != null) { Future.delayed(timeout, () { - isTimedOut = true; if (lockObtained == false) { + isTimedOut = true; completer.completeError( TimeoutException('Failed to acquire lock', timeout)); } From 3b78d5a290c17149dfcd44370fa5c05a40ee29c9 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 17:53:51 +0200 Subject: [PATCH 13/17] use timers --- .../sqlite_async/lib/src/web/web_mutex.dart | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index 6ec7900..4013201 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -54,14 +54,12 @@ class MutexImpl implements Mutex { final completer = Completer(); // Need to implement timeout manually for this bool isTimedOut = false; - bool lockObtained = false; + Timer? timer; if (timeout != null) { - Future.delayed(timeout, () { - if (lockObtained == false) { - isTimedOut = true; - completer.completeError( - TimeoutException('Failed to acquire lock', timeout)); - } + timer = Timer(timeout, () { + isTimedOut = true; + completer + .completeError(TimeoutException('Failed to acquire lock', timeout)); }); } @@ -71,7 +69,7 @@ class MutexImpl implements Mutex { // Don't actually run logic return; } - lockObtained = true; + timer?.cancel(); final result = await callback(); completer.complete(result); } catch (ex) { @@ -104,13 +102,10 @@ class MutexImpl implements Mutex { // Navigator locks can be timed out by using an AbortSignal final controller = AbortController(); - bool lockAcquired = false; + Timer? timer; + if (timeout != null) { - // Can't really abort the `delayed` call easily :( - Future.delayed(timeout, () { - if (lockAcquired == true) { - return; - } + timer = Timer(timeout, () { gotLock .completeError(TimeoutException('Failed to acquire lock', timeout)); controller.abort('Timeout'.toJS); @@ -119,8 +114,7 @@ class MutexImpl implements Mutex { // If timeout occurred before the lock is available, then this callback should not be called. JSPromise jsCallback(JSAny lock) { - // Mark that if the timeout occurs after this point then nothing should be done - lockAcquired = true; + timer?.cancel(); // Give the Held lock something to mark this Navigator lock as completed final jsCompleter = Completer.sync(); From 6b11376c031e58527ea730c77ac1dd90af7f5aee Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Tue, 9 Jul 2024 17:57:30 +0200 Subject: [PATCH 14/17] mutex fixes --- packages/sqlite_async/lib/src/common/mutex.dart | 2 ++ packages/sqlite_async/lib/src/native/native_isolate_mutex.dart | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sqlite_async/lib/src/common/mutex.dart b/packages/sqlite_async/lib/src/common/mutex.dart index 689f2b7..edcdd49 100644 --- a/packages/sqlite_async/lib/src/common/mutex.dart +++ b/packages/sqlite_async/lib/src/common/mutex.dart @@ -5,6 +5,8 @@ abstract class Mutex { { /// An optional identifier for this Mutex instance. /// This could be used for platform specific logic or debugging purposes. + /// Currently this is not used on native platforms. + /// On web this will be used for the lock name if Navigator locks are available. String? identifier}) { return MutexImpl(identifier: identifier); } diff --git a/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart b/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart index c42b8b8..7d82f68 100644 --- a/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart +++ b/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart @@ -8,7 +8,7 @@ import 'package:sqlite_async/src/common/port_channel.dart'; abstract class MutexImpl implements Mutex { factory MutexImpl({String? identifier}) { - return SimpleMutex(); + return SimpleMutex(identifier: identifier); } } From 0ac04f254248d73a9d65ce489d4dee1179331854 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Wed, 10 Jul 2024 18:03:38 +0200 Subject: [PATCH 15/17] melos tags --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 356835d..aa26a73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,8 @@ name: Compile Assets and Create Draft Release on: push: tags: - # Trigger on tags beginning with 'v' - - 'v*' + # Trigger on sqlite_async tags + - 'sqlite_async-v*' jobs: release: From d01cb6213f736daf6509205fd97d23ce87cdd24d Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Wed, 10 Jul 2024 18:04:05 +0200 Subject: [PATCH 16/17] chore(release): publish packages - sqlite_async@0.8.1 - drift_sqlite_async@0.1.0-alpha.3 --- CHANGELOG.md | 32 ++++++++++++++++++++++++ packages/drift_sqlite_async/CHANGELOG.md | 4 +++ packages/drift_sqlite_async/pubspec.yaml | 4 +-- packages/sqlite_async/CHANGELOG.md | 4 +++ packages/sqlite_async/pubspec.yaml | 2 +- 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7ed3924 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## 2024-07-10 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`sqlite_async` - `v0.8.1`](#sqlite_async---v081) + - [`drift_sqlite_async` - `v0.1.0-alpha.3`](#drift_sqlite_async---v010-alpha3) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `drift_sqlite_async` - `v0.1.0-alpha.3` + +--- + +#### `sqlite_async` - `v0.8.1` + + - **FEAT**: use navigator locks. + diff --git a/packages/drift_sqlite_async/CHANGELOG.md b/packages/drift_sqlite_async/CHANGELOG.md index ce4d763..11d4043 100644 --- a/packages/drift_sqlite_async/CHANGELOG.md +++ b/packages/drift_sqlite_async/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0-alpha.3 + + - Update a dependency to the latest release. + ## 0.1.0-alpha.2 - Update dependency `sqlite_async` to version 0.8.0. diff --git a/packages/drift_sqlite_async/pubspec.yaml b/packages/drift_sqlite_async/pubspec.yaml index 42d998c..66dc9bf 100644 --- a/packages/drift_sqlite_async/pubspec.yaml +++ b/packages/drift_sqlite_async/pubspec.yaml @@ -1,5 +1,5 @@ name: drift_sqlite_async -version: 0.1.0-alpha.2 +version: 0.1.0-alpha.3 homepage: https://github.com/powersync-ja/sqlite_async.dart repository: https://github.com/powersync-ja/sqlite_async.dart description: Use Drift with a sqlite_async database, allowing both to be used in the same application. @@ -15,7 +15,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: drift: ^2.15.0 - sqlite_async: ^0.8.0 + sqlite_async: ^0.8.1 dev_dependencies: build_runner: ^2.4.8 drift_dev: ^2.15.0 diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index e38d5cb..7859323 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.1 + + - **FEAT**: use navigator locks. + ## 0.8.0 - Added web support (web functionality is in beta) diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 89ab26e..58eb5df 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlite_async description: High-performance asynchronous interface for SQLite on Dart and Flutter. -version: 0.8.0 +version: 0.8.1 repository: https://github.com/powersync-ja/sqlite_async.dart environment: sdk: ">=3.4.0 <4.0.0" From d16b01a7f3d58f3c1bd1e118720711220115c450 Mon Sep 17 00:00:00 2001 From: Steven Ontong Date: Wed, 10 Jul 2024 18:05:19 +0200 Subject: [PATCH 17/17] changelog --- packages/sqlite_async/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index 7859323..cd48d89 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.8.1 - - **FEAT**: use navigator locks. + - Added Navigator locks for web `Mutex`s. ## 0.8.0