From c134dd2c933d954b563f3bb231fe67884daeb55c Mon Sep 17 00:00:00 2001 From: diwu31 Date: Thu, 30 Oct 2025 15:11:16 +0800 Subject: [PATCH] Add a new feature for Select --- select_benchmark_test.go | 27 ++++++++++++++++++ select_test.go | 22 ++++++++++++++- statement.go | 59 +++++++++++++++++++++++++++++++++++++++- statement_test.go | 32 ++++++++++++++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 select_benchmark_test.go diff --git a/select_benchmark_test.go b/select_benchmark_test.go new file mode 100644 index 00000000..bd580125 --- /dev/null +++ b/select_benchmark_test.go @@ -0,0 +1,27 @@ +package sqrl + +import "testing" + +type SimpleStruct struct { + ID int + Name string + Age int +} + +func Benchmark_SelectStruct(b *testing.B) { + s := SimpleStruct{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + // 重复调用,验证缓存效果 + SelectFromStruct(&s) + } +} + +func Benchmark_Select(b *testing.B) { + b.ResetTimer() + fields := []string{"ID", "Name", "Age"} + for i := 0; i < b.N; i++ { + // 手写字段,仅作对比 + Select(fields...) + } +} diff --git a/select_test.go b/select_test.go index 09d4ec48..f33598eb 100644 --- a/select_test.go +++ b/select_test.go @@ -96,7 +96,6 @@ func TestSelectBuilderZeroOffsetLimit(t *testing.T) { assert.Equal(t, expectedSql, sql) } - func TestSelectBuilderFromSelect(t *testing.T) { subQ := Select("c").From("d").Where(Eq{"i": 0}) b := Select("a", "b").FromSelect(subQ, "subq") @@ -110,6 +109,27 @@ func TestSelectBuilderFromSelect(t *testing.T) { assert.Equal(t, expectedArgs, args) } +func TestSelectBuilderFromSelectWithPojo(t *testing.T) { + s1 := struct { + C string `db:"c"` + }{} + + s2 := struct { + A string `db:"a"` + B string `db:"b"` + }{} + subQ := SelectFromStruct(s1).From("d").Where(Eq{"i": 0}) + b := SelectFromStruct(s2).FromSelect(subQ, "subq") + sql, args, err := b.ToSql() + assert.NoError(t, err) + + expectedSql := "SELECT a, b FROM (SELECT c FROM d WHERE i = ?) AS subq" + assert.Equal(t, expectedSql, sql) + + expectedArgs := []interface{}{0} + assert.Equal(t, expectedArgs, args) +} + func TestSelectBuilderToSqlErr(t *testing.T) { _, _, err := Select().From("x").ToSql() assert.Error(t, err) diff --git a/statement.go b/statement.go index 2feeb694..7ccfa214 100644 --- a/statement.go +++ b/statement.go @@ -1,9 +1,15 @@ package sqrl +import ( + "errors" + "reflect" +) + // StatementBuilderType is the type of StatementBuilder. type StatementBuilderType struct { placeholderFormat PlaceholderFormat runWith BaseRunner + columnsCacher columnsCacher } // Select returns a SelectBuilder for this StatementBuilder. @@ -38,9 +44,18 @@ func (b StatementBuilderType) RunWith(runner BaseRunner) StatementBuilderType { return b } +// SelectFromStruct according to the target returns a SelectBuilder for this StatementBuilder. +func (b StatementBuilderType) SelectFromStruct(target interface{}) *SelectBuilder { + fields, err := b.columnsCacher.parseFields(target) + if err != nil { + return b.Select("*") + } + return b.Select(fields...) +} + // StatementBuilder is a basic statement builder, holds global configuration options // like placeholder format or SQL runner -var StatementBuilder = StatementBuilderType{placeholderFormat: Question} +var StatementBuilder = StatementBuilderType{placeholderFormat: Question, columnsCacher: columnsCacher{cache: make(map[reflect.Type][]string)}} // Select returns a new SelectBuilder, optionally setting some result columns. // @@ -49,6 +64,48 @@ func Select(columns ...string) *SelectBuilder { return StatementBuilder.Select(columns...) } +// SelectFromStruct returns a new SelectBuilder, according to the pojo object. +// +// See SelectBuilder.Columns. +func SelectFromStruct(target interface{}) *SelectBuilder { + return StatementBuilder.SelectFromStruct(target) +} + +// columnsCacher for store the pojo struct to columns +type columnsCacher struct { + cache map[reflect.Type][]string +} + +func (cacher *columnsCacher) parseFields(v interface{}) ([]string, error) { + if v == nil { + return nil, errors.New("must not be nil") + } + t := reflect.TypeOf(v) + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, errors.New("must be a struct") + } + if fields, ok := cacher.cache[t]; ok { + return fields, nil + } + fields := make([]string, 0, t.NumField()) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + tag := field.Tag.Get("db") + if field.PkgPath == "" { + if tag != "" && tag != "-" { + fields = append(fields, tag) + } else { + fields = append(fields, field.Name) + } + } + } + cacher.cache[t] = fields + return fields, nil +} + // Insert returns a new InsertBuilder with the given table name. // // See InsertBuilder.Into. diff --git a/statement_test.go b/statement_test.go index 54954c71..cad5f7b3 100644 --- a/statement_test.go +++ b/statement_test.go @@ -15,6 +15,38 @@ func TestStatementBuilder(t *testing.T) { assert.Equal(t, "SELECT test", db.LastExecSql) } +func TestStatementWithPojo_WithTag(t *testing.T) { + db := &DBStub{} + sb := StatementBuilder.RunWith(db) + + user := &struct { + Name string `db:"user_name"` + Password string `db:"password"` + otherField string + limitSelectField string `db:"-"` + }{ + Name: "John Doe", + Password: "fgakfgkahfjka", + } + sb.SelectFromStruct(&user).Exec() + assert.Equal(t, "SELECT user_name, password", db.LastExecSql) +} + +func TestStatementWithPojo_WithNoTag(t *testing.T) { + db := &DBStub{} + sb := StatementBuilder.RunWith(db) + + user := &struct { + Name string + Password string + }{ + Name: "John Doe", + Password: "fgakfgkahfjka", + } + sb.SelectFromStruct(&user).Exec() + assert.Equal(t, "SELECT Name, Password", db.LastExecSql) +} + func TestStatementBuilderPlaceholderFormat(t *testing.T) { db := &DBStub{} sb := StatementBuilder.RunWith(db).PlaceholderFormat(Dollar)