From 605b7943b3795c5cb0b88b3f15613c8dc2b6758c Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Thu, 7 Jan 2021 01:42:42 +0100 Subject: [PATCH] Add identifier referencer (#3) --- .github/workflows/bench.yml | 69 +++++++++ .github/workflows/golangci-lint.yml | 2 +- .github/workflows/test-unit.yml | 75 ++++++++++ .github/workflows/test.yml | 40 ------ Makefile | 3 +- README.md | 58 +++++++- example_referencer_test.go | 55 ++++++++ go.mod | 4 +- go.sum | 2 + mapper.go | 24 ++++ referencer.go | 117 +++++++++++++++ referencer_test.go | 212 ++++++++++++++++++++++++++++ storage.go | 19 ++- 13 files changed, 633 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/bench.yml create mode 100644 .github/workflows/test-unit.yml delete mode 100644 .github/workflows/test.yml create mode 100644 example_referencer_test.go create mode 100644 referencer.go create mode 100644 referencer_test.go diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000..e8be7ef --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,69 @@ +name: bench +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: +env: + GO111MODULE: "on" +jobs: + bench: + strategy: + matrix: + go-version: [ 1.15.x ] + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Restore vendor + uses: actions/cache@v2 + with: + path: | + vendor + key: ${{ runner.os }}-go${{ matrix.go-version }}-vendor-${{ hashFiles('**/go.mod') }} + - name: Populate dependencies + run: | + (test -d vendor && echo vendor found) || (go mod vendor && du -sh vendor && du -sh ~/go/pkg/mod) + - name: Restore benchstat + uses: actions/cache@v2 + with: + path: ~/go/bin/benchstat + key: ${{ runner.os }}-benchstat + - name: Restore base benchmark result + uses: actions/cache@v2 + with: + path: | + bench-master.txt + bench-main.txt + # Use base sha for PR or new commit hash for master/main push in benchmark result key. + key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }} + - name: Benchmark + id: bench + run: | + export REF_NAME=${GITHUB_REF##*/} + BENCH_COUNT=5 make bench-run bench-stat + OUTPUT=$(make bench-stat) + OUTPUT="${OUTPUT//'%'/'%25'}" + OUTPUT="${OUTPUT//$'\n'/'%0A'}" + OUTPUT="${OUTPUT//$'\r'/'%0D'}" + echo "::set-output name=result::$OUTPUT" + - name: Comment Benchmark Result + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + header: bench + message: | + ### Benchmark Result +
Benchmark diff with base branch + + ``` + ${{ steps.bench.outputs.result }} + ``` +
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index c42e65d..746f57c 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,7 +17,7 @@ jobs: uses: golangci/golangci-lint-action@v2.3.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.32.2 + version: v1.34.1 # Optional: golangci-lint command line arguments. # args: ./the-only-dir-to-analyze/... diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml new file mode 100644 index 0000000..a8b0490 --- /dev/null +++ b/.github/workflows/test-unit.yml @@ -0,0 +1,75 @@ +name: test-unit +on: + push: + branches: + - master + - main + pull_request: +env: + GO111MODULE: "on" +jobs: + test: + strategy: + matrix: + go-version: [ 1.13.x, 1.14.x, 1.15.x ] + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Restore base test coverage + uses: actions/cache@v2 + with: + path: | + unit-base.txt + # Use base sha for PR or new commit hash for master/main push in test result key. + key: ${{ runner.os }}-unit-test-coverage-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }} + - name: Restore vendor + uses: actions/cache@v2 + with: + path: | + vendor + key: ${{ runner.os }}-go${{ matrix.go-version }}-vendor-${{ hashFiles('**/go.mod') }} + - name: Populate dependencies + run: | + (test -d vendor && echo vendor found) || (go mod vendor && du -sh vendor && du -sh ~/go/pkg/mod) + - name: Test + id: test + run: | + make test-unit + go tool cover -func=./unit.coverprofile | sed -e 's/.go:[0-9]*:\t/.go\t/g' | sed -e 's/\t\t*/\t/g' > unit.txt + OUTPUT=$(test -e unit-base.txt && (diff unit-base.txt unit.txt || exit 0) || cat unit.txt) + OUTPUT="${OUTPUT//'%'/'%25'}" + OUTPUT="${OUTPUT//$'\n'/'%0A'}" + OUTPUT="${OUTPUT//$'\r'/'%0D'}" + TOTAL=$(grep 'total:' unit.txt) + echo "::set-output name=diff::$OUTPUT" + echo "::set-output name=total::$TOTAL" + - name: Store base coverage + 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' + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + header: unit-test + message: | + ### Unit Test Coverage + ${{ steps.test.outputs.total }} +
Coverage diff with base branch + + ```diff + ${{ steps.test.outputs.diff }} + ``` +
+ + - name: Upload code coverage + if: matrix.go-version == '1.15.x' + uses: codecov/codecov-action@v1 + with: + file: ./unit.coverprofile + flags: unittests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index d151fc9..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: test -on: - push: - tags: - - v* - branches: - - master - - main - pull_request: -env: - GO111MODULE: "on" -jobs: - test: - strategy: - matrix: - go-version: [ 1.13.x, 1.14.x, 1.15.x ] - runs-on: ubuntu-latest - steps: - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v2 - - name: Restore vendor - uses: actions/cache@v1 - with: - path: vendor - key: ${{ runner.os }}-go-vendor-${{ hashFiles('**/go.mod') }} - - name: Populate dependencies - if: matrix.go-version == '1.15.x' # Use latest Go to populate vendor. - run: '(test -d vendor && echo vendor found) || go mod vendor' - - name: Test - run: make test-unit - - name: Upload code coverage - if: matrix.go-version == '1.15.x' - uses: codecov/codecov-action@v1 - with: - file: ./unit.coverprofile - flags: unittests diff --git a/Makefile b/Makefile index e0269f7..3cabc1e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,3 @@ -GOLANGCI_LINT_VERSION := "v1.32.2" - # The head of Makefile determines location of dev-go to include standard targets. GO ?= go export GO111MODULE = on @@ -29,6 +27,7 @@ endif -include $(DEVGO_PATH)/makefiles/main.mk -include $(DEVGO_PATH)/makefiles/test-unit.mk +-include $(DEVGO_PATH)/makefiles/bench.mk -include $(DEVGO_PATH)/makefiles/lint.mk -include $(DEVGO_PATH)/makefiles/github-actions.mk diff --git a/README.md b/README.md index 9dd9900..6055c7c 100644 --- a/README.md +++ b/README.md @@ -11,7 +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. -## Example +Field tags (`db` by default) act as a source of truth for column names to allow better maintainability and fewer errors. + +## Simple CRUD ```go var ( @@ -72,4 +74,58 @@ _, err = s.Exec(ctx, s.DeleteStmt(tableName).Where(Product{ID: 2}, sqluct.SkipZe if err != nil { log.Fatal(err) } +``` + +## Referencing Fields In Complex Statements + +```go +type User struct { + ID int `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` +} + +type DirectReport struct { + ManagerID int `db:"manager_id"` + EmployeeID int `db:"employee_id"` +} + +var s sqluct.Storage + +rf := s.Ref() + +// Add aliased tables as pointers to structs. +manager := &User{} +rf.AddTableAlias(manager, "manager") + +employee := &User{} +rf.AddTableAlias(employee, "employee") + +dr := &DirectReport{} +rf.AddTableAlias(dr, "dr") + +// Find direct reports that share same last name and manager is not named John. +qb := squirrel.StatementBuilder.Select(rf.Fmt("%s, %s", &dr.ManagerID, &dr.EmployeeID)). + From(rf.Fmt("%s AS %s", rf.Q("users"), manager)). // Quote literal name and alias it with registered struct pointer. + InnerJoin(rf.Fmt("%s AS %s ON %s = %s AND %s = %s", + rf.Q("direct_reports"), dr, + &dr.ManagerID, &manager.ID, // Identifiers are resolved using row field pointers. + &dr.EmployeeID, &employee.ID)). + Where(rf.Fmt("%s = %s", &manager.LastName, &employee.LastName)). + Where(rf.Fmt("%s != ?", &manager.FirstName), "John") // Regular binds work same way as in standard squirrel. + +stmt, args, err := qb.ToSql() +if err != nil { + log.Fatal(err) +} + +fmt.Println(stmt) +fmt.Println(args) + +// SELECT dr.manager_id, dr.employee_id +// FROM users AS manager +// INNER JOIN direct_reports AS dr ON dr.manager_id = manager.id AND dr.employee_id = employee.id +// WHERE manager.last_name = employee.last_name AND manager.first_name != ? +// +// [John] ``` \ No newline at end of file diff --git a/example_referencer_test.go b/example_referencer_test.go new file mode 100644 index 0000000..a9c303e --- /dev/null +++ b/example_referencer_test.go @@ -0,0 +1,55 @@ +package sqluct_test + +import ( + "fmt" + "log" + + "github.com/Masterminds/squirrel" + "github.com/bool64/sqluct" +) + +func ExampleReferencer_Fmt() { + type User struct { + ID int `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + + type DirectReport struct { + ManagerID int `db:"manager_id"` + EmployeeID int `db:"employee_id"` + } + + rf := sqluct.Referencer{} + + manager := &User{} + rf.AddTableAlias(manager, "manager") + + employee := &User{} + rf.AddTableAlias(employee, "employee") + + dr := &DirectReport{} + rf.AddTableAlias(dr, "dr") + + // Find direct reports that share same last name and manager is not named John. + qb := squirrel.StatementBuilder.Select(rf.Fmt("%s, %s", &dr.ManagerID, &dr.EmployeeID)). + From(rf.Fmt("%s AS %s", rf.Q("users"), manager)). + InnerJoin(rf.Fmt("%s AS %s ON %s = %s AND %s = %s", + rf.Q("direct_reports"), dr, + &dr.ManagerID, &manager.ID, + &dr.EmployeeID, &employee.ID)). + Where(rf.Fmt("%s = %s", &manager.LastName, &employee.LastName)). + Where(rf.Fmt("%s != ?", &manager.FirstName), "John") + + stmt, args, err := qb.ToSql() + if err != nil { + log.Fatal(err) + } + + fmt.Println(stmt) + fmt.Println(args) + + // Output: + // SELECT dr.manager_id, dr.employee_id FROM users AS manager INNER JOIN direct_reports AS dr ON dr.manager_id = manager.id AND dr.employee_id = employee.id WHERE manager.last_name = employee.last_name AND manager.first_name != ? + // [John] +} diff --git a/go.mod b/go.mod index c932e8f..20f413f 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/bool64/sqluct -go 1.15 +go 1.13 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.10 + github.com/bool64/dev v0.1.12 github.com/jmoiron/sqlx v1.2.0 github.com/stretchr/testify v1.6.1 google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index d3f3668..6e89490 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/bool64/ctxd v0.1.3/go.mod h1:rhUkoNE4mKFSJmo9l+78u2j+FVQifRCj0MHRhyZ2 github.com/bool64/dev v0.1.0/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg= github.com/bool64/dev v0.1.10 h1:4L6eLD+qo1QgWDy+Y7OhJxi/gLwOAuV1rd07noMc3dU= 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/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= diff --git a/mapper.go b/mapper.go index 4c40afe..f609edc 100644 --- a/mapper.go +++ b/mapper.go @@ -274,6 +274,30 @@ func (sm *Mapper) FindColumnName(structPtr, fieldPtr interface{}) (string, error return "", errFieldNotFound } +// FindColumnNames returns column names mapped by a pointer to a field. +func (sm *Mapper) FindColumnNames(structPtr interface{}) (map[interface{}]string, error) { + if structPtr == nil { + return nil, errNilArgument + } + + v := reflect.Indirect(reflect.ValueOf(structPtr)) + t := v.Type() + + if !v.CanAddr() { + return nil, errNotAPointer + } + + res := make(map[interface{}]string) + + tm := sm.reflectMapper().TypeMap(t) + for _, fi := range tm.Index { + fv := reflectx.FieldByIndexesReadOnly(v, fi.Index) + res[fv.Addr().Interface()] = fi.Name + } + + return res, nil +} + // Col will try to find column name and will panic on error. func (sm *Mapper) Col(structPtr, fieldPtr interface{}) string { name, err := sm.FindColumnName(structPtr, fieldPtr) diff --git a/referencer.go b/referencer.go new file mode 100644 index 0000000..e041a7a --- /dev/null +++ b/referencer.go @@ -0,0 +1,117 @@ +package sqluct + +import ( + "fmt" + "strings" +) + +// QuoteANSI adds double quotes to symbols names. +// +// Suitable for PostgreSQL, MySQL in ANSI SQL_MODE, SQLite statements. +// Used in Referencer by default. +func QuoteANSI(tableAndColumn ...string) string { + res := strings.Builder{} + + for i, item := range tableAndColumn { + if i != 0 { + res.WriteString(".") + } + + res.WriteString(`"`) + res.WriteString(strings.ReplaceAll(item, `"`, `""`)) + res.WriteString(`"`) + } + + return res.String() +} + +// QuoteBackticks quotes symbol names with backticks. +// +// Suitable for MySQL, SQLite statements. +func QuoteBackticks(tableAndColumn ...string) string { + res := strings.Builder{} + + for i, item := range tableAndColumn { + if i != 0 { + res.WriteString(".") + } + + res.WriteString("`") + res.WriteString(strings.ReplaceAll(item, "`", "``")) + res.WriteString("`") + } + + return res.String() +} + +// QuoteNoop does not add any quotes to symbol names. +func QuoteNoop(tableAndColumn ...string) string { + return strings.Join(tableAndColumn, ".") +} + +// Referencer maintains a list of string references to fields and table aliases. +type Referencer struct { + Mapper *Mapper + + // IdentifierQuoter is formatter of column and table names. + // Default QuoteNoop. + IdentifierQuoter func(tableAndColumn ...string) string + + refs map[interface{}]string +} + +// AddTableAlias creates string references for row pointer and all suitable field pointers in it. +func (r *Referencer) AddTableAlias(rowStructPtr interface{}, alias string) { + f, err := r.Mapper.FindColumnNames(rowStructPtr) + if err != nil { + panic(err) + } + + if r.refs == nil { + r.refs = make(map[interface{}]string, len(f)+1) + } + + r.refs[rowStructPtr] = r.Q(alias) + + for ptr, fieldName := range f { + r.refs[ptr] = r.Q(alias, fieldName) + } +} + +// Q quotes identifier. +func (r *Referencer) Q(tableAndColumn ...string) string { + q := r.IdentifierQuoter + if q == nil { + q = QuoteNoop + } + + return q(tableAndColumn...) +} + +// Ref returns reference string for struct or field pointer that was previously added with AddTableAlias. +// +// It panics if pointer is unknown. +func (r *Referencer) Ref(ptr interface{}) string { + if ref, found := r.refs[ptr]; found { + return ref + } + + panic(errFieldNotFound) +} + +// Fmt formats according to a format specified replacing ptrs with their reference strings where possible. +// +// Values that are not available as reference string are passed to fmt.Sprintf as is. +func (r *Referencer) Fmt(format string, ptrs ...interface{}) string { + args := make([]interface{}, 0, len(ptrs)) + + for _, fieldPtr := range ptrs { + if ref, found := r.refs[fieldPtr]; found { + args = append(args, ref) + } else { + args = append(args, fieldPtr) + } + } + + return fmt.Sprintf(format, args...) +} diff --git a/referencer_test.go b/referencer_test.go new file mode 100644 index 0000000..c59ff07 --- /dev/null +++ b/referencer_test.go @@ -0,0 +1,212 @@ +package sqluct_test + +import ( + "testing" + + "github.com/Masterminds/squirrel" + "github.com/bool64/sqluct" + "github.com/stretchr/testify/assert" +) + +func TestReferencer_Fmt(t *testing.T) { + rf := sqluct.Referencer{} + + type User struct { + ID int `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + + type DirectReport struct { + ManagerID int `db:"manager_id"` + EmployeeID int `db:"employee_id"` + } + + manager := &User{} + rf.AddTableAlias(manager, "manager") + + employee := &User{} + rf.AddTableAlias(employee, "employee") + + dr := &DirectReport{} + rf.AddTableAlias(dr, "dr") + + // Find direct reports that share same last name and manager is not named John. + qb := squirrel.StatementBuilder.Select(rf.Fmt("%s, %s", &dr.ManagerID, &dr.EmployeeID)). + From(rf.Fmt("%s AS %s", rf.Q("users"), manager)). + InnerJoin(rf.Fmt("%s AS %s ON %s = %s AND %s = %s", + rf.Q("direct_reports"), dr, + &dr.ManagerID, &manager.ID, + &dr.EmployeeID, &employee.ID)). + Where(rf.Fmt("%s = %s", &manager.LastName, &employee.LastName)). + Where(rf.Fmt("%s != ?", &manager.FirstName), "John") + + stmt, args, err := qb.ToSql() + assert.NoError(t, err) + assert.Equal(t, `SELECT dr.manager_id, dr.employee_id `+ + `FROM users AS manager `+ + `INNER JOIN direct_reports AS dr ON dr.manager_id = manager.id AND dr.employee_id = employee.id `+ + `WHERE manager.last_name = employee.last_name AND manager.first_name != ?`, stmt) + assert.Equal(t, []interface{}{"John"}, args) +} + +func TestReferencer_Ref(t *testing.T) { + rf := sqluct.Referencer{} + rf.IdentifierQuoter = sqluct.QuoteANSI + + row := &struct { + ID int `db:"id"` + }{} + + rf.AddTableAlias(row, "some_table") + assert.Equal(t, `"some_table"`, rf.Ref(row)) + assert.Equal(t, `"some_table"."id"`, rf.Ref(&row.ID)) + assert.Panics(t, func() { + rf.Ref(nil) + }) + assert.Panics(t, func() { + // Must not be nil. + rf.AddTableAlias(nil, "some_table") + }) + assert.Panics(t, func() { + // Must be a pointer. + rf.AddTableAlias(*row, "some_table") + }) +} + +func TestQuoteNoop(t *testing.T) { + assert.Equal(t, "one.two", sqluct.QuoteNoop("one", "two")) + assert.Equal(t, "", sqluct.QuoteNoop()) +} + +func TestQuoteBackticks(t *testing.T) { + assert.Equal(t, "`one`.`two`", sqluct.QuoteBackticks("one", "two")) + assert.Equal(t, "", sqluct.QuoteBackticks()) + assert.Equal(t, "`spacy id`.`back``ticky`.`quo\"ty`", sqluct.QuoteBackticks("spacy id", "back`ticky", `quo"ty`)) +} + +func TestQuoteANSI(t *testing.T) { + assert.Equal(t, `"one"."two"`, sqluct.QuoteANSI("one", "two")) + assert.Equal(t, "", sqluct.QuoteANSI()) + assert.Equal(t, `"spacy id"."back`+"`"+`ticky"."quo""ty"`, sqluct.QuoteANSI("spacy id", "back`ticky", `quo"ty`)) +} + +// Three benchmarks show different scenarios: +// * full - referencer is recreated for each iteration, formatting is done in each iteration, +// * lite - referencer is reused in all iterations, formatting is done in each iteration, +// * raw - referencer is not used, squirrel uses manually prepared template. +// +// Performance overhead seems affordable, especially in case of reusable referencer. +// +// Sample benchmark result: +// goos: darwin +// goarch: amd64 +// pkg: github.com/bool64/sqluct +// cpu: Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz +// BenchmarkReferencer_Fmt_full-8 78751 15938 ns/op 8472 B/op 130 allocs/op +// BenchmarkReferencer_Fmt_lite-8 151257 7939 ns/op 4785 B/op 102 allocs/op +// BenchmarkReferencer_Fmt_raw-8 169131 5986 ns/op 4040 B/op 75 allocs/op + +func BenchmarkReferencer_Fmt_full(b *testing.B) { + rf := sqluct.Referencer{} + + type User struct { + ID int `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + + type DirectReport struct { + ManagerID int `db:"manager_id"` + EmployeeID int `db:"employee_id"` + } + + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + manager := &User{} + rf.AddTableAlias(manager, "manager") + + employee := &User{} + rf.AddTableAlias(employee, "employee") + + dr := &DirectReport{} + rf.AddTableAlias(dr, "dr") + + // Find direct reports that share same last name and manager is not named John. + qb := squirrel.StatementBuilder.Select(rf.Fmt("%s, %s", &dr.ManagerID, &dr.EmployeeID)). + From(rf.Fmt("%s AS %s", rf.Q("users"), manager)). + InnerJoin(rf.Fmt("%s AS %s ON %s = %s AND %s = %s", + rf.Q("direct_reports"), dr, + &dr.ManagerID, &manager.ID, + &dr.EmployeeID, &employee.ID)). + Where(rf.Fmt("%s = %s", &manager.LastName, &employee.LastName)). + Where(rf.Fmt("%s != ?", &manager.FirstName), "John") + + _, _, err := qb.ToSql() + if err != nil { + b.Fail() + } + } +} + +func BenchmarkReferencer_Fmt_lite(b *testing.B) { + rf := sqluct.Referencer{} + + type User struct { + ID int `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + + type DirectReport struct { + ManagerID int `db:"manager_id"` + EmployeeID int `db:"employee_id"` + } + + manager := &User{} + rf.AddTableAlias(manager, "manager") + + employee := &User{} + rf.AddTableAlias(employee, "employee") + + dr := &DirectReport{} + rf.AddTableAlias(dr, "dr") + + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + // Find direct reports that share same last name and manager is not named John. + qb := squirrel.StatementBuilder.Select(rf.Fmt("%s, %s", &dr.ManagerID, &dr.EmployeeID)). + From(rf.Fmt("%s AS %s", rf.Q("users"), manager)). + InnerJoin(rf.Fmt("%s AS %s ON %s = %s AND %s = %s", + rf.Q("direct_reports"), dr, + &dr.ManagerID, &manager.ID, + &dr.EmployeeID, &employee.ID)). + Where(rf.Fmt("%s = %s", &manager.LastName, &employee.LastName)). + Where(rf.Fmt("%s != ?", &manager.FirstName), "John") + + _, _, err := qb.ToSql() + if err != nil { + b.Fail() + } + } +} + +func BenchmarkReferencer_Fmt_raw(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + // Find direct reports that share same last name and manager is not named John. + qb := squirrel.StatementBuilder.Select(`"dr"."manager_id", "dr"."employee_id"`). + From(`"users" AS "manager"`). + InnerJoin(`"direct_reports" AS "dr" ON "dr"."manager_id" = "manager"."id" AND "dr"."employee_id" = "employee"."id"`). + Where(`"manager"."last_name" = "employee"."last_name"`). + Where(`"manager"."first_name" != ?`, "John") + + _, _, err := qb.ToSql() + if err != nil { + b.Fail() + } + } +} diff --git a/storage.go b/storage.go index 3b4aadc..a0fca1b 100644 --- a/storage.go +++ b/storage.go @@ -42,6 +42,10 @@ type Storage struct { // Other values are squirrel.Question, squirrel.AtP and squirrel.Colon. Format squirrel.PlaceholderFormat + // IdentifierQuoter is formatter of column and table names. + // Default QuoteNoop. + IdentifierQuoter func(tableAndColumn ...string) string + // OnError is called when error is encountered, could be useful for logging. OnError func(ctx context.Context, err error) @@ -233,7 +237,20 @@ func (s *Storage) DeleteStmt(tableName string) squirrel.DeleteBuilder { // Col will try to find column name and will panic on error. func (s *Storage) Col(structPtr, fieldPtr interface{}) string { - return s.Mapper.Col(structPtr, fieldPtr) + col := s.Mapper.Col(structPtr, fieldPtr) + if s.IdentifierQuoter != nil { + col = s.IdentifierQuoter(col) + } + + return col +} + +// Ref creates Referencer for query builder. +func (s *Storage) Ref() *Referencer { + return &Referencer{ + Mapper: s.Mapper, + IdentifierQuoter: s.IdentifierQuoter, + } } // WhereEq maps struct values as conditions to squirrel.Eq.