Skip to content

Commit 1032954

Browse files
Custom requests for executing in transactions (#46)
* Custom requests for execute in transactions * Clean up js to dart * remove requirement for checking SQL statements when checking autocommit --------- Co-authored-by: Steven Ontong <[email protected]>
1 parent 3dc27b7 commit 1032954

File tree

3 files changed

+107
-38
lines changed

3 files changed

+107
-38
lines changed

lib/src/web/database.dart

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:js_interop';
44
import 'package:sqlite3/common.dart';
55
import 'package:sqlite3_web/sqlite3_web.dart';
66
import 'package:sqlite_async/mutex.dart';
7+
import 'package:sqlite_async/sqlite_async.dart';
78
import 'package:sqlite_async/src/common/sqlite_database.dart';
89
import 'package:sqlite_async/src/sqlite_connection.dart';
910
import 'package:sqlite_async/src/sqlite_queries.dart';
@@ -84,33 +85,29 @@ class WebDatabase
8485
Stream<UpdateNotification> get updates =>
8586
_database.updates.map((event) => UpdateNotification({event.tableName}));
8687

87-
@override
88-
Future<T> writeLock<T>(Future<T> Function(SqliteWriteContext tx) callback,
89-
{Duration? lockTimeout, String? debugContext}) async {
90-
return _writeLock(callback,
91-
lockTimeout: lockTimeout, debugContext: debugContext);
92-
}
93-
9488
@override
9589
Future<T> writeTransaction<T>(
9690
Future<T> Function(SqliteWriteContext tx) callback,
9791
{Duration? lockTimeout}) {
98-
return _writeLock((ctx) => internalWriteTransaction(ctx, callback),
92+
return writeLock(
93+
(writeContext) =>
94+
internalWriteTransaction(writeContext, (context) async {
95+
// All execute calls done in the callback will be checked for the
96+
// autocommit state
97+
return callback(_ExclusiveTransactionContext(this, writeContext));
98+
}),
9999
debugContext: 'writeTransaction()',
100-
isTransaction: true,
101100
lockTimeout: lockTimeout);
102101
}
103102

103+
@override
104+
104105
/// Internal writeLock which intercepts transaction context's to verify auto commit is not active
105-
Future<T> _writeLock<T>(Future<T> Function(SqliteWriteContext tx) callback,
106-
{Duration? lockTimeout,
107-
String? debugContext,
108-
bool isTransaction = false}) async {
106+
Future<T> writeLock<T>(Future<T> Function(SqliteWriteContext tx) callback,
107+
{Duration? lockTimeout, String? debugContext}) async {
109108
if (_mutex case var mutex?) {
110109
return await mutex.lock(() async {
111-
final context = isTransaction
112-
? _ExclusiveTransactionContext(this)
113-
: _ExclusiveContext(this);
110+
final context = _ExclusiveContext(this);
114111
try {
115112
return await callback(context);
116113
} finally {
@@ -121,11 +118,7 @@ class WebDatabase
121118
// No custom mutex, coordinate locks through shared worker.
122119
await _database.customRequest(CustomDatabaseMessage(
123120
CustomDatabaseMessageKind.requestExclusiveLock));
124-
final context = isTransaction
125-
? _ExclusiveTransactionContext(this)
126-
: _ExclusiveContext(this);
127-
;
128-
121+
final context = _ExclusiveContext(this);
129122
try {
130123
return await callback(context);
131124
} finally {
@@ -207,30 +200,56 @@ class _ExclusiveContext extends _SharedContext implements SqliteWriteContext {
207200
}
208201

209202
class _ExclusiveTransactionContext extends _ExclusiveContext {
210-
_ExclusiveTransactionContext(super.database);
203+
SqliteWriteContext baseContext;
204+
_ExclusiveTransactionContext(super.database, this.baseContext);
205+
206+
@override
207+
bool get closed => baseContext.closed;
211208

212209
@override
213210
Future<ResultSet> execute(String sql,
214211
[List<Object?> parameters = const []]) async {
215-
// TODO offload this to a custom request for single round trip
216-
final isAutoCommit = await getAutoCommit();
217-
if (isAutoCommit && !sql.toLowerCase().contains('begin')) {
218-
throw SqliteException(0,
219-
"Transaction rolled back by earlier statement. Cannot execute: $sql");
220-
}
221-
return super.execute(sql, parameters);
212+
return await wrapSqliteException(() async {
213+
var res = await _database._database.customRequest(CustomDatabaseMessage(
214+
CustomDatabaseMessageKind.executeInTransaction, sql, parameters));
215+
var result =
216+
Map<String, dynamic>.from((res as JSObject).dartify() as Map);
217+
final columnNames = [
218+
for (final entry in result['columnNames']) entry as String
219+
];
220+
final rawTableNames = result['tableNames'];
221+
final tableNames = rawTableNames != null
222+
? [
223+
for (final entry in (rawTableNames as List<Object?>))
224+
entry as String
225+
]
226+
: null;
227+
228+
final rows = <List<Object?>>[];
229+
for (final row in (result['rows'] as List<Object?>)) {
230+
final dartRow = <Object?>[];
231+
232+
for (final column in (row as List<Object?>)) {
233+
dartRow.add(column);
234+
}
235+
236+
rows.add(dartRow);
237+
}
238+
final resultSet = ResultSet(columnNames, tableNames, rows);
239+
return resultSet;
240+
});
222241
}
223242

224243
@override
225244
Future<void> executeBatch(
226245
String sql, List<List<Object?>> parameterSets) async {
227-
// TODO offload this to a custom request for single round trip
228-
final isAutoCommit = await getAutoCommit();
229-
if (isAutoCommit && !sql.toLowerCase().contains('begin')) {
230-
throw SqliteException(0,
231-
"Transaction rolled back by earlier statement. Cannot execute: $sql");
232-
}
233-
return super.executeBatch(sql, parameterSets);
246+
return await wrapSqliteException(() async {
247+
for (final set in parameterSets) {
248+
await _database._database.customRequest(CustomDatabaseMessage(
249+
CustomDatabaseMessageKind.executeBatchInTransaction, sql, set));
250+
}
251+
return;
252+
});
234253
}
235254
}
236255

lib/src/web/protocol.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,32 @@ enum CustomDatabaseMessageKind {
1010
releaseLock,
1111
lockObtained,
1212
getAutoCommit,
13+
executeInTransaction,
14+
executeBatchInTransaction,
1315
}
1416

1517
extension type CustomDatabaseMessage._raw(JSObject _) implements JSObject {
1618
external factory CustomDatabaseMessage._({
1719
required JSString rawKind,
20+
JSString rawSql,
21+
JSArray rawParameters,
1822
});
1923

20-
factory CustomDatabaseMessage(CustomDatabaseMessageKind kind) {
21-
return CustomDatabaseMessage._(rawKind: kind.name.toJS);
24+
factory CustomDatabaseMessage(CustomDatabaseMessageKind kind,
25+
[String? sql, List<Object?> parameters = const []]) {
26+
final rawSql = sql?.toJS ?? ''.toJS;
27+
final rawParameters =
28+
<JSAny?>[for (final parameter in parameters) parameter.jsify()].toJS;
29+
return CustomDatabaseMessage._(
30+
rawKind: kind.name.toJS, rawSql: rawSql, rawParameters: rawParameters);
2231
}
2332

2433
external JSString get rawKind;
2534

35+
external JSString get rawSql;
36+
37+
external JSArray get rawParameters;
38+
2639
CustomDatabaseMessageKind get kind {
2740
return CustomDatabaseMessageKind.values.byName(rawKind.toDart);
2841
}

lib/src/web/worker/worker_utils.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:js_interop';
2+
import 'dart:js_util' as js_util;
23

34
import 'package:mutex/mutex.dart';
45
import 'package:sqlite3/wasm.dart';
@@ -52,8 +53,44 @@ class AsyncSqliteDatabase extends WorkerDatabase {
5253
throw UnsupportedError('This is a response, not a request');
5354
case CustomDatabaseMessageKind.getAutoCommit:
5455
return database.autocommit.toJS;
56+
case CustomDatabaseMessageKind.executeInTransaction:
57+
final sql = message.rawSql.toDart;
58+
final parameters = [
59+
for (final raw in (message.rawParameters).toDart) raw.dartify()
60+
];
61+
if (database.autocommit) {
62+
throw SqliteException(0,
63+
"Transaction rolled back by earlier statement. Cannot execute: $sql");
64+
}
65+
var res = database.select(sql, parameters);
66+
67+
var dartMap = resultSetToMap(res);
68+
69+
var jsObject = js_util.jsify(dartMap);
70+
71+
return jsObject;
72+
case CustomDatabaseMessageKind.executeBatchInTransaction:
73+
final sql = message.rawSql.toDart;
74+
final parameters = [
75+
for (final raw in (message.rawParameters).toDart) raw.dartify()
76+
];
77+
if (database.autocommit) {
78+
throw SqliteException(0,
79+
"Transaction rolled back by earlier statement. Cannot execute: $sql");
80+
}
81+
database.execute(sql, parameters);
5582
}
5683

5784
return CustomDatabaseMessage(CustomDatabaseMessageKind.lockObtained);
5885
}
86+
87+
Map<String, dynamic> resultSetToMap(ResultSet resultSet) {
88+
var resultSetMap = <String, dynamic>{};
89+
90+
resultSetMap['columnNames'] = resultSet.columnNames;
91+
resultSetMap['tableNames'] = resultSet.tableNames;
92+
resultSetMap['rows'] = resultSet.rows;
93+
94+
return resultSetMap;
95+
}
5996
}

0 commit comments

Comments
 (0)