readysite / pkg / database / mirror.go
3.9 KB
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
}
← Back