Skip to content

Commit 4905611

Browse files
committed
Merge remote-tracking branch 'origin/main' into get_all_connections
2 parents 082e772 + 6705d13 commit 4905611

File tree

18 files changed

+416
-257
lines changed

18 files changed

+416
-257
lines changed

CHANGELOG.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,63 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## 2025-08-08
7+
8+
### Changes
9+
10+
---
11+
12+
Packages with breaking changes:
13+
14+
- There are no breaking changes in this release.
15+
16+
Packages with other changes:
17+
18+
- [`sqlite_async` - `v0.12.1`](#sqlite_async---v0121)
19+
- [`sqlite_async` - `v0.12.0`](#sqlite_async---v0120)
20+
- [`drift_sqlite_async` - `v0.2.3+1`](#drift_sqlite_async---v0231)
21+
22+
Packages with dependency updates only:
23+
24+
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
25+
26+
- `drift_sqlite_async` - `v0.2.3+1`
27+
28+
---
29+
30+
#### `sqlite_async` - `v0.12.1`
31+
32+
- Fix distributing updates from shared worker.
33+
34+
#### `sqlite_async` - `v0.12.0`
35+
36+
- Avoid large transactions creating a large internal update queue.
37+
38+
39+
## 2025-07-29
40+
41+
---
42+
43+
Packages with breaking changes:
44+
45+
- There are no breaking changes in this release.
46+
47+
Packages with other changes:
48+
49+
- [`sqlite_async` - `v0.11.8`](#sqlite_async---v0118)
50+
- [`drift_sqlite_async` - `v0.2.3`](#drift_sqlite_async---v023)
51+
52+
---
53+
54+
#### `sqlite_async` - `v0.11.8`
55+
56+
- Support nested transactions (emulated with `SAVEPOINT` statements).
57+
- Fix web compilation issues with version `2.8.0` of `package:sqlite3`.
58+
59+
#### `drift_sqlite_async` - `v0.2.3`
60+
61+
- Support nested transactions.
62+
663
## 2025-06-03
764

865
---

packages/drift_sqlite_async/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 0.2.3+1
2+
3+
- Update a dependency to the latest release.
4+
5+
## 0.2.3
6+
7+
- Support nested transactions.
8+
19
## 0.2.2
210

311
- Fix write detection when using UPDATE/INSERT/DELETE with RETURNING in raw queries.

packages/drift_sqlite_async/lib/src/executor.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,28 @@ class _SqliteAsyncTransactionDelegate extends SupportedTransactionDelegate {
129129

130130
_SqliteAsyncTransactionDelegate(this._db);
131131

132+
@override
133+
FutureOr<void> Function(QueryDelegate, Future<void> Function(QueryDelegate))?
134+
get startNested => _startNested;
135+
132136
@override
133137
Future<void> startTransaction(Future Function(QueryDelegate p1) run) async {
134-
await _db.writeTransaction((context) async {
138+
await _startTransaction(_db, run);
139+
}
140+
141+
Future<void> _startTransaction(
142+
SqliteWriteContext context, Future Function(QueryDelegate p1) run) async {
143+
await context.writeTransaction((context) async {
135144
final delegate = _SqliteAsyncQueryDelegate(context, null);
136145
return run(delegate);
137146
});
138147
}
148+
149+
Future<void> _startNested(
150+
QueryDelegate outer, Future<void> Function(QueryDelegate) block) async {
151+
await _startTransaction(
152+
(outer as _SqliteAsyncQueryDelegate)._context, block);
153+
}
139154
}
140155

141156
class _SqliteAsyncVersionDelegate extends DynamicVersionDelegate {

packages/drift_sqlite_async/pubspec.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: drift_sqlite_async
2-
version: 0.2.2
2+
version: 0.2.3+1
33
homepage: https://github.com/powersync-ja/sqlite_async.dart
44
repository: https://github.com/powersync-ja/sqlite_async.dart
55
description: Use Drift with a sqlite_async database, allowing both to be used in the same application.
@@ -14,12 +14,12 @@ topics:
1414
environment:
1515
sdk: ">=3.0.0 <4.0.0"
1616
dependencies:
17-
drift: ">=2.19.0 <3.0.0"
18-
sqlite_async: ^0.11.0
17+
drift: ">=2.28.0 <3.0.0"
18+
sqlite_async: ^0.12.0
1919

2020
dev_dependencies:
2121
build_runner: ^2.4.8
22-
drift_dev: ">=2.19.0 <3.0.0"
22+
drift_dev: ">=2.28.0 <3.0.0"
2323
glob: ^2.1.2
2424
lints: ^5.0.0
2525
sqlite3: ^2.4.0

packages/drift_sqlite_async/test/db_test.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,35 @@ void main() {
117117
final deleted = await dbu.delete(dbu.todoItems).go();
118118
expect(deleted, 10);
119119
});
120+
121+
test('nested transactions', () async {
122+
await dbu
123+
.into(dbu.todoItems)
124+
.insert(TodoItemsCompanion.insert(description: 'root'));
125+
126+
await dbu.transaction(() async {
127+
await dbu
128+
.into(dbu.todoItems)
129+
.insert(TodoItemsCompanion.insert(description: 'tx0'));
130+
131+
await dbu.transaction(() async {
132+
await dbu
133+
.into(dbu.todoItems)
134+
.insert(TodoItemsCompanion.insert(description: 'tx1'));
135+
136+
await expectLater(() {
137+
return dbu.transaction(() async {
138+
await dbu
139+
.into(dbu.todoItems)
140+
.insert(TodoItemsCompanion.insert(description: 'tx2'));
141+
throw 'rollback';
142+
});
143+
}, throwsA(anything));
144+
});
145+
});
146+
147+
final items = await dbu.todoItems.all().get();
148+
expect(items.map((e) => e.description).toSet(), {'root', 'tx0', 'tx1'});
149+
});
120150
});
121151
}

packages/sqlite_async/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## 0.12.1
2+
3+
- Fix distributing updates from shared worker.
4+
5+
## 0.12.0
6+
7+
- Avoid large transactions creating a large internal update queue.
8+
9+
## 0.11.8
10+
11+
- Support nested transactions (emulated with `SAVEPOINT` statements).
12+
- Fix web compilation issues with version `2.8.0` of `package:sqlite3`.
13+
114
## 0.11.7
215

316
- Shared worker: Release locks owned by connected client tab when it closes.

packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -303,40 +303,13 @@ Future<void> _sqliteConnectionIsolateInner(_SqliteConnectionParams params,
303303
final server = params.portServer;
304304
final commandPort = ReceivePort();
305305

306-
Timer? updateDebouncer;
307-
Set<String> updatedTables = {};
306+
db.updatedTables.listen((changedTables) {
307+
client.fire(UpdateNotification(changedTables));
308+
});
309+
308310
int? txId;
309311
Object? txError;
310312

311-
void maybeFireUpdates() {
312-
// We keep buffering the set of updated tables until we are not
313-
// in a transaction. Firing transactions inside a transaction
314-
// has multiple issues:
315-
// 1. Watched queries would detect changes to the underlying tables,
316-
// but the data would not be visible to queries yet.
317-
// 2. It would trigger many more notifications than required.
318-
//
319-
// This still includes updates for transactions that are rolled back.
320-
// We could handle those better at a later stage.
321-
322-
if (updatedTables.isNotEmpty && db.autocommit) {
323-
client.fire(UpdateNotification(updatedTables));
324-
updatedTables.clear();
325-
}
326-
updateDebouncer?.cancel();
327-
updateDebouncer = null;
328-
}
329-
330-
db.updates.listen((event) {
331-
updatedTables.add(event.tableName);
332-
333-
// This handles two cases:
334-
// 1. Update arrived after _SqliteIsolateClose (not sure if this could happen).
335-
// 2. Long-running _SqliteIsolateClosure that should fire updates while running.
336-
updateDebouncer ??=
337-
Timer(const Duration(milliseconds: 1), maybeFireUpdates);
338-
});
339-
340313
ResultSet runStatement(_SqliteIsolateStatement data) {
341314
if (data.sql == 'BEGIN' || data.sql == 'BEGIN IMMEDIATE') {
342315
if (txId != null) {
@@ -388,8 +361,6 @@ Future<void> _sqliteConnectionIsolateInner(_SqliteConnectionParams params,
388361
throw sqlite.SqliteException(
389362
0, 'Transaction must be closed within the read or write lock');
390363
}
391-
// We would likely have received updates by this point - fire now.
392-
maybeFireUpdates();
393364
return null;
394365
case _SqliteIsolateStatement():
395366
return task.timeSync(
@@ -399,11 +370,7 @@ Future<void> _sqliteConnectionIsolateInner(_SqliteConnectionParams params,
399370
parameters: data.args,
400371
);
401372
case _SqliteIsolateClosure():
402-
try {
403-
return await data.cb(db);
404-
} finally {
405-
maybeFireUpdates();
406-
}
373+
return await data.cb(db);
407374
case _SqliteIsolateConnectionClose():
408375
db.dispose();
409376
return null;

packages/sqlite_async/lib/src/utils/shared_utils.dart

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:async';
22
import 'dart:convert';
33

4+
import 'package:sqlite3/common.dart';
5+
46
import '../sqlite_connection.dart';
57

68
Future<T> internalReadTransaction<T>(SqliteReadContext ctx,
@@ -75,3 +77,87 @@ Object? mapParameter(Object? parameter) {
7577
List<Object?> mapParameters(List<Object?> parameters) {
7678
return [for (var p in parameters) mapParameter(p)];
7779
}
80+
81+
extension ThrottledUpdates on CommonDatabase {
82+
/// An unthrottled stream of updated tables that emits on every commit.
83+
///
84+
/// A paused subscription on this stream will buffer changed tables into a
85+
/// growing set instead of losing events, so this stream is simple to throttle
86+
/// downstream.
87+
Stream<Set<String>> get updatedTables {
88+
final listeners = <_UpdateListener>[];
89+
var uncommitedUpdates = <String>{};
90+
var underlyingSubscriptions = <StreamSubscription<void>>[];
91+
92+
void handleUpdate(SqliteUpdate update) {
93+
uncommitedUpdates.add(update.tableName);
94+
}
95+
96+
void afterCommit() {
97+
for (final listener in listeners) {
98+
listener.notify(uncommitedUpdates);
99+
}
100+
101+
uncommitedUpdates.clear();
102+
}
103+
104+
void afterRollback() {
105+
uncommitedUpdates.clear();
106+
}
107+
108+
void addListener(_UpdateListener listener) {
109+
listeners.add(listener);
110+
111+
if (listeners.length == 1) {
112+
// First listener, start listening for raw updates on underlying
113+
// database.
114+
underlyingSubscriptions = [
115+
updatesSync.listen(handleUpdate),
116+
commits.listen((_) => afterCommit()),
117+
commits.listen((_) => afterRollback())
118+
];
119+
}
120+
}
121+
122+
void removeListener(_UpdateListener listener) {
123+
listeners.remove(listener);
124+
if (listeners.isEmpty) {
125+
for (final sub in underlyingSubscriptions) {
126+
sub.cancel();
127+
}
128+
}
129+
}
130+
131+
return Stream.multi(
132+
(listener) {
133+
final wrapped = _UpdateListener(listener);
134+
addListener(wrapped);
135+
136+
listener.onResume = wrapped.addPending;
137+
listener.onCancel = () => removeListener(wrapped);
138+
},
139+
isBroadcast: true,
140+
);
141+
}
142+
}
143+
144+
class _UpdateListener {
145+
final MultiStreamController<Set<String>> downstream;
146+
Set<String> buffered = {};
147+
148+
_UpdateListener(this.downstream);
149+
150+
void notify(Set<String> pendingUpdates) {
151+
buffered.addAll(pendingUpdates);
152+
if (!downstream.isPaused) {
153+
addPending();
154+
}
155+
}
156+
157+
void addPending() {
158+
if (buffered.isNotEmpty) {
159+
downstream.add(buffered);
160+
buffered = {};
161+
}
162+
}
163+
}

packages/sqlite_async/lib/src/web/database.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class WebDatabase
2121
final Mutex? _mutex;
2222
final bool profileQueries;
2323

24+
@override
25+
final Stream<UpdateNotification> updates;
26+
2427
/// For persistent databases that aren't backed by a shared worker, we use
2528
/// web broadcast channels to forward local update events to other tabs.
2629
final BroadcastUpdates? broadcastUpdates;
@@ -32,6 +35,7 @@ class WebDatabase
3235
this._database,
3336
this._mutex, {
3437
required this.profileQueries,
38+
required this.updates,
3539
this.broadcastUpdates,
3640
});
3741

@@ -113,10 +117,6 @@ class WebDatabase
113117
}
114118
}
115119

116-
@override
117-
Stream<UpdateNotification> get updates =>
118-
_database.updates.map((event) => UpdateNotification({event.tableName}));
119-
120120
@override
121121
Future<T> writeTransaction<T>(
122122
Future<T> Function(SqliteWriteContext tx) callback,

packages/sqlite_async/lib/src/web/protocol.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ enum CustomDatabaseMessageKind {
1313
getAutoCommit,
1414
executeInTransaction,
1515
executeBatchInTransaction,
16+
updateSubscriptionManagement,
17+
notifyUpdates,
1618
}
1719

1820
extension type CustomDatabaseMessage._raw(JSObject _) implements JSObject {

0 commit comments

Comments
 (0)