Skip to content

Commit f994e16

Browse files
authored
Merge pull request #23 from powersync-ja/resume-transactions
Support resuming transactions / v0.6.0
2 parents 08495cf + 4fa057b commit f994e16

9 files changed

+145
-23
lines changed

.github/workflows/test.yaml

+9-3
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,21 @@ jobs:
3030
strategy:
3131
matrix:
3232
include:
33+
- sqlite_version: "3440200"
34+
sqlite_url: "https://www.sqlite.org/2023/sqlite-autoconf-3440200.tar.gz"
35+
dart_sdk: 3.2.4
36+
- sqlite_version: "3430200"
37+
sqlite_url: "https://www.sqlite.org/2023/sqlite-autoconf-3430200.tar.gz"
38+
dart_sdk: 3.2.4
3339
- sqlite_version: "3420000"
3440
sqlite_url: "https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz"
35-
dart_sdk: 3.0.6
41+
dart_sdk: 3.2.4
3642
- sqlite_version: "3410100"
3743
sqlite_url: "https://www.sqlite.org/2023/sqlite-autoconf-3410100.tar.gz"
38-
dart_sdk: 2.19.1
44+
dart_sdk: 3.2.4
3945
- sqlite_version: "3380000"
4046
sqlite_url: "https://www.sqlite.org/2022/sqlite-autoconf-3380000.tar.gz"
41-
dart_sdk: 2.19.1
47+
dart_sdk: 3.2.0
4248
steps:
4349
- uses: actions/checkout@v3
4450
- uses: dart-lang/setup-dart@v1

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.6.0
2+
3+
- Allow catching errors and continuing the transaction. This is technically a breaking change, although it should not be an issue in most cases.
4+
- Add `tx.closed` and `db/tx.getAutoCommit()` to check whether transactions are active.
5+
- Requires sqlite3 ^2.3.0 and Dart ^3.2.0.
6+
17
## 0.5.2
28

39
- Fix releasing of locks when closing `SharedMutex``.

lib/src/connection_pool.dart

+9
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
4949
: _writeConnection = writeConnection,
5050
_upstreamPort = upstreamPort;
5151

52+
/// Returns true if the _write_ connection is currently in autocommit mode.
53+
@override
54+
Future<bool> getAutoCommit() async {
55+
if (_writeConnection == null) {
56+
throw AssertionError('Closed');
57+
}
58+
return await _writeConnection!.getAutoCommit();
59+
}
60+
5261
@override
5362
Future<T> readLock<T>(Future<T> Function(SqliteReadContext tx) callback,
5463
{Duration? lockTimeout, String? debugContext}) async {

lib/src/port_channel.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ class _PortChannelResult<T> {
286286
return _result as T;
287287
} else {
288288
if (_error != null && stackTrace != null) {
289-
Error.throwWithStackTrace(_error!, stackTrace!);
289+
Error.throwWithStackTrace(_error, stackTrace!);
290290
} else {
291291
throw _error!;
292292
}

lib/src/sqlite_connection.dart

+13
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ abstract class SqliteReadContext {
1313
Future<sqlite.Row?> getOptional(String sql,
1414
[List<Object?> parameters = const []]);
1515

16+
/// For transactions, returns true if the lock is held (even if it has been
17+
/// rolled back).
18+
///
19+
/// For database connections, returns true if the connection hasn't been closed
20+
/// yet.
21+
bool get closed;
22+
23+
/// Returns true if auto-commit is enabled. This means the database is not
24+
/// currently in a transaction. This may be true even if a transaction lock
25+
/// is still held, when the transaction has been committed or rolled back.
26+
Future<bool> getAutoCommit();
27+
1628
/// Run a function within a database isolate, with direct synchronous access
1729
/// to the underlying database.
1830
///
@@ -105,5 +117,6 @@ abstract class SqliteConnection extends SqliteWriteContext {
105117
Future<void> close();
106118

107119
/// Returns true if the connection is closed
120+
@override
108121
bool get closed;
109122
}

lib/src/sqlite_connection_impl.dart

+39-5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ class SqliteConnectionImpl with SqliteQueries implements SqliteConnection {
4949
return _isolateClient.closed;
5050
}
5151

52+
@override
53+
Future<bool> getAutoCommit() async {
54+
if (closed) {
55+
throw AssertionError('Closed');
56+
}
57+
// We use a _TransactionContext without a lock here.
58+
// It is safe to call this in the middle of another transaction.
59+
final ctx = _TransactionContext(_isolateClient);
60+
try {
61+
return await ctx.getAutoCommit();
62+
} finally {
63+
await ctx.close();
64+
}
65+
}
66+
5267
Future<void> _open(SqliteOpenFactory openFactory,
5368
{required bool primary,
5469
required SerializedPortClient upstreamPort}) async {
@@ -145,6 +160,11 @@ class _TransactionContext implements SqliteWriteContext {
145160

146161
_TransactionContext(this._sendPort);
147162

163+
@override
164+
bool get closed {
165+
return _closed;
166+
}
167+
148168
@override
149169
Future<sqlite.ResultSet> execute(String sql,
150170
[List<Object?> parameters = const []]) async {
@@ -176,6 +196,15 @@ class _TransactionContext implements SqliteWriteContext {
176196
}
177197
}
178198

199+
@override
200+
Future<bool> getAutoCommit() async {
201+
return await computeWithDatabase(
202+
(db) async {
203+
return db.autocommit;
204+
},
205+
);
206+
}
207+
179208
@override
180209
Future<T> computeWithDatabase<T>(
181210
Future<T> Function(sqlite.Database db) compute) async {
@@ -269,10 +298,8 @@ Future<void> _sqliteConnectionIsolateInner(_SqliteConnectionParams params,
269298
server.open((data) async {
270299
if (data is _SqliteIsolateClose) {
271300
if (txId != null) {
272-
try {
301+
if (!db.autocommit) {
273302
db.execute('ROLLBACK');
274-
} catch (e) {
275-
// Ignore
276303
}
277304
txId = null;
278305
txError = null;
@@ -290,7 +317,8 @@ Future<void> _sqliteConnectionIsolateInner(_SqliteConnectionParams params,
290317
txId = data.ctxId;
291318
} else if (txId != null && txId != data.ctxId) {
292319
// Locks should prevent this from happening
293-
throw AssertionError('Mixed transactions: $txId and ${data.ctxId}');
320+
throw sqlite.SqliteException(
321+
0, 'Mixed transactions: $txId and ${data.ctxId}');
294322
} else if (data.sql == 'ROLLBACK') {
295323
// This is the only valid way to clear an error
296324
txError = null;
@@ -307,7 +335,13 @@ Future<void> _sqliteConnectionIsolateInner(_SqliteConnectionParams params,
307335
return result;
308336
} catch (err) {
309337
if (txId != null) {
310-
txError = err;
338+
if (db.autocommit) {
339+
// Transaction rolled back
340+
txError = sqlite.SqliteException(0,
341+
'Transaction rolled back by earlier statement: ${err.toString()}');
342+
} else {
343+
// Recoverable error
344+
}
311345
}
312346
rethrow;
313347
}

lib/src/sqlite_database.dart

+7
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ class SqliteDatabase with SqliteQueries implements SqliteConnection {
109109
return _pool.closed;
110110
}
111111

112+
/// Returns true if the _write_ connection is in auto-commit mode
113+
/// (no active transaction).
114+
@override
115+
Future<bool> getAutoCommit() {
116+
return _pool.getAutoCommit();
117+
}
118+
112119
void _listenForEvents() {
113120
UpdateNotification? updates;
114121

pubspec.yaml

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
name: sqlite_async
22
description: High-performance asynchronous interface for SQLite on Dart and Flutter.
3-
version: 0.5.2
4-
repository: https://github.com/journeyapps/sqlite_async.dart
3+
version: 0.6.0
4+
repository: https://github.com/powersync-ja/sqlite_async.dart
55
environment:
6-
sdk: '>=2.19.1 <4.0.0'
6+
sdk: '>=3.2.0 <4.0.0'
77

88
dependencies:
9-
sqlite3: '>=1.10.1 <3.0.0'
9+
sqlite3: '^2.3.0'
1010
async: ^2.10.0
1111
collection: ^1.17.0
1212

1313
dev_dependencies:
14-
lints: ^2.0.0
14+
lints: ^3.0.0
1515
test: ^1.21.0
16-
test_api: ^0.4.18
16+
test_api: ^0.7.0
1717
glob: ^2.1.1
1818
benchmarking: ^0.6.1

test/basic_test.dart

+55-8
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,37 @@ void main() {
222222
await createTables(db);
223223

224224
var tp = db.writeTransaction((tx) async {
225-
tx.execute('INSERT INTO test_data(description) VALUES(?)', ['test1']);
226-
tx.execute('INSERT INTO test_data(description) VALUES(?)', ['test2']);
227-
ignore(tx.execute('INSERT INTO test_data(description) VALUES(json(?))',
228-
['test3'])); // Errors
229-
// Will not be executed because of the above error
225+
await tx.execute(
226+
'INSERT OR ROLLBACK INTO test_data(id, description) VALUES(?, ?)',
227+
[1, 'test1']);
228+
await tx.execute(
229+
'INSERT OR ROLLBACK INTO test_data(id, description) VALUES(?, ?)',
230+
[2, 'test2']);
231+
expect(await tx.getAutoCommit(), equals(false));
232+
try {
233+
await tx.execute(
234+
'INSERT OR ROLLBACK INTO test_data(id, description) VALUES(?, ?)',
235+
[2, 'test3']);
236+
} catch (e) {
237+
// Ignore
238+
}
239+
expect(await tx.getAutoCommit(), equals(true));
240+
expect(tx.closed, equals(false));
241+
242+
// Will not be executed because of the above rollback
230243
ignore(tx.execute(
231-
'INSERT INTO test_data(description) VALUES(?) RETURNING *',
232-
['test4']));
244+
'INSERT OR ROLLBACK INTO test_data(id, description) VALUES(?, ?)',
245+
[4, 'test4']));
233246
});
234247

235248
// The error propagates up to the transaction
236-
await expectLater(tp, throwsA((e) => e is sqlite.SqliteException));
249+
await expectLater(
250+
tp,
251+
throwsA((e) =>
252+
e is sqlite.SqliteException &&
253+
e.message
254+
.contains('Transaction rolled back by earlier statement') &&
255+
e.message.contains('UNIQUE constraint failed')));
237256

238257
expect(await db.get('SELECT count() count FROM test_data'),
239258
equals({'count': 0}));
@@ -321,6 +340,34 @@ void main() {
321340
});
322341
expect(computed, equals(5));
323342
});
343+
344+
test('should allow resuming transaction after errors', () async {
345+
final db = await setupDatabase(path: path);
346+
await createTables(db);
347+
SqliteWriteContext? savedTx;
348+
await db.writeTransaction((tx) async {
349+
savedTx = tx;
350+
var caught = false;
351+
try {
352+
// This error does not rollback the transaction
353+
await tx.execute('NOT A VALID STATEMENT');
354+
} catch (e) {
355+
// Ignore
356+
caught = true;
357+
}
358+
expect(caught, equals(true));
359+
360+
expect(await tx.getAutoCommit(), equals(false));
361+
expect(tx.closed, equals(false));
362+
363+
final rs = await tx.execute(
364+
'INSERT INTO test_data(description) VALUES(?) RETURNING description',
365+
['Test Data']);
366+
expect(rs.rows[0], equals(['Test Data']));
367+
});
368+
expect(await savedTx!.getAutoCommit(), equals(true));
369+
expect(savedTx!.closed, equals(true));
370+
});
324371
});
325372
}
326373

0 commit comments

Comments
 (0)