# 第 12 章 服务端编程 - 配套代码

本章脚本带你「亲眼看到」PL/pgSQL 函数 / 触发器 / 事件触发器 / LISTEN-NOTIFY / PG 11+ 存储过程在跑：从应用代码看不见的"幕后操作"全部跑出来。

## 准备工作

1. 跑 `psql -h 127.0.0.1 -U postgres -d learn_pg -f ../init.sql` 初始化 `ch12_orders` / `ch12_orders_audit` / `ch12_ddl_log` / `ch12_orders_archive`，以及函数/触发器（全部带 `ch12_` 前缀）。
2. 安装依赖：`pip install "psycopg[binary]>=3.1"`。
3. （可选）`export PG_DSN="host=... port=... dbname=... user=..."` 覆盖默认连接。
4. `03_event_trigger.py` 创建/删除事件触发器，需要 **superuser** 权限（默认 `postgres` 用户即可）。

## 脚本一览（推荐运行顺序）

| 脚本 | 一句话说明 | 关键 PG 特性 |
|------|------------|--------------|
| `01_plpgsql_function.py` | 调用 `ch12_compound_interest` 标量函数、`ch12_list_orders` 表函数；故意触发业务异常 | PL/pgSQL 函数 / `RAISE EXCEPTION` |
| `02_trigger_audit.py` | INSERT/UPDATE/DELETE `ch12_orders`，让触发器自动写 `ch12_orders_audit` | 行级触发器 / `TG_OP` / `to_jsonb` |
| `03_event_trigger.py` | 临时安装"禁止 DROP TABLE"事件触发器并测试拦截，再清理 | 事件触发器 / `pg_event_trigger_*` |
| `04_listen_notify.py` | N 个订阅者 LISTEN `ch12_orders_changed`，发布者 INSERT 触发 NOTIFY | LISTEN/NOTIFY / `pg_notify` |
| `05_procedure_with_commit.py` | 调用 `CALL ch12_archive_done_orders(20)`，演示存储过程内 COMMIT 分批归档 | PG 11+ PROCEDURE / 内部 COMMIT |

运行示例：

```bash
python 01_plpgsql_function.py
python 02_trigger_audit.py
python 03_event_trigger.py
python 04_listen_notify.py --subs 3 --events 5
python 05_procedure_with_commit.py
```

## 预期输出

`02_trigger_audit.py` 会显示：每次 INSERT/UPDATE/DELETE `ch12_orders` 都会自动在 `ch12_orders_audit` 多出一行（应用代码并没有写任何审计逻辑）：

```
>>> INSERT 一条订单
新增订单 id=12
>>> UPDATE 状态为 paid
>>> UPDATE 状态为 done
>>> DELETE
--- ch12_orders_audit 最近 6 行 ---
(id=21, op='DELETE', order_id=12, ...)
(id=20, op='UPDATE', order_id=12, new='done', old='paid', ...)
...
```

`05_procedure_with_commit.py` 会按 batch_size 分批输出 RAISE NOTICE。

## 常见报错

- `relation "ch12_orders" does not exist` → 没跑 `init.sql`，先 `psql ... -f ../init.sql`
- `function ch12_compound_interest(...) does not exist` → 同上，函数定义也在 `init.sql`
- `permission denied to create event trigger` → `03_event_trigger.py` 需要 superuser
- `event trigger "ch12_no_drop_table" already exists` → 上次脚本异常退出残留，手动 `DROP EVENT TRIGGER ch12_no_drop_table; DROP EVENT TRIGGER ch12_log_ddl;` 再重跑
- `cannot commit while a subtransaction is active` → 在事务里 `CALL` 含 COMMIT 的过程；连接必须 `autocommit=True`，且不要在 `with conn.transaction():` 里调用
- LISTEN 收不到 → 检查触发器 `ch12_trg_orders_audit` 是否存在；`SELECT pg_notification_queue_usage()` 应小于 0.25
