// Backported from pgx/v5: https://github.com/jackc/pgx/blob/v5.3.1/rows.go#L408 package pgxv5 import ( "reflect" "strings" "github.com/go-errors/errors" "github.com/jackc/pgproto3/v2" "github.com/jackc/pgx/v4" ) func CollectStrings(rows pgx.Rows) ([]string, error) { defer rows.Close() result := []string{} for rows.Next() { var version string if err := rows.Scan(&version); err != nil { return nil, errors.Errorf("failed to scan rows: %w", err) } result = append(result, version) } if err := rows.Err(); err != nil { return nil, errors.Errorf("failed to parse rows: %w", err) } return result, nil } // CollectRows iterates through rows, calling fn for each row, and collecting the results into a slice of T. func CollectRows[T any](rows pgx.Rows) ([]T, error) { defer rows.Close() slice := []T{} for rows.Next() { var value T if err := ScanRowToStruct(rows, &value); err != nil { return nil, err } slice = append(slice, value) } if err := rows.Err(); err != nil { return nil, errors.Errorf("failed to collect rows: %w", err) } return slice, nil } func ScanRowToStruct(rows pgx.Rows, dst any) error { dstValue := reflect.ValueOf(dst) if dstValue.Kind() != reflect.Ptr { return errors.Errorf("dst not a pointer") } dstElemValue := dstValue.Elem() scanTargets, err := appendScanTargets(dstElemValue, nil, rows.FieldDescriptions()) if err != nil { return err } for i, t := range scanTargets { if t == nil { return errors.Errorf("struct doesn't have corresponding row field %s", rows.FieldDescriptions()[i].Name) } } if err := rows.Scan(scanTargets...); err != nil { return errors.Errorf("failed to scan targets: %w", err) } return nil } const structTagKey = "db" func fieldPosByName(fldDescs []pgproto3.FieldDescription, field string) (i int) { i = -1 for i, desc := range fldDescs { if strings.EqualFold(string(desc.Name), field) { return i } } return } func appendScanTargets(dstElemValue reflect.Value, scanTargets []any, fldDescs []pgproto3.FieldDescription) ([]any, error) { var err error dstElemType := dstElemValue.Type() if scanTargets == nil { scanTargets = make([]any, len(fldDescs)) } for i := 0; i < dstElemType.NumField(); i++ { sf := dstElemType.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // Field is unexported, skip it. continue } // Handle anoymous struct embedding, but do not try to handle embedded pointers. if sf.Anonymous && sf.Type.Kind() == reflect.Struct { scanTargets, err = appendScanTargets(dstElemValue.Field(i), scanTargets, fldDescs) if err != nil { return nil, err } } else { colName := GetColumnName(sf) if len(colName) == 0 { // Field is ignored, skip it. continue } fpos := fieldPosByName(fldDescs, colName) if fpos == -1 || fpos >= len(scanTargets) { return nil, errors.Errorf("cannot find field %s in returned row", colName) } scanTargets[fpos] = dstElemValue.Field(i).Addr().Interface() } } return scanTargets, err } func GetColumnName(sf reflect.StructField) string { dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey) if !dbTagPresent { return sf.Name } if dbTag = strings.Split(dbTag, ",")[0]; dbTag != "-" { return dbTag } return "" }