Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 105 additions & 57 deletions rows.go
Original file line number Diff line number Diff line change
Expand Up @@ -880,18 +880,50 @@ func lookupNamedStructFields(
return cached.(*namedStructFields), nil
}

// We could probably do two-levels of caching, where we compute the key -> fields mapping
// for a type only once, cache it by type, then use that to compute the column -> fields
// mapping for a given set of columns.
fieldStack := make([]int, 0, 1)
checkedIndexes := make(map[int]bool)
fields, missingField := computeNamedStructFields(
l := len(fldDescs)
tagToField := mappingTagToField{
tableFields: make(map[string]*int, l),
tagsList: make([]tag, 0, l),
fieldsList: make([]field, 0, l),
indexNow: l - 1,
notFoundTags: make(map[string]bool, l),
}
fields := iterateNamedStructFields(
fldDescs,
t,
make([]structRowField, len(fldDescs)),
make([]structRowField, l),
&fieldStack,
checkedIndexes,
&tagToField,
)
var missingField string

for _, tag := range tagToField.tagsList {
var exist bool
for i := len(tagToField.fieldsList) - 1; i >= 0; i-- {
value := tagToField.fieldsList[i]
if *value.index == -1 {
continue
}
key := value.name
if tag.normalize {
key = value.normalizedName
}
if strings.EqualFold(key, tag.name) {
fields[*value.index] = structRowField{
path: append(make([]int, 0, len(tag.fieldStack)), tag.fieldStack...),
}
*tagToField.fieldsList[i].index = -1
exist = true
tagToField.notFoundTags[strings.ToLower(tag.name)] = false
break
}
}
if !exist && tagToField.notFoundTags[strings.ToLower(tag.name)] {
missingField = tag.name
}
}

for i, f := range fields {
if f.path == nil {
return nil, fmt.Errorf(
Expand Down Expand Up @@ -930,17 +962,36 @@ func joinFieldNames(fldDescs []fbconn.FieldDescription) string {
return b.String()
}

func computeNamedStructFields(
type tag struct {
normalize bool
name string
fieldStack []int
}

type field struct {
name string
normalizedName string
index *int
}

type mappingTagToField struct {
tableFields map[string]*int
notFoundTags map[string]bool
fieldsList []field
tagsList []tag
indexNow int
}

func iterateNamedStructFields(
fldDescs []fbconn.FieldDescription,
t reflect.Type,
fields []structRowField,
fieldStack *[]int,
checkedIndexes map[int]bool,
) ([]structRowField, string) {
var missingField string
tagToField *mappingTagToField,
) []structRowField {
tail := len(*fieldStack)
*fieldStack = append(*fieldStack, 0)
for i := 0; i < t.NumField(); i++ {
for i := t.NumField() - 1; i >= 0; i-- {
sf := t.Field(i)
(*fieldStack)[tail] = i
if sf.PkgPath != "" && !sf.Anonymous {
Expand All @@ -949,17 +1000,13 @@ func computeNamedStructFields(
}
// Handle anonymous struct embedding, but do not try to handle embedded pointers.
if sf.Anonymous && sf.Type.Kind() == reflect.Struct {
var missingSubField string
fields, missingSubField = computeNamedStructFields(
fields = iterateNamedStructFields(
fldDescs,
sf.Type,
fields,
fieldStack,
checkedIndexes,
tagToField,
)
if missingField == "" {
missingField = missingSubField
}
} else {
dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey)
if dbTagPresent {
Expand All @@ -971,57 +1018,58 @@ func computeNamedStructFields(
}
colName := dbTag
if !dbTagPresent {
colName = sf.Name
colName = strings.ReplaceAll(sf.Name, "_", "")
}
fpos, exactMatch := fieldPosByName(fldDescs, colName, !dbTagPresent)
if fpos == -1 {
if missingField == "" {
missingField = colName
// If the map containing field names already has a value with exactly the same name,
// we compare the field name in the structure with the name in the table and remove it from the map.
if j, ok := tagToField.tableFields[colName]; ok && dbTagPresent {
fields[*j] = structRowField{
path: append(make([]int, 0, len(*fieldStack)), *fieldStack...),
}
continue
}
if wasExact, ok := checkedIndexes[fpos]; !ok || !wasExact && exactMatch || wasExact == exactMatch {
fields[fpos] = structRowField{
path: append([]int(nil), *fieldStack...),
*tagToField.tableFields[colName] = -1
delete(tagToField.tableFields, colName)
tagToField.notFoundTags[strings.ToLower(colName)] = false
} else {
// If we find an exact match between the field name in the structure and the column,
// we map them immediately; if not, we set aside the structure field to check for an inexact match.
find := false
for ; tagToField.indexNow >= 0; tagToField.indexNow-- {
fieldName := fldDescs[tagToField.indexNow].Name
if dbTagPresent && colName == fieldName {
fields[tagToField.indexNow] = structRowField{
path: append(make([]int, 0, len(*fieldStack)), *fieldStack...),
}
find = true
tagToField.indexNow--
tagToField.notFoundTags[strings.ToLower(colName)] = false
break
} else {
tmp := tagToField.indexNow
tagToField.tableFields[fieldName] = &tmp
tagToField.fieldsList = append(tagToField.fieldsList, field{fieldName, strings.ReplaceAll(fieldName, "_", ""), &tmp})
}
}
if !find {
if _, ok := tagToField.notFoundTags[strings.ToLower(colName)]; !ok {
tagToField.notFoundTags[strings.ToLower(colName)] = true
}
tagToField.tagsList = append(tagToField.tagsList, tag{
normalize: !dbTagPresent,
name: colName,
fieldStack: append(make([]int, 0, len(*fieldStack)), *fieldStack...),
})
}
checkedIndexes[fpos] = exactMatch

}
}
}
*fieldStack = (*fieldStack)[:tail]

return fields, missingField
return fields
}

const structTagKey = "db"

func fieldPosByName(fldDescs []fbconn.FieldDescription, field string, normalize bool) (i int, exactMatch bool) {
i = -1

if normalize {
field = strings.ReplaceAll(field, "_", "")
} else {
// Explicit db tags can distinguish quoted identifiers that differ only by case.
for i, desc := range fldDescs {
if desc.Name == field {
return i, true
}
}
}
for i, desc := range fldDescs {
if normalize {
if strings.EqualFold(strings.ReplaceAll(desc.Name, "_", ""), field) {
return i, false
}
} else {
if strings.EqualFold(desc.Name, field) {
return i, false
}
}
}
return
}

// structRowField describes a field of a struct.
type structRowField struct {
path []int
Expand Down
81 changes: 80 additions & 1 deletion rows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,26 @@ func TestRowToStructByNameDbTagsPreferExactMatch(t *testing.T) {
})
}

func TestRowToStructByNameDbTagsPreferExact(t *testing.T) {
func TestRowToStructByNameDbTagsPreferExactMatchLast(t *testing.T) {
type record struct {
Upper int32 `db:"Foo"`
Lower int32 `db:"foo"`
}

sql := `select cast(1 as int) as "foo", cast(2 as int) as "FOO" from rdb$database`

defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *fbx.Conn) {

rows, err := conn.Query(ctx, sql)
require.NoError(t, err)
got, err := fbx.CollectOneRow(rows, fbx.RowToStructByName[record])
require.NoError(t, err)
require.Equal(t, record{Lower: 1, Upper: 2}, got)

})
}

func TestRowToStructByNameDbTagsPreferLast(t *testing.T) {
type Inner struct{ Name string }
type record struct {
Inner
Expand Down Expand Up @@ -1064,6 +1083,66 @@ func TestRowToStructByNameEmbeddedDbTagKeepsExactMatch(t *testing.T) {
})
}

func TestRowToStructByNameDuplicateColumnNames(t *testing.T) {
type record struct {
First int32 `db:"foo"`
Second int32 `db:"foo"`
Third int32 `db:"foo"`
Fourth int32 `db:"foo"`
}

for _, tt := range []struct {
name string
query string
}{
{"exact", `select cast(1 as int) as "foo", cast(2 as int) as "foo", cast(3 as int) as "foo", cast(4 as int) as "foo" from rdb$database`},
{"case insensitive", `select cast(1 as int) as "FOO", cast(2 as int) as "FOO", cast(3 as int) as "FOO", cast(4 as int) as "FOO" from rdb$database`},
{"mixed case", `select cast(1 as int) as "foo", cast(2 as int) as "FOO", cast(3 as int) as "Foo", cast(4 as int) as "fOo" from rdb$database`},
} {
t.Run(tt.name, func(t *testing.T) {
defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *fbx.Conn) {
rows, err := conn.Query(ctx, tt.query)
require.NoError(t, err)
got, err := fbx.CollectOneRow(rows, fbx.RowToStructByName[record])
require.NoError(t, err)

// Every column must have its own destination; ambiguous aliases do not
// impose an order on the four otherwise equivalent fields.
require.ElementsMatch(t, []int32{1, 2, 3, 4}, []int32{got.First, got.Second, got.Third, got.Fourth})
})
})
}
}

func TestRowToAddrOfStructByNameExactAndFoldedAliases(t *testing.T) {
type record struct {
Lower int32 `db:"foo"`
Title int32 `db:"Foo"`
Upper int32 `db:"FOO"`
Folded int32 `db:"fOo"`
}

for _, tt := range []struct {
name string
query string
}{
{"forward", `select cast(1 as int) as "foo", cast(2 as int) as "Foo", cast(3 as int) as "FOO", cast(4 as int) as "fOO" from rdb$database`},
{"reverse", `select cast(4 as int) as "fOO", cast(3 as int) as "FOO", cast(2 as int) as "Foo", cast(1 as int) as "foo" from rdb$database`},
} {
t.Run(tt.name, func(t *testing.T) {
defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *fbx.Conn) {
rows, err := conn.Query(ctx, tt.query)
require.NoError(t, err)
got, err := fbx.CollectOneRow(rows, fbx.RowToAddrOfStructByName[record])
require.NoError(t, err)

// Exact tags reserve their columns before the remaining folded tag.
require.Equal(t, &record{Lower: 1, Title: 2, Upper: 3, Folded: 4}, got)
})
})
}
}

func TestRowToStructByNameEmbeddedStruct(t *testing.T) {
type Name struct {
Last string `db:"LAST_NAME"`
Expand Down