Skip to content

Commit 27488e2

Browse files
fix: Allow valid sql types to be used by Columns and Values (#63)
* fix: Allow valid sql types to be used by Columns and Values * fix columns and lint
1 parent 3e43b34 commit 27488e2

5 files changed

Lines changed: 90 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.coverprofile
2+
.idea

columns.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scan
22

33
import (
4+
"database/sql/driver"
45
"errors"
56
"fmt"
67
"reflect"
@@ -99,7 +100,7 @@ func columnNames(model reflect.Value, strict bool, excluded ...string) []string
99100

100101
typeField := model.Type().Field(i)
101102

102-
if typeField.Type.Kind() == reflect.Struct {
103+
if typeField.Type.Kind() == reflect.Struct && !isValidSqlValue(valField) {
103104
embeddedNames := columnNames(valField, strict, excluded...)
104105
names = append(names, embeddedNames...)
105106
continue
@@ -120,7 +121,7 @@ func columnNames(model reflect.Value, strict bool, excluded ...string) []string
120121
continue
121122
}
122123

123-
if supportedColumnType(valField.Kind()) {
124+
if supportedColumnType(valField.Kind()) || isValidSqlValue(valField) {
124125
names = append(names, fieldName)
125126
}
126127
}
@@ -162,3 +163,16 @@ func supportedColumnType(k reflect.Kind) bool {
162163
return false
163164
}
164165
}
166+
167+
func isValidSqlValue(v reflect.Value) bool {
168+
// This method covers two cases in which we know the Value can be converted to sql:
169+
// 1. It returns true for sql.driver's type check for types like time.Time
170+
// 2. It implements the driver.Valuer interface allowing conversion directly
171+
// into sql statements
172+
if driver.IsValue(v.Interface()) {
173+
return true
174+
}
175+
176+
valuerType := reflect.TypeOf((*driver.Valuer)(nil)).Elem()
177+
return v.Type().Implements(valuerType)
178+
}

columns_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package scan
22

33
import (
4+
"database/sql/driver"
5+
"fmt"
46
"reflect"
57
"testing"
8+
"time"
69

710
"github.com/stretchr/testify/assert"
811
"github.com/stretchr/testify/require"
@@ -269,6 +272,37 @@ func TestColumnsStoresOneCacheEntryPerInstance(t *testing.T) {
269272
assert.Equal(t, 1, after-before, "Cache size grew unexpectedly")
270273
}
271274

275+
func TestValuesWorkWithValidSqlValueTypes(t *testing.T) {
276+
type coupon struct {
277+
Value int `db:"value"`
278+
Expires time.Time `db:"expires"`
279+
}
280+
281+
c := &coupon{}
282+
cols, err := Columns(c)
283+
assert.NoError(t, err)
284+
assert.EqualValues(t, []string{"value", "expires"}, cols)
285+
}
286+
287+
type Pet struct {
288+
Species string
289+
Name string
290+
}
291+
292+
func (p Pet) Value() (driver.Value, error) {
293+
return fmt.Sprintf("%s, a %s", p.Name, p.Species), nil
294+
}
295+
296+
func TestValuesWorkWithDriverValuerImplementers(t *testing.T) {
297+
type person struct {
298+
Name string `db:"name"`
299+
Pet Pet `db:"pet"`
300+
}
301+
cols, err := ColumnsStrict(&person{})
302+
assert.NoError(t, err)
303+
assert.EqualValues(t, []string{"name", "pet"}, cols)
304+
}
305+
272306
func BenchmarkColumnsLargeStruct(b *testing.B) {
273307
ls := &largeStruct{ID: "test", Index: 88, UUID: "test", IsActive: false, Balance: "test", Picture: "test", Age: 88, EyeColor: "test", Name: "test", Gender: "test", Company: "test", Email: "test", Phone: "test", Address: "test", About: "test", Registered: "test", Latitude: 0.566439688205719, Longitude: 0.48440760374069214, Greeting: "test", FavoriteFruit: "test", AID: "test", AIndex: 19, AUUID: "test", AIsActive: true, ABalance: "test", APicture: "test", AAge: 12, AEyeColor: "test", AName: "test", AGender: "test", ACompany: "test", AEmail: "test", APhone: "test", AAddress: "test", AAbout: "test", ARegistered: "test", ALatitude: 0.16338545083999634, ALongitude: 0.24648870527744293, AGreeting: "test", AFavoriteFruit: "test"}
274308

values.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,16 @@ func writeFields(val reflect.Value, m map[string][]int, index []int) {
5050
numfield := val.NumField()
5151

5252
for i := 0; i < numfield; i++ {
53-
if !val.Field(i).CanSet() {
53+
valField := val.Field(i)
54+
if !valField.CanSet() {
5455
continue
5556
}
5657

5758
field := typ.Field(i)
5859
fieldIndex := append(index, field.Index...)
5960

60-
if field.Type.Kind() == reflect.Struct {
61-
writeFields(val.Field(i), m, fieldIndex)
61+
if field.Type.Kind() == reflect.Struct && !isValidSqlValue(valField) {
62+
writeFields(valField, m, fieldIndex)
6263
continue
6364
}
6465

values_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scan
33
import (
44
"reflect"
55
"testing"
6+
"time"
67

78
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
@@ -107,6 +108,40 @@ func TestValuesReadsFromCacheFirst(t *testing.T) {
107108
assert.EqualValues(t, []interface{}{"Brett"}, vals)
108109
}
109110

111+
func TestValuesValidSqlTypes(t *testing.T) {
112+
tNow := time.Now()
113+
type coupon struct {
114+
Value int
115+
Expires time.Time
116+
}
117+
c := &coupon{
118+
Value: 25,
119+
Expires: tNow,
120+
}
121+
122+
vals, err := Values([]string{"Value", "Expires"}, c)
123+
require.NoError(t, err)
124+
assert.EqualValues(t, []interface{}{25, tNow}, vals)
125+
}
126+
127+
func TestValuesDriverValuerImplementers(t *testing.T) {
128+
type person struct {
129+
Name string `db:"name"`
130+
Pet Pet `db:"pet"`
131+
}
132+
p := &person{
133+
Name: "Brett",
134+
Pet: Pet{
135+
Species: "dog",
136+
Name: "Mila",
137+
},
138+
}
139+
140+
vals, err := Values([]string{"Name", "Pet"}, p)
141+
require.NoError(t, err)
142+
assert.EqualValues(t, []interface{}{"Brett", Pet{Name: "Mila", Species: "dog"}}, vals)
143+
}
144+
110145
// benchmarks
111146

112147
func BenchmarkValuesLargeStruct(b *testing.B) {

0 commit comments

Comments
 (0)