SQLite Android AAR Version Mismatch: sqlite_version() Reports 3.46.0 Instead of 3.46.1
Discrepancy Between sqlite-android-3460100.aar Implementation and Runtime Version Reporting
1. Core Symptom: SQLite Version Mismatch in Android Build Pipeline
The fundamental conflict arises when an Android application built with the official SQLite Android Archive (AAR) file sqlite-android-3460100.aar
reports version 3.46.0 via SELECT sqlite_version()
at runtime, despite the AAR’s metadata and included binaries containing 3.46.1 identifiers. This occurs specifically in a Flutter environment where the sqlite3_flutter_libs
package introduces a transitive dependency conflict through eu.simonbinder:sqlite3-native-library:3.46.0+1
. The Android build system prioritizes this older native library over the explicitly included AAR file, causing version string discrepancies between the developer’s expectations (based on the AAR’s versioning) and actual runtime behavior.
Key technical relationships:
- AAR File Contents: Contains ARM64/x86 JNI libraries (
libsqliteX.so
) with embedded version strings 3.46.1 in their metadata sections - Gradle Dependency Tree: Resolves conflicting native library versions through default version matching rules
- Flutter Plugin Architecture:
sqlite3_flutter_libs
injects precompiled SQLite binaries via Maven coordinates, overriding local AAR inclusions - APK Packaging: Merges multiple JNI implementations from different sources without version validation checks
The mismatch manifests through three observable layers:
- Build Configuration: Explicit AAR dependency declaration in
app/build.gradle
- Native Binary Inspection: Verified SHA3-256 hash matching SQLite.org’s signed distribution
- Runtime Query Execution: Inconsistent version reporting across platforms (iOS vs Android)
2. Underlying Conflict: Native Library Resolution Hierarchy in Multi-Dependency Environments
2.1. Gradle’s Dependency Resolution Precedence Rules
Android’s build system follows strict priority rules when merging dependencies:
- Transitive Dependencies from plugin-injected Maven coordinates
- Local .AAR Files in project’s
lib/
directory - Precompiled NDK Libraries in
jniLibs/
directories - Explicit Version Declarations in
build.gradle
In this scenario:
sqlite3_flutter_libs
declareseu.simonbinder:sqlite3-native-library:3.46.0+1
as a transitive dependency- Gradle prioritizes this Maven-hosted artifact over the local
sqlite-android-3460100.aar
- The older 3.46.0 binaries get merged into the final APK’s
lib/arm64-v8a
folder
2.2. Flutter FFI Initialization Sequence
The sqfliteFfiInit()
method in Dart code triggers SQLite’s native binding process:
- Probes
android.jar
for system SQLite implementation (typically outdated) - Falls back to plugin-provided JNI libraries via
android.database.sqlite
- Loads first compatible
.so
file found in APK’s native library directories
With conflicting libraries present:
- The build system merges both 3.46.0 (from Maven) and 3.46.1 (from AAR) into intermediate build artifacts
- Android’s APK packaging process retains duplicate .so files with identical names but different contents
- Dynamic linker loads whichever library appears first in the filesystem order (nondeterministic without explicit ordering)
2.3. Version String Embedding in SQLite Binaries
SQLite’s version information exists in multiple locations within compiled binaries:
- Header Metadata:
#define SQLITE_VERSION "3.46.1"
- Compile-Time Constants:
sqlite3_libversion_number()
returns3046001
- Export Table:
sqlite3_version[]
char array populated at build time
When multiple implementations coexist:
SELECT sqlite_version()
reflects the actually loaded library’s header metadata- Binary inspection via
strings
command shows all embedded versions present in APK - No runtime checks prevent mixing major/minor versions across dependencies
3. Resolution Protocol: Enforcing AAR Priority in Multi-Source Builds
3.1. Dependency Tree Analysis and Conflict Resolution
Step 1: Generate Gradle Dependency Report
./gradlew :app:dependencies --configuration releaseRuntimeClasspath > deps.txt
Search for sqlite3-native-library
in output:
+--- eu.simonbinder:sqlite3-native-library:3.46.0+1
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.20
| \--- com.almworks.sqlite4java:libsqlite:0.6
Step 2: Exclude Transitive Dependency
Modify app/build.gradle
:
dependencies {
implementation files('lib/sqlite-android-3460100.aar')
implementation('com.tekartik.sqflite:sqflite') {
exclude group: 'eu.simonbinder', module: 'sqlite3-native-library'
}
}
Step 3: Verify Clean Dependency Tree
Rerun dependency report to confirm removal of 3.46.0+1
references.
3.2. APK Binary Validation via Reverse Engineering
Step 1: Extract APK’s Native Libraries
unzip -q app-release.apk -d apk_contents
find apk_contents -name 'libsqlite*.so' -exec strings {} \; | grep '3\.46\.'
Expected Output:
3.46.1
3.46.1
Step 2: Cross-Reference Library Checksums
Compare extracted .so
files against AAR contents:
# Extract AAR
unzip sqlite-android-3460100.aar -d aar_contents
# Compare ARM64 library
diff aar_contents/jni/arm64-v8a/libsqliteX.so apk_contents/lib/arm64-v8a/libsqliteX.so
3.3. Build Configuration Hardening
Option 1: Enforce AAR Priority via ResolutionStrategy
Add to app/build.gradle
:
configurations.all {
resolutionStrategy {
force files('lib/sqlite-android-3460100.aar')
preferProjectModules()
}
}
Option 2: Explicit Native Library Source Sets
Override Flutter plugin’s default JNI inclusion:
android {
sourceSets {
main {
jniLibs.srcDirs = ['libs/sqlite-android-3460100/jni']
}
}
}
3.4. Runtime Validation Layer
Implement startup check in Dart code:
Future<void> validateSqliteVersion() async {
final result = await db.rawQuery('SELECT sqlite_version(), sqlite_source_id()');
final version = result.first['sqlite_version'];
final sourceId = result.first['sqlite_source_id'];
assert(version == '3.46.1',
'SQLite version mismatch: Expected 3.46.1, found $version. Source ID: $sourceId');
}
Key Validation Points:
- Compare both
sqlite_version()
andsqlite_source_id()
against official build manifests - Halt initialization if version mismatch detected
- Log full
sqlite3_config()
settings during debug builds
3.5. Flutter Plugin Configuration Audit
Review pubspec.yaml
for implicit SQLite dependencies:
dependencies:
sqflite: ^2.2.8 # Might bring in sqlite3_flutter_libs
Mitigation Strategies:
- Pin Exact Versions:
sqlite3_flutter_libs: 0.5.24 # Avoid 'any' version specifier
- Use Dependency Overrides:
dependency_overrides:
sqlite3_flutter_libs:
path: ../patched_sqlite_flutter
3.6. Advanced: Custom SQLite Loading with JNI Shim
For critical applications requiring absolute version control:
Step 1: Disable Default JNI Loading
public class CustomSQLiteLoader {
static {
System.loadLibrary("custom_sqliteX");
}
}
Step 2: Embed AAR Libraries as Custom JNI
- Rename
libsqliteX.so
tolibcustom_sqliteX.so
- Place in
app/src/main/jniLibs/${ABI}/
- Modify Flutter plugin initialization to use custom loader
3.7. Continuous Integration Safeguards
Implement CI pipeline checks:
- name: Verify SQLite Version
run: |
adb shell "dumpsys package com.example.app | grep versionName"
adb shell "run-as com.example.app cat /data/data/com.example.app/databases/test.db | sqlite3 'SELECT sqlite_version();'"
# Fail build if output != 3.46.1
4. Post-Resolution Monitoring
After applying fixes:
- APK Diff Analysis: Use
apkanalyzer
to confirm native library changes - Performance Benchmarking: Compare
PRAGMA compile_options;
outputs - Crash Reporting: Monitor for
SQLITE_MISMATCH
errors in production
This comprehensive approach addresses dependency conflicts at the build system level, verifies binary integrity through multiple validation layers, and institutes safeguards against regression through CI/CD integration.