From 73fd685ead1928ed765745bf3857a954e03193a2 Mon Sep 17 00:00:00 2001 From: timedout Date: Mon, 20 Jul 2026 14:27:42 +0100 Subject: [PATCH 1/2] dbutil: Add tag-based reflection scanner --- dbutil/reflectscan.go | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/dbutil/reflectscan.go b/dbutil/reflectscan.go index 77dc3ac..5b73eb5 100644 --- a/dbutil/reflectscan.go +++ b/dbutil/reflectscan.go @@ -7,7 +7,10 @@ package dbutil import ( + "errors" + "fmt" "reflect" + "slices" ) func reflectScan[T any](row Scannable) (*T, error) { @@ -28,3 +31,46 @@ func reflectScan[T any](row Scannable) (*T, error) { func NewSimpleReflectRowIter[T any](rows Rows, err error) RowIter[*T] { return ConvertRowFn[*T](reflectScan[T]).NewRowIter(rows, err) } + +var ErrUnknownField = errors.New("column has no associated struct field") + +func reflectFieldScan[T any](columns []string) func(Scannable) (*T, error) { + return func(row Scannable) (*T, error) { + t := new(T) + val := reflect.ValueOf(t).Elem() + fields := reflect.VisibleFields(val.Type()) + fieldMap := make(map[string][]int, len(fields)) + for _, field := range fields { + colname := field.Tag.Get("column") + if colname == "" { + continue + } + if !slices.Contains(columns, colname) { + continue + } + fieldMap[colname] = field.Index + } + + scanInto := make([]any, len(columns)) + for i, column := range columns { + index, ok := fieldMap[column] + if !ok { + return nil, fmt.Errorf("%w: %s", ErrUnknownField, column) + } + scanInto[i] = val.FieldByIndex(index).Addr().Interface() + } + err := row.Scan(scanInto...) + return t, err + } +} + +// NewComplicatedReflectRowIter creates a new RowIter that uses reflection to scan rows into the given type. +// +// It scans into the struct by determining which columns were returned in the query, searching struct fields for the +// `colname` tag. Columns that have no associated struct field will return ErrUnknownField. +// Struct fields that have an empty colname tag, or a tag value which is not in the column list, are skipped. +func NewComplicatedReflectRowIter[T any](rows Rows, err error) RowIter[*T] { + var cols []string + cols, err = rows.Columns() + return ConvertRowFn[*T](reflectFieldScan[T](cols)).NewRowIter(rows, err) +} From 442e00944df3b862669b7b1594ead763156ed19d Mon Sep 17 00:00:00 2001 From: timedout Date: Mon, 20 Jul 2026 14:32:33 +0100 Subject: [PATCH 2/2] dbutil: Fix typos --- dbutil/reflectscan.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dbutil/reflectscan.go b/dbutil/reflectscan.go index 5b73eb5..2ed0358 100644 --- a/dbutil/reflectscan.go +++ b/dbutil/reflectscan.go @@ -67,10 +67,13 @@ func reflectFieldScan[T any](columns []string) func(Scannable) (*T, error) { // NewComplicatedReflectRowIter creates a new RowIter that uses reflection to scan rows into the given type. // // It scans into the struct by determining which columns were returned in the query, searching struct fields for the -// `colname` tag. Columns that have no associated struct field will return ErrUnknownField. -// Struct fields that have an empty colname tag, or a tag value which is not in the column list, are skipped. +// `column` tag. Columns that have no associated struct field will return ErrUnknownField. +// Struct fields that have an empty column tag, or a tag value which is not in the column list, are skipped. func NewComplicatedReflectRowIter[T any](rows Rows, err error) RowIter[*T] { var cols []string - cols, err = rows.Columns() + if err == nil { + // Don't overwrite err + cols, err = rows.Columns() + } return ConvertRowFn[*T](reflectFieldScan[T](cols)).NewRowIter(rows, err) }