Saltare al contenuto principale

Come utilizzare estensioni SQLite personalizzate in Capacitor

Carica estensioni SQLite personalizzate in un'app Capacitor: limiti iOS e Android, un'estensione C che costruisce per entrambi e come registrare per connessione.

Crediti dell'articolo

Martin Donadieu

Scrittore

Valeria

Recensore

Jordan

Editore

Come utilizzare estensioni SQLite personalizzate in Capacitor

È possibile utilizzare estensioni SQLite personalizzate in un'app Capacitor, ma solo come code nativi compilati nell'app: su iOS si collega staticamente l'estensione e si chiama la funzione di inizializzazione su ogni connessione, e su Android si compila un SQLite che supporta il caricamento di estensioni e si spedisce l'estensione come libreria nativa per ogni architettura di processore. Questo guide spiega perché le piattaforme differiscono, fornisce un'estensione di lavoro in C e mostra le modifiche native esatte per @capgo/capacitor-fast-sql su Capacitor 8.

Prima di scrivere C, verificare che si abbia effettivamente bisogno di esso. Molte “estensioni” sono già integrate.

What è un'estensione SQLite

Un'estensione è un codice nativo code che registra nuove cose con una connessione SQLite: funzioni scalari (haversine_km(...)), funzioni aggregate, ordinamenti, tabelle virtuali e tokenizzatori FTS5. Ogni estensione esporre un punto di ingresso:

int sqlite3_<name>_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);

SQLite lo chiama una volta per connessione. Tutto ciò che registri esiste solo su quella connessione.

Motivi tipici per scriverne in un'app mobile:

  • un tokenizzatore FTS5 personalizzato per una lingua la predefinita unicode61 il tokenizzatore gestisce male
  • a math or geo function you need in WHERE e ORDER BY (distante ordinamento, punteggio)
  • una collazione che ordina i nomi come i tuoi utenti si aspettano
  • una estensione di ricerca vettoriale per la ricerca in dispositivo.

Controlla cosa è già costruito

Esegui questi attraverso il tuo plugin sulle versioni iOS e Android più vecchie che supporti:

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

Cose che spesso non hai bisogno di un'estensione:

Hai bisogno Caratteristica integrata Controlla
JSON nelle colonne Funzioni JSON (json_extract, ->>) Integrato dal SQLite 3.38. I sistemi Android più vecchi di SQLite possono mancarli
Ricerca testo completo FTS4, FTS5 ENABLE_FTS4 / ENABLE_FTS5 in compile_options
Boxe spaziali di confine R*Tree ENABLE_RTREE
sin, cos, sqrt, pow Funzioni matematiche ENABLE_MATH_FUNCTIONS, SQLite 3.35+

Se la funzione è disponibile su ogni piattaforma, fermati qui. La versione web di Fast SQL utilizza l'SQLite Wasm ufficiale, che include JSON, FTS5 e funzioni matematiche.

Perché iOS e Android funzionano in modo diverso

iOS. Il sistema libsqlite3 è costruito con SQLITE_OMIT_LOAD_EXTENSION, quindi load_extension() non esiste. Le regole dell'App Store vietano anche il download e l'esecuzione di nuovi code (linee guida 2.5.2), quindi caricare un .dylib Al runtime non è comunque una possibilità. L'estensione deve essere compilata nel binario dell'app. Molti tutorial poi chiamano sqlite3_auto_extension() al lancio. Non contare su di esso: nell'SDK iOS che viene con Xcode, sqlite3.h lo dichiara con API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...). L'approccio affidabile è chiamare la tua funzione di inizializzazione su ogni gestore di connessione subito dopo che si apre.

Android. Il SQLite all'interno del framework Android ('android.database.sqliteNon ha API per caricare gli estensioni. Serve un build SQLite integrato con l'applicazione che lo faccia. requery/sqlite-android è una sostituzione a gettone per le classi del framework, invia una SQLite corrente (con FTS5 e JSON), e supporta gli estensioni caricabili attraverso SQLiteCustomExtension. La tua estensione diventa un .so file per ABI all'interno dell'APK.

Web. La SQLite Wasm di serie non può caricare code nativi. Avresti bisogno di una costruzione Wasm personalizzata di SQLite con la tua estensione compilata in essa.

Cosa supportano oggi i plugin

iOS Android Web
@capgo/capacitor-fast-sql 8.2 Nessuna funzione, necessario un patch Utilizza SQLite del framework, necessario un patch SQLite Wasm di serie
@capacitor-community/sqlite 8.1 loadExtension() è dichiarato nel JS API ma non è implementato nativamente Lo stesso Non implementato

Quindi, per ora, aggiungere un'estensione significa un piccolo patch nativo. È un cambiamento contenuto, e Bun può mantenerlo applicato durante le installazioni.

Passo 1: scrivi l'estensione

A una funzione di distanza è un buon esempio: utile, autonomo e facile da verificare. Salvalo come 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);
}

Lo stesso file costruisce entrambe le vie:

  • Come un'estensione caricabile (Android): SQLITE_EXTENSION_INIT1/2 indirizza ogni chiamata SQLite attraverso il pApi pointer SQLite passa in, quindi il .so non collega a SQLite.
  • Staticamente, con -DSQLITE_CORE (iOS): i macro diventano no-op e le chiamate vanno direttamente alla libreria SQLite con cui l'app è collegata.

SQLITE_DETERMINISTIC consente a SQLite di utilizzare la funzione nelle chiavi di espressione. SQLITE_INNOCUOUS la segnala come sicura per l'uso in viste e trigger.

Io ho compilato questo file entrambe le vie su macOS: staticamente con -DSQLITE_CORE e chiamato sqlite3_geo_init(db, NULL, NULL) su una connessione SQLite del sistema, e come libreria caricabile attraverso load_extension. Entrambi restituiscono circa 343,6 km per Parigi a Londra.

Passo 2: iOS, compila nell'app e registra per connessione

Aggiungi la sorgente al target dell'app

  1. Aggiungi la fonte all'obiettivo dell'app geo.c dentro App gruppo e seleziona il App target.
  2. Xcode offers to create a bridging header. Accept, or create App/App-Bridging-Header.h Xcode offre di creare un file di intestazione di bridging. Accetta, o crea Impostazioni di costruzione > Compilatore Swift - Generale > Intestazione di bridging Objective-C.
  3. In Build Phases > Compilazione Sorgenti, doppio clic geo.c e aggiungi il flag per file -DSQLITE_CORE.

Il SDK iOS dispone sia di sqlite3.h e sqlite3ext.h, quindi non sono necessari altri header.

App-Bridging-Header.h:

#include <sqlite3.h>

int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);

Patch Fast SQL per esporre un hook aperto

Il plugin apre le connessioni all'interno del proprio modulo, quindi ha bisogno di un hook che il tuo app può impostare. Il Bun può patchare una dipendenza e riapplicare il patch ad ogni installazione:

bun patch @capgo/capacitor-fast-sql

Modifica node_modules/@capgo/capacitor-fast-sql/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift. Aggiungi un hook pubblico al livello superiore del 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)?
}

Quindi in init(...), dopo il blocco di cifratura e prima try applyPerformanceOptions(...), chiamalo:

if let hook = FastSqlExtensions.onOpen {
    let rc = hook(db)
    if rc != SQLITE_OK {
        throw SQLError.openFailed(message: "Extension init failed with code \(rc)")
    }
}

Salva il patch:

bun patch --commit node_modules/@capgo/capacitor-fast-sql

Bun scrive un file sotto patches/ e lo registra in package.json sotto patchedDependenciesCommit entrambi. La nostra guida su come patchare un plugin Capacitor copre questo workflow in modo più dettagliato.

Registra l'estensione all'avvio

In ios/App/App/AppDelegate.swift, importa il modulo del plugin e imposta il 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
}

Poiché l'hook esegue il codice all'interno del percorso aperto del plugin, ogni connessione riceve la funzione, comprese quelle aperte dopo il riavvio della vista web. didFinishLaunching Assicurandosi che sia presente prima che JavaScript possa chiamare connect().

SQLCipher costruisce. Se si utilizza la sottospecie SQLCipher del plugin, la connessione appartiene a SQLite di SQLCipher, mentre il proprio -DSQLITE_CORE Il file potrebbe collegarsi alle icone del sistema. Verificare questa combinazione esplicitamente prima di distribuire, o mantenere i dati dipendenti dall'estensione in una database non crittografata.

Passo 3: Android, creare un SQLite che possa caricare estensioni

Compila l'estensione per ogni ABI

Hai bisogno dell'NDK per Android (installalo dal Manager di Android Studio's SDK) e sqlite3ext.h plus sqlite3.h e l'amalgama di SQLite vicino a 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 allinea la libreria per dispositivi con pagine di memoria da 16 KB. Google Play richiede il supporto per 16 KB per le app che puntano a Android 15 e versioni successive, vedere dimensione delle pagine da 16 KB e Capacitor plugin. Se la tua app invia solo su dispositivi 64-bit, puoi abbandonare i due ABIs 32-bit.

La libreria si chiama libgeo.socosì il punto di ingresso predefinito di SQLite si chiama sqlite3_geo_initche corrisponde alla funzione C.

Estrai le librerie native al momento dell'installazione

Le versioni recenti del plugin Android Gradle conservano .so i file non compressi all'interno dell'APK al posto di estrarli. SQLite carica gli estensioni con un percorso di file, quindi chiedi l'estrazione in android/app/build.gradle:

android {
    packaging {
        jniLibs {
            useLegacyPackaging = true
        }
    }
}

Aggiungi requery sqlite-android

In android/build.gradle (root), aggiungi JitPack:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

Patch Fast SQL per utilizzarlo

Apre la stessa sessione di patch Bun (bun patch @capgo/capacitor-fast-sql) e modifica due file.

node_modules/@capgo/capacitor-fast-sql/android/build.gradle, in dependencies:

implementation 'com.github.requery:sqlite-android:3.49.0'

Utilizza la versione più recente che JitPack mostra costruita per il repository.

node_modules/@capgo/capacitor-fast-sql/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java: scambia le importazioni del framework per quelle di requery e apri attraverso una configurazione che trasporta le estensioni.

// 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
}

Le classi di requery riflettono il framework API (rawQuery, compileStatement, execSQL, transazioni), quindi il resto del file compila invariato. android.database.Cursor Rimane invariato. Committa la patch con bun patch --commit node_modules/@capgo/capacitor-fast-sql.

Questo cambia solo la cartella non crittografata. Le connessioni crittografate utilizzano le classi di SQLCipher, che questa patch non tocca.

Registra l'estensione 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);
    }
}

Registra prima super.onCreate così la lista viene riempita prima che il WebView possa eseguire qualsiasi script JavaScript.

Android senza C: funzioni Java

Se tutto ciò di cui hai bisogno è una funzione scalare, requery può registrare una scritta in Java, senza richiedere l'NDK. SQLDatabase costruttore, dopo aver aperto:

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

Questo è più lento del C per le query pesanti perché ogni chiamata attraversa JNI, ma è molto più semplice da mantenere. I tokenizzatori FTS5 e le tabelle virtuali ancora richiedono C.

Passo 4: utilizzalo da JavaScript

After bunx cap sync e una ricostruzione nativa:

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 box consente a SQLite di utilizzare un indice su (lat, lon) Ridurre i candidati prima che la funzione calcoli le distanze esatte. Senza di esso, la funzione esegue il calcolo su ogni riga.

Un fallback JavaScript per il web

The costruzione web non può caricare il tuo C code, quindi rilevalo e ricadi:

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.

Tokenizzatori personalizzati FTS5

Lo stesso meccanismo registra i tokenizzatori FTS5. Nella tua funzione di inizializzazione, ottieni il fts5_api puntatore con il SELECT fts5(?1) pattern dal modello documentazione FTS5, quindi chiama xCreateTokenizerDue requisiti aggiuntivi:

  • La libreria SQLite deve includere FTS5. La costruzione di requery lo include. Su iOS, controlla PRAGMA compile_options per ENABLE_FTS5.
  • Tutte le connessioni che scrivono o leggono dalla tabella FTS devono avere il tokenizzatore registrato, perché FTS5 memorizza il nome del tokenizzatore nella definizione della tabella. Questo è il motivo per cui il hook per connessione è importante.
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');

La spedizione e la manutenzione di esso

  • Riavvio nativo richiesto. Estensioni, patch e modifiche Gradle sono native. Richiedono una nuova versione per le App Store e Play Store. Costruzione Capgo Puoi costruire e firmare entrambi in cloud. Dalla primavera 2026, gli upload di iOS devono essere costruiti con Xcode 26 o successivo.
  • Il JavaScript può ancora muoversi velocemente. Il JavaScript può ancora muoversi velocemente. haversine_km vive nella tua bundle web e può essere spedito con aggiornamenti in tempo reale, as long as the native build that contains the extension is already installed. Gate new SQL on a native version check if users may still run an older binary.
  • Aggiornamenti dei plugin. When you update Fast SQL, Bun re-applies the patch. If the patched lines moved, the install fails loudly instead of silently dropping your hook. Re-create the patch on the new version.

Troubleshooting

no such function: haversine_km su iOS. La chiamata non era stata impostata prima connect(), o il database era stato aperto prima didFinishLaunching ran. Log inside the hook to confirm it is called.

Crash all'interno sqlite3_geo_init su iOS. -DSQLITE_CORE è mancante, quindi il file è stato compilato come estensione caricabile che dereferenzia il pApi puntatore, che è nil qui. Aggiungi la flag per-file in Compile Sources.

no such function su Android. La patch SQLDatabase importa ancora android.database.sqlite.SQLiteDatabaseo l'estensione è stata aggiunta alla lista dopo la prima connessione. SQLDatabase.extensions.size() quando si connette.

dlopen failed: library "…/libgeo.so" not found. La libreria manca per l'ABI del dispositivo (la maggior parte dei telefoni è arm64-v8ao le librerie native non sono state estratte. Controlla useLegacyPackaging e ispeziona l'APK con Android Studio's APK Analyzer.

undefined symbol: sqlite3_geo_init. il nome del punto di ingresso passato a SQLiteCustomExtension non corrisponde alla funzione C, o la funzione è stata dichiarata static.

Il Console di Gioco avverte per l'allineamento a 16 KB. Ricompila il .so con -Wl,-z,max-page-size=16384 e un recente NDK, e aggiorna requery a una versione costruita con allineamento a 16 KB.

La funzione funziona su Android ma non nel simulatore iOS. Assicurati che geo.c sia un membro del App target, non solo presente nel tree dei file.

Riepilogo

Piattaforma Come l'estensione entra Dove viene registrato
iOS Compilato all'interno dell'applicazione con -DSQLITE_CORE Hook del plugin che chiama sqlite3_geo_init su ogni nuova connessione
Android .so per ABI in jniLibs, caricata da requery sqlite-android SQLiteCustomExtension lista di lettura quando si apre ogni connessione
Web Non supportato da SQLite Wasm di serie Fallback JavaScript

Inizia controllando le funzionalità integrate. Se hai ancora bisogno di un'estensione, scrivila una volta in C, mantieni le patch native piccole e testa sui dispositivi più vecchi che supporti. Per la configurazione del plugin generale, vedi Come utilizzare SQLite in un'app Capacitor.

Aggiornamenti in tempo reale per le app Capacitor

Quando un bug nel layer web è attivo, invia la correzione attraverso Capgo invece di attendere giorni per l'approvazione della store. Gli utenti ricevono l'aggiornamento in background mentre le modifiche native rimangono nel normale percorso di revisione.

Ultimi articoli dal nostro Blog

Capgo vi dà le migliori informazioni che avete bisogno per creare un'app mobile veramente professionale.