---
document_type: "test_doc"
service: "POSTGRESQL"
title: "PostgreSQL Schema 隔離改善計畫"
date: "2026-03-31"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
  - "schema"
  - "postgresql"
  - "隔離改善計畫"
ai_query_hints:
  - "查詢 PostgreSQL Schema 隔離改善計畫 的內容"
  - "PostgreSQL Schema 隔離改善計畫 的主要目的是什麼？"
  - "如何操作或實施 PostgreSQL Schema 隔離改善計畫？"
---

# PostgreSQL Schema 隔離改善計畫

| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-31 |
| 文件版本 | V1.0 |

---

## 問題分析

### 現象
- 伺服器啟動時，`search_path` 正確設置為 `dev`
- Tables 在 `dev` schema 中成功創建
- 但運行時的 INSERT/SELECT 查詢仍使用 `public` schema

### 根本原因
```
sqlx 連接池工作原理:
┌─────────────────────────────────────────────────────────────┐
│  Connection Pool                                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │  Conn 1    │  │  Conn 2    │  │  Conn 3    │  ...     │
│  │ (fresh)     │  │ (reused)   │  │ (reused)   │         │
│  │ search_path │  │  ?         │  │  ?         │         │
│  │  = dev ✅   │  │  = public ❌│  │  = public ❌│         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│                                                             │
│ after_connect 鉤子只在「新建連接」時執行                    │
│ 連接池重用連接時，search_path 不持久                         │
└─────────────────────────────────────────────────────────────┘
```

---

## 解決方案比較

| 方案 | 優點 | 缺點 | 推薦度 |
|------|------|------|--------|
| A. 明確 Schema 前綴 | 簡單直接，精確控制 | 需要改大量 SQL 查詢 | ⭐⭐⭐⭐⭐ |
| B. 自定義連接器 | 一勞永逸 | 實現複雜，需要深入 sqlx | ⭐⭐⭐ |
| C. PostgreSQL 會話預設 | 無需改代碼 | 需要 DB 管理權限 | ⭐⭐⭐ |
| D. 兩個資料庫 | 完全隔離 | 需要雙倍資源 | ⭐⭐ |

---

## 推薦方案: 明確 Schema 前綴

### 實作策略

在所有 SQL 查詢中明確指定 schema 前綴，格式：`{schema}.{table}`

#### 1. 創建 Schema 前綴工具函數

```rust
// src/core/db/schema.rs

use crate::core::config::DATABASE_SCHEMA;

pub fn table_name(table: &str) -> String {
    let schema = DATABASE_SCHEMA.as_str();
    if schema == "public" {
        table.to_string()
    } else {
        format!("{}.{}", schema, table)
    }
}

// 使用範例
let videos_table = table_name("videos");  // "dev.videos" 或 "videos"
```

#### 2. 修改 PostgresDb 方法

```rust
// register_video 方法改造範例

// 改造前
sqlx::query("INSERT INTO videos ...")

# 改造後
let table = table_name("videos");
sqlx::query(&format!("INSERT INTO {} ...", table))
```

#### 3. 需要修改的文件清單

| 文件 | 需要修改的函數/方法 |
|------|---------------------|
| `postgres_db.rs` | register_video, get_video, list_videos, delete_video, register_chunk, get_chunks, 等所有 DB 操作 |
| `sync_db.rs` | 調用 postgres 的方法 |

---

## 實施步驟

### Step 1: 創建 Schema 工具模組

**目標**: 創建統一的 schema 前綴處理函數

**檔案**: `src/core/db/schema.rs`

```rust
use once_cell::sync::Lazy;
use std::env;

pub static DATABASE_SCHEMA: Lazy<String> = Lazy::new(|| {
    env::var("DATABASE_SCHEMA").unwrap_or_else(|_| "public".to_string())
});

pub fn table_name(table: &str) -> String {
    let schema = DATABASE_SCHEMA.as_str();
    if schema == "public" {
        table.to_string()
    } else {
        format!("{}.{}", schema, table)
    }
}

pub fn qualified_table(table: &str) -> String {
    table_name(table)
}
```

### Step 2: 重構 PostgresDb 方法

**目標**: 將所有 SQL 查詢改為使用明確的 schema 前綴

**範例 - register_video**:

```rust
// 改造前
pub async fn register_video(&self, record: &VideoRecord) -> Result<i64> {
    let result = sqlx::query(
        r#"
        INSERT INTO videos (uuid, file_path, ...)
        "#
    )
}

// 改造後
pub async fn register_video(&self, record: &VideoRecord) -> Result<i64> {
    let table = table_name("videos");
    let result = sqlx::query(
        &format!(
            r#"
            INSERT INTO {} (uuid, file_path, ...)
            "#,
            table
        )
    )
}
```

### Step 3: 測試驗證

**目標**: 確保數據正確寫入 `dev` schema

```bash
# 1. 重啟 Playground Server
pkill -f momentry_playground
cargo run --bin momentry_playground -- server --port 3003 &

# 2. 註冊測試視頻
curl -X POST "http://localhost:3003/api/v1/videos/register" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{"file_path": "/path/to/video.mp4"}'

# 3. 驗證數據寫入 dev schema
psql "postgres://accusys:accusys@localhost:5432/momentry" \
  -c "SET search_path TO dev; SELECT uuid, file_name FROM videos;"
```

---

## 工作量估計

### 需要修改的函數數量

| 類別 | 數量 |
|------|------|
| Videos 相關 | ~8 |
| Chunks 相關 | ~12 |
| Frames 相關 | ~5 |
| Pre_chunks 相關 | ~4 |
| Monitor_jobs 相關 | ~6 |
| API Keys 相關 | ~8 |
| 其他 | ~5 |
| **總計** | **~48** |

### 預估時間
- 修改: 2-3 小時
- 測試: 1 小時
- 總計: 3-4 小時

---

## 替代方案: 使用 ConnectionInitializer

如果不想修改所有 SQL 查詢，可以嘗試自定義 sqlx 的連接初始化器：

```rust
// 使用 sqlx::postgres::PgPoolOptions 的連接初始化
// 注意：這需要 sqlx 0.7+ 版本

use sqlx::postgres::{PgPoolOptions, Postgres};

let pool = PgPoolOptions::new()
    .max_connections(10)
    .connect_with(
        connection_options
            .after_connect(|conn, _| {
                Box::pin(async move {
                    // 在這裡設置 session 參數
                    conn.execute("SET search_path TO dev").await?;
                    Ok(())
                })
            })
    )
    .await?;
```

---

## 驗收標準

改造完成後，以下測試必須通過：

```bash
# TC-01: PostgreSQL Schema 隔離驗證
psql "postgres://accusys:accusys@localhost:5432/momentry" \
  -c "SET search_path TO dev; SELECT COUNT(*) FROM videos;"
# 預期: 能夠查到數據

# TC-02: 數據隔離驗證
# 在 Playground 註冊視頻後
psql "postgres://accusys:accusys@localhost:5432/momentry" \
  -c "SET search_path TO dev; SELECT COUNT(*) FROM videos;" 
# 預期: > 0

psql "postgres://accusys:accusys@localhost:5432/momentry" \
  -c "SET search_path TO public; SELECT COUNT(*) FROM videos;"
# 預期: 不包含 Playground 註冊的數據
```

---

## 風險與緩解

| 風險 | 影響 | 緩解措施 |
|------|------|---------|
| 漏改某些查詢 | 數據寫入錯誤 schema | 全面測試覆蓋 |
| Schema 前綴語法錯誤 | SQL 執行失敗 | 先在 psql 測試確認語法 |
| 效能輕微下降 | 需要字符串格式化 | 影響可忽略 |

---

## 版本資訊

- 版本: V1.0
- 建立日期: 2026-03-31
