---
title: PostgreSQL Kit
description: 为有状态 Plugin 的自有 PostgreSQL schema 建立显式生命周期。
---

`lenso-postgres-kit` 是一个 Plugin 自有 PostgreSQL schema 的生命周期 kit。它不是
共享 State Plugin、通用 SQL Capability、repository abstraction 或 ORM。数据模型、
query、transaction 边界、备份与保留策略以及专用数据库 role 仍由所属 Plugin 定义。

## 定义并安装 schema

把 SQL 放在所属 Plugin 的 `migrations/` 目录中；Rust 只保留显式版本、名称和文件
顺序：

```text
orders-module/
├── migrations/
│   ├── 001_create_orders.sql
│   └── 002_add_order_status.sql
└── src/lib.rs
```

```sql title="migrations/001_create_orders.sql"
CREATE TABLE orders (
    id bigint PRIMARY KEY,
    total_cents bigint NOT NULL
);
```

```rust
use lenso_postgres_kit::{
    Migration, OwnedPostgres, SchemaOperator, SchemaPlan, sql_migrations,
};

const MIGRATIONS: &[Migration] = sql_migrations![
    (1, "create-orders", "migrations/001_create_orders.sql"),
    (2, "add-order-status", "migrations/002_add_order_status.sql"),
];

async fn install(database_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let plan = SchemaPlan::new("orders_module", MIGRATIONS)?;
    SchemaOperator::connect(database_url, plan.clone()).await?.setup().await?;
    Ok(())
}

async fn prepare(database_url: &str) -> Result<OwnedPostgres, Box<dyn std::error::Error>> {
    let plan = SchemaPlan::new("orders_module", MIGRATIONS)?;
    // 运行时 prepare 只验证精确 schema，绝不自动 migration。
    Ok(OwnedPostgres::prepare(database_url, plan).await?)
}
```

路径相对于所属 crate 的 `Cargo.toml`。`sql_migrations!` 使用 `include_str!` 在编译期
嵌入 SQL：文件缺失会直接编译失败，文件变化会触发重编译；运行时不会扫描目录。
版本和名称仍然显式，因此顺序、连续版本校验和 checksum 漂移检测保持确定。

`install` 必须作为显式安装或部署操作执行，不能放在 Plugin `prepare` 中。
`OwnedPostgres::prepare` 成功后，通过 `postgres.pool()` 使用普通 SQLx query 与
transaction。pool 会把自有 schema 选为 `search_path`。

## 显式升级

追加新的 SQL 文件和声明项，不要修改已经应用的文件。新的 Plugin generation 会先返回
`PostgresKitError::UpgradeRequired`；停止所属 Plugin，执行 operator action，再
prepare 新 generation：

```rust
let plan = SchemaPlan::new("orders_module", MIGRATIONS)?;
let outcome = SchemaOperator::connect(database_url, plan)
    .await?
    .upgrade()
    .await?;
println!("{outcome:?}");
```

Setup 与 upgrade 使用 advisory lock 和一个原子 migration transaction。私有 ledger
记录 version、name 与 checksum。

| 失败 | 含义 |
| --- | --- |
| `SetupRequired` | managed schema 尚未安装。 |
| `UpgradeRequired` | 当前链接的 Plugin 有待执行 migration。 |
| `UnmanagedSchema` | schema 存在但没有 kit ledger；不会自动接管。 |
| `HistoryDiverged` | 已应用的 name、version 或 SQL checksum 被改动。 |
| `SchemaAhead` | 数据库版本比当前 Plugin generation 更新。 |
| `OwnershipMismatch` | 当前数据库 role 不是 schema owner。 |

`setup` 是幂等的：首次返回 `Created`，之后返回 `AlreadyCurrent`。失败时 schema 与
ledger 一起回滚。

## 在 PostgreSQL 中强制隔离

`search_path` 只是 query 便利，不是安全边界。每个 Plugin 应使用专用、非 superuser
role，只拥有自己的 schema，并通过数据库 grant 限制权限。共享同一个物理 cluster
不代表可以直接访问其他 Plugin 的 table。

跨 Plugin workflow 使用 Capability call 与应用层协调，不使用共享 SQL transaction。
数据库 URL 通过 [Secrets Plugin](/docs/zh/core/secrets-plugin) 解析，绝不写进 Resolved
App Plan。

## 验证 kit

```sh
cargo fmt --all -- --check
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo test --locked --all-features
LENSO_POSTGRES_TEST_URL=postgres://... \
  cargo test --locked --test postgres_acceptance -- --ignored
```
