# PostgreSQL
Deft 自带原生 PostgreSQL 客户端——无需 `libpq`,也无需安装
驱动。`pg/*` 标准库函数直接使用 PostgreSQL 线路
协议(v3),在脚本二进制、ANSI 二进制和完整 TUI 中
行为完全一致。默认使用 SCRAM-SHA-256 认证,
并支持 MD5 和明文回退;TLS 通过 HTTP 客户端所用的
同一 mbedTLS 栈进行带内升级。
与 `keyval/*` 一样,每个查询都返回一个 Future——`[await ...]`
会挂起协程直到回复到达,因此整条流水线
可以在一个连接上运行而不阻塞事件循环。
注意:postgres 的 Zig 代码取自 Bun 3.14。
## 连接
三种连接形式,完全等价:
# URL (bun/libpq style) def db [pg/connect "postgres://user:pass@localhost:5432/mydb?sslmode=require"] # libpq kv-string def db [pg/connect "host=localhost port=5432 user=deft password=secret dbname=mydb sslmode=disable"] # Positional + opts map def db [pg/connect "localhost" 5432 %{ :user "deft" :password "secret" :database "mydb" :sslmode "disable" }]
连接是惰性拨号的——第一条命令才触发
TCP 连接,因此 `pg/connect` 本身从不阻塞。在任何命令之前
调用 `pg/info` 会报告 `:state :disconnected`。
### 选项 map
| 选项 | 含义 |
|---|---|
| `:host` | 服务器主机名或 IP |
| `:port` | 服务器端口(默认 5432) |
| `:user` / `:username` | 角色名 |
| `:password` | 密码 |
| `:database` / `:db` | 数据库名(默认为用户名) |
| `:sslmode` | `"disable"` `"prefer"` `"require"` `"verify-ca"` `"verify-full"` |
| `:ssl` | 布尔简写——`true` = `"require"`,`false` = `"disable"` |
| `:application-name` | 在 `pg_stat_activity` 中向服务器报告 |
| `:timeout` | 命令截止时间(毫秒)(默认 60000;`0` 表示禁用) |
任何未设置的值都会应用环境变量回退:`PGHOST`、`PGPORT`、`PGUSER`、
`PGPASSWORD`、`PGDATABASE`、`PGSSLMODE`、`PGAPPNAME`。
### SSL 模式
| 模式 | 行为 |
|---|---|
| `disable` | 仅明文 |
| `prefer` | 尝试 TLS;如果服务器拒绝或握手失败则回退到明文 |
| `require` | 需要 TLS,不验证证书(仅加密,libpq 语义) |
| `verify-ca` | TLS + 证书链验证 |
| `verify-full` | TLS + 证书链 + 主机名验证 |
## Exec——DDL 和 DML
`pg/exec` 运行不返回行的 SQL。不带参数时
使用简单查询协议,因此多语句字符串可用——
便于模式迁移。带参数时则切换到
扩展协议(`$1..$n` 占位符)。
set r [await [pg/exec $db ` CREATE TABLE IF NOT EXISTS items ( id serial PRIMARY KEY, name text, price numeric ) `]] # %{:tag "CREATE TABLE" :command "CREATE" :rows 0} set r [await [pg/exec $db "INSERT INTO items (name, price) VALUES ($1, $2)" "widget" 9.5]] # %{:tag "INSERT 0 1" :command "INSERT" :rows 1}
结果 map 原样携带命令标签(`:tag`)、
命令词(`:command`)和受影响行数(`:rows`)。
## 查询——SELECT
def rows [await [pg/query $db "SELECT id, name, price FROM items ORDER BY id"]] for row $rows { echo "$row~name (price $row~price)" } def one [await [pg/query-one $db "SELECT * FROM items WHERE name = $1" "widget"]] # first row as a map, or nil def n [await [pg/scalar $db "SELECT count(*) FROM items"]] # first column of the first row, or nil
行是以关键字列名为键的 map(`$row~name`)。列
顺序可通过 `pg/columns` 获取(行 map 按哈希排序)。
### 惰性序列
`pg/query-seq` 返回由游标支持的惰性序列——行字节在
迭代时解码,因此 `for`、`map`、`filter` 和流水线运行时
无需先物化整个结果:
set seq [await [pg/query-seq $db "SELECT id, name FROM items ORDER BY id"]] set names $seq |> map {|row| $row~name} |> collect for row in $seq { echo $row~name }
序列在被实现之前只能迭代一次;之后再迭代
会重放已实现的缓存。
## 参数
参数为可变参数或单个向量,按顺序对应 `$1..$n`:
[await [pg/exec $db "INSERT INTO items VALUES ($1, $2, $3)" "a" 1 @{ "x" "y" }]]
| Deft 值 | 发送为 |
|---|---|
| `nil` | SQL NULL |
| `true` / `false` | `t` / `f` |
| number | 十进制文本(整数不带 `.0`) |
| string | 文本 |
| keyword | 其名称 |
| vector | 数组字面量 `{...}` |
| 其他任何值 | 调用时出错 |
## 类型映射
行以文本形式到达;解码由列的类型 OID 驱动:
| PostgreSQL 类型 | Deft 值 |
|---|---|
| `int2` `int4` `int8` `oid` `xid` `cid` | number(f64——超出 ±2^53 的值会丢失精度,与 `sqlite` 相同的取舍) |
| `float4` `float8` `numeric` | number |
| `bool` | bool |
| `text` `varchar` `char` `name` `json` `xml` `uuid` `inet` `cidr` `macaddr` `money` `bit` `varbit` | string |
| `date` `time` `timestamp` `timestamptz` `timetz` `interval` | string(服务器的 ISO 文本形式) |
| `bytea` | 原始字节字符串(Deft 的二进制约定——NUL 安全,与 `crypto/*` 相同) |
| 数组(`int[]`、`text[]` 等) | vector(嵌套数组亦然;`NULL` 元素 → nil) |
| NULL | nil |
| 未知 OID | 原样字符串 |
`bytea` 参数以 `\x` 十六进制文本发送:原始字节字符串
会原样传递,因此 `"\x414243"` 会变成字节 `ABC`。
## 事务
[await [pg/begin $db]] [await [pg/exec $db "INSERT INTO items (name, price) VALUES ($1, $2)" "kept" 1]] [await [pg/commit $db]] # or [await [pg/begin $db]] [await [pg/rollback $db]]
事务内部的错误会在服务器端中止事务:后续
语句会失败,直到你发出 `pg/rollback`(或 `pg/commit`)。
## LISTEN / NOTIFY
注册回调并通过 `pg/exec` 执行 `LISTEN`;通知
作为推送事件在同一连接上到达:
[pg/on-notify $db {|ev| echo "channel $ev~channel: $ev~payload" # %{ :pid :channel :payload } }] [await [pg/exec $db "LISTEN items_changed"]] [await [pg/exec $db "NOTIFY items_changed, 'row 42 updated'"]]
服务器警告(`NOTICE`、`WARNING` 等)在设置了处理器时
会路由到 `pg/on-notice`,携带 `%{ :message :severity :code }`。
## 内省
[await [pg/tables $db]] # @{ "items" "users" ... } — user tables only [await [pg/columns $db "items"]] # @{ %{ :name "id" :type "integer" :not_null true :default nil } ... }
## 错误
查询失败会以 `%Err` 拒绝 future,其中携带 SQLSTATE
码和服务器消息字段:
set r [await [pg/query $db "SELECT * FROM missing_table"]] [err? $r] # true $r~message # relation "missing_table" does not exist $r~code # 42P01 (SQLSTATE) $r~severity # ERROR $r~detail # optional extra detail $r~hint # optional hint $r~position # optional character offset in the query
出错后连接仍然可用(失败的语句是
唯一的牺牲品——事务除外,见上文)。
## 连接状态与关闭
[pg/info $db] # %{ :state :host :port :user :database :sslmode :ssl # :txn :pending :pid :server-version } [pg/close $db] # sends Terminate, rejects pending requests → :ok
`pg/close` 是幂等的;使用已关闭的客户端会出错。
## 尚不支持
- `COPY` 导入/导出——客户端会以明确的错误拒绝
- Unix 套接字连接(仅 TCP)
- 二进制格式的参数/结果(始终使用文本格式)
- 语句缓存 / 连接池