# 实战：把人工操作变成可重跑任务

LLMS 索引： [llms.txt](/llms.txt)

---

现在把连接保护、可靠脚本、确定性数据、最小负载和证据清单组合成一个任务。它会创建并覆盖 `shop.ch02_fixture`，因此只能在明确的 L1 教学数据库运行，不能把“表名前缀看起来安全”当作生产授权。

风险分级：

- `setup`：`R1·可逆变更`，创建或重建本章专属 100 行夹具；
- `verify`、`baseline`：`R0·观察`，其中 pgbench 只读；
- `inject-error`：`R2·破坏性演练`，故意制造语法错误，但由单事务回滚隔离；
- `reset`：`R2·破坏性演练`，只删除 `shop.ch02_fixture`，需要双重确认令牌。

## 2.7.1 生成 `pg36_shop` 初始数据与校验摘要 {#item-2-7-1}

下载本章全部实验文件到同一目录，至少包括：

```text
context.sql
setup.sql
verify.sql
workload.sql
broken.sql
reset.sql
task.sh
```

复制[service file 示例](/labs/ch02/pg_service.conf.example)，替换主机并设置私有权限：

```bash
chmod 600 "$PWD/pg_service.conf"
export PGSERVICEFILE="$PWD/pg_service.conf"
export PGSERVICE=pg36-admin
```

为 `dbuser_dba` 准备 passfile 或等价的非交互凭据。本书不提供真实密码，也不要求把密码写入 service file。先人工确认落点：

```bash
psql -X -w "service=$PGSERVICE" <<'PSQL'
\conninfo
SELECT current_database(), session_user, pg_is_in_recovery();
PSQL
```

必须是 `pg36_shop`、受控管理员且 `pg_is_in_recovery() = false`。

### setup 怎样收敛

[`setup.sql`](/labs/ch02/setup.sql)先包含 `context.sql`，然后在事务内：

1. 创建 `shop.ch02_fixture`（若不存在）；
2. 从 `pg_attribute` 计算五列的名称、类型与非空形状；
3. 发现同名表形状漂移则抛出异常；
4. 截断本章专属表并按确定公式生成 100 行；
5. 给 `pg36_app` 写权限、给 `pg36_ro` 只读权限；
6. 提交事务。

夹具故意不是电商领域模型：

| 列 | 用途 |
|---|---|
| `fixture_id` | 稳定排序键与 pgbench 选择范围 |
| `sku` | 可读、可计算的唯一字符串 |
| `label` | 哈希派生文本 |
| `amount` | 确定的 `numeric(10,2)` 值 |
| `payload` | 校验与读取负载 |

ch03 会从业务规则重新设计正式模型；本表只训练工作流，避免在建模之前偷渡随意业务约束。

运行：

```bash
chmod +x task.sh
export PG36_EVIDENCE_DIR="$PWD/evidence/ch02/setup-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh setup
./task.sh verify
```

也可直接运行 SQL：

```bash
psql -X -w "service=pg36-admin" \
  -v ON_ERROR_STOP=1 \
  -f setup.sql

psql -X -w "service=pg36-admin" \
  -v ON_ERROR_STOP=1 \
  -f verify.sql
```

基线版本实测摘要：

```text
status=ok
database=pg36_shop
effective_role=pg36_owner
row_count=100
min_id=1
max_id=100
checksum=00ed4599a6ed75e4441f5211909480fa
```

再次执行 setup 与 verify，应得到相同状态。若校验和不同，先检查脚本版本哈希、服务端主要版本与本地是否修改过生成公式；不要更新“期望值”来迁就未知漂移。

### 形状漂移为什么要失败

假如已有 `shop.ch02_fixture` 只是同名、列却不同，`CREATE TABLE IF NOT EXISTS` 会发 NOTICE 后继续。形状保护会随后抛出异常，整个事务不再 `TRUNCATE`。这才是可重入：认识并拒绝未知中间状态，而不是把所有错误压成“对象已存在”。

## 2.7.2 从 Pigsty 服务端点执行并保存证据 {#item-2-7-2}

综合入口是 [`task.sh`](/labs/ch02/task.sh)。它采用 Linux Shell 的严格模式和 `umask 077`，检查 `psql`、`pgbench` 与 `sha256sum`，再把每类输出写入独立文件。

```bash
export PGSERVICEFILE="$PWD/pg_service.conf"
export PGSERVICE=pg36-admin
export PG36_EVIDENCE_DIR="$PWD/evidence/ch02/all-$(date -u +%Y%m%dT%H%M%SZ)"

./task.sh all
```

`all` 的顺序固定：

```mermaid
sequenceDiagram
  participant T as task.sh
  participant H as Pigsty 5436 / HAProxy
  participant P as PostgreSQL primary
  T->>H: service=pg36-admin
  H->>P: direct primary connection
  T->>P: capture manifest
  T->>P: setup deterministic fixture
  T->>P: verify state
  T->>P: pgbench 20 read-only transactions
  T->>P: run broken.sql in one transaction
  P-->>T: syntax error; rollback
  T->>P: verify fixture_id=999 is absent
  T-->>T: write exit code and evidence path
```

任务使用的是 Pigsty `default` 服务，而不是固定实例 `5432`。服务层提供“当前主库直连”意图，PostgreSQL 仍负责事务、角色、目录和数据。若 `5436` 在你的配置中含义不同，必须修改 service file 并在清单中记录，不要改书中预期输出来掩盖端点差异。

### 清单与证据

成功后目录应包含：

```text
manifest.txt
setup.stdout
setup.stderr
verify.txt
verify.stderr
pgbench.txt
pgbench.stderr
broken.stdout
broken.stderr
broken.status
```

`manifest.txt` 记录 UTC 时间、任务动作、service 名、客户端版本、七个执行文件的 SHA-256，以及服务端版本、数据库、登录角色和恢复状态。它有意不打印 host、密码或 passfile 内容；如组织审计需要记录脱敏端点，可在外层清单增加。

验证重点：

```bash
sed -n '1,120p' "$PG36_EVIDENCE_DIR/verify.txt"
sed -n '1,160p' "$PG36_EVIDENCE_DIR/pgbench.txt"
sed -n '1,80p'  "$PG36_EVIDENCE_DIR/broken.status"
```

期望：

```text
row_count=100
checksum=00ed4599a6ed75e4441f5211909480fa
number of transactions actually processed: 20/20
number of failed transactions: 0 (0.000%)
exit_code=3
rollback_marker_count=0
```

不验收具体 latency 或 TPS。它们会随环境变化，保留在证据中供观察，不作为通过条件。

### 分动作重跑

```bash
./task.sh setup
./task.sh verify
./task.sh baseline
./task.sh inject-error
```

每次最好给 `PG36_EVIDENCE_DIR` 一个新路径，防止覆盖上次失败证据。`verify` 和 `baseline` 假设夹具已经存在；`inject-error` 会先验证正常基线，再注入错误。

task 的目标是把协议做显式，并不替代通用工作流平台。生产上的 CI、Ansible、Kubernetes Job 或调度器仍应保留同样语义：输入、目标保护、超时、失败状态、证据、重试策略和回退边界。

## 2.7.3 注入脚本错误，验证停止、修复与复位 {#item-2-7-3}

[`broken.sql`](/labs/ch02/broken.sql)先插入一行标记，再故意把 `SELECT` 写成 `SELEC`：

```sql
INSERT INTO shop.ch02_fixture
    (fixture_id, sku, label, amount, payload)
VALUES
    (999, 'SKU-0999', 'must-be-rolled-back', 9.99, md5('broken'));

SELEC 'intentional syntax error';
```

任务调用：

```bash
psql -X -w \
  --single-transaction \
  "service=pg36-admin" \
  -v ON_ERROR_STOP=1 \
  -f broken.sql
```

必须同时满足三项：

1. stderr 含明确语法错误与位置；
2. `psql` 返回状态 `3`；
3. 新连接查询 `fixture_id = 999` 得到 `0` 行。

只满足前两项不够。若忘记 `--single-transaction`，`ON_ERROR_STOP` 会停止后续发送，却无法撤销已经自动提交的 INSERT。错误退出与状态回滚是两个独立性质。

### 修复并不自动等于正确

把 `broken.sql` 复制成临时 `repaired.sql`，将 `SELEC` 改为 `SELECT` 后再次以单事务运行，标记行会成功提交。此时语法已修复，但 `verify.sql` 会因为行数变成 101、确定公式不匹配而失败。

这说明：

- 修复执行错误，只证明脚本能跑完；
- 状态验证才证明结果符合任务契约；
- 幂等 setup 可以把本章拥有的夹具重新收敛到 100 行；
- 未经定义的数据不能因为“是成功 SQL 写进去的”就留在基线。

运行：

```bash
./task.sh setup
./task.sh verify
```

确认校验和恢复。不要在有业务价值的表上用 `TRUNCATE + 重建` 套用这个教学复位模式。

### 显式 reset

默认 `all` 不删除夹具。若要回到 ch01 末尾状态，需要两个一致令牌：

```bash
export PG36_RESET_TOKEN=RESET_CH02_FIXTURE
./task.sh reset
unset PG36_RESET_TOKEN
```

Shell 先检查环境变量，SQL 文件再检查 `confirm_reset`。脚本只执行：

```sql
DROP TABLE IF EXISTS shop.ch02_fixture;
```

它不会删除 `pg36_shop`、`shop` 模式或 ch01 的角色。完成后：

```sql
SELECT to_regclass('shop.ch02_fixture') IS NULL AS removed;
```

应返回 `true`。若下一章继续使用案例，重新执行 `./task.sh setup`，不要 reset。

### 本章最终验收

逐项打勾：

- [ ] service file 与 passfile 分离，Git 中没有密码；
- [ ] 正确目标通过 context，错误数据库返回状态 `3`；
- [ ] setup 连续执行两次仍得到 100 行和固定校验和；
- [ ] 人读探索使用元命令，机器证据查询明确目录字段；
- [ ] 所有脚本设置 `ON_ERROR_STOP`，调用方保存原始退出码；
- [ ] CSV 的 NULL、编码、顺序和错误策略明确；
- [ ] pgbench 完成 20/20、零失败，且未宣称固定 TPS；
- [ ] custom dump 能列清单、恢复到隔离数据库并验证；
- [ ] 故障注入返回 `3`，标记行回滚；
- [ ] reset 需要令牌且只删除本章表。

达到这些条件后，读者拥有的不只是几个命令，而是一套后续 34 章都能复用的执行语法：**先验证上下文，再应用动作；用状态而不是屏幕感觉验收；把失败当作需要设计的正常路径。**

下一章进入 [ch03《正本清源：从业务规则到关系模型》](/logical-data-model/)。`ch02_fixture` 只作为确定性输入与反例，正式业务表将从业务不变量重新推导。

## 参考资料

- [PostgreSQL 18：psql](https://www.postgresql.org/docs/18/app-psql.html)
- [PostgreSQL 18：pgbench](https://www.postgresql.org/docs/18/pgbench.html)
- [PostgreSQL 18：pg_dump](https://www.postgresql.org/docs/18/app-pgdump.html)
- [PostgreSQL 18：pg_restore](https://www.postgresql.org/docs/18/app-pgrestore.html)
- [Pigsty v4.5：服务与接入](https://pigsty.io/docs/pgsql/service/)

---

[上一节：最小逻辑备份闭环](../06/) · [返回本章目录](../) · [下一章：正本清源：从业务规则到关系模型](/logical-data-model/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
