mirror.go

141 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
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
}

// reflectType creates a mirror for the given type.
func reflectType[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
}