mirror of https://github.com/AMT-Cheif/drift.git
Extract sql.js wrapper into own file, batched statement
This commit is contained in:
parent
544db67d17
commit
1c423d9d7b
|
@ -8,14 +8,14 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:html';
|
||||
import 'dart:indexed_db';
|
||||
import 'dart:js';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:meta/dart2js.dart';
|
||||
|
||||
import 'moor.dart';
|
||||
import 'src/web/sql_js.dart';
|
||||
|
||||
export 'moor.dart';
|
||||
|
||||
part 'src/web/sql_js.dart';
|
||||
part 'src/web/binary_string_conversion.dart';
|
||||
part 'src/web/web_db.dart';
|
||||
|
|
|
@ -1,221 +1,127 @@
|
|||
part of 'package:moor/moor_web.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:js';
|
||||
|
||||
const _initSqlJs = 'initSqlJs';
|
||||
import 'dart:typed_data';
|
||||
|
||||
// ignore_for_file: cascade_invocations
|
||||
// We write our own mapping code to js instead of depending on package:js
|
||||
// This way, projects using moor can run on flutter as long as they don't import
|
||||
// this file.
|
||||
|
||||
/// Experimental moor backend for the web. To use this platform, you need to
|
||||
/// include the latest version of `sql.js` in your html.
|
||||
class WebDatabase extends QueryExecutor {
|
||||
final bool logStatements;
|
||||
final String name;
|
||||
Completer<SqlJsModule> _moduleCompleter;
|
||||
|
||||
Completer<bool> _opening;
|
||||
JsObject _database;
|
||||
|
||||
// resolves to the SQL module. See the `initSqlJs` call in https://github.com/kripken/sql.js#example-html-file
|
||||
// This completer resolves to the `SQL` variable in that example.
|
||||
static Completer<JsObject> _initializedWasm;
|
||||
|
||||
WebDatabase(this.name, {this.logStatements = false}) {
|
||||
if (context.hasProperty(_initSqlJs) == null) {
|
||||
throw UnsupportedError('Could not access the sql.js javascript library. '
|
||||
'The moor documentation contains instructions on how to setup moor '
|
||||
'the web, which might help you fix this.');
|
||||
}
|
||||
_loadWasmIfNeeded();
|
||||
Future<SqlJsModule> initSqlJs() {
|
||||
if (_moduleCompleter != null) {
|
||||
return _moduleCompleter.future;
|
||||
}
|
||||
|
||||
void _loadWasmIfNeeded() {
|
||||
if (_initializedWasm != null) return;
|
||||
|
||||
_initializedWasm = Completer();
|
||||
// initSqlJs().then((sql) => _initialitedWasm.complete(sql));
|
||||
final promise = context.callMethod(_initSqlJs) as JsObject;
|
||||
promise.callMethod('then', [
|
||||
allowInterop((JsObject data) {
|
||||
_initializedWasm.complete(data);
|
||||
})
|
||||
]);
|
||||
_moduleCompleter = Completer();
|
||||
if (!context.hasProperty('initSqlJs')) {
|
||||
return Future.error(
|
||||
UnsupportedError('Could not access the sql.js javascript library. '
|
||||
'The moor documentation contains instructions on how to setup moor '
|
||||
'the web, which might help you fix this.'));
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
throw StateError(
|
||||
'Transactions are not currently supported with the sql.js backend');
|
||||
}
|
||||
(context.callMethod('initSqlJs') as JsObject).callMethod('then', [
|
||||
allowInterop((sqlModule) {
|
||||
_moduleCompleter.complete(SqlJsModule._(sqlModule as JsObject));
|
||||
})
|
||||
]);
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen() async {
|
||||
if (_opening == null) {
|
||||
_opening = Completer();
|
||||
await _openInternal();
|
||||
_opening.complete();
|
||||
} else {
|
||||
await _opening.future;
|
||||
}
|
||||
return _moduleCompleter.future;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
class SqlJsModule {
|
||||
final JsObject _obj;
|
||||
SqlJsModule._(this._obj);
|
||||
|
||||
Future<void> _openInternal() async {
|
||||
// We don't get information about the database version from sql.js, so we
|
||||
// create another database just to manage versions.
|
||||
if (!IdbFactory.supported) {
|
||||
throw UnsupportedError("This browser doesn't support IndexedDb");
|
||||
}
|
||||
|
||||
int version;
|
||||
var upgradeNeeded = false;
|
||||
|
||||
final db = await window.indexedDB.open(
|
||||
name,
|
||||
version: databaseInfo.schemaVersion,
|
||||
onUpgradeNeeded: (event) {
|
||||
upgradeNeeded = true;
|
||||
version = event.oldVersion;
|
||||
},
|
||||
);
|
||||
db.close();
|
||||
|
||||
final sql = await _initializedWasm.future;
|
||||
final restored = _restoreDb();
|
||||
// var db = new SQL.Database()
|
||||
_database = JsObject(sql['Database'] as JsFunction,
|
||||
restored != null ? [restored] : const []);
|
||||
SqlJsDatabase createDatabase([Uint8List data]) {
|
||||
final dbObj = _createInternally(data);
|
||||
assert(() {
|
||||
// set the window.db variable to make debugging easier
|
||||
context['db'] = _database;
|
||||
context['db'] = dbObj;
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (upgradeNeeded) {
|
||||
if (version == null || version < 1) {
|
||||
await databaseInfo.handleDatabaseCreation(executor: _runWithoutArgs);
|
||||
} else {
|
||||
await databaseInfo.handleDatabaseVersionChange(
|
||||
executor: _runWithoutArgs,
|
||||
from: version,
|
||||
to: databaseInfo.schemaVersion);
|
||||
}
|
||||
}
|
||||
return SqlJsDatabase._(dbObj);
|
||||
}
|
||||
|
||||
String get _persistenceKey => 'moor_db_str_$name';
|
||||
JsObject _createInternally(Uint8List data) {
|
||||
final constructor = _obj['Database'] as JsFunction;
|
||||
|
||||
// todo base64 works, but is very slow. Figure out why bin2str is broken
|
||||
|
||||
Uint8List _restoreDb() {
|
||||
final raw = window.localStorage[_persistenceKey];
|
||||
if (raw != null) {
|
||||
return base64.decode(raw);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _storeDb() {
|
||||
final data = _database.callMethod('export') as Uint8List;
|
||||
final binStr = base64.encode(data);
|
||||
window.localStorage[_persistenceKey] = binStr;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(List<BatchedStatement> statements) {
|
||||
throw StateError(
|
||||
'Batched statements are not currently supported with the web backend');
|
||||
}
|
||||
|
||||
@tryInline
|
||||
void _log(String sql, List<dynamic> variables) {
|
||||
if (logStatements) {
|
||||
print('[moor_web]: Running $sql with bound args: $variables');
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes [sql] with the bound [variables], and ignores the result.
|
||||
void _runSimple(String sql, List<dynamic> variables) {
|
||||
_log(sql, variables);
|
||||
if (variables.isEmpty) {
|
||||
_database.callMethod('run', [sql]);
|
||||
if (data != null) {
|
||||
return JsObject(constructor, [data]);
|
||||
} else {
|
||||
final ar = JsArray.from(variables);
|
||||
_database.callMethod('run', [sql, ar]);
|
||||
return JsObject(constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runWithoutArgs(String query) {
|
||||
_runSimple(query, const []);
|
||||
return Future.value(null);
|
||||
class SqlJsDatabase {
|
||||
final JsObject _obj;
|
||||
SqlJsDatabase._(this._obj);
|
||||
|
||||
PreparedStatement prepare(String sql) {
|
||||
final obj = _obj.callMethod('prepare', [sql]) as JsObject;
|
||||
return PreparedStatement._(obj);
|
||||
}
|
||||
|
||||
void run(String sql) {
|
||||
_obj.callMethod('run', [sql]);
|
||||
}
|
||||
|
||||
void runWithArgs(String sql, List<dynamic> args) {
|
||||
final ar = JsArray.from(args);
|
||||
_obj.callMethod('run', [sql, ar]);
|
||||
}
|
||||
|
||||
/// Returns the amount of rows affected by the most recent INSERT, UPDATE or
|
||||
/// DELETE statement.
|
||||
int _getModifiedRows() {
|
||||
return _database.callMethod('getRowsModified') as int;
|
||||
int lastModifiedRows() {
|
||||
return _obj.callMethod('getRowsModified') as int;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement) {
|
||||
return _runWithoutArgs(statement);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List args) {
|
||||
_runSimple(statement, args);
|
||||
return _handlePotentialUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List args) {
|
||||
_runSimple(statement, args);
|
||||
return _handlePotentialUpdate();
|
||||
}
|
||||
|
||||
/// Saves the database if the last statement changed rows. As a side-effect,
|
||||
/// saving the database resets the `last_insert_id` counter in sqlite.
|
||||
Future<int> _handlePotentialUpdate() {
|
||||
final modified = _getModifiedRows();
|
||||
if (modified > 0) {
|
||||
_storeDb();
|
||||
}
|
||||
return Future.value(modified);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List args) async {
|
||||
_runSimple(statement, args);
|
||||
|
||||
/// The row id of the last inserted row. This counter is reset when calling
|
||||
/// [export].
|
||||
int lastInsertId() {
|
||||
// load insert id. Will return [{columns: [...], values: [[id]]}]
|
||||
final results = _database
|
||||
final results = _obj
|
||||
.callMethod('exec', const ['SELECT last_insert_rowid();']) as JsArray;
|
||||
final row = results.first as JsObject;
|
||||
final data = (row['values'] as JsArray).first as JsArray;
|
||||
|
||||
await _handlePotentialUpdate();
|
||||
|
||||
return Future.value(data.first as int);
|
||||
return data.first as int;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> runSelect(
|
||||
String statement, List args) async {
|
||||
_log(statement, args);
|
||||
// todo at least for stream queries we should cache prepared statements.
|
||||
final stmt = _database.callMethod('prepare', [statement]) as JsObject;
|
||||
stmt.callMethod('bind', [args]);
|
||||
|
||||
List<String> columnNames;
|
||||
final rows = <Map<String, dynamic>>[];
|
||||
|
||||
while (stmt.callMethod('step') as bool) {
|
||||
columnNames ??=
|
||||
(stmt.callMethod('getColumnNames') as JsArray).cast<String>();
|
||||
|
||||
final row = stmt.callMethod('get') as JsArray;
|
||||
rows.add({for (var i = 0; i < row.length; i++) columnNames[i]: row[i]});
|
||||
}
|
||||
|
||||
stmt.callMethod('free');
|
||||
return rows;
|
||||
Uint8List export() {
|
||||
return _obj.callMethod('export') as Uint8List;
|
||||
}
|
||||
}
|
||||
|
||||
class PreparedStatement {
|
||||
final JsObject _obj;
|
||||
PreparedStatement._(this._obj);
|
||||
|
||||
/// Executes this statement with the bound [args].
|
||||
void executeWith(List<dynamic> args) {
|
||||
_obj.callMethod('bind', [JsArray.from(args)]);
|
||||
}
|
||||
|
||||
bool step() {
|
||||
return _obj.callMethod('step') as bool;
|
||||
}
|
||||
|
||||
List<dynamic> currentRow() {
|
||||
return _obj.callMethod('get') as JsArray;
|
||||
}
|
||||
|
||||
/// The columns returned by this statement. This will only be available after
|
||||
/// [step] has been called once.
|
||||
List<String> columnNames() {
|
||||
return (_obj.callMethod('getColumnNames') as JsArray).cast<String>();
|
||||
}
|
||||
|
||||
void free() {
|
||||
_obj.callMethod('free');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
part of 'package:moor/moor_web.dart';
|
||||
|
||||
/// Experimental moor backend for the web. To use this platform, you need to
|
||||
/// include the latest version of `sql.js` in your html.
|
||||
class WebDatabase extends QueryExecutor {
|
||||
final bool logStatements;
|
||||
final String name;
|
||||
|
||||
Completer<bool> _openingCompleter;
|
||||
SqlJsDatabase _db;
|
||||
|
||||
WebDatabase(this.name, {this.logStatements = false});
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
throw StateError(
|
||||
'Transactions are not currently supported with the sql.js backend');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen() async {
|
||||
// sync mechanism to make sure _openInternal is only called once
|
||||
if (_db != null) {
|
||||
return true;
|
||||
} else if (_openingCompleter != null) {
|
||||
return _openingCompleter.future;
|
||||
} else {
|
||||
_openingCompleter = Completer();
|
||||
await _openInternal();
|
||||
_openingCompleter.complete(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openInternal() async {
|
||||
// We don't get information about the database version from sql.js, so we
|
||||
// create another database just to manage versions.
|
||||
if (!IdbFactory.supported) {
|
||||
throw UnsupportedError("This browser doesn't support IndexedDb");
|
||||
}
|
||||
|
||||
int version;
|
||||
var upgradeNeeded = false;
|
||||
|
||||
final db = await window.indexedDB.open(
|
||||
name,
|
||||
version: databaseInfo.schemaVersion,
|
||||
onUpgradeNeeded: (event) {
|
||||
upgradeNeeded = true;
|
||||
version = event.oldVersion;
|
||||
},
|
||||
);
|
||||
db.close();
|
||||
|
||||
final module = await initSqlJs();
|
||||
final restored = _restoreDb();
|
||||
_db = module.createDatabase(restored);
|
||||
|
||||
if (upgradeNeeded) {
|
||||
if (version == null || version < 1) {
|
||||
await databaseInfo.handleDatabaseCreation(executor: _runWithoutArgs);
|
||||
} else {
|
||||
await databaseInfo.handleDatabaseVersionChange(
|
||||
executor: _runWithoutArgs,
|
||||
from: version,
|
||||
to: databaseInfo.schemaVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String get _persistenceKey => 'moor_db_str_$name';
|
||||
|
||||
// todo base64 works, but is very slow. Figure out why bin2str is broken
|
||||
|
||||
Uint8List _restoreDb() {
|
||||
final raw = window.localStorage[_persistenceKey];
|
||||
if (raw != null) {
|
||||
return base64.decode(raw);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _storeDb() {
|
||||
final data = _db.export();
|
||||
final binStr = base64.encode(data);
|
||||
window.localStorage[_persistenceKey] = binStr;
|
||||
}
|
||||
|
||||
@tryInline
|
||||
void _log(String sql, List<dynamic> variables) {
|
||||
if (logStatements) {
|
||||
print('[moor_web]: Running $sql with bound args: $variables');
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes [sql] with the bound [variables], and ignores the result.
|
||||
void _runSimple(String sql, List<dynamic> variables) {
|
||||
_log(sql, variables);
|
||||
_db.runWithArgs(sql, variables);
|
||||
}
|
||||
|
||||
Future<void> _runWithoutArgs(String query) {
|
||||
_db.run(query);
|
||||
return Future.value(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement) {
|
||||
return _runWithoutArgs(statement);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List args) {
|
||||
_runSimple(statement, args);
|
||||
return _handlePotentialUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List args) {
|
||||
_runSimple(statement, args);
|
||||
return _handlePotentialUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(List<BatchedStatement> statements) async {
|
||||
for (var stmt in statements) {
|
||||
final prepared = _db.prepare(stmt.sql);
|
||||
|
||||
for (var args in stmt.variables) {
|
||||
prepared.executeWith(args);
|
||||
}
|
||||
}
|
||||
|
||||
await _handlePotentialUpdate();
|
||||
}
|
||||
|
||||
/// Saves the database if the last statement changed rows. As a side-effect,
|
||||
/// saving the database resets the `last_insert_id` counter in sqlite.
|
||||
Future<int> _handlePotentialUpdate() {
|
||||
final modified = _db.lastModifiedRows();
|
||||
if (modified > 0) {
|
||||
_storeDb();
|
||||
}
|
||||
return Future.value(modified);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List args) async {
|
||||
_runSimple(statement, args);
|
||||
final insertId = _db.lastInsertId();
|
||||
await _handlePotentialUpdate();
|
||||
return insertId;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> runSelect(
|
||||
String statement, List args) async {
|
||||
_log(statement, args);
|
||||
// todo at least for stream queries we should cache prepared statements.
|
||||
final stmt = _db.prepare(statement)..executeWith(args);
|
||||
|
||||
List<String> columnNames;
|
||||
final rows = <Map<String, dynamic>>[];
|
||||
|
||||
while (stmt.step()) {
|
||||
columnNames ??= stmt.columnNames();
|
||||
final row = stmt.currentRow();
|
||||
|
||||
rows.add({for (var i = 0; i < row.length; i++) columnNames[i]: row[i]});
|
||||
}
|
||||
|
||||
stmt.free();
|
||||
return rows;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue