mirror.go
141 lines1
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
}