---
title: "SQL 插件"
description: "Kasumi SQL 驱动、连接生命周期、参数规则和安全边界"
---


> 理解 SQL 插件的驱动可用性、连接生命周期、参数规则和 SQLite 安全边界。

## 模块边界

SQL 插件只提供连接管理、参数化查询和参数化执行，不负责 ORM、迁移、连接池配置或业务
事务编排。它通过 `sql.drivers` 报告宿主实际注册的驱动，不把协议中的驱动标识当成已安装
客户端。

| 层级 | 入口 | 责任 |
| --- | --- | --- |
| 前端 API | `frontend/src/api/sql.ts` | 参数守卫、Promise 和命令封装 |
| 协议插件 | `protocol/plugins.json` | `sql` 插件与五个命令的归属 |
| Windows 运行时 | `windows/src/plugins/ksm_sql_plugin.cpp` | 命令分发和错误映射 |
| Windows 驱动 SPI | `windows/src/platform/ksm_sql_driver.h` | 连接、查询和执行抽象 |
| Android 运行时 | `KSMSqlPlugin.kt` | 命令分发和错误映射 |
| Android 驱动 SPI | `KSMSqlDriver.kt` | 连接、查询、执行和关闭 |

## 驱动支持

协议保留四个驱动标识：

| 驱动 | 内置状态 | URL 约定 |
| --- | --- | --- |
| `sqlite` | Windows / Android 内置 | `:memory:` 或可写虚拟路径 |
| `mysql` | Windows 检测到系统 ODBC 驱动后内置；Android 宿主注册 | Windows 使用 `mysql://host[:port]/database?ssl-mode=required` |
| `postgres` | Windows 检测到系统 ODBC 驱动后内置；Android 宿主注册 | Windows 使用 `postgresql://host[:port]/database?sslmode=require` |
| `mongodb` | 宿主注册后可用 | 由 MongoDB 驱动定义 |

`sql.drivers` 只返回当前宿主注册表里的驱动。调用方必须先查询，再调用 `sql.open`。
远程驱动可以按应用需要选择原生客户端、托管连接层或业务自己的网络适配器；Kasumi 不把
数据库凭据写入前端配置或能力缓存。Windows 的 ODBC 路由要求系统安装 MySQL / MariaDB
或 PostgreSQL ODBC 驱动，SDK 不随包分发对应客户端。远程 URL 支持可选用户名和密码，
TLS 默认开启，只有显式查询参数才允许关闭；凭据不会进入错误消息、日志或 `sql.drivers`。

## 方法索引

| 方法 | 参数 | 返回 | 说明 |
| --- | --- | --- | --- |
| `sql.drivers` | `{}` | `{ drivers }` | 查询当前可用驱动 |
| `sql.open` | `{ driver, url }` | `{ connectionId }` | 创建独立连接 |
| `sql.query` | `{ connectionId, statement, params? }` | `{ columns, rows }` | 执行返回行的语句 |
| `sql.execute` | `{ connectionId, statement, params? }` | `{ rowsAffected, lastInsertId }` | 执行写操作 |
| `sql.close` | `{ connectionId }` | `{ closed }` | 关闭连接 |

`connectionId` 是宿主生成的短生命周期句柄，不是数据库连接串。连接关闭后再次使用返回
`NOT_FOUND`，应用不得把旧 ID 复用为新的业务会话。

## 参数与结果

`params` 只允许字符串、有限数值、布尔值和 `null`。SQL 方言的占位符由驱动决定：
SQLite 和 MySQL 通常使用 `?`，PostgreSQL 通常使用 `$1`。调用方必须使用目标驱动支持的
占位符，不得把用户输入拼接到 `statement`。

`sql.query` 返回列名数组和行对象：

```json
{
  "columns": ["id", "name"],
  "rows": [
    { "id": 1, "name": "first" }
  ]
}
```

`sql.execute` 返回：

```json
{
  "rowsAffected": 1,
  "lastInsertId": 1
}
```

驱动无法提供主键时，`lastInsertId` 为 `null`。`rowsAffected` 必须是非负整数，不能用
数据库驱动的内部错误字符串冒充成功结果。

## SQLite 边界

SQLite 只接受以下连接目标：

| 目标 | 允许 | 说明 |
| --- | --- | --- |
| `:memory:` | 是 | 每个连接独立的内存库 |
| `app://data/**` | 是 | 持久应用数据 |
| `app://cache/**` | 是 | 可清理缓存 |
| `app://tmp/**` | 是 | 临时文件 |
| `app://picked/**` | 否 | 用户选择文件不自动成为数据库 |
| `app://res/**`、`app://assets/**` | 否 | 只读资源 |
| 真实文件路径、`content://` | 否 | 不暴露宿主路径 |

目录不存在时由驱动创建；路径包含父目录穿越、空字节、只读根或无法解析的 picked 句柄时，
返回 `PERMISSION_DENIED`。SQLite 打开后启用外键约束，语句错误返回 `INVALID_ARGS`，
打开或写入失败返回 `IO_ERROR`。

## 示例

```ts
const available = await KSM.sql.drivers();
if (!available.drivers.includes('sqlite')) {
  throw new Error('SQLite is not available on this host.');
}

const { connectionId } = await KSM.sql.open({
  driver: 'sqlite',
  url: 'app://data/app.db'
});

try {
  await KSM.sql.execute({
    connectionId,
    statement: 'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)'
  });

  await KSM.sql.execute({
    connectionId,
    statement: 'INSERT INTO notes (body) VALUES (?)',
    params: ['hello']
  });

  const result = await KSM.sql.query({
    connectionId,
    statement: 'SELECT id, body FROM notes ORDER BY id'
  });

  console.info(result.rows);
} finally {
  await KSM.sql.close({ connectionId });
}
```

业务层必须在 `finally` 中关闭连接。请求超时或页面销毁时，宿主会取消活动命令并释放连接；
应用不应依赖浏览器垃圾回收。

## 驱动实现

Windows 实现 `KSMSqlDriver` 和 `KSMSqlConnection`，由
`KSMHostServices::RegisterSqlDriver` 注册。Android 实现 `KSMSqlDriver` 和
`KSMSqlConnection`，由 `KSMAndroidHostServices.registerSqlDriver` 注册。驱动必须：

- 为每个 `sql.open` 创建独立连接对象。
- 在查询和执行期间串行访问同一连接。
- 将驱动异常转换为标准协议错误码。
- 关闭时释放网络连接、游标、临时文件和敏感内存。
- 不在错误消息、日志或 `sql.drivers` 中泄漏用户名、密码或完整 URL。

远程驱动的 URL、TLS 策略、证书校验和连接超时由驱动实现负责。默认应使用 TLS，并在部署
文档中说明自签名证书、代理和证书轮换策略。

## 平台差异

| 项 | Windows | Android |
| --- | --- | --- |
| SQLite | 内置 | 内置 |
| MySQL / PostgreSQL | 系统 ODBC 驱动自动发现 | 宿主 SPI 注册 |
| MongoDB | 宿主 SPI 注册 | 宿主 SPI 注册 |
| 远程驱动注册 | `KSMHostServices::RegisterSqlDriver` | `KSMAndroidHostServices.registerSqlDriver` |
| 连接执行线程 | 宿主工作线程 | IO 协程 |
| 取消 | 现有请求取消链路 | 协程取消与活动连接清理 |
| 后台生命周期 | 窗口宿主关闭时释放 | `onTrimMemory` / `onDestroy` 时清理 |

Android 低内存回调会取消活动下载和命令；驱动必须响应中断并关闭连接。Windows 窗口或
WebView 关闭后，驱动不得再向前端投递迟到响应。

## 错误行为

| 错误码 | 场景 |
| --- | --- |
| `INVALID_ARGS` | 驱动名、URL、语句、参数或 SQL 语法非法 |
| `PERMISSION_DENIED` | SQLite 路径越界或驱动业务授权失败 |
| `NOT_FOUND` | 连接 ID 不存在或已关闭 |
| `IO_ERROR` | 打开、网络、磁盘、执行或关闭失败 |
| `NOT_SUPPORTED` | 驱动未注册或平台没有对应适配器 |
| `TIMEOUT` | 请求超过前端或宿主时限 |
| `CANCELLED` | 请求被应用或宿主取消 |

## 排查路径

1. 先调用 `KSM.sql.drivers()`，确认目标驱动存在。
2. SQLite 连接失败时检查虚拟根、目录、文件名和只读限制。
3. 远程连接失败时检查驱动的 URL 解析、TLS、证书、网络权限和数据库端口。
4. SQL 语法错误时检查方言、占位符和参数数量。
5. 连接失效时检查是否在 `finally` 中关闭，或是否被宿主生命周期取消。
6. 需要数据库迁移时在应用层执行版本表和幂等迁移，不把迁移状态放进前端全局变量。
