メインコンテンツへスキップ

CapacitorでカスタムSQLite拡張機能を使用する方法

Capacitor アプリでカスタム SQLite extension をロードする方法: iOS と Android の制限、両方でビルドできる C extension、接続ごとの登録方法。

Capacitor ロゴ。Capacitor アプリでのカスタム SQLite 拡張機能に関するガイド

Capacitor アプリではカスタム SQLite 拡張機能を使用できますが、アプリにコンパイルされたネイティブ code としてのみ可能です。iOS では拡張機能を静的にリンクし、各接続で init 関数を呼び出します。Android では拡張機能の読み込みをサポートする SQLite ビルドをバンドルし、CPU アーキテクチャごとにネイティブライブラリとして配布します。本ガイドでは、プラットフォーム間の違い、動作する C による拡張機能、および @capgo/capacitor-fast-sql Capacitor 8 での具体的なネイティブ変更について説明します。

C を書く前に、本当に必要かどうかを確認してください。多くの「拡張機能」はすでに組み込まれています。

SQLite拡張機能とは

拡張機能とは、SQLite 接続に新しいものを登録するネイティブ code です:スカラー関数(haversine_km(...))、集約関数、コレーション、仮想テーブル、FTS5 トークナイザー。すべての拡張機能は 1 つのエントリポイントを公開します:

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

SQLite は接続ごとに 1 回だけこれを呼び出します。登録したものはその接続でのみ存在します。

モバイルアプリで拡張機能を書く典型的な理由:

  • 組み込みの unicode61 tokenizer がうまく処理できない場合、
  • 必要な数学関数や地理関数が WHERE と ORDER BY (距離ソート、スコアリングなど)、
  • ユーザーが期待する通りに名前をソートする collation、
  • オンデバイスでの検索用のベクトル検索拡張機能。

組み込み済みの機能を確認する

サポートしている最も古い iOS と Android バージョンで、プラグイン経由でこれらを実行してください:

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

拡張機能が不要なことが多いもの:

要件 組み込み機能 確認
カラム内のJSON JSON関数 (json_extract, ->>) SQLite 3.38以降で標準搭載。古いAndroidのシステムSQLiteには未対応の場合あり
全文検索 FTS4、FTS5 ENABLE_FTS4 / ENABLE_FTS5 における compile_options
空間バウンディングボックス R*Tree ENABLE_RTREE
sin, cos, sqrt, pow 数学関数 ENABLE_MATH_FUNCTIONS、SQLite 3.35+

すべてのターゲットで機能が揃っていれば、ここまでで十分です。Fast SQLのWebビルドは公式のSQLite Wasmを使用しており、JSON、FTS5、数学関数が含まれています。

iOSとAndroidで挙動が異なる理由

iOS。 システム libsqlite3 は SQLITE_OMIT_LOAD_EXTENSIONを使用しているため、 load_extension() が存在しません。App Store のルールも、新しい code のダウンロードと実行を禁止しています(ガイドライン 2.5.2)。そのため、 .dylib 実行時のロードはそもそも論外です。拡張機能はアプリのバイナリにコンパイルする必要があります。多くのチュートリアルでは、 sqlite3_auto_extension() 起動時に呼び出すよう説明しています。ただし、これに頼らないでください。Xcodeに同梱されるiOSの SDK では、 sqlite3.h 次のように宣言しています。 API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...)確実な方法は、各接続ハンドルがオープンした直後に初期化関数を呼び出すことです。

Android。 Androidフレームワーク内のSQLite(android.database.sqlite)には、拡張機能をロードするための API がありません。アプリには、拡張機能をロードできるSQLiteビルドを同梱する必要があります。 requery/sqlite-android フレームワーククラスのドロップイン代替で、最新のSQLite(FTS5とJSONを含む)を同梱し、ロード可能な拡張機能をサポートしています。 SQLiteCustomExtension拡張機能は、 .so APK内のABIごとに1ファイルになります。

Web。 標準のSQLite Wasmビルドでは、ネイティブのcodeを読み込めません。拡張機能を組み込んでコンパイルしたカスタムSQLite Wasmビルドが必要です。

現在のプラグインのサポート状況

iOSと Androidでは Web
@capgo/capacitor-fast-sql 8.2 フック不要、パッチが必要 framework SQLiteを使用、パッチが必要 標準のSQLite Wasm
@capacitor-community/sqlite 8.1 loadExtension() JSのAPIで宣言されていますが、ネイティブでは実装されていません 同じ 未実装

そのため、現時点で拡張機能を追加するには小さなネイティブパッチが必要です。変更は限定的で、Bun を使えばインストールをまたいでも適用したままにできます。

ステップ 1: 拡張機能の作成

距離関数は良い例です。有用で自己完結しており、検証も容易です。次の名前で保存します: 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);
}

同じファイルが両方の方法でビルドされます:

  • ロード可能な拡張機能として (Android): SQLITE_EXTENSION_INIT1/2 すべての SQLite 呼び出しを次に通します: pApi SQLite が渡すポインタを使うので、 .so SQLite とリンクしません。
  • 静的リンクの場合、 -DSQLITE_CORE (iOS): マクロは no-op になり、呼び出しはアプリがリンクしている SQLite ライブラリへ直接届きます。

SQLITE_DETERMINISTIC SQLite が式インデックスでその関数を使えるようにします。 SQLITE_INNOCUOUS ビューやトリガーで安全に使えるとマークします。

macOS でこのファイルを両方の方法でコンパイルしました。静的リンクでは -DSQLITE_CORE を呼び出して sqlite3_geo_init(db, NULL, NULL) を呼び出しました。また、ロード可能ライブラリとしては load_extension経由でも。どちらもパリからロンドンまで約343.6 kmを返します。

ステップ2: iOS — アプリにコンパイルし、接続ごとに登録

ソースをアプリのターゲットに追加

  1. Xcode で geo.c を App グループにドラッグし、 App ターゲット。
  2. Xcode がブリッジングヘッダーの作成を提案します。「Accept」を選ぶか、 App/App-Bridging-Header.h を作成し、以下で設定します Build Settings > Swift Compiler - General > Objective-C Bridging Header.
  3. では Build Phases > Compile Sources、ダブルクリックして geo.c ファイルごとのフラグを追加します -DSQLITE_CORE.

iOS 版の SDK は両方を同梱しているため、 sqlite3.h と sqlite3ext.h追加のヘッダーは不要です。

App-Bridging-Header.h:

#include <sqlite3.h>

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

Fast SQLをパッチしてオープンなフックを公開する

プラグインは独自のモジュール内で接続を開くため、アプリ側で設定できるフックが必要です。Bun を使えば、依存関係をパッチしてインストールごとに再適用できます:

bun patch @capgo/capacitor-fast-sql

編集 node_modules/@capgo/capacitor-fast-sql/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift。ファイルのトップレベルに public フックを追加します:

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

次に init(...)、暗号化ブロックの後に、 try applyPerformanceOptions(...)で、これを呼び出します:

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

パッチを保存します:

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

Bun は以下の下にファイルを書き込みます patches/ 、そしてそこに記録します package.json 内の patchedDependencies. 両方をコミットします。私たちのガイド Capacitor プラグインのパッチ方法) では、このワークフローをより詳しく説明しています。

起動時に拡張機能を登録する

では ios/App/App/AppDelegate.swift、プラグインモジュールをインポートしてフックを設定します 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
}

フックはプラグインの open パス内で実行されるため、WebView のリロード後に開かれた接続も含め、すべての接続にその関数が設定されます。これを設定するには didFinishLaunching JavaScriptが呼び出す前に確実に設定されていることを保証します connect().

SQLCipherのビルド。 プラグインのSQLCipherサブスペックを使用している場合、接続はSQLCipherのSQLiteに属しますが、 -DSQLITE_CORE ファイルはシステムライブラリのシンボルにリンクしている可能性があります。リリース前にこの組み合わせを明示的にテストするか、拡張機能に依存するデータは暗号化されていないデータベースに保存してください。

ステップ3: Androidで拡張機能をロードできるSQLiteをバンドルする

各ABI向けに拡張機能をコンパイルする

Android NDK(Android Studioの SDK Managerからインストール)と、 sqlite3ext.h と sqlite3.h を SQLiteのamalgamation の横にある 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 16 KBメモリページを使用するデバイス向けにライブラリを調整します。Android 15以降をターゲットとするアプリでは、Google Playが16 KBサポートを必須としています。詳細は 16 KBページサイズとCapacitorプラグインをご確認ください。アプリが64ビットデバイスにのみ配布する場合は、2つの32ビットABIを削除できます。

ライブラリ名は libgeo.soです。そのため、SQLiteのデフォルトのエントリポイント名は sqlite3_geo_initとなり、C関数と一致します。

インストール時にネイティブライブラリを展開する

最近のAndroid Gradle Pluginのバージョンでは、 .so ファイルをAPK内で非圧縮のまま保持し、展開は行いません。SQLiteはファイルパスで拡張機能をロードするため、展開を指定するには次を設定します: android/app/build.gradle:

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

requery sqlite-androidを追加

では android/build.gradle (root) に JitPack を追加します:

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

Fast SQL をパッチ適用して使用します

同じ Bun パッチセッションを開きます (bun patch @capgo/capacitor-fast-sql)。2つのファイルを編集します。

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

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

JitPack がリポジトリ用にビルド済みと表示している最新バージョンを使用します。

node_modules/@capgo/capacitor-fast-sql/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java:フレームワークのインポートを requery のものに置き換え、拡張機能を保持する設定経由で開きます。

// 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 のクラスはフレームワーク API (rawQuery, compileStatement, execSQL、トランザクション) をミラーリングしているため、ファイルの残りは変更なしでコンパイルできます。 android.database.Cursor はそのままです。パッチをコミットするには bun patch --commit node_modules/@capgo/capacitor-fast-sql.

これは暗号化されていないパスのみを変更します。暗号化された接続は SQLCipher 独自のクラスを使用しており、このパッチでは変更しません。

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

の前に登録 super.onCreate することで、WebView が JavaScript を実行する前にリストが埋められます。

C なしの Android: Java 関数

スカラー関数だけで十分な場合、requery は NDK を必要とせずに Java で記述された関数を登録できます。パッチ適用済みの SQLDatabase コンストラクタ内で、オープン後に:

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

これは重いクエリでは C より遅く、すべての呼び出しが JNI をまたぐためです。ただし、メンテナンスははるかに簡単です。FTS5 トークナイザーと仮想テーブルには引き続き C が必要です。

ステップ4: JavaScriptから使う

完了後、 bunx cap sync およびネイティブ再ビルド:

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

この BETWEEN box を使うと SQLite はインデックスを利用して (lat, lon) 関数が正確な距離を計算する前に候補を絞り込めます。これがないと、関数はすべての行で実行されます。

Web用のJavaScriptフォールバック

Web ビルドは C の code を読み込めないため、検出してフォールバックしてください:

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.

カスタムFTS5トークナイザー

同じメカニズムで FTS5 トークナイザーを登録します。init 関数内で、 fts5_api ポインターを取得します。使うのは SELECT fts5(?1) 次のパターンです。出典: FTS5 ドキュメント、その後呼び出す xCreateTokenizer。追加の要件が2つあります:

  • SQLite ライブラリには FTS5 が含まれている必要があります。requery のビルドには含まれています。iOS では、 PRAGMA compile_options を ENABLE_FTS5.
  • FTS テーブルへの読み書きを行うすべての接続で、トークナイザーを登録する必要があります。FTS5 はテーブル定義にトークナイザー名を保存するためです。これが、接続ごとのフックが重要である理由です。
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');

配信とメンテナンス

  • ネイティブ再ビルドが必要です。 拡張機能、パッチ、Gradle の変更はネイティブです。App Store と Play Store の新しいビルドが必要です。 Capgo のビルド はクラウドで両方のビルドと署名を行うことができます。2026年4月以降、iOS のアップロードには Xcode 26 以降でのビルドが必須です。
  • JavaScript は引き続き素早く進められます。 を呼び出す SQL haversine_km は Web bundle 内にあり、一緒に配布できます Live Updatesただし、拡張機能を含むネイティブビルドがすでにインストールされている場合に限ります。ユーザーが古いバイナリを実行している可能性がある場合は、新しい SQL はネイティブのバージョンチェックでガードしてください。
  • プラグインのアップグレード。 Fast SQL を更新すると、Bun がパッチを再適用します。パッチ対象の行が移動している場合、フックが黙って外れるのではなく、インストールはエラーを出して失敗します。新しいバージョン向けにパッチを作り直してください。

トラブルシューティング

no such function: haversine_km iOS上で。 フックが事前に設定されていませんでした connect()、またはデータベースが didFinishLaunching 実行前に開かれていました。フック内でログを出力して、呼び出されていることを確認してください。

内部でクラッシュ sqlite3_geo_init iOS上で。 -DSQLITE_CORE が欠落しているため、ファイルは参照解除を行うロード可能拡張機能としてコンパイルされました pApi ポインタ、これは nil です。Compile Sources にファイルごとのフラグを追加してください。

no such function Androidの場合 パッチを適用した SQLDatabase は依然として android.database.sqlite.SQLiteDatabaseをインポートしています。または、拡張機能が最初の接続後にリストに追加されました。接続時に SQLDatabase.extensions.size() をログ出力してください。

dlopen failed: library "…/libgeo.so" not found. デバイス ABI 用のライブラリが欠落しています(ほとんどのスマートフォンは arm64-v8aです)。または、ネイティブライブラリが展開されていません。 useLegacyPackaging を確認し、Android Studio の APK Analyzer で APK を検査してください。

undefined symbol: sqlite3_geo_init. に渡されるエントリポイント名 SQLiteCustomExtension C関数と一致しないか、関数が宣言されていません static.

Play Consoleが16 KBアライメントについて警告します。 を再ビルドします .so ファイルを以下で -Wl,-z,max-page-size=16384 および最新のNDKで、16 KBアライメント対応のバージョンにrequeryを更新してください。

この関数はAndroidでは動作しますが、iOSシミュレータでは動作しません。 必ず geo.c が〜のメンバーであることを確認してください。 App ターゲットに含まれている必要があり、ファイルツリーに存在するだけでは不十分です。

まとめ

プラットフォーム 拡張機能がどのように組み込まれるか 登録場所
iOSと 〜でアプリにコンパイルされる -DSQLITE_CORE 〜を呼び出すプラグインフック sqlite3_geo_init 新しい接続のたびに
Androidでは .so ABI ごとに jniLibs、requery sqlite-android によって読み込まれます SQLiteCustomExtension 各接続がオープンされる際に読み取られるリスト
Web 標準の SQLite Wasm ではサポートされていません JavaScript フォールバック

まず組み込み機能を確認してください。それでも拡張機能が必要な場合は、Cで一度だけ実装し、ネイティブのパッチを最小限に保ち、サポートしている最古のデバイスでテストしてください。一般的なプラグインのセットアップについては、こちらを参照してください。 Capacitor アプリでSQLiteを使う方法.

Capacitor アプリ向けの Live Updates

Web レイヤーのバグが本番で発生したら、App Store の審査を何日も待たずに Capgo で修正を配信しましょう。ネイティブの変更は通常の審査フローに残したまま、ユーザーはバックグラウンドでアップデートを受け取れます。

最新のブログ記事

Capgoは、真にプロフェッショナルなモバイルアプリを作るために必要な最高のインサイトを提供します。