# SQLite
Deft 内置了 SQLite——无需安装驱动,也无需加载共享库。`sqlite/*` 标准库
函数在脚本二进制、ANSI 二进制和完整 TUI 中行为完全一致。SQLite 始终可用,
无需任何构建标志。
## 打开与关闭
def db [sqlite/open ":memory:"] # in-memory database # ... or ... def db [sqlite/open "data.db"] # file-backed sqlite/close $db
句柄是一个 `native_resource`。关闭它会释放底层的 SQLite 句柄;使用已关闭
的句柄会出错。
## Exec——DDL 与 DML
`sqlite/exec` 运行不返回行的 SQL。返回受更改影响的行数。
sqlite/exec $db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)" sqlite/exec $db "INSERT INTO users (name, age) VALUES ('Alice', 30)"
### 参数化 exec
在 SQL 字符串之后以列表形式传入参数。占位符是 `?`:
sqlite/exec $db "INSERT INTO users (name, age) VALUES (?, ?)" @{ "Alice" 30 } sqlite/exec $db "INSERT INTO users (name, age) VALUES (?, ?)" @{ "Bob" 25 }
数字、浮点数、字符串和 nil 都能正确绑定。无效 SQL 返回 `%Err`——用
`err?` 检查:
set r [sqlite/exec $db "INSERT INTO bogus_table VALUES (1)"] if [err? $r] { echo "failed: $r~message" }
## Query——SELECT
`sqlite/query` 返回以列名为键的映射列表:
def rows [sqlite/query $db "SELECT * FROM users"] for row $rows { echo "$row~name (age $row~age)" } # Alice (age 30) # Bob (age 25)
带参数时:
def adults [sqlite/query $db "SELECT * FROM users WHERE age >= ?" @{ 18 }]
NULL 单元格以 `nil` 返回。
### `query-one`——单行
`sqlite/query-one` 返回第一个匹配的行,或 `nil`:
def alice [sqlite/query-one $db "SELECT * FROM users WHERE name = ?" @{ "Alice" }] if $alice { echo "found: $alice~name" }
### `scalar`——单值
`sqlite/scalar` 返回第一行的第一列,或 `nil`:
def count [sqlite/scalar $db "SELECT COUNT(*) FROM users"] echo "user count: $count"
## 内省
sqlite/tables $db # => list of table names sqlite/columns $db "users" # => column metadata list
`sqlite/columns` 返回 `PRAGMA table_info(...)` 中的行。每行包含 `:name`、
`:type`、`:notnull`、`:pk` 等字段。
## 事务
sqlite/begin $db try { sqlite/exec $db "INSERT INTO users (name, age) VALUES (?, ?)" @{ "Carol" 28 } sqlite/exec $db "UPDATE counters SET value = value + 1 WHERE name = 'users'" sqlite/commit $db } catch e { sqlite/rollback $db echo "rolled back: $e~message" }
回滚会丢弃自对应 `begin` 以来的一切:
sqlite/begin $db sqlite/exec $db "INSERT INTO users (name, age) VALUES ('Temp', 0)" sqlite/rollback $db # The 'Temp' row is gone.
## 管道与转换器
查询结果是普通列表,因此所有集合操作都可以直接使用:
# Filter and map results def adults $rows |> filter { ($it~age >= 18) } |> map { $it~name } |> collect # Aggregate set total-age $rows |> map { $it~age } |> reduce 0 { $acc + $it } # Index by name def by-name [from-entries [map $rows {|r| @{ $r~name $r }}]] echo $by-name~Alice~age # => 30
对于可复用的变换,将其抽成转换器:
def xf [comp [filter { ($it~age >= 18) }] [map { $r~name }]] [into @{} xf $rows]
def db [sqlite/open "data.db"] runtime/on-shutdown { try { sqlite/close $db } catch _e { nil } }
该模式在 `packages/chat/src/chat.dft` 和
`packages/stdext/src/semantic.dft` 中用于生产环境的数据库持久化。
## 完整示例
#!/usr/bin/env deft def db [sqlite/open ":memory:"] sqlite/exec $db """ CREATE TABLE posts ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, body TEXT ) """ sqlite/exec $db "INSERT INTO posts (title, body) VALUES (?, ?)" @{ "First Post" "This is my first post." } sqlite/exec $db "INSERT INTO posts (title, body) VALUES (?, ?)" @{ "Second Post" "More content here." } def count [sqlite/scalar $db "SELECT COUNT(*) FROM posts"] echo "Total posts: $count" def posts [sqlite/query $db "SELECT * FROM posts ORDER BY id"] # Render via pipeline $posts |> map { " - $it~title" } |> each { echo $it } sqlite/close $db
## 并发说明
SQLite 句柄不能安全地在 OS 线程之间共享。在单个运行时内这不是问题——只有
一个线程会访问该句柄。对于跨运行时访问,可以:
- 每个运行时针对同一文件打开单独的句柄(SQLite 通过 WAL 模式处理并发
文件访问),或者
- 使用 `runtime/eval` 将数据库工作分派到唯一的所属运行时。
## `sqlite/*` 快速参考
| 函数 | 返回值 | 用途 |
|---|---|---|
| `[sqlite/open ":memory:" \| path]` | 句柄 | 打开 |
| `[sqlite/close $db]` | nil | 关闭 |
| `[sqlite/exec $db sql ?params]` | 数字 | DDL/DML;受影响的行数 |
| `[sqlite/query $db sql ?params]` | 映射列表 | SELECT |
| `[sqlite/query-one $db sql ?params]` | 映射或 nil | 第一行 |
| `[sqlite/scalar $db sql ?params]` | 值或 nil | 第一行的第一列 |
| `[sqlite/tables $db]` | 列表 | 所有表名 |
| `[sqlite/columns $db "table"]` | 列表 | 列元数据 |
| `[sqlite/begin $db]` | nil | 开始事务 |
| `[sqlite/commit $db]` | nil | 提交 |
| `[sqlite/rollback $db]` | nil | 回滚 |