diff --git a/lib/src/web/database.dart b/lib/src/web/database.dart index b250622..c136c6c 100644 --- a/lib/src/web/database.dart +++ b/lib/src/web/database.dart @@ -4,6 +4,7 @@ import 'dart:js_interop'; import 'package:sqlite3/common.dart'; import 'package:sqlite3_web/sqlite3_web.dart'; import 'package:sqlite_async/mutex.dart'; +import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite_async/src/common/sqlite_database.dart'; import 'package:sqlite_async/src/sqlite_connection.dart'; import 'package:sqlite_async/src/sqlite_queries.dart'; @@ -84,33 +85,29 @@ class WebDatabase Stream get updates => _database.updates.map((event) => UpdateNotification({event.tableName})); - @override - Future writeLock(Future Function(SqliteWriteContext tx) callback, - {Duration? lockTimeout, String? debugContext}) async { - return _writeLock(callback, - lockTimeout: lockTimeout, debugContext: debugContext); - } - @override Future writeTransaction( Future Function(SqliteWriteContext tx) callback, {Duration? lockTimeout}) { - return _writeLock((ctx) => internalWriteTransaction(ctx, callback), + return writeLock( + (writeContext) => + internalWriteTransaction(writeContext, (context) async { + // All execute calls done in the callback will be checked for the + // autocommit state + return callback(_ExclusiveTransactionContext(this, writeContext)); + }), debugContext: 'writeTransaction()', - isTransaction: true, lockTimeout: lockTimeout); } + @override + /// Internal writeLock which intercepts transaction context's to verify auto commit is not active - Future _writeLock(Future Function(SqliteWriteContext tx) callback, - {Duration? lockTimeout, - String? debugContext, - bool isTransaction = false}) async { + Future writeLock(Future Function(SqliteWriteContext tx) callback, + {Duration? lockTimeout, String? debugContext}) async { if (_mutex case var mutex?) { return await mutex.lock(() async { - final context = isTransaction - ? _ExclusiveTransactionContext(this) - : _ExclusiveContext(this); + final context = _ExclusiveContext(this); try { return await callback(context); } finally { @@ -121,11 +118,7 @@ class WebDatabase // No custom mutex, coordinate locks through shared worker. await _database.customRequest(CustomDatabaseMessage( CustomDatabaseMessageKind.requestExclusiveLock)); - final context = isTransaction - ? _ExclusiveTransactionContext(this) - : _ExclusiveContext(this); - ; - + final context = _ExclusiveContext(this); try { return await callback(context); } finally { @@ -207,30 +200,56 @@ class _ExclusiveContext extends _SharedContext implements SqliteWriteContext { } class _ExclusiveTransactionContext extends _ExclusiveContext { - _ExclusiveTransactionContext(super.database); + SqliteWriteContext baseContext; + _ExclusiveTransactionContext(super.database, this.baseContext); + + @override + bool get closed => baseContext.closed; @override Future execute(String sql, [List parameters = const []]) async { - // TODO offload this to a custom request for single round trip - final isAutoCommit = await getAutoCommit(); - if (isAutoCommit && !sql.toLowerCase().contains('begin')) { - throw SqliteException(0, - "Transaction rolled back by earlier statement. Cannot execute: $sql"); - } - return super.execute(sql, parameters); + return await wrapSqliteException(() async { + var res = await _database._database.customRequest(CustomDatabaseMessage( + CustomDatabaseMessageKind.executeInTransaction, sql, parameters)); + var result = + Map.from((res as JSObject).dartify() as Map); + final columnNames = [ + for (final entry in result['columnNames']) entry as String + ]; + final rawTableNames = result['tableNames']; + final tableNames = rawTableNames != null + ? [ + for (final entry in (rawTableNames as List)) + entry as String + ] + : null; + + final rows = >[]; + for (final row in (result['rows'] as List)) { + final dartRow = []; + + for (final column in (row as List)) { + dartRow.add(column); + } + + rows.add(dartRow); + } + final resultSet = ResultSet(columnNames, tableNames, rows); + return resultSet; + }); } @override Future executeBatch( String sql, List> parameterSets) async { - // TODO offload this to a custom request for single round trip - final isAutoCommit = await getAutoCommit(); - if (isAutoCommit && !sql.toLowerCase().contains('begin')) { - throw SqliteException(0, - "Transaction rolled back by earlier statement. Cannot execute: $sql"); - } - return super.executeBatch(sql, parameterSets); + return await wrapSqliteException(() async { + for (final set in parameterSets) { + await _database._database.customRequest(CustomDatabaseMessage( + CustomDatabaseMessageKind.executeBatchInTransaction, sql, set)); + } + return; + }); } } diff --git a/lib/src/web/protocol.dart b/lib/src/web/protocol.dart index 583d792..e6206d6 100644 --- a/lib/src/web/protocol.dart +++ b/lib/src/web/protocol.dart @@ -10,19 +10,32 @@ enum CustomDatabaseMessageKind { releaseLock, lockObtained, getAutoCommit, + executeInTransaction, + executeBatchInTransaction, } extension type CustomDatabaseMessage._raw(JSObject _) implements JSObject { external factory CustomDatabaseMessage._({ required JSString rawKind, + JSString rawSql, + JSArray rawParameters, }); - factory CustomDatabaseMessage(CustomDatabaseMessageKind kind) { - return CustomDatabaseMessage._(rawKind: kind.name.toJS); + factory CustomDatabaseMessage(CustomDatabaseMessageKind kind, + [String? sql, List parameters = const []]) { + final rawSql = sql?.toJS ?? ''.toJS; + final rawParameters = + [for (final parameter in parameters) parameter.jsify()].toJS; + return CustomDatabaseMessage._( + rawKind: kind.name.toJS, rawSql: rawSql, rawParameters: rawParameters); } external JSString get rawKind; + external JSString get rawSql; + + external JSArray get rawParameters; + CustomDatabaseMessageKind get kind { return CustomDatabaseMessageKind.values.byName(rawKind.toDart); } diff --git a/lib/src/web/worker/worker_utils.dart b/lib/src/web/worker/worker_utils.dart index 9d4f5de..7053a02 100644 --- a/lib/src/web/worker/worker_utils.dart +++ b/lib/src/web/worker/worker_utils.dart @@ -1,4 +1,5 @@ import 'dart:js_interop'; +import 'dart:js_util' as js_util; import 'package:mutex/mutex.dart'; import 'package:sqlite3/wasm.dart'; @@ -52,8 +53,44 @@ class AsyncSqliteDatabase extends WorkerDatabase { throw UnsupportedError('This is a response, not a request'); case CustomDatabaseMessageKind.getAutoCommit: return database.autocommit.toJS; + case CustomDatabaseMessageKind.executeInTransaction: + final sql = message.rawSql.toDart; + final parameters = [ + for (final raw in (message.rawParameters).toDart) raw.dartify() + ]; + if (database.autocommit) { + throw SqliteException(0, + "Transaction rolled back by earlier statement. Cannot execute: $sql"); + } + var res = database.select(sql, parameters); + + var dartMap = resultSetToMap(res); + + var jsObject = js_util.jsify(dartMap); + + return jsObject; + case CustomDatabaseMessageKind.executeBatchInTransaction: + final sql = message.rawSql.toDart; + final parameters = [ + for (final raw in (message.rawParameters).toDart) raw.dartify() + ]; + if (database.autocommit) { + throw SqliteException(0, + "Transaction rolled back by earlier statement. Cannot execute: $sql"); + } + database.execute(sql, parameters); } return CustomDatabaseMessage(CustomDatabaseMessageKind.lockObtained); } + + Map resultSetToMap(ResultSet resultSet) { + var resultSetMap = {}; + + resultSetMap['columnNames'] = resultSet.columnNames; + resultSetMap['tableNames'] = resultSet.tableNames; + resultSetMap['rows'] = resultSet.rows; + + return resultSetMap; + } }