Skip to content

Commit 8b189dc

Browse files
committed
Merge main
2 parents 58700fd + 355ee98 commit 8b189dc

31 files changed

+2207
-709
lines changed

.github/workflows/main.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,33 @@ jobs:
156156
if-no-files-found: error
157157
retention-days: 1
158158

159+
integration_test_web:
160+
needs: [analyze, fetch_sqlite]
161+
runs-on: ubuntu-latest
162+
steps:
163+
- uses: actions/checkout@v5
164+
- uses: dart-lang/setup-dart@v1
165+
with:
166+
sdk: beta
167+
- name: Download compiled sqlite3
168+
uses: actions/download-artifact@v5
169+
with:
170+
artifact-ids: ${{ needs.fetch_sqlite.outputs.artifact_id }}
171+
path: sqlite-compiled
172+
- uses: actions/cache@v4
173+
with:
174+
path: "${{ env.PUB_CACHE }}"
175+
key: dart-dependencies-${{ matrix.dart }}-${{ runner.os }}
176+
restore-keys: |
177+
dart-dependencies-${{ matrix.dart }}-
178+
dart-dependencies-
179+
180+
- name: Web tests
181+
run: |
182+
cp ../sqlite-compiled/*.wasm web/
183+
dart pub get
184+
dart test -P gh_actions -r expanded
185+
working-directory: sqlite3_web
159186

160187
# The integration tests for android are currently broken (the emulator doesn't want to
161188
# start up...)

sqlite3/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
`sqlcipher_flutter_libs` when upgrading.
88
- __Breaking change__: Parameters to `SqliteException`s are now named.
99

10+
## 2.9.4
11+
12+
- `SimpleOpfsFileSystem`: Allow opening with `readwrite-unsafe`, which can be used to implement
13+
multi-tab OPFS databases on Chrome with an outer locking scheme.
14+
15+
## 2.9.3
16+
17+
- Allow iterating over statements after `SQLITE_BUSY` errors.
18+
1019
## 2.9.2
1120

1221
- Fix a bug introduced in version `2.9.1` where the SQLite framework provided by

sqlite3/lib/src/implementation/statement.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,11 @@ class _ActiveCursorIterator extends IteratingCursor {
397397
}
398398

399399
// We're at the end of the result set or encountered an exception here.
400-
statement._currentCursor = null;
400+
if (result != SqlError.SQLITE_BUSY) {
401+
// Statements failing with SQLITE_BUSY can be retried in some instances,
402+
// so we don't want to detach the cursor.
403+
statement._currentCursor = null;
404+
}
401405

402406
if (result != SqlError.SQLITE_OK && result != SqlError.SQLITE_DONE) {
403407
throwException(

sqlite3/lib/src/wasm/js_interop/new_file_system_access.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,23 @@ extension FileSystemDirectoryHandleApi on FileSystemDirectoryHandle {
8585
}
8686
}
8787
}
88+
89+
// https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md
90+
extension ProposedLockingSchemeApi on FileSystemFileHandle {
91+
external JSPromise<FileSystemSyncAccessHandle> createSyncAccessHandle(
92+
FileSystemCreateSyncAccessHandleOptions options,
93+
);
94+
}
95+
96+
// https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md#modes-of-creating-a-filesystemsyncaccesshandle
97+
@anonymous
98+
extension type FileSystemCreateSyncAccessHandleOptions._(JSObject _)
99+
implements JSObject {
100+
external factory FileSystemCreateSyncAccessHandleOptions({JSString? mode});
101+
102+
static FileSystemCreateSyncAccessHandleOptions unsafeReadWrite() {
103+
return FileSystemCreateSyncAccessHandleOptions(
104+
mode: 'readwrite-unsafe'.toJS,
105+
);
106+
}
107+
}

sqlite3/lib/src/wasm/vfs/simple_opfs.dart

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,27 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem {
9696
/// Throws a [VfsException] if OPFS is not available - please note that
9797
/// this file system implementation requires a recent browser and only works
9898
/// in dedicated web workers.
99+
///
100+
/// When [readWriteUnsafe] is passed, the synchronous file handles are opened
101+
/// using the [proposed lock mode](https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md).
102+
/// This mode is currently not supported across browsers, but can be used on
103+
/// Chrome for faster database access across tabs.
99104
static Future<SimpleOpfsFileSystem> loadFromStorage(
100105
String path, {
101106
String vfsName = 'simple-opfs',
107+
bool readWriteUnsafe = false,
102108
}) async {
103109
final storage = storageManager;
104110
if (storage == null) {
105111
throw VfsException(SqlError.SQLITE_ERROR);
106112
}
107113

108114
final (_, directory) = await _resolveDir(path);
109-
return inDirectory(directory, vfsName: vfsName);
115+
return inDirectory(
116+
directory,
117+
vfsName: vfsName,
118+
readWriteUnsafe: readWriteUnsafe,
119+
);
110120
}
111121

112122
/// Deletes the file system directory handle that would store sqlite3
@@ -136,14 +146,27 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem {
136146
/// Loads an [SimpleOpfsFileSystem] in the desired [root] directory, which must be
137147
/// a Dart wrapper around a [FileSystemDirectoryHandle].
138148
///
149+
/// When [readWriteUnsafe] is passed, the synchronous file handles are opened
150+
/// using the [proposed lock mode](https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md).
151+
/// This mode is currently not supported across browsers, but can be used on
152+
/// Chrome for faster database access across tabs.
153+
///
139154
/// [FileSystemDirectoryHandle]: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle
140155
static Future<SimpleOpfsFileSystem> inDirectory(
141156
FileSystemDirectoryHandle root, {
142157
String vfsName = 'simple-opfs',
158+
bool readWriteUnsafe = false,
143159
}) async {
144160
Future<FileSystemSyncAccessHandle> open(String name) async {
145161
final handle = await root.openFile(name, create: true);
146-
return await handle.createSyncAccessHandle().toDart;
162+
163+
final syncHandlePromise = readWriteUnsafe
164+
? ProposedLockingSchemeApi(handle).createSyncAccessHandle(
165+
FileSystemCreateSyncAccessHandleOptions.unsafeReadWrite(),
166+
)
167+
: handle.createSyncAccessHandle();
168+
169+
return await syncHandlePromise.toDart;
147170
}
148171

149172
final meta = await open('meta');

sqlite3/test/common/prepared_statement.dart

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,38 @@ void testPreparedStatements(
587587
throwsSqlError(19, 2067),
588588
);
589589
});
590+
591+
test('recovers from SQLITE_BUSY', () {
592+
final vfs = _ErrorInjectingVfs(
593+
InMemoryFileSystem(),
594+
name: 'test-recover-sqlite-busy',
595+
);
596+
sqlite3.registerVirtualFileSystem(vfs);
597+
addTearDown(() => sqlite3.unregisterVirtualFileSystem(vfs));
598+
599+
var db = sqlite3.open('/db', vfs: vfs.name);
600+
addTearDown(() => db.dispose());
601+
602+
db
603+
..execute('CREATE TABLE foo (bar TEXT) STRICT')
604+
..execute('INSERT INTO foo (bar) VALUES (?)', ['testing'])
605+
..dispose();
606+
607+
db = db = sqlite3.open('/db', vfs: vfs.name);
608+
final stmt = db.prepare('SELECT * FROM foo');
609+
final cursor = stmt.selectCursor();
610+
vfs.maybeError = () => throw VfsException(SqlError.SQLITE_BUSY);
611+
612+
expect(
613+
() => cursor.moveNext(),
614+
throwsSqlError(SqlError.SQLITE_BUSY, SqlError.SQLITE_BUSY),
615+
);
616+
vfs.maybeError = null;
617+
expect(cursor.moveNext(), isTrue);
618+
expect(cursor.current, {'bar': 'testing'});
619+
620+
stmt.dispose();
621+
});
590622
});
591623
}
592624

@@ -604,3 +636,111 @@ class _CustomValue implements CustomStatementParameter {
604636
stmt.statement.sqlite3_bind_int64(index, 42);
605637
}
606638
}
639+
640+
final class _ErrorInjectingVfs extends BaseVirtualFileSystem {
641+
final VirtualFileSystem _base;
642+
void Function()? maybeError;
643+
644+
_ErrorInjectingVfs(this._base, {required super.name});
645+
646+
void _op() {
647+
maybeError?.call();
648+
}
649+
650+
@override
651+
int xAccess(String path, int flags) {
652+
_op();
653+
return _base.xAccess(path, flags);
654+
}
655+
656+
@override
657+
void xDelete(String path, int syncDir) {
658+
_op();
659+
return _base.xDelete(path, syncDir);
660+
}
661+
662+
@override
663+
String xFullPathName(String path) {
664+
_op();
665+
return _base.xFullPathName(path);
666+
}
667+
668+
@override
669+
XOpenResult xOpen(Sqlite3Filename path, int flags) {
670+
_op();
671+
final inner = _base.xOpen(path, flags);
672+
return (
673+
outFlags: inner.outFlags,
674+
file: _ErrorInjectingFile(this, inner.file),
675+
);
676+
}
677+
678+
@override
679+
void xSleep(Duration duration) {
680+
return _base.xSleep(duration);
681+
}
682+
}
683+
684+
final class _ErrorInjectingFile implements VirtualFileSystemFile {
685+
final _ErrorInjectingVfs _vfs;
686+
final VirtualFileSystemFile _base;
687+
688+
_ErrorInjectingFile(this._vfs, this._base);
689+
690+
@override
691+
void xRead(Uint8List target, int fileOffset) {
692+
_vfs._op();
693+
return _base.xRead(target, fileOffset);
694+
}
695+
696+
@override
697+
int xCheckReservedLock() {
698+
_vfs._op();
699+
return _base.xCheckReservedLock();
700+
}
701+
702+
@override
703+
int get xDeviceCharacteristics => _base.xDeviceCharacteristics;
704+
705+
@override
706+
void xClose() {
707+
_vfs._op();
708+
_base.xClose();
709+
}
710+
711+
@override
712+
int xFileSize() {
713+
_vfs._op();
714+
return _base.xFileSize();
715+
}
716+
717+
@override
718+
void xLock(int mode) {
719+
_vfs._op();
720+
_base.xLock(mode);
721+
}
722+
723+
@override
724+
void xSync(int flags) {
725+
_vfs._op();
726+
_base.xSync(flags);
727+
}
728+
729+
@override
730+
void xTruncate(int size) {
731+
_vfs._op();
732+
_base.xTruncate(size);
733+
}
734+
735+
@override
736+
void xUnlock(int mode) {
737+
_vfs._op();
738+
_base.xUnlock(mode);
739+
}
740+
741+
@override
742+
void xWrite(Uint8List buffer, int fileOffset) {
743+
_vfs._op();
744+
_base.xWrite(buffer, fileOffset);
745+
}
746+
}

sqlite3_web/CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
## 0.4.0-dev
1+
## 0.5.0-dev
22

3-
- Use version 3.x of `package:sqlite3`.
3+
- Use version 3 of `package:sqlite3`.
4+
5+
## 0.4.0
6+
7+
- Remove `userVersion` and `setUserVersion`. Users should run the pragma statements manually.
8+
- Add `requestLock`, which can be used to make multiple database calls in a locked context.
9+
- Allow aborting requests.
10+
- Add support for a new Web FS (OPFS) access mode based on `readwrite-unsafe`.
411

512
## 0.3.2
613

sqlite3_web/README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ And that is what this package can do for you:
2828
## Getting started
2929

3030
Note: While this package can be used by end applications, it is meant as a
31-
building block for database packages like `sqlite3_async` or `drift`. Using
31+
building block for database packages like `sqlite_async` or `drift`. Using
3232
these packages helps avoid some setup work.
3333

3434
Workers are responsible for opening databases and exposing them through message
@@ -75,6 +75,36 @@ Future<void> connectToDatabase() async {
7575
}
7676
```
7777

78+
## Lock and transaction management
79+
80+
This package provides the `select` and `execute` method returning:
81+
82+
1. The `autocommit` state of the database after running the statement.
83+
2. The last insert rowid value.
84+
3. For `select`, results.
85+
86+
Because each database typically only uses a single connection (likely hosted in a shared worker),
87+
implementing transactions requires multiple statements to run without interference from others.
88+
89+
The `requestLock` API is a helpful building block for this. In its callback, no other tab will
90+
have access to the database:
91+
92+
```dart
93+
await db.requestLock((lock) async {
94+
await db.execute('BEGIN', token: lock);
95+
await db.execute(
96+
'... other statements',
97+
token: lock,
98+
checkInTransaction: true,
99+
);
100+
await db.execute('COMMIT', token: lock, checkInTransaction: true);
101+
});
102+
```
103+
104+
Inside a lock context, the `LockToken` must be passed to `select` and `execute`.
105+
Additionally, the `checkInTransaction` flag can be used to verify that the database
106+
is not in autocommit mode before running a statement.
107+
78108
## Custom requests
79109

80110
In some cases, it may be useful to re-use the worker communication scheme
@@ -86,3 +116,10 @@ open transactions can be implemented in the worker by overriding
86116
`handleCustomRequest` in `WorkerDatabase`. You can encode requests and
87117
responses as arbitrary values exchanged as `JSAny?`.
88118
On the client side, requests can be issued with `Database.customRequest`.
119+
120+
## Testing this package
121+
122+
This package uses both regular `dart test` tests and integration tests.
123+
All tests need a compatible `sqlite3.wasm` file in `web/`.
124+
125+
Integration tests also require `geckodriver` and `chromedriver` to be installed.

sqlite3_web/dart_test.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
platforms: [vm, chrome, firefox]
22
compilers: [dart2wasm, dart2js]
33

4+
tags:
5+
webdriver:
6+
# Sometimes spawning the driver times out, it's just easier to retry.
7+
retry: 2
8+
49
override_platforms:
510
firefox:
611
settings:
712
arguments: "-headless"
13+
14+
presets:
15+
gh_actions:

0 commit comments

Comments
 (0)