107 lines
2.8 KiB
Go

// Package scanner provides fast struct scanning for SQLite results without runtime reflection
package scanner
import (
"reflect"
"strings"
"unsafe"
"zombiezen.com/go/sqlite"
)
// ScanFunc defines how to scan a column into a field
type ScanFunc func(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer)
// Scanner holds pre-compiled scanning information for a struct type
type Scanner struct {
scanners []ScanFunc
offsets []uintptr
columns []string
}
// Predefined scan functions for common types
func scanInt(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) {
*(*int)(fieldPtr) = stmt.ColumnInt(colIndex)
}
func scanInt64(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) {
*(*int64)(fieldPtr) = stmt.ColumnInt64(colIndex)
}
func scanString(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) {
*(*string)(fieldPtr) = stmt.ColumnText(colIndex)
}
func scanFloat64(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) {
*(*float64)(fieldPtr) = stmt.ColumnFloat(colIndex)
}
func scanBool(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) {
*(*bool)(fieldPtr) = stmt.ColumnInt(colIndex) != 0
}
// New creates a scanner for the given struct type using reflection once at creation time
func New[T any]() *Scanner {
var zero T
typ := reflect.TypeOf(zero)
var scanners []ScanFunc
var offsets []uintptr
var columns []string
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// Skip fields without db tag or with "-"
dbTag := field.Tag.Get("db")
if dbTag == "" || dbTag == "-" {
continue
}
columns = append(columns, dbTag)
offsets = append(offsets, field.Offset)
// Map field types to scan functions
switch field.Type.Kind() {
case reflect.Int:
scanners = append(scanners, scanInt)
case reflect.Int64:
scanners = append(scanners, scanInt64)
case reflect.String:
scanners = append(scanners, scanString)
case reflect.Float64:
scanners = append(scanners, scanFloat64)
case reflect.Bool:
scanners = append(scanners, scanBool)
default:
// Fallback to string for unknown types
scanners = append(scanners, scanString)
}
}
return &Scanner{
scanners: scanners,
offsets: offsets,
columns: columns,
}
}
// Columns returns the comma-separated column list for SQL queries
func (s *Scanner) Columns() string {
return strings.Join(s.columns, ", ")
}
// Scan fills the destination struct with data from the SQLite statement
// This method uses no reflection and operates at near-native performance
func (s *Scanner) Scan(stmt *sqlite.Stmt, dest any) {
// Get pointer to the struct data
ptr := (*[2]uintptr)(unsafe.Pointer(&dest))
structPtr := unsafe.Pointer(ptr[1])
// Scan each field using pre-compiled function pointers and offsets
for i := 0; i < len(s.scanners); i++ {
fieldPtr := unsafe.Add(structPtr, s.offsets[i])
s.scanners[i](stmt, i, fieldPtr)
}
}