# Goose SessionStorage SQLite → PostgreSQL 迁移指南

> 目标:让 goosed 多实例真正 stateless,session 状态存 Postgres 而非本地 SQLite。
>
> 工作量:改 1 个结构体 + 3 处 SQL,新增 connection pool 初始化。预计 2-4 小时。
>
> 前置:**fork `/Users/john/Project/tkmind_go`** 或在那个仓库新建分支。

## 改造点

### 1. `crates/goose/src/session/session_manager.rs:525`

**当前代码结构:**
```rust
pub struct SessionStorage {
    pool: SqlitePool,  // <-- 改这里
    // ...
}

impl SessionStorage {
    pub fn new(data_dir: PathBuf) -> Self {
        let options = SqliteConnectOptions::default()
            .filename(data_dir.join(SESSIONS_FOLDER))
            .create_if_missing(true)
            .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal);
        
        let pool = SqlitePoolOptions::new()
            .connect_lazy_with(options);  // <-- 这里要改
        // ...
    }
}
```

**改成:**
```rust
pub struct SessionStorage {
    pool: sqlx::postgres::PgPool,  // 改这行
    // ...
}

impl SessionStorage {
    pub fn new(connection_string: String) -> Self {
        // 从环境变量或参数读取 PostgreSQL 连接串
        // e.g. GOOSE_SESSION_DB_URL=postgresql://...
        let pool = sqlx::postgres::PgPoolOptions::new()
            .max_connections(20)
            .connect_lazy(&connection_string);
        // ...
    }
}
```

### 2. `schema.sql` / 迁移 SQL

**SQLite 专用语法转 PostgreSQL:**

| 问题 | SQLite | PostgreSQL | 改法 |
|------|--------|-----------|------|
| **主键自增** | `INTEGER PRIMARY KEY AUTOINCREMENT` | `SERIAL PRIMARY KEY` | 改为 `BIGSERIAL` 或 `GENERATED ALWAYS AS IDENTITY` |
| **类型** | `TEXT`(什么都行) | 严格类型(JSONB/TEXT/TIMESTAMP) | 用 `JSONB` 存 JSON,用 `TIMESTAMP` 存时间戳 |
| **模式检查** | `sqlite_master` | `information_schema` | 改用 `information_schema.tables` |
| **WAL/Journal** | `PRAGMA journal_mode=WAL` | N/A | 删除,PG 有自己的 WAL |
| **Collate** | `COLLATE NOCASE` | 用 collation 或 LOWER()  | 改成 `LOWER(column) = LOWER($1)` |

**例:**
```sql
-- SQLite
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    data JSON,
    created_at INTEGER
);

-- PostgreSQL
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    data JSONB,
    created_at BIGINT
);
```

### 3. `FromRow<SqliteRow>` → `FromRow<PgRow>`

**当前:** 
```rust
impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
    fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
        // column 取值方式对 SQLite 优化
    }
}
```

**改成:**
```rust
impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for Session {
    fn from_row(row: &sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
        // 取值方式对 PG 优化(JSONB 用 get_raw 后 JSON 解析)
    }
}
```

### 4. 调用处:初始化时读环境变量

**地点:** `main.rs` / 启动 `SessionManager`

**改法:**
```rust
// 改前
let session_manager = SessionManager::new(data_dir);

// 改后
let session_db_url = std::env::var("GOOSE_SESSION_DB_URL")
    .expect("set GOOSE_SESSION_DB_URL env");
let session_manager = SessionManager::new(session_db_url);
```

## 环境变量

启动 goosed 时设置:
```bash
export GOOSE_SESSION_DB_URL=postgresql://boot:PASSWORD@120.26.184.105:5432/goose_sessions
goosed agent
```

数据库需提前创建:
```bash
psql -U boot -h 120.26.184.105 -c "CREATE DATABASE goose_sessions;"
```

## 测试步骤

1. **编译:**
   ```bash
   cd /Users/john/Project/tkmind_go
   GOOSE_SESSION_DB_URL=... cargo build --release --package goose-server --bin goosed
   ```

2. **启两个实例,挂同一 PG:**
   ```bash
   goosed agent &
   goosed agent &  # 另一个进程/端口
   ```

3. **同一 session 轮换到两个实例:**
   - 客户端起 session on instance 1
   - 后续命令路由到 instance 2 —— 状态应无缝恢复

## 文件清单

修改的文件(全在 crates/goose/src/):
- `session/session_manager.rs` —— 3-5 处改动
- `session/mod.rs` —— 可能有 FromRow(如果有多个 impl)
- `session/legacy.rs` —— 如果涉及导入(通常不用改)
- `Cargo.toml` —— 检查 sqlx features(`postgres` 要打开)

## 风险与回滚

- **数据迁移:** SQLite→PG 用 `pgloader` 命令行工具(一条命令搞定 schema+数据+类型映射)
- **灰度:** 先让新 goosed 实例用 PG,老实例仍用 SQLite,流量按 session 分配。验证无误后全量切换。
- **回滚:** PG 有完整 session 数据,可随时起新 SQLite 实例从 PG 导回。

## 相关文档

- [Goose Scale Architecture](goose-scale-architecture-2026-06-26.md) —— 背景与决策
- [g2 Load Balancing](g2-load-balancing.md) —— 路由与灰度机制
