QRM returns sql.ErrNoRows when scanning into struct destination and query result set is empty.

This commit is contained in:
go-jet 2019-10-12 18:45:09 +02:00
parent 3544977d7f
commit f9efee77ff
7 changed files with 103 additions and 90 deletions

View file

@ -15,10 +15,12 @@ type Statement interface {
DebugSql() (query string) DebugSql() (query string)
// Query executes statement over database connection db and stores row result in destination. // Query executes statement over database connection db and stores row result in destination.
// Destination can be arbitrary structure // Destination can be either pointer to struct or pointer to a slice.
// If destination is pointer to struct and query result set is empty, method returns sql.ErrNoRows.
Query(db qrm.DB, destination interface{}) error Query(db qrm.DB, destination interface{}) error
// QueryContext executes statement with a context over database connection db and stores row result in destination. // QueryContext executes statement with a context over database connection db and stores row result in destination.
// Destination can be of arbitrary structure // Destination can be either pointer to struct or pointer to a slice.
// If destination is pointer to struct and query result set is empty, method returns sql.ErrNoRows.
QueryContext(context context.Context, db qrm.DB, destination interface{}) error QueryContext(context context.Context, db qrm.DB, destination interface{}) error
//Exec executes statement over db connection without returning any rows. //Exec executes statement over db connection without returning any rows.

View file

@ -10,6 +10,7 @@ import (
// Query executes Query Result Mapping (QRM) of `query` with list of parametrized arguments `arg` over database connection `db` // Query executes Query Result Mapping (QRM) of `query` with list of parametrized arguments `arg` over database connection `db`
// using context `ctx` into destination `destPtr`. // using context `ctx` into destination `destPtr`.
// Destination can be either pointer to struct or pointer to slice of structs. // Destination can be either pointer to struct or pointer to slice of structs.
// If destination is pointer to struct and query result set is empty, method returns sql.ErrNoRows.
func Query(ctx context.Context, db DB, query string, args []interface{}, destPtr interface{}) error { func Query(ctx context.Context, db DB, query string, args []interface{}, destPtr interface{}) error {
utils.MustBeInitializedPtr(db, "jet: db is nil") utils.MustBeInitializedPtr(db, "jet: db is nil")
@ -19,21 +20,27 @@ func Query(ctx context.Context, db DB, query string, args []interface{}, destPtr
destinationPtrType := reflect.TypeOf(destPtr) destinationPtrType := reflect.TypeOf(destPtr)
if destinationPtrType.Elem().Kind() == reflect.Slice { if destinationPtrType.Elem().Kind() == reflect.Slice {
return queryToSlice(ctx, db, query, args, destPtr) _, err := queryToSlice(ctx, db, query, args, destPtr)
return err
} else if destinationPtrType.Elem().Kind() == reflect.Struct { } else if destinationPtrType.Elem().Kind() == reflect.Struct {
tempSlicePtrValue := reflect.New(reflect.SliceOf(destinationPtrType)) tempSlicePtrValue := reflect.New(reflect.SliceOf(destinationPtrType))
tempSliceValue := tempSlicePtrValue.Elem() tempSliceValue := tempSlicePtrValue.Elem()
err := queryToSlice(ctx, db, query, args, tempSlicePtrValue.Interface()) rowsProcessed, err := queryToSlice(ctx, db, query, args, tempSlicePtrValue.Interface())
if err != nil { if err != nil {
return err return err
} }
if tempSliceValue.Len() == 0 { if rowsProcessed == 0 {
return sql.ErrNoRows return sql.ErrNoRows
} }
// edge case when row result set contains only NULLs.
if tempSliceValue.Len() == 0 {
return nil
}
structValue := reflect.ValueOf(destPtr).Elem() structValue := reflect.ValueOf(destPtr).Elem()
firstTempStruct := tempSliceValue.Index(0).Elem() firstTempStruct := tempSliceValue.Index(0).Elem()
@ -46,7 +53,7 @@ func Query(ctx context.Context, db DB, query string, args []interface{}, destPtr
} }
} }
func queryToSlice(ctx context.Context, db DB, query string, args []interface{}, slicePtr interface{}) error { func queryToSlice(ctx context.Context, db DB, query string, args []interface{}, slicePtr interface{}) (rowsProcessed int64, err error) {
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
@ -54,27 +61,27 @@ func queryToSlice(ctx context.Context, db DB, query string, args []interface{},
rows, err := db.QueryContext(ctx, query, args...) rows, err := db.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return err return
} }
defer rows.Close() defer rows.Close()
scanContext, err := newScanContext(rows) scanContext, err := newScanContext(rows)
if err != nil { if err != nil {
return err return
} }
if len(scanContext.row) == 0 { if len(scanContext.row) == 0 {
return nil return
} }
slicePtrValue := reflect.ValueOf(slicePtr) slicePtrValue := reflect.ValueOf(slicePtr)
for rows.Next() { for rows.Next() {
err := rows.Scan(scanContext.row...) err = rows.Scan(scanContext.row...)
if err != nil { if err != nil {
return err return
} }
scanContext.rowNum++ scanContext.rowNum++
@ -82,22 +89,24 @@ func queryToSlice(ctx context.Context, db DB, query string, args []interface{},
_, err = mapRowToSlice(scanContext, "", slicePtrValue, nil) _, err = mapRowToSlice(scanContext, "", slicePtrValue, nil)
if err != nil { if err != nil {
return err return
} }
} }
err = rows.Close() err = rows.Close()
if err != nil { if err != nil {
return err return
} }
err = rows.Err() err = rows.Err()
if err != nil { if err != nil {
return err return
} }
return nil rowsProcessed = scanContext.rowNum
return
} }
func mapRowToSlice(scanContext *scanContext, groupKey string, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) { func mapRowToSlice(scanContext *scanContext, groupKey string, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) {
@ -165,6 +174,7 @@ func mapRowToBaseTypeSlice(scanContext *scanContext, slicePtrValue reflect.Value
} }
func mapRowToStruct(scanContext *scanContext, groupKey string, structPtrValue reflect.Value, parentField *reflect.StructField, onlySlices ...bool) (updated bool, err error) { func mapRowToStruct(scanContext *scanContext, groupKey string, structPtrValue reflect.Value, parentField *reflect.StructField, onlySlices ...bool) (updated bool, err error) {
mapOnlySlices := len(onlySlices) > 0
structType := structPtrValue.Type().Elem() structType := structPtrValue.Type().Elem()
typeInf := scanContext.getTypeInfo(structType, parentField) typeInf := scanContext.getTypeInfo(structType, parentField)
@ -193,22 +203,21 @@ func mapRowToStruct(scanContext *scanContext, groupKey string, structPtrValue re
updated = true updated = true
} }
} else if len(onlySlices) == 0 { } else {
if mapOnlySlices || fieldMap.columnIndex == -1 {
if fieldMap.columnIndex == -1 {
continue continue
} }
cellValue := scanContext.rowElem(fieldMap.columnIndex)
if cellValue == nil {
continue
}
initializeValueIfNilPtr(fieldValue)
updated = true
if fieldMap.implementsScanner { if fieldMap.implementsScanner {
cellValue := scanContext.rowElem(fieldMap.columnIndex)
if cellValue == nil {
continue
}
initializeValueIfNilPtr(fieldValue)
scanner := getScanner(fieldValue) scanner := getScanner(fieldValue)
err = scanner.Scan(cellValue) err = scanner.Scan(cellValue)
@ -216,15 +225,8 @@ func mapRowToStruct(scanContext *scanContext, groupKey string, structPtrValue re
if err != nil { if err != nil {
panic("jet: " + err.Error() + ", " + fieldToString(&field) + " of type " + structType.String()) panic("jet: " + err.Error() + ", " + fieldToString(&field) + " of type " + structType.String())
} }
updated = true
} else { } else {
cellValue := scanContext.rowElem(fieldMap.columnIndex) setReflectValue(reflect.ValueOf(cellValue), fieldValue)
if cellValue != nil {
updated = true
initializeValueIfNilPtr(fieldValue)
setReflectValue(reflect.ValueOf(cellValue), fieldValue)
}
} }
} }
} }
@ -232,21 +234,6 @@ func mapRowToStruct(scanContext *scanContext, groupKey string, structPtrValue re
return return
} }
func mapRowToDestinationPtr(scanContext *scanContext, groupKey string, destPtrValue reflect.Value, structField *reflect.StructField) (updated bool, err error) {
utils.ValueMustBe(destPtrValue, reflect.Ptr, "jet: internal error. Destination is not pointer.")
destValueKind := destPtrValue.Elem().Kind()
if destValueKind == reflect.Struct {
return mapRowToStruct(scanContext, groupKey, destPtrValue, structField)
} else if destValueKind == reflect.Slice {
return mapRowToSlice(scanContext, groupKey, destPtrValue, structField)
} else {
panic("jet: unsupported dest type: " + structField.Name + " " + structField.Type.String())
}
}
func mapRowToDestinationValue(scanContext *scanContext, groupKey string, dest reflect.Value, structField *reflect.StructField) (updated bool, err error) { func mapRowToDestinationValue(scanContext *scanContext, groupKey string, dest reflect.Value, structField *reflect.StructField) (updated bool, err error) {
var destPtrValue reflect.Value var destPtrValue reflect.Value
@ -273,3 +260,18 @@ func mapRowToDestinationValue(scanContext *scanContext, groupKey string, dest re
return return
} }
func mapRowToDestinationPtr(scanContext *scanContext, groupKey string, destPtrValue reflect.Value, structField *reflect.StructField) (updated bool, err error) {
utils.ValueMustBe(destPtrValue, reflect.Ptr, "jet: internal error. Destination is not pointer.")
destValueKind := destPtrValue.Elem().Kind()
if destValueKind == reflect.Struct {
return mapRowToStruct(scanContext, groupKey, destPtrValue, structField)
} else if destValueKind == reflect.Slice {
return mapRowToSlice(scanContext, groupKey, destPtrValue, structField)
} else {
panic("jet: unsupported dest type: " + structField.Name + " " + structField.Type.String())
}
}

View file

@ -3,23 +3,19 @@ package qrm
import ( import (
"database/sql" "database/sql"
"database/sql/driver" "database/sql/driver"
"fmt"
"github.com/go-jet/jet/internal/utils" "github.com/go-jet/jet/internal/utils"
"reflect" "reflect"
"strconv"
"strings" "strings"
) )
type scanContext struct { type scanContext struct {
rowNum int rowNum int64
row []interface{}
row []interface{} uniqueDestObjectsMap map[string]int
uniqueDestObjectsMap map[string]int commonIdentToColumnIndex map[string]int
groupKeyInfoCache map[string]groupKeyInfo
typeToColumnIndexMap map[string]int typeInfoMap map[string]typeInfo
groupKeyInfoCache map[string]groupKeyInfo
typeInfoMap map[string]typeInfo
} }
func newScanContext(rows *sql.Rows) (*scanContext, error) { func newScanContext(rows *sql.Rows) (*scanContext, error) {
@ -35,26 +31,25 @@ func newScanContext(rows *sql.Rows) (*scanContext, error) {
return nil, err return nil, err
} }
typeToIndexMap := map[string]int{} commonIdentToColumnIndex := map[string]int{}
for i, alias := range aliases { for i, alias := range aliases {
names := strings.SplitN(alias, ".", 2) names := strings.SplitN(alias, ".", 2)
commonIdentifier := toCommonIdentifier(names[0])
goName := toCommonIdentifier(names[0])
if len(names) > 1 { if len(names) > 1 {
goName += "." + toCommonIdentifier(names[1]) commonIdentifier += "." + toCommonIdentifier(names[1])
} }
typeToIndexMap[strings.ToLower(goName)] = i commonIdentToColumnIndex[commonIdentifier] = i
} }
return &scanContext{ return &scanContext{
row: createScanValue(columnTypes), row: createScanValue(columnTypes),
uniqueDestObjectsMap: make(map[string]int), uniqueDestObjectsMap: make(map[string]int),
groupKeyInfoCache: make(map[string]groupKeyInfo), groupKeyInfoCache: make(map[string]groupKeyInfo),
typeToColumnIndexMap: typeToIndexMap, commonIdentToColumnIndex: commonIdentToColumnIndex,
typeInfoMap: make(map[string]typeInfo), typeInfoMap: make(map[string]typeInfo),
}, nil }, nil
@ -65,7 +60,7 @@ type typeInfo struct {
} }
type fieldMapping struct { type fieldMapping struct {
complexType bool complexType bool // slice or struct
columnIndex int columnIndex int
implementsScanner bool implementsScanner bool
} }
@ -137,7 +132,7 @@ func (s *scanContext) getGroupKey(structType reflect.Type, structField *reflect.
func (s *scanContext) constructGroupKey(groupKeyInfo groupKeyInfo) string { func (s *scanContext) constructGroupKey(groupKeyInfo groupKeyInfo) string {
if len(groupKeyInfo.indexes) == 0 && len(groupKeyInfo.subTypes) == 0 { if len(groupKeyInfo.indexes) == 0 && len(groupKeyInfo.subTypes) == 0 {
return "|ROW: " + strconv.Itoa(s.rowNum) + "|" return fmt.Sprintf("|ROW:%d|", s.rowNum)
} }
groupKeys := []string{} groupKeys := []string{}
@ -204,7 +199,7 @@ func (s *scanContext) typeToColumnIndex(typeName, fieldName string) int {
key = strings.ToLower(fieldName) key = strings.ToLower(fieldName)
} }
index, ok := s.typeToColumnIndexMap[key] index, ok := s.commonIdentToColumnIndex[key]
if !ok { if !ok {
return -1 return -1

View file

@ -33,14 +33,9 @@ func getScanner(value reflect.Value) sql.Scanner {
func getSliceElemType(slicePtrValue reflect.Value) reflect.Type { func getSliceElemType(slicePtrValue reflect.Value) reflect.Type {
sliceTypePtr := slicePtrValue.Type() sliceTypePtr := slicePtrValue.Type()
elemType := indirectType(sliceTypePtr).Elem()
elemType := sliceTypePtr.Elem().Elem() return indirectType(elemType)
if elemType.Kind() == reflect.Ptr {
return elemType.Elem()
}
return elemType
} }
func getSliceElemPtrAt(slicePtrValue reflect.Value, index int) reflect.Value { func getSliceElemPtrAt(slicePtrValue reflect.Value, index int) reflect.Value {

View file

@ -134,7 +134,7 @@ LIMIT $5;
func TestExpressionCast(t *testing.T) { func TestExpressionCast(t *testing.T) {
query := AllTypes.SELECT( query := AllTypes.SELECT(
postgres.CAST(Int(150)).AS_CHAR(12), postgres.CAST(Int(150)).AS_CHAR(12).AS("char12"),
postgres.CAST(String("TRUE")).AS_BOOL(), postgres.CAST(String("TRUE")).AS_BOOL(),
postgres.CAST(String("111")).AS_SMALLINT(), postgres.CAST(String("111")).AS_SMALLINT(),
postgres.CAST(String("111")).AS_INTEGER(), postgres.CAST(String("111")).AS_INTEGER(),

View file

@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"database/sql"
"fmt" "fmt"
"github.com/go-jet/jet/internal/testutils" "github.com/go-jet/jet/internal/testutils"
. "github.com/go-jet/jet/postgres" . "github.com/go-jet/jet/postgres"
@ -694,6 +695,35 @@ func TestScanToSlice(t *testing.T) {
}) })
} }
func TestStructScanErrNoRows(t *testing.T) {
query := SELECT(Customer.AllColumns).
FROM(Customer).
WHERE(Customer.CustomerID.EQ(Int(-1)))
customer := model.Customer{}
err := query.Query(db, &customer)
assert.Error(t, err, sql.ErrNoRows.Error())
}
func TestStructScanAllNull(t *testing.T) {
query := SELECT(NULL.AS("null1"), NULL.AS("null2"))
dest := struct {
Null1 *int
Null2 *int
}{}
err := query.Query(db, &dest)
assert.NilError(t, err)
assert.DeepEqual(t, dest, struct {
Null1 *int
Null2 *int
}{})
}
var address256 = model.Address{ var address256 = model.Address{
AddressID: 256, AddressID: 256,
Address: "1497 Yuzhou Drive", Address: "1497 Yuzhou Drive",

View file

@ -1,7 +1,6 @@
package postgres package postgres
import ( import (
"database/sql"
"fmt" "fmt"
"github.com/go-jet/jet/internal/testutils" "github.com/go-jet/jet/internal/testutils"
. "github.com/go-jet/jet/postgres" . "github.com/go-jet/jet/postgres"
@ -1798,13 +1797,3 @@ func TestJoinViewWithTable(t *testing.T) {
assert.Equal(t, len(dest[0].Rentals), 32) assert.Equal(t, len(dest[0].Rentals), 32)
assert.Equal(t, len(dest[1].Rentals), 27) assert.Equal(t, len(dest[1].Rentals), 27)
} }
func TestErrNoRows(t *testing.T) {
query := SELECT(Customer.AllColumns).FROM(Customer).WHERE(Customer.CustomerID.EQ(Int(-1)))
customer := model.Customer{}
err := query.Query(db, &customer)
assert.Error(t, err, sql.ErrNoRows.Error())
}