Du kannst benutzerdefinierte SQLite-Erweiterungen in einer Capacitor-App verwenden, aber nur als native code, die in die App kompiliert werden: Auf iOS verlinkst du die Erweiterung statisch und rufst ihre Init-Funktion bei jeder Verbindung auf, auf Android bündelst du einen SQLite-Build, der das Laden von Erweiterungen unterstützt, und lieferst die Erweiterung als native Bibliothek pro CPU-Architektur aus. Dieser Guide erklärt, warum sich die Plattformen unterscheiden, liefert eine funktionierende Erweiterung in C und zeigt die exakten nativen Änderungen für @capgo/capacitor-fast-sql auf Capacitor 8.
Bevor du C schreibst, prüfe, ob du es wirklich brauchst. Viele „Extensions“ sind bereits eingebaut.
Was eine SQLite-Extension ist
Eine Extension ist nativer code, der neue Dinge an einer SQLite-Verbindung registriert: Skalarfunktionen (haversine_km(...)), Aggregatfunktionen, Collations, Virtual Tables und FTS5-Tokenizer. Jede Extension bietet einen Einstiegspunkt:
int sqlite3_<name>_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
SQLite ruft ihn einmal pro Verbindung auf. Alles, was du registrierst, existiert nur in dieser Verbindung.
Typische Gründe für eine eigene Extension in einer mobilen App:
- ein benutzerdefinierter FTS5-Tokenizer für eine Sprache, die der integrierte
unicode61Tokenizer schlecht verarbeitet, - eine Mathe- oder Geo-Funktion, die du brauchst für
WHEREundORDER BY(Distanz-Sortierung, Scoring), - eine Collation, die Namen so sortiert, wie deine Nutzer es erwarten,
- eine Vector-Search-Extension für On-Device-Retrieval.
Prüfe, was bereits eingebaut ist
Teste sie mit deinem Plugin auf den ältesten iOS- und Android-Versionen, die du unterstützt:
import { FastSQL } from '@capgo/capacitor-fast-sql';
const db = await FastSQL.connect({ database: 'probe' });
const [{ v }] = await db.query('SELECT sqlite_version() AS v');
const options = await db.query('PRAGMA compile_options');
const functions = await db.query('SELECT name FROM pragma_function_list ORDER BY name');
console.log(v, options.map((r) => r.compile_options));
Dinge, für die du oft keine Extension benötigst:
| Benötigt | Eingebaute Funktion | Prüfen |
|---|---|---|
| JSON in Spalten | JSON-Funktionen (json_extract, ->>) |
Seit SQLite 3.38 integriert. Ältere Android-System-SQLite-Versionen unterstützen sie eventuell nicht |
| Volltextsuche | FTS4, FTS5 | ENABLE_FTS4 / ENABLE_FTS5 in einem compile_options |
| Räumliche Bounding Boxes | R*Tree | ENABLE_RTREE |
sin, cos, sqrt, pow |
Mathematische Funktionen | ENABLE_MATH_FUNCTIONS, SQLite 3.35+ |
Wenn die Funktion auf jeder Zielplattform vorhanden ist, kannst du hier aufhören. Der Web-Build von Fast SQL verwendet das offizielle SQLite Wasm, das JSON, FTS5 und mathematische Funktionen enthält.
Warum iOS und Android unterschiedlich funktionieren
iOS. Das System libsqlite3 ist so aufgebaut, dass SQLITE_OMIT_LOAD_EXTENSION, also load_extension() nicht existiert. Die App Store-Richtlinien verbieten zudem das Herunterladen und Ausführen neuer code (Richtlinie 2.5.2), daher ist das Laden einer .dylib zur Laufzeit ist ohnehin vom Tisch. Die Erweiterung muss in die App-Binary kompiliert werden. Viele Tutorials nennen dann sqlite3_auto_extension() beim Start auf. Darauf sollte man sich nicht verlassen: Im iOS SDK, das mit Xcode ausgeliefert wird, sqlite3.h wird sie mit API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...)deklariert. Der zuverlässige Ansatz besteht darin, die Init-Funktion für jedes Connection-Handle direkt nach dem Öffnen aufzurufen.
Android. Das SQLite im Android-Framework (android.database.sqlite) bietet kein API zum Laden von Extensions. Man benötigt einen SQLite-Build, der in der App gebündelt ist und dies unterstützt. requery/sqlite-android ist ein Drop-in-Ersatz für die Framework-Klassen, liefert ein aktuelles SQLite (mit FTS5 und JSON) und unterstützt ladbare Erweiterungen über SQLiteCustomExtension. Deine Erweiterung wird zu einer .so Datei pro ABI innerhalb der APK.
Web. Der Standard-SQLite-Wasm-Build kann native code nicht laden. Du benötigst einen eigenen Wasm-Build von SQLite, in den deine Extension kompiliert ist.
Was die Plugins heute unterstützen
| iOS | Android | Web | |
|---|---|---|---|
@capgo/capacitor-fast-sql 8.2 |
Kein Hook, kein Patch nötig | Nutzt Framework-SQLite, Patch nötig | Standard SQLite Wasm |
@capacitor-community/sqlite 8.1 |
loadExtension() wird im JS API deklariert, aber nicht nativ implementiert |
Dasselbe | Nicht implementiert |
Aktuell bedeutet das Hinzufügen einer Erweiterung also einen kleinen nativen Patch. Es handelt sich um eine eingegrenzte Änderung, und Bun kann sie über Installationen hinweg beibehalten.
Schritt 1: Die Extension schreiben
Eine Distanzfunktion ist ein gutes Beispiel: nützlich, in sich geschlossen und leicht zu verifizieren. Speichere sie als geo.c:
#include <math.h>
#include <stddef.h>
#include "sqlite3ext.h"
SQLITE_EXTENSION_INIT1
static void haversine_km(sqlite3_context *ctx, int argc, sqlite3_value **argv) {
(void)argc;
for (int i = 0; i < 4; i++) {
if (sqlite3_value_type(argv[i]) == SQLITE_NULL) {
sqlite3_result_null(ctx);
return;
}
}
const double earth_radius_km = 6371.0088;
const double to_rad = M_PI / 180.0;
double lat1 = sqlite3_value_double(argv[0]) * to_rad;
double lon1 = sqlite3_value_double(argv[1]) * to_rad;
double lat2 = sqlite3_value_double(argv[2]) * to_rad;
double lon2 = sqlite3_value_double(argv[3]) * to_rad;
double dlat = lat2 - lat1;
double dlon = lon2 - lon1;
double a = sin(dlat / 2) * sin(dlat / 2) +
cos(lat1) * cos(lat2) * sin(dlon / 2) * sin(dlon / 2);
sqlite3_result_double(ctx, 2 * earth_radius_km * asin(sqrt(a)));
}
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
SQLITE_EXTENSION_INIT2(pApi);
(void)pzErrMsg;
return sqlite3_create_function_v2(
db, "haversine_km", 4,
SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS,
NULL, haversine_km, NULL, NULL, NULL);
}
Dieselbe Datei lässt sich auf beide Arten bauen:
- Als ladbare Extension (Android):
SQLITE_EXTENSION_INIT1/2leite jeden SQLite-Aufruf über denpApiPointer, den SQLite übergibt, sodass der.sonicht gegen SQLite linkt. - Statisch, mit
-DSQLITE_CORE(iOS): Die Makros werden zu No-ops und die Aufrufe gehen direkt an die SQLite-Library, gegen die die App linkt.
SQLITE_DETERMINISTIC ermöglicht es SQLite, die Funktion in Indizes auf Ausdrücken zu nutzen. SQLITE_INNOCUOUS markiert es als sicher für die Verwendung in Views und Triggern.
Ich habe diese Datei auf macOS auf beiden Wegen kompiliert: statisch mit -DSQLITE_CORE und aufgerufen sqlite3_geo_init(db, NULL, NULL) auf einer System-SQLite-Verbindung, sowie als ladbare Bibliothek über load_extension. Beide liefern etwa 343,6 km für die Strecke Paris–London.
Schritt 2: iOS, in die App kompilieren und pro Verbindung registrieren
Quellcode dem App-Target hinzufügen
- In Xcode ziehen Sie
geo.cin dieAppGruppe und aktiviere dasAppZiel. - Xcode bietet an, eine Bridging-Header-Datei zu erstellen. Akzeptiere dies oder erstelle
App/App-Bridging-Header.hund setze sie unter Build Settings > Swift Compiler - General > Objective-C Bridging Header. - Unter Build Phases > Compile Sources, doppelklicken
geo.cund füge das Flag für die Datei hinzu-DSQLITE_CORE.
Das iOS SDK enthält beides sqlite3.h und sqlite3ext.h, sodass keine zusätzlichen Header nötig sind.
App-Bridging-Header.h:
#include <sqlite3.h>
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
Fast SQL patchen, um einen Open-Hook bereitzustellen
Das Plugin öffnet Verbindungen innerhalb seines eigenen Moduls und braucht daher einen Hook, den deine App setzen kann. Bun kann eine Dependency patchen und den Patch bei jeder Installation erneut anwenden:
bun patch @capgo/capacitor-fast-sql
Bearbeiten node_modules/@capgo/capacitor-fast-sql/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift. Füge einen öffentlichen Hook auf oberster Ebene der Datei hinzu:
public enum FastSqlExtensions {
/// Called with every new connection handle right after it opens.
/// Return SQLITE_OK (0) on success.
public static var onOpen: ((OpaquePointer) -> Int32)?
}
Dann in init(...), nach dem Verschlüsselungsblock und vor try applyPerformanceOptions(...), nenne es:
if let hook = FastSqlExtensions.onOpen {
let rc = hook(db)
if rc != SQLITE_OK {
throw SQLError.openFailed(message: "Extension init failed with code \(rc)")
}
}
Patch speichern:
bun patch --commit node_modules/@capgo/capacitor-fast-sql
Bun schreibt eine Datei unter patches/ und erfasst sie in package.json unter patchedDependencies. Beide committen. Unser Guide zu wie man ein Capacitor-Plugin patcht deckt diesen Workflow im Detail ab.
Erweiterung beim Start registrieren
Unter ios/App/App/AppDelegate.swift, das Plugin-Modul importieren und den Hook in application(_:didFinishLaunchingWithOptions:):
import UIKit
import Capacitor
import CapgoCapacitorFastSql // CocoaPods module name. With SPM: import CapgoCapacitorFastSqlPlugin
// Inside the existing AppDelegate class:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FastSqlExtensions.onOpen = { db in
sqlite3_geo_init(db, nil, nil)
}
return true
}
Da der Hook im Öffnungspfad des Plugins ausgeführt wird, erhält jede Verbindung die Funktion, einschließlich solcher, die nach einem WebView-Reload geöffnet werden. Das Setzen in didFinishLaunching stellt sicher, dass er vor dem Aufruf durch JavaScript gesetzt ist connect().
SQLCipher-Builds. Wenn Sie das SQLCipher-Subspec des Plugins verwenden, gehört die Verbindung zum SQLite von SQLCipher, während Ihre -DSQLITE_CORE Die Datei kann auf Symbole der Systembibliothek verweisen. Teste diese Kombination vor dem Release explizit oder speichere erweiterungsabhängige Daten in einer unverschlüsselten Datenbank.
Schritt 3: Android, ein SQLite bündeln, das Erweiterungen laden kann
Erweiterung für jede ABI kompilieren
Du brauchst das Android NDK (Installation über den SDK Manager in Android Studio) und sqlite3ext.h sowie sqlite3.h vom SQLite-Amalgamation neben geo.c.
export NDK=$HOME/Library/Android/sdk/ndk/<version>
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64 # linux-x86_64 on Linux
export API=24 # your minSdkVersion
for pair in \
aarch64-linux-android:arm64-v8a \
armv7a-linux-androideabi:armeabi-v7a \
x86_64-linux-android:x86_64 \
i686-linux-android:x86
do
triple=${pair%%:*}
abi=${pair##*:}
mkdir -p android/app/src/main/jniLibs/$abi
$TOOLCHAIN/bin/clang --target=$triple$API \
-shared -fPIC -O2 \
-Wl,-z,max-page-size=16384 \
-o android/app/src/main/jniLibs/$abi/libgeo.so \
geo.c -lm
done
-Wl,-z,max-page-size=16384 passt die Bibliothek für Geräte mit 16-KB-Speicherseiten an. Google Play verlangt 16-KB-Unterstützung für Apps, die Android 15 oder höher targeten, siehe 16-KB-Seitengröße und Capacitor Plugins. Wenn deine App nur auf 64-Bit-Geräten läuft, kannst du die beiden 32-Bit-ABIs weglassen.
Die Bibliothek heißt libgeo.so, daher ist der Standard-Einstiegspunkt von SQLite sqlite3_geo_init, was der C-Funktion entspricht.
Native Bibliotheken bei der Installation extrahieren
Neuere Android Gradle Plugin-Versionen behalten .so Dateien im APK-Container komprimiert belassen, statt sie zu extrahieren. SQLite lädt Erweiterungen über einen Dateipfad, daher die Extraktion an android/app/build.gradle:
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
requery sqlite-android hinzufügen
Unter android/build.gradle (root), JitPack hinzufügen:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Fast SQL patchen, um es zu verwenden
Öffne die gleiche Bun-Patch-Session (bun patch @capgo/capacitor-fast-sql) und bearbeite zwei Dateien.
node_modules/@capgo/capacitor-fast-sql/android/build.gradle, in dependencies:
implementation 'com.github.requery:sqlite-android:3.49.0'
Verwende die neueste Version, die JitPack als für das Repository gebaut anzeigt.
node_modules/@capgo/capacitor-fast-sql/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java: Ersetze die Framework-Imports durch die von requery und öffne über eine Konfiguration, die die Erweiterungen mitführt.
// Replace:
// import android.database.sqlite.SQLiteDatabase;
// import android.database.sqlite.SQLiteStatement;
import io.requery.android.database.sqlite.SQLiteCustomExtension;
import io.requery.android.database.sqlite.SQLiteDatabase;
import io.requery.android.database.sqlite.SQLiteDatabaseConfiguration;
import io.requery.android.database.sqlite.SQLiteStatement;
public class SQLDatabase implements DatabaseConnection {
/** Set from MainActivity before the first connection opens. */
public static final java.util.List<SQLiteCustomExtension> extensions =
new java.util.concurrent.CopyOnWriteArrayList<>();
private SQLiteDatabase db;
private volatile boolean inTransaction = false;
public SQLDatabase(String path, boolean walMode, boolean performancePresets) throws Exception {
SQLiteDatabaseConfiguration config = new SQLiteDatabaseConfiguration(
path, SQLiteDatabase.CREATE_IF_NECESSARY);
config.customExtensions.addAll(extensions);
this.db = SQLiteDatabase.openDatabase(config, null, null);
SQLPerformanceConfig.apply(db::execSQL, walMode, performancePresets);
}
// ... the rest of the class stays as it is
}
requery-Klassen spiegeln das Framework API (rawQuery, compileStatement, execSQL, Transaktionen) wider, sodass der Rest der Datei unverändert kompiliert. android.database.Cursor bleibt unverändert. Committe den Patch mit bun patch --commit node_modules/@capgo/capacitor-fast-sql.
Dies ändert nur den unverschlüsselten Pfad. Verschlüsselte Verbindungen verwenden die eigenen Klassen von SQLCipher, die dieser Patch nicht berührt.
Erweiterung in MainActivity registrieren
package com.example.app;
import android.os.Bundle;
import app.capgo.capacitor.fastsql.SQLDatabase;
import com.getcapacitor.BridgeActivity;
import io.requery.android.database.sqlite.SQLiteCustomExtension;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
String path = getApplicationInfo().nativeLibraryDir + "/libgeo.so";
SQLDatabase.extensions.add(new SQLiteCustomExtension(path, "sqlite3_geo_init"));
super.onCreate(savedInstanceState);
}
}
Vorher registrieren super.onCreate damit die Liste gefüllt ist, bevor die WebView JavaScript ausführen kann.
Android ohne C: Java-Funktionen
Wenn du nur eine Skalarfunktion brauchst, kann requery eine in Java geschriebene registrieren – ohne NDK. Innerhalb des gepatchten SQLDatabase Konstruktors, nach dem Öffnen:
db.addFunction("haversine_km", 4, (args, result) -> {
double r = 6371.0088;
double lat1 = Math.toRadians(args.getDouble(0));
double lat2 = Math.toRadians(args.getDouble(2));
double dlat = lat2 - lat1;
double dlon = Math.toRadians(args.getDouble(3) - args.getDouble(1));
double a = Math.pow(Math.sin(dlat / 2), 2)
+ Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon / 2), 2);
result.set(2 * r * Math.asin(Math.sqrt(a)));
});
Das ist bei aufwendigen Abfragen langsamer als C, da jeder Aufruf über JNI läuft, aber es ist deutlich einfacher zu warten. FTS5-Tokenizer und virtuelle Tabellen brauchen weiterhin C.
Schritt 4: Nutzung via JavaScript
Danach bunx cap sync und ein erneuter Native Build:
import { FastSQL } from '@capgo/capacitor-fast-sql';
const db = await FastSQL.connect({ database: 'places' });
await db.execute(`CREATE TABLE IF NOT EXISTS places (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
lat REAL NOT NULL,
lon REAL NOT NULL
)`);
const nearby = await db.query(
`SELECT id, name, haversine_km(?, ?, lat, lon) AS km
FROM places
WHERE lat BETWEEN ? AND ? AND lon BETWEEN ? AND ?
ORDER BY km
LIMIT 20`,
[userLat, userLon, userLat - 0.5, userLat + 0.5, userLon - 0.7, userLon + 0.7],
);
Der BETWEEN box ermöglicht es SQLite, einen Index auf (lat, lon) zu nutzen, um Kandidaten einzugrenzen, bevor die Funktion die exakten Distanzen berechnet. Ohne ihn läuft die Funktion für jede Zeile.
JavaScript-Fallback für Web
Der Web-Build kann dein C code nicht laden, also erkennen und zurückfallen:
import { Capacitor } from '@capacitor/core';
export const hasGeoExtension = Capacitor.isNativePlatform();
// On the web: query the bounding box, then sort in JS with the same formula.
Eigene FTS5-Tokenisierer
Derselbe Mechanismus registriert FTS5-Tokenisierer. In deiner init-Funktion holst du den fts5_api Pointer mit dem SELECT fts5(?1) Muster aus der FTS5-Dokumentation, dann aufrufen xCreateTokenizer. Zwei zusätzliche Voraussetzungen:
- Die SQLite-Bibliothek muss FTS5 enthalten. Der Build von requery tut das. Auf iOS prüfen
PRAGMA compile_optionsfürENABLE_FTS5. - Jede Verbindung, die in die FTS-Tabelle schreibt oder daraus liest, muss den Tokenizer registriert haben, da FTS5 den Tokenizer-Namen in der Tabellendefinition speichert. Deshalb ist der Hook pro Verbindung entscheidend.
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');
Veröffentlichen und Wartung
- Neuer nativer Build erforderlich. Erweiterungen, Patches und Gradle-Änderungen sind nativ. Sie erfordern einen neuen App Store- und Play Store-Build. Capgo Build können beide im Cloud-Build erstellt und signiert werden. Seit April 2026 müssen iOS-Uploads mit Xcode 26 oder neuer erstellt werden.
- JavaScript kann weiterhin schnell iterieren. SQL, das aufruft
haversine_kmbefindet sich in deinem Web-Bundle und kann zusammen mit Live-Updatesausgeliefert werden, sofern der Native Build, der die Extension enthält, bereits installiert ist. Neue SQL-Features an eine Native-Version-Prüfung koppeln, falls Nutzer noch eine ältere Binary ausführen könnten. - Plugin-Updates. Wenn du Fast SQL aktualisierst, wendet Bun den Patch erneut an. Falls sich die gepatchten Zeilen verschoben haben, schlägt die Installation deutlich fehl, anstatt deinen Hook still zu entfernen. Erstelle den Patch für die neue Version neu.
Fehlerbehebung
no such function: haversine_km auf iOS. wurde der Hook nicht vor connect()gesetzt, oder die Datenbank wurde vor didFinishLaunching ran. Log inside the hook to confirm it is called.
Crash innerhalb von sqlite3_geo_init auf iOS. -DSQLITE_CORE fehlt, daher wurde die Datei als ladbare Extension kompiliert, die auf den pApi Zeiger verweist, der nil hier liegt. Füge das Flag pro Datei in Compile Sources hinzu.
no such function auf Android. Das gepatchte SQLDatabase importiert immer noch android.database.sqlite.SQLiteDatabase, oder die Extension wurde nach der ersten Verbindung zur Liste hinzugefügt. Logge SQLDatabase.extensions.size() beim Verbinden.
dlopen failed: library "…/libgeo.so" not found. Die Bibliothek fehlt für die Geräte-ABI (die meisten Handys sind arm64-v8a), oder die nativen Bibliotheken wurden nicht extrahiert. Prüfe useLegacyPackaging und untersuche die APK mit dem APK Analyzer in Android Studio.
undefined symbol: sqlite3_geo_init. Der an übergebene Entry-Point-Name SQLiteCustomExtension stimmt nicht mit der C-Funktion überein, oder die Funktion wurde deklariert static.
Play Console warnt vor 16-KB-Ausrichtung. Erstelle die .so Dateien neu mit -Wl,-z,max-page-size=16384 und einem aktuellen NDK, und aktualisiere requery auf eine Version, die mit 16-KB-Ausrichtung gebaut wurde.
Die Funktion funktioniert auf Android, aber nicht im iOS-Simulator. Make sure geo.c ein Mitglied des App Ziels ist, nicht nur im Dateibaum vorhanden.
Zusammenfassung
| Plattform | Wie die Extension eingebunden wird | Registrierungsort |
|---|---|---|
| iOS | In die App kompiliert mit -DSQLITE_CORE |
Plugin-Hook, der sqlite3_geo_init bei jeder neuen Verbindung aufruft |
| Android | .so pro ABI in jniLibs, geladen von requery sqlite-android |
SQLiteCustomExtension Liste wird beim Öffnen jeder Verbindung gelesen |
| Web | Nicht von Standard-SQLite-Wasm unterstützt | JavaScript-Fallback |
Beginnen Sie damit, die integrierten Funktionen zu prüfen. Wenn Sie dennoch eine Erweiterung benötigen, schreiben Sie sie einmalig in C, halten Sie die nativen Patches klein und testen Sie auf den ältesten unterstützten Geräten. Für die allgemeine Plugin-Einrichtung siehe So nutzt du SQLite in einer Capacitor App.