Anda dapat menggunakan ekstensi SQLite kustom di aplikasi Capacitor, tetapi hanya sebagai code native yang dikompilasi ke dalam aplikasi: di iOS, Anda meng-link ekstensi secara statis dan memanggil fungsi init-nya di setiap koneksi, sedangkan di Android, Anda membundel build SQLite yang mendukung pemuatan ekstensi dan menyertakan ekstensi sebagai pustaka native per arsitektur CPU. Panduan ini menjelaskan mengapa platformnya berbeda, memberikan ekstensi C yang berfungsi, dan menunjukkan perubahan native yang tepat untuk @capgo/capacitor-fast-sql di Capacitor 8.
Sebelum menulis C, periksa apakah Anda benar-benar membutuhkannya. Banyak “ekstensi” sudah dibangun di dalamnya.
Apa itu ekstensi SQLite
Ekstensi adalah code native yang mendaftarkan hal-hal baru ke koneksi SQLite: fungsi skalar (haversine_km(...)), fungsi agregat, kolasi, tabel virtual, dan tokenizer FTS5. Setiap ekstensi mengekspos satu titik masuk:
int sqlite3_<name>_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
SQLite memanggilnya sekali per koneksi. Apa pun yang Anda daftarkan hanya ada di koneksi tersebut.
Alasan umum untuk menulisnya di aplikasi mobile:
- tokenizer FTS5 kustom untuk bahasa yang tidak didukung bawaan
unicode61tokenizer yang kurang optimal, - fungsi matematika atau geo yang Anda butuhkan di
WHEREdanORDER BY(pengurutan jarak, scoring), - collation yang mengurutkan nama sesuai ekspektasi pengguna Anda,
- ekstensi pencarian vektor untuk retrieval on-device.
Cek fitur bawaan yang ada
Jalankan ini melalui plugin Anda di versi iOS dan Android tertua yang Anda dukung:
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));
Hal-hal yang biasanya tidak memerlukan ekstensi:
| Butuh | Fitur bawaan | Periksa |
|---|---|---|
| JSON di kolom | Fungsi JSON (json_extract, ->>) |
Tersedia bawaan sejak SQLite 3.38. SQLite sistem Android lama mungkin tidak memilikinya |
| Pencarian teks penuh | FTS4, FTS5 | ENABLE_FTS4 / ENABLE_FTS5 di compile_options |
| Bounding box spasial | R*Tree | ENABLE_RTREE |
sin, cos, sqrt, pow |
Fungsi matematika | ENABLE_MATH_FUNCTIONS, SQLite 3.35+ |
Jika fitur tersebut sudah ada di semua target, cukup sampai di sini. Web build Fast SQL menggunakan SQLite Wasm resmi, yang sudah mencakup JSON, FTS5, dan fungsi matematika.
Mengapa iOS dan Android berbeda
iOS. Sistemnya libsqlite3 dibangun dengan SQLITE_OMIT_LOAD_EXTENSION, jadi load_extension() tidak ada. Aturan App Store juga melarang download dan eksekusi code baru (guideline 2.5.2), jadi memuat sebuah .dylib melakukan hal tersebut saat runtime tidaklah memungkinkan. Ekstensi harus dikompilasi ke dalam biner aplikasi. Banyak tutorial kemudian menyebutkan sqlite3_auto_extension() saat peluncuran. Jangan andalkan ini: pada SDK iOS yang disertakan bersama Xcode, sqlite3.h mendeklarasikannya dengan API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...). Pendekatan yang andal adalah memanggil fungsi init pada setiap handle koneksi tepat setelah terbuka.
Android. SQLite di dalam framework Android (android.database.sqlite) tidak punya API untuk memuat ekstensi. Anda perlu build SQLite yang dibundel bersama aplikasi Anda yang mendukungnya. requery/sqlite-android adalah drop-in replacement untuk class framework, menyertakan SQLite versi terbaru (dengan FTS5 dan JSON), dan mendukung loadable extension melalui SQLiteCustomExtension. Ekstensi Anda akan menjadi .so file per ABI di dalam APK.
Web. Build SQLite Wasm standar tidak bisa memuat code native. Anda perlu build Wasm SQLite kustom dengan ekstensi yang sudah dikompilasi di dalamnya.
Dukungan plugin saat ini
| iOS | Android | Web | |
|---|---|---|---|
@capgo/capacitor-fast-sql 8.2 |
Tidak perlu hook atau patch | Menggunakan framework SQLite, perlu patch | SQLite Wasm bawaan |
@capacitor-community/sqlite 8.1 |
loadExtension() dideklarasikan di JS API tetapi tidak diimplementasikan secara native |
Sama | Belum diimplementasikan |
Jadi, untuk saat ini, menambahkan ekstensi berarti patch native kecil. Ini perubahan yang terbatas, dan Bun dapat menjaganya tetap diterapkan di semua instalasi.
Langkah 1: tulis ekstensi
Fungsi jarak adalah contoh yang bagus: berguna, mandiri, dan mudah diverifikasi. Simpan sebagai 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);
}
File yang sama di-build dengan kedua cara:
- Sebagai ekstensi yang dapat dimuat (Android):
SQLITE_EXTENSION_INIT1/2mengarahkan setiap panggilan SQLite melaluipApipointer yang diteruskan SQLite, sehingga.sotidak me-link ke SQLite. - Secara statis, dengan
-DSQLITE_CORE(iOS): makro menjadi no-op dan panggilan langsung diteruskan ke library SQLite yang di-link aplikasi.
SQLITE_DETERMINISTIC memungkinkan SQLite menggunakan fungsi tersebut dalam indeks pada ekspresi. SQLITE_INNOCUOUS menandainya aman digunakan di view dan trigger.
Saya mengompilasi file ini dengan dua cara di macOS: secara statis dengan -DSQLITE_CORE dan memanggil sqlite3_geo_init(db, NULL, NULL) pada koneksi SQLite sistem, dan sebagai library yang dapat dimuat melalui load_extension. Keduanya mengembalikan sekitar 343,6 km untuk rute Paris–London.
Langkah 2: iOS, kompilasi ke dalam app dan daftarkan per koneksi
Tambahkan source ke target app
- Di Xcode, drag
geo.cke dalamAppgrup dan centangApptarget. - Xcode menawarkan untuk membuat bridging header. Terima, atau buat
App/App-Bridging-Header.hdan atur di bawah Build Settings > Swift Compiler - General > Objective-C Bridging Header. - Di Build Phases > Compile Sources, klik dua kali
geo.cdan tambahkan flag per-file-DSQLITE_CORE.
iOS SDK menyediakan keduanya sqlite3.h dan sqlite3ext.h, jadi tidak perlu header tambahan.
App-Bridging-Header.h:
#include <sqlite3.h>
int sqlite3_geo_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
Patch Fast SQL untuk mengekspos hook terbuka
Plugin ini membuka koneksi di dalam modulnya sendiri, sehingga membutuhkan hook yang bisa disetel oleh aplikasi Anda. Bun dapat mem-patch dependensi dan menerapkan ulang patch tersebut setiap kali instalasi:
bun patch @capgo/capacitor-fast-sql
Edit node_modules/@capgo/capacitor-fast-sql/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift. Tambahkan hook publik di level teratas 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)?
}
Lalu di init(...), setelah blok enkripsi dan sebelum try applyPerformanceOptions(...), panggil:
if let hook = FastSqlExtensions.onOpen {
let rc = hook(db)
if rc != SQLITE_OK {
throw SQLError.openFailed(message: "Extension init failed with code \(rc)")
}
}
Simpan patch:
bun patch --commit node_modules/@capgo/capacitor-fast-sql
Bun menulis file di bawah patches/ dan mencatatnya di package.json di bawah patchedDependencies. Commit keduanya. Panduan kami tentang cara patch plugin Capacitor menjelaskan workflow ini secara lebih detail.
Registrasi ekstensi saat startup
Di ios/App/App/AppDelegate.swift, import modul plugin dan set hook di 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
}
Karena hook berjalan di dalam path open plugin, setiap koneksi mendapatkan fungsi tersebut, termasuk yang dibuka setelah reload WebView. Menyetelnya di didFinishLaunching menjaminnya sudah terpasang sebelum JavaScript bisa memanggil connect().
build SQLCipher. Jika Anda menggunakan subspec SQLCipher dari plugin, koneksi tersebut milik SQLite SQLCipher, sedangkan file -DSQLITE_CORE file mungkin link ke simbol library sistem. Uji kombinasi ini secara eksplisit sebelum rilis, atau simpan data yang bergantung pada extension di database tanpa enkripsi.
Langkah 3: Android, bundle SQLite yang dapat memuat extension
Kompilasi ekstensi untuk setiap ABI
Anda membutuhkan Android NDK (instal dari SDK Manager di Android Studio) dan sqlite3ext.h plus sqlite3.h dari Amalgamation SQLite di sebelah 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 menyelaraskan pustaka untuk perangkat dengan halaman memori 16 KB. Google Play mensyaratkan dukungan 16 KB untuk aplikasi yang menargetkan Android 15 ke atas, lihat ukuran halaman 16 KB dan plugin Capacitor. Jika aplikasi Anda hanya didistribusikan ke perangkat 64-bit, Anda dapat menghapus dua ABI 32-bit.
Pustaka ini diberi nama libgeo.so, sehingga nama titik masuk default SQLite adalah sqlite3_geo_init, yang cocok dengan fungsi C.
Ekstrak library native saat instalasi
Versi Android Gradle Plugin terbaru menyimpan .so file tanpa kompresi di dalam APK alih-alih mengekstraknya. SQLite memuat ekstensi melalui path file, jadi minta ekstraksi di android/app/build.gradle:
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
Tambahkan requery sqlite-android
Di android/build.gradle (root), tambahkan JitPack:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Patch Fast SQL untuk menggunakannya
Buka sesi patch Bun yang sama (bun patch @capgo/capacitor-fast-sql) dan edit dua file.
node_modules/@capgo/capacitor-fast-sql/android/build.gradle, di dependencies:
implementation 'com.github.requery:sqlite-android:3.49.0'
Gunakan versi terbaru yang ditampilkan JitPack sebagai sudah di-build untuk repository tersebut.
node_modules/@capgo/capacitor-fast-sql/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java: ganti import framework dengan milik requery dan buka melalui konfigurasi yang membawa ekstensi.
// 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
}
kelas requery mencerminkan framework API (rawQuery, compileStatement, execSQL, transaksi), sehingga sisa file tetap terkompilasi tanpa perubahan. android.database.Cursor tetap apa adanya. Commit patch dengan bun patch --commit node_modules/@capgo/capacitor-fast-sql.
Ini hanya mengubah jalur tanpa enkripsi. Koneksi terenkripsi menggunakan kelas SQLCipher sendiri, yang tidak disentuh oleh patch ini.
Daftarkan ekstensi di 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);
}
}
Daftarkan sebelum super.onCreate sehingga daftar terisi sebelum WebView dapat menjalankan JavaScript apa pun.
Android tanpa C: fungsi Java
Jika yang Anda butuhkan hanya fungsi skalar, requery dapat mendaftarkan satu yang ditulis dalam Java, tanpa NDK. Di dalam SQLDatabase konstruktor yang di-patch, setelah membuka:
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)));
});
Ini lebih lambat daripada C untuk query berat karena setiap panggilan melewati JNI, tetapi jauh lebih sederhana untuk dipelihara. Tokenizer FTS5 dan tabel virtual tetap membutuhkan C.
Langkah 4: gunakan dari JavaScript
Setelah bunx cap sync dan native rebuild:
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],
);
Panduan BETWEEN box memungkinkan SQLite menggunakan index pada (lat, lon) untuk menyaring kandidat sebelum fungsi menghitung jarak yang tepat. Tanpa ini, fungsi berjalan di setiap baris.
Fallback JavaScript untuk web
Build web tidak dapat memuat C code Anda, jadi deteksi dan lakukan fallback:
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.
Tokenizer FTS5 kustom
Mekanisme yang sama mendaftarkan tokenizer FTS5. Di fungsi init Anda, dapatkan fts5_api pointer dengan SELECT fts5(?1) pattern from the dokumentasi FTS5, lalu panggil xCreateTokenizer. Dua persyaratan tambahan:
- Library SQLite harus menyertakan FTS5. Build dari requery sudah menyertakannya. Di iOS, periksa
PRAGMA compile_optionsuntukENABLE_FTS5. - Setiap koneksi yang menulis atau membaca dari tabel FTS harus mendaftarkan tokenizer, karena FTS5 menyimpan nama tokenizer dalam definisi tabel. Inilah mengapa hook per-koneksi penting.
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');
Merilis dan memeliharanya
- Perlu rebuild native. Ekstensi, patch, dan perubahan Gradle bersifat native. Perlu build baru untuk App Store dan Play Store. Build Capgo bisa build dan sign keduanya di cloud. Sejak April 2026, upload iOS harus di-build dengan Xcode 26 atau lebih baru.
- JavaScript tetap bisa bergerak cepat. SQL yang memanggil
haversine_kmberada di web bundle Anda dan bisa dirilis lewat live updates, selama native build yang berisi ekstensi tersebut sudah terinstal. Batasi SQL baru dengan pengecekan versi native jika user mungkin masih menjalankan binary lama. - Upgrade plugin. Saat Anda memperbarui Fast SQL, Bun akan menerapkan ulang patch. Jika baris yang di-patch berpindah, instalasi akan gagal dengan error yang jelas, bukan diam-diam membuang hook Anda. Buat ulang patch di versi baru.
Pemecahan Masalah
no such function: haversine_km di iOS. Hook belum diatur sebelum connect(), atau database sudah dibuka sebelum didFinishLaunching ran. Log di dalam hook untuk memastikan hook tersebut dipanggil.
Crash di dalam sqlite3_geo_init di iOS. -DSQLITE_CORE hilang, sehingga file dikompilasi sebagai loadable extension yang melakukan dereference pada pApi pointer, yang ada nil di sini. Tambahkan flag per-file di Compile Sources.
no such function di Android. Yang di-patch SQLDatabase masih mengimpor android.database.sqlite.SQLiteDatabase, atau ekstensi baru ditambahkan ke daftar setelah koneksi pertama. Log SQLDatabase.extensions.size() saat menghubungkan.
dlopen failed: library "…/libgeo.so" not found. Library tidak ditemukan untuk ABI perangkat (kebanyakan ponsel adalah arm64-v8a), atau library native tidak diekstrak. Periksa useLegacyPackaging dan periksa APK dengan APK Analyzer di Android Studio.
undefined symbol: sqlite3_geo_init. Nama entry point yang diteruskan ke SQLiteCustomExtension tidak cocok dengan fungsi C, atau fungsi tersebut dideklarasikan static.
Play Console memperingatkan soal alignment 16 KB. Build ulang .so file dengan -Wl,-z,max-page-size=16384 dan NDK terbaru, lalu update requery ke versi dengan alignment 16 KB.
Fungsinya jalan di Android, tapi tidak di simulator iOS. Pastikan geo.c adalah anggota dari App target, bukan cuma ada di file tree.
Ringkasan
| Platform | Cara ekstensi masuk | Tempat registrasi |
|---|---|---|
| iOS | Dikompilasi ke dalam app dengan -DSQLITE_CORE |
Plugin hook yang memanggil sqlite3_geo_init pada setiap koneksi baru |
| Android | .so per ABI di jniLibs, dimuat oleh requery sqlite-android |
SQLiteCustomExtension daftar yang dibaca setiap kali koneksi dibuka |
| Web | Tidak didukung oleh SQLite Wasm bawaan | Fallback JavaScript |
Mulailah dengan mengecek fitur bawaan. Jika masih butuh ekstensi, tulis sekali dalam C, jaga patch native tetap kecil, dan tes di perangkat tertua yang Anda dukung. Untuk setup plugin umum, lihat Cara menggunakan SQLite di aplikasi Capacitor.