DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。 OIDC / pairing FK 修復(接 DB 上線必要): - 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback 驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID) - pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token; 自建 device 空 serial 寫 NULL(避免撞 partial unique) - device.SaveTx / session.CreateTx 新增 tx-aware 版本 B4 model metadata: - 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model: converter_client → flow → adapter → model.Model → PG → ModelResponse DTO - input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性 - 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md nginx healthz(部署層): - 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB - 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因) storage error 統一映射(不洩漏 storage 後端細節)。 測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、 internal/db(redis)、relay/session 弱處,含 testcontainers integration。 DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.6 KiB
Go
90 lines
3.6 KiB
Go
package db
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
"github.com/jackc/pgx/v5/pgconn"
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
)
|
||
|
||
// Querier 抽象「能跑查詢的東西」——同時被 *pgxpool.Pool 與 pgx.Tx 滿足。
|
||
//
|
||
// 設計目的(DB 接入塊 5.2 跨 store 交易):
|
||
//
|
||
// repository 方法收 Querier 而非寫死 *pgxpool.Pool,就能在「池上直接跑(自動 commit)」
|
||
// 或「在某個 tx 內跑(隨 tx commit/rollback)」之間自由切換,而呼叫端 / handler 一行不改。
|
||
// cascade 撤銷(刪 device → 同 tx 撤 pairing + session token)正是靠這個介面,讓 device repo
|
||
// 與 token store 在同一個 pgx.Tx 下操作、達成「整筆成功或整筆回滾」。
|
||
//
|
||
// 方法集刻意只取 repository 實際用到的三個(Exec / Query / QueryRow),不暴露 Begin/Commit
|
||
// 等交易控制——交易邊界由 WithTx 統一掌控,repository 不該自行 commit/rollback。
|
||
//
|
||
// pgxpool.Pool 與 pgx.Tx 都已實作這三個方法(簽章完全相符),故下方兩個編譯期斷言成立。
|
||
type Querier interface {
|
||
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
||
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
|
||
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
||
}
|
||
|
||
// WithTx 在一個 pgx 交易內執行 fn,成功則 commit、失敗(fn 回 error 或 panic)則 rollback。
|
||
//
|
||
// 語意(塊 5.1):
|
||
// - Begin 失敗 → 直接回 error(連線層問題,fail-fast)。
|
||
// - fn 回 error → rollback 並回 fn 的 error(保留原因,呼叫端可 errors.Is 比對 domain error)。
|
||
// - fn 成功 → commit;commit 失敗回 commit error。
|
||
// - panic → rollback 後重新 panic(不吞,讓上層 recovery middleware 處理)。
|
||
//
|
||
// ctx 取消會讓 Begin / fn 內的查詢 / Commit 各自因 context 失敗而中止——交易不會半開。
|
||
//
|
||
// 注意:fn 內所有 DB 操作都必須用傳入的 q(tx),不可再用外層 pool,否則那些操作不在交易內、
|
||
// 失敗時不會被 rollback。
|
||
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(q Querier) error) (err error) {
|
||
if pool == nil {
|
||
return errors.New("db: WithTx requires non-nil pool")
|
||
}
|
||
|
||
tx, beginErr := pool.Begin(ctx)
|
||
if beginErr != nil {
|
||
return fmt.Errorf("db: begin tx: %w", beginErr)
|
||
}
|
||
|
||
// committed 避免 commit 後又 rollback(pgx 對已結束的 tx rollback 會回 ErrTxClosed)。
|
||
committed := false
|
||
defer func() {
|
||
if committed {
|
||
return
|
||
}
|
||
// Rollback 用 background context:若是 ctx 已取消才走到這裡,仍要盡力 rollback。
|
||
if rbErr := tx.Rollback(context.Background()); rbErr != nil && !errors.Is(rbErr, pgx.ErrTxClosed) {
|
||
// 只有在 fn 本身沒回 error(亦即 err 為 nil)時,rollback error 才升級成回傳值;
|
||
// 否則保留 fn 的原始 error(更有診斷價值),rollback error 只是次要訊號。
|
||
if err == nil {
|
||
err = fmt.Errorf("db: rollback tx: %w", rbErr)
|
||
}
|
||
}
|
||
}()
|
||
|
||
if fnErr := fn(tx); fnErr != nil {
|
||
return fnErr // defer 會 rollback
|
||
}
|
||
|
||
if cErr := tx.Commit(ctx); cErr != nil {
|
||
return fmt.Errorf("db: commit tx: %w", cErr)
|
||
}
|
||
committed = true
|
||
return nil
|
||
}
|
||
|
||
// 編譯期斷言:*pgxpool.Pool 與 pgx.Tx 都滿足 Querier。
|
||
//
|
||
// 兩者都顯式斷言,避免任一方未來簽章漂移時只在 runtime 才爆。
|
||
// pgx.Tx 本身是 interface,typed-nil((pgx.Tx)(nil))可作為靜態斷言對象——編譯器只看
|
||
// 靜態型別是否滿足 Querier、不解參考該 nil 值,故安全。
|
||
var (
|
||
_ Querier = (*pgxpool.Pool)(nil)
|
||
_ Querier = (pgx.Tx)(nil)
|
||
)
|