From e3b346e88133f5d4c12c42fb60e458167bc34412 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 8 Feb 2026 15:59:27 -0800 Subject: [PATCH] fix: correct Stats() to scan only hit/miss counts, compute HitRate properly Stats() was scanning 5 SQL columns (hit_count, miss_count, upstream_fetch_count, upstream_fetch_bytes, transform_count) into mismatched struct fields, causing HitRate to contain the integer transform_count instead of a 0.0-1.0 ratio. Simplify the query to only fetch hit_count and miss_count, then compute TotalItems, TotalSizeBytes, and HitRate correctly. Fixes #4 --- internal/imgcache/cache.go | 8 +-- internal/imgcache/stats_test.go | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 internal/imgcache/stats_test.go diff --git a/internal/imgcache/cache.go b/internal/imgcache/cache.go index 6d5d4dc..f644fa7 100644 --- a/internal/imgcache/cache.go +++ b/internal/imgcache/cache.go @@ -297,19 +297,21 @@ func (c *Cache) CleanExpired(ctx context.Context) error { func (c *Cache) Stats(ctx context.Context) (*CacheStats, error) { var stats CacheStats + // Fetch hit/miss counts from the stats table err := c.db.QueryRowContext(ctx, ` - SELECT hit_count, miss_count, upstream_fetch_count, upstream_fetch_bytes, transform_count + SELECT hit_count, miss_count FROM cache_stats WHERE id = 1 - `).Scan(&stats.HitCount, &stats.MissCount, &stats.TotalItems, &stats.TotalSizeBytes, &stats.HitRate) + `).Scan(&stats.HitCount, &stats.MissCount) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("failed to get cache stats: %w", err) } - // Get actual counts + // Get actual item count and total size from content tables _ = c.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM request_cache`).Scan(&stats.TotalItems) _ = c.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(size_bytes), 0) FROM output_content`).Scan(&stats.TotalSizeBytes) + // Compute hit rate as a ratio if stats.HitCount+stats.MissCount > 0 { stats.HitRate = float64(stats.HitCount) / float64(stats.HitCount+stats.MissCount) } diff --git a/internal/imgcache/stats_test.go b/internal/imgcache/stats_test.go new file mode 100644 index 0000000..5b36a3d --- /dev/null +++ b/internal/imgcache/stats_test.go @@ -0,0 +1,90 @@ +package imgcache + +import ( + "context" + "database/sql" + "math" + "testing" + "time" + + "sneak.berlin/go/pixa/internal/database" +) + +func setupStatsTestDB(t *testing.T) *sql.DB { + t.Helper() + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + if err := database.ApplyMigrations(db); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { db.Close() }) + return db +} + +func TestStats_HitRateIsRatio(t *testing.T) { + db := setupStatsTestDB(t) + dir := t.TempDir() + + cache, err := NewCache(db, CacheConfig{ + StateDir: dir, + CacheTTL: time.Hour, + NegativeTTL: 5 * time.Minute, + }) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + + // Set some hit/miss counts and a transform_count + _, err = db.ExecContext(ctx, ` + UPDATE cache_stats SET hit_count = 75, miss_count = 25, transform_count = 9999 WHERE id = 1 + `) + if err != nil { + t.Fatal(err) + } + + stats, err := cache.Stats(ctx) + if err != nil { + t.Fatal(err) + } + + if stats.HitCount != 75 { + t.Errorf("HitCount = %d, want 75", stats.HitCount) + } + if stats.MissCount != 25 { + t.Errorf("MissCount = %d, want 25", stats.MissCount) + } + + // HitRate should be 0.75, NOT 9999 (transform_count) + expectedRate := 0.75 + if math.Abs(stats.HitRate-expectedRate) > 0.001 { + t.Errorf("HitRate = %f, want %f (was it scanning transform_count?)", stats.HitRate, expectedRate) + } +} + +func TestStats_ZeroCounts(t *testing.T) { + db := setupStatsTestDB(t) + dir := t.TempDir() + + cache, err := NewCache(db, CacheConfig{ + StateDir: dir, + CacheTTL: time.Hour, + NegativeTTL: 5 * time.Minute, + }) + if err != nil { + t.Fatal(err) + } + + stats, err := cache.Stats(context.Background()) + if err != nil { + t.Fatal(err) + } + + // With zero hits and misses, HitRate should be 0, not some garbage value + if stats.HitRate != 0.0 { + t.Errorf("HitRate = %f, want 0.0 for zero counts", stats.HitRate) + } +}