Repository: grafana/grafana-plugin-sdk-go (go)

Issue:
SQL utils not handling the unknown types
When using the sql utils to convert sql rows into dataframes, some unknown types are throwing error when casting to string. Framer trying to cast decimal into string and failed.

exact error message : 
```
Could not process SQL results: sql: Scan error on column index 0, name "1.2":
unsupported Scan, storing driver.Value type *big.Rat into type *string
```

Example : 

query `select 1.2 from dummy` from database GUI and from grafana.

![image](https://user-images.githubusercontent.com/153843/113019402-7b432380-9179-11eb-9ab4-e315fc1e1f9c.png)
![image](https://user-images.githubusercontent.com/153843/113019443-85fdb880-9179-11eb-98a7-1cf2dd381a70.png)

Tried adding a convertor and still got the same error
```go
sqlutil.StringConverter{
Name:          "SMALLDECIMAL to float64",
InputScanKind: reflect.Float64,
InputTypeName: "DECIMAL",
Replacer: &sqlutil.StringFieldReplacer{
	OutputFieldType: data.FieldTypeFloat64,
	ReplaceFunc: func(in *string) (interface{}, error) {
		if in == nil {
			return nil, nil
		}
		v, err := strconv.ParseFloat(*in, 64)
		if err != nil {
			return nil, err
		}
		return &v, nil
	},
},
}
```

**Environment**:
- SDK version: `github.com/grafana/grafana-plugin-sdk-go v0.89.0`
- Go version:`go version go1.16 darwin/amd64`


Pull request:
sqlutil package refactor
**What this PR does / why we need it**:

The current `FrameFromRows` function assumes that all SQL values can be scanned into a string. While this may be true for most common SQL servers / drivers, some less common ones require scanning into certain types.

SAP Hana, for example, a `FIXED8` will not scan into a string.

Instead, I changed the converters to allow them to provide a reflect.Type to scan into. Instead of scanning into a dataframe row directly, this version creates a `[]interface{}` and scans the sql.Row into that, and then appends it to the dataframe.
 
**Which issue(s) this PR fixes**:

Fixes #303 

Tests coming soon hopefully. As I'm sure @kylebrandt experienced, writing tests using `sql.Rows` is quite difficult. There are plenty of things in here though that can be unit tested and don't have an absurd signature.


---

~I put this into a `v2` package so that I don't break things that are currently in development using the `sqlutil` package, especially in `grafana`.~

Patch:
diff --git a/data/sqlutil/converter.go b/data/sqlutil/converter.go
new file mode 100644
index 0000000..768c52a
--- /dev/null
+++ b/data/sqlutil/converter.go
@@ -0,0 +1,299 @@
+package sqlutil
+
+import (
+	"database/sql"
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+// FrameConverter defines how to convert the scanned value into a value that can be put into a dataframe (OutputFieldType)
+type FrameConverter struct {
+	// FieldType is the type that is created for the dataframe field.
+	// The returned value from `ConverterFunc` should match this type, otherwise the data package will panic.
+	FieldType data.FieldType
+	// ConverterFunc defines how to convert the scanned `InputScanType` to the supplied `FieldType`.
+	// `in` is always supplied as a pointer, as it is scanned as a pointer, even if `InputScanType` is not a pointer.
+	// For example, if `InputScanType` is `string`, then `in` is `*string`
+	ConverterFunc func(in interface{}) (interface{}, error)
+}
+
+// StringConverter can be used to store types not supported by
+// a Frame into a *string. When scanning, if a SQL's row's InputScanType's Kind
+// and InputScanKind match that returned by the sql response, then the
+// conversion func will be run on the row.
+// Note, a Converter should be favored over a StringConverter as not all SQL rows can be scanned into a string.
+// This type is only here for backwards compatibility.
+type StringConverter struct {
+	// Name is an optional property that can be used to identify a converter
+	Name          string
+	InputScanKind reflect.Kind // reflect.Type might better or worse option?
+	InputTypeName string
+
+	// Conversion func may be nil to do no additional operations on the string conversion.
+	ConversionFunc func(in *string) (*string, error)
+
+	// If the Replacer is not nil, the replacement will be performed.
+	Replacer *StringFieldReplacer
+}
+
+// Note: StringConverter is perhaps better understood as []byte. However, currently
+// the Vector type ([][]byte) is not supported. https://github.com/grafana/grafana-plugin-sdk-go/issues/57
+
+// StringFieldReplacer is used to replace a *string Field in a Frame. The type
+// returned by the ReplaceFunc must match the type of elements of VectorType.
+// Both properties must be non-nil.
+// Note, a Converter should be favored over a StringConverter as not all SQL rows can be scanned into a string.
+// This type is only here for backwards compatibility.
+type StringFieldReplacer struct {
+	OutputFieldType data.FieldType
+	ReplaceFunc     func(in *string) (interface{}, error)
+}
+
+// ToConverter turns this StringConverter into a Converter, using the ScanType of string
+func (s StringConverter) ToConverter() Converter {
+	return Converter{
+		Name:           s.Name,
+		InputScanType:  reflect.TypeOf(sql.NullString{}),
+		InputTypeName:  s.InputTypeName,
+		FrameConverter: StringFrameConverter(s),
+	}
+}
+
+// StringFrameConverter creates a FrameConverter from a StringConverter
+func StringFrameConverter(s StringConverter) FrameConverter {
+	f := data.FieldTypeNullableString
+
+	if s.Replacer != nil {
+		f = s.Replacer.OutputFieldType
+	}
+
+	return FrameConverter{
+		FieldType: f,
+		ConverterFunc: func(in interface{}) (interface{}, error) {
+			ns := in.(*sql.NullString)
+			if !ns.Valid {
+				return nil, nil
+			}
+
+			v := &ns.String
+			if s.ConversionFunc != nil {
+				converted, err := s.ConversionFunc(v)
+				if err != nil {
+					return nil, err
+				}
+				v = converted
+			}
+
+			if s.Replacer.ReplaceFunc != nil {
+				return s.Replacer.ReplaceFunc(v)
+			}
+
+			return v, nil
+		},
+	}
+}
+
+// ToConverters creates a slice of Converters from a slice of StringConverters
+func ToConverters(s ...StringConverter) []Converter {
+	n := make([]Converter, len(s))
+	for i, v := range s {
+		n[i] = v.ToConverter()
+	}
+
+	return n
+}
+
+// Converter is used to convert known types returned in sql.Row to a type usable in a dataframe.
+type Converter struct {
+	// Name is the name of the converter that is used to distinguish them when debugging or parsing log output
+	Name string
+
+	// InputScanType is the type that is used when (*sql.Rows).Scan(...) is called.
+	// Some drivers require certain data types to be used when scanning data from sql rows, and this type should reflect that.
+	InputScanType reflect.Type
+
+	// InputTypeName is the case-sensitive name that must match the type that this converter matches
+	InputTypeName string
+
+	// FrameConverter defines how to convert the scanned value into a value that can be put into a dataframe
+	FrameConverter FrameConverter
+}
+
+// DefaultConverterFunc assumes that the scanned value, in, is already a type that can be put into a dataframe.
+func DefaultConverterFunc(t reflect.Type) func(in interface{}) (interface{}, error) {
+	return func(in interface{}) (interface{}, error) {
+		inType := reflect.TypeOf(in)
+		if inType == reflect.PtrTo(t) {
+			n := reflect.ValueOf(in)
+
+			return n.Elem().Interface(), nil
+		}
+
+		return in, nil
+	}
+}
+
+// NewDefaultConverter creates a Converter that assumes that the value is scannable into a String, and placed into the dataframe as a nullable string.
+func NewDefaultConverter(name string, nullable bool, t reflect.Type) Converter {
+	slice := reflect.MakeSlice(reflect.SliceOf(t), 0, 0).Interface()
+	if !data.ValidFieldType(slice) {
+		return Converter{
+			Name:          fmt.Sprintf("[%s] String converter", t),
+			InputScanType: reflect.TypeOf(sql.NullString{}),
+			InputTypeName: name,
+			FrameConverter: FrameConverter{
+				FieldType: data.FieldTypeNullableString,
+				ConverterFunc: func(in interface{}) (interface{}, error) {
+					v := in.(*sql.NullString)
+
+					if !v.Valid {
+						return (*string)(nil), nil
+					}
+
+					f := v.String
+					return &f, nil
+				},
+			},
+		}
+	}
+
+	v := reflect.New(t)
+	var fieldType data.FieldType
+	if v.Type() == reflect.PtrTo(t) {
+		v = v.Elem()
+		fieldType = data.FieldTypeFor(v.Interface())
+	} else {
+		fieldType = data.FieldTypeFor(v.Interface()).NullableType()
+	}
+
+	if nullable {
+		if converter, ok := NullConverters[t]; ok {
+			return converter
+		}
+	}
+
+	return Converter{
+		Name:          fmt.Sprintf("Default converter for %s", name),
+		InputScanType: t,
+		InputTypeName: name,
+		FrameConverter: FrameConverter{
+			FieldType:     fieldType,
+			ConverterFunc: DefaultConverterFunc(t),
+		},
+	}
+}
+
+var (
+	// NullStringConverter creates a *string using the scan type of `sql.NullString`
+	NullStringConverter = Converter{
+		Name:          "nullable string converter",
+		InputScanType: reflect.TypeOf(sql.NullString{}),
+		InputTypeName: "STRING",
+		FrameConverter: FrameConverter{
+			FieldType: data.FieldTypeNullableString,
+			ConverterFunc: func(n interface{}) (interface{}, error) {
+				v := n.(*sql.NullString)
+
+				if !v.Valid {
+					return (*string)(nil), nil
+				}
+
+				f := v.String
+				return &f, nil
+			},
+		},
+	}
+
+	// NullDecimalConverter creates a *float64 using the scan type of `sql.NullFloat64`
+	NullDecimalConverter = Converter{
+		Name:          "NULLABLE decimal converter",
+		InputScanType: reflect.TypeOf(sql.NullFloat64{}),
+		InputTypeName: "DOUBLE",
+		FrameConverter: FrameConverter{
+			FieldType: data.FieldTypeNullableFloat64,
+			ConverterFunc: func(n interface{}) (interface{}, error) {
+				v := n.(*sql.NullFloat64)
+
+				if !v.Valid {
+					return (*float64)(nil), nil
+				}
+
+				f := v.Float64
+				return &f, nil
+			},
+		},
+	}
+
+	// NullInt64Converter creates a *int64 using the scan type of `sql.NullInt64`
+	NullInt64Converter = Converter{
+		Name:          "NULLABLE int64 converter",
+		InputScanType: reflect.TypeOf(sql.NullInt64{}),
+		InputTypeName: "INTEGER",
+		FrameConverter: FrameConverter{
+			FieldType: data.FieldTypeNullableInt64,
+			ConverterFunc: func(n interface{}) (interface{}, error) {
+				v := n.(*sql.NullInt64)
+
+				if !v.Valid {
+					return (*int64)(nil), nil
+				}
+
+				f := v.Int64
+				return &f, nil
+			},
+		},
+	}
+
+	// NullInt32Converter creates a *int32 using the scan type of `sql.NullInt32`
+	NullInt32Converter = Converter{
+		Name:          "NULLABLE int32 converter",
+		InputScanType: reflect.TypeOf(sql.NullInt32{}),
+		InputTypeName: "INTEGER",
+		FrameConverter: FrameConverter{
+			FieldType: data.FieldTypeNullableInt32,
+			ConverterFunc: func(n interface{}) (interface{}, error) {
+				v := n.(*sql.NullInt32)
+
+				if !v.Valid {
+					return (*int32)(nil), nil
+				}
+
+				f := v.Int32
+				return &f, nil
+			},
+		},
+	}
+
+	// NullTimeConverter creates a *time.time using the scan type of `sql.NullTime`
+	NullTimeConverter = Converter{
+		Name:          "NULLABLE time.Time converter",
+		InputScanType: reflect.TypeOf(sql.NullTime{}),
+		InputTypeName: "TIMESTAMP",
+		FrameConverter: FrameConverter{
+			FieldType: data.FieldTypeNullableTime,
+			ConverterFunc: func(n interface{}) (interface{}, error) {
+				v := n.(*sql.NullTime)
+
+				if !v.Valid {
+					return (*time.Time)(nil), nil
+				}
+
+				f := v.Time
+				return &f, nil
+			},
+		},
+	}
+)
+
+// NullConverters is a map of data type names (from reflect.TypeOf(...).String()) to converters
+// Converters supplied here are used as defaults for fields that do not have a supplied Converter
+var NullConverters = map[reflect.Type]Converter{
+	reflect.TypeOf(float64(0)):  NullDecimalConverter,
+	reflect.TypeOf(int64(0)):    NullInt64Converter,
+	reflect.TypeOf(int32(0)):    NullInt32Converter,
+	reflect.TypeOf(""):          NullStringConverter,
+	reflect.TypeOf(time.Time{}): NullTimeConverter,
+}
diff --git a/data/sqlutil/frame.go b/data/sqlutil/frame.go
new file mode 100644
index 0000000..eeaa094
--- /dev/null
+++ b/data/sqlutil/frame.go
@@ -0,0 +1,38 @@
+package sqlutil
+
+import (
+	"errors"
+
+	"github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+// NewFrame creates a new data.Frame with empty fields given the columns and converters
+func NewFrame(columns []string, converters ...Converter) *data.Frame {
+	fields := make(data.Fields, len(converters))
+
+	for i, v := range converters {
+		fields[i] = data.NewFieldFromFieldType(v.FrameConverter.FieldType, 0)
+		fields[i].Name = columns[i]
+	}
+
+	return data.NewFrame("results", fields...)
+}
+
+// Append appends the row to the dataframe, using the converters to convert the scanned value into a value that can be put into a data.Frame
+func Append(frame *data.Frame, row []interface{}, converters ...Converter) error {
+	if len(row) != len(converters) {
+		return errors.New("the number of rows must match the number of converters")
+	}
+
+	d := make([]interface{}, len(row))
+	for i, v := range row {
+		value, err := converters[i].FrameConverter.ConverterFunc(v)
+		if err != nil {
+			return err
+		}
+		d[i] = value
+	}
+
+	frame.AppendRow(d...)
+	return nil
+}
diff --git a/data/sqlutil/scanrow.go b/data/sqlutil/scanrow.go
new file mode 100644
index 0000000..a238d56
--- /dev/null
+++ b/data/sqlutil/scanrow.go
@@ -0,0 +1,101 @@
+package sqlutil
+
+import (
+	"database/sql"
+	"fmt"
+	"reflect"
+)
+
+// A ScanRow is a container for SQL metadata for a single row.
+// The row metadata is used to generate dataframe fields and a slice that can be used with sql.Scan
+type ScanRow struct {
+	Columns []string
+	Types   []reflect.Type
+}
+
+// NewScanRow creates a new ScanRow with a length of `length`. Use the `Set` function to manually set elements at specific indices.
+func NewScanRow(length int) *ScanRow {
+	return &ScanRow{
+		Columns: make([]string, length),
+		Types:   make([]reflect.Type, length),
+	}
+}
+
+// Append adds data to the end of the list of types and columns
+func (s *ScanRow) Append(name string, colType reflect.Type) {
+	s.Columns = append(s.Columns, name)
+	s.Types = append(s.Types, colType)
+}
+
+// Set sets the internal data at i
+func (s *ScanRow) Set(i int, name string, colType reflect.Type) {
+	s.Columns[i] = name
+	s.Types[i] = colType
+}
+
+// NewScannableRow creates a slice where each element is usable in a call to `(database/sql.Rows).Scan`
+// aka a pointer
+func (s *ScanRow) NewScannableRow() []interface{} {
+	values := make([]interface{}, len(s.Types))
+
+	for i, v := range s.Types {
+		if v.Kind() == reflect.Ptr {
+			n := reflect.New(v.Elem())
+			values[i] = n.Interface()
+		} else {
+			values[i] = reflect.New(v).Interface()
+		}
+	}
+
+	return values
+}
+
+// MakeScanRow creates a new scan row given the column types and names.
+// Applicable converters will substitute the SQL scan type with the one provided by the converter.
+// The list of returned converters is the same length as the SQL rows and corresponds with the rows at the same index. (e.g. value at slice element 3 corresponds with the converter at slice element 3)
+// If no converter is provided for a row that has a type that does not fit into a dataframe, it is skipped.
+func MakeScanRow(colTypes []*sql.ColumnType, colNames []string, converters ...Converter) (*ScanRow, []Converter, error) {
+	// In the future we can probably remove this restriction. But right now we map names to Arrow Field Names.
+	// Arrow Field names must be unique: https://github.com/grafana/grafana-plugin-sdk-go/issues/59
+	seen := map[string]int{}
+	for i, name := range colNames {
+		if j, ok := seen[name]; ok {
+			return nil, nil, fmt.Errorf(`duplicate column names are not allowed, found identical name "%v" at column indices %v and %v`, name, j, i)
+		}
+		seen[name] = i
+	}
+
+	r := NewScanRow(0)
+	c := []Converter{}
+
+	// For each column, define a concrete type in the list of values
+	for i, colType := range colTypes {
+		colName := colNames[i]
+		nullable, ok := colType.Nullable()
+		if !ok {
+			nullable = true // If we don't know if it is nullable, assume it is
+		}
+
+		var converter *Converter
+		scanType := colType.ScanType()
+		for i, v := range converters {
+			// If there's an applicable converter for this column, scan using the InputScanType.
+			if v.InputTypeName == colType.DatabaseTypeName() {
+				scanType = v.InputScanType
+				converter = &converters[i]
+				break
+			}
+		}
+
+		if converter == nil {
+			v := NewDefaultConverter(colType.Name(), nullable, scanType)
+			converter = &v
+			scanType = v.InputScanType
+		}
+
+		r.Append(colName, scanType)
+		c = append(c, *converter)
+	}
+
+	return r, c, nil
+}
diff --git a/data/sqlutil/sql.go b/data/sqlutil/sql.go
index e91805d..4dc367a 100644
--- a/data/sqlutil/sql.go
+++ b/data/sqlutil/sql.go
@@ -1,10 +1,8 @@
-// Package sqlutil provides helpers for scanning database/sql responses into a data.Frame.
 package sqlutil
 
 import (
 	"database/sql"
 	"fmt"
-	"reflect"
 
 	"github.com/grafana/grafana-plugin-sdk-go/data"
 )
@@ -18,26 +16,29 @@ import (
 //
 // Fields will be named to match name of the SQL columns.
 //
-// All the types must be supported by the Frame or a StringConverter will be created and
-// the resulting FieldType type will be FieldTypeNullableString ([]*string).
-//
-// The StringConverter's ConversionFunc will be applied to matching rows if it is not nil.
-// Additionally, if the StringConverter's Replacer is not nil, the replacement will be performed.
-// A map of Field/Column index to the corresponding StringConverter is returned so what conversions were
-// done can be inspected.
-func FrameFromRows(rows *sql.Rows, rowLimit int64, converters ...StringConverter) (*data.Frame, map[int]StringConverter, error) {
-	frame, mappers, err := newForSQLRows(rows, converters...)
+// A converter must be supplied in order to support data types that are scanned from sql.Rows, but not supported in data.Frame.
+// The converter defines what type to use for scanning, what type to place in the data frame, and a function for converting from one to the other.
+// If you find yourself here after upgrading, you can continue to your StringConverters here by using the `ToConverters` function.
+func FrameFromRows(rows *sql.Rows, rowLimit int64, converters ...Converter) (*data.Frame, error) {
+	types, err := rows.ColumnTypes()
+	if err != nil {
+		return nil, err
+	}
+
+	names, err := rows.Columns()
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
+	scanner, converters, err := MakeScanRow(types, names, converters...)
+	if err != nil {
+		return nil, err
+	}
+
+	frame := NewFrame(names, converters...)
+
 	var i int64
 	for rows.Next() {
-		sRow := newScannableRow(frame)
-		err := rows.Scan(sRow...)
-		if err != nil {
-			return nil, nil, err
-		}
 		if i == rowLimit {
 			frame.AppendNotices(data.Notice{
 				Severity: data.NoticeSeverityWarning,
@@ -45,157 +46,18 @@ func FrameFromRows(rows *sql.Rows, rowLimit int64, converters ...StringConverter
 			})
 			break
 		}
-		i++
-	}
 
-	for fieldIdx, mapper := range mappers {
-		if mapper.ConversionFunc == nil {
-			continue
+		r := scanner.NewScannableRow()
+		if err := rows.Scan(r...); err != nil {
+			return nil, err
 		}
-		field := frame.Fields[fieldIdx]
-		for i := 0; i < field.Len(); i++ {
-			v, err := mapper.ConversionFunc(field.At(i).(*string))
-			if err != nil {
-				return nil, nil, err
-			}
-			field.Set(i, v)
-		}
-		if mapper.Replacer == nil {
-			continue
-		}
-		if err := Replace(frame, fieldIdx, mapper.Replacer); err != nil {
-			return nil, nil, err
-		}
-	}
-
-	return frame, mappers, nil
-}
 
-// newForSQLRows creates a new Frame appropriate for scanning SQL rows with
-// the the new data.Frame's ScannableRow() method.
-func newForSQLRows(rows *sql.Rows, converters ...StringConverter) (*data.Frame, map[int]StringConverter, error) {
-	mapping := make(map[int]StringConverter)
-	colTypes, err := rows.ColumnTypes()
-	if err != nil {
-		return nil, nil, err
-	}
-	colNames, err := rows.Columns()
-	if err != nil {
-		return nil, nil, err
-	}
-	// In the future we can probably remove this restriction. But right now we map names to Arrow Field Names.
-	// Arrow Field names must be unique: https://github.com/grafana/grafana-plugin-sdk-go/issues/59
-	seen := map[string]int{}
-	for i, name := range colNames {
-		if j, ok := seen[name]; ok {
-			return nil, nil, fmt.Errorf(`duplicate column names are not allowed, found identical name "%v" at column indices %v and %v`, name, j, i)
+		if err := Append(frame, r, converters...); err != nil {
+			return nil, err
 		}
-		seen[name] = i
-	}
-	frame := &data.Frame{}
-	for i, colType := range colTypes {
-		colName := colNames[i]
-		nullable, ok := colType.Nullable()
-		if !ok {
-			nullable = true // If we don't know if it is nullable, assume it is
-		}
-		scanType := colType.ScanType()
-		for _, converter := range converters {
-			if converter.InputScanKind == scanType.Kind() && converter.InputTypeName == colType.DatabaseTypeName() {
-				nullable = true // String converters are always nullable
-				scanType = reflect.TypeOf("")
-				mapping[i] = converter
-			}
-		}
-		var vec interface{}
-		if !nullable {
-			vec = reflect.MakeSlice(reflect.SliceOf(scanType), 0, 0).Interface()
-		} else {
-			ptrType := reflect.TypeOf(reflect.New(scanType).Interface())
-			// Nullabe types get passed to scan as a pointer to a pointer
-			vec = reflect.MakeSlice(reflect.SliceOf(ptrType), 0, 0).Interface()
-		}
-		if !data.ValidFieldType(vec) {
-			// Automatically create string mapper if we end up with an unsupported type
-			mapping[i] = StringConverter{
-				Name:          fmt.Sprintf("Autogenerated for column %v", i),
-				InputTypeName: colType.DatabaseTypeName(),
-				InputScanKind: colType.ScanType().Kind(),
-			}
-			ptrType := reflect.TypeOf(reflect.New(reflect.TypeOf("")).Interface())
-			vec = reflect.MakeSlice(reflect.SliceOf(ptrType), 0, 0).Interface()
-		}
-		frame.Fields = append(frame.Fields, data.NewField(colName, nil, vec))
-	}
-	return frame, mapping, nil
-}
-
-// newScannableRow adds a row to the Frame by extending each Field's Vector. It returns
-// a slice of references that can be passed to the database/sql rows.Scan() to scan directly into
-// the extended Vectors of the data.
-func newScannableRow(f *data.Frame) []interface{} {
-	row := make([]interface{}, len(f.Fields))
-	for i, field := range f.Fields {
-		field.Extend(1)
-		// non-nullable fields will be *T, and nullable fields will be **T
-		ptr := field.PointerAt(field.Len() - 1)
-		row[i] = ptr
-	}
-	return row
-}
-
-// StringConverter can be used to store types not supported by
-// a Frame into a *string. When scanning, if a SQL's row's InputScanType's Kind
-// and InputScanKind match that returned by the sql response, then the
-// conversion func will be run on the row.
-type StringConverter struct {
-	// Name is an optional property that can be used to identify a converter
-	Name          string
-	InputScanKind reflect.Kind // reflect.Type might better or worse option?
-	InputTypeName string
-
-	// Conversion func may be nil to do no additional operations on the string conversion.
-	ConversionFunc func(in *string) (*string, error)
 
-	// If the Replacer is not nil, the replacement will be performed.
-	Replacer *StringFieldReplacer
-}
-
-// Note: StringConverter is perhaps better understood as []byte. However, currently
-// the Vector type ([][]byte) is not supported. https://github.com/grafana/grafana-plugin-sdk-go/issues/57
-
-// StringFieldReplacer is used to replace a *string Field in a Frame. The type
-// returned by the ReplaceFunc must match the type of elements of VectorType.
-// Both properties must be non-nil.
-type StringFieldReplacer struct {
-	OutputFieldType data.FieldType
-	ReplaceFunc     func(in *string) (interface{}, error)
-}
-
-// Replace will replace a *string Vector of the specified Field's index
-// using the StringFieldReplacer.
-func Replace(frame *data.Frame, fieldIdx int, replacer *StringFieldReplacer) error {
-	if fieldIdx > len(frame.Fields) {
-		return fmt.Errorf("fieldIdx is out of bounds, field len: %v", len(frame.Fields))
-	}
-	field := frame.Fields[fieldIdx]
-	if field.Type() != data.FieldTypeNullableString {
-		return fmt.Errorf("can only replace []*string vectors, vector is of type %s", field.Type())
-	}
-	newField := data.NewFieldFromFieldType(replacer.OutputFieldType, field.Len())
-	newField.Name = field.Name
-	newField.Config = field.Config
-	newField.Labels = field.Labels
-
-	for i := 0; i < newField.Len(); i++ {
-		oldVal := field.At(i).(*string) // Vector type is checked earlier above
-		newVal, err := replacer.ReplaceFunc(oldVal)
-		if err != nil {
-			return err
-		}
-		newField.Set(i, newVal)
+		i++
 	}
-	frame.Fields[fieldIdx] = newField
 
-	return nil
+	return frame, nil
 }


Test patch:
diff --git a/data/sqlutil/converter_test.go b/data/sqlutil/converter_test.go
new file mode 100644
index 0000000..ec87aab
--- /dev/null
+++ b/data/sqlutil/converter_test.go
@@ -0,0 +1,113 @@
+package sqlutil_test
+
+import (
+	"database/sql"
+	"fmt"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana-plugin-sdk-go/data"
+	"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultConverter(t *testing.T) {
+	type Suite struct {
+		Name     string
+		Type     reflect.Type
+		Nullable bool
+		Expected sqlutil.Converter
+	}
+
+	suite := []Suite{
+		{
+			Name:     "non-nullable type",
+			Type:     reflect.TypeOf(int64(0)),
+			Nullable: false,
+			Expected: sqlutil.Converter{
+				InputScanType: reflect.TypeOf(int64(0)),
+				FrameConverter: sqlutil.FrameConverter{
+					FieldType: data.FieldTypeInt64,
+				},
+			},
+		},
+		{
+			Name:     "nullable int64",
+			Type:     reflect.TypeOf(int64(0)),
+			Nullable: true,
+			Expected: sqlutil.Converter{
+				InputScanType: reflect.TypeOf(sql.NullInt64{}),
+				FrameConverter: sqlutil.FrameConverter{
+					FieldType: data.FieldTypeInt64.NullableType(),
+				},
+			},
+		},
+		{
+			Name:     "string",
+			Type:     reflect.TypeOf(""),
+			Nullable: false,
+			Expected: sqlutil.Converter{
+				InputScanType: reflect.TypeOf(""),
+				FrameConverter: sqlutil.FrameConverter{
+					FieldType: data.FieldTypeString,
+				},
+			},
+		},
+		{
+			Name:     "nullable string",
+			Type:     reflect.TypeOf(""),
+			Nullable: true,
+			Expected: sqlutil.Converter{
+				InputScanType: reflect.TypeOf(sql.NullString{}),
+				FrameConverter: sqlutil.FrameConverter{
+					FieldType: data.FieldTypeString.NullableType(),
+				},
+			},
+		},
+		{
+			Name:     "string",
+			Type:     reflect.TypeOf(time.Time{}),
+			Nullable: false,
+			Expected: sqlutil.Converter{
+				InputScanType: reflect.TypeOf(time.Time{}),
+				FrameConverter: sqlutil.FrameConverter{
+					FieldType: data.FieldTypeTime,
+				},
+			},
+		},
+		{
+			Name:     "nullable time",
+			Type:     reflect.TypeOf(time.Time{}),
+			Nullable: true,
+			Expected: sqlutil.Converter{
+				InputScanType: reflect.TypeOf(sql.NullTime{}),
+				FrameConverter: sqlutil.FrameConverter{
+					FieldType: data.FieldTypeTime.NullableType(),
+				},
+			},
+		},
+	}
+
+	for i, v := range suite {
+		t.Run(fmt.Sprintf("[%d/%d] %s", i+1, len(suite), v.Name), func(t *testing.T) {
+			c := sqlutil.NewDefaultConverter(v.Name, v.Nullable, v.Type)
+			assert.Equal(t, c.FrameConverter.FieldType, v.Expected.FrameConverter.FieldType)
+			assert.Equal(t, c.InputScanType.String(), v.Expected.InputScanType.String())
+
+			t.Run("When the converter is called, the expected type should be returned", func(t *testing.T) {
+				n := reflect.New(v.Expected.InputScanType).Interface()
+				value, err := c.FrameConverter.ConverterFunc(n)
+				assert.NoError(t, err)
+
+				if !v.Nullable {
+					// non-nullable fields should exactly match
+					assert.Equal(t, reflect.TypeOf(value).String(), v.Type.String())
+				} else {
+					// nullable fields should not exactly match
+					assert.Equal(t, reflect.TypeOf(value).String(), reflect.PtrTo(v.Type).String())
+				}
+			})
+		})
+	}
+}
diff --git a/data/sqlutil/sql_test.go b/data/sqlutil/sql_test.go
index 0520482..9fd4a4b 100644
--- a/data/sqlutil/sql_test.go
+++ b/data/sqlutil/sql_test.go
@@ -1,117 +1,1 @@
 package sqlutil_test
-
-import (
-	"database/sql"
-	"fmt"
-	"reflect"
-	"strconv"
-
-	"github.com/grafana/grafana-plugin-sdk-go/data"
-	"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
-)
-
-func ExampleStringConverter() {
-	_ = sqlutil.StringConverter{
-		Name:          "BIGINT to *int64",
-		InputScanKind: reflect.Struct,
-		InputTypeName: "BIGINT",
-		Replacer: &sqlutil.StringFieldReplacer{
-			OutputFieldType: data.FieldTypeNullableInt64,
-			ReplaceFunc: func(in *string) (interface{}, error) {
-				if in == nil {
-					return nil, nil
-				}
-				v, err := strconv.ParseInt(*in, 10, 64)
-				if err != nil {
-					return nil, err
-				}
-				return &v, nil
-			},
-		},
-	}
-}
-
-func ExampleReplace() {
-	i := 0
-	getString := func() *string {
-		i++
-		s := strconv.Itoa(i)
-		return &s
-	}
-
-	frame := data.NewFrame("Before",
-		data.NewField("string", nil, []*string{getString(), getString()}))
-
-	st, _ := frame.StringTable(-1, -1)
-	fmt.Println(st)
-
-	intReplacer := &sqlutil.StringFieldReplacer{
-		OutputFieldType: data.FieldTypeNullableInt64,
-		ReplaceFunc: func(in *string) (interface{}, error) {
-			if in == nil {
-				return nil, nil
-			}
-			v, err := strconv.ParseInt(*in, 10, 64)
-			if err != nil {
-				return nil, err
-			}
-			return &v, nil
-		},
-	}
-
-	err := sqlutil.Replace(frame, 0, intReplacer)
-	if err != nil {
-		// return err
-	}
-
-	frame.Name = "After"
-	st, _ = frame.StringTable(-1, -1)
-	fmt.Println(st) // After
-	// Output:
-	// Name: Before
-	// Dimensions: 1 Fields by 2 Rows
-	// +-----------------+
-	// | Name: string    |
-	// | Labels:         |
-	// | Type: []*string |
-	// +-----------------+
-	// | 1               |
-	// | 2               |
-	// +-----------------+
-	//
-	// Name: After
-	// Dimensions: 1 Fields by 2 Rows
-	// +----------------+
-	// | Name: string   |
-	// | Labels:        |
-	// | Type: []*int64 |
-	// +----------------+
-	// | 1              |
-	// | 2              |
-	// +----------------+
-}
-
-func ExampleFrameFromRows() {
-	aQuery := "SELECT * FROM GoodData"
-	db, err := sql.Open("fancySql", "fancysql://user:pass@localhost:1433")
-	if err != nil {
-		// return err
-	}
-
-	defer db.Close()
-
-	rows, err := db.Query(aQuery)
-	if err != nil {
-		// return err
-	}
-	if rows.Err() != nil {
-		// return err
-	}
-	defer rows.Close()
-
-	frame, mappings, err := sqlutil.FrameFromRows(rows, 1000)
-	if err != nil {
-		// return err
-	}
-	_, _ = frame, mappings
-}