diff --git a/internal/chat/queries/usage.sql b/internal/chat/queries/usage.sql index 778a6d0..4bade39 100644 --- a/internal/chat/queries/usage.sql +++ b/internal/chat/queries/usage.sql @@ -27,7 +27,7 @@ GROUP BY model_id ORDER BY cost_usd DESC; -- name: SummarizeUsageByDay :many -SELECT date_trunc('day', created_at) AS day, +SELECT date_trunc('day', created_at)::timestamptz AS day, SUM(prompt_tokens)::int AS prompt_tokens, SUM(completion_tokens)::int AS completion_tokens, SUM(thinking_tokens)::int AS thinking_tokens, @@ -38,7 +38,7 @@ GROUP BY day ORDER BY day; -- name: SummarizeUserDaily :many -SELECT date_trunc('day', created_at) AS day, +SELECT date_trunc('day', created_at)::timestamptz AS day, SUM(prompt_tokens)::int AS prompt_tokens, SUM(completion_tokens)::int AS completion_tokens, SUM(thinking_tokens)::int AS thinking_tokens, diff --git a/internal/chat/repository.go b/internal/chat/repository.go index 7646257..056304a 100644 --- a/internal/chat/repository.go +++ b/internal/chat/repository.go @@ -25,7 +25,6 @@ import ( ) // Postgres epoch: 2000-01-01 00:00:00 UTC -var pgEpoch = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) // Repository 是 chat 模块对 PG 的全部依赖。 // Service 单元测试通过手写 fakeRepo 满足该接口。 @@ -276,17 +275,12 @@ func float64ToNumeric(f float64) pgtype.Numeric { return n } -// intervalToTime は date_trunc('day',...) が pgtype.Interval として返ってくる -// sqlc 型推論ドリフトへの対処。Postgres は timestamptz を binary で -// 「2000-01-01 エポックからのマイクロ秒」として送るが、pgx が pgtype.Interval -// へスキャンした場合 Microseconds フィールドに入る。 -// Drift 注記: sqlc v1.31.1 は date_trunc の返り型を誤って interval と推論する。 -func intervalToTime(iv pgtype.Interval) time.Time { - if !iv.Valid { +// timestamptzToTime safely extracts time.Time from pgtype.Timestamptz. +func timestamptzToTime(ts pgtype.Timestamptz) time.Time { + if !ts.Valid { return time.Time{} } - // Microseconds はここでは「Postgres epoch からのマイクロ秒数」として解釈する。 - return pgEpoch.Add(time.Duration(iv.Microseconds) * time.Microsecond) + return ts.Time } // ===== PG エラー変換 ===== @@ -462,10 +456,7 @@ func modelUsageRowFromRow(r chatsqlc.SummarizeUsageByModelRow) ModelUsageRow { func dayUsageRowFromRow(r chatsqlc.SummarizeUsageByDayRow) DayUsageRow { return DayUsageRow{ - // Drift: sqlc 把 date_trunc('day', timestamptz) 推断为 pgtype.Interval。 - // pgx 在 binary 模式下把 timestamptz 值的 "Postgres epoch + microseconds" - // 送入 Interval.Microseconds 字段。用 pgEpoch + Duration 还原。 - Day: intervalToTime(r.Day), + Day: timestamptzToTime(r.Day), PromptTokens: int(r.PromptTokens), CompletionTokens: int(r.CompletionTokens), ThinkingTokens: int(r.ThinkingTokens), @@ -1142,7 +1133,7 @@ func (r *pgRepo) SummarizeUserDaily(ctx context.Context, userID uuid.UUID, from, out := make([]DayUsageRow, 0, len(rows)) for _, row := range rows { out = append(out, DayUsageRow{ - Day: intervalToTime(row.Day), + Day: timestamptzToTime(row.Day), PromptTokens: int(row.PromptTokens), CompletionTokens: int(row.CompletionTokens), ThinkingTokens: int(row.ThinkingTokens), diff --git a/internal/chat/sqlc/usage.sql.go b/internal/chat/sqlc/usage.sql.go index f4aaa09..f42661d 100644 --- a/internal/chat/sqlc/usage.sql.go +++ b/internal/chat/sqlc/usage.sql.go @@ -46,7 +46,7 @@ func (q *Queries) InsertUsage(ctx context.Context, arg InsertUsageParams) error } const summarizeUsageByDay = `-- name: SummarizeUsageByDay :many -SELECT date_trunc('day', created_at) AS day, +SELECT date_trunc('day', created_at)::timestamptz AS day, SUM(prompt_tokens)::int AS prompt_tokens, SUM(completion_tokens)::int AS completion_tokens, SUM(thinking_tokens)::int AS thinking_tokens, @@ -63,11 +63,11 @@ type SummarizeUsageByDayParams struct { } type SummarizeUsageByDayRow struct { - Day pgtype.Interval `json:"day"` - PromptTokens int32 `json:"prompt_tokens"` - CompletionTokens int32 `json:"completion_tokens"` - ThinkingTokens int32 `json:"thinking_tokens"` - CostUsd pgtype.Numeric `json:"cost_usd"` + Day pgtype.Timestamptz `json:"day"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` } func (q *Queries) SummarizeUsageByDay(ctx context.Context, arg SummarizeUsageByDayParams) ([]SummarizeUsageByDayRow, error) { @@ -199,7 +199,7 @@ func (q *Queries) SummarizeUsageByUser(ctx context.Context, arg SummarizeUsageBy } const summarizeUserDaily = `-- name: SummarizeUserDaily :many -SELECT date_trunc('day', created_at) AS day, +SELECT date_trunc('day', created_at)::timestamptz AS day, SUM(prompt_tokens)::int AS prompt_tokens, SUM(completion_tokens)::int AS completion_tokens, SUM(thinking_tokens)::int AS thinking_tokens, @@ -217,11 +217,11 @@ type SummarizeUserDailyParams struct { } type SummarizeUserDailyRow struct { - Day pgtype.Interval `json:"day"` - PromptTokens int32 `json:"prompt_tokens"` - CompletionTokens int32 `json:"completion_tokens"` - ThinkingTokens int32 `json:"thinking_tokens"` - CostUsd pgtype.Numeric `json:"cost_usd"` + Day pgtype.Timestamptz `json:"day"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` } func (q *Queries) SummarizeUserDaily(ctx context.Context, arg SummarizeUserDailyParams) ([]SummarizeUserDailyRow, error) {