# SQLite 扩展库

## 功能

`examples/extensions/sqlite` 是 BT 官方 SQLite 文件数据库扩展库示例。它通过 shared WASM 扩展在 worker 内保留 SQLite 连接，支持链式 SQL 构建、参数绑定、事务、并发读、busy timeout、结果上限和对象释放。

该扩展不是内置标准库。使用前需要把扩展编译成 `module.wasm`，再打包为 `.bts` 并安装到项目 `extensions/` 目录。官方扩展库安装可直接使用：

```text
bt install sqlite
```

## 语法

```bt
db = sqlite_open(path, options)

row = db.query(sql).bind(value).one()
rows = db.query(sql).bind(value).all()
ret = db.query(sql).bind(value).exec()

ret = db.query(sql).bind(prefix).binds(rows).batch(size).workers(count).exec()
changed = db.transaction(statements)

db.close()
```

## sqlite_open

打开 SQLite 数据库文件，并返回 `Sqlite` 连接对象。

### 语法

```bt
db = sqlite_open(path, options)
```

### 参数

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| `path` | String | 是 | 无 | SQLite 数据库文件路径。bindings 使用 `path_write` role，路径必须位于项目根目录内。 |
| `options` | Object | 是 | `{}` | 连接配置对象。 |

### options 字段

| 字段 | 类型 | 必填 | 默认值 | 范围 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| `wal` | Bool | 否 | `false` | `true` 或 `false` | 是否启用 SQLite WAL 日志模式。`true` 会执行 `PRAGMA journal_mode = WAL`，读写并发体验通常更好，但数据库旁边会出现 `-wal`、`-shm` 辅助文件。单脚本临时数据库可以不启用；Web 服务、桌面应用、长期运行项目建议启用。 |
| `busy_timeout_ms` | Int | 否 | `1000` | `0..=300000` | 数据库被其他连接锁住时，最多等待多少毫秒再报错。`1000` 表示最多等 1 秒；`0` 表示不等待。它只处理 SQLite 文件锁等待，不是 SQL 查询执行超时。 |
| `max_rows` | Int | 否 | `1000` | `1..=100000` | `all()` 单次最多允许返回多少行。超过后会报错，防止一次把大量数据读进 BT VM。只想取一页数据时，建议 SQL 自己加 `LIMIT`。 |
| `max_result_bytes` | Int | 否 | `4194304` | `1..=16777216` | `one()` 和 `all()` 返回结果的字节估算上限。默认约 4MB，最大 16MB。它用来保护常驻进程内存，避免大文本或大 BLOB 一次性返回过多。 |

新手可以先传 `{}` 使用默认值；当你知道结果集可能很大、会有并发读写或想限制内存占用时，再按需配置这些字段。

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| `Sqlite` | SQLite 连接对象，`type(db)` 返回 `Sqlite`。 |

## Sqlite.query

创建链式查询对象。`query()` 只保存 SQL，不执行数据库操作。

### 语法

```bt
query = db.query(sql)
```

### 参数

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| `sql` | String | 是 | 无 | 待执行 SQL，使用 `?` 作为参数占位符。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| `SqliteQuery` | 链式查询对象，`type(query)` 返回 `SqliteQuery`。 |

## SqliteQuery.bind

追加一个普通绑定参数。绑定值会按调用顺序对应 SQL 中的 `?` 占位符。

### 语法

```bt
query = db.query(sql).bind(value)
```

### 参数

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| `value` | Any | 是 | 无 | 绑定值。支持 `null`、Bool、Int、Float、String、Bytes；不允许 `empty`。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| `SqliteQuery` | 返回同一个查询对象，便于继续链式调用。 |

## SqliteQuery.binds

追加多行批量绑定参数，只用于 `exec()`。

### 语法

```bt
query = db.query(sql).bind(prefix).binds(rows)
```

### 参数

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| `rows` | Array | 是 | 无 | 二维数组。每一行是一组绑定值；如果行元素不是数组，会按单值行处理。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| `SqliteQuery` | 返回同一个查询对象。 |

## SqliteQuery.batch

设置批量 `exec()` 的批大小统计值。它主要用于和 MySQL 标准库的批量写入写法保持一致。

SQLite 当前会在同一个事务里串行执行这些绑定行；`batch(size)` 会影响返回对象里的 `batch_count` 和 `batch_size`，方便迁移代码和观察批量规模。

### 语法

```bt
query = db.query(sql).binds(rows).batch(size)
```

### 参数

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| `size` | Int | 是 | 未调用时为 `0` | 批大小。小于 `0` 按 `0` 处理；`0` 表示使用全部绑定行作为一批。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| `SqliteQuery` | 返回同一个查询对象。 |

## SqliteQuery.workers

设置迁移兼容的工作数统计值。这个方法是为了让从 MySQL 标准库迁移过来的代码可以保留相近写法。

SQLite 的同一个连接不能像 MySQL 连接池那样并发写入；当前实现仍会在一个事务内串行执行。也就是说，`workers(4)` 不会让同一个 SQLite 连接同时并发写 4 条 SQL。

### 语法

```bt
query = db.query(sql).binds(rows).workers(count)
```

### 参数

| 参数 | 类型 | 必填 | 默认值 | 范围 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| `count` | Int | 是 | 未调用时为 `1` | `1..=4096` | 迁移兼容工作数。小于 `1` 会按 `1` 处理，大于 `4096` 会按 `4096` 处理。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| `SqliteQuery` | 返回同一个查询对象。 |

## SqliteQuery.one

执行查询并返回第一行。

### 语法

```bt
row = db.query(sql).bind(value).one()
```

### 参数

无参数。

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| Object/empty | 查询到行时返回对象；无结果返回 `empty`。SQLite `NULL` 返回 BT `null`，BLOB 返回 BT Bytes。 |

## SqliteQuery.all

执行查询并返回多行。

### 语法

```bt
rows = db.query(sql).bind(value).all()
```

### 参数

无参数。

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| Array | 返回行对象数组，受 `max_rows` 和 `max_result_bytes` 限制。 |

## SqliteQuery.exec

执行不需要返回结果集的 SQL。普通 `bind()` 执行一次；`binds()` 会在 SQLite 事务中按绑定行串行执行。

### 语法

```bt
ret = db.query(sql).bind(value).exec()
ret = db.query(sql).bind(prefix).binds(rows).batch(size).workers(count).exec()
```

### 参数

无参数。

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| Object | 返回 SQL 执行统计对象。 |

### 执行结果字段

| 字段 | 类型 | 必定存在 | 说明 |
| ------ | ------ | ------ | ------ |
| `total` | Int | 是 | 本次执行处理的绑定行数。普通 `bind()` 执行时为 `1`；`binds()` 批量执行时为绑定数组行数；空绑定行时为 `0`。 |
| `rows_affected` | Int | 是 | SQLite 报告的受影响行数。批量执行时为逐行累加值。 |
| `last_insert_id` | Int | 是 | SQLite 当前连接的 `last_insert_rowid()`。 |
| `batch_count` | Int | 是 | 按 `batch()` 计算出的批次数。SQLite 当前执行仍在一个事务内完成。 |
| `batch_size` | Int | 是 | 当前查询对象配置的批大小；未调用 `batch()` 时为 `0`。 |
| `workers` | Int | 是 | 当前查询对象配置并规范化后的工作数。SQLite 当前不并发写同一个连接。 |

## SqliteQuery.sql

返回当前 SQL 的调试文本。

### 语法

```bt
text = db.query(sql).bind(value).sql()
```

### 参数

无参数。

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| String | 返回把绑定值渲染到 `?` 占位符后的 SQL 预览文本。该文本只用于调试，不参与执行。 |

## Sqlite.transaction

在同一个 SQLite 事务中串行执行多条写语句。

### 语法

```bt
changed = db.transaction(statements)
```

### 参数

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| `statements` | Array | 是 | 无 | 事务语句数组。元素可以是 SQL 字符串，也可以是 `{ sql, binds }` 对象。 |

### statements 对象字段

| 字段 | 类型 | 必填 | 默认值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| `sql` | String | 是 | 无 | 待执行 SQL。 |
| `binds` | Array | 否 | `[]` | SQL 参数数组。支持 `null`、Bool、Int、Float、String、Bytes；不允许 `empty`。 |
| `params` | Array | 否 | `[]` | 旧字段别名，建议新代码使用 `binds`。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| Int | 事务内累计影响行数。 |

## close

释放查询对象或数据库连接对象。

### 语法

```bt
query.close()
db.close()
```

### 参数

无参数。

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| Bool | 成功释放返回 `true`；释放后旧句柄失效。 |

## 代码示例

```bt
fs('@/data').create_dir()

db = sqlite_open('@/data/sqlite-demo.db', {
    wal: true,
    busy_timeout_ms: 1000,
    max_rows: 100,
    max_result_bytes: 1048576
})

// 输出：Sqlite
print type(db)

db.query('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, payload BLOB, note TEXT)').exec()
db.query('DELETE FROM users').exec()

ret = db.query('INSERT INTO users (name, payload, note) VALUES (?, ?, ?)')
    .bind('Alice')
    .bind(bytes('4254', 'hex'))
    .bind(null)
    .exec()

// 输出：1
print ret.rows_affected

db.query('INSERT INTO users (name, payload, note) VALUES (?, ?, ?)')
    .binds([
        ['Bob', bytes('0102', 'hex'), 'writer'],
        ['Carol', bytes('0304', 'hex'), 'writer']
    ])
    .batch(2)
    .workers(1)
    .exec()

row = db.query('SELECT name, payload, note FROM users WHERE name = ?').bind('Alice').one()

// 输出：Alice
print row.name

// 输出：true
print is_null(row.note)

missing = db.query('SELECT name FROM users WHERE name = ?').bind('Missing').one()

// 输出：true
print is_empty(missing)

rows = db.query('SELECT id, name FROM users ORDER BY id').all()

// 输出：3
print rows.len()

changed = db.transaction([
    { sql: 'UPDATE users SET note = ? WHERE name = ?', binds: ['reader', 'Bob'] },
    { sql: 'UPDATE users SET note = ? WHERE name = ?', binds: ['reader', 'Carol'] }
])

// 输出：2
print changed

db.close()
```

## 构建

```text
rustup target add wasm32-wasip1
cargo build --manifest-path examples/extensions/sqlite/Cargo.toml --target wasm32-wasip1 --release
copy examples\extensions\sqlite\target\wasm32-wasip1\release\sqlite.wasm examples\extensions\sqlite\module.wasm
cargo run -- ext build examples/extensions/sqlite -o examples/extensions/sqlite/sqlite.bts
```

`rusqlite` 的 bundled SQLite 在 WASI 目标下需要 C 编译器。Windows 环境需要先安装 LLVM clang 或 WASI SDK，并确保 `clang` 在 `PATH` 中。

## 注意事项

- SQLite 扩展的主用法和 MySQL 标准库保持一致：先 `query(sql)`，再 `bind()` 或 `binds()`，最后调用 `one()`、`all()` 或 `exec()`。
- 扩展方法参数数量由 `bindings.json` 严格校验；`bind()` 每次只绑定一个值，需要多个参数时连续调用多次。
- `binds()` 只支持 `exec()`，不支持 `one()` 和 `all()`。
- `workers()` 为 MySQL 迁移兼容接口；SQLite 当前不会并发写同一个连接。
- `all()` 必须设置合理的 `max_rows` 和 `max_result_bytes`，避免大结果无控进入 BT VM。
- `close()` / `SqliteQuery.close()` 应显式调用；bindings 已标记 `lifecycle: "dispose"`，调用成功后旧句柄失效。
- `busy_timeout_ms` 只处理 SQLite 锁等待，不是 SQL 逻辑超时；shared worker 的 `call_timeout_ms` 仍由宿主 ExtensionService 负责。
