Created
December 20, 2023 08:34
-
-
Save benhoyt/e7c362014f8c369db96f5a9f8281ced1 to your computer and use it in GitHub Desktop.
Proposed Rows.ScanRow implementation (cut-down version)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package model | |
import ( | |
"database/sql" | |
"errors" | |
"fmt" | |
"reflect" | |
"sync" | |
) | |
// ScanRow is a cut-down version of the proposed Rows.ScanRow method. It | |
// currently only handles dest being a (pointer to) struct, and does not | |
// handle embedded fields. See https://github.com/golang/go/issues/61637 | |
func ScanRow(rows *sql.Rows, dest any) error { | |
rv := reflect.ValueOf(dest) | |
if rv.Kind() != reflect.Pointer || rv.IsNil() { | |
return errors.New("dest must be a non-nil pointer") | |
} | |
elem := rv.Elem() | |
if elem.Kind() != reflect.Struct { | |
return errors.New("dest must point to a struct") | |
} | |
indexes := cachedFieldIndexes(reflect.TypeOf(dest).Elem()) | |
columns, err := rows.Columns() | |
if err != nil { | |
return fmt.Errorf("cannot fetch columns: %w", err) | |
} | |
var scanArgs []any | |
for _, column := range columns { | |
index, ok := indexes[column] | |
if ok { | |
// We have a column to field mapping, scan the value. | |
field := elem.Field(index) | |
scanArgs = append(scanArgs, field.Addr().Interface()) | |
} else { | |
// Unassigned column, throw away the scanned value. | |
var throwAway any | |
scanArgs = append(scanArgs, &throwAway) | |
} | |
} | |
return rows.Scan(scanArgs...) | |
} | |
// fieldIndexes returns a map of database column name to struct field index. | |
func fieldIndexes(structType reflect.Type) map[string]int { | |
indexes := make(map[string]int) | |
for i := 0; i < structType.NumField(); i++ { | |
field := structType.Field(i) | |
tag := field.Tag.Get("sql") | |
if tag != "" { | |
// Use "sql" tag if set | |
indexes[tag] = i | |
} else { | |
// Otherwise use field name (with exact case) | |
indexes[field.Name] = i | |
} | |
} | |
return indexes | |
} | |
var fieldIndexesCache sync.Map // map[reflect.Type]map[string]int | |
// cachedFieldIndexes is like fieldIndexes, but cached per struct type. | |
func cachedFieldIndexes(structType reflect.Type) map[string]int { | |
if f, ok := fieldIndexesCache.Load(structType); ok { | |
return f.(map[string]int) | |
} | |
indexes := fieldIndexes(structType) | |
fieldIndexesCache.Store(structType, indexes) | |
return indexes | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment