Sie können benutzerdefinierte SQLite-Erweiterungen in einer Capacitor-App verwenden, aber nur als native code-Dateien, die in die App kompiliert wurden: auf iOS verlinken Sie die Erweiterung statisch und rufen die init-Funktion auf jeder Verbindung auf, und auf Android bündeln Sie eine SQLite-Build, die Erweiterungen laden kann, und liefern die Erweiterung als native Bibliothek pro CPU-Architektur. Diese Anleitung erklärt, warum sich die Plattformen unterscheiden, gibt Ihnen eine funktionierende Erweiterung in C und zeigt die genauen native Änderungen für @capgo/capacitor-fast-sql auf Capacitor 8.
Bevor Sie C schreiben, überprüfen Sie, ob Sie es wirklich benötigen. Viele „Erweiterungen“ sind bereits in der App integriert.
Was ist eine SQLite-Erweiterung?
Eine Erweiterung ist eine native code-Komponente, die neue Dinge mit einer SQLite-Verbindung registriert: Skalarfunktionen (haversine_km(...)Aggregatefunktionen, Collationen, virtuelle Tabellen und FTS5-Tokenisierer. Jede Erweiterung bietet einen Einstiegspunkt:
int sqlite3_<name>_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
SQLite ruft sie einmal pro Verbindung auf. Alles, was Sie registrieren, existiert nur auf dieser Verbindung.
Häufige Gründe, eine in einer mobilen App zu schreiben:
- ein benutzerdefinierter FTS5-Tokenizer für eine Sprache die im integrierten
unicode61tokenizer verarbeitet schlecht. - a math or geo function you need in
WHEREandORDER BY(Distanzsortierung, Bewertung) - eine Kollation, die Namen so sortiert, wie Ihre Benutzer es erwarten.
- Ein Vektorsuch-Extension für die lokale Abfrage.
Überprüfen Sie, was bereits implementiert ist
Führen Sie diese durch Ihren Plugin auf den ältesten iOS- und Android-Versionen durch, die Sie unterstützen:
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 Sie oft keine Erweiterung benötigen:
| Brauchen | Eingebaute Funktion | Überprüfen |
|---|---|---|
| JSON in Spalten | JSON-Funktionen (json_extract, ->>) |
Seit SQLite 3.38 ist dies eingebaut. Ältere Android-System-SQLite-Versionen mögen sie nicht haben. |
| Volltextsuche | FTS4, FTS5 | ENABLE_FTS4 / ENABLE_FTS5 in compile_options |
| Raumgebundene Boxen | R*-Baum | ENABLE_RTREE |
sin, cos, sqrt, pow |
Mathematikfunktionen | ENABLE_MATH_FUNCTIONS, SQLite 3.35+ |
Wenn die Funktion auf jedem Ziel verfügbar ist, hierauf zu stoppen. Die Web-Build von Fast SQL verwendet die offizielle SQLite-Wasm, die JSON, FTS5 und mathematische Funktionen enthält.
Weshalb iOS und Android unterschiedlich funktionieren
iOS. Das System libsqlite3 is built with SQLITE_OMIT_LOAD_EXTENSION, so load_extension() existiert nicht. Die App-Store-Regeln verbieten auch das Herunterladen und Ausführen neuer code (Richtlinie 2.5.2), so dass die Laden eines .dylib Bei der Ausführung ist das ohnehin nicht möglich. Die Erweiterung muss in das App-Binär kompiliert werden. Viele Tutorials rufen dann sqlite3_auto_extension() zum Start. Verlassen Sie sich nicht darauf: in der iOS SDK , die mit Xcode geliefert wird, sqlite3.h erklärt es mit API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...)Die zuverlässige Vorgehensweise besteht darin, Ihre init-Funktion auf jedem Verbindungshandle direkt nach dem Öffnen aufzurufen.
Android. Der SQLite-Server im Android-Framework (android.database.sqlite) hat keine API , um Erweiterungen zu laden. Sie benötigen eine SQLite-Build, die mit Ihrer App verbunden ist, die dies tut. requery/sqlite-android ist ein einfacher Ersatz für die Framework-Klassen, liefert einen aktuellen SQLite-Server (mit FTS5 und JSON) und unterstützt ladenfähige Erweiterungen über SQLiteCustomExtensionIhre Erweiterung wird zu einem .so Datei pro ABI innerhalb der APK.
Web. Der Standard-SQLite-Wasm-Build kann keine nativen code laden. Sie benötigen eine benutzerdefinierte Wasm-Build von SQLite mit Ihrer Erweiterung, die in sie kompiliert wurde.
Was die Plugins heute unterstützen
| iOS | Android | Web | |
|---|---|---|---|
@capgo/capacitor-fast-sql 8.2 |
Keine Hook, Patcher erforderlich | Verwendet Framework-SQLite, Patcher erforderlich | Standard-SQLite Wasm |
@capacitor-community/sqlite 8.1 |
loadExtension() wird in der JS API deklariert, aber nicht nativ implementiert |
Gleich | Nicht implementiert |
Also, für den Moment bedeutet die Hinzufügung einer Erweiterung einen kleinen nativen Patcher. Es ist ein enthaltener Änderung, und Bun kann sie auf allen Installationsmedien beibehalten.
Schritt 1: Erweiterung schreiben
Auf Distanz funktioniert es gut: nützlich, eigenständig und leicht zu überprüfen. Speichern Sie es 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);
}
Das gleiche Datei baut beide Wege auf:
- Als eine ladenbare Erweiterung (Android):
SQLITE_EXTENSION_INIT1/2Routen Sie jeden SQLite-Aufruf über denpApiden SQLite in der Funktion übergibt, so dass.sokein Link gegen SQLite erfolgt. - Stattdessen, mit
-DSQLITE_CORE(iOS): Die Makros werden zu No-ops und Aufrufe gehen direkt an die SQLite-Bibliothek, die das App verlinkt.
SQLITE_DETERMINISTIC lässt SQLite die Funktion in Ausdrücken in Indexen verwenden. SQLITE_INNOCUOUS markiert es als sicher zum Einsatz in Ansichten und Trigger.
Ich habe diese Datei beide Wege auf macOS kompiliert: statisch mit -DSQLITE_CORE und rufen sqlite3_geo_init(db, NULL, NULL) auf einem System-SQLite-Verbindung und als eine ladenbare Bibliothek durch load_extension. Beide liefern etwa 343,6 km für Paris nach London.
Schritt 2: iOS, kompiliere in die App und registriere pro Verbindung
Fügen Sie die Quelle zur App-Ziel hinzufügen
- In Xcode, ziehen Sie
geo.cinsAppGruppe und markieren Sie dasAppZiel. - Xcode bietet eine Brückenkopfdatei an. Akzeptieren oder erstellen.
App/App-Bridging-Header.hBuild-Einstellungen > Swift-Compiler - Allgemein > Objective-C-Brückenkopfdatei Einstellungen > Swift-Compiler - Allgemein > Header für Objective-C. - In Build Phasen > Quellcode kompilieren, doppelklicken
geo.cund fügen Sie die pro-Datei-Flag hinzu-DSQLITE_CORE.
Das iOS SDK liefert sowohl sqlite3.h und sqlite3ext.hKeine zusätzlichen Header sind erforderlich.
App-Bridging-Header.h:
#include <sqlite3.h>
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
Patch Fast SQL, um einen offenen Hook freizugeben
Der Plugin öffnet Verbindungen innerhalb seines eigenen Moduls, daher benötigt es einen Hook, den Ihre App einstellen kann. Bun kann eine Abhängigkeit anpassen und den Patch bei jedem Installieren erneut anwenden:
bun patch @capgo/capacitor-fast-sql
Bearbeiten node_modules/@capgo/capacitor-fast-sql/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift. Add a public hook at the top level of the file:
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)?
}
Anschließend in init(...), nach dem Verschlüsselungsblock und vor try applyPerformanceOptions(...), nennen Sie es:
if let hook = FastSqlExtensions.onOpen {
let rc = hook(db)
if rc != SQLITE_OK {
throw SQLError.openFailed(message: "Extension init failed with code \(rc)")
}
}
Speichern Sie den Patch:
bun patch --commit node_modules/@capgo/capacitor-fast-sql
Bun schreibt ein Datei unter patches/ und registriert es in package.json unter patchedDependenciesCommit beide. Unsere Anleitung zum Patchen eines __CAPGO_KEEP_0__-Plugins Wie man einen Capacitor-Plugin anpasst Registrieren Sie die Erweiterung bei der Startphase
Beim Start registrieren Sie die Erweiterung.
In ios/App/App/AppDelegate.swift, importieren Sie das Plugin-Modul und setzen Sie 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 innerhalb des Plugins im offenen Pfad läuft, wird die Funktion jedem Verbindung zugewiesen, einschließlich solcher, die nach einem WebView-Neustart geöffnet werden. Die Zuweisung in didFinishLaunching stellt sicher, dass sie vor der Ausführung von JavaScript bereits vorliegt. connect().
SQLCipher wird erstellt. Wenn Sie die SQLCipher-Unterpakete des Plugins verwenden, gehört die Verbindung zum SQLite der SQLCipher, während Ihr -DSQLITE_CORE Das Datei kann auf die Symbol des Systembibliothek verweisen. Überprüfen Sie diese Combination explizit vor dem Versand, oder speichern Sie die abhängige Daten in einer unverschlüsselten Datenbank.
Schritt 3: Android, bündeln Sie eine SQLite-Datenbank, die Erweiterungen laden kann
Kompiliere die Erweiterung für jeden ABI
Sie benötigen das Android NDK (installieren Sie es aus dem Android Studio SDK Manager). sqlite3ext.h plus sqlite3.h Manager) und SQLite-Bibliotheksamalgam nächstbar 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 erfordert eine 16 KB-Unterstützung für Apps, die sich auf Android 15 und höher richten, siehe 16 KB-Seitengröße und Capacitor-Plugins. Wenn Ihre App nur auf 64-Bit-Geräte verteilt wird, können Sie die beiden 32-Bit-ABIs streichen.
Die Bibliothek heißt libgeo.so, daher ist der Standard-Einstiegspunkt von SQLite sqlite3_geo_init, der dem C-Funktion entspricht.
Native Bibliotheken extrahieren Sie bei der Installation
Neuere Versionen des Android Gradle Plugins behalten .so Dateien unkomprimiert innerhalb der APK, anstatt sie zu extrahieren. SQLite lädt Erweiterungen mit einem Dateipfad, daher bitten Sie um eine Extraktion in android/app/build.gradle:
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
Hinzufügen von requery sqlite-android
In android/build.gradle (root), fügen Sie JitPack ein:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Patch Fast SQL zu verwenden
Öffnen Sie das gleiche Bun Patchesitzung (bun patch @capgo/capacitor-fast-sql) und bearbeiten Sie zwei Dateien.
node_modules/@capgo/capacitor-fast-sql/android/build.gradle, in dependencies:
implementation 'com.github.requery:sqlite-android:3.49.0'
Verwenden Sie die neueste Version, die JitPack als für das Repository erstellt zeigt.
node_modules/@capgo/capacitor-fast-sql/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java: Tauschen Sie die Framework-Importe für requery’s und öffnen Sie sie über eine Konfiguration, die die Erweiterungen trägt.
// 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’s Klassen spiegeln die Framework API (rawQuery, compileStatement, execSQL, Transaktionen), so bleibt der Rest der Datei unverändert. android.database.Cursor bleibt wie es ist. Committe das Patchesitzung mit bun patch --commit node_modules/@capgo/capacitor-fast-sql.
Dies ändert nur den unverschlüsselten Pfad. Verschlüsselte Verbindungen verwenden SQLCipher’s eigene Klassen, die dieses Patches nicht berührt.
Registrieren Sie die Erweiterung in MainActivity
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);
}
}
Registrieren Sie vor super.onCreate so die Liste wird vorher gefüllt, bevor die WebView JavaScript ausführen kann.
Java-Funktionen ohne C: Android
Wenn Sie nur eine Skalarfunktion benötigen, kann requery eine in Java geschriebene registrieren, ohne dass ein NDK erforderlich ist. Im gepatchten SQLDatabase Konstruktor, nachdem Sie geöffnet haben:
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)));
});
Dies ist langsamer als C für schwere Abfragen, da jeder Aufruf die JNI überquert, aber es ist viel einfacher zu pflegen. FTS5-Tokenisierer und virtuelle Tabellen benötigen immer noch C.
Schritt 4: Verwenden Sie es aus JavaScript
After bunx cap sync und eine native Rekonstruktion:
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],
);
The BETWEEN Die Box ermöglicht es SQLite, einen Index auf (lat, lon) um Kandidaten vor der Funktion zu verengen, bevor sie genaue Entfernungen berechnet. Ohne es läuft die Funktion auf jeder Zeile.
Eine JavaScript-Fallback für die Webanwendung
Die Web-Ausgabe kann Ihren C code nicht laden, daher detektieren Sie ihn und fahren Sie zurück:
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.
Benutzerdefinierte FTS5-Tokenisierer
Das gleiche Mechanismus registriert FTS5-Tokenisierer. In Ihrer Initialisierungsfunktion erhalten Sie den fts5_api Zeiger mit dem SELECT fts5(?1) Muster aus der FTS5-Dokumentation , rufen Sie dann xCreateTokenizerzwei zusätzliche Anforderungen:
- Die SQLite-Bibliothek muss FTS5 enthalten. requery’s Build enthält es. Auf iOS überprüfen Sie
PRAGMA compile_optionsfürENABLE_FTS5. - Jede Verbindung, die in das FTS-Table schreibt oder ausliest, muss den Tokenisierer registrieren, weil FTS5 den Tokenisierer-Namen in der Tabelle definiert. Dies ist der Grund, warum die pro-Verbindung-Hook wichtig ist.
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');
Versand und Wartung
- Native Rebuild erforderlich. Erweiterungen, Patches und Gradle-Änderungen sind native. Sie benötigen eine neue App-Store- und Play-Store-Version. Capgo Build Man kann sowohl im Cloud bereitstellen als auch signieren. Seit April 2026 müssen iOS-Uploads mit Xcode 26 oder höher erstellt werden.
- JavaScript kann immer noch schnell laufen. SQL, das aufruft
haversine_kmlebt in Ihrem Web-Bundle und kann mit live Updatesversendet werden, solange die native App, die die Erweiterung enthält, bereits installiert ist. Neue SQL-Abfragen werden auf eine native Version überprüft, wenn Benutzer noch eine ältere Version ausführen können. - Plugin-Updates. Wenn Sie Fast SQL aktualisieren, wird Bun die Patches erneut angewendet. Wenn die gepatchten Zeilen verschoben wurden, erfolgt die Installation lautstark anstatt stillschweigend, um Ihre Hook zu entfernen. Erstellen Sie den Patch auf der neuen Version neu.
Fehlersuche
no such function: haversine_km auf iOS. Der Hook wurde vorher nicht gesetzt. connect()oder die Datenbank wurde vor der Anwendung geöffnet. didFinishLaunching prüfen. Logge in der Hook, um sicherzustellen, dass er aufgerufen wird.
Crasht innerhalb sqlite3_geo_init auf iOS. -DSQLITE_CORE fehlt, sodass das kompilierte Datei als eine ladenbare Erweiterung, die auf den pApi Zeiger verweist, kompiliert wird, der nil hier ist. Füge die pro-Datei-Flag in Compile Sources hinzu.
no such function auf Android. Der gepatchte SQLDatabase importiert immer noch android.database.sqlite.SQLiteDatabaseoder die Erweiterung wurde der Liste nach der ersten Verbindung hinzugefügt. Log SQLDatabase.extensions.size() bei der Verbindung
dlopen failed: library "…/libgeo.so" not found. Die Bibliothek fehlt für das Geräte-ABI (die meisten Smartphones sind arm64-v8aoder native Bibliotheken wurden nicht extrahiert. Überprüfen useLegacyPackaging und inspizieren Sie das APK mit Android Studios APK-Analysewerkzeug.
undefined symbol: sqlite3_geo_init. Der Eingangspunktname, der an SQLiteCustomExtension passt nicht zum C-Funktion, oder die Funktion wurde deklariert static.
Das Play-Console warnt vor der 16-Byte-Alignment. Rebuild die .so Dateien mit -Wl,-z,max-page-size=16384 und einem aktuellen NDK, und aktualisieren Sie requery auf eine Version, die mit 16-Byte-Alignment erstellt wurde.
Die Funktion funktioniert auf Android, aber nicht im iOS-Simulator. Make sure geo.c ist ein Mitglied der App Zielgruppe, nicht nur in der Datei-Struktur vorhanden ist.
Zusammenfassung
| Plattform | Wie die Erweiterung in das | Wo sie registriert ist |
|---|---|---|
| iOS | Mit der App kompiliert wird -DSQLITE_CORE |
Plugin-Hook, der auf jeden neuen Verbindung sqlite3_geo_init bei jeder neuen Verbindung |
| Android | .so per ABI in jniLibs, geladen durch requery sqlite-android |
SQLiteCustomExtension Liste wird bei jedem Verbindungsaufbau gelesen |
| Web | Von der Standard SQLite Wasm nicht unterstützt | JavaScript-Fallback |
Start by checking the built-in features. If you still need an extension, write it once in C, keep the native patches small, and test on the oldest devices you support. For the general plugin setup, see Wie Sie SQLite in einer Capacitor-App verwenden.