-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathsqlite3_stmt_cache_test.go
More file actions
323 lines (290 loc) · 8.42 KB
/
Copy pathsqlite3_stmt_cache_test.go
File metadata and controls
323 lines (290 loc) · 8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
//go:build cgo
package sqlite3
import (
"context"
"database/sql"
"path/filepath"
"reflect"
"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 := len(c.stmtCache), 2; got != want {
t.Fatalf("after filling: len(stmtCache) = %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 := len(c.stmtCache), 2; got != want {
t.Fatalf("after q3: len(stmtCache) = %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 index 0).
prepareAndClose(q2)
if len(c.stmtCache) == 0 || c.stmtCache[0].cacheKey != q3 {
var head string
if len(c.stmtCache) > 0 {
head = c.stmtCache[0].cacheKey
}
t.Fatalf("after touching q2: expected q3 at stmtCache[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 := len(c.stmtCache), 2; got != want {
t.Fatalf("after reinserting q1: len(stmtCache) = %d, want %d", got, want)
}
// Sanity-check: no dangling entries past len(stmtCache).
tail := c.stmtCache[:cap(c.stmtCache)]
for i := len(c.stmtCache); i < len(tail); i++ {
if tail[i] != nil {
t.Fatalf("stmtCache tail slot %d = %p, expected nil", i, tail[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)
if !c.stmtCacheEnabled {
t.Skip("statement cache disabled on this SQLite runtime")
}
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)
}
}
// TestStmtCacheSchemaChange verifies that a schema change does not let the
// cache hand back an expired statement whose captured column metadata still
// describes the old schema (issue #1447). Both same-connection DDL and DDL
// issued through a second database handle must invalidate the cache.
func TestStmtCacheSchemaChange(t *testing.T) {
fn := filepath.Join(t.TempDir(), "schemachange.db")
db, err := sql.Open("sqlite3", "file:"+fn+"?_stmt_cache_size=8")
if err != nil {
t.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(1)
if _, err := db.Exec("CREATE TABLE t (a TEXT)"); err != nil {
t.Fatal(err)
}
if _, err := db.Exec("INSERT INTO t VALUES ('x')"); err != nil {
t.Fatal(err)
}
queryCols := func() []string {
rows, err := db.Query("SELECT * FROM t")
if err != nil {
t.Fatal(err)
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
t.Fatal(err)
}
return cols
}
// Populate the cache.
if cols := queryCols(); !reflect.DeepEqual(cols, []string{"a"}) {
t.Fatalf("initial columns: got %v, want [a]", cols)
}
// Same-connection DDL.
if _, err := db.Exec("ALTER TABLE t ADD COLUMN b TEXT"); err != nil {
t.Fatal(err)
}
if cols := queryCols(); !reflect.DeepEqual(cols, []string{"a", "b"}) {
t.Fatalf("columns after same-connection ALTER: got %v, want [a b]", cols)
}
// DDL through a second database handle (different connection).
db2, err := sql.Open("sqlite3", "file:"+fn)
if err != nil {
t.Fatal(err)
}
if _, err := db2.Exec("ALTER TABLE t ADD COLUMN c TEXT"); err != nil {
db2.Close()
t.Fatal(err)
}
db2.Close()
if cols := queryCols(); !reflect.DeepEqual(cols, []string{"a", "b", "c"}) {
t.Fatalf("columns after cross-connection ALTER: got %v, want [a b c]", cols)
}
// The row data must scan consistently with the new column set.
var a string
var b, c any
if err := db.QueryRow("SELECT * FROM t").Scan(&a, &b, &c); err != nil {
t.Fatal(err)
}
if a != "x" || b != nil || c != nil {
t.Fatalf("row after ALTERs: got (%q, %v, %v), want (\"x\", <nil>, <nil>)", a, b, c)
}
}
// TestStmtCacheTempSchemaChange verifies that DDL on the temp schema also
// invalidates cached statements referencing it.
func TestStmtCacheTempSchemaChange(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:?_stmt_cache_size=8")
if err != nil {
t.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(1)
if _, err := db.Exec("CREATE TEMP TABLE tt (a TEXT)"); err != nil {
t.Fatal(err)
}
rows, err := db.Query("SELECT * FROM tt")
if err != nil {
t.Fatal(err)
}
rows.Close() // cached
if _, err := db.Exec("ALTER TABLE tt ADD COLUMN b TEXT"); err != nil {
t.Fatal(err)
}
rows, err = db.Query("SELECT * FROM tt")
if err != nil {
t.Fatal(err)
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
t.Fatal(err)
}
if want := []string{"a", "b"}; !reflect.DeepEqual(cols, want) {
t.Fatalf("columns after temp ALTER: got %v, want %v", cols, want)
}
}
// TestStmtCacheInTransaction verifies that DDL inside an explicit
// transaction is honored by queries later in the same transaction (the
// cache reports a miss there instead of probing the schema).
func TestStmtCacheInTransaction(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:?_stmt_cache_size=8")
if err != nil {
t.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(1)
if _, err := db.Exec("CREATE TABLE t (a TEXT)"); err != nil {
t.Fatal(err)
}
rows, err := db.Query("SELECT * FROM t")
if err != nil {
t.Fatal(err)
}
rows.Close() // cached
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
if _, err := tx.Exec("ALTER TABLE t ADD COLUMN b TEXT"); err != nil {
t.Fatal(err)
}
rows, err = tx.Query("SELECT * FROM t")
if err != nil {
t.Fatal(err)
}
cols, err := rows.Columns()
rows.Close()
if err != nil {
t.Fatal(err)
}
if want := []string{"a", "b"}; !reflect.DeepEqual(cols, want) {
t.Fatalf("columns inside transaction after ALTER: got %v, want %v", cols, want)
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
// After commit the next lookup probes again and must see the change.
rows, err = db.Query("SELECT * FROM t")
if err != nil {
t.Fatal(err)
}
defer rows.Close()
cols, err = rows.Columns()
if err != nil {
t.Fatal(err)
}
if want := []string{"a", "b"}; !reflect.DeepEqual(cols, want) {
t.Fatalf("columns after commit: got %v, want %v", cols, want)
}
}
func cacheKeys(c *SQLiteConn) map[string]int {
out := make(map[string]int)
for _, s := range c.stmtCache {
out[s.cacheKey]++
}
return out
}
func cacheCount(c *SQLiteConn, q string) int {
n := 0
for _, s := range c.stmtCache {
if s.cacheKey == q {
n++
}
}
return n
}