mirror of https://github.com/AMT-Cheif/drift.git
Support executing queries over isolates
This commit is contained in:
parent
5cc1f85441
commit
a2c7c11abf
|
@ -0,0 +1,5 @@
|
||||||
|
/// Contains utils to run moor databases in a background isolate. This API is
|
||||||
|
/// not supported on the web.
|
||||||
|
library isolate;
|
||||||
|
|
||||||
|
export 'src/runtime/isolate/moor_isolate.dart';
|
|
@ -438,7 +438,9 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
|
||||||
|
|
||||||
/// Used by generated code to connect to a database that is already open.
|
/// Used by generated code to connect to a database that is already open.
|
||||||
GeneratedDatabase.connect(DatabaseConnection connection)
|
GeneratedDatabase.connect(DatabaseConnection connection)
|
||||||
: super.fromConnection(connection);
|
: super.fromConnection(connection) {
|
||||||
|
connection?.executor?.databaseInfo = this;
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a [Migrator] with the provided query executor. Migrators generate
|
/// Creates a [Migrator] with the provided query executor. Migrators generate
|
||||||
/// sql statements to create or drop tables.
|
/// sql statements to create or drop tables.
|
||||||
|
|
|
@ -19,9 +19,10 @@ class _MoorClient {
|
||||||
_channel.setRequestHandler(_handleRequest);
|
_channel.setRequestHandler(_handleRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<_MoorClient> connect(MoorIsolate isolate) async {
|
static Future<_MoorClient> connect(
|
||||||
final connection =
|
MoorIsolate isolate, bool isolateDebugLog) async {
|
||||||
await IsolateCommunication.connectAsClient(isolate._server);
|
final connection = await IsolateCommunication.connectAsClient(
|
||||||
|
isolate._server, isolateDebugLog);
|
||||||
|
|
||||||
final typeSystem =
|
final typeSystem =
|
||||||
await connection.request<SqlTypeSystem>(_NoArgsRequest.getTypeSystem);
|
await connection.request<SqlTypeSystem>(_NoArgsRequest.getTypeSystem);
|
||||||
|
@ -34,19 +35,19 @@ class _MoorClient {
|
||||||
if (payload is _NoArgsRequest) {
|
if (payload is _NoArgsRequest) {
|
||||||
switch (payload) {
|
switch (payload) {
|
||||||
case _NoArgsRequest.runOnCreate:
|
case _NoArgsRequest.runOnCreate:
|
||||||
connectedDb.handleDatabaseCreation(executor: executor);
|
return connectedDb.handleDatabaseCreation(executor: executor);
|
||||||
return null;
|
|
||||||
default:
|
default:
|
||||||
throw UnsupportedError('This operation must be run on the server');
|
throw UnsupportedError('This operation must be run on the server');
|
||||||
}
|
}
|
||||||
} else if (payload is _RunOnUpgrade) {
|
} else if (payload is _RunOnUpgrade) {
|
||||||
connectedDb.handleDatabaseVersionChange(
|
return connectedDb.handleDatabaseVersionChange(
|
||||||
executor: executor,
|
executor: executor,
|
||||||
from: payload.versionBefore,
|
from: payload.versionBefore,
|
||||||
to: payload.versionNow,
|
to: payload.versionNow,
|
||||||
);
|
);
|
||||||
} else if (payload is _RunBeforeOpen) {
|
} else if (payload is _RunBeforeOpen) {
|
||||||
connectedDb.beforeOpenCallback(_connection.executor, payload.details);
|
return connectedDb.beforeOpenCallback(
|
||||||
|
_connection.executor, payload.details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,4 +112,10 @@ class _IsolateQueryExecutor extends QueryExecutor {
|
||||||
Future<List<Map<String, dynamic>>> runSelect(String statement, List args) {
|
Future<List<Map<String, dynamic>>> runSelect(String statement, List args) {
|
||||||
return _runRequest(_StatementMethod.select, statement, args);
|
return _runRequest(_StatementMethod.select, statement, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
client._channel.close();
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,9 @@ class IsolateCommunication {
|
||||||
/// The [SendPort] used to send messages to the peer.
|
/// The [SendPort] used to send messages to the peer.
|
||||||
final SendPort sendPort;
|
final SendPort sendPort;
|
||||||
|
|
||||||
/// The [ReceivePort] used to receive messages from the peer.
|
/// The input stream of this channel. This could be a [ReceivePort].
|
||||||
final ReceivePort receivePort;
|
final Stream<dynamic> input;
|
||||||
|
StreamSubscription _inputSubscription;
|
||||||
|
|
||||||
// note that there are two IsolateCommunication instances in each connection,
|
// note that there are two IsolateCommunication instances in each connection,
|
||||||
// and each of them has an independent _currentRequestId field!
|
// and each of them has an independent _currentRequestId field!
|
||||||
|
@ -20,8 +21,10 @@ class IsolateCommunication {
|
||||||
final Map<int, Completer> _pendingRequests = {};
|
final Map<int, Completer> _pendingRequests = {};
|
||||||
final StreamController<Request> _incomingRequests = StreamController();
|
final StreamController<Request> _incomingRequests = StreamController();
|
||||||
|
|
||||||
IsolateCommunication._(this.sendPort, this.receivePort) {
|
final bool _debugLog;
|
||||||
receivePort.listen(_handleMessage);
|
|
||||||
|
IsolateCommunication._(this.sendPort, this.input, [this._debugLog = false]) {
|
||||||
|
_inputSubscription = input.listen(_handleMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a future that resolves when this communication channel was closed,
|
/// Returns a future that resolves when this communication channel was closed,
|
||||||
|
@ -33,25 +36,27 @@ class IsolateCommunication {
|
||||||
|
|
||||||
/// Establishes an [IsolateCommunication] by connecting to the [Server] which
|
/// Establishes an [IsolateCommunication] by connecting to the [Server] which
|
||||||
/// emitted the [key].
|
/// emitted the [key].
|
||||||
static Future<IsolateCommunication> connectAsClient(ServerKey key) async {
|
static Future<IsolateCommunication> connectAsClient(ServerKey key,
|
||||||
|
[bool debugLog = false]) async {
|
||||||
final clientReceive = ReceivePort();
|
final clientReceive = ReceivePort();
|
||||||
|
final stream = clientReceive.asBroadcastStream();
|
||||||
|
|
||||||
key.openConnectionPort
|
key.openConnectionPort
|
||||||
.send(_ClientConnectionRequest(clientReceive.sendPort));
|
.send(_ClientConnectionRequest(clientReceive.sendPort));
|
||||||
|
|
||||||
final response = (await clientReceive.first) as _ServerConnectionResponse;
|
final response = (await stream.first) as _ServerConnectionResponse;
|
||||||
|
|
||||||
return IsolateCommunication._(response.sendPort, clientReceive);
|
return IsolateCommunication._(response.sendPort, stream, debugLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes the connection to the server.
|
/// Closes the connection to the server.
|
||||||
void close() {
|
void close() {
|
||||||
sendPort.send(_ConnectionClose());
|
_send(_ConnectionClose());
|
||||||
_closeLocally();
|
_closeLocally();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _closeLocally() {
|
void _closeLocally() {
|
||||||
receivePort.close();
|
_inputSubscription?.cancel();
|
||||||
_closeCompleter.complete();
|
_closeCompleter.complete();
|
||||||
|
|
||||||
for (var pending in _pendingRequests.values) {
|
for (var pending in _pendingRequests.values) {
|
||||||
|
@ -61,6 +66,10 @@ class IsolateCommunication {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleMessage(dynamic msg) {
|
void _handleMessage(dynamic msg) {
|
||||||
|
if (_debugLog) {
|
||||||
|
print('[IN]: $msg');
|
||||||
|
}
|
||||||
|
|
||||||
if (msg is _ConnectionClose) {
|
if (msg is _ConnectionClose) {
|
||||||
_closeLocally();
|
_closeLocally();
|
||||||
} else if (msg is _Response) {
|
} else if (msg is _Response) {
|
||||||
|
@ -79,6 +88,8 @@ class IsolateCommunication {
|
||||||
|
|
||||||
_pendingRequests.remove(msg.requestId);
|
_pendingRequests.remove(msg.requestId);
|
||||||
}
|
}
|
||||||
|
} else if (msg is Request) {
|
||||||
|
_incomingRequests.add(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,28 +100,40 @@ class IsolateCommunication {
|
||||||
final completer = Completer<T>();
|
final completer = Completer<T>();
|
||||||
|
|
||||||
_pendingRequests[id] = completer;
|
_pendingRequests[id] = completer;
|
||||||
sendPort.send(Request._(_currentRequestId++, request));
|
_send(Request._(id, request));
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _send(dynamic msg) {
|
||||||
|
if (_debugLog) {
|
||||||
|
print('[OUT]: $msg');
|
||||||
|
}
|
||||||
|
sendPort.send(msg);
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends a response for a handled [Request].
|
/// Sends a response for a handled [Request].
|
||||||
void respond(Request request, dynamic response) {
|
void respond(Request request, dynamic response) {
|
||||||
sendPort.send(_Response(request.id, response));
|
_send(_Response(request.id, response));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an erroneous response for a [Request].
|
/// Sends an erroneous response for a [Request].
|
||||||
void respondError(Request request, dynamic error, [StackTrace trace]) {
|
void respondError(Request request, dynamic error, [StackTrace trace]) {
|
||||||
sendPort.send(_ErrorResponse(request.id, error, trace.toString()));
|
_send(_ErrorResponse(request.id, error, trace.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utility that listens to [incomingRequests] and invokes the [handler] on
|
/// Utility that listens to [incomingRequests] and invokes the [handler] on
|
||||||
/// each request, sending the result back to the originating client. If
|
/// each request, sending the result back to the originating client. If
|
||||||
/// [handler] throws, the error will be re-directed to the client.
|
/// [handler] throws, the error will be re-directed to the client. If
|
||||||
|
/// [handler] returns a [Future], it will be awaited.
|
||||||
void setRequestHandler(dynamic Function(Request) handler) {
|
void setRequestHandler(dynamic Function(Request) handler) {
|
||||||
incomingRequests.listen((request) {
|
incomingRequests.listen((request) {
|
||||||
try {
|
try {
|
||||||
final result = handler(request);
|
final result = handler(request);
|
||||||
respond(request, result);
|
if (result is Future) {
|
||||||
|
result.then((value) => respond(request, value));
|
||||||
|
} else {
|
||||||
|
respond(request, result);
|
||||||
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
respondError(request, e, s);
|
respondError(request, e, s);
|
||||||
}
|
}
|
||||||
|
@ -213,6 +236,11 @@ class Request {
|
||||||
final dynamic payload;
|
final dynamic payload;
|
||||||
|
|
||||||
Request._(this.id, this.payload);
|
Request._(this.id, this.payload);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'request (id = $id): $payload';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Response {
|
class _Response {
|
||||||
|
@ -220,6 +248,11 @@ class _Response {
|
||||||
final dynamic response;
|
final dynamic response;
|
||||||
|
|
||||||
_Response(this.requestId, this.response);
|
_Response(this.requestId, this.response);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'response (id = $requestId): $response';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ErrorResponse extends _Response {
|
class _ErrorResponse extends _Response {
|
||||||
|
@ -229,4 +262,9 @@ class _ErrorResponse extends _Response {
|
||||||
|
|
||||||
_ErrorResponse(int requestId, dynamic error, [this.stackTrace])
|
_ErrorResponse(int requestId, dynamic error, [this.stackTrace])
|
||||||
: super(requestId, error);
|
: super(requestId, error);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'error response (id = $requestId): $error at $stackTrace';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:moor/moor.dart';
|
import 'package:moor/moor.dart';
|
||||||
import 'package:moor/moor_web.dart';
|
|
||||||
import 'communication.dart';
|
import 'communication.dart';
|
||||||
|
|
||||||
part 'client.dart';
|
part 'client.dart';
|
||||||
|
@ -34,15 +33,23 @@ class MoorIsolate {
|
||||||
/// Identifier for the server isolate that we can connect to.
|
/// Identifier for the server isolate that we can connect to.
|
||||||
final ServerKey _server;
|
final ServerKey _server;
|
||||||
|
|
||||||
MoorIsolate._(this._server);
|
final Isolate _isolate;
|
||||||
|
|
||||||
|
MoorIsolate._(this._server, this._isolate);
|
||||||
|
|
||||||
/// Connects to this [MoorIsolate] from another isolate. All operations on the
|
/// Connects to this [MoorIsolate] from another isolate. All operations on the
|
||||||
/// returned [DatabaseConnection] will be executed on a background isolate.
|
/// returned [DatabaseConnection] will be executed on a background isolate.
|
||||||
Future<DatabaseConnection> connect() async {
|
/// Setting the [isolateDebugLog] is only helpful when debugging moor itself.
|
||||||
final client = await _MoorClient.connect(this);
|
Future<DatabaseConnection> connect({bool isolateDebugLog = false}) async {
|
||||||
|
final client = await _MoorClient.connect(this, isolateDebugLog);
|
||||||
return client._connection;
|
return client._connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls [Isolate.kill] on the underlying isolate.
|
||||||
|
void kill() {
|
||||||
|
_isolate.kill();
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a new [MoorIsolate] on a background thread.
|
/// Creates a new [MoorIsolate] on a background thread.
|
||||||
///
|
///
|
||||||
/// The [opener] function will be used to open the [DatabaseConnection] used
|
/// The [opener] function will be used to open the [DatabaseConnection] used
|
||||||
|
@ -57,9 +64,10 @@ class MoorIsolate {
|
||||||
final receiveServer = ReceivePort();
|
final receiveServer = ReceivePort();
|
||||||
final keyFuture = receiveServer.first;
|
final keyFuture = receiveServer.first;
|
||||||
|
|
||||||
await Isolate.spawn(_startMoorIsolate, [receiveServer.sendPort, opener]);
|
final isolate = await Isolate.spawn(
|
||||||
|
_startMoorIsolate, [receiveServer.sendPort, opener]);
|
||||||
final key = await keyFuture as ServerKey;
|
final key = await keyFuture as ServerKey;
|
||||||
return MoorIsolate._(key);
|
return MoorIsolate._(key, isolate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a [MoorIsolate] in the [Isolate.current] isolate. The returned
|
/// Creates a [MoorIsolate] in the [Isolate.current] isolate. The returned
|
||||||
|
@ -68,7 +76,7 @@ class MoorIsolate {
|
||||||
/// connection which operations are all executed on this isolate.
|
/// connection which operations are all executed on this isolate.
|
||||||
static MoorIsolate inCurrent(DatabaseOpener opener) {
|
static MoorIsolate inCurrent(DatabaseOpener opener) {
|
||||||
final server = _MoorServer(opener);
|
final server = _MoorServer(opener);
|
||||||
return MoorIsolate._(server.key);
|
return MoorIsolate._(server.key, Isolate.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,11 @@ class _ExecuteQuery {
|
||||||
final List<dynamic> args;
|
final List<dynamic> args;
|
||||||
|
|
||||||
_ExecuteQuery(this.method, this.sql, this.args);
|
_ExecuteQuery(this.method, this.sql, this.args);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '$method: $sql with $args';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sent from the client to notify the server of the
|
/// Sent from the client to notify the server of the
|
||||||
|
|
|
@ -41,8 +41,26 @@ class _MoorServer {
|
||||||
} else if (payload is _SetSchemaVersion) {
|
} else if (payload is _SetSchemaVersion) {
|
||||||
_fakeDb.schemaVersion = payload.schemaVersion;
|
_fakeDb.schemaVersion = payload.schemaVersion;
|
||||||
return null;
|
return null;
|
||||||
|
} else if (payload is _ExecuteQuery) {
|
||||||
|
return _runQuery(payload.method, payload.sql, payload.args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _runQuery(_StatementMethod method, String sql, List args) {
|
||||||
|
final executor = connection.executor;
|
||||||
|
switch (method) {
|
||||||
|
case _StatementMethod.custom:
|
||||||
|
return executor.runCustom(sql, args);
|
||||||
|
case _StatementMethod.deleteOrUpdate:
|
||||||
|
return executor.runDelete(sql, args);
|
||||||
|
case _StatementMethod.insert:
|
||||||
|
return executor.runInsert(sql, args);
|
||||||
|
case _StatementMethod.select:
|
||||||
|
return executor.runSelect(sql, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw AssertionError("Unknown _StatementMethod, this can't happen.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A mock database so that the [QueryExecutor] which is running on a background
|
/// A mock database so that the [QueryExecutor] which is running on a background
|
||||||
|
|
|
@ -20,6 +20,8 @@ dependencies:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
moor_generator: ^2.0.0
|
moor_generator: ^2.0.0
|
||||||
|
moor_ffi: # Used to run some tests
|
||||||
|
path: ../moor_ffi
|
||||||
build_runner: '>=1.3.0 <2.0.0'
|
build_runner: '>=1.3.0 <2.0.0'
|
||||||
build_test: ^0.10.8
|
build_test: ^0.10.8
|
||||||
test: ^1.6.4
|
test: ^1.6.4
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:moor/isolate.dart';
|
||||||
|
import 'package:moor/moor.dart';
|
||||||
|
import 'package:moor_ffi/moor_ffi.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'data/tables/todos.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
MoorIsolate isolate;
|
||||||
|
DatabaseConnection isolateConnection;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
isolate = await MoorIsolate.spawn(_backgroundConnection);
|
||||||
|
isolateConnection = await isolate.connect(isolateDebugLog: false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
isolateConnection.executor.close();
|
||||||
|
isolate.kill();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can open database and send requests', () async {
|
||||||
|
final database = TodoDb.connect(isolateConnection);
|
||||||
|
|
||||||
|
final result = await database.select(database.todosTable).get();
|
||||||
|
expect(result, isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseConnection _backgroundConnection() {
|
||||||
|
return DatabaseConnection.fromExecutor(VmDatabase.memory());
|
||||||
|
}
|
Loading…
Reference in New Issue