Skip to content
Merged
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
138 changes: 138 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"testing"
"time"
Expand All @@ -18,6 +19,7 @@ import (
var (
db *sql.DB
dataSize = 100
wideSize = 1000
)

func TestMain(m *testing.M) {
Expand All @@ -37,6 +39,19 @@ func TestMain(m *testing.M) {
panic(err)
}

for _, w := range []struct {
table string
numCols int
}{
{"wide5", 5},
{"wide15", 15},
{"wide45", 45},
} {
if err := prepareWideData(ctx, w.table, w.numCols); err != nil {
panic(err)
}
}

exitVal := m.Run()

os.Exit(exitVal)
Expand Down Expand Up @@ -78,6 +93,28 @@ func BenchmarkScanOne(b *testing.B) {
}
}

func BenchmarkScanWide5(b *testing.B) { benchmarkScanWide[Wide5](b, "wide5") }
func BenchmarkScanWide15(b *testing.B) { benchmarkScanWide[Wide15](b, "wide15") }
func BenchmarkScanWide45(b *testing.B) { benchmarkScanWide[Wide45](b, "wide45") }

func benchmarkScanWide[T any](b *testing.B, table string) {
b.StopTimer()
ctx := context.Background()

for i := 0; i < b.N; i++ {
b.StopTimer()
rows, err := db.Query("SELECT|" + table + "||")
if err != nil {
panic(err)
}
b.StartTimer()
if _, err := AllFromRows(ctx, StructMapper[T](), rows); err != nil {
panic(err)
}
rows.Close()
}
}

func prepareData(ctx context.Context) error {
create := "CREATE|user|id=int64,username=string,password=string"
create += ",email=string,mobile_phone=string,company=string,avatar_url=string"
Expand Down Expand Up @@ -108,6 +145,107 @@ func prepareData(ctx context.Context) error {
return nil
}

func prepareWideData(ctx context.Context, table string, numCols int) error {
colDefs := make([]string, numCols)
colAssigns := make([]string, numCols)
for i := range colDefs {
colDefs[i] = fmt.Sprintf("col_%d=string", i)
colAssigns[i] = fmt.Sprintf("col_%d=?", i)
}

create := fmt.Sprintf("CREATE|%s|%s", table, strings.Join(colDefs, ","))
if _, err := db.ExecContext(ctx, create); err != nil {
return err
}

insert := fmt.Sprintf("INSERT|%s|%s", table, strings.Join(colAssigns, ","))
args := make([]any, numCols)
for i := 0; i < wideSize; i++ {
for c := range args {
args[c] = fmt.Sprintf("value_%d_%d", i, c)
}
if _, err := db.ExecContext(ctx, insert, args...); err != nil {
return err
}
}

return nil
}

type Wide5 struct {
Col0 string `db:"col_0"`
Col1 string `db:"col_1"`
Col2 string `db:"col_2"`
Col3 string `db:"col_3"`
Col4 string `db:"col_4"`
}

type Wide15 struct {
Col0 string `db:"col_0"`
Col1 string `db:"col_1"`
Col2 string `db:"col_2"`
Col3 string `db:"col_3"`
Col4 string `db:"col_4"`
Col5 string `db:"col_5"`
Col6 string `db:"col_6"`
Col7 string `db:"col_7"`
Col8 string `db:"col_8"`
Col9 string `db:"col_9"`
Col10 string `db:"col_10"`
Col11 string `db:"col_11"`
Col12 string `db:"col_12"`
Col13 string `db:"col_13"`
Col14 string `db:"col_14"`
}

type Wide45 struct {
Col0 string `db:"col_0"`
Col1 string `db:"col_1"`
Col2 string `db:"col_2"`
Col3 string `db:"col_3"`
Col4 string `db:"col_4"`
Col5 string `db:"col_5"`
Col6 string `db:"col_6"`
Col7 string `db:"col_7"`
Col8 string `db:"col_8"`
Col9 string `db:"col_9"`
Col10 string `db:"col_10"`
Col11 string `db:"col_11"`
Col12 string `db:"col_12"`
Col13 string `db:"col_13"`
Col14 string `db:"col_14"`
Col15 string `db:"col_15"`
Col16 string `db:"col_16"`
Col17 string `db:"col_17"`
Col18 string `db:"col_18"`
Col19 string `db:"col_19"`
Col20 string `db:"col_20"`
Col21 string `db:"col_21"`
Col22 string `db:"col_22"`
Col23 string `db:"col_23"`
Col24 string `db:"col_24"`
Col25 string `db:"col_25"`
Col26 string `db:"col_26"`
Col27 string `db:"col_27"`
Col28 string `db:"col_28"`
Col29 string `db:"col_29"`
Col30 string `db:"col_30"`
Col31 string `db:"col_31"`
Col32 string `db:"col_32"`
Col33 string `db:"col_33"`
Col34 string `db:"col_34"`
Col35 string `db:"col_35"`
Col36 string `db:"col_36"`
Col37 string `db:"col_37"`
Col38 string `db:"col_38"`
Col39 string `db:"col_39"`
Col40 string `db:"col_40"`
Col41 string `db:"col_41"`
Col42 string `db:"col_42"`
Col43 string `db:"col_43"`
Col44 string `db:"col_44"`
}

type Userss struct {
ID int `db:"id"`
UserName string `db:"username"`
Expand Down
19 changes: 19 additions & 0 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,25 @@ func TestStruct(t *testing.T) {
})
}

func TestStructDuplicateColumns(t *testing.T) {
// Duplicate column names in the result set (e.g. `SELECT id, id`)
// all scan into the same field.
// Previously, every duplicate was scheduled to the first occurrence,
// leaving the others without a destination and failing with
// "no destination for column id".
user1 := User{ID: 1, Name: "foo"}
user2 := User{ID: 2, Name: "bar"}

testQuery(t, "duplicate columns", queryCase[User]{
columns: strstr{{"id", "int64"}, {"name", "string"}},
rows: rows{[]any{1, "foo"}, []any{2, "bar"}},
query: []string{"id", "id", "name"},
mapper: StructMapper[User](),
expectOne: user1,
expectAll: []User{user1, user2},
})
}

func TestAllowUnknownColumns(t *testing.T) {
type testStruct struct {
ID int64
Expand Down
1 change: 1 addition & 0 deletions mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func (v visited) copy() visited {

type mapinfo struct {
name string
colIndex int
position []int
init [][]int
isPointer bool
Expand Down
4 changes: 2 additions & 2 deletions mapper_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func (s regular[T]) regular() (func(*Row) (any, error), func(any) (T, error)) {
}

fv := row.FieldByIndex(info.position)
v.ScheduleScanByNameX(info.name, fv.Addr())
v.ScheduleScanByIndexX(info.colIndex, fv.Addr())
}

return row, nil
Expand Down Expand Up @@ -229,7 +229,7 @@ func (s regular[T]) allOptions() (func(*Row) (any, error), func(any) (T, error))
row[i] = reflect.New(ft)
}

v.ScheduleScanByNameX(info.name, row[i])
v.ScheduleScanByIndexX(info.colIndex, row[i])
}

return row, nil
Expand Down
11 changes: 11 additions & 0 deletions mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,17 @@ func TestStructMapper(t *testing.T) {
ExpectedVal: User{ID: 1, Name: "The Name"},
})

// Each occurrence of a duplicated column scans into the same field,
// so the value of the last occurrence wins.
RunStructMapperTest(t, "duplicate columns", MapperTest[User]{
row: &Row{
columns: columnNames("id", "id"),
},
scanned: []any{1, 2},
Mapper: StructMapper[User](),
ExpectedVal: User{ID: 2},
})

RunStructMapperTest(t, "with pointer columns 1", MapperTest[PtrUser1]{
row: &Row{
columns: columnNames("id", "name", "created_at", "updated_at"),
Expand Down
3 changes: 2 additions & 1 deletion source.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (s *mapperSourceImpl) setMappings(typ reflect.Type, prefix string, v visite
func filterColumns(c cols, m mapping, prefix string) (mapping, error) {
// Filter the mapping so we only ask for the available columns
filtered := make(mapping, 0, len(c))
for _, name := range c {
for colIdx, name := range c {
key := name
if prefix != "" {
if !strings.HasPrefix(name, prefix) {
Expand All @@ -253,6 +253,7 @@ func filterColumns(c cols, m mapping, prefix string) (mapping, error) {
for _, info := range m {
if key == info.name {
info.name = name
info.colIndex = colIdx
filtered = append(filtered, info)
break
}
Expand Down
Loading