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。 // // pgx.Tx 是 interface,無法直接取 (pgx.Tx)(nil) 當靜態斷言對象(nil interface 沒有具體型別), // 故只對 *pgxpool.Pool 做編譯期斷言;pgx.Tx 的相符性由 WithTx 內 `fn(tx)` 的傳參處由編譯器保證。 var _ Querier = (*pgxpool.Pool)(nil)