cache column metadata for prepared and cached statements

Column names and declared types are invariant for the lifetime of a
prepared statement, but Columns()/declTypes() called C.sqlite3_column_name
and sqlite3_column_decltype on every query, once per column. On hot
QueryRow paths reusing an explicit Prepare or a _stmt_cache_size statement,
this is a fixed set of cgo crossings paid on every execution.

Cache the names and decltypes on the SQLiteStmt the first time they are
materialized and reuse them on subsequent executions. Caching is gated by
cacheMetadata(): explicit prepared statements always cache, Query-created
ephemeral statements cache only when they live in the connection stmt
cache. One-shot statements keep the previous per-call behavior.
This commit is contained in:
Yasuhiro Matsumoto
2026-06-18 14:15:01 +09:00
parent eb06f26148
commit e99486c6b5

View File

@@ -476,6 +476,12 @@ type SQLiteStmt struct {
cls bool // True if the statement was created by SQLiteConn.Query
namedParams map[string][3]int
cacheKey string
metadata *sqliteStmtMetadata
}
type sqliteStmtMetadata struct {
cols []string
decltype []string
}
// SQLiteResult implements sql.Result.
@@ -2475,6 +2481,36 @@ func (rc *SQLiteRows) Close() error {
return nil
}
func (s *SQLiteStmt) cacheMetadata() bool {
return !s.cls || s.cacheKey != ""
}
func (s *SQLiteStmt) columnNamesLocked(n int) []string {
if s.metadata == nil {
s.metadata = &sqliteStmtMetadata{}
}
if len(s.metadata.cols) != n {
s.metadata.cols = make([]string, n)
for i := range s.metadata.cols {
s.metadata.cols[i] = C.GoString(C.sqlite3_column_name(s.s, C.int(i)))
}
}
return s.metadata.cols
}
func (s *SQLiteStmt) declTypesLocked(n int) []string {
if s.metadata == nil {
s.metadata = &sqliteStmtMetadata{}
}
if len(s.metadata.decltype) != n {
s.metadata.decltype = make([]string, n)
for i := range s.metadata.decltype {
s.metadata.decltype[i] = strings.ToLower(C.GoString(C.sqlite3_column_decltype(s.s, C.int(i))))
}
}
return s.metadata.decltype
}
// Columns return column names.
func (rc *SQLiteRows) Columns() []string {
if rc.s == nil {
@@ -2483,21 +2519,29 @@ func (rc *SQLiteRows) Columns() []string {
rc.s.mu.Lock()
defer rc.s.mu.Unlock()
if rc.s.s != nil && int(rc.nc) != len(rc.cols) {
if rc.s.cacheMetadata() {
rc.cols = rc.s.columnNamesLocked(int(rc.nc))
} else {
rc.cols = make([]string, rc.nc)
for i := range rc.cols {
rc.cols[i] = C.GoString(C.sqlite3_column_name(rc.s.s, C.int(i)))
}
}
}
return rc.cols
}
func (rc *SQLiteRows) declTypes() []string {
if rc.s.s != nil && rc.decltype == nil {
if rc.s.cacheMetadata() {
rc.decltype = rc.s.declTypesLocked(int(rc.nc))
} else {
rc.decltype = make([]string, rc.nc)
for i := range rc.decltype {
rc.decltype[i] = strings.ToLower(C.GoString(C.sqlite3_column_decltype(rc.s.s, C.int(i))))
}
}
}
return rc.decltype
}