Skip to content

Commit 2938232

Browse files
authored
Merge pull request #86 from powersync-ja/feat/allow-wrapping-common-database
Allow wrapping raw synchronous databases as `SqliteDatabase`
2 parents bc3416c + 1d8de88 commit 2938232

9 files changed

+172
-6
lines changed

packages/sqlite_async/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.11.4
2+
3+
- Add `SqliteConnection.synchronousWrapper` and `SqliteDatabase.singleConnection`.
4+
Together, these can be used to wrap raw `CommonDatabase` instances from `package:sqlite3`
5+
as a `Database` (without an automated worker or isolate setup). This can be useful in tests
6+
where synchronous access to the underlying database is convenient.
7+
18
## 0.11.3
29

310
- Support being compiled with `package:build_web_compilers`.

packages/sqlite_async/lib/src/common/sqlite_database.dart

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:async';
33
import 'package:meta/meta.dart';
44
import 'package:sqlite_async/src/common/abstract_open_factory.dart';
55
import 'package:sqlite_async/src/common/isolate_connection_factory.dart';
6+
import 'package:sqlite_async/src/impl/single_connection_database.dart';
67
import 'package:sqlite_async/src/impl/sqlite_database_impl.dart';
78
import 'package:sqlite_async/src/sqlite_options.dart';
89
import 'package:sqlite_async/src/sqlite_queries.dart';
@@ -82,4 +83,24 @@ abstract class SqliteDatabase
8283
{int maxReaders = SqliteDatabase.defaultMaxReaders}) {
8384
return SqliteDatabaseImpl.withFactory(openFactory, maxReaders: maxReaders);
8485
}
86+
87+
/// Opens a [SqliteDatabase] that only wraps an underlying connection.
88+
///
89+
/// This function may be useful in some instances like tests, but should not
90+
/// typically be used by applications. Compared to the other ways to open
91+
/// databases, it has the following downsides:
92+
///
93+
/// 1. No connection pool / concurrent readers for native databases.
94+
/// 2. No reliable update notifications on the web.
95+
/// 3. There is no reliable transaction management in Dart, and opening the
96+
/// same database with [SqliteDatabase.singleConnection] multiple times
97+
/// may cause "database is locked" errors.
98+
///
99+
/// Together with [SqliteConnection.synchronousWrapper], this can be used to
100+
/// open in-memory databases (e.g. via [SqliteOpenFactory.open]). That
101+
/// bypasses most convenience features, but may still be useful for
102+
/// short-lived databases used in tests.
103+
factory SqliteDatabase.singleConnection(SqliteConnection connection) {
104+
return SingleConnectionDatabase(connection);
105+
}
85106
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'package:sqlite3/common.dart';
2+
import 'package:sqlite_async/sqlite_async.dart';
3+
4+
/// A database implementation that delegates everything to a single connection.
5+
///
6+
/// This doesn't provide an automatic connection pool or the web worker
7+
/// management, but it can still be useful in cases like unit tests where those
8+
/// features might not be necessary. Since only a single sqlite connection is
9+
/// used internally, this also allows using in-memory databases.
10+
final class SingleConnectionDatabase
11+
with SqliteQueries, SqliteDatabaseMixin
12+
implements SqliteDatabase {
13+
final SqliteConnection connection;
14+
15+
SingleConnectionDatabase(this.connection);
16+
17+
@override
18+
Future<void> close() => connection.close();
19+
20+
@override
21+
bool get closed => connection.closed;
22+
23+
@override
24+
Future<bool> getAutoCommit() => connection.getAutoCommit();
25+
26+
@override
27+
Future<void> get isInitialized => Future.value();
28+
29+
@override
30+
IsolateConnectionFactory<CommonDatabase> isolateConnectionFactory() {
31+
throw UnsupportedError(
32+
"SqliteDatabase.singleConnection instances can't be used across "
33+
'isolates.');
34+
}
35+
36+
@override
37+
int get maxReaders => 1;
38+
39+
@override
40+
AbstractDefaultSqliteOpenFactory<CommonDatabase> get openFactory =>
41+
throw UnimplementedError();
42+
43+
@override
44+
Future<T> readLock<T>(Future<T> Function(SqliteReadContext tx) callback,
45+
{Duration? lockTimeout, String? debugContext}) {
46+
return connection.readLock(callback,
47+
lockTimeout: lockTimeout, debugContext: debugContext);
48+
}
49+
50+
@override
51+
Stream<UpdateNotification> get updates =>
52+
connection.updates ?? const Stream.empty();
53+
54+
@override
55+
Future<T> writeLock<T>(Future<T> Function(SqliteWriteContext tx) callback,
56+
{Duration? lockTimeout, String? debugContext}) {
57+
return connection.writeLock(callback,
58+
lockTimeout: lockTimeout, debugContext: debugContext);
59+
}
60+
}

packages/sqlite_async/lib/src/sqlite_connection.dart

+23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import 'dart:async';
22

33
import 'package:sqlite3/common.dart' as sqlite;
4+
import 'package:sqlite_async/mutex.dart';
5+
import 'package:sqlite_async/sqlite3_common.dart';
46
import 'package:sqlite_async/src/update_notification.dart';
57

8+
import 'common/connection/sync_sqlite_connection.dart';
9+
610
/// Abstract class representing calls available in a read-only or read-write context.
711
abstract class SqliteReadContext {
812
/// Execute a read-only (SELECT) query and return the results.
@@ -74,7 +78,26 @@ abstract class SqliteWriteContext extends SqliteReadContext {
7478
}
7579

7680
/// Abstract class representing a connection to the SQLite database.
81+
///
82+
/// This package typically pools multiple [SqliteConnection] instances into a
83+
/// managed [SqliteDatabase] automatically.
7784
abstract class SqliteConnection extends SqliteWriteContext {
85+
/// Default constructor for subclasses.
86+
SqliteConnection();
87+
88+
/// Creates a [SqliteConnection] instance that wraps a raw [CommonDatabase]
89+
/// from the `sqlite3` package.
90+
///
91+
/// Users should not typically create connections manually at all. Instead,
92+
/// open a [SqliteDatabase] through a factory. In special scenarios where it
93+
/// may be easier to wrap a [raw] databases (like unit tests), this method
94+
/// may be used as an escape hatch for the asynchronous wrappers provided by
95+
/// this package.
96+
factory SqliteConnection.synchronousWrapper(CommonDatabase raw,
97+
{Mutex? mutex}) {
98+
return SyncSqliteConnection(raw, mutex ?? Mutex());
99+
}
100+
78101
/// Reports table change update notifications
79102
Stream<UpdateNotification>? get updates;
80103

packages/sqlite_async/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: sqlite_async
22
description: High-performance asynchronous interface for SQLite on Dart and Flutter.
3-
version: 0.11.3
3+
version: 0.11.4
44
repository: https://github.com/powersync-ja/sqlite_async.dart
55
environment:
66
sdk: ">=3.5.0 <4.0.0"

packages/sqlite_async/test/basic_test.dart

+21
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,27 @@ void main() {
188188
),
189189
);
190190
});
191+
192+
test('can use raw database instance', () async {
193+
final factory = await testUtils.testFactory();
194+
final raw = await factory.openDatabaseForSingleConnection();
195+
// Creating a fuction ensures that this database is actually used - if
196+
// a connection were set up in a background isolate, it wouldn't have this
197+
// function.
198+
raw.createFunction(
199+
functionName: 'my_function', function: (args) => 'test');
200+
201+
final db = SqliteDatabase.singleConnection(
202+
SqliteConnection.synchronousWrapper(raw));
203+
await createTables(db);
204+
205+
expect(db.updates, emits(UpdateNotification({'test_data'})));
206+
await db
207+
.execute('INSERT INTO test_data(description) VALUES (my_function())');
208+
209+
expect(await db.get('SELECT description FROM test_data'),
210+
{'description': 'test'});
211+
});
191212
});
192213
}
193214

packages/sqlite_async/test/utils/abstract_test_utils.dart

+5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import 'package:sqlite_async/sqlite3_common.dart';
12
import 'package:sqlite_async/sqlite_async.dart';
23

34
class TestDefaultSqliteOpenFactory extends DefaultSqliteOpenFactory {
45
final String sqlitePath;
56

67
TestDefaultSqliteOpenFactory(
78
{required super.path, super.sqliteOptions, this.sqlitePath = ''});
9+
10+
Future<CommonDatabase> openDatabaseForSingleConnection() async {
11+
return openDB(SqliteOpenOptions(primaryConnection: true, readOnly: false));
12+
}
813
}
914

1015
abstract class AbstractTestUtils {

packages/sqlite_async/test/utils/native_test_utils.dart

+13-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'dart:isolate';
55

66
import 'package:glob/glob.dart';
77
import 'package:glob/list_local_fs.dart';
8+
import 'package:sqlite_async/sqlite3.dart';
89
import 'package:sqlite_async/sqlite3_common.dart';
910
import 'package:sqlite_async/sqlite_async.dart';
1011
import 'package:sqlite3/open.dart' as sqlite_open;
@@ -21,11 +22,15 @@ class TestSqliteOpenFactory extends TestDefaultSqliteOpenFactory {
2122
super.sqlitePath = defaultSqlitePath,
2223
initStatements});
2324

24-
@override
25-
CommonDatabase open(SqliteOpenOptions options) {
25+
void _applyOpenOverrides() {
2626
sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.linux, () {
2727
return DynamicLibrary.open(sqlitePath);
2828
});
29+
}
30+
31+
@override
32+
CommonDatabase open(SqliteOpenOptions options) {
33+
_applyOpenOverrides();
2934
final db = super.open(options);
3035

3136
db.createFunction(
@@ -48,6 +53,12 @@ class TestSqliteOpenFactory extends TestDefaultSqliteOpenFactory {
4853

4954
return db;
5055
}
56+
57+
@override
58+
Future<CommonDatabase> openDatabaseForSingleConnection() async {
59+
_applyOpenOverrides();
60+
return sqlite3.openInMemory();
61+
}
5162
}
5263

5364
class TestUtils extends AbstractTestUtils {

packages/sqlite_async/test/utils/web_test_utils.dart

+21-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:js_interop';
33
import 'dart:math';
44

5+
import 'package:sqlite_async/sqlite3_wasm.dart';
56
import 'package:sqlite_async/sqlite_async.dart';
67
import 'package:test/test.dart';
78
import 'package:web/web.dart' show Blob, BlobPart, BlobPropertyBag;
@@ -12,6 +13,20 @@ external String _createObjectURL(Blob blob);
1213

1314
String? _dbPath;
1415

16+
class TestSqliteOpenFactory extends TestDefaultSqliteOpenFactory {
17+
TestSqliteOpenFactory(
18+
{required super.path, super.sqliteOptions, super.sqlitePath = ''});
19+
20+
@override
21+
Future<CommonDatabase> openDatabaseForSingleConnection() async {
22+
final sqlite = await WasmSqlite3.loadFromUrl(
23+
Uri.parse(sqliteOptions.webSqliteOptions.wasmUri));
24+
sqlite.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true);
25+
26+
return sqlite.openInMemory();
27+
}
28+
}
29+
1530
class TestUtils extends AbstractTestUtils {
1631
late Future<void> _isInitialized;
1732
late final SqliteOptions webOptions;
@@ -57,12 +72,15 @@ class TestUtils extends AbstractTestUtils {
5772
@override
5873
Future<TestDefaultSqliteOpenFactory> testFactory(
5974
{String? path,
60-
String? sqlitePath,
75+
String sqlitePath = '',
6176
List<String> initStatements = const [],
6277
SqliteOptions options = const SqliteOptions.defaults()}) async {
6378
await _isInitialized;
64-
return super.testFactory(
65-
path: path, options: webOptions, initStatements: initStatements);
79+
return TestSqliteOpenFactory(
80+
path: path ?? dbPath(),
81+
sqlitePath: sqlitePath,
82+
sqliteOptions: webOptions,
83+
);
6684
}
6785

6886
@override

0 commit comments

Comments
 (0)