跳过主要内容

如何使用自定义 SQLite 扩展在 Capacitor

在 Capacitor 中加载自定义 SQLite 扩展:iOS 和 Android 的限制,一个可以同时编译的 C 扩展,以及如何根据连接注册它。

文章来源

马丁·多纳迪厄

作者

瓦莱里娅

审阅者

乔丹

编辑

如何在 Capacitor 中使用自定义 SQLite 扩展

您可以在 Capacitor 应用程序中使用自定义 SQLite 扩展,但只能作为已编译的本机 code:在 iOS 上,您可以静态链接扩展并在每个连接上调用其 init 函数,在 Android 上,您可以将支持加载扩展的 SQLite 构建打包,并将扩展作为每个 CPU 架构的本机库一起部署。这个指南解释了平台之间的差异,提供了一个 C 扩展的工作示例,并展示了 Capacitor 8 中的精确本机更改。 @capgo/capacitor-fast-sql 在Capacitor 8.

__CAPGO_KEEP_0__

SQLite 扩展是什么

扩展是本地 code,注册 SQLite 连接的新内容:标量函数()、聚合函数、排序规则、虚拟表和 FTS5 tokenizers。每个扩展都暴露一个入口点:haversine_km(...)SQLite 在每个连接上调用它一次。您注册的任何内容都只存在于该连接上。

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

在移动应用中编写一个的典型原因是:

为语言而编写的自定义 FTS5 tokenizers,内置 tokenizer 处理得不好

  • 需要在中使用的数学或地理函数 unicode61 (距离排序、评分)
  • 以用户期望的方式排序的排序规则 WHERE and ORDER BY and
  • and
  • and

查看已有的构建内容

在您支持的最旧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*树 ENABLE_RTREE
sin, cos, sqrt, pow 数学函数 ENABLE_MATH_FUNCTIONSSQLite 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() 在发布时不要依赖它:在 iOS 中的 SDK(Xcode 中的) sqlite3.h 声明它使用 API_DEPRECATED("Process-global auto extensions are not supported on Apple platforms", ...). 可靠的方法是在连接句柄打开后立即调用您的初始化函数。

Android。 SQLite 在 Android 框架内android.database.sqlite) 没有 API 来加载扩展。您需要在您的应用程序中捆绑一个带有扩展的 SQLite 构建。 是框架类的替代品,带有当前的 SQLite(支持 FTS5 和 JSON),并且通过 您的扩展成为 SQLiteCustomExtension文件每个 ABI 内部的 APK 中。 .so Web。

Web. SQLite内置Wasm构建无法加载本地code。您需要一个自定义的SQLite Wasm构建,内置了您的扩展。

什么插件今天支持

iOS Android Web
@capgo/capacitor-fast-sql 8.2 无需hook,需要补丁 使用框架SQLite,需要补丁 原生SQLite Wasm
@capacitor-community/sqlite 8.1 loadExtension() 在JS中声明了API但没有原生实现 相同 未实现

因此,目前添加一个扩展意味着一个小的原生补丁。它是一个包含的变化,Bun可以在安装时保持它

步骤1:编写扩展

A distance function 是一个很好的例子:有用、独立且易于验证。将其保存为 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 调用路由到 SQLite 通过指针传递的指针中,因此 pApi 不需要将 SQLite 链接到静态库中。 .so 静态编译时,使用
  • (iOS): 宏变为无操作,调用直接跳转到应用程序链接的 SQLite 库中。 -DSQLITE_CORE 让 SQLite 在表达式上使用该函数创建索引。

SQLITE_DETERMINISTIC 标记它在视图和触发器中安全使用。 SQLITE_INNOCUOUS 我在 macOS 上以静态方式编译了这个文件:

以动态方式编译: -DSQLITE_CORE 和被称为 sqlite3_geo_init(db, NULL, NULL) 在系统SQLite连接上,和作为可加载库通过 load_extension. 两者都返回约343.6公里的巴黎到伦敦.

步骤2:iOS,编译到应用中并注册每个连接

context

  1. Page/area: About Capgo page. Role: UI label. Seen in: page about.astro. Message key `about_how_step_label` (About How Step Label). geo.c 将源代码添加到应用目标 App 在Xcode中,拖拽 App 到组中并勾选
  2. 目标. App/App-Bridging-Header.h Xcode会提示创建一个桥接头文件。接受,或者创建并设置它在 构建设置 > Swift编译器 - 通用 > Objective-C桥接头文件.
  3. In 构建阶段 > 编译源码, 双击 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 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调用SQLCipher构建之前就已经准备好了。 connect().

如果您使用插件的SQLCipher子集,连接属于SQLCipher的SQLite,而您的 文件可能会链接到系统库的符号。测试此组合之前,请在未加密数据库中存储扩展依赖的数据。 -DSQLITE_CORE 步骤 3:Android,打包一个可以加载扩展的 SQLite

区域:Capgo 页面。角色:UI 标签。见于:关于.astro 页面。消息键 `about_how_step_label` (关于如何步骤标签)。

为每个 ABI 编译扩展

您需要 Android NDK(从 Android Studio 的 SDK 管理器中安装它)和 sqlite3ext.h 加上 sqlite3.h 从 SQLite 合并文件 下一个 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 内存页的设备。Google Play 要求 Android 15 及以上版本的应用支持 16 KB 页面,见 16 KB 页面大小和Capacitor插件。如果您的应用只部署到 64 位设备,可以丢弃 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

In android/build.gradle 在

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

修复 Fast SQL 以使用它

打开相同的 Bun 补丁会话(bun patch @capgo/capacitor-fast-sql)并编辑两个文件。

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 之前就已经填充好了。

Java 函数的 Android

如果您只需要一个标量函数,requery 可以注册一个用 Java 编写的函数,无需 NDK。内置的修补程序中,打开后: SQLDatabase 这是比 C 慢得多的,因为每次调用都需要 JNI,但它更容易维护。FTS5 tokenizers 和虚拟表仍然需要 C。

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

步骤 4:从 JavaScript 使用它

context

After 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 盒子让 SQLite 使用一个索引来 (lat, lon) 来缩小候选项之前函数计算精确距离。没有它,函数会在每一行上运行。

JavaScript 的 web fallback

无法加载您的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令牌化器。在您的初始化函数中,获取指针并从 fts5_api FTS5文档 SELECT fts5(?1) 中模式,然后调用 两个额外的要求:, 然后调用 xCreateTokenizer每个写入或读取FTS表的连接都必须注册令牌化器,因为FTS5将令牌名称存储在表定义中。这就是为什么每个连接钩子很重要的原因。

  • 运输和维护它 PRAGMA compile_options blog ENABLE_FTS5.
  • custom-sqlite-extensions-capacitor
CREATE VIRTUAL TABLE docs USING fts5(title, body, tokenize = 'my_tokenizer');

blog/custom-sqlite-extensions-capacitor

  • 原生重建需要。 扩展、补丁和Gradle更改是原生的。它们需要一个新的App Store和Play Store应用程序。 Capgo 构建 上下文: Capgo解决方案营销页面。角色:短UI标签或导航项。见于:页面解决方案/白标签.astro。保留Capgo产品/品牌和开发人员术语的原始形式。消息键`解决方案白标签视觉单元3值` (解决方案白标签视觉单元3值)。
  • 可以在云端构建和签名。从2026年4月起,iOS上传必须使用Xcode 26或更高版本构建。 JavaScript仍然可以快速移动。 haversine_km SQL调用 存储在您的Web包中并可以随着实时更新
  • ,只要包含扩展的原生构建已经安装。检查用户是否仍然可以运行旧二进制文件的原生版本来控制新SQL。 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 在 iOS 上。 钩子函数未在此之前设置 connect()或数据库在此之前已打开 didFinishLaunching 运行。将日志添加到钩子函数以确认它是否被调用

在此处崩溃 sqlite3_geo_init 在 iOS 上。 -DSQLITE_CORE 缺失,因此该文件以可加载的扩展形式编译,指向 pApi 指针,位于此处。添加 Compile Sources 中的每个文件标志 nil 在 Android 上。

no such function 修补后的 仍然导入 SQLDatabase 仍然导入 android.database.sqlite.SQLiteDatabase或扩展在首次连接后添加到列表中。 SQLDatabase.extensions.size() 连接时。

dlopen failed: library "…/libgeo.so" not found. 该库对于设备ABI(大多数手机是)缺失,或者本机库未提取。检查并使用Android Studio的APK Analyzer检查APK。 arm64-v8a传递给的入口点名称不匹配C函数,或者函数被声明 useLegacyPackaging Google Play Console警告16 KB对齐问题。

undefined symbol: sqlite3_geo_init. 重建 SQLiteCustomExtension 文件 static.

并使用最新的NDK,并更新requery到使用16 KB对齐构建的版本。 重建 .so when connecting. -Wl,-z,max-page-size=16384 The library is missing for the device ABI (most phones are

or native libraries were not extracted. Check 确保它是目标的成员 geo.c 不仅仅在文件树中存在 App 概要

平台

扩展如何进入 它在哪里注册 iOS
将其编译到应用程序中 插件钩子 -DSQLITE_CORE 在每个新连接上调用 sqlite3_geo_init Android
Make sure .so 根据 ABI 在 jniLibs, 由 requery sqlite-android 加载 SQLiteCustomExtension 列表读取每个连接打开时
Web 不支持 SQLite Wasm 的标准库 JavaScript fallback

首先检查内置功能。如果您仍然需要扩展,请在 C 中编写它,保持原生补丁小,测试在您支持的最旧设备上。有关通用插件设置,请参见 如何在 Capacitor 应用中使用 SQLite.

Capacitor 应用程序的即时更新

当 web 层面的 bug 活跃时,通过 Capgo 将修复推送给用户,而不是等待几天的 app 商店审批。用户在后台接收更新,而原生变化保持在正常的审批路径中。

__CAPGO_KEEP_0__ 给您需要创建真正专业的移动应用程序所需的最佳见解。

双向通信在 Capgo 应用程序中