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.