Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
Back to tool/Back to issues
#7831·gorm

Cached prepared statement closed by eviction between lookup and execution

Author: sazaryCreated Jul 30, 2026Updated Aug 4, 2026
Labelstype:with reproduction steps

GORM Playground Link

https://github.com/go-gorm/playground/pull/856

Description

With gorm.Config{PrepareStmt: true} and a bounded cache (PrepareStmtMaxSize, or just PrepareStmtTTL expiry), live requests intermittently fail with sql: statement is closed.

prepare has released every cache lock by the time it returns, so the execution on the next line runs unprotected — prepare_stmt.go#L109-L118:

go
func (db *PreparedStmtDB) ExecContext(ctx context.Context, query string, args ...interface{}) (result sql.Result, err error) {
	stmt, err := db.prepare(ctx, db.ConnPool, false, query)
	if err == nil {
		result, err = stmt.ExecContext(ctx, args...)
		if errors.Is(err, driver.ErrBadConn) {
			db.Stmts.Delete(query)
		}
	}
	return result, err
}

In that window another caller can evict the entry — by size in LRU.Add or by the TTL cleanup goroutine in deleteExpired. Both reach removeElement:

go
func (c *LRU[K, V]) removeElement(e *Entry[K, V]) {
	c.evictList.Remove(e)
	delete(c.items, e.Key)
	c.removeFromBucket(e)
	if c.onEvict != nil {
		c.onEvict(e.Key, e.Value)
	}
}

and stmt_store's callback closes the statement on another goroutine - internal/stmt_store/stmt_store.go#L108-L112:

go
	onEvicted := func(k string, v *Stmt) {
		if v != nil {
			go v.Close()
		}
	}

Two things turn this into a failed request rather than a retry:

  • sql: statement is closed is not driver.ErrBadConn, and ExecContext above drops the cache entry only on ErrBadConn, so the entry is neither invalidated nor re-prepared.
  • Cache hits don't extend an entry's TTL - LRU.Get only calls MoveToFront — so the hottest statements expire on the same schedule as idle ones.

Reproduction

Two callers, two queries, cache size 1: each call evicts the other's statement while that other call sits between prepare() and Exec. Fails in ~30ms, and the test also asserts the error is not driver.ErrBadConn.

Versions

  • GORM: master @ 1d6ce99 (= v1.31.2)
  • Go: 1.25.12 linux/amd64 (playground CI, ubuntu-latest); also reproduced on go1.26.5 darwin/arm64
  • Driver: gorm.io/driver/sqlite v1.6.0 / mattn/go-sqlite3 v1.14.49 — the affected code is dialect-independent, so it reproduces on every driver

Related

#7832 is a statement leak in the same cache: LRU.Add's existing-item branch overwrites an expired entry's value without calling onEvict, so the replaced *sql.Stmt is never closed. Both are symptoms of statement lifetime being tied to cache-slot lifetime rather than to whether anyone is still using the statement, so a fix for either should account for the other.

Source: go-gorm/gorm

View original on GitHubView discussion on GitHub