// Copyright (C) 2019 Yasuhiro Matsumoto . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. //go:build cgo // +build cgo package sqlite3 import ( "context" "testing" ) // TestStmtCacheLRUEviction verifies that when the prepared-statement cache is // full, the least-recently-used entry is evicted to make room for a new one. // Without eviction, the first N queries to enter the cache would squat on // every slot forever and any subsequently-prepared query (even a hot one) // would never benefit from caching. func TestStmtCacheLRUEviction(t *testing.T) { d := SQLiteDriver{} conn, err := d.Open(":memory:?_stmt_cache_size=2") if err != nil { t.Fatal(err) } defer conn.Close() c := conn.(*SQLiteConn) ctx := context.Background() prepareAndClose := func(q string) { t.Helper() stmt, err := c.prepareWithCache(ctx, q) if err != nil { t.Fatalf("prepareWithCache(%q): %v", q, err) } if err := stmt.Close(); err != nil { t.Fatalf("Close(%q): %v", q, err) } } q1 := "SELECT 1" q2 := "SELECT 2" q3 := "SELECT 3" // Fill the cache with q1 and q2. prepareAndClose(q1) prepareAndClose(q2) if got, want := c.stmtCacheCount, 2; got != want { t.Fatalf("after filling: stmtCacheCount = %d, want %d", got, want) } if cacheCount(c, q1) != 1 || cacheCount(c, q2) != 1 { t.Fatalf("after filling: expected q1 and q2 cached, got %#v", cacheKeys(c)) } // Insert q3. q1 is the oldest entry and should be evicted. prepareAndClose(q3) if got, want := c.stmtCacheCount, 2; got != want { t.Fatalf("after q3: stmtCacheCount = %d, want %d", got, want) } if cacheCount(c, q1) != 0 { t.Fatalf("after q3: q1 should have been evicted, cache=%#v", cacheKeys(c)) } if cacheCount(c, q2) != 1 || cacheCount(c, q3) != 1 { t.Fatalf("after q3: expected q2 and q3 cached, got %#v", cacheKeys(c)) } // Touching q2 should make q3 the oldest (the entry at buf[0]). prepareAndClose(q2) if c.stmtCacheCount == 0 || c.stmtCacheBuf[0].cacheKey != q3 { var head string if c.stmtCacheCount > 0 { head = c.stmtCacheBuf[0].cacheKey } t.Fatalf("after touching q2: expected q3 at buf[0] (LRU), got %q", head) } // Insert q1 again. Now q3 should be evicted (q2 is newer). prepareAndClose(q1) if cacheCount(c, q3) != 0 { t.Fatalf("after reinserting q1: q3 should have been evicted, cache=%#v", cacheKeys(c)) } if cacheCount(c, q1) != 1 || cacheCount(c, q2) != 1 { t.Fatalf("after reinserting q1: expected q1 and q2 cached, got %#v", cacheKeys(c)) } if got, want := c.stmtCacheCount, 2; got != want { t.Fatalf("after reinserting q1: stmtCacheCount = %d, want %d", got, want) } // Sanity-check: no dangling entries past stmtCacheCount. for i := c.stmtCacheCount; i < len(c.stmtCacheBuf); i++ { if c.stmtCacheBuf[i] != nil { t.Fatalf("stmtCacheBuf[%d] = %p, expected nil tail slot", i, c.stmtCacheBuf[i]) } } } // TestStmtCacheReuseReturnsSameHandle verifies that a cached prepare reuses // the underlying sqlite3_stmt rather than preparing a fresh one. func TestStmtCacheReuseReturnsSameHandle(t *testing.T) { d := SQLiteDriver{} conn, err := d.Open(":memory:?_stmt_cache_size=4") if err != nil { t.Fatal(err) } defer conn.Close() c := conn.(*SQLiteConn) ctx := context.Background() const q = "SELECT 42" stmt1, err := c.prepareWithCache(ctx, q) if err != nil { t.Fatal(err) } h1 := stmt1.(*SQLiteStmt).s if err := stmt1.Close(); err != nil { t.Fatal(err) } stmt2, err := c.prepareWithCache(ctx, q) if err != nil { t.Fatal(err) } h2 := stmt2.(*SQLiteStmt).s if err := stmt2.Close(); err != nil { t.Fatal(err) } if h1 != h2 { t.Fatalf("expected cached prepare to reuse sqlite3_stmt handle, got %p vs %p", h1, h2) } } func cacheKeys(c *SQLiteConn) map[string]int { out := make(map[string]int) for i := 0; i < c.stmtCacheCount; i++ { out[c.stmtCacheBuf[i].cacheKey]++ } return out } func cacheCount(c *SQLiteConn, q string) int { n := 0 for i := 0; i < c.stmtCacheCount; i++ { if c.stmtCacheBuf[i].cacheKey == q { n++ } } return n }