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

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

Capacitor アプリでカスタム SQLite 拡張機能をロードする:iOS と Android の制限、両対応の C 拡張機能のビルド、および接続ごとの登録方法について。

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

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

C言語を書く前に、本当にそれが必要か検討してください。多くの「拡張機能」はすでに組み込まれています。

SQLite拡張機能とは何か

拡張機能とは、SQLite接続に新しい機能を登録するネイティブな code です。具体的にはスカラー関数(haversine_km(...))、集計関数、照合順序(collation)、仮想テーブル、FTS5トークナイザーなどが含まれます。すべての拡張機能には、以下のエントリーポイントが1つ用意されています:

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

SQLiteは接続ごとにこれを1回呼び出します。登録された機能は、その接続内でのみ有効です。

モバイルアプリで拡張機能を自作する主な理由:

  • 組み込みのトークナイザーでは対応が難しい、特定の言語向けのカスタムFTS5トークナイザー unicode61 (言語の処理が不十分な場合)
  • アプリ内で必要となる数学関数や地理関数 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ビルドは、JSON、FTS5、数学関数を含む公式のSQLite Wasmを使用しています。

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ごとのファイルになります。

Webの場合。 標準のSQLite Wasmビルドでは、ネイティブのcodeをロードできません。拡張機能をコンパイルしたカスタムWasmビルドのSQLiteが必要になります。

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

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 View や Trigger で安全に使用できることを示します。

macOS上で、このファイルを以下の2つの方法でコンパイルしました: -DSQLITE_CORE 、そして sqlite3_geo_init(db, NULL, NULL) によるシステムSQLite接続と、 load_extension経由のロード可能ライブラリとして。どちらもパリからロンドンまで約343.6 kmと返します。

ステップ2: iOS、アプリ内へのコンパイルと接続ごとの登録

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

  1. Xcodeで、 geo.c を App グループにドラッグし、 App target.
  2. Xcodeがブリッジングヘッダーの作成を提案します。承諾するか、 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
}

このフックはプラグインのオープンパス内で実行されるため、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: framework の import を 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 のクラスは framework の API (rawQuery, compileStatement, execSQL、transactions) をミラーリングしているため、ファイルの残りの部分は変更なしでコンパイルできます。 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関数

スカラー関数だけで十分な場合は、NDK不要でJavaで書かれた関数をrequeryに登録できます。パッチを当てた 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)));
});

JNIを介するため重いクエリではCより低速になりますが、メンテナンスは遥かに簡単です。FTS5のtokenizerや仮想テーブルには依然としてCが必要です。

ステップ 4: JavaScriptから使用する

の後に bunx cap sync およびNative Buildの実行後:

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 ボックスにより、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.
  • FTS5はテーブル定義にトークナイザー名を保存するため、FTSテーブルへの書き込み・読み込みを行うすべての接続でトークナイザーを登録しておく必要があります。そのため、接続ごとのフックが重要になります。
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');

デプロイとメンテナンス

  • Native Build が必要です。 拡張機能、パッチ、Gradle の変更はネイティブ層に属します。App Store や Play Store での新しいビルドが必要です。 Capgo ビルド クラウド上で両方のビルドと署名が可能です。2026年4月以降、iOS のアップロードには Xcode 26 以降でのビルドが必須となります。
  • JavaScript なら、開発スピードを落とさずに済みます。 呼び出しを行う SQL は haversine_km Web bundle 内に存在するため、以下と一緒に配信できます: Live Updates拡張機能を含む Native Build が既にインストールされていれば可能です。ユーザーが古いバイナリを使用している可能性がある場合は、ネイティブのバージョンチェックによって新しい 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)、またはネイティブライブラリが展開されていません。Android Studioの useLegacyPackaging APK Analyzerを使用してAPKを検査してください。

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

Play Consoleが16 KBアライメントに関する警告を出しています。 を .so を使用して -Wl,-z,max-page-size=16384 でファイルを再ビルドし、16 KBアライメントでビルドされたバージョンの requery にアップデートしてください。

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

概要

プラットフォーム 拡張機能がどのように組み込まれるか 登録場所
iOS アプリにコンパイルして組み込み -DSQLITE_CORE 〜を呼び出すプラグインフック sqlite3_geo_init 新しい接続のたびに
Android .so ABIごとの jniLibsrequery sqlite-androidによってロード SQLiteCustomExtension 接続開始時にリストを読み込み
Web 標準のSQLite Wasmでは未対応 JavaScriptフォールバック

まずは組み込み機能を確認してください。それでも拡張機能が必要な場合は、C言語で一度記述し、ネイティブパッチを最小限に抑え、サポート対象の最も古いデバイスでテストを行ってください。一般的なプラグインの設定については、以下を参照してください: Capacitor アプリでのSQLiteの使用方法.

Capacitor アプリ向けの Live Update

Webレイヤーのバグが本番に出たときは、アプリストアの審査を何日も待たずに Capgo で修正を配信できます。ネイティブの変更は通常の審査フローに乗せたまま、ユーザーにはバックグラウンドでアップデートが届きます。

最新のブログ記事

Capgo は、本格的なプロフェッショナルモバイルアプリの開発に必要な最高のインサイトを提供します。