Langsung ke konten utama

Cara Menggunakan Ekstensi SQLite Kustom di Capacitor

Muat ekstensi SQLite kustom di aplikasi Capacitor: batasan iOS dan Android, ekstensi C yang dibangun untuk keduanya, dan cara mendaftarkannya per koneksi.

Logo Capacitor untuk panduan ekstensi SQLite kustom di aplikasi Capacitor

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 unicode61 tokenizer yang kurang optimal,
  • fungsi matematika atau geo yang Anda butuhkan di WHERE dan ORDER 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/2 mengarahkan setiap panggilan SQLite melalui pApi pointer yang diteruskan SQLite, sehingga .so tidak 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

  1. Di Xcode, drag geo.c ke dalam App grup dan centang App target.
  2. Xcode menawarkan untuk membuat bridging header. Terima, atau buat App/App-Bridging-Header.h dan atur di bawah Build Settings &gt; Swift Compiler - General &gt; Objective-C Bridging Header.
  3. Di Build Phases &gt; Compile Sources, klik dua kali geo.c dan 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_options untuk ENABLE_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_km berada 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.

Live updates untuk aplikasi Capacitor

Saat bug web-layer sudah live, kirim perbaikannya lewat Capgo alih-alih menunggu berhari-hari untuk persetujuan app store. Pengguna mendapat update di background, sementara perubahan native tetap mengikuti jalur review normal.

Terbaru dari Blog kami

Capgo memberikan insight terbaik yang Anda butuhkan untuk membuat aplikasi mobile yang benar-benar profesional.