107 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|