# 实战：评审三个候选扩展

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

---

本节把前六节压成一个 release proposal：

```text
three problems
  -> three decisions
  -> package/control evidence
  -> privilege boundaries
  -> member/index/query evidence
  -> one object upgrade
  -> dump and portable exit
  -> exact reset and rebuild
```

它不是扩展性能评测，也不是生产安装脚本。实验的价值是证明评审结构能够
运行、失败、复位和重复。

> **环境与破坏边界**
>
> 正式 evidence 来自 Homebrew PostgreSQL 18.6 直连服务，未在 Pigsty L1
> 运行。`task.sh all` 会精确删除并重建带本章 marker 的 `shop_ch14`、
> `pg_trgm` 和 `vector`，只适合本地/开发数据库。生产变更不得运行这一
> “删后重建”入口。

## 14.7.1 一个接受、一个试点、一个拒绝 {#item-14-7-1}

### 问题 A：有界单字段拼写容错

候选 `pg_trgm`。

问题边界：

```text
field: one title text column
query: typo-tolerant lookup
fixture typo: "PostgreSQL extenson"
result limit: 3
not in scope: language segmentation, semantic ranking, document search
```

原生替代：

- 精确 B-tree；
- 规范化前缀搜索；
- PostgreSQL FTS；
- 应用侧拼写纠正。

采用理由：

- PostgreSQL contrib 扩展；
- 当前 control 为 trusted/relocatable；
- 不改变 `title text` 类型；
- GIN `gin_trgm_ops` 可由目录与计划验证；
- 可以先切回精确/FTS，再删 GIN 和扩展；
- 1.3 → 1.6 更新路径与行为回归可重复。

结论：

```text
accept pg_trgm
only for bounded fuzzy matching
```

不是批准它替代第 15 章的全部检索设计。

### 问题 B：语义近邻检索

候选 `vector`（项目/包别名常为 pgvector）。

本地 PoC：

```text
type: vector(3)
distance: L2
index: HNSW vector_l2_ops
query vector: [1,0,0]
top ids: 1,2,5
```

已证明：

- control/安装 SQL/动态库存在并有 hash；
- `trusted=false`，非超级用户创建以 `42501` 失败；
- 管理员能在目标 schema 创建 0.8.4；
- 表、类型、HNSW opclass/index 有目录证据；
- 应用角色可查询但没有表写权限；
- `embedding::text` 可导出五行；
- 全库 dump 用 `CREATE EXTENSION vector` 表示成员。

未证明：

- 真实 embedding model、dimension 与 normalization；
- 真实语料 relevance/recall；
- 过滤组合下 ANN 行为；
- 索引 build、内存、磁盘、WAL 与更新成本；
- 并发 P95/P99；
- 物理备库/failover；
- clean restore；
- PostgreSQL major upgrade；
- 在 Pigsty L1 所有节点的包一致性。

结论：

```text
pilot vector 0.8.4
bounded to an isolated workload and evidence plan
```

任何真实业务接入前必须补上第 15 章的质量语料与 L1 生命周期证据。

### 问题 C：分布式分片

候选 Citus。

当前事实：

```text
no measured single-cluster capacity breach
no shard-key contract
no co-location model
no cross-shard transaction budget
no rebalance/failure test
```

本地 Homebrew server 的 `pg_available_extensions` 也没有 Citus，但这不是
拒绝的主要理由。Pigsty 当前扩展目录提供 Citus 14.0.0；平台有包仍不能替
架构证明问题。

当前替代：

- 修正查询与索引；
- 生命周期/归档治理；
- PostgreSQL declarative partitioning；
- 垂直扩容；
- 读副本或分析副本；
- 到第 17 章测量单集群容量边界。

重新打开 ADR 的条件：

```text
measured capacity/SLO crossover
  + stable distribution key
  + transaction and uniqueness model
  + rebalance/failure/backup plan
```

结论：

```text
reject Citus now
```

拒绝的是当前采用时机，不是产品评价。

### 把结论写进数据库

[setup.sql](/labs/ch14/setup.sql) 建立：

```sql
CREATE TABLE shop_ch14.extension_review (
    candidate text PRIMARY KEY,
    extension_name text NOT NULL,
    package_alias text NOT NULL,
    decision text NOT NULL
        CHECK (decision IN ('accept', 'pilot', 'reject')),
    problem text NOT NULL,
    success_criterion text NOT NULL,
    exit_path text NOT NULL,
    review_trigger text NOT NULL,
    reviewed_on date NOT NULL
);
```

最终必须精确得到：

```text
citus:reject,pg_trgm:accept,vector:pilot
```

把 ADR 行放进实验数据库不是建议生产数据库存文档；它使 fixture checksum
同时覆盖数据与决策，防止测试脚本与文字结论分叉。

### 最小架构

```text
shop_ch14
├── extension_review
├── candidate_doc
│   ├── title text
│   └── embedding vector(3)
├── pg_trgm 1.3 -> 1.6
│   └── GIN gin_trgm_ops
└── vector 0.8.4
    └── HNSW vector_l2_ops
```

所有 schema、扩展与非成员 relation/index 带 marker：

```text
pg36 ch14 extension lifecycle lab; safe to rebuild
```

扩展成员通过 `pg_depend.deptype='e'` 识别，不要求逐个添加 comment。

## 14.7.2 在 L1 安装并验证原生对象与平台状态 {#item-14-7-2}

标题中的 L1 是目标运行形态，不是本地证据伪装。流程分两步：

1. 在受控直连 PostgreSQL 完成机制 fixture；
2. 把同一合同移植到 Pigsty L1，补齐节点、HA 与恢复证据。

### 1. 准备 libpq service

```ini
[pg36-admin]
host=/path/to/socket-or-host
port=5432
dbname=pg36_shop
user=postgres
```

```bash
export PGSERVICEFILE=/path/to/pg_service.conf
export PGSERVICE=pg36-admin
```

service 文件权限收窄，不在命令行或 evidence 打印密码。

context guard 要求：

```text
database=pg36_shop
writable primary/direct PostgreSQL
server major=14..18
session superuser=true
can SET ROLE pg36_owner
ch04-v1 model exists
pg36_app is constrained LOGIN
pg_trgm 1.3 and 1.6 support files available
vector 0.8.4 support files available
```

版本不符时脚本拒绝；读者应复制 proposal、更新版本与 golden 后重新评审，
不应删掉 guard。

### 2. 分阶段入口

```bash
./static/labs/ch14/task.sh setup
./static/labs/ch14/task.sh inventory
./static/labs/ch14/task.sh upgrade
./static/labs/ch14/task.sh dump
```

每个会精确重建 fixture，适合单独教学。最终只认：

```bash
PG36_EVIDENCE_DIR="$PWD/evidence/ch14" \
  ./static/labs/ch14/task.sh all
```

### 3. 先验证支持文件

`package-manifest.txt` 记录：

```text
pg_config path/version
server major
sharedir/pkglibdir
validation_path=direct-postgresql
pigsty_l1=not-run
```

并对：

```text
pg_trgm.control
pg_trgm--1.3.sql
pg_trgm--1.3--1.4.sql
pg_trgm--1.4--1.5.sql
pg_trgm--1.5--1.6.sql
pg_trgm.dylib/.so
vector.control
vector--0.8.4.sql
vector.dylib/.so
```

生成 SHA-256。

脚本先比较 `pg_config` major 与 live server major。PATH 指向错误 PG 安装时
立即失败，不会拿另一套支持文件做出“可用”结论。

在 Pigsty L1，这份清单要按所有主备 host 展开，而不是只在 primary 生成。

### 4. 碰撞保护与 trusted 安装

[setup.sql](/labs/ch14/setup.sql) 若发现：

- `shop_ch14` marker/owner 不符；
- `pg_trgm` 或 `vector` 已位于别的 schema；
- extension marker、owner 或版本不在允许集合；
- schema 中有未知非 extension relation/routine/type/operator/opclass；

就拒绝重建。

随后：

```sql
SET ROLE pg36_owner;

CREATE SCHEMA shop_ch14 AUTHORIZATION pg36_owner;

CREATE EXTENSION pg_trgm
  WITH SCHEMA shop_ch14
  VERSION '1.3';

RESET ROLE;
```

结果：

```text
pg_trgm_owner=pg36_owner
pg_trgm_version=1.3
vector_installed=false
```

这证明 trusted 规则与数据库 owner 权限，不表示 `pg36_owner` 是超级用户。

### 5. 注入预期特权失败

[owner-create-vector.sql](/labs/ch14/owner-create-vector.sql)：

```sql
SET ROLE pg36_owner;
CREATE EXTENSION vector
  WITH SCHEMA shop_ch14
  VERSION '0.8.4';
```

必须：

```text
psql exit=3
SQLSTATE=42501
Must be superuser to create this extension
```

若它意外成功，说明 control/权限环境与 proposal 不同，review 失败，而不是
把差异忽略。

管理员再执行 [install-vector.sql](/labs/ch14/install-vector.sql)：

```sql
CREATE EXTENSION vector
  WITH SCHEMA shop_ch14
  VERSION '0.8.4';
```

并由 owner 建表：

```sql
CREATE TABLE shop_ch14.candidate_doc (
    doc_id bigint PRIMARY KEY,
    title text NOT NULL,
    embedding shop_ch14.vector(3) NOT NULL
);
```

### 6. 建立两个可验证索引

```sql
CREATE INDEX candidate_doc_title_trgm_idx
ON shop_ch14.candidate_doc
USING gin (title shop_ch14.gin_trgm_ops);

CREATE INDEX candidate_doc_embedding_hnsw_idx
ON shop_ch14.candidate_doc
USING hnsw (embedding shop_ch14.vector_l2_ops)
WITH (m = 8, ef_construction = 32);
```

目录验收：

| index | AM | opclass | valid/ready/live |
|---|---|---|---|
| `candidate_doc_title_trgm_idx` | `gin` | `shop_ch14.gin_trgm_ops` | true/true/true |
| `candidate_doc_embedding_hnsw_idx` | `hnsw` | `shop_ch14.vector_l2_ops` | true/true/true |

`CREATE INDEX` 成功还不够；检查 `pg_index`、`pg_am` 与 `pg_opclass`，防止
名字相同但实现漂移。

### 7. 采集扩展与成员目录

[extension-inventory.sql](/labs/ch14/extension-inventory.sql)：

```text
name
object version
owner
nominal schema
relocatable
superuser/trusted/requires
member count
marker
```

更新前 PostgreSQL 18.6：

```text
pg_trgm  1.3    owner=pg36_owner  trusted=t  members=37
vector   0.8.4  owner=postgres    trusted=f  members=237
```

[member-catalog.sql](/labs/ch14/member-catalog.sql) 再按 catalog 分解：

```text
pg_trgm:
  pg_opclass, pg_operator, pg_opfamily, pg_proc, pg_type

vector:
  pg_am, pg_cast, pg_opclass, pg_operator,
  pg_opfamily, pg_proc, pg_type
```

更新后 `pg_trgm` 成员为 47。数量只冻结本次 PG18.6 build；其他 major 可有
条件差异。

### 8. 权限矩阵

`pg36_app`：

```text
USAGE shop_ch14       = true
SELECT review/docs    = true
INSERT/UPDATE/DELETE  = false
extension owner       = false
```

[app-query.sql](/labs/ch14/app-query.sql) 成功使用函数、操作符与类型；随后：

```sql
ALTER EXTENSION pg_trgm UPDATE TO '1.6';
```

必须：

```text
SQLSTATE 42501
must be owner of extension pg_trgm
```

应用使用能力与扩展管理权被分离。

### 9. 行为 baseline

模糊检索：

```sql
SELECT
    doc_id,
    round(
      shop_ch14.similarity(
        title,
        'PostgreSQL extenson'
      )::numeric,
      6
    ) AS score
FROM shop_ch14.candidate_doc
ORDER BY score DESC, doc_id
LIMIT 3;
```

结果：

```text
1  0.620690
5  0.305556
2  0.205128
```

向量检索：

```sql
SELECT
    doc_id,
    round(
      (
        embedding
        OPERATOR(shop_ch14.<->)
        '[1,0,0]'::shop_ch14.vector(3)
      )::numeric,
      6
    ) AS distance
FROM shop_ch14.candidate_doc
ORDER BY
    embedding
      OPERATOR(shop_ch14.<->)
      '[1,0,0]'::shop_ch14.vector(3),
    doc_id
LIMIT 3;
```

结果：

```text
1  0.000000
2  0.141421
5  0.282843
```

### 10. 索引计划

五行表优化器自然可能选择 seq scan。实验：

```sql
SET enable_seqscan = off;
```

只用于证明索引路径存在，不用于性能结论。

trigram：

```text
Bitmap Index Scan on candidate_doc_title_trgm_idx
```

vector：

```text
Index Scan using candidate_doc_embedding_hnsw_idx
```

生产验收应恢复默认 planner 配置，用真实数据比较：

```text
EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS)
```

并检查结果质量；不能用 `enable_seqscan=off` 证明索引值得使用。

### 11. 更新 1.3 → 1.6

[update-paths.sql](/labs/ch14/update-paths.sql) 先证明：

```text
1.3--1.4--1.5--1.6
```

[upgrade.sql](/labs/ch14/upgrade.sql) 要求 source 精确为 1.3：

```sql
SET ROLE pg36_owner;
ALTER EXTENSION pg_trgm UPDATE TO '1.6';
RESET ROLE;
```

若 source 已经变化，返回本章自定义 `P3640`，不猜迁移路径。

更新后重新采集：

- available version installed flag；
- extension/member catalog；
- index validity/opclass；
- ACL；
- 两个查询；
- 两个计划。

除 `pg_trgm_version` 与成员清单外，行为 golden 不变。

### 12. dump 的正反例

全库：

```bash
pg_dump \
  --schema-only \
  --no-owner \
  --no-privileges \
  --dbname='service=pg36-admin' \
  > database-schema.sql
```

必须包含：

```text
CREATE EXTENSION ... pg_trgm
CREATE EXTENSION ... vector
```

且不展开 `shop_ch14` 扩展成员函数/类型。

选择性 schema：

```bash
pg_dump \
  --schema-only \
  --schema=shop_ch14 \
  --no-owner \
  --no-privileges \
  --dbname='service=pg36-admin' \
  > selected-schema.sql
```

它包含应用表和索引，却没有 `CREATE EXTENSION`。review 把这个“不完整依赖”
作为预期证据，提醒恢复 runbook 先供应并创建扩展。

### 13. portable exit

[portable-export.sql](/labs/ch14/portable-export.sql)：

```text
doc_id,title,embedding_text
1,PostgreSQL extension guide,"[1,0,0]"
...
```

review 要求：

- header 精确；
- 五个主键按 1..5；
- 每个 embedding 是 bracketed text。

生产退出还需导入目标、语义比对和删依赖；本章只证明可携带 representation。

### 14. 最终不变量

[final-state.sql](/labs/ch14/final-state.sql)：

```text
review_rows=3
document_rows=5
pg_trgm_version=1.6
vector_version=0.8.4
pg_trgm_members=47
vector_members=237
trigram_top_ids=1,5,2
vector_top_ids=1,2,5
business_checksum=5398634500fe53ba1fb683e9a2c6e745
```

checksum 包含：

- 三行 ADR 内容；
- 五行文档/向量文本；
- extension name/version/schema/relocatable。

它不包含管理员用户名，避免换一个受控超级用户就改变业务 golden。

### 15. 精确复位

手工入口：

```bash
PG36_RESET_TOKEN=RESET_CH14_EXTENSION_LAB \
PG36_RESET_TARGET='pg36_shop/shop_ch14/pg_trgm+vector' \
  ./static/labs/ch14/task.sh reset
```

[reset.sql](/labs/ch14/reset.sql) 在删除前验证：

- database、writable instance、server 与角色；
- token/target；
- schema marker/owner；
- extension name/version/schema/owner/marker；
- 非成员 relation/type/routine/operator/opclass 白名单；
- 没有 `pg36-ch14-*` 活跃 worker。

删除顺序：

```sql
DROP TABLE shop_ch14.candidate_doc;
DROP TABLE shop_ch14.extension_review;
DROP EXTENSION vector;
DROP EXTENSION pg_trgm;
DROP SCHEMA shop_ch14;
```

没有 `CASCADE`。若仍有未知业务依赖，`DROP EXTENSION` 失败并暴露它。

`all` 还注入：

```text
wrong token  -> P3650
wrong target -> P3651
active worker -> P3653
```

拒绝后才精确复位，再完整重建第二遍。最终环境保留通过验收的 fixture。

### 16. 移植到 Pigsty L1

先审查 [Pigsty 声明片段](/labs/ch14/pigsty-declaration.example.yml)：

```yaml
pg_extensions:
  - pgvector

pg_databases:
  - name: pg36_shop
    schemas:
      - { name: app_ext, owner: pg36_owner }
    extensions:
      - { name: vector, schema: app_ext }
```

stock Pigsty 默认把 `pg_trgm` 启用在 `public`，无需与本地 `shop_ch14` 布局
完全相同。

L1 执行顺序：

```text
review inventory diff
  -> verify repo/alias availability for exact PG/OS/arch
  -> install package on all nodes
  -> hash control/SQL/library on all nodes
  -> verify no preload requirement for these exact versions
  -> create extension through reviewed database migration
  -> query catalog/member/index/ACL
  -> run behavior and negative tests
  -> test replica query and controlled switchover
  -> clean restore to fresh L1/clone
  -> attach evidence to a new proposal
```

L1 不应强行复用本地 proposal checksum，因为：

- schema 布局可能不同；
- package build/OS 不同；
- owner 名或 default extension state 不同；
- 应补主备/restore evidence。

复制 ADR 结构，生成属于目标 L1 的新 baseline。

## 14.7.3 产出供 ch15–ch17 复用的 ADR 模板 {#item-14-7-3}

### 交付包

本章交付不是一张“推荐扩展”表，而是：

```text
candidate-review.md
extension-adr-template.md
baseline-v1.2-proposal.json
pigsty-declaration.example.yml
lab-contract.md
SQL/Bash/Python executable evidence chain
```

[candidate-review.md](/labs/ch14/candidate-review.md) 记录三项结论；
[extension-adr-template.md](/labs/ch14/extension-adr-template.md) 提供十段结构：

1. 决策元数据；
2. 问题与边界；
3. 候选与原生替代；
4. 成功与停止标准；
5. 供应链与运行条件；
6. 数据与兼容性；
7. 安全与治理；
8. 最小 PoC；
9. 退出路径；
10. 结论。

### 第 15 章：检索候选如何复用

继承通用字段，再增加：

```text
language/tokenizer/dictionary/config identity
query grammar
ranking formula
golden relevance corpus
GIN/GiST/RUM/other index behavior
write/pending-list/bloat cost
adversarial query boundary
```

`pg_trgm` 的 accept 不能自动批准所有字段。每个字段/查询形态仍需索引与
相关性 ADR。

`vector` 的 pilot 进入第 15 章后，要补：

```text
embedding model/version
dimension
normalization
distance metric
exact-vs-ANN control
recall@k
filter selectivity
HNSW/IVFFlat build/update/maintenance
```

### 第 16 章：时空候选如何复用

增加：

```text
SRID
coordinate order and units
geometry/geography choice
validity and precision
spatial predicate semantics
temporal interval/time zone
GiST/SP-GiST/BRIN behavior
WKT/WKB/GeoJSON export
```

PostGIS 若被采用，自定义类型的 restore/exit 门槛不能因为生态成熟而省略。

### 第 17 章：分析与分布式候选如何复用

增加：

```text
single-node measured ceiling
shard/distribution key
co-location
global uniqueness/FK
cross-shard transaction
rebalance
node failure
DDL propagation
backup/restore and topology exit
```

Citus 只有在这些字段有证据后才从 reject 重新进入 proposed；“Pigsty 有包”
不是触发批准。

### 自动审校器检查什么

[review.py](/labs/ch14/review.py) 不比较终端输出的外观，而比较关系：

```text
manifest proposal identity
package support-file hashes
two exact SQLSTATE 42501 failures
three candidate decisions and availability
before/after extversion
trusted/owner/schema/member relationships
update path
index AM/opclass/validity
least-privilege matrix
query results stable across update
forced index paths present
full dump vs selective dump semantics
portable export shape
final checksum
no-CASCADE reset source
```

关系式 review 比“命令 exit 0”更接近发布验收。

### 审校结果

正式两轮输出：

```text
status=ok
decision=pg_trgm:accept/vector:pilot/citus:reject
boundary=package+control+database-object
failure=42501-owner+42501-superuser
upgrade=pg_trgm:1.3->1.6-behavior-stable
index=gin+hnsw
dump=create-extension+selective-dependency-warning
exit=portable-text-export
pigsty_l1=not-run
release=1.2-proposal
release_candidate_checksum=6a4b74baec5f522eb098c868f1d4f1b441bf5b5f6708411588af0a8793f7f573
```

第一轮通过后，脚本证明复位 guard，再删除并重建，第二轮得到同一关系和
proposal identity。

### 哪些结论可以带走

可以：

- 扩展要同时管理供应、进程和数据库三层；
- trusted/untrusted 与 owner 边界必须负面测试；
- package version 与 extversion 分开；
- update 前后比较 catalog、行为和计划；
- dump 不携带支持文件，选择性 dump 不保证依赖闭包；
- 自定义类型采用前先定义交换格式；
- Pigsty 声明后回到原生证据；
- ADR 允许 accept/pilot/reject，而不是所有候选二选一。

不能：

- `pg_trgm` 对所有搜索都足够；
- pgvector 0.8.4 已通过生产验证；
- Citus 不值得使用；
- PostgreSQL 14–17 会得到相同成员数；
- Homebrew 文件 hash 能代表 Pigsty 包；
- 五行查询速度能代表生产性能。

能清楚说出“实验没有证明什么”，是扩展治理成熟度的一部分。

### 本章最终检查

完成本章后，面对新扩展先写：

```text
problem
native alternative
success/stop
data/exit
lifecycle
maintenance/license
privilege/supply
version scope
review triggers
```

然后才写：

```sql
CREATE EXTENSION ...
```

顺序反过来，数据库很快会积累一组谁也不敢升级、恢复或删除的隐性平台。

---

[上一节：建立可复用扩展 ADR](../06/) · [返回本章目录](../) · [下一章：见微知著：全文、模糊与向量检索](/search/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
