diff --git a/sqlite3_web/lib/src/client.dart b/sqlite3_web/lib/src/client.dart index 9f6b9a38..19cb85d7 100644 --- a/sqlite3_web/lib/src/client.dart +++ b/sqlite3_web/lib/src/client.dart @@ -149,6 +149,29 @@ final class RemoteDatabase implements Database { return response.asResultWithResultSet(); } + @override + Future executeMultiple( + String sql, { + bool checkInTransaction = false, + LockToken? token, + Future? abortTrigger, + }) async { + await connection.sendRequest( + RunQuery( + requestId: 0, + databaseId: databaseId, + lockId: token == null ? null : lockTokenToId(token), + sql: sql, + parameters: const [], + returnRows: false, + checkInTransaction: checkInTransaction, + multipleStatements: true, + ), + MessageType.simpleSuccessResponse, + abortTrigger: abortTrigger, + ); + } + @override Future requestLock(Future Function(LockToken token) body, {Future? abortTrigger}) async { diff --git a/sqlite3_web/lib/src/database.dart b/sqlite3_web/lib/src/database.dart index 74065773..df57bce2 100644 --- a/sqlite3_web/lib/src/database.dart +++ b/sqlite3_web/lib/src/database.dart @@ -112,6 +112,24 @@ abstract class Database { Future? abortTrigger, }); + /// Prepares each statement [sql] and executes them each, sequentially. If + /// any sequence fails, the execution is aborted without running the + /// remaining statements. + /// + /// If [checkInTransaction] is enabled, the host will verify that the + /// autocommit mode is disabled before running the statements (and report an + /// exception otherwise). + /// + /// The [abortTrigger] can be used to abort the request. When that future + /// completes before the lock has been granted, the future may complete + /// with a [AbortException] without running the statements. + Future executeMultiple( + String sql, { + bool checkInTransaction = false, + LockToken? token, + Future? abortTrigger, + }); + /// Prepares [sql], executes it with the given [parameters] and returns the /// [ResultSet]. /// diff --git a/sqlite3_web/lib/src/protocol.dart b/sqlite3_web/lib/src/protocol.dart index b4fb2fd9..3cac0624 100644 --- a/sqlite3_web/lib/src/protocol.dart +++ b/sqlite3_web/lib/src/protocol.dart @@ -123,6 +123,7 @@ class _UniqueFieldNames { static const autocommit = 'x'; static const lastInsertRowid = 'y'; static const lockId = 'z'; + static const multipleStatements = 'm'; } sealed class Message { @@ -563,6 +564,7 @@ final class RunQuery extends Request { final int? lockId; final bool returnRows; final bool checkInTransaction; + final bool multipleStatements; RunQuery({ required super.requestId, @@ -572,6 +574,7 @@ final class RunQuery extends Request { required this.lockId, required this.returnRows, required this.checkInTransaction, + this.multipleStatements = false, }); factory RunQuery.deserialize(JSObject object) { @@ -587,6 +590,9 @@ final class RunQuery extends Request { returnRows: (object[_UniqueFieldNames.returnRows] as JSBoolean).toDart, checkInTransaction: (object[_UniqueFieldNames.checkInTransaction] as JSBoolean).toDart, + multipleStatements: + (object[_UniqueFieldNames.multipleStatements] as JSBoolean?)?.toDart ?? + false, ); } @@ -611,6 +617,7 @@ final class RunQuery extends Request { } object[_UniqueFieldNames.checkInTransaction] = checkInTransaction.toJS; + object[_UniqueFieldNames.multipleStatements] = multipleStatements.toJS; } @override diff --git a/sqlite3_web/lib/src/worker.dart b/sqlite3_web/lib/src/worker.dart index ccf02e62..66d6805d 100644 --- a/sqlite3_web/lib/src/worker.dart +++ b/sqlite3_web/lib/src/worker.dart @@ -331,6 +331,11 @@ final class _ClientConnection extends ProtocolChannel final database = _requireDatabase(request); final openedDatabase = await database.database.opened; + if (request.returnRows && request.multipleStatements) { + throw ArgumentError( + 'Cannot return rows when executing multiple statements'); + } + return database.useLock(request.lockId, abortSignal, () { final db = openedDatabase.database; @@ -341,6 +346,17 @@ final class _ClientConnection extends ProtocolChannel ResultSet? resultSet; if (request.returnRows) { resultSet = db.select(request.sql, request.parameters); + } else if (request.multipleStatements) { + final statements = db.prepareMultiple(request.sql); + try { + for (var statement in statements) { + statement.execute(); + } + } finally { + for (var statement in statements) { + statement.dispose(); + } + } } else { db.execute(request.sql, request.parameters); } diff --git a/sqlite3_web/test/protocol_test.dart b/sqlite3_web/test/protocol_test.dart index 096cc16e..3480657b 100644 --- a/sqlite3_web/test/protocol_test.dart +++ b/sqlite3_web/test/protocol_test.dart @@ -137,6 +137,30 @@ void main() { ); }); + test('serializes multipleStatements flag', () async { + server.handleRequestFunction = expectAsync1((request) async { + final run = request as RunQuery; + expect(run.sql, 'CREATE TABLE a (x); CREATE TABLE b (y);'); + expect(run.multipleStatements, true); + expect(run.returnRows, false); + return SimpleSuccessResponse(requestId: request.requestId, response: null); + }); + + await client.sendRequest( + RunQuery( + requestId: 0, + databaseId: 0, + sql: 'CREATE TABLE a (x); CREATE TABLE b (y);', + checkInTransaction: false, + lockId: null, + parameters: [], + returnRows: false, + multipleStatements: true, + ), + MessageType.simpleSuccessResponse, + ); + }); + test('can serialize SqliteExceptions', () async { server.handleRequestFunction = expectAsync1((req) { throw SqliteException(