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++);
}
}