|
| 1 | +// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>. |
| 2 | +// |
| 3 | +// Use of this source code is governed by an MIT-style |
| 4 | +// license that can be found in the LICENSE file. |
| 5 | + |
| 6 | +//go:build cgo |
| 7 | +// +build cgo |
| 8 | + |
| 9 | +package sqlite3 |
| 10 | + |
| 11 | +import ( |
| 12 | + "context" |
| 13 | + "testing" |
| 14 | +) |
| 15 | + |
| 16 | +// TestStmtCacheLRUEviction verifies that when the prepared-statement cache is |
| 17 | +// full, the least-recently-used entry is evicted to make room for a new one. |
| 18 | +// Without eviction, the first N queries to enter the cache would squat on |
| 19 | +// every slot forever and any subsequently-prepared query (even a hot one) |
| 20 | +// would never benefit from caching. |
| 21 | +func TestStmtCacheLRUEviction(t *testing.T) { |
| 22 | + d := SQLiteDriver{} |
| 23 | + conn, err := d.Open(":memory:?_stmt_cache_size=2") |
| 24 | + if err != nil { |
| 25 | + t.Fatal(err) |
| 26 | + } |
| 27 | + defer conn.Close() |
| 28 | + |
| 29 | + c := conn.(*SQLiteConn) |
| 30 | + ctx := context.Background() |
| 31 | + |
| 32 | + prepareAndClose := func(q string) { |
| 33 | + t.Helper() |
| 34 | + stmt, err := c.prepareWithCache(ctx, q) |
| 35 | + if err != nil { |
| 36 | + t.Fatalf("prepareWithCache(%q): %v", q, err) |
| 37 | + } |
| 38 | + if err := stmt.Close(); err != nil { |
| 39 | + t.Fatalf("Close(%q): %v", q, err) |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + q1 := "SELECT 1" |
| 44 | + q2 := "SELECT 2" |
| 45 | + q3 := "SELECT 3" |
| 46 | + |
| 47 | + // Fill the cache with q1 and q2. |
| 48 | + prepareAndClose(q1) |
| 49 | + prepareAndClose(q2) |
| 50 | + if got, want := c.stmtCacheCount, 2; got != want { |
| 51 | + t.Fatalf("after filling: stmtCacheCount = %d, want %d", got, want) |
| 52 | + } |
| 53 | + if cacheCount(c, q1) != 1 || cacheCount(c, q2) != 1 { |
| 54 | + t.Fatalf("after filling: expected q1 and q2 cached, got %#v", cacheKeys(c)) |
| 55 | + } |
| 56 | + |
| 57 | + // Insert q3. q1 is the oldest entry and should be evicted. |
| 58 | + prepareAndClose(q3) |
| 59 | + if got, want := c.stmtCacheCount, 2; got != want { |
| 60 | + t.Fatalf("after q3: stmtCacheCount = %d, want %d", got, want) |
| 61 | + } |
| 62 | + if cacheCount(c, q1) != 0 { |
| 63 | + t.Fatalf("after q3: q1 should have been evicted, cache=%#v", cacheKeys(c)) |
| 64 | + } |
| 65 | + if cacheCount(c, q2) != 1 || cacheCount(c, q3) != 1 { |
| 66 | + t.Fatalf("after q3: expected q2 and q3 cached, got %#v", cacheKeys(c)) |
| 67 | + } |
| 68 | + |
| 69 | + // Touching q2 should make q3 the oldest (the entry at buf[0]). |
| 70 | + prepareAndClose(q2) |
| 71 | + if c.stmtCacheCount == 0 || c.stmtCacheBuf[0].cacheKey != q3 { |
| 72 | + var head string |
| 73 | + if c.stmtCacheCount > 0 { |
| 74 | + head = c.stmtCacheBuf[0].cacheKey |
| 75 | + } |
| 76 | + t.Fatalf("after touching q2: expected q3 at buf[0] (LRU), got %q", head) |
| 77 | + } |
| 78 | + |
| 79 | + // Insert q1 again. Now q3 should be evicted (q2 is newer). |
| 80 | + prepareAndClose(q1) |
| 81 | + if cacheCount(c, q3) != 0 { |
| 82 | + t.Fatalf("after reinserting q1: q3 should have been evicted, cache=%#v", cacheKeys(c)) |
| 83 | + } |
| 84 | + if cacheCount(c, q1) != 1 || cacheCount(c, q2) != 1 { |
| 85 | + t.Fatalf("after reinserting q1: expected q1 and q2 cached, got %#v", cacheKeys(c)) |
| 86 | + } |
| 87 | + if got, want := c.stmtCacheCount, 2; got != want { |
| 88 | + t.Fatalf("after reinserting q1: stmtCacheCount = %d, want %d", got, want) |
| 89 | + } |
| 90 | + |
| 91 | + // Sanity-check: no dangling entries past stmtCacheCount. |
| 92 | + for i := c.stmtCacheCount; i < len(c.stmtCacheBuf); i++ { |
| 93 | + if c.stmtCacheBuf[i] != nil { |
| 94 | + t.Fatalf("stmtCacheBuf[%d] = %p, expected nil tail slot", i, c.stmtCacheBuf[i]) |
| 95 | + } |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +// TestStmtCacheReuseReturnsSameHandle verifies that a cached prepare reuses |
| 100 | +// the underlying sqlite3_stmt rather than preparing a fresh one. |
| 101 | +func TestStmtCacheReuseReturnsSameHandle(t *testing.T) { |
| 102 | + d := SQLiteDriver{} |
| 103 | + conn, err := d.Open(":memory:?_stmt_cache_size=4") |
| 104 | + if err != nil { |
| 105 | + t.Fatal(err) |
| 106 | + } |
| 107 | + defer conn.Close() |
| 108 | + |
| 109 | + c := conn.(*SQLiteConn) |
| 110 | + ctx := context.Background() |
| 111 | + |
| 112 | + const q = "SELECT 42" |
| 113 | + stmt1, err := c.prepareWithCache(ctx, q) |
| 114 | + if err != nil { |
| 115 | + t.Fatal(err) |
| 116 | + } |
| 117 | + h1 := stmt1.(*SQLiteStmt).s |
| 118 | + if err := stmt1.Close(); err != nil { |
| 119 | + t.Fatal(err) |
| 120 | + } |
| 121 | + |
| 122 | + stmt2, err := c.prepareWithCache(ctx, q) |
| 123 | + if err != nil { |
| 124 | + t.Fatal(err) |
| 125 | + } |
| 126 | + h2 := stmt2.(*SQLiteStmt).s |
| 127 | + if err := stmt2.Close(); err != nil { |
| 128 | + t.Fatal(err) |
| 129 | + } |
| 130 | + |
| 131 | + if h1 != h2 { |
| 132 | + t.Fatalf("expected cached prepare to reuse sqlite3_stmt handle, got %p vs %p", h1, h2) |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +func cacheKeys(c *SQLiteConn) map[string]int { |
| 137 | + out := make(map[string]int) |
| 138 | + for i := 0; i < c.stmtCacheCount; i++ { |
| 139 | + out[c.stmtCacheBuf[i].cacheKey]++ |
| 140 | + } |
| 141 | + return out |
| 142 | +} |
| 143 | + |
| 144 | +func cacheCount(c *SQLiteConn, q string) int { |
| 145 | + n := 0 |
| 146 | + for i := 0; i < c.stmtCacheCount; i++ { |
| 147 | + if c.stmtCacheBuf[i].cacheKey == q { |
| 148 | + n++ |
| 149 | + } |
| 150 | + } |
| 151 | + return n |
| 152 | +} |
0 commit comments