diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..b8c1bf81 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "sqlite3_android"] + path = sqlite3_android + url = https://github.com/rodydavis/sqlite-native-libraries +[submodule "sqlite_swift_package"] + path = sqlite_swift_package + url = https://github.com/rodydavis/CSQLite diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c39a51f..2c28dd75 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "rust-analyzer.linkedProjects": [ "sqlite3/example/custom_wasm_build/Cargo.toml" + ], + "cSpell.words": [ + "Patchset" ] } diff --git a/integration_tests/flutter_libs_swiftpm/ios/Flutter/Debug.xcconfig b/integration_tests/flutter_libs_swiftpm/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/integration_tests/flutter_libs_swiftpm/ios/Flutter/Debug.xcconfig +++ b/integration_tests/flutter_libs_swiftpm/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/integration_tests/flutter_libs_swiftpm/ios/Flutter/Release.xcconfig b/integration_tests/flutter_libs_swiftpm/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/integration_tests/flutter_libs_swiftpm/ios/Flutter/Release.xcconfig +++ b/integration_tests/flutter_libs_swiftpm/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/integration_tests/flutter_libs_swiftpm/ios/Podfile b/integration_tests/flutter_libs_swiftpm/ios/Podfile new file mode 100644 index 00000000..e549ee22 --- /dev/null +++ b/integration_tests/flutter_libs_swiftpm/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Debug.xcconfig b/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b6..4b81f9b2 100644 --- a/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Debug.xcconfig +++ b/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Release.xcconfig b/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Release.xcconfig index c2efd0b6..5caa9d15 100644 --- a/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Release.xcconfig +++ b/integration_tests/flutter_libs_swiftpm/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/integration_tests/flutter_libs_swiftpm/macos/Podfile b/integration_tests/flutter_libs_swiftpm/macos/Podfile new file mode 100644 index 00000000..29c8eb32 --- /dev/null +++ b/integration_tests/flutter_libs_swiftpm/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/sqlite3/assets/sqlite3.h b/sqlite3/assets/sqlite3.h index 3fb956b8..2b5cebcd 100644 --- a/sqlite3/assets/sqlite3.h +++ b/sqlite3/assets/sqlite3.h @@ -8,10 +8,13 @@ typedef struct sqlite3 sqlite3; typedef struct sqlite3_stmt sqlite3_stmt; typedef struct sqlite3_backup sqlite3_backup; typedef struct sqlite3_api_routines sqlite3_api_routines; +typedef struct sqlite3_session sqlite3_session; +typedef struct sqlite3_changeset_iter sqlite3_changeset_iter; sqlite3_char *sqlite3_temp_directory; int sqlite3_initialize(); +void sqlite3_free(void*); int sqlite3_open_v2(sqlite3_char *filename, sqlite3 **ppDb, int flags, sqlite3_char *zVfs); @@ -216,3 +219,80 @@ struct sqlite3_vfs { }; int sqlite3_vfs_register(sqlite3_vfs *, int makeDflt); int sqlite3_vfs_unregister(sqlite3_vfs *); + +// Session + +int sqlite3session_create( + sqlite3 *db, /* Database handle */ + const sqlite3_char *zDb, /* Name of db (e.g. "main") */ + sqlite3_session **ppSession /* OUT: New session object */ +); +void sqlite3session_delete(sqlite3_session *pSession); +int sqlite3session_enable(sqlite3_session *pSession, int bEnable); +int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect); + +int sqlite3changeset_start( + sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */ + int nChangeset, /* Size of changeset blob in bytes */ + void *pChangeset /* Pointer to blob containing changeset */ +); +int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter); +int sqlite3changeset_next(sqlite3_changeset_iter *pIter); +int sqlite3changeset_op( + sqlite3_changeset_iter *pIter, /* Iterator object */ + const sqlite3_char **pzTab, /* OUT: Pointer to table name */ + int *pnCol, /* OUT: Number of columns in table */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True for an 'indirect' change */ +); +int sqlite3changeset_old( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ +); +int sqlite3changeset_new( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ +); +int sqlite3changeset_apply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +); + +int sqlite3changeset_invert( + int nIn, const void *pIn, /* Input changeset */ + int *pnOut, void **ppOut /* OUT: Inverse of input */ +); +int sqlite3session_patchset( + sqlite3_session *pSession, /* Session object */ + int *pnPatchset, /* OUT: Size of buffer at *ppPatchset */ + void **ppPatchset /* OUT: Buffer containing patchset */ +); +int sqlite3session_changeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +); +int sqlite3session_isempty(sqlite3_session *pSession); +int sqlite3session_attach( + sqlite3_session *pSession, /* Session object */ + const sqlite3_char *zTab /* Table name */ +); +int sqlite3session_diff( + sqlite3_session *pSession, + const sqlite3_char *zFromDb, + const sqlite3_char *zTbl, + sqlite3_char **pzErrMsg +); diff --git a/sqlite3/assets/wasm/CMakeLists.txt b/sqlite3/assets/wasm/CMakeLists.txt index 3a27e0ce..a712145d 100644 --- a/sqlite3/assets/wasm/CMakeLists.txt +++ b/sqlite3/assets/wasm/CMakeLists.txt @@ -12,7 +12,7 @@ include(FetchContent) FetchContent_Declare( sqlite3 # NOTE: When changing this, also update `test/wasm/sqlite3_test.dart` - URL https://sqlite.org/2025/sqlite-autoconf-3490100.tar.gz + URL https://sqlite.org/2025/sqlite-amalgamation-3490100.zip DOWNLOAD_EXTRACT_TIMESTAMP NEW ) @@ -80,6 +80,8 @@ macro(base_sqlite3_target name debug crypto) ${flags} -o ${clang_output} -I ${PROJECT_SOURCE_DIR} -I ${sqlite3_SOURCE_DIR} + -DSQLITE_ENABLE_SESSION + -DSQLITE_ENABLE_PREUPDATE_HOOK -D_HAVE_SQLITE_CONFIG_H -D__WASM__ -mcpu=generic diff --git a/sqlite3/assets/wasm/bridge.h b/sqlite3/assets/wasm/bridge.h index b38e6128..c54f85d6 100644 --- a/sqlite3/assets/wasm/bridge.h +++ b/sqlite3/assets/wasm/bridge.h @@ -64,3 +64,12 @@ import_dart("function_compare") extern int dartXCompare(void *id, int lengthA, import_dart("localtime") extern int dartLocalTime(int64_t time, struct tm *result); +import_dart("changeset_apply_filter") extern int dartChangesetApplyFilter( + void* context, + const char *zTab +); +import_dart("changeset_apply_conflict") extern int dartChangesetApplyConflict( + void* context, + int eConflict, + sqlite3_changeset_iter *p +); diff --git a/sqlite3/assets/wasm/helpers.c b/sqlite3/assets/wasm/helpers.c index cd8fc6f7..a0f6d2cc 100644 --- a/sqlite3/assets/wasm/helpers.c +++ b/sqlite3/assets/wasm/helpers.c @@ -244,3 +244,14 @@ SQLITE_API int dart_sqlite3_create_collation(sqlite3 *db, const char *zName, SQLITE_API int dart_sqlite3_db_config_int(sqlite3 *db, int op, int arg) { return sqlite3_db_config(db, op, arg); } + +SQLITE_API int dart_sqlite3changeset_apply(sqlite3 *db, int nChangeset, void *pChangeset, void *pCtx, bool filter) { + return sqlite3changeset_apply( + db, + nChangeset, + pChangeset, + filter ? &dartChangesetApplyFilter : 0, + &dartChangesetApplyConflict, + pCtx + ); +} \ No newline at end of file diff --git a/sqlite3/assets/wasm/sqlite_cfg.h b/sqlite3/assets/wasm/sqlite_cfg.h index 74e8dd7d..4295a1fa 100644 --- a/sqlite3/assets/wasm/sqlite_cfg.h +++ b/sqlite3/assets/wasm/sqlite_cfg.h @@ -38,6 +38,8 @@ #define SQLITE_ENABLE_FTS5 1 #define SQLITE_ENABLE_MATH_FUNCTIONS 1 #define SQLITE_ENABLE_RTREE 1 +#define SQLITE_ENABLE_SESSION 1 +#define SQLITE_ENABLE_PREUPDATE_HOOK 1 // Disable things we don't need #define SQLITE_OMIT_DEPRECATED diff --git a/sqlite3/lib/common.dart b/sqlite3/lib/common.dart index 8806c869..6ee51248 100644 --- a/sqlite3/lib/common.dart +++ b/sqlite3/lib/common.dart @@ -8,6 +8,7 @@ library; export 'src/constants.dart'; export 'src/database.dart'; +export 'src/session.dart'; export 'src/exception.dart'; export 'src/functions.dart'; export 'src/in_memory_vfs.dart' show InMemoryFileSystem; diff --git a/sqlite3/lib/src/database.dart b/sqlite3/lib/src/database.dart index 6bf452ff..8a4ed87b 100644 --- a/sqlite3/lib/src/database.dart +++ b/sqlite3/lib/src/database.dart @@ -256,13 +256,28 @@ enum SqliteUpdateKind { // used in the sqlite3_web protocol. /// Notification for a new row being inserted into the database. - insert, + insert(SQLITE_INSERT), /// Notification for a row being updated. - update, + update(SQLITE_UPDATE), /// Notification for a row being deleted. - delete + delete(SQLITE_DELETE); + + /// The raw code to identify this update kind. + final int code; + + const SqliteUpdateKind(this.code); + + /// Attempts to extract a [SqliteUpdateKind] from the raw [flags]. + static SqliteUpdateKind? fromCode(int code) { + return switch (code) { + SQLITE_INSERT => insert, + SQLITE_UPDATE => update, + SQLITE_DELETE => delete, + _ => null, + }; + } } /// A data change notification from sqlite. diff --git a/sqlite3/lib/src/ffi/bindings.dart b/sqlite3/lib/src/ffi/bindings.dart index a599de47..6408edfa 100644 --- a/sqlite3/lib/src/ffi/bindings.dart +++ b/sqlite3/lib/src/ffi/bindings.dart @@ -5,12 +5,13 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart' as ffi; import 'package:meta/meta.dart'; -import 'package:sqlite3/src/vfs.dart'; import '../constants.dart'; import '../exception.dart'; import '../functions.dart'; import '../implementation/bindings.dart'; +import '../implementation/exception.dart'; +import '../vfs.dart'; import 'generated/dynamic_library.dart'; import 'generated/native_library.dart' as native; import 'memory.dart'; @@ -27,6 +28,8 @@ class BindingsWithLibrary { final SqliteLibrary bindings; final DynamicLibrary? library; + final NativeFinalizer sessionDelete; + final NativeFinalizer changesetIteratorFinalize; final bool supportsPrepareV3; final bool supportsErrorOffset; @@ -79,7 +82,11 @@ class BindingsWithLibrary { : supportsErrorOffset = bindings.sqlite3_libversion_number() >= _firstVersionForErrorOffset, supportsPrepareV3 = - bindings.sqlite3_libversion_number() >= _firstVersionForV3; + bindings.sqlite3_libversion_number() >= _firstVersionForV3, + sessionDelete = + NativeFinalizer(bindings.addresses.sqlite3session_delete.cast()), + changesetIteratorFinalize = NativeFinalizer( + bindings.addresses.sqlite3changeset_finalize.cast()); } final class FfiBindings extends RawSqliteBindings { @@ -88,6 +95,130 @@ final class FfiBindings extends RawSqliteBindings { FfiBindings(this.bindings); + @override + RawSqliteSession sqlite3session_create( + RawSqliteDatabase db, + String name, + ) { + final dbImpl = db as FfiDatabase; + final namePtr = Utf8Utils.allocateZeroTerminated(name); + final sessionPtr = allocate>(); + final result = bindings.bindings.sqlite3session_create( + dbImpl.db, + namePtr, + sessionPtr, + ); + namePtr.free(); + final sessionValue = sessionPtr.value; + sessionPtr.free(); + + if (result != 0) { + throw createExceptionOutsideOfDatabase(this, result); + } + + return FfiSession(this, sessionValue); + } + + @override + int sqlite3changeset_apply( + RawSqliteDatabase database, + Uint8List changeset, + int Function( + String tableName, + )? filter, + int Function( + int eConflict, + RawChangesetIterator iter, + ) conflict, + ) { + final dbImpl = database as FfiDatabase; + final changesetPtr = allocateBytes(changeset); + final ctxPtr = dbImpl.db.cast(); + + final NativeCallable, Pointer)>? + filterImpl = filter == null + ? null + : (NativeCallable.isolateLocal( + (Pointer ctx, Pointer zTab) { + final tbl = zTab.cast().readString(); + return filter(tbl); + }, + exceptionalReturn: 1, + )..keepIsolateAlive = true); + + final NativeCallable< + Int Function(Pointer, Int, Pointer)> + conflictImpl = (NativeCallable.isolateLocal( + (Pointer ctx, int eConflict, Pointer p) { + final iter = FfiChangesetIterator(this, p, owned: false); + return conflict(eConflict, iter); + }, + exceptionalReturn: 1, + )..keepIsolateAlive = true); + + final result = bindings.bindings.sqlite3changeset_apply( + dbImpl.db, + changeset.length, + changesetPtr.cast(), + filterImpl?.nativeFunction ?? nullPtr(), + conflictImpl.nativeFunction, + ctxPtr, + ); + changesetPtr.free(); + filterImpl?.close(); + conflictImpl.close(); + + return result; + } + + @override + RawChangesetIterator sqlite3changeset_start(Uint8List changeset) { + final asPtr = allocateBytes(changeset); + final iteratorOut = allocate>(); + + final result = bindings.bindings + .sqlite3changeset_start(iteratorOut, changeset.length, asPtr.cast()); + final iterator = iteratorOut.value; + iteratorOut.free(); + + if (result != SqlError.SQLITE_OK) { + asPtr.free(); + throw createExceptionOutsideOfDatabase(this, result); + } + + // TODO: Deallocate asPtr when deallocated + return FfiChangesetIterator(this, iterator); + } + + @override + Uint8List sqlite3changeset_invert(Uint8List changeset) { + final sessionPtr = allocateBytes(changeset).cast(); + final outSize = allocate(); + final outChangeset = allocate>(); + + try { + final result = bindings.bindings.sqlite3changeset_invert( + changeset.length, + sessionPtr, + outSize, + outChangeset, + ); + + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(this, result); + } + + final size = outSize.value; + final inverted = outChangeset.value.cast().asTypedList(size, + finalizer: bindings.bindings.addresses.sqlite3_free.cast()); + return inverted; + } finally { + sessionPtr.free(); + outSize.free(); + outChangeset.free(); + } + } + @override String? get sqlite3_temp_directory { return bindings.bindings.sqlite3_temp_directory.readNullableString(); @@ -429,6 +560,211 @@ final class _DartFile extends Struct { external int dartFileId; } +final class FfiSession extends RawSqliteSession implements Finalizable { + final FfiBindings bindings; + final Pointer session; + final Object detachToken = Object(); + + SqliteLibrary get _library => bindings.bindings.bindings; + + FfiSession(this.bindings, this.session) { + bindings.bindings.sessionDelete + .attach(this, session.cast(), detach: detachToken); + } + + @override + int sqlite3session_attach([String? name]) { + final namePtr = name == null + ? nullPtr() + : Utf8Utils.allocateZeroTerminated(name); + final result = _library.sqlite3session_attach( + session, + namePtr, + ); + if (name != null) { + namePtr.free(); + } + return result; + } + + Uint8List _handleChangesetResult(int result, int size, Pointer buffer) { + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, result); + } + + return buffer + .cast() + .asTypedList(size, finalizer: _library.addresses.sqlite3_free); + } + + @override + Uint8List sqlite3session_changeset() { + final outSize = allocate(); + final outChangeset = allocate>(); + final result = _library.sqlite3session_changeset( + session, + outSize, + outChangeset, + ); + + final size = outSize.value; + final changeset = outChangeset.value; + outSize.free(); + outChangeset.free(); + + return _handleChangesetResult(result, size, changeset); + } + + @override + Uint8List sqlite3session_patchset() { + final outSize = allocate(); + final outPatchset = allocate>(); + final result = _library.sqlite3session_patchset( + session, + outSize, + outPatchset, + ); + + final size = outSize.value; + final patchset = outPatchset.value; + outSize.free(); + outPatchset.free(); + + return _handleChangesetResult(result, size, patchset); + } + + @override + void sqlite3session_delete() { + bindings.bindings.sessionDelete.detach(detachToken); + _library.sqlite3session_delete(session); + } + + @override + int sqlite3session_diff(String fromDb, String table) { + final fromDbPtr = Utf8Utils.allocateZeroTerminated(fromDb); + final tablePtr = Utf8Utils.allocateZeroTerminated(table); + final result = _library.sqlite3session_diff( + session, + fromDbPtr, + tablePtr, + nullPtr(), + ); + fromDbPtr.free(); + tablePtr.free(); + return result; + } + + @override + int sqlite3session_enable(int enable) { + return _library.sqlite3session_enable(session, enable); + } + + @override + int sqlite3session_indirect(int indirect) { + return _library.sqlite3session_indirect(session, indirect); + } + + @override + int sqlite3session_isempty() { + return _library.sqlite3session_isempty(session); + } +} + +final class FfiChangesetIterator extends RawChangesetIterator + implements Finalizable { + final FfiBindings bindings; + + final Pointer iterator; + final bool owned; + final Object detachToken = Object(); + + SqliteLibrary get _library => bindings.bindings.bindings; + + FfiChangesetIterator(this.bindings, this.iterator, {this.owned = true}) { + if (owned) { + bindings.bindings.changesetIteratorFinalize + .attach(this, iterator.cast(), detach: detachToken); + } + } + + @override + int sqlite3changeset_finalize() { + bindings.bindings.changesetIteratorFinalize.detach(detachToken); + final result = _library.sqlite3changeset_finalize(iterator); + return result; + } + + @override + SqliteResult sqlite3changeset_new(int columnNumber) { + final outValue = allocate>(); + final result = _library.sqlite3changeset_new( + iterator, + columnNumber, + outValue, + ); + final value = outValue.value; + outValue.free(); + + return SqliteResult(result, FfiValue(_library, value)); + } + + @override + int sqlite3changeset_next() { + return _library.sqlite3changeset_next(iterator); + } + + @override + SqliteResult sqlite3changeset_old(int columnNumber) { + final outValue = allocate>(); + final result = _library.sqlite3changeset_old( + iterator, + columnNumber, + outValue, + ); + final value = outValue.value; + outValue.free(); + + return SqliteResult(result, FfiValue(_library, value)); + } + + @override + RawChangeSetOp sqlite3changeset_op() { + final tablePtr = allocate>(); + final columnCountPtr = allocate(); + final typePtr = allocate(); + final indirectPtr = allocate(); + + final result = _library.sqlite3changeset_op( + iterator, + tablePtr, + columnCountPtr, + typePtr, + indirectPtr, + ); + + final tableValue = tablePtr.value; + final columnCountValue = columnCountPtr.value; + final typeValue = typePtr.value; + final indirectValue = indirectPtr.value; + tablePtr.free(); + columnCountPtr.free(); + typePtr.free(); + indirectPtr.free(); + + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, result); + } + + final table = tableValue.readString(); + return RawChangeSetOp( + tableName: table, + columnCount: columnCountValue, + operation: typeValue, + indirect: indirectValue, + ); + } +} + final class FfiDatabase extends RawSqliteDatabase { final BindingsWithLibrary bindings; final Pointer db; diff --git a/sqlite3/lib/src/ffi/generated/dynamic_library.dart b/sqlite3/lib/src/ffi/generated/dynamic_library.dart index 52c99d60..3178f415 100644 --- a/sqlite3/lib/src/ffi/generated/dynamic_library.dart +++ b/sqlite3/lib/src/ffi/generated/dynamic_library.dart @@ -39,6 +39,20 @@ final class NativeLibrary implements imp$1.SqliteLibrary { late final _sqlite3_initialize = _sqlite3_initializePtr.asFunction(); + void sqlite3_free( + ffi.Pointer arg0, + ) { + return _sqlite3_free( + arg0, + ); + } + + late final _sqlite3_freePtr = + _lookup)>>( + 'sqlite3_free'); + late final _sqlite3_free = + _sqlite3_freePtr.asFunction)>(); + int sqlite3_open_v2( ffi.Pointer filename, ffi.Pointer> ppDb, @@ -188,20 +202,6 @@ final class NativeLibrary implements imp$1.SqliteLibrary { late final _sqlite3_error_offset = _sqlite3_error_offsetPtr .asFunction)>(); - void sqlite3_free( - ffi.Pointer ptr, - ) { - return _sqlite3_free( - ptr, - ); - } - - late final _sqlite3_freePtr = - _lookup)>>( - 'sqlite3_free'); - late final _sqlite3_free = - _sqlite3_freePtr.asFunction)>(); - ffi.Pointer sqlite3_libversion() { return _sqlite3_libversion(); } @@ -1516,4 +1516,408 @@ final class NativeLibrary implements imp$1.SqliteLibrary { 'sqlite3_vfs_unregister'); late final _sqlite3_vfs_unregister = _sqlite3_vfs_unregisterPtr .asFunction)>(); + + int sqlite3session_create( + ffi.Pointer db, + ffi.Pointer zDb, + ffi.Pointer> ppSession, + ) { + return _sqlite3session_create( + db, + zDb, + ppSession, + ); + } + + late final _sqlite3session_createPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>>( + 'sqlite3session_create'); + late final _sqlite3session_create = _sqlite3session_createPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>(); + + void sqlite3session_delete( + ffi.Pointer pSession, + ) { + return _sqlite3session_delete( + pSession, + ); + } + + late final _sqlite3session_deletePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer)>>('sqlite3session_delete'); + late final _sqlite3session_delete = _sqlite3session_deletePtr + .asFunction)>(); + + int sqlite3session_enable( + ffi.Pointer pSession, + int bEnable, + ) { + return _sqlite3session_enable( + pSession, + bEnable, + ); + } + + late final _sqlite3session_enablePtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Int)>>('sqlite3session_enable'); + late final _sqlite3session_enable = _sqlite3session_enablePtr + .asFunction, int)>(); + + int sqlite3session_indirect( + ffi.Pointer pSession, + int bIndirect, + ) { + return _sqlite3session_indirect( + pSession, + bIndirect, + ); + } + + late final _sqlite3session_indirectPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Int)>>('sqlite3session_indirect'); + late final _sqlite3session_indirect = _sqlite3session_indirectPtr + .asFunction, int)>(); + + int sqlite3changeset_start( + ffi.Pointer> pp, + int nChangeset, + ffi.Pointer pChangeset, + ) { + return _sqlite3changeset_start( + pp, + nChangeset, + pChangeset, + ); + } + + late final _sqlite3changeset_startPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer>, + ffi.Int, + ffi.Pointer)>>('sqlite3changeset_start'); + late final _sqlite3changeset_start = _sqlite3changeset_startPtr.asFunction< + int Function(ffi.Pointer>, int, + ffi.Pointer)>(); + + int sqlite3changeset_finalize( + ffi.Pointer pIter, + ) { + return _sqlite3changeset_finalize( + pIter, + ); + } + + late final _sqlite3changeset_finalizePtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer)>>( + 'sqlite3changeset_finalize'); + late final _sqlite3changeset_finalize = _sqlite3changeset_finalizePtr + .asFunction)>(); + + int sqlite3changeset_next( + ffi.Pointer pIter, + ) { + return _sqlite3changeset_next( + pIter, + ); + } + + late final _sqlite3changeset_nextPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer)>>( + 'sqlite3changeset_next'); + late final _sqlite3changeset_next = _sqlite3changeset_nextPtr + .asFunction)>(); + + int sqlite3changeset_op( + ffi.Pointer pIter, + ffi.Pointer> pzTab, + ffi.Pointer pnCol, + ffi.Pointer pOp, + ffi.Pointer pbIndirect, + ) { + return _sqlite3changeset_op( + pIter, + pzTab, + pnCol, + pOp, + pbIndirect, + ); + } + + late final _sqlite3changeset_opPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer>, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('sqlite3changeset_op'); + late final _sqlite3changeset_op = _sqlite3changeset_opPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer>, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>(); + + int sqlite3changeset_old( + ffi.Pointer pIter, + int iVal, + ffi.Pointer> ppValue, + ) { + return _sqlite3changeset_old( + pIter, + iVal, + ppValue, + ); + } + + late final _sqlite3changeset_oldPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Int, ffi.Pointer>)>>( + 'sqlite3changeset_old'); + late final _sqlite3changeset_old = _sqlite3changeset_oldPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer>)>(); + + int sqlite3changeset_new( + ffi.Pointer pIter, + int iVal, + ffi.Pointer> ppValue, + ) { + return _sqlite3changeset_new( + pIter, + iVal, + ppValue, + ); + } + + late final _sqlite3changeset_newPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Int, ffi.Pointer>)>>( + 'sqlite3changeset_new'); + late final _sqlite3changeset_new = _sqlite3changeset_newPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer>)>(); + + int sqlite3changeset_apply( + ffi.Pointer db, + int nChangeset, + ffi.Pointer pChangeset, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, ffi.Pointer zTab)>> + xFilter, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer pCtx, ffi.Int eConflict, + ffi.Pointer p)>> + xConflict, + ffi.Pointer pCtx, + ) { + return _sqlite3changeset_apply( + db, + nChangeset, + pChangeset, + xFilter, + xConflict, + pCtx, + ); + } + + late final _sqlite3changeset_applyPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer pCtx, + ffi.Pointer zTab)>>, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, + ffi.Int eConflict, + ffi.Pointer p)>>, + ffi.Pointer)>>('sqlite3changeset_apply'); + late final _sqlite3changeset_apply = _sqlite3changeset_applyPtr.asFunction< + int Function( + ffi.Pointer, + int, + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, ffi.Pointer zTab)>>, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, + ffi.Int eConflict, + ffi.Pointer p)>>, + ffi.Pointer)>(); + + int sqlite3changeset_invert( + int nIn, + ffi.Pointer pIn, + ffi.Pointer pnOut, + ffi.Pointer> ppOut, + ) { + return _sqlite3changeset_invert( + nIn, + pIn, + pnOut, + ppOut, + ); + } + + late final _sqlite3changeset_invertPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Int, ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>>('sqlite3changeset_invert'); + late final _sqlite3changeset_invert = _sqlite3changeset_invertPtr.asFunction< + int Function(int, ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>(); + + int sqlite3session_patchset( + ffi.Pointer pSession, + ffi.Pointer pnPatchset, + ffi.Pointer> ppPatchset, + ) { + return _sqlite3session_patchset( + pSession, + pnPatchset, + ppPatchset, + ); + } + + late final _sqlite3session_patchsetPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>>('sqlite3session_patchset'); + late final _sqlite3session_patchset = _sqlite3session_patchsetPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>(); + + int sqlite3session_changeset( + ffi.Pointer pSession, + ffi.Pointer pnChangeset, + ffi.Pointer> ppChangeset, + ) { + return _sqlite3session_changeset( + pSession, + pnChangeset, + ppChangeset, + ); + } + + late final _sqlite3session_changesetPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>>('sqlite3session_changeset'); + late final _sqlite3session_changeset = + _sqlite3session_changesetPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>(); + + int sqlite3session_isempty( + ffi.Pointer pSession, + ) { + return _sqlite3session_isempty( + pSession, + ); + } + + late final _sqlite3session_isemptyPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer)>>('sqlite3session_isempty'); + late final _sqlite3session_isempty = _sqlite3session_isemptyPtr + .asFunction)>(); + + int sqlite3session_attach( + ffi.Pointer pSession, + ffi.Pointer zTab, + ) { + return _sqlite3session_attach( + pSession, + zTab, + ); + } + + late final _sqlite3session_attachPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Pointer)>>('sqlite3session_attach'); + late final _sqlite3session_attach = _sqlite3session_attachPtr.asFunction< + int Function(ffi.Pointer, + ffi.Pointer)>(); + + int sqlite3session_diff( + ffi.Pointer pSession, + ffi.Pointer zFromDb, + ffi.Pointer zTbl, + ffi.Pointer> pzErrMsg, + ) { + return _sqlite3session_diff( + pSession, + zFromDb, + zTbl, + pzErrMsg, + ); + } + + late final _sqlite3session_diffPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>>( + 'sqlite3session_diff'); + late final _sqlite3session_diff = _sqlite3session_diffPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>(); + + late final addresses = _SymbolAddresses(this); +} + +final class _SymbolAddresses implements imp$1.SharedSymbolAddresses { + final NativeLibrary _library; + _SymbolAddresses(this._library); + ffi.Pointer)>> + get sqlite3_free => _library._sqlite3_freePtr; + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer)>> + get sqlite3session_delete => _library._sqlite3session_deletePtr; + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer)>> + get sqlite3changeset_finalize => _library._sqlite3changeset_finalizePtr; } diff --git a/sqlite3/lib/src/ffi/generated/native.dart b/sqlite3/lib/src/ffi/generated/native.dart index bb1543e7..9d4fc5aa 100644 --- a/sqlite3/lib/src/ffi/generated/native.dart +++ b/sqlite3/lib/src/ffi/generated/native.dart @@ -7,6 +7,7 @@ library; import 'dart:ffi' as ffi; import 'shared.dart' as imp$1; +import '' as self; @ffi.Native>() external ffi.Pointer sqlite3_temp_directory; @@ -14,6 +15,11 @@ external ffi.Pointer sqlite3_temp_directory; @ffi.Native() external int sqlite3_initialize(); +@ffi.Native)>() +external void sqlite3_free( + ffi.Pointer arg0, +); + @ffi.Native< ffi.Int Function( ffi.Pointer, @@ -72,11 +78,6 @@ external int sqlite3_error_offset( ffi.Pointer db, ); -@ffi.Native)>() -external void sqlite3_free( - ffi.Pointer ptr, -); - @ffi.Native Function()>() external ffi.Pointer sqlite3_libversion(); @@ -619,3 +620,186 @@ external int sqlite3_vfs_register( external int sqlite3_vfs_unregister( ffi.Pointer arg0, ); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>() +external int sqlite3session_create( + ffi.Pointer db, + ffi.Pointer zDb, + ffi.Pointer> ppSession, +); + +@ffi.Native)>() +external void sqlite3session_delete( + ffi.Pointer pSession, +); + +@ffi.Native, ffi.Int)>() +external int sqlite3session_enable( + ffi.Pointer pSession, + int bEnable, +); + +@ffi.Native, ffi.Int)>() +external int sqlite3session_indirect( + ffi.Pointer pSession, + int bIndirect, +); + +@ffi.Native< + ffi.Int Function(ffi.Pointer>, + ffi.Int, ffi.Pointer)>() +external int sqlite3changeset_start( + ffi.Pointer> pp, + int nChangeset, + ffi.Pointer pChangeset, +); + +@ffi.Native)>() +external int sqlite3changeset_finalize( + ffi.Pointer pIter, +); + +@ffi.Native)>() +external int sqlite3changeset_next( + ffi.Pointer pIter, +); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer>, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>() +external int sqlite3changeset_op( + ffi.Pointer pIter, + ffi.Pointer> pzTab, + ffi.Pointer pnCol, + ffi.Pointer pOp, + ffi.Pointer pbIndirect, +); + +@ffi.Native< + ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Pointer>)>() +external int sqlite3changeset_old( + ffi.Pointer pIter, + int iVal, + ffi.Pointer> ppValue, +); + +@ffi.Native< + ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Pointer>)>() +external int sqlite3changeset_new( + ffi.Pointer pIter, + int iVal, + ffi.Pointer> ppValue, +); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, ffi.Pointer zTab)>>, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer pCtx, ffi.Int eConflict, + ffi.Pointer p)>>, + ffi.Pointer)>() +external int sqlite3changeset_apply( + ffi.Pointer db, + int nChangeset, + ffi.Pointer pChangeset, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, ffi.Pointer zTab)>> + xFilter, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer pCtx, ffi.Int eConflict, + ffi.Pointer p)>> + xConflict, + ffi.Pointer pCtx, +); + +@ffi.Native< + ffi.Int Function(ffi.Int, ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>() +external int sqlite3changeset_invert( + int nIn, + ffi.Pointer pIn, + ffi.Pointer pnOut, + ffi.Pointer> ppOut, +); + +@ffi.Native< + ffi.Int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>() +external int sqlite3session_patchset( + ffi.Pointer pSession, + ffi.Pointer pnPatchset, + ffi.Pointer> ppPatchset, +); + +@ffi.Native< + ffi.Int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer>)>() +external int sqlite3session_changeset( + ffi.Pointer pSession, + ffi.Pointer pnChangeset, + ffi.Pointer> ppChangeset, +); + +@ffi.Native)>() +external int sqlite3session_isempty( + ffi.Pointer pSession, +); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, ffi.Pointer)>() +external int sqlite3session_attach( + ffi.Pointer pSession, + ffi.Pointer zTab, +); + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>() +external int sqlite3session_diff( + ffi.Pointer pSession, + ffi.Pointer zFromDb, + ffi.Pointer zTbl, + ffi.Pointer> pzErrMsg, +); + +const addresses = _SymbolAddresses(); + +final class _SymbolAddresses implements imp$1.SharedSymbolAddresses { + const _SymbolAddresses(); + ffi.Pointer)>> + get sqlite3_free => ffi.Native.addressOf(self.sqlite3_free); + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer)>> + get sqlite3session_delete => + ffi.Native.addressOf(self.sqlite3session_delete); + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer)>> + get sqlite3changeset_finalize => + ffi.Native.addressOf(self.sqlite3changeset_finalize); +} diff --git a/sqlite3/lib/src/ffi/generated/native_library.dart b/sqlite3/lib/src/ffi/generated/native_library.dart index e92c932f..9e4d33f3 100644 --- a/sqlite3/lib/src/ffi/generated/native_library.dart +++ b/sqlite3/lib/src/ffi/generated/native_library.dart @@ -5,6 +5,8 @@ import 'native.dart' as native; import 'shared.dart'; final class NativeAssetsLibrary implements SqliteLibrary { + @override + SharedSymbolAddresses get addresses => native.addresses; @override ffi.Pointer get sqlite3_temp_directory { return native.sqlite3_temp_directory; @@ -20,6 +22,11 @@ final class NativeAssetsLibrary implements SqliteLibrary { return native.sqlite3_initialize(); } + @override + void sqlite3_free(ffi.Pointer arg0) { + return native.sqlite3_free(arg0); + } + @override int sqlite3_open_v2( ffi.Pointer filename, @@ -70,11 +77,6 @@ final class NativeAssetsLibrary implements SqliteLibrary { return native.sqlite3_error_offset(db); } - @override - void sqlite3_free(ffi.Pointer ptr) { - return native.sqlite3_free(ptr); - } - @override ffi.Pointer sqlite3_libversion() { return native.sqlite3_libversion(); @@ -497,4 +499,133 @@ final class NativeAssetsLibrary implements SqliteLibrary { int sqlite3_vfs_unregister(ffi.Pointer arg0) { return native.sqlite3_vfs_unregister(arg0); } + + @override + int sqlite3session_create( + ffi.Pointer db, + ffi.Pointer zDb, + ffi.Pointer> ppSession) { + return native.sqlite3session_create(db, zDb, ppSession); + } + + @override + void sqlite3session_delete(ffi.Pointer pSession) { + return native.sqlite3session_delete(pSession); + } + + @override + int sqlite3session_enable( + ffi.Pointer pSession, int bEnable) { + return native.sqlite3session_enable(pSession, bEnable); + } + + @override + int sqlite3session_indirect( + ffi.Pointer pSession, int bIndirect) { + return native.sqlite3session_indirect(pSession, bIndirect); + } + + @override + int sqlite3changeset_start( + ffi.Pointer> pp, + int nChangeset, + ffi.Pointer pChangeset) { + return native.sqlite3changeset_start(pp, nChangeset, pChangeset); + } + + @override + int sqlite3changeset_finalize(ffi.Pointer pIter) { + return native.sqlite3changeset_finalize(pIter); + } + + @override + int sqlite3changeset_next(ffi.Pointer pIter) { + return native.sqlite3changeset_next(pIter); + } + + @override + int sqlite3changeset_op( + ffi.Pointer pIter, + ffi.Pointer> pzTab, + ffi.Pointer pnCol, + ffi.Pointer pOp, + ffi.Pointer pbIndirect) { + return native.sqlite3changeset_op(pIter, pzTab, pnCol, pOp, pbIndirect); + } + + @override + int sqlite3changeset_old(ffi.Pointer pIter, int iVal, + ffi.Pointer> ppValue) { + return native.sqlite3changeset_old(pIter, iVal, ppValue); + } + + @override + int sqlite3changeset_new(ffi.Pointer pIter, int iVal, + ffi.Pointer> ppValue) { + return native.sqlite3changeset_new(pIter, iVal, ppValue); + } + + @override + int sqlite3changeset_apply( + ffi.Pointer db, + int nChangeset, + ffi.Pointer pChangeset, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, ffi.Pointer zTab)>> + xFilter, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, + ffi.Int eConflict, + ffi.Pointer p)>> + xConflict, + ffi.Pointer pCtx) { + return native.sqlite3changeset_apply( + db, nChangeset, pChangeset, xFilter, xConflict, pCtx); + } + + @override + int sqlite3changeset_invert(int nIn, ffi.Pointer pIn, + ffi.Pointer pnOut, ffi.Pointer> ppOut) { + return native.sqlite3changeset_invert(nIn, pIn, pnOut, ppOut); + } + + @override + int sqlite3session_patchset( + ffi.Pointer pSession, + ffi.Pointer pnPatchset, + ffi.Pointer> ppPatchset) { + return native.sqlite3session_patchset(pSession, pnPatchset, ppPatchset); + } + + @override + int sqlite3session_changeset( + ffi.Pointer pSession, + ffi.Pointer pnChangeset, + ffi.Pointer> ppChangeset) { + return native.sqlite3session_changeset(pSession, pnChangeset, ppChangeset); + } + + @override + int sqlite3session_isempty(ffi.Pointer pSession) { + return native.sqlite3session_isempty(pSession); + } + + @override + int sqlite3session_attach( + ffi.Pointer pSession, ffi.Pointer zTab) { + return native.sqlite3session_attach(pSession, zTab); + } + + @override + int sqlite3session_diff( + ffi.Pointer pSession, + ffi.Pointer zFromDb, + ffi.Pointer zTbl, + ffi.Pointer> pzErrMsg) { + return native.sqlite3session_diff(pSession, zFromDb, zTbl, pzErrMsg); + } } diff --git a/sqlite3/lib/src/ffi/generated/shared.dart b/sqlite3/lib/src/ffi/generated/shared.dart index 97255f64..d5310a9a 100644 --- a/sqlite3/lib/src/ffi/generated/shared.dart +++ b/sqlite3/lib/src/ffi/generated/shared.dart @@ -14,6 +14,10 @@ final class sqlite3_backup extends ffi.Opaque {} final class sqlite3_api_routines extends ffi.Opaque {} +final class sqlite3_session extends ffi.Opaque {} + +final class sqlite3_changeset_iter extends ffi.Opaque {} + final class sqlite3_value extends ffi.Opaque {} final class sqlite3_context extends ffi.Opaque {} @@ -221,10 +225,24 @@ final class sqlite3_vfs extends ffi.Struct { xNextSystemCall; } +abstract interface class SharedSymbolAddresses { + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer)>> + get sqlite3changeset_finalize; + ffi.Pointer< + ffi.NativeFunction)>> + get sqlite3session_delete; + ffi.Pointer)>> + get sqlite3_free; +} + abstract interface class SqliteLibrary { + SharedSymbolAddresses get addresses; ffi.Pointer get sqlite3_temp_directory; set sqlite3_temp_directory(ffi.Pointer value); int sqlite3_initialize(); + void sqlite3_free(ffi.Pointer arg0); int sqlite3_open_v2( ffi.Pointer filename, ffi.Pointer> ppDb, @@ -239,7 +257,6 @@ abstract interface class SqliteLibrary { ffi.Pointer sqlite3_errmsg(ffi.Pointer db); ffi.Pointer sqlite3_errstr(int code); int sqlite3_error_offset(ffi.Pointer db); - void sqlite3_free(ffi.Pointer ptr); ffi.Pointer sqlite3_libversion(); ffi.Pointer sqlite3_sourceid(); int sqlite3_libversion_number(); @@ -416,4 +433,63 @@ abstract interface class SqliteLibrary { ffi.Pointer db, int op, int va, ffi.Pointer va$1); int sqlite3_vfs_register(ffi.Pointer arg0, int makeDflt); int sqlite3_vfs_unregister(ffi.Pointer arg0); + int sqlite3session_create( + ffi.Pointer db, + ffi.Pointer zDb, + ffi.Pointer> ppSession); + void sqlite3session_delete(ffi.Pointer pSession); + int sqlite3session_enable(ffi.Pointer pSession, int bEnable); + int sqlite3session_indirect( + ffi.Pointer pSession, int bIndirect); + int sqlite3changeset_start( + ffi.Pointer> pp, + int nChangeset, + ffi.Pointer pChangeset); + int sqlite3changeset_finalize(ffi.Pointer pIter); + int sqlite3changeset_next(ffi.Pointer pIter); + int sqlite3changeset_op( + ffi.Pointer pIter, + ffi.Pointer> pzTab, + ffi.Pointer pnCol, + ffi.Pointer pOp, + ffi.Pointer pbIndirect); + int sqlite3changeset_old(ffi.Pointer pIter, int iVal, + ffi.Pointer> ppValue); + int sqlite3changeset_new(ffi.Pointer pIter, int iVal, + ffi.Pointer> ppValue); + int sqlite3changeset_apply( + ffi.Pointer db, + int nChangeset, + ffi.Pointer pChangeset, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, ffi.Pointer zTab)>> + xFilter, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer pCtx, + ffi.Int eConflict, + ffi.Pointer p)>> + xConflict, + ffi.Pointer pCtx); + int sqlite3changeset_invert(int nIn, ffi.Pointer pIn, + ffi.Pointer pnOut, ffi.Pointer> ppOut); + int sqlite3session_patchset( + ffi.Pointer pSession, + ffi.Pointer pnPatchset, + ffi.Pointer> ppPatchset); + int sqlite3session_changeset( + ffi.Pointer pSession, + ffi.Pointer pnChangeset, + ffi.Pointer> ppChangeset); + int sqlite3session_isempty(ffi.Pointer pSession); + int sqlite3session_attach( + ffi.Pointer pSession, ffi.Pointer zTab); + int sqlite3session_diff( + ffi.Pointer pSession, + ffi.Pointer zFromDb, + ffi.Pointer zTbl, + ffi.Pointer> pzErrMsg); } diff --git a/sqlite3/lib/src/ffi/generated/shared_symbols.yml b/sqlite3/lib/src/ffi/generated/shared_symbols.yml index 960762f1..e0627bae 100644 --- a/sqlite3/lib/src/ffi/generated/shared_symbols.yml +++ b/sqlite3/lib/src/ffi/generated/shared_symbols.yml @@ -10,6 +10,8 @@ files: name: sqlite3_api_routines c:@S@sqlite3_backup: name: sqlite3_backup + c:@S@sqlite3_changeset_iter: + name: sqlite3_changeset_iter c:@S@sqlite3_char: name: sqlite3_char c:@S@sqlite3_context: @@ -18,6 +20,8 @@ files: name: sqlite3_file c:@S@sqlite3_io_methods: name: sqlite3_io_methods + c:@S@sqlite3_session: + name: sqlite3_session c:@S@sqlite3_stmt: name: sqlite3_stmt c:@S@sqlite3_value: diff --git a/sqlite3/lib/src/ffi/memory.dart b/sqlite3/lib/src/ffi/memory.dart index cd4b8db4..f4b66826 100644 --- a/sqlite3/lib/src/ffi/memory.dart +++ b/sqlite3/lib/src/ffi/memory.dart @@ -8,6 +8,8 @@ import 'generated/shared.dart'; const allocate = ffi.malloc; +final freeFinalizer = NativeFinalizer(allocate.nativeFree); + /// Loads a null-pointer with a specified type. /// /// The [nullptr] getter from `dart:ffi` can be slow due to being a @@ -16,17 +18,6 @@ const allocate = ffi.malloc; @pragma('vm:prefer-inline') Pointer nullPtr() => nullptr.cast(); -Pointer _freeImpl(Pointer ptr) { - ptr.free(); - return nullPtr(); -} - -/// Pointer to a function that frees memory we allocated. -/// -/// This corresponds to `void(*)(void*)` arguments found in sqlite. -final Pointer Function(Pointer)>> - freeFunctionPtr = Pointer.fromFunction(_freeImpl); - extension FreePointerExtension on Pointer { void free() => allocate.free(this); } diff --git a/sqlite3/lib/src/implementation/bindings.dart b/sqlite3/lib/src/implementation/bindings.dart index eaf49274..6c42d667 100644 --- a/sqlite3/lib/src/implementation/bindings.dart +++ b/sqlite3/lib/src/implementation/bindings.dart @@ -1,6 +1,8 @@ @internal library; +// ignore_for_file: non_constant_identifier_names + import 'dart:typed_data'; import 'package:meta/meta.dart'; @@ -8,7 +10,50 @@ import 'package:meta/meta.dart'; import '../functions.dart'; import '../vfs.dart'; -// ignore_for_file: non_constant_identifier_names +abstract base class RawChangesetIterator { + // int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter); + int sqlite3changeset_finalize(); + + // int sqlite3changeset_new( + // sqlite3_changeset_iter *pIter, /* Changeset iterator */ + // int iVal, /* Column number */ + // sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ + // ); + SqliteResult sqlite3changeset_new(int columnNumber); + + // int sqlite3changeset_next(sqlite3_changeset_iter *pIter); + int sqlite3changeset_next(); + + // int sqlite3changeset_old( + // sqlite3_changeset_iter *pIter, /* Changeset iterator */ + // int iVal, /* Column number */ + // sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ + // ); + SqliteResult sqlite3changeset_old(int columnNumber); + + // int sqlite3changeset_op( + // sqlite3_changeset_iter *pIter, /* Iterator object */ + // const char **pzTab, /* OUT: Pointer to table name */ + // int *pnCol, /* OUT: Number of columns in table */ + // int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + // int *pbIndirect /* OUT: True for an 'indirect' change */ + // ); + RawChangeSetOp sqlite3changeset_op(); +} + +final class RawChangeSetOp { + final String tableName; + final int columnCount; + final int operation; + final int indirect; + + RawChangeSetOp({ + required this.tableName, + required this.columnCount, + required this.operation, + required this.indirect, + }); +} /// Defines a lightweight abstraction layer around sqlite3 that can be accessed /// without platform-specific APIs (`dart:ffi` or `dart:js`). @@ -27,6 +72,52 @@ import '../vfs.dart'; /// All of the classes and methods defined here are internal and can be changed /// as needed. abstract base class RawSqliteBindings { + // int sqlite3session_create( + // sqlite3 *db, /* Database handle */ + // const char *zDb, /* Name of db (e.g. "main") */ + // sqlite3_session **ppSession /* OUT: New session object */ + // ); + RawSqliteSession sqlite3session_create( + RawSqliteDatabase db, + String name, + ); + + // int sqlite3changeset_apply( + // sqlite3 *db, /* Apply change to "main" db of this handle */ + // int nChangeset, /* Size of changeset in bytes */ + // void *pChangeset, /* Changeset blob */ + // int(*xFilter)( + // void *pCtx, /* Copy of sixth arg to _apply() */ + // const char *zTab /* Table name */ + // ), + // int(*xConflict)( + // void *pCtx, /* Copy of sixth arg to _apply() */ + // int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + // sqlite3_changeset_iter *p /* Handle describing change and conflict */ + // ), + // void *pCtx /* First argument passed to xConflict */ + // ); + int sqlite3changeset_apply( + RawSqliteDatabase database, + Uint8List changeset, + int Function( + String tableName, + )? filter, + int Function( + int eConflict, + RawChangesetIterator iter, + ) conflict, + ); + + // int sqlite3changeset_start( + // sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */ + // int nChangeset, /* Size of changeset blob in bytes */ + // void *pChangeset /* Pointer to blob containing changeset */ + // ); + RawChangesetIterator sqlite3changeset_start(Uint8List changeset); + + Uint8List sqlite3changeset_invert(Uint8List changeset); + String sqlite3_libversion(); String sqlite3_sourceid(); int sqlite3_libversion_number(); @@ -44,6 +135,41 @@ abstract base class RawSqliteBindings { int sqlite3_initialize(); } +abstract base class RawSqliteSession { + // int sqlite3session_attach( + // sqlite3_session *pSession, /* Session object */ + // const char *zTab /* Table name */ + // ); + int sqlite3session_attach([String? name]); + + // int sqlite3session_changeset( + // sqlite3_session *pSession, /* Session object */ + // int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + // void **ppChangeset /* OUT: Buffer containing changeset */ + // ); + Uint8List sqlite3session_changeset(); + Uint8List sqlite3session_patchset(); + + // void sqlite3session_delete(sqlite3_session *pSession); + void sqlite3session_delete(); + + // int sqlite3session_diff( + // sqlite3_session *pSession, + // const char *zFromDb, + // const char *zTbl, + // char **pzErrMsg + // ); + int sqlite3session_diff(String fromDb, String table); + + // int sqlite3session_enable(sqlite3_session *pSession, int bEnable); + int sqlite3session_enable(int enable); + + // int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect); + int sqlite3session_indirect(int indirect); + + int sqlite3session_isempty(); +} + /// Combines a sqlite result code and the result object. final class SqliteResult { final int resultCode; diff --git a/sqlite3/lib/src/implementation/database.dart b/sqlite3/lib/src/implementation/database.dart index bb21fea1..19926aae 100644 --- a/sqlite3/lib/src/implementation/database.dart +++ b/sqlite3/lib/src/implementation/database.dart @@ -27,6 +27,7 @@ final class FinalizableDatabase extends FinalizablePart { final RawSqliteDatabase database; final List _statements = []; + final List dartCleanup = []; FinalizableDatabase(this.bindings, this.database); @@ -36,6 +37,10 @@ final class FinalizableDatabase extends FinalizablePart { stmt.dispose(); } + for (final cleanup in dartCleanup.toList()) { + cleanup(); + } + final code = database.sqlite3_close_v2(); SqliteException? exception; if (code != SqlError.SQLITE_OK) { @@ -111,20 +116,9 @@ base class DatabaseImplementation implements CommonDatabase { database: this, register: () { database.sqlite3_update_hook((kind, tableName, rowId) { - SqliteUpdateKind updateKind; - - switch (kind) { - case SQLITE_INSERT: - updateKind = SqliteUpdateKind.insert; - break; - case SQLITE_UPDATE: - updateKind = SqliteUpdateKind.update; - break; - case SQLITE_DELETE: - updateKind = SqliteUpdateKind.delete; - break; - default: - return; + final updateKind = SqliteUpdateKind.fromCode(kind); + if (updateKind == null) { + return; } final update = SqliteUpdate(updateKind, tableName, rowId); @@ -546,27 +540,7 @@ class ValueList extends ListBase { ); RangeError.checkValidIndex(index, this, 'index', length); - final cached = _cachedCopies[index]; - if (cached != null) { - return cached; - } - - final result = rawValues[index]; - final type = result.sqlite3_value_type(); - - switch (type) { - case SqlType.SQLITE_INTEGER: - return result.sqlite3_value_int64(); - case SqlType.SQLITE_FLOAT: - return result.sqlite3_value_double(); - case SqlType.SQLITE_TEXT: - return result.sqlite3_value_text(); - case SqlType.SQLITE_BLOB: - return result.sqlite3_value_blob(); - case SqlType.SQLITE_NULL: - default: - return null; - } + return _cachedCopies[index] ??= rawValues[index].read(); } @override diff --git a/sqlite3/lib/src/implementation/exception.dart b/sqlite3/lib/src/implementation/exception.dart index ceec5176..b416074c 100644 --- a/sqlite3/lib/src/implementation/exception.dart +++ b/sqlite3/lib/src/implementation/exception.dart @@ -2,6 +2,14 @@ import '../exception.dart'; import 'bindings.dart'; import 'database.dart'; +SqliteException createExceptionOutsideOfDatabase( + RawSqliteBindings bindings, int resultCode, + {String? operation}) { + final errStr = bindings.sqlite3_errstr(resultCode); + + return SqliteException(resultCode, errStr, null, null, null, operation, null); +} + SqliteException createExceptionRaw( RawSqliteBindings bindings, RawSqliteDatabase db, diff --git a/sqlite3/lib/src/implementation/session.dart b/sqlite3/lib/src/implementation/session.dart new file mode 100644 index 00000000..be762511 --- /dev/null +++ b/sqlite3/lib/src/implementation/session.dart @@ -0,0 +1,238 @@ +import 'dart:typed_data'; + +import '../constants.dart'; +import '../database.dart'; +import '../session.dart'; +import 'bindings.dart'; +import 'database.dart'; +import 'exception.dart'; +import 'utils.dart'; + +final class SessionImplementation implements Session { + final RawSqliteBindings bindings; + final RawSqliteSession session; + final FinalizableDatabase database; + + bool _deleted = false; + + SessionImplementation(this.bindings, this.session, this.database) { + database.dartCleanup.add(delete); + } + + static SessionImplementation createSession( + DatabaseImplementation db, String name) { + final bindings = db.bindings; + final result = bindings.sqlite3session_create(db.database, name); + return SessionImplementation(bindings, result, db.finalizable); + } + + void _checkNotDeleted() { + if (_deleted) { + throw StateError('This session has already been deleted'); + } + } + + @override + Changeset changeset() { + _checkNotDeleted(); + final bytes = session.sqlite3session_changeset(); + return ChangesetImplementation(bytes, bindings); + } + + @override + Patchset patchset() { + _checkNotDeleted(); + final bytes = session.sqlite3session_patchset(); + return PatchsetImplementation(bytes, bindings); + } + + @override + void delete() { + if (!_deleted) { + _deleted = true; + session.sqlite3session_delete(); + database.dartCleanup.remove(delete); + } + } + + @override + void diff(String fromDb, String table) { + final result = session.sqlite3session_diff(fromDb, table); + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, result); + } + } + + @override + bool get enabled { + _checkNotDeleted(); + return session.sqlite3session_enable(-1) == 1; + } + + @override + set enabled(bool enabled) { + _checkNotDeleted(); + final result = session.sqlite3session_enable(enabled ? 1 : 0); + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, result); + } + } + + @override + bool get isIndirect { + _checkNotDeleted(); + return session.sqlite3session_indirect(-1) == 1; + } + + @override + set isIndirect(bool isIndirect) { + _checkNotDeleted(); + final result = session.sqlite3session_enable(isIndirect ? 1 : 0); + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, result); + } + } + + @override + bool get isEmpty { + _checkNotDeleted(); + return session.sqlite3session_isempty() != 0; + } + + @override + bool get isNotEmpty => !isEmpty; + + @override + void attach([String? table]) { + _checkNotDeleted(); + final result = session.sqlite3session_attach(table); + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, result); + } + } +} + +final class PatchsetImplementation + with Iterable + implements Patchset { + @override + final Uint8List bytes; + final RawSqliteBindings bindings; + + PatchsetImplementation(this.bytes, this.bindings); + + @override + void applyTo(CommonDatabase database, + [ApplyChangesetOptions options = const ApplyChangesetOptions()]) { + final db = database as DatabaseImplementation; + + final filter = switch (options.filter) { + null => null, + final filter => (String table) { + return filter(table) ? 1 : 0; + } + }; + + final conflict = switch (options.onConflict) { + null => (int _, RawChangesetIterator __) { + return ApplyChangesetConflict.abort.flag; + }, + final conflict => (int conflictKind, RawChangesetIterator it) { + return conflict.flag; + } + }; + + db.bindings.sqlite3changeset_apply( + db.database, + bytes, + filter, + conflict, + ); + } + + @override + ChangesetIterator get iterator { + final raw = bindings.sqlite3changeset_start(bytes); + return ChangesetIteratorImplementation(bindings, raw); + } +} + +final class ChangesetImplementation extends PatchsetImplementation + implements Changeset { + ChangesetImplementation(super.bytes, super.bindings); + + @override + Changeset operator -() { + final result = bindings.sqlite3changeset_invert(bytes); + return ChangesetImplementation(result, bindings); + } +} + +final class ChangesetIteratorImplementation implements ChangesetIterator { + final RawSqliteBindings bindings; + final RawChangesetIterator raw; + + bool _isFinalized = false; + + @override + late ChangesetOperation current; + + ChangesetIteratorImplementation(this.bindings, this.raw); + + @override + bool moveNext() { + if (_isFinalized) { + return false; + } + + final result = raw.sqlite3changeset_next(); + if (result == SqlError.SQLITE_ROW) { + final op = raw.sqlite3changeset_op(); + final kind = SqliteUpdateKind.fromCode(op.operation)!; + + final oldColumns = kind != SqliteUpdateKind.insert + ? List.generate( + op.columnCount, + (i) => raw + .sqlite3changeset_old(i) + .okOrThrowOutsideOfDatabase(bindings) + .read(), + ) + : null; + final newColumns = kind != SqliteUpdateKind.delete + ? List.generate( + op.columnCount, + (i) => raw + .sqlite3changeset_new(i) + .okOrThrowOutsideOfDatabase(bindings) + .read(), + ) + : null; + current = ChangesetOperation( + table: op.tableName, + columnCount: op.columnCount, + operation: kind, + oldValues: oldColumns, + newValues: newColumns, + ); + + return true; + } + + finalize(); + return false; + } + + @override + void finalize() { + if (_isFinalized) { + return; + } + + _isFinalized = true; + final result = raw.sqlite3changeset_finalize(); + if (result != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, result); + } + } +} diff --git a/sqlite3/lib/src/implementation/utils.dart b/sqlite3/lib/src/implementation/utils.dart index 471fc74b..2a0641b5 100644 --- a/sqlite3/lib/src/implementation/utils.dart +++ b/sqlite3/lib/src/implementation/utils.dart @@ -1,4 +1,6 @@ import '../constants.dart'; +import 'bindings.dart'; +import 'exception.dart'; extension BigIntRangeCheck on BigInt { BigInt get checkRange { @@ -20,3 +22,25 @@ int eTextRep(bool deterministic, bool directOnly) { return flags; } + +extension HandleResult on SqliteResult { + T okOrThrowOutsideOfDatabase(RawSqliteBindings bindings) { + if (resultCode != SqlError.SQLITE_OK) { + throw createExceptionOutsideOfDatabase(bindings, resultCode); + } + + return result; + } +} + +extension ReadDartValue on RawSqliteValue { + Object? read() { + return switch (sqlite3_value_type()) { + SqlType.SQLITE_INTEGER => sqlite3_value_int64(), + SqlType.SQLITE_FLOAT => sqlite3_value_double(), + SqlType.SQLITE_TEXT => sqlite3_value_text(), + SqlType.SQLITE_BLOB => sqlite3_value_blob(), + SqlType.SQLITE_BLOB || _ => null, + }; + } +} diff --git a/sqlite3/lib/src/session.dart b/sqlite3/lib/src/session.dart new file mode 100644 index 00000000..e2fd2a3a --- /dev/null +++ b/sqlite3/lib/src/session.dart @@ -0,0 +1,128 @@ +import 'dart:typed_data'; + +import 'database.dart'; +import 'implementation/database.dart'; +import 'implementation/session.dart'; + +/// A Session tracks database changes made by a Conn. +// +/// It is used to build changesets. +/// +/// Equivalent to the sqlite3_session* C object. +abstract interface class Session { + factory Session(CommonDatabase database, {String name = 'main'}) { + final asImpl = database as DatabaseImplementation; + return SessionImplementation.createSession(asImpl, name); + } + + /// Changeset generates a changeset from a session. + /// + /// https://www.sqlite.org/session/sqlite3session_changeset.html + Changeset changeset(); + + /// Patchset generates a patchset from a session. + /// + /// https://www.sqlite.org/session/sqlite3session_patchset.html + Patchset patchset(); + + /// Delete deletes a Session object. + /// + /// https://www.sqlite.org/session/sqlite3session_delete.html + void delete(); + + /// https://www.sqlite.org/session/sqlite3session_attach.html + void attach([String? table]); + + /// Diff appends the difference between two tables (srcDB and the session DB) to the session. + /// The two tables must have the same name and schema. + /// https://www.sqlite.org/session/sqlite3session_diff.html + void diff(String fromDb, String table); + + /// IsEnabled queries if the session is currently enabled. + /// https://www.sqlite.org/session/sqlite3session_enable.html + abstract bool enabled; + + /// https://sqlite.org/session/sqlite3session_indirect.html + abstract bool isIndirect; + + /// https://sqlite.org/session/sqlite3session_isempty.html + bool get isEmpty; + + bool get isNotEmpty; +} + +abstract interface class Patchset implements Iterable { + /// The binary representation of this patchset or changeset. + Uint8List get bytes; + + void applyTo( + CommonDatabase database, [ + ApplyChangesetOptions options = const ApplyChangesetOptions(), + ]); + + @override + ChangesetIterator get iterator; +} + +abstract interface class Changeset implements Patchset { + Changeset operator -(); +} + +final class ChangesetOperation { + final String table; + final int columnCount; + final SqliteUpdateKind operation; + + final List? oldValues; + final List? newValues; + + ChangesetOperation({ + required this.table, + required this.columnCount, + required this.operation, + required this.oldValues, + required this.newValues, + }); + + @override + String toString() { + return 'ChangesetOperation: $operation on $table. old: $oldValues, new: $newValues'; + } +} + +abstract interface class ChangesetIterator + implements Iterator { + void finalize(); +} + +class ApplyChangesetOptions { + /// Skip changes that, when targeted table name is supplied to this function, return a truthy value. + /// By default, all changes are attempted. + final bool Function(String tableName)? filter; + + /// Determines how conflicts are handled. **Default**: `SQLITE_CHANGESET_ABORT`. + final ApplyChangesetConflict? onConflict; + + const ApplyChangesetOptions({this.filter, this.onConflict}); +} + +// #define SQLITE_CHANGESET_OMIT 0 +// #define SQLITE_CHANGESET_REPLACE 1 +// #define SQLITE_CHANGESET_ABORT 2 +enum ApplyChangesetConflict { + /// Abort the changeset application. + abort(2), + + /// Replace the conflicting row. + replace(1), + + /// Omit the current change. + omit(0); + + final int flag; + const ApplyChangesetConflict(this.flag); + + static ApplyChangesetConflict parse(int flag) { + return ApplyChangesetConflict.values.firstWhere((e) => e.flag == flag); + } +} diff --git a/sqlite3/lib/src/wasm/bindings.dart b/sqlite3/lib/src/wasm/bindings.dart index cb4f5cf1..d3e8e61b 100644 --- a/sqlite3/lib/src/wasm/bindings.dart +++ b/sqlite3/lib/src/wasm/bindings.dart @@ -7,6 +7,7 @@ import 'package:sqlite3/src/vfs.dart'; import '../constants.dart'; import '../functions.dart'; import '../implementation/bindings.dart'; +import '../implementation/exception.dart'; import 'wasm_interop.dart' as wasm; import 'wasm_interop.dart'; @@ -111,6 +112,108 @@ final class WasmSqliteBindings extends RawSqliteBindings { final dartId = bindings.memory.int32ValueOfPointer(pAppDataPtr); bindings.callbacks.registeredVfs.remove(dartId); } + + @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 callbackId = bindings.callbacks.registerChangesetApply(callbacks); + final changesetPtr = bindings.allocateBytes(changeset); + + final result = bindings.dart_sqlite3changeset_apply( + (database as WasmDatabase).db, + changeset.length, + changesetPtr, + callbackId, + filter != null ? 1 : 0, + ); + + bindings.callbacks.sessionApply.remove(callbackId); + 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(changesetPtr) + ..free(outPtr); + + if (result != 0) { + throw createExceptionOutsideOfDatabase(this, result); + } + return WasmChangesetIterator(this, iterator); + } } final class WasmDatabase extends RawSqliteDatabase { @@ -631,3 +734,173 @@ class WasmValueList extends ListBase { throw UnsupportedError('Setting element in WasmValueList'); } } + +final class WasmSession extends RawSqliteSession { + static final Finalizer<(WasmBindings, int)> _finalizer = Finalizer((args) { + args.$1.sqlite3session_delete(args.$2); + }); + + 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 { + _finalizer.attach(this, (_bindings, 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() { + _finalizer.detach(this); + _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 extends RawChangesetIterator { + static final Finalizer<(WasmBindings, int)> _finalizer = Finalizer((args) { + args.$1.sqlite3changeset_finalize(args.$2); + }); + + final WasmSqliteBindings bindings; + final int pointer; // the sqlite3_changeset_iter ptr + final Object detach = Object(); + + final WasmBindings _bindings; + + WasmChangesetIterator(this.bindings, this.pointer, {bool owned = true}) + : _bindings = bindings.bindings { + if (owned) { + _finalizer.attach(this, (_bindings, pointer), detach: detach); + } + } + + @override + int sqlite3changeset_finalize() { + _finalizer.detach(detach); + + return _bindings.sqlite3changeset_finalize(pointer); + } + + @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 SqliteResult(resultCode, WasmValue(_bindings, value)); + } + + @override + SqliteResult sqlite3changeset_old(int columnNumber) { + return _extractValue(_bindings.sqlite3changeset_old, columnNumber); + } + + @override + SqliteResult sqlite3changeset_new(int columnNumber) { + return _extractValue(_bindings.sqlite3changeset_old, 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 table = value == 0 ? _bindings.memory.readString(outTable) : ''; + + _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, + ); + } +} diff --git a/sqlite3/lib/src/wasm/wasm_interop.dart b/sqlite3/lib/src/wasm/wasm_interop.dart index 0fdcf4be..825616b2 100644 --- a/sqlite3/lib/src/wasm/wasm_interop.dart +++ b/sqlite3/lib/src/wasm/wasm_interop.dart @@ -90,7 +90,24 @@ class WasmBindings { _sqlite3_initialize, _commit_hooks, _rollback_hooks, - _sqlite3_error_offset; + _sqlite3_error_offset, + _sqlite3session_create, + _sqlite3session_delete, + _sqlite3session_enable, + _sqlite3session_indirect, + _sqlite3changeset_invert, + _sqlite3changeset_start, + _sqlite3changeset_finalize, + _sqlite3changeset_next, + _sqlite3changeset_op, + _sqlite3changeset_old, + _sqlite3changeset_new, + _dart_sqlite3changeset_apply, + _sqlite3session_patchset, + _sqlite3session_changeset, + _sqlite3session_isempty, + _sqlite3session_attach, + _sqlite3session_diff; final Global _sqlite3_temp_directory; @@ -171,7 +188,30 @@ class WasmBindings { _sqlite3_error_offset = instance.functions['sqlite3_error_offset'], _commit_hooks = instance.functions['dart_sqlite3_commits'], _rollback_hooks = instance.functions['dart_sqlite3_rollbacks'], - _sqlite3_temp_directory = instance.globals['sqlite3_temp_directory']! + _sqlite3_temp_directory = instance.globals['sqlite3_temp_directory']!, + _sqlite3session_create = instance.functions['sqlite3session_create'], + _sqlite3session_delete = instance.functions['sqlite3session_delete'], + _sqlite3session_enable = instance.functions['sqlite3session_enable'], + _sqlite3session_indirect = + instance.functions['sqlite3session_indirect'], + _sqlite3changeset_invert = + instance.functions['sqlite3changeset_invert'], + _sqlite3changeset_start = instance.functions['sqlite3changeset_start'], + _sqlite3changeset_finalize = + instance.functions['sqlite3changeset_finalize'], + _sqlite3changeset_next = instance.functions['sqlite3changeset_next'], + _sqlite3changeset_op = instance.functions['sqlite3changeset_op'], + _sqlite3changeset_old = instance.functions['sqlite3changeset_old'], + _sqlite3changeset_new = instance.functions['sqlite3changeset_new'], + _dart_sqlite3changeset_apply = + instance.functions['dart_sqlite3changeset_apply'], + _sqlite3session_patchset = + instance.functions['sqlite3session_patchset'], + _sqlite3session_changeset = + instance.functions['sqlite3session_changeset'], + _sqlite3session_isempty = instance.functions['sqlite3session_isempty'], + _sqlite3session_attach = instance.functions['sqlite3session_attach'], + _sqlite3session_diff = instance.functions['sqlite3session_diff'] // Note when adding new fields: We remove functions from the wasm module that // aren't referenced in Dart. We consider a symbol used when it appears in a @@ -493,6 +533,91 @@ class WasmBindings { } } + int sqlite3session_create(Pointer db, Pointer zDb, Pointer sessionOut) { + return _sqlite3session_create! + .callReturningInt3(db.toJS, zDb.toJS, sessionOut.toJS); + } + + void sqlite3session_delete(Pointer session) { + _sqlite3session_delete!.callAsFunction(null, session.toJS); + } + + int sqlite3session_enable(Pointer session, int enable) { + return _sqlite3session_enable!.callReturningInt2(session.toJS, enable.toJS); + } + + int sqlite3session_indirect(Pointer session, int enable) { + return _sqlite3session_indirect! + .callReturningInt2(session.toJS, enable.toJS); + } + + int sqlite3session_isempty(Pointer session) { + return _sqlite3session_isempty!.callReturningInt(session.toJS); + } + + int sqlite3session_attach(Pointer session, Pointer zTab) { + return _sqlite3session_attach!.callReturningInt2(session.toJS, zTab.toJS); + } + + int sqlite3session_diff( + Pointer session, Pointer zFromDb, Pointer zTbl, Pointer pzErrMsg) { + return _sqlite3session_diff!.callReturningInt4( + session.toJS, zFromDb.toJS, zTbl.toJS, pzErrMsg.toJS); + } + + int sqlite3session_patchset( + Pointer session, Pointer pnPatchset, Pointer ppPatchset) { + return _sqlite3session_patchset! + .callReturningInt3(session.toJS, pnPatchset.toJS, ppPatchset.toJS); + } + + int sqlite3session_changeset( + Pointer session, Pointer pnPatchset, Pointer ppPatchset) { + return _sqlite3session_changeset! + .callReturningInt3(session.toJS, pnPatchset.toJS, ppPatchset.toJS); + } + + int sqlite3changeset_invert( + int nIn, Pointer pIn, Pointer pnOut, Pointer ppOut) { + return _sqlite3changeset_invert! + .callReturningInt4(nIn.toJS, pIn.toJS, pnOut.toJS, ppOut.toJS); + } + + int sqlite3changeset_start(Pointer outPtr, int size, Pointer changeset) { + return _sqlite3changeset_start! + .callReturningInt3(outPtr.toJS, size.toJS, changeset.toJS); + } + + int sqlite3changeset_finalize(Pointer iterator) { + return _sqlite3changeset_finalize!.callReturningInt(iterator.toJS); + } + + int sqlite3changeset_next(Pointer iterator) { + return _sqlite3changeset_next!.callReturningInt(iterator.toJS); + } + + int sqlite3changeset_op(Pointer iterator, Pointer outTable, + Pointer outColCount, Pointer outOp, Pointer outIndirect) { + return _sqlite3changeset_op!.callReturningInt5(iterator.toJS, outTable.toJS, + outColCount.toJS, outOp.toJS, outIndirect.toJS); + } + + int sqlite3changeset_old(Pointer iterator, int iVal, Pointer outValue) { + return _sqlite3changeset_old! + .callReturningInt3(iterator.toJS, iVal.toJS, outValue.toJS); + } + + int sqlite3changeset_new(Pointer iterator, int iVal, Pointer outValue) { + return _sqlite3changeset_new! + .callReturningInt3(iterator.toJS, iVal.toJS, outValue.toJS); + } + + int dart_sqlite3changeset_apply( + Pointer db, int length, Pointer changeset, Pointer context, int filter) { + return _dart_sqlite3changeset_apply!.callReturningInt5( + db.toJS, length.toJS, changeset.toJS, context.toJS, filter.toJS); + } + Pointer get sqlite3_temp_directory { return _sqlite3_temp_directory.value.toDartInt; } @@ -803,6 +928,15 @@ class _InjectedValues { // the other fields don't matter though, localtime_r is not supposed // to set them. }).toJS, + 'changeset_apply_filter': (Pointer context, Pointer zTab) { + final cb = callbacks.sessionApply[context]!; + return cb.filter!(zTab); + }.toJS, + 'changeset_apply_conflict': + (Pointer context, int eConflict, Pointer iter) { + final cb = callbacks.sessionApply[context]!; + return cb.conflict!(eConflict, iter); + }.toJS, } }; } @@ -817,6 +951,7 @@ class DartCallbacks { final Map registeredVfs = {}; final Map openedFiles = {}; + final Map sessionApply = {}; RawUpdateHook? installedUpdateHook; RawCommitHook? installedCommitHook; @@ -829,17 +964,23 @@ class DartCallbacks { } int registerVfs(VirtualFileSystem vfs) { - final id = registeredVfs.length; + final id = _id++; registeredVfs[id] = vfs; return id; } int registerFile(VirtualFileSystemFile file) { - final id = openedFiles.length; + final id = _id++; openedFiles[id] = file; return id; } + int registerChangesetApply(SessionApplyCallbacks cb) { + final id = _id++; + sessionApply[id] = cb; + return id; + } + void forget(int id) => functions.remove(id); static final sqliteVfsPointer = Expando(); @@ -865,6 +1006,17 @@ class RegisteredFunctionSet { }); } +typedef RawFilter = int Function(Pointer tableName); + +typedef RawConflict = int Function(int eConflict, Pointer iterator); + +final class SessionApplyCallbacks { + final RawFilter? filter; + final RawConflict? conflict; + + SessionApplyCallbacks(this.filter, this.conflict); +} + extension on JSFunction { @JS('call') external JSNumber _call5( diff --git a/sqlite3/test/common/session.dart b/sqlite3/test/common/session.dart new file mode 100644 index 00000000..d5c726db --- /dev/null +++ b/sqlite3/test/common/session.dart @@ -0,0 +1,155 @@ +import 'dart:async'; + +import 'package:sqlite3/common.dart'; +import 'package:test/test.dart'; + +void testSession( + FutureOr Function() loadSqlite, +) { + late CommonSqlite3 sqlite3; + late CommonDatabase database; + + setUpAll(() async => sqlite3 = await loadSqlite()); + setUp(() { + database = sqlite3.openInMemory(); + + database + ..execute('CREATE TABLE entries (id INTEGER PRIMARY KEY, content TEXT);') + ..execute( + 'CREATE TABLE other (id INTEGER PRIMARY KEY, content INTEGER);'); + }); + tearDown(() => database.dispose()); + + test('enabled by default', () { + final session = Session(database); + expect(session.enabled, isTrue); + }); + + test('isEmpty', () { + final session = Session(database); + expect(session.isEmpty, isTrue); + expect(session.isNotEmpty, isFalse); + + // Change without attaching session + database.execute('INSERT INTO entries DEFAULT VALUES;'); + expect(session.isEmpty, isTrue); + + session.attach(); + database.execute( + 'INSERT INTO entries (content) VALUES (?);', ['my first entry']); + + expect(session.isEmpty, isFalse); + expect(session.isNotEmpty, isTrue); + }); + + test('attaching to some tables only', () { + final session = Session(database); + expect(session.isEmpty, isTrue); + session.attach('entries'); + database + .execute('INSERT INTO other (content) VALUES (?);', ['ignored table']); + + expect(session.isEmpty, isTrue); + }); + + test('iterator', () { + final session = Session(database)..attach(); + database + ..execute('INSERT INTO entries (content) VALUES (?);', ['a']) + ..execute('UPDATE entries SET content = ?', ['b']); + + final changeset = session.changeset(); + expect(changeset.bytes, isNotEmpty); + expect(changeset, [ + isOp( + operation: SqliteUpdateKind.insert, + oldValues: isNull, + newValues: [1, 'b'], + ) + ]); + }); + + test('changeset invert', () { + final session = Session(database)..attach(); + database.execute('INSERT INTO entries (content) VALUES (?);', ['a']); + + final changeset = session.changeset(); + final inverted = -changeset; + expect(inverted, [ + isOp( + operation: SqliteUpdateKind.delete, + oldValues: [1, 'a'], + newValues: null) + ]); + + expect(database.select('SELECT * FROM entries'), isNotEmpty); + inverted.applyTo(database); + expect(database.select('SELECT * FROM entries'), isEmpty); + + // Full changeset should be empty after applying a and -a + expect(session.changeset(), isEmpty); + }); + + test('apply changeset', () { + final session = Session(database)..attach(); + database.execute('INSERT INTO entries (content) VALUES (?);', ['a']); + final changeset = session.changeset(); + session.delete(); + + database.execute('DELETE FROM entries'); + changeset.applyTo(database); + + expect(database.select('SELECT * FROM entries'), [ + {'id': 1, 'content': 'a'} + ]); + }); + + test('apply patchset', () { + final session = Session(database)..attach(); + database.execute('INSERT INTO entries (content) VALUES (?);', ['a']); + final patchset = session.patchset(); + session.delete(); + + database.execute('DELETE FROM entries'); + patchset.applyTo(database); + + expect(database.select('SELECT * FROM entries'), [ + {'id': 1, 'content': 'a'} + ]); + }); + + test('diff', () { + var session = Session(database); + database.execute('INSERT INTO entries (content) VALUES (?);', ['a']); + + database + ..execute("ATTACH ':memory:' AS another;") + ..execute( + 'CREATE TABLE another.entries (id INTEGER PRIMARY KEY, content TEXT);') + ..execute('INSERT INTO another.entries (content) VALUES (?);', ['b']); + + session = Session(database)..diff('another', 'entries'); + final changeset = session.changeset(); + expect(changeset, [ + isOp( + operation: SqliteUpdateKind.update, + oldValues: [1, 'b'], + newValues: [1, 'a']) + ]); + }, skip: "TODO: Figure out what I'm dong wrong"); +} + +TypeMatcher isOp({ + Object? table = 'entries', + Object? columnCount = 2, + required Object? operation, + required Object? oldValues, + required Object? newValues, +}) { + return isA() + .having((e) => e.table, 'table', table) + .having((e) => e.columnCount, 'colummCount', columnCount) + .having((e) => e.operation, 'operation', operation) + .having((e) => e.oldValues, 'oldValues', oldValues) + .having((e) => e.newValues, 'newValues', newValues); +} diff --git a/sqlite3/test/ffi/common_database_test.dart b/sqlite3/test/ffi/common_database_test.dart index 7d0e239d..196ff0b5 100644 --- a/sqlite3/test/ffi/common_database_test.dart +++ b/sqlite3/test/ffi/common_database_test.dart @@ -6,10 +6,16 @@ import 'package:sqlite3/sqlite3.dart'; import 'package:test/scaffolding.dart'; import '../common/database.dart'; +import '../common/session.dart'; void main() { final hasColumnMeta = open.openSqlite().providesSymbol('sqlite3_column_table_name'); + final hasSession = open.openSqlite().providesSymbol('sqlite3session_create'); testDatabase(() => sqlite3, hasColumnMetadata: hasColumnMeta); + + group('session', () { + testSession(() => sqlite3); + }, skip: hasSession ? false : 'Missing session extension'); } diff --git a/sqlite3/test/wasm/common_database_test.dart b/sqlite3/test/wasm/common_database_test.dart index 1e470b6b..e3b81406 100644 --- a/sqlite3/test/wasm/common_database_test.dart +++ b/sqlite3/test/wasm/common_database_test.dart @@ -4,8 +4,12 @@ library; import 'package:test/test.dart'; import '../common/database.dart'; +import '../common/session.dart'; import 'utils.dart'; void main() { testDatabase(loadSqlite3); + group('session', () { + testSession(loadSqlite3); + }); } diff --git a/sqlite3/tool/generate_bindings.dart b/sqlite3/tool/generate_bindings.dart index 5131eaea..c56a9b20 100644 --- a/sqlite3/tool/generate_bindings.dart +++ b/sqlite3/tool/generate_bindings.dart @@ -25,17 +25,35 @@ void postProcess() { // bindings. final sharedLibrary = StringBuffer() ..writeln() - ..writeln('abstract interface class SqliteLibrary {'); + ..writeln(r''' +abstract interface class SharedSymbolAddresses { + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer)>> + get sqlite3changeset_finalize; + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer)>> + get sqlite3session_delete; + ffi.Pointer)>> + get sqlite3_free; +} +''') + ..writeln('abstract interface class SqliteLibrary {') + ..writeln(' SharedSymbolAddresses get addresses;'); final nativeImplementation = StringBuffer() ..writeln('// Generated by tool/generate_bindings.dart') ..writeln('// ignore_for_file: type=lint') ..writeln("import 'dart:ffi' as ffi;") ..writeln("import 'native.dart' as native;") ..writeln("import 'shared.dart';") - ..writeln('final class NativeAssetsLibrary implements SqliteLibrary {'); + ..writeln('final class NativeAssetsLibrary implements SqliteLibrary {') + ..writeln('@override') + ..writeln('SharedSymbolAddresses get addresses => native.addresses;'); final dynamicBindingsFile = File('lib/src/ffi/generated/dynamic_library.dart'); + final nativeBindingsFile = File('lib/src/ffi/generated/native.dart'); final dynamicBindingsContents = dynamicBindingsFile.readAsStringSync(); String patchSource(String source) { @@ -101,12 +119,23 @@ void postProcess() { nativeImplementation.writeln('}'); final sharedFile = File('lib/src/ffi/generated/shared.dart'); + final originalSharedBindings = sharedFile.readAsStringSync(); sharedFile.writeAsStringSync( - formatter.format('${sharedFile.readAsStringSync()}$sharedLibrary')); - - dynamicBindingsFile.writeAsStringSync(dynamicBindingsContents.replaceFirst( - 'class NativeLibrary', - r'final class NativeLibrary implements imp$1.SqliteLibrary')); + formatter.format('$originalSharedBindings$sharedLibrary')); + + dynamicBindingsFile.writeAsStringSync(dynamicBindingsContents + .replaceFirst('class NativeLibrary', + r'final class NativeLibrary implements imp$1.SqliteLibrary') + .replaceFirst( + 'class _SymbolAddresses', + r'final class _SymbolAddresses implements imp$1.SharedSymbolAddresses', + )); + + nativeBindingsFile + .writeAsStringSync(nativeBindingsFile.readAsStringSync().replaceFirst( + 'class _SymbolAddresses', + r'final class _SymbolAddresses implements imp$1.SharedSymbolAddresses', + )); File('lib/src/ffi/generated/native_library.dart') .writeAsStringSync(formatter.format(nativeImplementation.toString())); @@ -157,8 +186,16 @@ enum _GenerationMode { static Uri symbolUri = Uri.parse('lib/src/ffi/generated/shared_symbols.yml'); - static DeclarationFilters _includeSqlite3Only = - DeclarationFilters(shouldInclude: (d) => d.isSqlite3Symbol); + static DeclarationFilters _includeSqlite3Only = DeclarationFilters( + shouldInclude: (d) => d.isSqlite3Symbol, + shouldIncludeSymbolAddress: (decl) { + return switch (decl.originalName) { + 'sqlite3changeset_finalize' => true, + 'sqlite3session_delete' => true, + 'sqlite3_free' => true, + _ => false, + }; + }); static DeclarationFilters _includeNothing = DeclarationFilters(shouldInclude: (d) => false); diff --git a/sqlite3_android b/sqlite3_android new file mode 160000 index 00000000..e0fdad21 --- /dev/null +++ b/sqlite3_android @@ -0,0 +1 @@ +Subproject commit e0fdad211a6f84efce6ed1b33d619f5f2b1cdc6f diff --git a/sqlite3_native_assets/.gitignore b/sqlite3_native_assets/.gitignore index 3cceda55..676e2e05 100644 --- a/sqlite3_native_assets/.gitignore +++ b/sqlite3_native_assets/.gitignore @@ -5,3 +5,5 @@ # Avoid committing pubspec.lock for library packages; see # https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock +changeset.bin +example/server.js diff --git a/sqlite3_native_assets/example/main.dart b/sqlite3_native_assets/example/main.dart index 5319e5d4..7ab8647d 100644 --- a/sqlite3_native_assets/example/main.dart +++ b/sqlite3_native_assets/example/main.dart @@ -10,6 +10,11 @@ void main() { // Create a new in-memory database. To use a database backed by a file, you // can replace this with sqlite3.open(yourFilePath). final db = sqlite3.openInMemory(); + final db2 = sqlite3.openInMemory(); + + final session = sqlite3.createSession(db, 'main'); + + print('session: ${session.runtimeType}'); // Create a table and insert some data db.execute(''' @@ -18,6 +23,15 @@ void main() { name TEXT NOT NULL ); '''); + db2.execute(''' + CREATE TABLE artists ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL + ); + '''); + + session.attach('artists'); + print('attached to artists'); // Prepare a statement to run it multiple times: final stmt = db.prepare('INSERT INTO artists (name) VALUES (?)'); @@ -30,6 +44,38 @@ void main() { // Dispose a statement when you don't need it anymore to clean up resources. stmt.dispose(); + // final changeset = session.changeset(); + // print('changeset: ${changeset.lengthInBytes} bytes'); + + final changeset = session.patchset(); + print('patchset: ${changeset.lengthInBytes} bytes'); + + // apply changes + sqlite3.sessionChangesetApply( + db2, + changeset, + // conflict: (ctx, eConflict, iter) { + // print('conflict: $eConflict'); + // return ApplyChangesetRule.omit; + // }, + // filter: (ctx, table) { + // print('filter: $table'); + // return 1; + // }, + ); + + // save to ./changeset.bin + File('./changeset.bin').writeAsBytes(changeset); + + // query the database using a simple select statement + final result = db2.select('SELECT * FROM artists'); + for (final row in result) { + print('cs: Artist[id: ${row['id']}, name: ${row['name']}]'); + } + + session.delete(); + print('deleted session'); + // You can run select statements with PreparedStatement.select, or directly // on the database: final ResultSet resultSet = db.select( diff --git a/sqlite3_native_assets/lib/src/defines.dart b/sqlite3_native_assets/lib/src/defines.dart index ae3f9b65..faf837a8 100644 --- a/sqlite3_native_assets/lib/src/defines.dart +++ b/sqlite3_native_assets/lib/src/defines.dart @@ -95,4 +95,5 @@ const _defaultDefines = ''' SQLITE_HAVE_LOCALTIME_S SQLITE_HAVE_MALLOC_USABLE_SIZE SQLITE_HAVE_STRCHRNUL + SQLITE_ENABLE_SESSION '''; diff --git a/sqlite_swift_package b/sqlite_swift_package new file mode 160000 index 00000000..46487a42 --- /dev/null +++ b/sqlite_swift_package @@ -0,0 +1 @@ +Subproject commit 46487a425229d2fb183dea93e75d7918df1f57f2