// 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) } }