import 'dart:collection'; import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; import 'package:sqlite3/src/vfs.dart'; import '../constants.dart'; import '../functions.dart'; import '../implementation/bindings.dart'; import '../implementation/exception.dart'; import 'injected_values.dart'; import 'wasm_interop.dart' as wasm; import 'sqlite3_wasm.g.dart'; import 'wasm_interop.dart'; // ignore_for_file: non_constant_identifier_names final class WasmSqliteBindings implements RawSqliteBindings { final wasm.WasmBindings bindings; WasmSqliteBindings(this.bindings); @override String? get sqlite3_temp_directory { final charPtr = bindings.sqlite3_temp_directory; if (charPtr == 0) { return null; } else { return bindings.memory.readString(charPtr); } } @override set sqlite3_temp_directory(String? value) { if (value == null) { bindings.sqlite3_temp_directory = 0; } else { bindings.sqlite3_temp_directory = bindings.allocateZeroTerminated(value); } } @override String sqlite3_errstr(int extendedErrorCode) { return bindings.memory.readString( bindings.sqlite3_errstr(extendedErrorCode), ); } @override String sqlite3_libversion() { return bindings.memory.readString(bindings.sqlite3_libversion()); } @override int sqlite3_libversion_number() { return bindings.sqlite3_libversion_number(); } @override SqliteResult sqlite3_open_v2( String name, int flags, String? zVfs, ) { final namePtr = bindings.allocateZeroTerminated(name); final outDb = bindings.malloc(wasm.WasmBindings.pointerSize); final vfsPtr = zVfs == null ? 0 : bindings.allocateZeroTerminated(zVfs); final result = bindings.sqlite3_open_v2(namePtr, outDb, flags, vfsPtr); final dbPtr = bindings.memory.int32ValueOfPointer(outDb); // Free pointers we allocateed bindings ..free(namePtr) ..free(vfsPtr); if (zVfs != null) bindings.free(vfsPtr); return (resultCode: result, result: WasmDatabase(bindings, dbPtr)); } @override String sqlite3_sourceid() { return bindings.memory.readString(bindings.sqlite3_sourceid()); } @override int sqlite3_initialize() { return bindings.sqlite3_initialize(); } @override void registerVirtualFileSystem(VirtualFileSystem vfs, int makeDefault) { final name = bindings.allocateZeroTerminated(vfs.name); final ptr = bindings.sqlite3.dart_sqlite3_register_vfs( name, vfs.toExternalReference, makeDefault, ); if (ptr == 0) { throw StateError('could not register vfs'); } _sqliteVfsPointer[vfs] = ptr; } @override void unregisterVirtualFileSystem(VirtualFileSystem vfs) { final ptr = _sqliteVfsPointer[vfs]; if (ptr == null) { throw StateError('vfs has not been registered'); } bindings.sqlite3_vfs_unregister(ptr); } @override RawSqliteSession sqlite3session_create(RawSqliteDatabase db, String name) { final zDb = bindings.allocateZeroTerminated(name); final sessionPtr = bindings.malloc(WasmBindings.pointerSize); final result = bindings.sqlite3session_create( (db as WasmDatabase).db, zDb, sessionPtr, ); final session = bindings.memory.int32ValueOfPointer(sessionPtr); bindings ..free(zDb) ..free(sessionPtr); if (result != 0) { throw createExceptionOutsideOfDatabase(this, result); } return WasmSession(this, session); } @override int sqlite3changeset_apply( RawSqliteDatabase database, Uint8List changeset, int Function(String tableName)? filter, int Function(int eConflict, RawChangesetIterator iter) conflict, ) { final callbacks = SessionApplyCallbacks( switch (filter) { null => null, final filter => (Pointer tableName) { final table = bindings.memory.readString(tableName); return filter(table); }, }, (int eConflict, Pointer iterator) { final impl = WasmChangesetIterator(this, iterator, owned: false); return conflict(eConflict, impl); }, ); final changesetPtr = bindings.allocateBytes(changeset); final result = bindings.sqlite3.dart_sqlite3changeset_apply( (database as WasmDatabase).db, changeset.length, changesetPtr, callbacks.toExternalReference, filter != null ? 1 : 0, ); bindings.free(changesetPtr); return result; } @override Uint8List sqlite3changeset_invert(Uint8List changeset) { final originalPtr = bindings.allocateBytes(changeset); final lengthPtr = bindings.malloc(WasmBindings.pointerSize); final outPtr = bindings.malloc(WasmBindings.pointerSize); final result = bindings.sqlite3changeset_invert( changeset.length, originalPtr, lengthPtr, outPtr, ); final length = bindings.memory.int32ValueOfPointer(lengthPtr); final inverted = bindings.memory.int32ValueOfPointer(outPtr); bindings ..free(originalPtr) ..free(lengthPtr) ..free(outPtr); if (result != 0) { throw createExceptionOutsideOfDatabase(this, result); } final out = bindings.memory.copyRange(inverted, length); bindings.sqlite3_free(inverted); return out; } @override RawChangesetIterator sqlite3changeset_start(Uint8List changeset) { final changesetPtr = bindings.allocateBytes(changeset); final outPtr = bindings.malloc(WasmBindings.pointerSize); final result = bindings.sqlite3changeset_start( outPtr, changeset.length, changesetPtr, ); final iterator = bindings.memory.int32ValueOfPointer(outPtr); bindings.free(outPtr); if (result != 0) { throw createExceptionOutsideOfDatabase(this, result); } return WasmChangesetIterator(this, iterator, dataPointer: changesetPtr); } static final _sqliteVfsPointer = Expando(); } final class WasmDatabase implements RawSqliteDatabase { final wasm.WasmBindings bindings; final Pointer db; final Object detach = Object(); WasmDatabase(this.bindings, this.db) { bindings.databaseFinalizer?.attach(this, db, detach: detach); } @override int sqlite3_close_v2() { bindings.databaseFinalizer?.detach(detach); return bindings.sqlite3_close_v2(db); } @override String sqlite3_errmsg() { return bindings.memory.readString(bindings.sqlite3_errmsg(db)); } @override int sqlite3_extended_errcode() { return bindings.sqlite3_extended_errcode(db); } @override int sqlite3_error_offset() { return bindings.sqlite3_error_offset(db); } @override void sqlite3_extended_result_codes(int onoff) { bindings.sqlite3_extended_result_codes(db, onoff); } @override RawStatementCompiler newCompiler(List utf8EncodedSql) { final ptr = bindings.allocateBytes(utf8EncodedSql); return WasmStatementCompiler(this, ptr); } @override int sqlite3_changes() => bindings.sqlite3_changes(db); @override int sqlite3_create_collation_v2({ required Uint8List collationName, required int eTextRep, required RawCollation collation, }) { final ptr = bindings.allocateBytes(collationName, additionalLength: 1); final result = bindings.sqlite3.dart_sqlite3_create_collation( db, ptr, eTextRep, RegisteredFunctionSet(collation: collation).toExternalReference, ); bindings.free(ptr); return result; } @override int sqlite3_create_function_v2({ required Uint8List functionName, required int nArg, required int eTextRep, RawXFunc? xFunc, RawXStep? xStep, RawXFinal? xFinal, }) { final ptr = bindings.allocateBytes(functionName, additionalLength: 1); var isAggregate = false; RegisteredFunctionSet set; if (xFunc != null) { set = RegisteredFunctionSet(xFunc: xFunc); } else { isAggregate = true; set = RegisteredFunctionSet(xStep: xStep, xFinal: xFinal); } final result = bindings.sqlite3.dart_sqlite3_create_function_v2( db, ptr, nArg, eTextRep, isAggregate ? 1 : 0, set.toExternalReference, ); bindings.free(ptr); return result; } @override int sqlite3_create_window_function({ required Uint8List functionName, required int nArg, required int eTextRep, required RawXStep xStep, required RawXFinal xFinal, required RawXFinal xValue, required RawXStep xInverse, }) { final ptr = bindings.allocateBytes(functionName, additionalLength: 1); final result = bindings.create_window_function( db, ptr, nArg, eTextRep, RegisteredFunctionSet( xStep: xStep, xFinal: xFinal, xValue: xValue, xInverse: xInverse, ), ); bindings.free(ptr); return result; } @override int sqlite3_exec(String sql) { final stmt = bindings.allocateZeroTerminated(sql); final result = bindings.sqlite3_exec(db, stmt, 0, 0, 0); bindings.free(stmt); return result; } @override int sqlite3_last_insert_rowid() { return bindings.sqlite3_last_insert_rowid(db); } @override void sqlite3_update_hook(RawUpdateHook? hook) { bindings.dart_sqlite3_updates(db, hook); } @override void sqlite3_commit_hook(RawCommitHook? hook) { bindings.dart_sqlite3_commits(db, hook); } @override void sqlite3_rollback_hook(RawRollbackHook? hook) { bindings.dart_sqlite3_rollbacks(db, hook); } @override int sqlite3_get_autocommit() { return bindings.sqlite3_get_autocommit(db); } @override int sqlite3_db_config(int op, int value) { return bindings.sqlite3_db_config(db, op, value); } @override int sqlite3_busy_handler(int Function(int p1)? callback) { return bindings.sqlite3.dart_sqlite3_busy_handler( db, callback?.toExternalReference, ); } } final class WasmStatementCompiler implements RawStatementCompiler { final WasmDatabase database; final Pointer sql; final Pointer stmtOut; final Pointer pzTail; WasmStatementCompiler(this.database, this.sql) : stmtOut = database.bindings.malloc(WasmBindings.pointerSize), pzTail = database.bindings.malloc(WasmBindings.pointerSize); @override void close() { database.bindings ..free(sql) ..free(stmtOut) ..free(pzTail); } @override int get endOffset { final bindings = database.bindings; return bindings.memory.int32ValueOfPointer(pzTail) - sql; } @override SqliteResult sqlite3_prepare( int byteOffset, int length, int prepFlag, ) { final result = database.bindings.sqlite3_prepare_v3( database.db, sql + byteOffset, length, prepFlag, stmtOut, pzTail, ); final stmt = database.bindings.memory.int32ValueOfPointer(stmtOut); final libraryStatement = stmt == 0 ? null : WasmStatement(database, stmt); return (resultCode: result, result: libraryStatement); } } final class WasmStatement implements RawSqliteStatement { final WasmDatabase database; final Pointer stmt; final WasmBindings bindings; final Object detach = Object(); WasmStatement(this.database, this.stmt) : bindings = database.bindings { bindings.statementFinalizer?.attach(this, stmt, detach: detach); } @override int sqlite3_bind_blob64(int index, List value) { final ptr = bindings.allocateBytes(value); return bindings.sqlite3_bind_blob_finalizerFree( stmt, index, ptr, value.length, ); } @override int sqlite3_bind_double(int index, double value) { return bindings.sqlite3_bind_double(stmt, index, value); } @override int sqlite3_bind_int64(int index, int value) { return bindings.sqlite3_bind_int(stmt, index, value); } @override int sqlite3_bind_int64BigInt(int index, BigInt value) { return bindings.sqlite3_bind_int64(stmt, index, value); } @override int sqlite3_bind_null(int index) { return bindings.sqlite3_bind_null(stmt, index); } @override int sqlite3_bind_parameter_count() { return bindings.sqlite3_bind_parameter_count(stmt); } @override int sqlite3_stmt_isexplain() { return bindings.sqlite3_stmt_isexplain(stmt); } @override int sqlite3_stmt_readonly() { return bindings.sqlite3_stmt_readonly(stmt); } @override int sqlite3_bind_parameter_index(String name) { final namePtr = bindings.allocateZeroTerminated(name); final result = bindings.sqlite3_bind_parameter_index(stmt, namePtr); bindings.free(namePtr); return result; } @override int sqlite3_bind_text(int index, String value) { final encoded = utf8.encode(value); final ptr = bindings.allocateBytes(encoded); return bindings.sqlite3_bind_text_finalizerFree( stmt, index, ptr, encoded.length, ); } @override Uint8List sqlite3_column_bytes(int index) { final length = bindings.sqlite3_column_bytes(stmt, index); final ptr = bindings.sqlite3_column_blob(stmt, index); return bindings.memory.copyRange(ptr, length); } @override int sqlite3_column_count() { return bindings.sqlite3_column_count(stmt); } @override double sqlite3_column_double(int index) { return bindings.sqlite3_column_double(stmt, index); } @override int sqlite3_column_int64(int index) { final jsBigInt = bindings.sqlite3_column_int64(stmt, index); return jsBigInt.asDartInt; } @override Object sqlite3_column_int64OrBigInt(int index) { final jsBigInt = bindings.sqlite3_column_int64(stmt, index); return jsBigInt.toDart(); } @override String sqlite3_column_name(int index) { final namePtr = bindings.sqlite3_column_name(stmt, index); return bindings.memory.readString(namePtr); } @override String? sqlite3_column_table_name(int index) { return null; } @override String sqlite3_column_text(int index) { final ptr = bindings.sqlite3_column_text(stmt, index); return bindings.memory.readString(ptr); } @override int sqlite3_column_type(int index) { return bindings.sqlite3_column_type(stmt, index); } @override void sqlite3_finalize() { bindings.sqlite3_finalize(stmt); bindings.statementFinalizer?.detach(detach); } @override void sqlite3_reset() { bindings.sqlite3_reset(stmt); } @override int sqlite3_step() { return bindings.sqlite3_step(stmt); } @override bool get supportsReadingTableNameForColumn => false; } final class WasmContext implements RawSqliteContext { final WasmBindings bindings; final Pointer context; final DartBridgeCallbacks callbacks; WasmContext(this.bindings, this.context, this.callbacks); Pointer get _rawAggregateContext { final agCtxPtr = bindings.sqlite3_aggregate_context(context, 4); if (agCtxPtr == 0) { // We can't run without our 4 bytes! This indicates an out-of-memory error throw StateError( 'Internal error while allocating sqlite3 aggregate context (OOM?)', ); } return agCtxPtr; } @override AggregateContext? get dartAggregateContext { final agCtxPtr = _rawAggregateContext; final value = bindings.memory.int32ValueOfPointer(agCtxPtr); // Ok, we have a pointer (that sqlite3 zeroes out for us). Our state counter // starts at one, so if it's still zero we don't have a Dart context yet. if (value == 0) { return null; } else { return callbacks.aggregateContexts[value]; } } @override set dartAggregateContext(AggregateContext? value) { final ptr = _rawAggregateContext; final id = callbacks.aggregateContextId++; callbacks.aggregateContexts[id] = ArgumentError.checkNotNull(value); bindings.memory.setInt32Value(ptr, id); } @override void sqlite3_result_blob64(List blob) { final ptr = bindings.allocateBytes(blob); bindings.sqlite3_result_blob64( context, ptr, blob.length, SqlSpecialDestructor.SQLITE_TRANSIENT, ); bindings.free(ptr); } @override void sqlite3_result_double(double value) { return bindings.sqlite3_result_double(context, value); } @override void sqlite3_result_error(String message) { final encoded = utf8.encode(message); final ptr = bindings.allocateBytes(encoded); bindings.sqlite3_result_error(context, ptr, encoded.length); bindings.free(ptr); } @override void sqlite3_result_int64(int value) { bindings.sqlite3_result_int64(context, BigInt.from(value)); } @override void sqlite3_result_int64BigInt(BigInt value) { bindings.sqlite3_result_int64(context, value); } @override void sqlite3_result_null() { return bindings.sqlite3_result_null(context); } @override void sqlite3_result_text(String text) { final encoded = utf8.encode(text); final ptr = bindings.allocateBytes(encoded); bindings.sqlite3_result_text( context, ptr, encoded.length, SqlSpecialDestructor.SQLITE_TRANSIENT, ); bindings.free(ptr); } @override void sqlite3_result_subtype(int value) { bindings.sqlite3_result_subtype(context, value); } } final class WasmValue implements RawSqliteValue { final WasmBindings bindings; final Pointer value; WasmValue(this.bindings, this.value); @override Uint8List sqlite3_value_blob() { final length = bindings.sqlite3_value_bytes(value); return bindings.memory.copyRange( bindings.sqlite3_value_blob(value), length, ); } @override double sqlite3_value_double() { return bindings.sqlite3_value_double(value); } @override int sqlite3_value_int64() { return bindings.sqlite3_value_int64(value).asDartInt; } @override String sqlite3_value_text() { final length = bindings.sqlite3_value_bytes(value); return bindings.memory.readString( bindings.sqlite3_value_text(value), length, ); } @override int sqlite3_value_type() { return bindings.sqlite3_value_type(value); } @override int sqlite3_value_subtype() { return bindings.sqlite3_value_subtype(value); } } class WasmValueList extends ListBase { final WasmBindings bindings; @override final int length; final Pointer value; WasmValueList(this.bindings, this.length, this.value); @override set length(int value) { throw UnsupportedError('Setting length in WasmValueList'); } @override WasmValue operator [](int index) { final valuePtr = bindings.memory.int32ValueOfPointer( value + index * WasmBindings.pointerSize, ); return WasmValue(bindings, valuePtr); } @override void operator []=(int index, WasmValue value) { throw UnsupportedError('Setting element in WasmValueList'); } } final class WasmSession implements RawSqliteSession { final WasmSqliteBindings bindings; final int pointer; // the sqlite3_session ptr final Object detach = Object(); final WasmBindings _bindings; WasmSession(this.bindings, this.pointer) : _bindings = bindings.bindings { _bindings.sessionFinalizer?.attach(this, pointer, detach: detach); } @override int sqlite3session_attach([String? name]) { final zTab = name == null ? 0 : _bindings.malloc(WasmBindings.pointerSize); final resultCode = _bindings.sqlite3session_attach(pointer, zTab); if (name != null) { _bindings.free(zTab); } return resultCode; } Uint8List _extractBytes(int Function(Pointer, Pointer, Pointer) raw) { final sizePtr = _bindings.malloc(WasmBindings.pointerSize); final patchsetPtr = _bindings.malloc(WasmBindings.pointerSize); final rc = raw(pointer, sizePtr, patchsetPtr); if (rc != 0) { throw createExceptionOutsideOfDatabase(bindings, rc); } final length = _bindings.memory.int32ValueOfPointer(sizePtr); final patchset = _bindings.memory.int32ValueOfPointer(patchsetPtr); _bindings ..free(sizePtr) ..free(patchsetPtr); final bytes = _bindings.memory.copyRange(patchset, length); _bindings.sqlite3_free(patchset); return bytes; } @override Uint8List sqlite3session_changeset() { return _extractBytes(_bindings.sqlite3session_changeset); } @override Uint8List sqlite3session_patchset() { return _extractBytes(_bindings.sqlite3session_patchset); } @override void sqlite3session_delete() { _bindings.sessionFinalizer?.detach(detach); _bindings.sqlite3session_delete(pointer); } @override int sqlite3session_diff(String fromDb, String table) { final dbPtr = _bindings.allocateZeroTerminated(fromDb); final tableptr = _bindings.allocateZeroTerminated(table); final code = _bindings.sqlite3session_diff(pointer, dbPtr, tableptr, 0); _bindings ..free(dbPtr) ..free(tableptr); return code; } @override int sqlite3session_enable(int enable) { return _bindings.sqlite3session_enable(pointer, enable); } @override int sqlite3session_indirect(int indirect) { return _bindings.sqlite3session_indirect(pointer, indirect); } @override int sqlite3session_isempty() => _bindings.sqlite3session_isempty(pointer); } final class WasmChangesetIterator implements RawChangesetIterator { final WasmSqliteBindings bindings; /// If this iterator was created from an uint8list allocated when creating it, /// the pointer towards that. final int? dataPointer; final int pointer; // the sqlite3_changeset_iter ptr final Object detach = Object(); final WasmBindings _bindings; WasmChangesetIterator( this.bindings, this.pointer, { this.dataPointer, bool owned = true, }) : _bindings = bindings.bindings { if (owned) { bindings.bindings.changesetFinalizer?.attach( this, pointer, detach: detach, ); } } @override int sqlite3changeset_finalize() { final rc = _bindings.sqlite3changeset_finalize(pointer); bindings.bindings.changesetFinalizer?.detach(detach); if (dataPointer case final data?) { _bindings.free(data); } return rc; } @override int sqlite3changeset_next() => _bindings.sqlite3changeset_next(pointer); SqliteResult _extractValue( int Function(Pointer, int, Pointer) extract, int index, ) { final outValue = _bindings.malloc(WasmBindings.pointerSize); final resultCode = extract(pointer, index, outValue); final value = _bindings.memory.int32ValueOfPointer(outValue); _bindings.free(outValue); return ( resultCode: resultCode, result: value != 0 ? WasmValue(_bindings, value) : null, ); } @override SqliteResult sqlite3changeset_old(int columnNumber) { return _extractValue(_bindings.sqlite3changeset_old, columnNumber); } @override SqliteResult sqlite3changeset_new(int columnNumber) { return _extractValue(_bindings.sqlite3changeset_new, columnNumber); } @override RawChangeSetOp sqlite3changeset_op() { final outTable = _bindings.malloc(WasmBindings.pointerSize); final outColCount = _bindings.malloc(WasmBindings.pointerSize); final outOp = _bindings.malloc(WasmBindings.pointerSize); final outIndirect = _bindings.malloc(WasmBindings.pointerSize); final value = _bindings.sqlite3changeset_op( pointer, outTable, outColCount, outOp, outIndirect, ); final colCount = _bindings.memory.int32ValueOfPointer(outColCount); final op = _bindings.memory.int32ValueOfPointer(outOp); final indirect = _bindings.memory.int32ValueOfPointer(outIndirect); final rawTable = _bindings.memory.int32ValueOfPointer(outTable); final table = value == 0 ? _bindings.memory.readString(rawTable) : ''; _bindings ..free(outTable) ..free(outColCount) ..free(outOp) ..free(outIndirect); if (value != 0) { throw createExceptionOutsideOfDatabase(bindings, value); } return RawChangeSetOp( tableName: table, columnCount: colCount, operation: op, indirect: indirect, ); } }