mirror.go
package database
import (
"database/sql"
"reflect"
"time"
)
// Mirror reflects a Go struct type for database operations.
// It extracts field metadata once and provides methods for field access.
// Field indices are pre-computed for O(1) access instead of O(n) FieldByName.
type Mirror struct {
typ reflect.Type
columns []Column
fieldIndices map[string][]int // Pre-computed field indices for fast access
}
// Reflect creates a Mirror for the given type.
func Reflect[E any]() *Mirror {
t := reflect.TypeFor[E]()
columns := extractColumns(t)
// Pre-compute field indices for O(1) access
indices := make(map[string][]int, len(columns))
for _, col := range columns {
if index := findFieldIndex(t, col.Name, nil); len(index) > 0 {
indices[col.Name] = index
}
}
return &Mirror{
typ: t,
columns: columns,
fieldIndices: indices,
}
}
// findFieldIndex recursively finds the index path to a field by name.
// Returns the index slice for FieldByIndex, or nil if not found.
func findFieldIndex(t reflect.Type, name string, prefix []int) []int {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
index := append(append([]int{}, prefix...), i)
if field.Name == name {
return index
}
if field.Anonymous && field.Type.Kind() == reflect.Struct {
if found := findFieldIndex(field.Type, name, index); len(found) > 0 {
return found
}
}
}
return nil
}
// Name returns the reflected type name (used for table name).
func (m *Mirror) Name() string {
return m.typ.Name()
}
// Columns returns the column definitions.
func (m *Mirror) Columns() []Column {
return m.columns
}
// New creates a new pointer to the mirrored type.
func (m *Mirror) New() any {
return reflect.New(m.typ).Interface()
}
// Field returns the reflect.Value of a field by name using pre-computed indices.
// This is O(1) for index lookup instead of O(n) FieldByName.
func (m *Mirror) Field(v reflect.Value, name string) reflect.Value {
if index, ok := m.fieldIndices[name]; ok {
return v.FieldByIndex(index)
}
// Fallback to FieldByName for fields not in the cache
return v.FieldByName(name)
}
// Pointers returns field pointers for sql.Scan.
func (m *Mirror) Pointers(v reflect.Value) []any {
ptrs := make([]any, len(m.columns))
for i, col := range m.columns {
ptrs[i] = m.Field(v, col.Name).Addr().Interface()
}
return ptrs
}
// Values returns field values for sql parameters.
func (m *Mirror) Values(v reflect.Value) []any {
values := make([]any, len(m.columns))
for i, col := range m.columns {
values[i] = m.Field(v, col.Name).Interface()
}
return values
}
// Scan reads a database row into an entity using column definitions.
func (m *Mirror) Scan(rows *sql.Rows, entity any) error {
v := reflect.ValueOf(entity).Elem()
return rows.Scan(m.Pointers(v)...)
}
// extractColumns returns Column definitions from a struct type
func extractColumns(t reflect.Type) []Column {
var columns []Column
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Anonymous {
columns = append(columns, extractColumns(field.Type)...)
} else if field.IsExported() {
columns = append(columns, columnFor(field))
}
}
return columns
}
// columnFor returns a Column definition for a struct field
func columnFor(field reflect.StructField) Column {
col := Column{Name: field.Name, Primary: field.Name == "ID"}
switch field.Type.Kind() {
case reflect.String:
col.Type, col.Default = "TEXT", "''"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
col.Type, col.Default = "INTEGER", "0"
case reflect.Float32, reflect.Float64:
col.Type, col.Default = "REAL", "0.0"
case reflect.Bool:
col.Type, col.Default = "INTEGER", "0"
default:
if field.Type == reflect.TypeFor[time.Time]() {
col.Type, col.Default = "DATETIME", "CURRENT_TIMESTAMP"
} else {
col.Type, col.Default = "TEXT", "''"
}
}
return col
}