您可以在 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(距离排序、评分) - 以用户期望的方式排序的排序规则
WHEREandORDER BYand - 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
- 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到组中并勾选 - 目标.
App/App-Bridging-Header.hXcode会提示创建一个桥接头文件。接受,或者创建并设置它在 构建设置 > Swift编译器 - 通用 > Objective-C桥接头文件. - 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_optionsblogENABLE_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_kmSQL调用 存储在您的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.