diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c736eb9e..dd9d175a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,15 +15,15 @@ jobs: name: Compile sqlite3 for ${{ matrix.os }} runs-on: ${{ matrix.os }} env: - SQLITE_YEAR: "2024" - SQLITE_VERSION: "3470200" + SQLITE_YEAR: "2025" + SQLITE_VERSION: "3480000" steps: - uses: actions/cache@v4 id: cache_sqlite_build with: path: sqlite/out - key: sqlite-v3-${{ runner.os }}-${{ env.SQLITE_VERSION }} + key: sqlite-v4-${{ runner.os }}-${{ env.SQLITE_VERSION }} - name: Compile sqlite3 on Linux if: steps.cache_sqlite_build.outputs.cache-hit != 'true' && runner.os == 'Linux' run: | @@ -38,6 +38,18 @@ jobs: cp sqlite3 ../out cp .libs/libsqlite3.so ../out cp *.h ../out + - name: Compile sqlite3 on Linux (no autoinit) + if: steps.cache_sqlite_build.outputs.cache-hit != 'true' && runner.os == 'Linux' + working-directory: sqlite + run: | + curl https://www.sqlite.org/$SQLITE_YEAR/sqlite-amalgamation-$SQLITE_VERSION.zip --output sqlite.zip + unzip sqlite.zip + + cd sqlite-amalgamation-$SQLITE_VERSION + gcc -DSQLITE_OMIT_AUTOINIT=1 -c -fPIC sqlite3.c -o sqlite3.o + gcc -shared sqlite3.o -o libsqlite3.so + mkdir ../out/without_autoinit + cp libsqlite3.so ../out/without_autoinit - name: Compile sqlite3 on macOS if: steps.cache_sqlite_build.outputs.cache-hit != 'true' && runner.os == 'macOS' run: | @@ -179,6 +191,16 @@ jobs: dart test -P ci working-directory: sqlite3/ + - name: Test with SQLITE_OMIT_AUTOINIT + if: runner.os == 'Linux' + run: | + ls $LD_LIBRARY_PATH + dart run tool/check_compile_time_option.dart OMIT_AUTOINIT + dart test -P ci + env: + LD_LIBRARY_PATH: ../sqlite/out/without_autoinit + working-directory: sqlite3/ + - name: Test sqlite3_test package run: | dart pub get @@ -187,8 +209,8 @@ jobs: - name: Web tests run: | - curl https://simon-public.fsn1.your-objectstorage.com/assets/sqlite3/2.6.0/sqlite3.wasm -o example/web/sqlite3.wasm - curl https://simon-public.fsn1.your-objectstorage.com/assets/sqlite3/2.6.0/sqlite3mc.wasm -o example/web/sqlite3mc.wasm + curl https://simon-public.fsn1.your-objectstorage.com/assets/sqlite3/2.7.0/sqlite3.wasm -o example/web/sqlite3.wasm + curl https://simon-public.fsn1.your-objectstorage.com/assets/sqlite3/2.7.0/sqlite3mc.wasm -o example/web/sqlite3mc.wasm dart test -P web -r expanded # If browsers behave differently on different platforms, surely that's not our fault... # So, only run browser tests on Linux to be faster. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01f0f73b..32a00400 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,3 +35,11 @@ jobs: file: sqlite3/.dart_tool/sqlite3_build/sqlite3_debug.init.wasm asset_name: sqlite3.debug.wasm tag: ${{ github.ref_name }} + - name: Upload sqlite3 multipleciphers binary + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + overwrite: true + file: sqlite3/.dart_tool/sqlite3_build/sqlite3mc.wasm + asset_name: sqlite3mc.wasm + tag: ${{ github.ref_name }} diff --git a/integration_tests/flutter_libs/ios/Flutter/AppFrameworkInfo.plist b/integration_tests/flutter_libs/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78..8c6e5614 100644 --- a/integration_tests/flutter_libs/ios/Flutter/AppFrameworkInfo.plist +++ b/integration_tests/flutter_libs/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 12.0 diff --git a/integration_tests/flutter_libs/ios/Podfile b/integration_tests/flutter_libs/ios/Podfile index cd1e6092..d0817ae6 100644 --- a/integration_tests/flutter_libs/ios/Podfile +++ b/integration_tests/flutter_libs/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/integration_tests/flutter_libs/ios/Podfile.lock b/integration_tests/flutter_libs/ios/Podfile.lock new file mode 100644 index 00000000..b4534aa9 --- /dev/null +++ b/integration_tests/flutter_libs/ios/Podfile.lock @@ -0,0 +1,53 @@ +PODS: + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - sqlite3 (3.48.0): + - sqlite3/common (= 3.48.0) + - sqlite3/common (3.48.0) + - sqlite3/dbstatvtab (3.48.0): + - sqlite3/common + - sqlite3/fts5 (3.48.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.48.0): + - sqlite3/common + - sqlite3/rtree (3.48.0): + - sqlite3/common + - sqlite3/spellfix1 (3.48.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.48.0) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + +DEPENDENCIES: + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - sqlite3/spellfix1 + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + +SPEC REPOS: + trunk: + - sqlite3 + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + sqlite3: 3da10a59910c809fb584a93aa46a3f05b785e12e + sqlite3_flutter_libs: c26d86af4ad88f1465dc4e07e6dc6931eef228e4 + +PODFILE CHECKSUM: 6e2ae03bcd74fedeb24d86830a8bb8e09fba08f9 + +COCOAPODS: 1.16.2 diff --git a/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.pbxproj b/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.pbxproj index 7bbb8eff..ecb82645 100644 --- a/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.pbxproj +++ b/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.pbxproj @@ -3,16 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2BD4455609F2CE79A3BE13DF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1CFA466F0D8F132A212BC36 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,9 +34,12 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5E7190CDD9951245611B157A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7E7D24EEFEA94BD8EE13A226 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 8C9BAE5AED1B7453830E5757 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +47,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C1CFA466F0D8F132A212BC36 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +55,25 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 2BD4455609F2CE79A3BE13DF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 602D98FCC5C4B07445C377FE /* Pods */ = { + isa = PBXGroup; + children = ( + 7E7D24EEFEA94BD8EE13A226 /* Pods-Runner.debug.xcconfig */, + 5E7190CDD9951245611B157A /* Pods-Runner.release.xcconfig */, + 8C9BAE5AED1B7453830E5757 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +91,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 602D98FCC5C4B07445C377FE /* Pods */, + DCFEB4362520DA7C246FF1C5 /* Frameworks */, ); sourceTree = ""; }; @@ -98,19 +119,32 @@ path = Runner; sourceTree = ""; }; + DCFEB4362520DA7C246FF1C5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C1CFA466F0D8F132A212BC36 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + FB4139E3603895AF84F0B080 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ABDB1A1EEF0979391DF54B2D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -125,9 +159,12 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -171,10 +208,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -185,6 +224,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -197,6 +237,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + ABDB1A1EEF0979391DF54B2D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FB4139E3603895AF84F0B080 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -272,7 +351,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -354,7 +433,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -403,7 +482,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -490,6 +569,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/integration_tests/flutter_libs/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/integration_tests/flutter_libs/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/integration_tests/flutter_libs/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..2c3563b9 100644 --- a/integration_tests/flutter_libs/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/integration_tests/flutter_libs/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,10 +1,28 @@ + + + + + + + + + + diff --git a/integration_tests/flutter_libs/ios/Runner.xcworkspace/contents.xcworkspacedata b/integration_tests/flutter_libs/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/integration_tests/flutter_libs/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/integration_tests/flutter_libs/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/integration_tests/flutter_libs/ios/Runner/AppDelegate.swift b/integration_tests/flutter_libs/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/integration_tests/flutter_libs/ios/Runner/AppDelegate.swift +++ b/integration_tests/flutter_libs/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/integration_tests/flutter_libs/ios/Runner/Info.plist b/integration_tests/flutter_libs/ios/Runner/Info.plist index e4745f06..1dc590c5 100644 --- a/integration_tests/flutter_libs/ios/Runner/Info.plist +++ b/integration_tests/flutter_libs/ios/Runner/Info.plist @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/integration_tests/flutter_libs/macos/Podfile.lock b/integration_tests/flutter_libs/macos/Podfile.lock index 54819e8a..d2bd3818 100644 --- a/integration_tests/flutter_libs/macos/Podfile.lock +++ b/integration_tests/flutter_libs/macos/Podfile.lock @@ -1,22 +1,22 @@ PODS: - FlutterMacOS (1.0.0) - - sqlite3 (3.47.0): - - sqlite3/common (= 3.47.0) - - sqlite3/common (3.47.0) - - sqlite3/dbstatvtab (3.47.0): + - sqlite3 (3.48.0): + - sqlite3/common (= 3.48.0) + - sqlite3/common (3.48.0) + - sqlite3/dbstatvtab (3.48.0): - sqlite3/common - - sqlite3/fts5 (3.47.0): + - sqlite3/fts5 (3.48.0): - sqlite3/common - - sqlite3/perf-threadsafe (3.47.0): + - sqlite3/perf-threadsafe (3.48.0): - sqlite3/common - - sqlite3/rtree (3.47.0): + - sqlite3/rtree (3.48.0): - sqlite3/common - - sqlite3/spellfix1 (3.47.0): + - sqlite3/spellfix1 (3.48.0): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - FlutterMacOS - - sqlite3 (~> 3.47.0) + - sqlite3 (~> 3.48.0) - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe @@ -39,9 +39,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - sqlite3: 0aa20658a9b238a3b1ff7175eb7bdd863b0ab4fd - sqlite3_flutter_libs: 4ed45d66960c84b1616c887c9818c832d4289092 + sqlite3: 3da10a59910c809fb584a93aa46a3f05b785e12e + sqlite3_flutter_libs: c26d86af4ad88f1465dc4e07e6dc6931eef228e4 PODFILE CHECKSUM: 61e9fedf3423d4f00828847139028f442a455364 -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/integration_tests/flutter_libs_swiftpm/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/integration_tests/flutter_libs_swiftpm/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cbb27e9c..a591532b 100644 --- a/integration_tests/flutter_libs_swiftpm/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/integration_tests/flutter_libs_swiftpm/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -4,10 +4,9 @@ { "identity" : "csqlite", "kind" : "remoteSourceControl", - "location" : "https://github.com/sbooth/CSQLite.git", + "location" : "https://github.com/simolus3/CSQLite.git", "state" : { - "revision" : "f9bc82fd757667a5d1819db4fbb073c97488eb85", - "version" : "3.47.1" + "revision" : "7fa023c325a734f9dd1dcd30eeb65a620f745a2d" } } ], diff --git a/integration_tests/flutter_libs_swiftpm/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/integration_tests/flutter_libs_swiftpm/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index cbb27e9c..a591532b 100644 --- a/integration_tests/flutter_libs_swiftpm/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/integration_tests/flutter_libs_swiftpm/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -4,10 +4,9 @@ { "identity" : "csqlite", "kind" : "remoteSourceControl", - "location" : "https://github.com/sbooth/CSQLite.git", + "location" : "https://github.com/simolus3/CSQLite.git", "state" : { - "revision" : "f9bc82fd757667a5d1819db4fbb073c97488eb85", - "version" : "3.47.1" + "revision" : "7fa023c325a734f9dd1dcd30eeb65a620f745a2d" } } ], diff --git a/sqlite3/CHANGELOG.md b/sqlite3/CHANGELOG.md index c0829731..e18fb343 100644 --- a/sqlite3/CHANGELOG.md +++ b/sqlite3/CHANGELOG.md @@ -1,3 +1,24 @@ +## 2.7.2 + +- Web: Fix update events not being delivered in some shared worker setups. + +## 2.7.1 + +- Web: Fix a crash when using version `2.7.0` of this package with an older + WebAssembly bundle. + Note: Version `2.7.0` has been retracted from pub.dev for this reason. + +## 2.7.0 + +- Add support for commit and rollback hooks as well as a predicate that can + revert transactions. Thanks to [@jackd](https://github.com/jackd)! + +## 2.6.1 + +- Fix out-of-bound reads in the `xWrite` implementation of the OPFS-locks based + file-system implementation when writing more than 64 KiB in one operation. +- Support SQLite libraries compiled with `SQLITE_OMIT_AUTOINIT`. + ## 2.6.0 - Add `SimpleOpfsFileSystem.deleteFromStorage` to delete OPFS-based file diff --git a/sqlite3/README.md b/sqlite3/README.md index 0e770881..ee2545c6 100644 --- a/sqlite3/README.md +++ b/sqlite3/README.md @@ -200,23 +200,17 @@ cmake -S assets/wasm -B .dart_tool/sqlite3_build ##### macOS -On macOS, I'm installing `cmake`, `llvm` and `binaryen` through Homebrew. Afterwards, you can download the -wasi sysroot and the compiler runtimes from the Wasi SDK project: +On macOS, install a WebAssembly-capable C compiler. If you're using Homebrew, +you can use ``` -curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/libclang_rt.builtins-wasm32-wasi-22.0.tar.gz | \ - tar x -zf - -C /opt/homebrew/opt/llvm/lib/clang/18* - -curl -sS -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sysroot-22.0.tar.gz | \ - sudo tar x -zf - -C /opt +brew install cmake llvm binaryen wasi-libc wasi-runtimes ``` -Replace `clang/18` with the correct directory if you're using a different version. - Then, set up the build with ``` -cmake -Dwasi_sysroot=/opt/wasi-sysroot -Dclang=/opt/homebrew/opt/llvm/bin/clang -S assets/wasm -B .dart_tool/sqlite3_build +cmake -Dwasi_sysroot=/opt/homebrew/share/wasi-sysroot -Dclang=/opt/homebrew/opt/llvm/bin/clang -S assets/wasm -B .dart_tool/sqlite3_build ``` #### Building diff --git a/sqlite3/assets/sqlite3.h b/sqlite3/assets/sqlite3.h index e457ca22..83bba7c0 100644 --- a/sqlite3/assets/sqlite3.h +++ b/sqlite3/assets/sqlite3.h @@ -1,3 +1,6 @@ +// This file defines the definitions for which we generate FFI bindings on +// native platforms. To re-generate bindings, run: +// `dart run ffigen --config ffigen.yaml`. #include typedef struct sqlite3_char sqlite3_char; @@ -8,6 +11,8 @@ typedef struct sqlite3_api_routines sqlite3_api_routines; sqlite3_char *sqlite3_temp_directory; +int sqlite3_initialize(); + int sqlite3_open_v2(sqlite3_char *filename, sqlite3 **ppDb, int flags, sqlite3_char *zVfs); int sqlite3_close_v2(sqlite3 *db); @@ -35,6 +40,8 @@ void *sqlite3_update_hook(sqlite3 *, void (*)(void *, int, sqlite3_char const *, sqlite3_char const *, int64_t), void *); +void *sqlite3_commit_hook(sqlite3 *, int (*)(void *), void *); +void *sqlite3_rollback_hook(sqlite3 *, void (*)(void *), void *); int sqlite3_get_autocommit(sqlite3 *db); // Statements diff --git a/sqlite3/assets/wasm/CMakeLists.txt b/sqlite3/assets/wasm/CMakeLists.txt index 814f1053..a230b531 100644 --- a/sqlite3/assets/wasm/CMakeLists.txt +++ b/sqlite3/assets/wasm/CMakeLists.txt @@ -12,13 +12,13 @@ include(FetchContent) FetchContent_Declare( sqlite3 # NOTE: When changing this, also update `test/wasm/sqlite3_test.dart` - URL https://sqlite.org/2024/sqlite-autoconf-3470200.tar.gz + URL https://sqlite.org/2025/sqlite-autoconf-3480000.tar.gz DOWNLOAD_EXTRACT_TIMESTAMP NEW ) FetchContent_Declare( sqlite3mc - URL https://github.com/utelle/SQLite3MultipleCiphers/releases/download/v1.9.2/sqlite3mc-1.9.2-sqlite-3.47.2-amalgamation.zip + URL https://github.com/utelle/SQLite3MultipleCiphers/releases/download/v2.0.2/sqlite3mc-2.0.2-sqlite-3.48.0-amalgamation.zip DOWNLOAD_EXTRACT_TIMESTAMP NEW ) @@ -27,7 +27,7 @@ FetchContent_MakeAvailable(sqlite3mc) file(DOWNLOAD https://raw.githubusercontent.com/sqlite/sqlite/master/src/test_vfstrace.c "${CMAKE_BINARY_DIR}/vfstrace.c") -set(POWERSYNC_VERSION "0.3.8") +set(POWERSYNC_VERSION "0.3.9") set(POWERSYNC_A "${CMAKE_BINARY_DIR}/libpowersync-wasm.a") file(DOWNLOAD "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${POWERSYNC_VERSION}/libpowersync-wasm.a" "${POWERSYNC_A}") @@ -36,7 +36,7 @@ add_custom_command( OUTPUT required_symbols.txt WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../ COMMAND dart run tool/wasm_symbols.dart ${CMAKE_CURRENT_BINARY_DIR}/required_symbols.txt - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../../tool/wasm_symbols.dart + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../../tool/wasm_symbols.dart ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/src/wasm/wasm_interop.dart VERBATIM ) add_custom_target(required_symbols DEPENDS required_symbols.txt) @@ -55,7 +55,18 @@ macro(base_sqlite3_target name debug crypto) if(${crypto}) list(APPEND sources "${sqlite3mc_SOURCE_DIR}/sqlite3mc_amalgamation.c") list(APPEND sources "${CMAKE_CURRENT_SOURCE_DIR}/getentropy.c") - list(APPEND flags "-DSQLITE_OMIT_AUTOINIT") + # We only want to support the chacha20 cipher, some of the others are tricky to + # compile to webassembly. + list(APPEND flags + "-DSQLITE_OMIT_AUTOINIT" + "-DHAVE_CIPHER_AES_128_CBC=0" + "-DHAVE_CIPHER_AES_256_CBC=0" + "-DHAVE_CIPHER_SQLCIPHER=0" + "-DHAVE_CIPHER_RC4=0" + "-DHAVE_CIPHER_ASCON128=0" + "-DHAVE_CIPHER_AEGIS=0" + "-DHAVE_CIPHER_CHACHA20=1" + ) else() list(APPEND sources "${sqlite3_SOURCE_DIR}/sqlite3.c") endif() diff --git a/sqlite3/assets/wasm/bridge.h b/sqlite3/assets/wasm/bridge.h index 06bc9c46..9e1115a5 100644 --- a/sqlite3/assets/wasm/bridge.h +++ b/sqlite3/assets/wasm/bridge.h @@ -54,6 +54,8 @@ import_dart("function_hook") extern void dartUpdateHook(void *id, int kind, const char *db, const char *table, sqlite3_int64 rowid); +import_dart("function_commit_hook") extern int dartCommitHook(void *id); +import_dart("function_rollback_hook") extern void dartRollbackHook(void *id); import_dart("function_compare") extern int dartXCompare(void *id, int lengthA, const void *a, int lengthB, diff --git a/sqlite3/assets/wasm/helpers.c b/sqlite3/assets/wasm/helpers.c index 90b4e992..cd8fc6f7 100644 --- a/sqlite3/assets/wasm/helpers.c +++ b/sqlite3/assets/wasm/helpers.c @@ -227,6 +227,14 @@ SQLITE_API void dart_sqlite3_updates(sqlite3 *db, int id) { sqlite3_update_hook(db, id >= 0 ? &dartUpdateHook : NULL, (void *)id); } +SQLITE_API void dart_sqlite3_commits(sqlite3 *db, int id) { + sqlite3_commit_hook(db, id >= 0 ? &dartCommitHook : NULL, (void *)id); +} + +SQLITE_API void dart_sqlite3_rollbacks(sqlite3 *db, int id) { + sqlite3_rollback_hook(db, id >= 0 ? &dartRollbackHook : NULL, (void *)id); +} + SQLITE_API int dart_sqlite3_create_collation(sqlite3 *db, const char *zName, int eTextRep, int id) { return sqlite3_create_collation_v2(db, zName, eTextRep, (void *)id, diff --git a/sqlite3/build.yaml b/sqlite3/build.yaml index 179cc450..2d39ce87 100644 --- a/sqlite3/build.yaml +++ b/sqlite3/build.yaml @@ -29,6 +29,10 @@ targets: enabled: false $default: + sources: + include: + - lib/** + - example/web/** builders: build_web_compilers:entrypoint: generate_for: diff --git a/sqlite3/lib/src/database.dart b/sqlite3/lib/src/database.dart index 0bd2214f..3cf8f1bc 100644 --- a/sqlite3/lib/src/database.dart +++ b/sqlite3/lib/src/database.dart @@ -51,6 +51,52 @@ abstract class CommonDatabase { /// - [Data Change Notification Callbacks](https://www.sqlite.org/c3ref/update_hook.html) Stream get updates; + /// The [VoidPredicate] that is used to filter out transactions before commiting. + /// + /// This is run before every commit, i.e. before the end of an explicit + /// transaction and before the end of an implicit transactions created by + /// an insert / update / delete operation. + /// + /// If the filter returns `false`, the commit is converted into a rollback. + /// + /// The function should not do anything that modifies the database connection, + /// e.g. run SQL statements, prepare statements or step. + /// + /// See also: + /// - [Commit Hooks](https://www.sqlite.org/c3ref/commit_hook.html) + VoidPredicate? get commitFilter; + set commitFilter(VoidPredicate? commitFilter); + + /// An async stream that fires after each commit. + /// + /// Listening to this stream will register a "commit hook" on the native + /// database. Each commit that sqlite3 reports through that hook will then + /// be added to the stream. + /// + /// Note that the stream reports updates _asynchronously_, e.g. one event + /// loop iteration after sqlite reports them. + /// + /// Also note this works in conjunction with `commitFilter`. If the filter + /// function is not null and returns `false`, the commit will not occur and + /// this stream will not fire. + /// + /// See also: + /// - [Commit Hooks](https://www.sqlite.org/c3ref/commit_hook.html) + Stream get commits; + + /// An async stream that fires after each rollback. + /// + /// Listening to this stream will register a "rollback hook" on the native + /// database. Each rollback that sqlite3 reports through that hook will then + /// be added to the stream. + /// + /// Note that the stream reports updates _asynchronously_, e.g. one event + /// loop iteration after sqlite reports them. + /// + /// See also: + /// - [Commit Hooks](https://www.sqlite.org/c3ref/commit_hook.html) + Stream get rollbacks; + /// Executes the [sql] statement with the provided [parameters], ignoring any /// rows returned by the statement. /// diff --git a/sqlite3/lib/src/ffi/bindings.dart b/sqlite3/lib/src/ffi/bindings.dart index c58ec444..c9751cb1 100644 --- a/sqlite3/lib/src/ffi/bindings.dart +++ b/sqlite3/lib/src/ffi/bindings.dart @@ -84,6 +84,11 @@ final class FfiBindings extends RawSqliteBindings { } } + @override + int sqlite3_initialize() { + return bindings.bindings.sqlite3_initialize(); + } + @override String sqlite3_errstr(int extendedErrorCode) { return bindings.bindings.sqlite3_errstr(extendedErrorCode).readString(); @@ -409,6 +414,8 @@ final class FfiDatabase extends RawSqliteDatabase { final BindingsWithLibrary bindings; final Pointer db; NativeCallable<_UpdateHook>? _installedUpdateHook; + NativeCallable<_CommitHook>? _installedCommitHook; + NativeCallable<_RollbackHook>? _installedRollbackHook; FfiDatabase(this.bindings, this.db); @@ -561,6 +568,37 @@ final class FfiDatabase extends RawSqliteDatabase { previous?.close(); } + @override + void sqlite3_commit_hook(RawCommitHook? hook) { + final previous = _installedCommitHook; + + if (hook == null) { + _installedCommitHook = null; + bindings.bindings.sqlite3_commit_hook(db, nullPtr(), nullPtr()); + } else { + final native = _installedCommitHook = hook.toNative(); + bindings.bindings + .sqlite3_commit_hook(db, native.nativeFunction, nullPtr()); + } + + previous?.close(); + } + + @override + void sqlite3_rollback_hook(RawRollbackHook? hook) { + final previous = _installedRollbackHook; + + if (hook == null) { + bindings.bindings.sqlite3_rollback_hook(db, nullPtr(), nullPtr()); + } else { + final native = _installedRollbackHook = hook.toNative(); + bindings.bindings + .sqlite3_rollback_hook(db, native.nativeFunction, nullPtr()); + } + + previous?.close(); + } + @override int sqlite3_db_config(int op, int value) { final result = bindings.bindings.sqlite3_db_config( @@ -971,6 +1009,8 @@ typedef _XCompare = Int Function( Pointer, Int, Pointer, Int, Pointer); typedef _UpdateHook = Void Function( Pointer, Int, Pointer, Pointer, Int64); +typedef _CommitHook = Int Function(Pointer); +typedef _RollbackHook = Void Function(Pointer); extension on RawXFunc { NativeCallable<_XFunc> toNative(Bindings bindings) { @@ -1024,3 +1064,24 @@ extension on RawUpdateHook { )..keepIsolateAlive = false; } } + +extension on RawCommitHook { + NativeCallable<_CommitHook> toNative() { + return NativeCallable.isolateLocal( + (Pointer _) { + return this(); + }, + exceptionalReturn: 1, + )..keepIsolateAlive = false; + } +} + +extension on RawRollbackHook { + NativeCallable<_RollbackHook> toNative() { + return NativeCallable.isolateLocal( + (Pointer _) { + this(); + }, + )..keepIsolateAlive = false; + } +} diff --git a/sqlite3/lib/src/ffi/implementation.dart b/sqlite3/lib/src/ffi/implementation.dart index 750ddc56..0d057e21 100644 --- a/sqlite3/lib/src/ffi/implementation.dart +++ b/sqlite3/lib/src/ffi/implementation.dart @@ -51,6 +51,8 @@ final class FfiSqlite3 extends Sqlite3Implementation implements Sqlite3 { @override void ensureExtensionLoaded(SqliteExtension extension) { + initialize(); + final entrypoint = (extension as SqliteExtensionImpl)._resolveEntrypoint; final functionPtr = entrypoint(ffiBindings.bindings.library); diff --git a/sqlite3/lib/src/ffi/sqlite3.g.dart b/sqlite3/lib/src/ffi/sqlite3.g.dart index 288f485e..41c63d43 100644 --- a/sqlite3/lib/src/ffi/sqlite3.g.dart +++ b/sqlite3/lib/src/ffi/sqlite3.g.dart @@ -28,6 +28,15 @@ class Bindings { set sqlite3_temp_directory(ffi.Pointer value) => _sqlite3_temp_directory.value = value; + int sqlite3_initialize() { + return _sqlite3_initialize(); + } + + late final _sqlite3_initializePtr = + _lookup>('sqlite3_initialize'); + late final _sqlite3_initialize = + _sqlite3_initializePtr.asFunction(); + int sqlite3_open_v2( ffi.Pointer filename, ffi.Pointer> ppDb, @@ -308,6 +317,60 @@ class Bindings { ffi.Int64)>>, ffi.Pointer)>(); + ffi.Pointer sqlite3_commit_hook( + ffi.Pointer arg0, + ffi.Pointer)>> + arg1, + ffi.Pointer arg2, + ) { + return _sqlite3_commit_hook( + arg0, + arg1, + arg2, + ); + } + + late final _sqlite3_commit_hookPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction)>>, + ffi.Pointer)>>('sqlite3_commit_hook'); + late final _sqlite3_commit_hook = _sqlite3_commit_hookPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction)>>, + ffi.Pointer)>(); + + ffi.Pointer sqlite3_rollback_hook( + ffi.Pointer arg0, + ffi.Pointer)>> + arg1, + ffi.Pointer arg2, + ) { + return _sqlite3_rollback_hook( + arg0, + arg1, + arg2, + ); + } + + late final _sqlite3_rollback_hookPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction)>>, + ffi.Pointer)>>('sqlite3_rollback_hook'); + late final _sqlite3_rollback_hook = _sqlite3_rollback_hookPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction)>>, + ffi.Pointer)>(); + int sqlite3_get_autocommit( ffi.Pointer db, ) { diff --git a/sqlite3/lib/src/functions.dart b/sqlite3/lib/src/functions.dart index 12a82cce..57450d2c 100644 --- a/sqlite3/lib/src/functions.dart +++ b/sqlite3/lib/src/functions.dart @@ -1,5 +1,8 @@ import 'package:meta/meta.dart'; +/// A filter function without any arguments. +typedef VoidPredicate = bool Function(); + /// A collating function provided to a sql collation. /// /// The function must return a `int`. diff --git a/sqlite3/lib/src/implementation/bindings.dart b/sqlite3/lib/src/implementation/bindings.dart index 02cf6be3..7813b2e4 100644 --- a/sqlite3/lib/src/implementation/bindings.dart +++ b/sqlite3/lib/src/implementation/bindings.dart @@ -40,6 +40,8 @@ abstract base class RawSqliteBindings { void registerVirtualFileSystem(VirtualFileSystem vfs, int makeDefault); void unregisterVirtualFileSystem(VirtualFileSystem vfs); + + int sqlite3_initialize(); } /// Combines a sqlite result code and the result object. @@ -57,6 +59,8 @@ typedef RawXFunc = void Function(RawSqliteContext, List); typedef RawXStep = void Function(RawSqliteContext, List); typedef RawXFinal = void Function(RawSqliteContext); typedef RawUpdateHook = void Function(int kind, String tableName, int rowId); +typedef RawCommitHook = int Function(); +typedef RawRollbackHook = void Function(); typedef RawCollation = int Function(String? a, String? b); abstract base class RawSqliteDatabase { @@ -79,6 +83,10 @@ abstract base class RawSqliteDatabase { void sqlite3_update_hook(RawUpdateHook? hook); + void sqlite3_commit_hook(RawCommitHook? hook); + + void sqlite3_rollback_hook(RawRollbackHook? hook); + /// Returns a compiler able to create prepared statements from the utf8- /// encoded SQL string passed as its argument. RawStatementCompiler newCompiler(List utf8EncodedSql); diff --git a/sqlite3/lib/src/implementation/database.dart b/sqlite3/lib/src/implementation/database.dart index 4ead4300..bb21fea1 100644 --- a/sqlite3/lib/src/implementation/database.dart +++ b/sqlite3/lib/src/implementation/database.dart @@ -57,7 +57,9 @@ base class DatabaseImplementation implements CommonDatabase { final FinalizableDatabase finalizable; - final List> _updateListeners = []; + _StreamHandlers? _updates; + _StreamHandlers? _rollbacks; + _StreamHandlers? _commits; var _isClosed = false; @@ -104,6 +106,67 @@ base class DatabaseImplementation implements CommonDatabase { } } + _StreamHandlers _updatesHandler() { + return _updates ??= _StreamHandlers( + 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 update = SqliteUpdate(updateKind, tableName, rowId); + _updates!.deliverAsyncEvent(update); + }); + }, + unregister: () => database.sqlite3_update_hook(null), + ); + } + + _StreamHandlers _rollbackHandler() { + return _rollbacks ??= _StreamHandlers( + database: this, + register: () => database.sqlite3_rollback_hook(() { + _rollbacks!.deliverAsyncEvent(null); + }), + unregister: () => database.sqlite3_rollback_hook(null), + ); + } + + _StreamHandlers _commitHandler() { + return _commits ??= _StreamHandlers( + database: this, + register: () => database.sqlite3_commit_hook(() { + var complete = true; + if (_commits!.syncCallback case final callback?) { + complete = callback(); + } + + if (complete) { + _commits!.deliverAsyncEvent(null); + // There's no reason to deliver a rollback event if the synchronous + // handler determined that the transaction should be reverted, sqlite3 + // will emit a rollbacke event for us. + } + + return complete ? 0 : 1; + }), + unregister: () => database.sqlite3_commit_hook(null), + ); + } + Uint8List _validateAndEncodeFunctionName(String functionName) { final functionNameBytes = utf8.encode(functionName); @@ -224,10 +287,13 @@ base class DatabaseImplementation implements CommonDatabase { disposeFinalizer.detach(this); _isClosed = true; - for (final listener in _updateListeners) { - listener.close(); - } + _updates?.close(); + _commits?.close(); + _rollbacks?.close(); + database.sqlite3_update_hook(null); + database.sqlite3_commit_hook(null); + database.sqlite3_rollback_hook(null); finalizable.dispose(); } @@ -401,63 +467,20 @@ base class DatabaseImplementation implements CommonDatabase { } @override - Stream get updates { - return Stream.multi( - (newListener) { - if (_isClosed) { - newListener.closeSync(); - return; - } - - void addUpdateListener() { - final isFirstListener = _updateListeners.isEmpty; - _updateListeners.add(newListener); - - if (isFirstListener) { - // Add native update hook - 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 update = SqliteUpdate(updateKind, tableName, rowId); - for (final listener in _updateListeners) { - listener.add(update); - } - }); - } - } + Stream get updates => _updatesHandler().stream; - void removeUpdateListener() { - _updateListeners.remove(newListener); + @override + Stream get rollbacks => _rollbackHandler().stream; - if (_updateListeners.isEmpty && !_isClosed) { - database.sqlite3_update_hook(null); // Remove native hook - } - } + @override + Stream get commits => _commitHandler().stream; - newListener - ..onPause = removeUpdateListener - ..onCancel = removeUpdateListener - ..onResume = addUpdateListener; + @override + VoidPredicate? get commitFilter => _commitHandler().syncCallback; - // Since this is a onListen callback, add listener now - addUpdateListener(); - }, - isBroadcast: true, - ); + @override + set commitFilter(VoidPredicate? commitFilter) { + _commitHandler().syncCallback = commitFilter; } } @@ -565,3 +588,109 @@ final class DatabaseConfigImplementation extends DatabaseConfig { } } } + +/// A shared implementation for the [CommonDatabase.updates], +/// [CommonDatabase.commits] and [CommonDatabase.rollbacks] streams used by +/// [DatabaseImplementation]. +/// +/// [T] is the event type of the stream. These streams wrap SQLite callbacks +/// which are not supposed to make their own database calls. Thus, all streams +/// have an asynchronous delay from when the C callback is called. +/// The commits stream also supports a synchronous callback that can turn +/// commits into rollbacks. This is represented by [_syncCallback]. +final class _StreamHandlers { + final DatabaseImplementation _database; + final List> _asyncListeners = []; + SyncCallback? _syncCallback; + + /// Registers a native callback on the database. + final void Function() _register; + + /// Unregisters the native callback on the database. + final void Function() _unregister; + + Stream? _stream; + + Stream get stream => _stream!; + + _StreamHandlers({ + required DatabaseImplementation database, + required void Function() register, + required void Function() unregister, + }) : _database = database, + _register = register, + _unregister = unregister { + _stream = Stream.multi( + (newListener) { + if (_database._isClosed) { + newListener.close(); + return; + } + + void addListener() { + _addAsyncListener(newListener); + } + + void removeListener() { + _removeAsyncListener(newListener); + } + + newListener + ..onPause = removeListener + ..onCancel = removeListener + ..onResume = addListener; + // Since this is a onListen callback, add listener now + addListener(); + }, + isBroadcast: true, + ); + } + + bool get hasListener => _asyncListeners.isNotEmpty || _syncCallback != null; + + SyncCallback? get syncCallback => _syncCallback; + + set syncCallback(SyncCallback? value) { + if (value != _syncCallback) { + final hadListenerBefore = hasListener; + _syncCallback = value; + final hasListenerNow = hasListener; + + if (!hadListenerBefore && hasListenerNow) { + _register(); + } else if (hadListenerBefore && !hasListenerNow) { + _unregister(); + } + } + } + + void _addAsyncListener(MultiStreamController listener) { + final isFirstListener = !hasListener; + _asyncListeners.add(listener); + + if (isFirstListener) { + _register(); + } + } + + void _removeAsyncListener(MultiStreamController listener) { + _asyncListeners.remove(listener); + + if (!hasListener && !_database._isClosed) { + _unregister(); + } + } + + void deliverAsyncEvent(T event) { + for (final listener in _asyncListeners) { + listener.add(event); + } + } + + void close() { + for (final listener in _asyncListeners) { + listener.close(); + } + _syncCallback = null; + } +} diff --git a/sqlite3/lib/src/implementation/sqlite3.dart b/sqlite3/lib/src/implementation/sqlite3.dart index 34c1b7ac..f87fe7aa 100644 --- a/sqlite3/lib/src/implementation/sqlite3.dart +++ b/sqlite3/lib/src/implementation/sqlite3.dart @@ -2,6 +2,7 @@ import 'package:meta/meta.dart'; import '../constants.dart'; import '../database.dart'; +import '../exception.dart'; import '../sqlite3.dart'; import '../vfs.dart'; import 'bindings.dart'; @@ -24,12 +25,21 @@ base class Sqlite3Implementation implements CommonSqlite3 { @override set tempDirectory(String? value) => bindings.sqlite3_temp_directory = value; + void initialize() { + final rc = bindings.sqlite3_initialize(); + if (rc != 0) { + throw SqliteException(rc, 'Error returned by sqlite3_initialize'); + } + } + @override CommonDatabase open(String filename, {String? vfs, OpenMode mode = OpenMode.readWriteCreate, bool uri = false, bool? mutex}) { + initialize(); + int flags; switch (mode) { case OpenMode.readOnly: diff --git a/sqlite3/lib/src/wasm/bindings.dart b/sqlite3/lib/src/wasm/bindings.dart index bf210817..97bb38ff 100644 --- a/sqlite3/lib/src/wasm/bindings.dart +++ b/sqlite3/lib/src/wasm/bindings.dart @@ -5,7 +5,6 @@ import 'dart:typed_data'; import 'package:sqlite3/src/vfs.dart'; import '../constants.dart'; -import '../exception.dart'; import '../functions.dart'; import '../implementation/bindings.dart'; import 'wasm_interop.dart' as wasm; @@ -56,8 +55,6 @@ final class WasmSqliteBindings extends RawSqliteBindings { @override SqliteResult sqlite3_open_v2( String name, int flags, String? zVfs) { - sqlite3_initialize(); - final namePtr = bindings.allocateZeroTerminated(name); final outDb = bindings.malloc(wasm.WasmBindings.pointerSize); final vfsPtr = zVfs == null ? 0 : bindings.allocateZeroTerminated(zVfs); @@ -79,11 +76,9 @@ final class WasmSqliteBindings extends RawSqliteBindings { return bindings.memory.readString(bindings.sqlite3_sourceid()); } - void sqlite3_initialize() { - final rc = bindings.sqlite3_initialize(); - if (rc != 0) { - throw SqliteException(rc, 'sqlite3_initialize call failed'); - } + @override + int sqlite3_initialize() { + return bindings.sqlite3_initialize(); } @override @@ -95,7 +90,6 @@ final class WasmSqliteBindings extends RawSqliteBindings { if (ptr == 0) { throw StateError('could not register vfs'); } - sqlite3_initialize(); DartCallbacks.sqliteVfsPointer[vfs] = ptr; } @@ -262,6 +256,20 @@ final class WasmDatabase extends RawSqliteDatabase { bindings.dart_sqlite3_updates(db, hook != null ? 1 : -1); } + @override + void sqlite3_commit_hook(RawCommitHook? hook) { + bindings.callbacks.installedCommitHook = hook; + + bindings.dart_sqlite3_commits(db, hook != null ? 1 : -1); + } + + @override + void sqlite3_rollback_hook(RawRollbackHook? hook) { + bindings.callbacks.installedRollbackHook = hook; + + bindings.dart_sqlite3_rollbacks(db, hook != null ? 1 : -1); + } + @override int sqlite3_get_autocommit() { return bindings.sqlite3_get_autocommit(db); diff --git a/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart b/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart index 154bbbb7..e5244de6 100644 --- a/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart +++ b/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart @@ -202,9 +202,11 @@ class WasmFile extends BaseVfsFile { // buffer would otherwise overflow. final bytesToWrite = min(MessageSerializer.dataSize, remainingBytes); - final subBuffer = bytesToWrite == remainingBytes - ? buffer - : buffer.buffer.asUint8List(buffer.offsetInBytes, bytesToWrite); + final subBuffer = + (bytesToWrite == remainingBytes && totalBytesWritten == 0) + ? buffer + : buffer.buffer.asUint8List( + buffer.offsetInBytes + totalBytesWritten, bytesToWrite); vfs.serializer.byteView.set(subBuffer, 0); vfs._runInWorker(WorkerOperation.xWrite, diff --git a/sqlite3/lib/src/wasm/wasm_interop.dart b/sqlite3/lib/src/wasm/wasm_interop.dart index 1f9d1d68..4f365c19 100644 --- a/sqlite3/lib/src/wasm/wasm_interop.dart +++ b/sqlite3/lib/src/wasm/wasm_interop.dart @@ -83,7 +83,13 @@ class WasmBindings { _sqlite3_stmt_readonly, _sqlite3_stmt_isexplain; - final JSFunction? _sqlite3_db_config, _sqlite3_initialize; + // The released WASM bundle only exposes functions referenced in this file. + // So, when we release a new version of `package:sqlite3` using additional + // functions, we can't assume that existing bundles also have those functions. + final JSFunction? _sqlite3_db_config, + _sqlite3_initialize, + _commit_hooks, + _rollback_hooks; final Global _sqlite3_temp_directory; @@ -161,6 +167,8 @@ class WasmBindings { _sqlite3_stmt_readonly = instance.functions['sqlite3_stmt_readonly']!, _sqlite3_db_config = instance.functions['dart_sqlite3_db_config_int'], _sqlite3_initialize = instance.functions['sqlite3_initialize'], + _commit_hooks = instance.functions['dart_sqlite3_commits'], + _rollback_hooks = instance.functions['dart_sqlite3_rollbacks'], _sqlite3_temp_directory = instance.globals['sqlite3_temp_directory']! // Note when adding new fields: We remove functions from the wasm module that @@ -288,6 +296,14 @@ class WasmBindings { _update_hooks.callReturningVoid2(db.toJS, id.toJS); } + void dart_sqlite3_commits(Pointer db, int id) { + return _commit_hooks?.callReturningVoid2(db.toJS, id.toJS); + } + + void dart_sqlite3_rollbacks(Pointer db, int id) { + return _rollback_hooks?.callReturningVoid2(db.toJS, id.toJS); + } + int sqlite3_exec(Pointer db, Pointer sql, Pointer callback, Pointer callbackArg, Pointer errorOut) { return _sqlite3_exec.callReturningInt5( @@ -743,6 +759,12 @@ class _InjectedValues { callbacks.installedUpdateHook ?.call(kind, tableName, JsBigInt(rowId).asDartInt); }).toJS, + 'function_commit_hook': ((int id) { + return callbacks.installedCommitHook?.call(); + }).toJS, + 'function_rollback_hook': ((int id) { + callbacks.installedRollbackHook?.call(); + }).toJS, } }; } @@ -759,6 +781,8 @@ class DartCallbacks { final Map openedFiles = {}; RawUpdateHook? installedUpdateHook; + RawCommitHook? installedCommitHook; + RawRollbackHook? installedRollbackHook; int register(RegisteredFunctionSet set) { final id = _id++; diff --git a/sqlite3/pubspec.yaml b/sqlite3/pubspec.yaml index f3fb98f0..70aaccd0 100644 --- a/sqlite3/pubspec.yaml +++ b/sqlite3/pubspec.yaml @@ -1,11 +1,11 @@ name: sqlite3 description: Provides lightweight yet convenient bindings to SQLite by using dart:ffi -version: 2.6.0 +version: 2.7.2 homepage: https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3 issue_tracker: https://github.com/simolus3/sqlite3.dart/issues environment: - sdk: ">=3.5.0 <4.0.0" + sdk: '>=3.5.0 <4.0.0' # This package supports all platforms listed below. platforms: diff --git a/sqlite3/test/common/database.dart b/sqlite3/test/common/database.dart index 1b314a42..c0573ff6 100644 --- a/sqlite3/test/common/database.dart +++ b/sqlite3/test/common/database.dart @@ -707,6 +707,141 @@ void testDatabase( }); }); + group('rollback stream', () { + setUp(() { + database.execute('CREATE TABLE tbl (a TEXT, b INT);'); + }); + + test('emits on rollback', () { + expect(database.rollbacks, emits(anything)); + + database.execute('BEGIN TRANSACTION;'); + database.execute("ROLLBACK;"); + }); + + test('emits on rollback after insert', () { + expect(database.rollbacks, emits(anything)); + + database.execute('BEGIN TRANSACTION;'); + database.execute("INSERT INTO tbl VALUES ('', 1);"); + database.execute("ROLLBACK;"); + }); + + test('emits on rollback after erroneous SQL', () { + expect(database.rollbacks, emits(anything)); + + database.execute('BEGIN TRANSACTION;'); + try { + database.execute('Erroneous SQL'); + } catch (_) { + // ignore + } + database.execute("ROLLBACK;"); + }); + + test('emits on rollback due to commit filter', () { + expect(database.rollbacks, emits(anything)); + database.commitFilter = expectAsync0(() => false); + + database.execute('begin'); + database.execute("INSERT INTO tbl VALUES ('', 1);"); + expect(() => database.execute('commit'), throwsSqlError(19, 531)); + }); + }); + + group('commit filter', () { + setUp(() { + database.execute('CREATE TABLE tbl (a TEXT, b INT);'); + }); + + test('explicit commits with always fails filter raises exception', () { + database.commitFilter = () => false; + expect(() { + database.execute('BEGIN TRANSACTION;'); + database.execute("INSERT INTO tbl VALUES ('', 1);"); + database.execute("COMMIT;"); + }, + throwsA(predicate((e) => + e.operation == 'executing' && + e.message.startsWith('constraint failed')))); + }); + + test('implicit commits with always fails filter raises exception', () { + database.commitFilter = () => false; + expect( + () => database.execute("INSERT INTO tbl VALUES ('', 1);"), + throwsA(predicate((e) => + e.operation == 'executing' && + e.message.startsWith('constraint failed')))); + }); + + test('side effects run on explicit commit', () { + var sideEffects = 0; + database.commitFilter = () { + ++sideEffects; + return true; + }; + + database.execute('BEGIN TRANSACTION;'); + database.execute("INSERT INTO tbl VALUES ('', 1);"); + database.execute("COMMIT;"); + // ensure the transaction committed correctly + expect(database.select('SELECT COUNT(*) AS c FROM tbl;').first['c'], + equals(1)); + // ensure side-effects ran + expect(sideEffects, equals(1)); + }); + + test('side effects run on implicit commit', () { + var sideEffects = 0; + database.commitFilter = () { + ++sideEffects; + return true; + }; + + database.execute("INSERT INTO tbl VALUES ('', 1);"); + // ensure the transaction committed correctly + expect(database.select('SELECT COUNT(*) AS c FROM tbl;').first['c'], + equals(1)); + // ensure side-effects ran + expect(sideEffects, equals(1)); + }); + }); + + group('commit stream', () { + setUp(() { + database.commitFilter = null; + database.execute('CREATE TABLE tbl (a TEXT, b INT);'); + }); + + test('emits on implicit commit', () { + expect(database.commits, emits(anything)); + database.execute("INSERT INTO tbl VALUES ('', 1);"); + }); + + test('emits on explicit commit', () { + expect(database.commits, emits(anything)); + + database.execute('BEGIN TRANSACTION;'); + database.execute("INSERT INTO tbl VALUES ('', 1);"); + database.execute("COMMIT;"); + }); + + test('does not emit on implicit commit with commitFilter false', () async { + expect(database.commits, neverEmits(anything)); + database.commitFilter = () => false; + try { + database.execute("INSERT INTO tbl VALUES ('', 1);"); + } on SqliteException { + // ignore + } + + // Disposing the database here so that the stream closes and neverEmits + // completes. + database.dispose(); + }); + }); + group('unicode handling', () { test('accents in statements', () { final table = 'télé'; // with accent diff --git a/sqlite3/test/source_code_test.dart b/sqlite3/test/source_code_test.dart index 7e94adbb..e03932c9 100644 --- a/sqlite3/test/source_code_test.dart +++ b/sqlite3/test/source_code_test.dart @@ -8,7 +8,7 @@ import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:test/test.dart'; void main() { - test('drift does not import legacy JS interop files', () { + test('does not import legacy JS interop files', () { final failures = <(String, String)>[]; void check(FileSystemEntity e) { diff --git a/sqlite3/test/wasm/sqlite3_test.dart b/sqlite3/test/wasm/sqlite3_test.dart index 6f091db3..b4aa448c 100644 --- a/sqlite3/test/wasm/sqlite3_test.dart +++ b/sqlite3/test/wasm/sqlite3_test.dart @@ -45,7 +45,7 @@ void main() { expect( version, isA() - .having((e) => e.libVersion, 'libVersion', startsWith('3.47')), + .having((e) => e.libVersion, 'libVersion', startsWith('3.48')), ); }); diff --git a/sqlite3/tool/check_compile_time_option.dart b/sqlite3/tool/check_compile_time_option.dart new file mode 100644 index 00000000..d2449fad --- /dev/null +++ b/sqlite3/tool/check_compile_time_option.dart @@ -0,0 +1,49 @@ +import 'dart:ffi'; +import 'dart:io'; + +import 'package:sqlite3/open.dart'; +import 'package:sqlite3/src/ffi/memory.dart'; +import 'package:sqlite3/src/ffi/sqlite3.g.dart'; + +/// Checks whether the loaded sqlite3 library includes a specific compile-time +/// option. +/// +/// This is used to validate automated tests for this package around sqlite3 +/// libraries compiled with different options (we want to make sure we're +/// the options are actually set). +/// +/// Usage: `dart run tool/check_compile_time_option.dart options...` +void main(List args) { + final getCompileOption = open + .openSqlite() + .lookupFunction('sqlite3_compileoption_get'); + + Iterable compileTimeOptions() sync* { + String? lastOption; + var i = 0; + + do { + final ptr = getCompileOption(i).cast(); + + if (!ptr.isNullPointer) { + lastOption = ptr.readString(); + yield lastOption; + } else { + lastOption = null; + } + + i++; + } while (lastOption != null); + } + + final expectedOptions = args.toSet(); + compileTimeOptions().forEach(expectedOptions.remove); + + if (expectedOptions.isNotEmpty) { + print('Following compile-time options where not set: $expectedOptions'); + exit(1); + } +} + +typedef GetNative = Pointer Function(Int32 n); +typedef GetDart = Pointer Function(int n); diff --git a/sqlite3_flutter_libs/CHANGELOG.md b/sqlite3_flutter_libs/CHANGELOG.md index 34943a5d..3bc78388 100644 --- a/sqlite3_flutter_libs/CHANGELOG.md +++ b/sqlite3_flutter_libs/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.29 + +- Upgrade sqlite to version `3.38.0`. + ## 0.5.28 - Upgrade sqlite to version `3.37.2`. diff --git a/sqlite3_flutter_libs/android/build.gradle b/sqlite3_flutter_libs/android/build.gradle index f8b2d8c5..7a48986d 100644 --- a/sqlite3_flutter_libs/android/build.gradle +++ b/sqlite3_flutter_libs/android/build.gradle @@ -32,5 +32,5 @@ android { } dependencies { - implementation 'eu.simonbinder:sqlite3-native-library:3.47.2' + implementation 'eu.simonbinder:sqlite3-native-library:3.48.0+1' } diff --git a/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs.podspec b/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs.podspec index 458c23b0..177caa16 100644 --- a/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs.podspec +++ b/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs.podspec @@ -20,7 +20,7 @@ Pod::Spec.new do |s| } s.swift_version = '5.0' - s.dependency 'sqlite3', '~> 3.47.2' + s.dependency 'sqlite3', '~> 3.48.0' s.dependency 'sqlite3/fts5' s.dependency 'sqlite3/perf-threadsafe' s.dependency 'sqlite3/rtree' diff --git a/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.resolved b/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.resolved index 82d9b552..13e7e698 100644 --- a/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.resolved +++ b/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.resolved @@ -3,10 +3,9 @@ { "identity" : "csqlite", "kind" : "remoteSourceControl", - "location" : "https://github.com/sbooth/CSQLite.git", + "location" : "https://github.com/simolus3/CSQLite.git", "state" : { - "revision" : "c10dbeae1ea2bee3acd571c47509d6aaed1e9b92", - "version" : "3.47.2" + "revision" : "7fa023c325a734f9dd1dcd30eeb65a620f745a2d" } } ], diff --git a/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.swift b/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.swift index 7f754825..ee0bd3f1 100644 --- a/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.swift +++ b/sqlite3_flutter_libs/darwin/sqlite3_flutter_libs/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "sqlite3-flutter-libs", type: .static, targets: ["sqlite3_flutter_libs"]) ], dependencies: [ - .package(url: "https://github.com/sbooth/CSQLite.git", exact: "3.47.2") + .package(url: "https://github.com/simolus3/CSQLite.git", revision: "7fa023c325a734f9dd1dcd30eeb65a620f745a2d") ], targets: [ .target( diff --git a/sqlite3_flutter_libs/linux/CMakeLists.txt b/sqlite3_flutter_libs/linux/CMakeLists.txt index f3bcdbee..08d3e111 100644 --- a/sqlite3_flutter_libs/linux/CMakeLists.txt +++ b/sqlite3_flutter_libs/linux/CMakeLists.txt @@ -13,13 +13,13 @@ if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") # We can't really ask users to use a cmake that recent, so there's this if here. FetchContent_Declare( sqlite3 - URL https://sqlite.org/2024/sqlite-autoconf-3470200.tar.gz + URL https://sqlite.org/2025/sqlite-autoconf-3480000.tar.gz DOWNLOAD_EXTRACT_TIMESTAMP NEW ) else() FetchContent_Declare( sqlite3 - URL https://sqlite.org/2024/sqlite-autoconf-3470200.tar.gz + URL https://sqlite.org/2025/sqlite-autoconf-3480000.tar.gz ) endif() FetchContent_MakeAvailable(sqlite3) diff --git a/sqlite3_flutter_libs/pubspec.yaml b/sqlite3_flutter_libs/pubspec.yaml index f60eb5e7..2dafffc1 100644 --- a/sqlite3_flutter_libs/pubspec.yaml +++ b/sqlite3_flutter_libs/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlite3_flutter_libs description: Flutter plugin to include native sqlite3 libraries with your app -version: 0.5.28 +version: 0.5.29 homepage: https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs issue_tracker: https://github.com/simolus3/sqlite3.dart/issues diff --git a/sqlite3_flutter_libs/windows/CMakeLists.txt b/sqlite3_flutter_libs/windows/CMakeLists.txt index a618ceb3..bfa7afd8 100644 --- a/sqlite3_flutter_libs/windows/CMakeLists.txt +++ b/sqlite3_flutter_libs/windows/CMakeLists.txt @@ -29,13 +29,13 @@ if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") # We can't really ask users to use a cmake that recent, so there's this if here. FetchContent_Declare( sqlite3 - URL https://sqlite.org/2024/sqlite-autoconf-3470200.tar.gz + URL https://sqlite.org/2025/sqlite-autoconf-3480000.tar.gz DOWNLOAD_EXTRACT_TIMESTAMP NEW ) else() FetchContent_Declare( sqlite3 - URL https://sqlite.org/2024/sqlite-autoconf-3470200.tar.gz + URL https://sqlite.org/2025/sqlite-autoconf-3480000.tar.gz ) endif() FetchContent_MakeAvailable(sqlite3) diff --git a/sqlite3_web/CHANGELOG.md b/sqlite3_web/CHANGELOG.md index 750d243c..d8b8ff59 100644 --- a/sqlite3_web/CHANGELOG.md +++ b/sqlite3_web/CHANGELOG.md @@ -1,6 +1,11 @@ +## 0.2.2 + +- Recover from worker errors at startup. + ## 0.2.1 - Add `WebSqlite.deleteDatabase` to delete databases. +- Support opening databases without workers. ## 0.2.0 diff --git a/sqlite3_web/lib/src/channel.dart b/sqlite3_web/lib/src/channel.dart index 6ece4fef..19c46e40 100644 --- a/sqlite3_web/lib/src/channel.dart +++ b/sqlite3_web/lib/src/channel.dart @@ -103,7 +103,9 @@ abstract class ProtocolChannel { final Map> _responses = {}; ProtocolChannel(this._channel) { - _channel.stream.listen(_handleIncoming); + _channel.stream.listen(_handleIncoming, onError: (e) { + close(e); + }); } Future get closed => _channel.sink.done; @@ -165,8 +167,14 @@ abstract class ProtocolChannel { void handleNotification(Notification notification); - Future close() async { + Future close([Object? error]) async { await _channel.sink.close(); + + for (final response in _responses.values) { + response.completeError( + StateError('Channel closed before receiving response: $error')); + } + _responses.clear(); } } diff --git a/sqlite3_web/lib/src/client.dart b/sqlite3_web/lib/src/client.dart index fc2effe3..50039a13 100644 --- a/sqlite3_web/lib/src/client.dart +++ b/sqlite3_web/lib/src/client.dart @@ -14,19 +14,27 @@ import 'protocol.dart'; import 'shared.dart'; import 'worker.dart'; +final class _CommitOrRollbackStream { + StreamSubscription? workerSubscription; + final StreamController controller = StreamController.broadcast(); +} + final class RemoteDatabase implements Database { final WorkerConnection connection; final int databaseId; var _isClosed = false; - StreamSubscription? _notificationSubscription; + StreamSubscription? _updateNotificationSubscription; final StreamController _updates = StreamController.broadcast(); + final _CommitOrRollbackStream _commits = _CommitOrRollbackStream(); + final _CommitOrRollbackStream _rollbacks = _CommitOrRollbackStream(); + RemoteDatabase({required this.connection, required this.databaseId}) { _updates ..onListen = (() { - _notificationSubscription ??= + _updateNotificationSubscription ??= connection.notifications.stream.listen((notification) { if (notification case UpdateNotification()) { if (notification.databaseId == databaseId) { @@ -34,22 +42,54 @@ final class RemoteDatabase implements Database { } } }); - - _requestUpdates(true); + _requestStreamUpdates(MessageType.updateRequest, true); }) ..onCancel = (() { - _notificationSubscription?.cancel(); - _notificationSubscription = null; + _updateNotificationSubscription?.cancel(); + _updateNotificationSubscription = null; + _requestStreamUpdates(MessageType.updateRequest, false); + }); - _requestUpdates(false); + _setupCommitOrRollbackStream( + _commits, MessageType.commitRequest, MessageType.notifyCommit); + _setupCommitOrRollbackStream( + _rollbacks, MessageType.rollbackRequest, MessageType.notifyRollback); + } + + void _setupCommitOrRollbackStream( + _CommitOrRollbackStream stream, + MessageType requestSubscription, + MessageType notificationType, + ) { + stream.controller + ..onListen = (() { + stream.workerSubscription ??= + connection.notifications.stream.listen((notification) { + if (notification case EmptyNotification(type: final type)) { + if (notification.databaseId == databaseId && + type == notificationType) { + stream.controller.add(null); + } + } + }); + _requestStreamUpdates(requestSubscription, true); + }) + ..onCancel = (() { + stream.workerSubscription?.cancel(); + stream.workerSubscription = null; + _requestStreamUpdates(requestSubscription, false); }); } - void _requestUpdates(bool sendUpdates) { + void _requestStreamUpdates(MessageType streamType, bool subscribe) { if (!_isClosed) { connection.sendRequest( - UpdateStreamRequest( - action: sendUpdates, requestId: 0, databaseId: databaseId), + StreamRequest( + type: streamType, + action: subscribe, + requestId: 0, // filled out in sendRequest + databaseId: databaseId, + ), MessageType.simpleSuccessResponse, ); } @@ -63,10 +103,14 @@ final class RemoteDatabase implements Database { @override Future dispose() async { _isClosed = true; - _updates.close(); - await connection.sendRequest( - CloseDatabase(requestId: 0, databaseId: databaseId), - MessageType.simpleSuccessResponse); + await ( + _updates.close(), + _rollbacks.controller.close(), + _commits.controller.close(), + connection.sendRequest( + CloseDatabase(requestId: 0, databaseId: databaseId), + MessageType.simpleSuccessResponse) + ).wait; } @override @@ -127,6 +171,12 @@ final class RemoteDatabase implements Database { @override Stream get updates => _updates.stream; + @override + Stream get rollbacks => _rollbacks.controller.stream; + + @override + Stream get commits => _commits.controller.stream; + @override Future get userVersion async { final result = await select('pragma user_version;'); @@ -227,15 +277,19 @@ final class WorkerConnection extends ProtocolChannel { final class DatabaseClient implements WebSqlite { final Uri workerUri; final Uri wasmUri; + final DatabaseController _localController; final Lock _startWorkersLock = Lock(); bool _startedWorkers = false; WorkerConnection? _connectionToDedicated; WorkerConnection? _connectionToShared; WorkerConnection? _connectionToDedicatedInShared; + + WorkerConnection? _connectionToLocal; + final Set _missingFeatures = {}; - DatabaseClient(this.workerUri, this.wasmUri); + DatabaseClient(this.workerUri, this.wasmUri, this._localController); Future startWorkers() { return _startWorkersLock.synchronized(() async { @@ -244,36 +298,53 @@ final class DatabaseClient implements WebSqlite { } _startedWorkers = true; - if (globalContext.has('Worker')) { - final dedicated = Worker( + await _startDedicated(); + await _startShared(); + }); + } + + Future _startDedicated() async { + if (globalContext.has('Worker')) { + final Worker dedicated; + try { + dedicated = Worker( workerUri.toString().toJS, WorkerOptions(name: 'sqlite3_worker'), ); - - final (endpoint, channel) = await createChannel(); - ConnectRequest(endpoint: endpoint, requestId: 0) - .sendToWorker(dedicated); - - _connectionToDedicated = - WorkerConnection(channel.injectErrorsFrom(dedicated)); - } else { + } on Object { _missingFeatures.add(MissingBrowserFeature.dedicatedWorkers); + return; } - if (globalContext.has('SharedWorker')) { - final shared = SharedWorker(workerUri.toString().toJS); - shared.port.start(); + final (endpoint, channel) = await createChannel(); + ConnectRequest(endpoint: endpoint, requestId: 0).sendToWorker(dedicated); - final (endpoint, channel) = await createChannel(); - ConnectRequest(endpoint: endpoint, requestId: 0) - .sendToPort(shared.port); + _connectionToDedicated = + WorkerConnection(channel.injectErrorsFrom(dedicated)); + } else { + _missingFeatures.add(MissingBrowserFeature.dedicatedWorkers); + } + } - _connectionToShared = - WorkerConnection(channel.injectErrorsFrom(shared)); - } else { + Future _startShared() async { + if (globalContext.has('SharedWorker')) { + final SharedWorker shared; + try { + shared = SharedWorker(workerUri.toString().toJS); + } on Object { _missingFeatures.add(MissingBrowserFeature.sharedWorkers); + return; } - }); + + shared.port.start(); + + final (endpoint, channel) = await createChannel(); + ConnectRequest(endpoint: endpoint, requestId: 0).sendToPort(shared.port); + + _connectionToShared = WorkerConnection(channel.injectErrorsFrom(shared)); + } else { + _missingFeatures.add(MissingBrowserFeature.sharedWorkers); + } } Future _connectToDedicatedInShared() { @@ -291,6 +362,22 @@ final class DatabaseClient implements WebSqlite { }); } + Future _connectToLocal() async { + return _startWorkersLock.synchronized(() async { + if (_connectionToLocal case final conn?) { + return conn; + } + + final local = Local(); + final (endpoint, channel) = await createChannel(); + WorkerRunner(_localController, environment: local).handleRequests(); + local + .addTopLevelMessage(ConnectRequest(requestId: 0, endpoint: endpoint)); + + return _connectionToLocal = WorkerConnection(channel); + }); + } + @override Future deleteDatabase( {required String name, required StorageMode storage}) async { @@ -310,16 +397,24 @@ final class DatabaseClient implements WebSqlite { final existing = {}; final available = <(StorageMode, AccessMode)>[]; + var workersReportedIndexedDbSupport = false; + + Future dedicatedCompatibilityCheck( + WorkerConnection connection) async { + SimpleSuccessResponse response; + try { + response = await connection.sendRequest( + CompatibilityCheck( + requestId: 0, + type: MessageType.dedicatedCompatibilityCheck, + databaseName: databaseName, + ), + MessageType.simpleSuccessResponse, + ); + } on Object { + return; + } - if (_connectionToDedicated case final connection?) { - final response = await connection.sendRequest( - CompatibilityCheck( - requestId: 0, - type: MessageType.dedicatedCompatibilityCheck, - databaseName: databaseName, - ), - MessageType.simpleSuccessResponse, - ); final result = CompatibilityResult.fromJS(response.response as JSObject); existing.addAll(result.existingDatabases); available.add((StorageMode.inMemory, AccessMode.throughDedicatedWorker)); @@ -327,6 +422,8 @@ final class DatabaseClient implements WebSqlite { if (result.canUseIndexedDb) { available .add((StorageMode.indexedDb, AccessMode.throughDedicatedWorker)); + + workersReportedIndexedDbSupport = true; } else { _missingFeatures.add(MissingBrowserFeature.indexedDb); } @@ -351,18 +448,25 @@ final class DatabaseClient implements WebSqlite { } } - if (_connectionToShared case final connection?) { - final response = await connection.sendRequest( - CompatibilityCheck( - requestId: 0, - type: MessageType.sharedCompatibilityCheck, - databaseName: databaseName, - ), - MessageType.simpleSuccessResponse, - ); + Future sharedCompatibilityCheck(WorkerConnection connection) async { + SimpleSuccessResponse response; + try { + response = await connection.sendRequest( + CompatibilityCheck( + requestId: 0, + type: MessageType.sharedCompatibilityCheck, + databaseName: databaseName, + ), + MessageType.simpleSuccessResponse, + ); + } on Object { + return; + } + final result = CompatibilityResult.fromJS(response.response as JSObject); if (result.canUseIndexedDb) { + workersReportedIndexedDbSupport = true; available.add((StorageMode.indexedDb, AccessMode.throughSharedWorker)); } else { _missingFeatures.add(MissingBrowserFeature.indexedDb); @@ -383,6 +487,19 @@ final class DatabaseClient implements WebSqlite { } } + if (_connectionToDedicated case final dedicated?) { + await dedicatedCompatibilityCheck(dedicated); + } + if (_connectionToShared case final shared?) { + await sharedCompatibilityCheck(shared); + } + + available.add((StorageMode.inMemory, AccessMode.inCurrentContext)); + if (workersReportedIndexedDbSupport || await checkIndexedDbSupport()) { + // If the workers can use IndexedDb, so can we. + available.add((StorageMode.indexedDb, AccessMode.inCurrentContext)); + } + return FeatureDetectionResult( missingFeatures: _missingFeatures.toList(), existingDatabases: existing.toList(), @@ -425,7 +542,8 @@ final class DatabaseClient implements WebSqlite { connection = _connectionToDedicated!; shared = false; case AccessMode.inCurrentContext: - throw UnimplementedError('todo: Open database locally'); + connection = await _connectToLocal(); + shared = false; } final response = await connection.sendRequest( diff --git a/sqlite3_web/lib/src/database.dart b/sqlite3_web/lib/src/database.dart index 6211a311..871bb897 100644 --- a/sqlite3_web/lib/src/database.dart +++ b/sqlite3_web/lib/src/database.dart @@ -9,6 +9,9 @@ import 'worker.dart'; /// A controller responsible for opening databases in the worker. abstract base class DatabaseController { + /// Constant base constructor. + const DatabaseController(); + /// Loads a wasm module from the given [uri] with the specified [headers]. Future loadWasmModule(Uri uri, {Map? headers}) async { @@ -50,6 +53,18 @@ abstract class Database { /// stream is active. Stream get updates; + /// A relayed stream of events triggered by rollbacks from the remote worker. + /// + /// Updates are only sent across worker channels while a subscription to this + /// stream is active. + Stream get rollbacks; + + /// A relayed stream of events triggered by commits from the remote worker. + /// + /// Updates are only sent across worker channels while a subscription to this + /// stream is active. + Stream get commits; + /// A future that resolves when the database is closed. /// /// Typically, databases are closed because [dispose] is called. For databases @@ -199,11 +214,17 @@ abstract class WebSqlite { /// Opens a [WebSqlite] instance by connecting to the given [worker] and /// using the [wasmModule] url to load sqlite3. + /// + /// The [controller] is used when connecting to a sqlite3 database without + /// using workers. It should typically be the same implementation as the one + /// passed to [workerEntrypoint]. static WebSqlite open({ required Uri worker, required Uri wasmModule, + DatabaseController? controller, }) { - return DatabaseClient(worker, wasmModule); + return DatabaseClient( + worker, wasmModule, controller ?? const _DefaultDatabaseController()); } /// Connects to an endpoint previously obtained with [Database.additionalConnection]. @@ -218,7 +239,37 @@ abstract class WebSqlite { /// was called. This limitation does not exist for databases hosted by shared /// workers. static Future connectToPort(SqliteWebEndpoint endpoint) { - final client = DatabaseClient(Uri.base, Uri.base); + final client = + DatabaseClient(Uri.base, Uri.base, const _DefaultDatabaseController()); return client.connectToExisting(endpoint); } } + +final class _DefaultDatabaseController extends DatabaseController { + const _DefaultDatabaseController(); + + @override + Future handleCustomRequest( + ClientConnection connection, JSAny? request) { + throw UnimplementedError(); + } + + @override + Future openDatabase( + WasmSqlite3 sqlite3, String path, String vfs) async { + return _DefaultWorkerDatabase(sqlite3.open(path, vfs: vfs)); + } +} + +final class _DefaultWorkerDatabase extends WorkerDatabase { + @override + final CommonDatabase database; + + _DefaultWorkerDatabase(this.database); + + @override + Future handleCustomRequest( + ClientConnection connection, JSAny? request) { + throw UnimplementedError(); + } +} diff --git a/sqlite3_web/lib/src/protocol.dart b/sqlite3_web/lib/src/protocol.dart index 90f58310..8e249476 100644 --- a/sqlite3_web/lib/src/protocol.dart +++ b/sqlite3_web/lib/src/protocol.dart @@ -25,7 +25,9 @@ enum MessageType { fileSystemFlush(), connect(), startFileSystemServer(), - updateRequest(), + updateRequest(), + rollbackRequest(), + commitRequest(), simpleSuccessResponse(), rowsResponse(), errorResponse(), @@ -33,6 +35,8 @@ enum MessageType { closeDatabase(), openAdditionalConnection(), notifyUpdate(), + notifyRollback(), + notifyCommit(), ; static final Map byName = values.asNameMap(); @@ -93,13 +97,19 @@ sealed class Message { MessageType.closeDatabase => CloseDatabase.deserialize(object), MessageType.openAdditionalConnection => OpenAdditonalConnection.deserialize(object), - MessageType.updateRequest => UpdateStreamRequest.deserialize(object), + MessageType.updateRequest || + MessageType.rollbackRequest || + MessageType.commitRequest => + StreamRequest.deserialize(type, object), MessageType.simpleSuccessResponse => SimpleSuccessResponse.deserialize(object), MessageType.endpointResponse => EndpointResponse.deserialize(object), MessageType.rowsResponse => RowsResponse.deserialize(object), MessageType.errorResponse => ErrorResponse.deserialize(object), MessageType.notifyUpdate => UpdateNotification.deserialize(object), + MessageType.notifyRollback || + MessageType.notifyCommit => + EmptyNotification.deserialize(type, object), }; } @@ -629,7 +639,7 @@ final class ErrorResponse extends Response { } } -final class UpdateStreamRequest extends Request { +final class StreamRequest extends Request { /// When true, the client is requesting to be informed about updates happening /// on the database identified by this request. /// @@ -637,22 +647,25 @@ final class UpdateStreamRequest extends Request { /// updates. final bool action; - UpdateStreamRequest( - {required this.action, - required super.requestId, - required super.databaseId}); + final MessageType type; - factory UpdateStreamRequest.deserialize(JSObject object) { - return UpdateStreamRequest( + StreamRequest({ + required this.type, + required this.action, + required super.requestId, + required super.databaseId, + }); + + factory StreamRequest.deserialize( + MessageType type, JSObject object) { + return StreamRequest( + type: type, action: (object[_UniqueFieldNames.action] as JSBoolean).toDart, requestId: object.requestId, databaseId: object.databaseId, ); } - @override - MessageType get type => MessageType.updateRequest; - @override void serialize(JSObject object, List transferred) { super.serialize(object, transferred); @@ -816,6 +829,30 @@ final class UpdateNotification extends Notification { } } +/// Used as a notification without a payload, e.g. for commit or rollback +/// events. +final class EmptyNotification extends Notification { + final int databaseId; + @override + final MessageType type; + + EmptyNotification({required this.type, required this.databaseId}); + + factory EmptyNotification.deserialize( + MessageType type, JSObject object) { + return EmptyNotification( + type: type, + databaseId: object.databaseId, + ); + } + + @override + void serialize(JSObject object, List transferred) { + super.serialize(object, transferred); + object[_UniqueFieldNames.databaseId] = databaseId.toJS; + } +} + extension on JSObject { int get requestId { return (this[_UniqueFieldNames.id] as JSNumber).toDartInt; diff --git a/sqlite3_web/lib/src/worker.dart b/sqlite3_web/lib/src/worker.dart index 84411bc8..13f3f147 100644 --- a/sqlite3_web/lib/src/worker.dart +++ b/sqlite3_web/lib/src/worker.dart @@ -105,18 +105,51 @@ final class Shared extends WorkerEnvironment { } } +/// A fake worker environment running in the same context as the main +/// application. +/// +/// This allows using a communication channel based on message ports regardless +/// of where the database is hosted. While that adds overhead, a local +/// environment is only used as a fallback if workers are unavailable. +final class Local extends WorkerEnvironment { + final StreamController _messages = StreamController(); + + Local() : super._(); + + void addTopLevelMessage(Message message) { + _messages.add(message); + } + + @override + Stream get topLevelRequests { + return _messages.stream; + } +} + +class _StreamState { + StreamSubscription? subscription; + + void cancel() { + subscription?.cancel(); + subscription = null; + } +} + /// A database opened by a client. final class _ConnectionDatabase { final DatabaseState database; final int id; - StreamSubscription? updates; + final _StreamState updates = _StreamState(); + final _StreamState rollbacks = _StreamState(); + final _StreamState commits = _StreamState(); _ConnectionDatabase(this.database, [int? id]) : id = id ?? database.id; Future close() async { - updates?.cancel(); - updates = null; + updates.cancel(); + rollbacks.cancel(); + commits.cancel(); await database.decrementRefCount(); } @@ -212,23 +245,34 @@ final class _ClientConnection extends ProtocolChannel return SimpleSuccessResponse( response: null, requestId: request.requestId); } - case UpdateStreamRequest(action: true): - if (database!.updates == null) { + case StreamRequest(action: true, type: MessageType.updateRequest): + return await subscribe(database!.updates, () async { final rawDatabase = await database.database.opened; - database.updates ??= rawDatabase.database.updates.listen((event) { + return rawDatabase.database.updates.listen((event) { sendNotification(UpdateNotification( update: event, databaseId: database.database.id)); }); - } - return SimpleSuccessResponse( - response: null, requestId: request.requestId); - case UpdateStreamRequest(action: false): - if (database!.updates != null) { - database.updates?.cancel(); - database.updates = null; - } - return SimpleSuccessResponse( - response: null, requestId: request.requestId); + }, request); + case StreamRequest(action: true, type: MessageType.commitRequest): + return await subscribe(database!.commits, () async { + final rawDatabase = await database.database.opened; + return rawDatabase.database.commits.listen((event) { + sendNotification(EmptyNotification( + type: MessageType.notifyCommit, + databaseId: database.database.id)); + }); + }, request); + case StreamRequest(action: true, type: MessageType.rollbackRequest): + return await subscribe(database!.rollbacks, () async { + final rawDatabase = await database.database.opened; + return rawDatabase.database.rollbacks.listen((event) { + sendNotification(EmptyNotification( + type: MessageType.notifyRollback, + databaseId: database.database.id)); + }); + }, request); + case StreamRequest(action: false): + return unsubscribe(database!, request); case OpenAdditonalConnection(): final database = _databaseFor(request)!.database; database.refCount++; @@ -282,9 +326,38 @@ final class _ClientConnection extends ProtocolChannel } finally { file.xClose(); } + case StreamRequest(action: true): + // Suppported stream requests handled in cases above. + return ErrorResponse( + message: 'Invalid stream subscription request', + requestId: request.requestId); } } + Future subscribe( + _StreamState state, + Future> Function() subscribeInternally, + StreamRequest request, + ) async { + state.subscription ??= await subscribeInternally(); + return SimpleSuccessResponse(response: null, requestId: request.requestId); + } + + Response unsubscribe(_ConnectionDatabase database, StreamRequest request) { + assert(!request.action); + final handler = switch (request.type) { + MessageType.updateRequest => database.updates, + MessageType.rollbackRequest => database.rollbacks, + MessageType.commitRequest => database.commits, + _ => throw AssertionError(), + }; + handler.cancel(); + + return SimpleSuccessResponse(response: null, requestId: request.requestId); + } + + void handleStreamCancelRequest() {} + @override void handleNotification(Notification notification) { // There aren't supposed to be any notifications from the client. @@ -429,7 +502,8 @@ final class WorkerRunner { /// a shared context that can use synchronous JS APIs. Worker? _innerWorker; - WorkerRunner(this._controller) : _environment = WorkerEnvironment(); + WorkerRunner(this._controller, {WorkerEnvironment? environment}) + : _environment = environment ?? WorkerEnvironment(); void handleRequests() async { await for (final message in _environment.topLevelRequests) { diff --git a/sqlite3_web/pubspec.yaml b/sqlite3_web/pubspec.yaml index e66cb69a..633337c9 100644 --- a/sqlite3_web/pubspec.yaml +++ b/sqlite3_web/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlite3_web description: Utilities to simplify accessing sqlite3 on the web, with automated feature detection. -version: 0.2.0 +version: 0.2.2 homepage: https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_web repository: https://github.com/simolus3/sqlite3.dart @@ -8,7 +8,7 @@ environment: sdk: ^3.3.0 dependencies: - sqlite3: ^2.4.3 + sqlite3: ^2.7.0 stream_channel: ^2.1.2 web: ^1.0.0 diff --git a/sqlite3_web/test/integration_test.dart b/sqlite3_web/test/integration_test.dart index 77b4ddc9..f2ad66b2 100644 --- a/sqlite3_web/test/integration_test.dart +++ b/sqlite3_web/test/integration_test.dart @@ -45,8 +45,13 @@ enum Browser { final available = <(StorageMode, AccessMode)>{}; for (final storage in StorageMode.values) { for (final access in AccessMode.values) { - if (access != AccessMode.inCurrentContext && - !unsupportedImplementations.contains((storage, access))) { + if (access == AccessMode.inCurrentContext && + storage == StorageMode.opfs) { + // OPFS access is only available in workers. + continue; + } + + if (!unsupportedImplementations.contains((storage, access))) { available.add((storage, access)); } } @@ -103,21 +108,36 @@ void main() { }); setUp(() async { - final rawDriver = await createDriver( - spec: browser.isChromium ? WebDriverSpec.JsonWire : WebDriverSpec.W3c, - uri: browser.driverUri, - desired: { - 'goog:chromeOptions': { - 'args': [ - '--headless=new', - '--disable-search-engine-choice-screen', - ], - }, - 'moz:firefoxOptions': { - 'args': ['-headless'] - }, - }, - ); + late WebDriver rawDriver; + for (var i = 0; i < 3; i++) { + try { + rawDriver = await createDriver( + spec: browser.isChromium + ? WebDriverSpec.JsonWire + : WebDriverSpec.W3c, + uri: browser.driverUri, + desired: { + 'goog:chromeOptions': { + 'args': [ + '--headless=new', + '--disable-search-engine-choice-screen', + ], + }, + 'moz:firefoxOptions': { + 'args': ['-headless'] + }, + }, + ); + break; + } on SocketException { + // webdriver server taking a bit longer to start up... + if (i == 2) { + rethrow; + } + + await Future.delayed(const Duration(milliseconds: 500)); + } + } // logs.get() isn't supported on Firefox if (browser != Browser.firefox) { @@ -154,13 +174,23 @@ void main() { await driver.assertFile(false); await driver.execute('CREATE TABLE foo (bar TEXT);'); - expect(await driver.countUpdateEvents(), 0); + var events = await driver.countEvents(); + expect(events.updates, 0); + expect(events.commits, 0); + expect(events.rollbacks, 0); await driver.execute("INSERT INTO foo (bar) VALUES ('hello');"); - expect(await driver.countUpdateEvents(), 1); + events = await driver.countEvents(); + expect(events.updates, 1); + expect(events.commits, 1); expect(await driver.assertFile(true), isPositive); await driver.flush(); + await driver.execute('begin'); + await driver.execute('rollback'); + events = await driver.countEvents(); + expect(events.rollbacks, 1); + if (storage != StorageMode.inMemory) { await driver.driver.refresh(); await driver.waitReady(); @@ -181,6 +211,16 @@ void main() { await driver.assertFile(false); } }); + + test('check large write and read', () async { + await driver.openDatabase( + implementation: (storage, access), + onlyOpenVfs: true, + ); + await driver.assertFile(false); + + await driver.checkReadWrite(); + }); } }); } diff --git a/sqlite3_web/tool/server.dart b/sqlite3_web/tool/server.dart index 35b7980f..24e3fcf4 100644 --- a/sqlite3_web/tool/server.dart +++ b/sqlite3_web/tool/server.dart @@ -169,10 +169,14 @@ class TestWebDriver { await driver.executeAsync("close('', arguments[0])", []); } - Future countUpdateEvents() async { + Future<({int updates, int commits, int rollbacks})> countEvents() async { final result = await driver.executeAsync('get_updates("", arguments[0])', []); - return result as int; + return ( + updates: result[0] as int, + commits: result[1] as int, + rollbacks: result[2] as int, + ); } Future execute(String sql) async { @@ -209,6 +213,14 @@ class TestWebDriver { } } + Future checkReadWrite() async { + final result = + await driver.executeAsync('check_read_write("", arguments[0])', []); + if (result != null) { + throw 'check_read_write() failed: $result'; + } + } + Future delete(StorageMode mode) async { await driver .executeAsync('delete_db(arguments[0], arguments[1])', [mode.name]); diff --git a/sqlite3_web/web/main.dart b/sqlite3_web/web/main.dart index 76d82d08..a6996747 100644 --- a/sqlite3_web/web/main.dart +++ b/sqlite3_web/web/main.dart @@ -1,9 +1,12 @@ import 'dart:convert'; -import 'dart:html'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; +import 'dart:typed_data'; import 'package:sqlite3_web/sqlite3_web.dart'; +import 'package:web/web.dart'; + +import 'controller.dart'; final sqlite3WasmUri = Uri.parse('sqlite3.wasm'); final workerUri = Uri.parse('worker.dart.js'); @@ -13,6 +16,8 @@ WebSqlite? webSqlite; Database? database; int updates = 0; +int commits = 0; +int rollbacks = 0; bool listeningForUpdates = false; void main() { @@ -23,7 +28,11 @@ void main() { }); _addCallbackForWebDriver('get_updates', (arg) async { listenForUpdates(); - return updates.toJS; + return [ + updates.toJS, + commits.toJS, + rollbacks.toJS, + ].toJS; }); _addCallbackForWebDriver('open', (arg) => _open(arg, false)); _addCallbackForWebDriver('open_only_vfs', (arg) => _open(arg, true)); @@ -64,6 +73,29 @@ void main() { .deleteDatabase(name: databaseName, storage: storage); return true.toJS; }); + _addCallbackForWebDriver('check_read_write', (arg) async { + final vfs = database!.fileSystem; + + final bytes = Uint8List(1024 * 128); + for (var i = 0; i < 128; i++) { + bytes[i * 1024] = i; + } + + await vfs.writeFile(FileType.database, bytes); + await vfs.flush(); + final result = await vfs.readFile(FileType.database); + if (result.length != bytes.length) { + return 'length mismatch'.toJS; + } + + for (var i = 0; i < 128; i++) { + if (result[i * 1024] != i) { + return 'mismatch, i=$i, byte ${result[i * 1024]}'.toJS; + } + } + + return null; + }); document.getElementById('selfcheck')?.onClick.listen((event) async { print('starting'); @@ -74,7 +106,7 @@ void main() { print('missing features: ${database.features.missingFeatures}'); }); - document.body!.children.add(DivElement()..id = 'ready'); + document.body!.appendChild(HTMLDivElement()..id = 'ready'); } void _addCallbackForWebDriver( @@ -103,6 +135,7 @@ WebSqlite initializeSqlite() { return webSqlite ??= WebSqlite.open( worker: workerUri, wasmModule: sqlite3WasmUri, + controller: ExampleController(isInWorker: false), ); } @@ -152,6 +185,8 @@ void listenForUpdates() { if (!listeningForUpdates) { listeningForUpdates = true; database!.updates.listen((_) => updates++); + database!.commits.listen((_) => commits++); + database!.rollbacks.listen((_) => rollbacks++); } }