From 953f461d2552b6606a46e1cc2e871dfdec2d99b0 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Mon, 22 Feb 2021 21:50:16 +0100 Subject: [PATCH] Add omitempty field option to skip zero values (#5) --- .github/workflows/bench.yml | 2 +- .github/workflows/golangci-lint.yml | 25 +++- .github/workflows/test-unit.yml | 6 +- .golangci.yml | 43 ++++++ README.md | 16 +- example_mapper_test.go | 18 +-- example_test.go | 9 +- go.mod | 5 +- go.sum | 29 ++-- mapper.go | 93 ++++++++++-- mapper_test.go | 224 +++++++++++++++++++++++++++- referencer.go | 7 +- storage.go | 3 +- 13 files changed, 403 insertions(+), 77 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index e8be7ef..88cef99 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -13,7 +13,7 @@ jobs: bench: strategy: matrix: - go-version: [ 1.15.x ] + go-version: [ 1.16.x ] runs-on: ubuntu-latest steps: - name: Install Go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 746f57c..8a23dae 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,16 +14,25 @@ jobs: steps: - uses: actions/checkout@v2 - name: golangci-lint - uses: golangci/golangci-lint-action@v2.3.0 + uses: golangci/golangci-lint-action@v2.4.0 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.34.1 + version: v1.37.0 + + # Optional: working directory, useful for monorepos + # working-directory: somedir # Optional: golangci-lint command line arguments. - # args: ./the-only-dir-to-analyze/... + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the action will use pre-installed Go. + # skip-go-installation: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true - # Required: the token is used for fetching a list of releases of golangci-lint. - # The secret `GITHUB_TOKEN` is automatically created by GitHub, - # no need to create it manually. - # https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#about-the-github_token-secret - github-token: ${{ secrets.GITHUB_TOKEN }} + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true \ No newline at end of file diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index a8b0490..ba1b816 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -11,7 +11,7 @@ jobs: test: strategy: matrix: - go-version: [ 1.13.x, 1.14.x, 1.15.x ] + go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x ] runs-on: ubuntu-latest steps: - name: Install Go @@ -52,7 +52,7 @@ jobs: if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }} run: cp unit.txt unit-base.txt - name: Comment Test Coverage - if: matrix.go-version == '1.15.x' + if: matrix.go-version == '1.16.x' uses: marocchino/sticky-pull-request-comment@v2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -68,7 +68,7 @@ jobs: - name: Upload code coverage - if: matrix.go-version == '1.15.x' + if: matrix.go-version == '1.16.x' uses: codecov/codecov-action@v1 with: file: ./unit.coverprofile diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..510fbbf --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,43 @@ +# See https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml +run: + tests: true + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + gocyclo: + min-complexity: 20 + dupl: + threshold: 100 + misspell: + locale: US + unused: + check-exported: false + unparam: + check-exported: true + +linters: + enable-all: true + disable: + - lll + - maligned + - gochecknoglobals + - gomnd + - wrapcheck + - paralleltest + - forbidigo + - exhaustivestruct + +issues: + exclude-use-default: false + exclude-rules: + - linters: + - gomnd + - goconst + - goerr113 + - noctx + - funlen + - dupl + path: "_test.go" + diff --git a/README.md b/README.md index 6055c7c..4903d68 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ This module integrates [`github.com/Masterminds/squirrel`](https://github.com/Ma and [`github.com/jmoiron/sqlx`](https://github.com/jmoiron/sqlx) to allow seamless operation based on field tags of row structure. +This library helps to eliminate literal string column references (e.g. `"created_at"`) and use field references +instead (e.g. `s.Col(&row, &row.CreatedAt)` and other mapping functions). + Field tags (`db` by default) act as a source of truth for column names to allow better maintainability and fewer errors. ## Simple CRUD @@ -24,9 +27,9 @@ var ( const tableName = "products" type Product struct { - ID int `db:"id"` + ID int `db:"id,omitempty"` Title string `db:"title"` - CreatedAt time.Time `db:"created_at"` + CreatedAt time.Time `db:"created_at,omitempty"` } // INSERT INTO products (id, title, created_at) VALUES (1, 'Apples', ), (2, 'Oranges', ) @@ -47,8 +50,8 @@ if err != nil { // UPDATE products SET title = 'Bananas' WHERE id = 2 _, err = s.Exec( ctx, - s.UpdateStmt(tableName, Product{Title: "Bananas"}, sqluct.SkipZeroValues). - Where(s.WhereEq(Product{ID: 2}, sqluct.SkipZeroValues)), + s.UpdateStmt(tableName, Product{Title: "Bananas"}). + Where(s.WhereEq(Product{ID: 2})), ) if err != nil { log.Fatal(err) @@ -128,4 +131,7 @@ fmt.Println(args) // WHERE manager.last_name = employee.last_name AND manager.first_name != ? // // [John] -``` \ No newline at end of file +``` + +## Omitting Zero Values + diff --git a/example_mapper_test.go b/example_mapper_test.go index f2fe24b..f138912 100644 --- a/example_mapper_test.go +++ b/example_mapper_test.go @@ -12,8 +12,8 @@ func ExampleMapper_Col() { sm := sqluct.Mapper{} type Order struct { - ID int `db:"order_id"` - CreatedAt time.Time `db:"created_at"` + ID int `db:"order_id,omitempty"` + CreatedAt time.Time `db:"created_at,omitempty"` } o := Order{ @@ -36,7 +36,7 @@ func ExampleMapper_Insert() { sm := sqluct.Mapper{} type Order struct { - ID int `db:"order_id"` + ID int `db:"order_id,omitempty"` Amount int `db:"amount"` UserID int `db:"user_id"` } @@ -45,7 +45,7 @@ func ExampleMapper_Insert() { o.Amount = 100 o.UserID = 123 - q := sm.Insert(squirrel.Insert("orders"), o, sqluct.SkipZeroValues) + q := sm.Insert(squirrel.Insert("orders"), o) query, args, err := q.ToSql() fmt.Println(query, args, err) @@ -58,11 +58,11 @@ func ExampleMapper_Update() { type OrderData struct { Amount int `db:"amount"` - UserID int `db:"user_id"` + UserID int `db:"user_id,omitempty"` } type Order struct { - ID int `db:"order_id"` + ID int `db:"order_id,omitempty"` OrderData } @@ -86,11 +86,11 @@ func ExampleMapper_Select() { type OrderData struct { Amount int `db:"amount"` - UserID int `db:"user_id"` + UserID int `db:"user_id,omitempty"` } type Order struct { - ID int `db:"order_id"` + ID int `db:"order_id,omitempty"` OrderData } @@ -112,7 +112,7 @@ func ExampleMapper_WhereEq() { type OrderData struct { Amount int `db:"amount"` - UserID int `db:"user_id"` + UserID int `db:"user_id,omitempty"` } type Order struct { diff --git a/example_test.go b/example_test.go index 73e7080..f363b11 100644 --- a/example_test.go +++ b/example_test.go @@ -139,8 +139,7 @@ func ExampleStorage_Select_oneRow() { qb := s.SelectStmt("my_table", row) - err := s.Select(ctx, qb, &row) - if err != nil { + if err := s.Select(ctx, qb, &row); err != nil { log.Fatal(err) } } @@ -163,8 +162,7 @@ func ExampleStorage_InsertStmt() { qb := s.InsertStmt("my_table", row) - _, err := s.Exec(ctx, qb) - if err != nil { + if _, err := s.Exec(ctx, qb); err != nil { log.Fatal(err) } } @@ -192,8 +190,7 @@ func ExampleStorage_UpdateStmt() { qb := s.UpdateStmt("my_table", row). Where(s.Mapper.WhereEq(MyIdentity{ID: 123})) - _, err := s.Exec(ctx, qb) - if err != nil { + if _, err := s.Exec(ctx, qb); err != nil { log.Fatal(err) } } diff --git a/go.mod b/go.mod index 20f413f..74f8c7f 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,7 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/squirrel v1.5.0 github.com/bool64/ctxd v0.1.3 - github.com/bool64/dev v0.1.12 - github.com/jmoiron/sqlx v1.2.0 + github.com/bool64/dev v0.1.19 + github.com/jmoiron/sqlx v1.3.1 github.com/stretchr/testify v1.6.1 - google.golang.org/appengine v1.6.7 // indirect ) diff --git a/go.sum b/go.sum index 4903c30..791f094 100644 --- a/go.sum +++ b/go.sum @@ -6,24 +6,23 @@ github.com/bool64/ctxd v0.1.3 h1:n+aQ6UdoZXlltETFXqIhZR2DoMxhMYE2CW9BQn8aNBY= github.com/bool64/ctxd v0.1.3/go.mod h1:rhUkoNE4mKFSJmo9l+78u2j+FVQifRCj0MHRhyZ2GDA= github.com/bool64/dev v0.1.0/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg= github.com/bool64/dev v0.1.10/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg= -github.com/bool64/dev v0.1.12 h1:mbuWBtCtOGwqt2lN1/9oPmn70XaOHdv2YHzQ31Zf9ks= -github.com/bool64/dev v0.1.12/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg= +github.com/bool64/dev v0.1.19 h1:SLAOGmJIrbroYjkGuV0MBLtww2NddzPDne1C7ATJdJk= +github.com/bool64/dev v0.1.19/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= +github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -33,14 +32,6 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8 h1:XGJJai6ngqYpy/cTMvGreiO+jradIn1uq7Q5hY2OCF4= github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8/go.mod h1:rcngDv7OaBXZyEXdEtimcDeNon7sq3iqLm9hxT06s3c= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/mapper.go b/mapper.go index 580cb64..453db3e 100644 --- a/mapper.go +++ b/mapper.go @@ -33,6 +33,11 @@ func SkipZeroValues(o *Options) { o.SkipZeroValues = true } +// IgnoreOmitEmpty instructs mapper to use zero values of fields with `omitempty`. +func IgnoreOmitEmpty(o *Options) { + o.IgnoreOmitEmpty = true +} + // Columns is used to control which columns from the structure should be used. func Columns(columns ...string) func(o *Options) { return func(o *Options) { @@ -47,9 +52,12 @@ func OrderDesc(o *Options) { // Options defines mapping parameters. type Options struct { - // SkipZeroValues instructs mapper to ignore fields with zero values. + // SkipZeroValues instructs mapper to ignore fields with zero values regardless of `omitempty` tag. SkipZeroValues bool + // IgnoreOmitEmpty instructs mapper to use zero values of fields with `omitempty`. + IgnoreOmitEmpty bool + // Columns is used to control which columns from the structure should be used. Columns []string @@ -66,19 +74,74 @@ func (sm *Mapper) Insert(q squirrel.InsertBuilder, val interface{}, options ...f v := reflect.Indirect(reflect.ValueOf(val)) if v.Kind() == reflect.Slice { - for i := 0; i < v.Len(); i++ { - item := v.Index(i) - cols, vals := sm.ColumnsValues(item, options...) + return sm.sliceInsert(q, v, options...) + } + + cols, vals := sm.ColumnsValues(v, options...) + q = q.Columns(cols...) + q = q.Values(vals...) + + return q +} + +func (sm *Mapper) sliceInsert(q squirrel.InsertBuilder, v reflect.Value, options ...func(*Options)) squirrel.InsertBuilder { + var ( + hCols = make(map[string]struct{}) + heterogeneous = false + qq = q + ) + + for i := 0; i < v.Len(); i++ { + item := v.Index(i) + cols, vals := sm.ColumnsValues(item, options...) + + if i == 0 { + for _, c := range cols { + hCols[c] = struct{}{} + } - if i == 0 { - q = q.Columns(cols...) + qq = qq.Columns(cols...) + } else { + for _, c := range cols { + if _, found := hCols[c]; !found { + heterogeneous = true + hCols[c] = struct{}{} + } } + } - q = q.Values(vals...) + if !heterogeneous { + qq = qq.Values(vals...) } - } else { - cols, vals := sm.ColumnsValues(v, options...) - q = q.Columns(cols...) + } + + if heterogeneous { + return sm.heterogeneousInsert(q, v, hCols, options...) + } + + return qq +} + +func (sm *Mapper) heterogeneousInsert(q squirrel.InsertBuilder, v reflect.Value, hCols map[string]struct{}, options ...func(*Options)) squirrel.InsertBuilder { + cols := make([]string, 0, len(hCols)) + for c := range hCols { + cols = append(cols, c) + } + + options = append(options[0:len(options):len(options)], func(options *Options) { + options.SkipZeroValues = false + options.IgnoreOmitEmpty = true + options.Columns = cols + }) + + for i := 0; i < v.Len(); i++ { + item := v.Index(i) + cols, vals := sm.ColumnsValues(item, options...) + + if i == 0 { + q = q.Columns(cols...) + } + q = q.Values(vals...) } @@ -105,7 +168,7 @@ func (sm *Mapper) Select(q squirrel.SelectBuilder, columns interface{}, options return q } - cols, _ := sm.ColumnsValues(reflect.ValueOf(columns), options...) + cols, _ := sm.ColumnsValues(reflect.ValueOf(columns), append(options, IgnoreOmitEmpty)...) q = q.Columns(cols...) return q @@ -240,7 +303,13 @@ func (sm *Mapper) ColumnsValues(v reflect.Value, options ...func(*Options)) ([]s colV := reflectx.FieldByIndexesReadOnly(v, fi.Index) val := colV.Interface() - if o.SkipZeroValues && isZero(colV, val) { + _, omitEmpty := fi.Options["omitempty"] + + if o.IgnoreOmitEmpty && omitEmpty { + omitEmpty = false + } + + if (o.SkipZeroValues || omitEmpty) && isZero(colV, val) { continue } diff --git a/mapper_test.go b/mapper_test.go index d7617f9..d3de425 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -11,14 +11,14 @@ import ( type ( Sample struct { - A int `db:"a"` + A int `db:"a,omitempty"` DeeplyEmbedded // Recursively embedded fields are used as root fields. Meta AnotherRow `db:"meta"` // Meta is a column, but its fields are not. } DeeplyEmbedded struct { SampleEmbedded - E string `db:"e"` + E string `db:"e,omitempty"` } SampleEmbedded struct { @@ -53,6 +53,79 @@ func TestInsertValue(t *testing.T) { assert.Equal(t, []interface{}{1, AnotherRow{SampleEmbedded: SampleEmbedded{B: 0, C: ""}, D: ""}, "e!", 2.2, "3"}, args) } +func BenchmarkMapper_Insert_single(b *testing.B) { + z := Sample{ + A: 1, + DeeplyEmbedded: DeeplyEmbedded{ + SampleEmbedded: SampleEmbedded{ + B: 2.2, + C: "3", + }, + E: "e!", + }, + } + + ps := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + sm := sqluct.Mapper{} + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + q := sm.Insert(ps.Insert("sample"), z) + + _, _, err := q.ToSql() + if err != nil { + b.Fail() + } + } +} + +func TestInsertValue_omitempty(t *testing.T) { + z := Sample{} + + ps := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + sm := sqluct.Mapper{} + q := sm.Insert(ps.Insert("sample"), z) + query, args, err := q.ToSql() + assert.NoError(t, err) + // a and e are missing for `omitempty` + assert.Equal(t, "INSERT INTO sample (meta,b,c) VALUES ($1,$2,$3)", query) + assert.Equal(t, []interface{}{AnotherRow{SampleEmbedded: SampleEmbedded{B: 0, C: ""}, D: ""}, 0.0, ""}, args) +} + +func BenchmarkMapper_Insert_singleOmitempty(b *testing.B) { + z := Sample{} + + ps := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + sm := sqluct.Mapper{} + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + q := sm.Insert(ps.Insert("sample"), z) + + _, _, err := q.ToSql() + if err != nil { + b.Fail() + } + } +} + +func TestInsertValue_IgnoreOmitEmpty(t *testing.T) { + z := Sample{} + + ps := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + sm := sqluct.Mapper{} + q := sm.Insert(ps.Insert("sample"), z, sqluct.IgnoreOmitEmpty) + query, args, err := q.ToSql() + assert.NoError(t, err) + // a and e are missing for `omitempty` + assert.Equal(t, "INSERT INTO sample (a,meta,e,b,c) VALUES ($1,$2,$3,$4,$5)", query) + assert.Equal(t, []interface{}{0, AnotherRow{SampleEmbedded: SampleEmbedded{B: 0, C: ""}, D: ""}, "", 0.0, ""}, args) +} + func TestMapper_Insert_nil(t *testing.T) { sm := sqluct.Mapper{} q := squirrel.Insert("sample") @@ -71,7 +144,49 @@ func TestMapper_Select_nil(t *testing.T) { assert.Equal(t, q, sm.Select(q, nil)) } -func TestInsertValueSlice(t *testing.T) { +func TestInsertValueSlice_heterogeneous(t *testing.T) { + z := []Sample{ + { + A: 0, + DeeplyEmbedded: DeeplyEmbedded{ + SampleEmbedded: SampleEmbedded{ + B: 2.2, + C: "3", + }, + E: "e!", + }, + }, + { + A: 4, + DeeplyEmbedded: DeeplyEmbedded{ + SampleEmbedded: SampleEmbedded{ + B: 5.5, + C: "6", + }, + E: "ee!", + }, + }, + } + + ps := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + sm := sqluct.Mapper{} + q := ps.Insert("sample") + assert.Equal(t, q, sm.Insert(q, nil)) + q = sm.Insert(q, z) + query, args, err := q.ToSql() + assert.NoError(t, err) + assert.Equal(t, "INSERT INTO sample (a,meta,e,b,c) VALUES ($1,$2,$3,$4,$5),($6,$7,$8,$9,$10)", query) + assert.Equal(t, []interface{}{ + 0, + AnotherRow{SampleEmbedded: SampleEmbedded{B: 0, C: ""}, D: ""}, + "e!", 2.2, "3", + 4, + AnotherRow{SampleEmbedded: SampleEmbedded{B: 0, C: ""}, D: ""}, + "ee!", 5.5, "6", + }, args) +} + +func TestInsertValueSlice_homogeneous(t *testing.T) { z := []Sample{ { A: 1, @@ -113,6 +228,88 @@ func TestInsertValueSlice(t *testing.T) { }, args) } +func BenchmarkMapper_Insert_slice_heterogeneous(b *testing.B) { + z := []Sample{ + { + A: 0, + DeeplyEmbedded: DeeplyEmbedded{ + SampleEmbedded: SampleEmbedded{ + B: 2.2, + C: "3", + }, + E: "e!", + }, + }, + { + A: 4, + DeeplyEmbedded: DeeplyEmbedded{ + SampleEmbedded: SampleEmbedded{ + B: 5.5, + C: "6", + }, + E: "ee!", + }, + }, + } + + ps := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + sm := sqluct.Mapper{} + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + q := ps.Insert("sample") + q = sm.Insert(q, z) + + _, _, err := q.ToSql() + if err != nil { + b.Fail() + } + } +} + +func BenchmarkMapper_Insert_slice_homogeneous(b *testing.B) { + z := []Sample{ + { + A: 1, + DeeplyEmbedded: DeeplyEmbedded{ + SampleEmbedded: SampleEmbedded{ + B: 2.2, + C: "3", + }, + E: "e!", + }, + }, + { + A: 4, + DeeplyEmbedded: DeeplyEmbedded{ + SampleEmbedded: SampleEmbedded{ + B: 5.5, + C: "6", + }, + E: "ee!", + }, + }, + } + + ps := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + sm := sqluct.Mapper{} + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + q := ps.Insert("sample") + q = sm.Insert(q, z) + + _, _, err := q.ToSql() + if err != nil { + b.Fail() + } + } +} + func TestInsertValueSlicePtr(t *testing.T) { z := []Sample{ { @@ -178,7 +375,7 @@ func TestMapper_Update(t *testing.T) { } func TestMapper_Select_struct(t *testing.T) { - z := SampleEmbedded{} + z := Sample{} sm := sqluct.Mapper{} q := sm.Select(squirrel.Select(), z) @@ -186,10 +383,27 @@ func TestMapper_Select_struct(t *testing.T) { query, args, err := q.ToSql() assert.NoError(t, err) - assert.Equal(t, "SELECT b, c FROM sample", query) + assert.Equal(t, "SELECT a, meta, e, b, c FROM sample", query) assert.Equal(t, []interface{}(nil), args) } +func BenchmarkMapper_Select_struct(b *testing.B) { + z := Sample{} + sm := sqluct.Mapper{} + + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + q := sm.Select(squirrel.Select(), z) + q = q.From("sample") + + _, _, err := q.ToSql() + if err != nil { + b.Fail() + } + } +} + func TestMapper_Select_slice(t *testing.T) { z := []SampleEmbedded{} diff --git a/referencer.go b/referencer.go index e041a7a..9a691fe 100644 --- a/referencer.go +++ b/referencer.go @@ -80,12 +80,11 @@ func (r *Referencer) AddTableAlias(rowStructPtr interface{}, alias string) { // Q quotes identifier. func (r *Referencer) Q(tableAndColumn ...string) string { - q := r.IdentifierQuoter - if q == nil { - q = QuoteNoop + if r.IdentifierQuoter == nil { + return QuoteNoop(tableAndColumn...) } - return q(tableAndColumn...) + return r.IdentifierQuoter(tableAndColumn...) } // Ref returns reference string for struct or field pointer that was previously added with AddTableAlias. diff --git a/storage.go b/storage.go index a0fca1b..b83a061 100644 --- a/storage.go +++ b/storage.go @@ -62,8 +62,7 @@ type Storage struct { func (s *Storage) InTx(ctx context.Context, fn func(context.Context) error) (err error) { var finish func(ctx context.Context, err error) error - tx := TxFromContext(ctx) - if tx == nil { + if tx := TxFromContext(ctx); tx == nil { finish = s.submitTx // Start a new transaction.