diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index cf3a887..a1e77b1 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,10 +1,10 @@ -## 0.14.0-wip.0 +## 0.14.0 __Note__: This version of `sqlite_async` is still in development and there might be additional API changes between this release and the final `0.14.0` version. This release is mostly meant for internal testing. -- Support versions 3.x of the `sqlite3` package and 0.6.0 of `sqlite3_web`. +- Support versions 3.x of the `sqlite3` package and 0.7.x of `sqlite3_web`. - Remove the `sqlite3_open.dart` library, SQLite libraries are no longer loaded through Dart. - __Breaking__: Rewrite the native connection pool implementation. - Remove isolate connection factories. Simply open the same database on another isolate, it's safe to do so now. diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 9dba389..3e464f4 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -52,8 +52,8 @@ final class WebDatabase extends SqliteDatabaseImpl @override Future getAutoCommit() async { - final response = await _database.customRequest( - CustomDatabaseMessage(CustomDatabaseMessageKind.getAutoCommit)); + final response = await _database + .customRequest(BaseCustomDatabaseMessage.getAutoCommit()); return (response as JSBoolean?)?.toDart ?? false; } @@ -274,16 +274,14 @@ final class _UnscopedContext extends UnscopedContext { Future executeBatch(String sql, List> parameterSets) { return _task.timeAsync('executeBatch', sql: sql, () { return wrapSqliteException(() async { - for (final set in parameterSets) { - // use execute instead of select to avoid transferring rows from the - // worker to this context. - await _database._database.execute( - sql, - parameters: set, - token: _lock, - checkInTransaction: _checkInTransaction, - ); - } + await _database._database.customRequest( + RunBatchRequest( + sql: sql, + parameters: parameterSets, + requireTransaction: _checkInTransaction, + ), + token: _lock, + ); }); }); } diff --git a/packages/sqlite_async/lib/src/web/protocol.dart b/packages/sqlite_async/lib/src/web/protocol.dart index 9fcbb57..08147a2 100644 --- a/packages/sqlite_async/lib/src/web/protocol.dart +++ b/packages/sqlite_async/lib/src/web/protocol.dart @@ -8,12 +8,35 @@ import 'package:sqlite3_web/protocol_utils.dart' as proto; enum CustomDatabaseMessageKind { ok, getAutoCommit, - executeBatchInTransaction, + executeBatch, updateSubscriptionManagement, notifyUpdates, } -extension type CustomDatabaseMessage._raw(JSObject _) implements JSObject { +extension type BaseCustomDatabaseMessage._raw(JSObject _) implements JSObject { + external JSString get rawKind; + + external factory BaseCustomDatabaseMessage({required JSString rawKind}); + + factory BaseCustomDatabaseMessage.getAutoCommit() { + return BaseCustomDatabaseMessage( + rawKind: CustomDatabaseMessageKind.getAutoCommit.name.toJS, + ); + } + + factory BaseCustomDatabaseMessage.okResponse() { + return BaseCustomDatabaseMessage( + rawKind: CustomDatabaseMessageKind.ok.name.toJS, + ); + } + + CustomDatabaseMessageKind get kind { + return CustomDatabaseMessageKind.values.byName(rawKind.toDart); + } +} + +extension type CustomDatabaseMessage._raw(JSObject _) + implements BaseCustomDatabaseMessage { external factory CustomDatabaseMessage._({ required JSString rawKind, JSString rawSql, @@ -38,16 +61,55 @@ extension type CustomDatabaseMessage._raw(JSObject _) implements JSObject { ); } - external JSString get rawKind; - external JSString get rawSql; external JSArray get rawParameters; /// Not set in earlier versions of this package. external JSArrayBuffer? get typeInfo; +} - CustomDatabaseMessageKind get kind { - return CustomDatabaseMessageKind.values.byName(rawKind.toDart); +extension type RunBatchRequest._raw(JSObject _) + implements BaseCustomDatabaseMessage { + external factory RunBatchRequest._({ + required JSString rawKind, + required JSString rawSql, + required JSArray parameters, + required JSBoolean requireTransaction, + }); + + factory RunBatchRequest({ + required String sql, + required List> parameters, + required bool requireTransaction, + }) { + return RunBatchRequest._( + rawKind: CustomDatabaseMessageKind.executeBatch.name.toJS, + rawSql: sql.toJS, + parameters: parameters.map(BatchParameters.new).toList().toJS, + requireTransaction: requireTransaction.toJS, + ); + } + + external JSString get rawSql; + external JSArray get parameters; + external JSBoolean get requireTransaction; +} + +extension type BatchParameters._raw(JSObject _) implements JSObject { + external JSArray get parameters; + external JSArrayBuffer get parameterTypes; + + external factory BatchParameters._({ + required JSArray parameters, + required JSArrayBuffer parameterTypes, + }); + + factory BatchParameters(List parameters) { + final (params, types) = proto.serializeParameters(parameters); + return BatchParameters._(parameters: params, parameterTypes: types); } + + List get decodedParameters => + proto.deserializeParameters(parameters, parameterTypes); } diff --git a/packages/sqlite_async/lib/src/web/update_notifications.dart b/packages/sqlite_async/lib/src/web/update_notifications.dart index f04a785..38373d4 100644 --- a/packages/sqlite_async/lib/src/web/update_notifications.dart +++ b/packages/sqlite_async/lib/src/web/update_notifications.dart @@ -20,8 +20,9 @@ final class UpdateNotificationStreams { final Map> _updates = {}; Future handleRequest(JSAny? request) async { - final customRequest = request as CustomDatabaseMessage; + final customRequest = request as BaseCustomDatabaseMessage; if (customRequest.kind == CustomDatabaseMessageKind.notifyUpdates) { + customRequest as CustomDatabaseMessage; final notification = UpdateNotification(customRequest.rawParameters.toDart .map((e) => (e as JSString).toDart) .toSet()); diff --git a/packages/sqlite_async/lib/src/web/worker/worker_utils.dart b/packages/sqlite_async/lib/src/web/worker/worker_utils.dart index 157a914..5d5cc53 100644 --- a/packages/sqlite_async/lib/src/web/worker/worker_utils.dart +++ b/packages/sqlite_async/lib/src/web/worker/worker_utils.dart @@ -4,7 +4,6 @@ import 'dart:js_interop'; import 'package:meta/meta.dart'; import 'package:sqlite3/wasm.dart'; import 'package:sqlite3_web/sqlite3_web.dart'; -import 'package:sqlite3_web/protocol_utils.dart' as proto; import 'package:sqlite_async/src/utils/shared_utils.dart'; import '../protocol.dart'; @@ -35,7 +34,7 @@ base class AsyncSqliteController extends DatabaseController { @override Future handleCustomRequest( - ClientConnection connection, JSAny? request) { + ClientConnection connection, CustomClientRequest request) { throw UnimplementedError(); } } @@ -68,8 +67,8 @@ class AsyncSqliteDatabase extends WorkerDatabase { @override Future handleCustomRequest( - ClientConnection connection, JSAny? request) async { - final message = request as CustomDatabaseMessage; + ClientConnection connection, CustomClientDatabaseRequest request) async { + final message = request.request as BaseCustomDatabaseMessage; switch (message.kind) { case CustomDatabaseMessageKind.ok: @@ -77,20 +76,30 @@ class AsyncSqliteDatabase extends WorkerDatabase { throw UnsupportedError('This is a response, not a request'); case CustomDatabaseMessageKind.getAutoCommit: return database.autocommit.toJS; - case CustomDatabaseMessageKind.executeBatchInTransaction: - final sql = message.rawSql.toDart; - final parameters = proto.deserializeParameters( - message.rawParameters, message.typeInfo); - if (database.autocommit) { - throw SqliteException( - extendedResultCode: 0, - message: - 'Transaction rolled back by earlier statement. Cannot execute', - causingStatement: sql, - ); - } - database.execute(sql, parameters); + case CustomDatabaseMessageKind.executeBatch: + final data = message as RunBatchRequest; + + await request.useLock(() { + if (data.requireTransaction.toDart && database.autocommit) { + throw SqliteException( + extendedResultCode: 0, + message: + 'Transaction rolled back by earlier statement. Cannot execute', + causingStatement: data.rawSql.toDart, + ); + } + + final stmt = database.prepare(data.rawSql.toDart); + try { + for (final parameter in data.parameters.toDart) { + stmt.execute(parameter.decodedParameters); + } + } finally { + stmt.close(); + } + }); case CustomDatabaseMessageKind.updateSubscriptionManagement: + message as CustomDatabaseMessage; final shouldSubscribe = (message.rawParameters.toDart[0] as JSBoolean).toDart; final id = message.rawSql.toDart; @@ -113,7 +122,7 @@ class AsyncSqliteDatabase extends WorkerDatabase { } } - return CustomDatabaseMessage(CustomDatabaseMessageKind.ok); + return BaseCustomDatabaseMessage.okResponse(); } Map resultSetToMap(ResultSet resultSet) { diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 4eed256..075c0b7 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.14.0-wip.0 +version: 0.14.0 resolution: workspace repository: https://github.com/powersync-ja/sqlite_async.dart environment: @@ -14,7 +14,7 @@ topics: dependencies: sqlite3: ^3.2.0 - sqlite3_web: ^0.6.0 + sqlite3_web: ^0.7.0 sqlite3_connection_pool: ^0.2.3 async: ^2.10.0 collection: ^1.17.0 diff --git a/packages/sqlite_async/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart index c9e3421..ac8958b 100644 --- a/packages/sqlite_async/test/basic_test.dart +++ b/packages/sqlite_async/test/basic_test.dart @@ -348,6 +348,50 @@ void main() { expect(result.rows[0][1], equals('test returning without params')); }); + group('executeBatch', () { + late SqliteDatabase db; + + setUp(() async { + db = await testUtils.setupDatabase(path: path); + }); + + tearDown(() => db.close()); + + test('can execute multiple times', () async { + await createTables(db); + + await db + .executeBatch('INSERT INTO test_data (description) VALUES (?)', [ + ['foo'], + ['bar'] + ]); + + final results = + await db.getAll('SELECT description FROM test_data ORDER BY id'); + expect(results.length, equals(2)); + expect(results.rows[0], equals(['foo'])); + expect(results.rows[1], equals(['bar'])); + }); + + test('can execute in transaction', () async { + await createTables(db); + const exception = 'exception thrown for rollback'; + + await expectLater(db.writeTransaction((tx) async { + await tx + .executeBatch('INSERT INTO test_data (description) VALUES (?)', [ + ['foo'], + ['bar'] + ]); + + expect(await tx.getAll('SELECT * FROM test_data'), hasLength(2)); + throw exception; + }), throwsA(exception)); + + expect(await db.getAll('SELECT * FROM test_data'), isEmpty); + }); + }); + test('executeMultiple handles multiple statements', () async { final db = await testUtils.setupDatabase(path: path); await createTables(db);