Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/sqlite_async/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
## 0.14.0-wip.0
## 0.14.0

__Note__: This version of `sqlite_async` is still in development and there might be additional
API changes between this release and the final `0.14.0` version. This release is mostly meant for
internal testing.

- Support versions 3.x of the `sqlite3` package and 0.6.0 of `sqlite3_web`.
- Support versions 3.x of the `sqlite3` package and 0.7.x of `sqlite3_web`.
- Remove the `sqlite3_open.dart` library, SQLite libraries are no longer loaded through Dart.
- __Breaking__: Rewrite the native connection pool implementation.
- Remove isolate connection factories. Simply open the same database on another isolate, it's safe to do so now.
Expand Down
22 changes: 10 additions & 12 deletions packages/sqlite_async/lib/src/web/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ final class WebDatabase extends SqliteDatabaseImpl

@override
Future<bool> getAutoCommit() async {
final response = await _database.customRequest(
CustomDatabaseMessage(CustomDatabaseMessageKind.getAutoCommit));
final response = await _database
.customRequest(BaseCustomDatabaseMessage.getAutoCommit());
return (response as JSBoolean?)?.toDart ?? false;
}

Expand Down Expand Up @@ -274,16 +274,14 @@ final class _UnscopedContext extends UnscopedContext {
Future<void> executeBatch(String sql, List<List<Object?>> parameterSets) {
return _task.timeAsync('executeBatch', sql: sql, () {
return wrapSqliteException(() async {
for (final set in parameterSets) {
// use execute instead of select to avoid transferring rows from the
// worker to this context.
await _database._database.execute(
sql,
parameters: set,
token: _lock,
checkInTransaction: _checkInTransaction,
);
}
await _database._database.customRequest(
RunBatchRequest(
sql: sql,
parameters: parameterSets,
requireTransaction: _checkInTransaction,
),
token: _lock,
);
});
});
}
Expand Down
74 changes: 68 additions & 6 deletions packages/sqlite_async/lib/src/web/protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,35 @@ import 'package:sqlite3_web/protocol_utils.dart' as proto;
enum CustomDatabaseMessageKind {
ok,
getAutoCommit,
executeBatchInTransaction,
executeBatch,
updateSubscriptionManagement,
notifyUpdates,
}

extension type CustomDatabaseMessage._raw(JSObject _) implements JSObject {
extension type BaseCustomDatabaseMessage._raw(JSObject _) implements JSObject {
external JSString get rawKind;

external factory BaseCustomDatabaseMessage({required JSString rawKind});

factory BaseCustomDatabaseMessage.getAutoCommit() {
return BaseCustomDatabaseMessage(
rawKind: CustomDatabaseMessageKind.getAutoCommit.name.toJS,
);
}

factory BaseCustomDatabaseMessage.okResponse() {
return BaseCustomDatabaseMessage(
rawKind: CustomDatabaseMessageKind.ok.name.toJS,
);
}

CustomDatabaseMessageKind get kind {
return CustomDatabaseMessageKind.values.byName(rawKind.toDart);
}
}

extension type CustomDatabaseMessage._raw(JSObject _)
implements BaseCustomDatabaseMessage {
external factory CustomDatabaseMessage._({
required JSString rawKind,
JSString rawSql,
Expand All @@ -38,16 +61,55 @@ extension type CustomDatabaseMessage._raw(JSObject _) implements JSObject {
);
}

external JSString get rawKind;

external JSString get rawSql;

external JSArray get rawParameters;

/// Not set in earlier versions of this package.
external JSArrayBuffer? get typeInfo;
}

CustomDatabaseMessageKind get kind {
return CustomDatabaseMessageKind.values.byName(rawKind.toDart);
extension type RunBatchRequest._raw(JSObject _)
implements BaseCustomDatabaseMessage {
external factory RunBatchRequest._({
required JSString rawKind,
required JSString rawSql,
required JSArray<BatchParameters> parameters,
required JSBoolean requireTransaction,
});

factory RunBatchRequest({
required String sql,
required List<List<Object?>> parameters,
required bool requireTransaction,
}) {
return RunBatchRequest._(
rawKind: CustomDatabaseMessageKind.executeBatch.name.toJS,
rawSql: sql.toJS,
parameters: parameters.map(BatchParameters.new).toList().toJS,
requireTransaction: requireTransaction.toJS,
);
}

external JSString get rawSql;
external JSArray<BatchParameters> get parameters;
external JSBoolean get requireTransaction;
}

extension type BatchParameters._raw(JSObject _) implements JSObject {
external JSArray get parameters;
external JSArrayBuffer get parameterTypes;

external factory BatchParameters._({
required JSArray parameters,
required JSArrayBuffer parameterTypes,
});

factory BatchParameters(List<Object?> parameters) {
final (params, types) = proto.serializeParameters(parameters);
return BatchParameters._(parameters: params, parameterTypes: types);
}

List<Object?> get decodedParameters =>
proto.deserializeParameters(parameters, parameterTypes);
}
3 changes: 2 additions & 1 deletion packages/sqlite_async/lib/src/web/update_notifications.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ final class UpdateNotificationStreams {
final Map<String, StreamController<UpdateNotification>> _updates = {};

Future<JSAny?> handleRequest(JSAny? request) async {
final customRequest = request as CustomDatabaseMessage;
final customRequest = request as BaseCustomDatabaseMessage;
if (customRequest.kind == CustomDatabaseMessageKind.notifyUpdates) {
customRequest as CustomDatabaseMessage;
final notification = UpdateNotification(customRequest.rawParameters.toDart
.map((e) => (e as JSString).toDart)
.toSet());
Expand Down
45 changes: 27 additions & 18 deletions packages/sqlite_async/lib/src/web/worker/worker_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'dart:js_interop';
import 'package:meta/meta.dart';
import 'package:sqlite3/wasm.dart';
import 'package:sqlite3_web/sqlite3_web.dart';
import 'package:sqlite3_web/protocol_utils.dart' as proto;
import 'package:sqlite_async/src/utils/shared_utils.dart';

import '../protocol.dart';
Expand Down Expand Up @@ -35,7 +34,7 @@ base class AsyncSqliteController extends DatabaseController {

@override
Future<JSAny?> handleCustomRequest(
ClientConnection connection, JSAny? request) {
ClientConnection connection, CustomClientRequest request) {
throw UnimplementedError();
}
}
Expand Down Expand Up @@ -68,29 +67,39 @@ class AsyncSqliteDatabase extends WorkerDatabase {

@override
Future<JSAny?> handleCustomRequest(
ClientConnection connection, JSAny? request) async {
final message = request as CustomDatabaseMessage;
ClientConnection connection, CustomClientDatabaseRequest request) async {
final message = request.request as BaseCustomDatabaseMessage;

switch (message.kind) {
case CustomDatabaseMessageKind.ok:
case CustomDatabaseMessageKind.notifyUpdates:
throw UnsupportedError('This is a response, not a request');
case CustomDatabaseMessageKind.getAutoCommit:
return database.autocommit.toJS;
case CustomDatabaseMessageKind.executeBatchInTransaction:
final sql = message.rawSql.toDart;
final parameters = proto.deserializeParameters(
message.rawParameters, message.typeInfo);
if (database.autocommit) {
throw SqliteException(
extendedResultCode: 0,
message:
'Transaction rolled back by earlier statement. Cannot execute',
causingStatement: sql,
);
}
database.execute(sql, parameters);
case CustomDatabaseMessageKind.executeBatch:
final data = message as RunBatchRequest;

await request.useLock(() {
if (data.requireTransaction.toDart && database.autocommit) {
throw SqliteException(
extendedResultCode: 0,
message:
'Transaction rolled back by earlier statement. Cannot execute',
causingStatement: data.rawSql.toDart,
);
}

final stmt = database.prepare(data.rawSql.toDart);
try {
for (final parameter in data.parameters.toDart) {
stmt.execute(parameter.decodedParameters);
}
} finally {
stmt.close();
}
});
case CustomDatabaseMessageKind.updateSubscriptionManagement:
message as CustomDatabaseMessage;
final shouldSubscribe =
(message.rawParameters.toDart[0] as JSBoolean).toDart;
final id = message.rawSql.toDart;
Expand All @@ -113,7 +122,7 @@ class AsyncSqliteDatabase extends WorkerDatabase {
}
}

return CustomDatabaseMessage(CustomDatabaseMessageKind.ok);
return BaseCustomDatabaseMessage.okResponse();
}

Map<String, dynamic> resultSetToMap(ResultSet resultSet) {
Expand Down
4 changes: 2 additions & 2 deletions packages/sqlite_async/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: sqlite_async
description: High-performance asynchronous interface for SQLite on Dart and Flutter.
version: 0.14.0-wip.0
version: 0.14.0
resolution: workspace
repository: https://github.com/powersync-ja/sqlite_async.dart
environment:
Expand All @@ -14,7 +14,7 @@ topics:

dependencies:
sqlite3: ^3.2.0
sqlite3_web: ^0.6.0
sqlite3_web: ^0.7.0
sqlite3_connection_pool: ^0.2.3
async: ^2.10.0
collection: ^1.17.0
Expand Down
44 changes: 44 additions & 0 deletions packages/sqlite_async/test/basic_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,50 @@ void main() {
expect(result.rows[0][1], equals('test returning without params'));
});

group('executeBatch', () {
late SqliteDatabase db;

setUp(() async {
db = await testUtils.setupDatabase(path: path);
});

tearDown(() => db.close());

test('can execute multiple times', () async {
await createTables(db);

await db
.executeBatch('INSERT INTO test_data (description) VALUES (?)', [
['foo'],
['bar']
]);

final results =
await db.getAll('SELECT description FROM test_data ORDER BY id');
expect(results.length, equals(2));
expect(results.rows[0], equals(['foo']));
expect(results.rows[1], equals(['bar']));
});

test('can execute in transaction', () async {
await createTables(db);
const exception = 'exception thrown for rollback';

await expectLater(db.writeTransaction((tx) async {
await tx
.executeBatch('INSERT INTO test_data (description) VALUES (?)', [
['foo'],
['bar']
]);

expect(await tx.getAll('SELECT * FROM test_data'), hasLength(2));
throw exception;
}), throwsA(exception));

expect(await db.getAll('SELECT * FROM test_data'), isEmpty);
});
});

test('executeMultiple handles multiple statements', () async {
final db = await testUtils.setupDatabase(path: path);
await createTables(db);
Expand Down
Loading