diff --git a/bench_test.go b/bench_test.go index 7902ec2..eb2a242 100644 --- a/bench_test.go +++ b/bench_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/signal" + "strings" "syscall" "testing" "time" @@ -18,6 +19,7 @@ import ( var ( db *sql.DB dataSize = 100 + wideSize = 1000 ) func TestMain(m *testing.M) { @@ -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) @@ -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" @@ -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"` diff --git a/exec_test.go b/exec_test.go index 7143c0b..fa85aea 100644 --- a/exec_test.go +++ b/exec_test.go @@ -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 diff --git a/mapper.go b/mapper.go index 0b7aa49..b0c8f59 100644 --- a/mapper.go +++ b/mapper.go @@ -23,6 +23,7 @@ func (v visited) copy() visited { type mapinfo struct { name string + colIndex int position []int init [][]int isPointer bool diff --git a/mapper_struct.go b/mapper_struct.go index df47033..83536e1 100644 --- a/mapper_struct.go +++ b/mapper_struct.go @@ -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 @@ -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 diff --git a/mapper_test.go b/mapper_test.go index 75fabd9..e9b406e 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -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"), diff --git a/source.go b/source.go index 808456d..113fc21 100644 --- a/source.go +++ b/source.go @@ -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) { @@ -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 }