From cfb2396dd94546084e57ac8cf218dab9971cb5a2 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Tue, 11 Oct 2022 20:54:07 +0200 Subject: [PATCH] Add INSERT IGNORE option (#12) --- .github/workflows/bench.yml | 2 +- .github/workflows/cloc.yml | 4 +- .github/workflows/golangci-lint.yml | 6 +- .github/workflows/gorelease.yml | 6 +- .github/workflows/test-unit.yml | 53 +++++++++++- .golangci.yml | 14 ++++ Makefile | 2 +- go.mod | 4 +- go.sum | 9 ++- mapper.go | 120 +++++++++++++++++++++------- mapper_test.go | 29 +++++++ storage.go | 13 ++- 12 files changed, 211 insertions(+), 51 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 0e6bddc..d750b9d 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -21,7 +21,7 @@ env: GO111MODULE: "on" CACHE_BENCHMARK: "off" # Enables benchmark result reuse between runs, may skew latency results. RUN_BASE_BENCHMARK: "on" # Runs benchmark for PR base in case benchmark result is missing. - GO_VERSION: 1.18.x + GO_VERSION: 1.19.x jobs: bench: runs-on: ubuntu-latest diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml index 4592bb4..619ca74 100644 --- a/.github/workflows/cloc.yml +++ b/.github/workflows/cloc.yml @@ -24,7 +24,9 @@ jobs: - name: Count Lines Of Code id: loc run: | - curl -sLO https://github.com/vearutop/sccdiff/releases/download/v1.0.2/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && echo "b17e76bede22af0206b4918d3b3c4e7357f2a21b57f8de9e7c9dc0eb56b676c0 sccdiff" | shasum -c + curl -sLO https://github.com/vearutop/sccdiff/releases/download/v1.0.3/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz + sccdiff_hash=$(git hash-object ./sccdiff) + [ "$sccdiff_hash" == "ae8a07b687bd3dba60861584efe724351aa7ff63" ] || (echo "::error::unexpected hash for sccdiff, possible tampering: $sccdiff_hash" && exit 1) OUTPUT=$(cd pr && ../sccdiff -basedir ../base) echo "${OUTPUT}" OUTPUT="${OUTPUT//$'\n'/%0A}" diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 48207f9..bf0bcdb 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,13 +21,13 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.18.x + go-version: 1.19.x - uses: actions/checkout@v2 - name: golangci-lint - uses: golangci/golangci-lint-action@v3.1.0 + uses: golangci/golangci-lint-action@v3.2.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.46.2 + version: v1.50.0 # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml index 0a5a701..6267500 100644 --- a/.github/workflows/gorelease.yml +++ b/.github/workflows/gorelease.yml @@ -9,7 +9,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: 1.18.x + GO_VERSION: 1.19.x jobs: gorelease: runs-on: ubuntu-latest @@ -35,11 +35,11 @@ jobs: with: path: | ~/go/bin/gorelease - key: ${{ runner.os }}-gorelease-fork + key: ${{ runner.os }}-gorelease-generic - name: Gorelease id: gorelease run: | - test -e ~/go/bin/gorelease || (rm -rf /tmp/gorelease && mkdir -p /tmp/gorelease && cd /tmp/gorelease && go mod init foo && go mod edit -replace golang.org/x/exp=github.com/vearutop/golang-exp@gorelease-generic && go get golang.org/x/exp/cmd/gorelease && go install golang.org/x/exp/cmd/gorelease) + test -e ~/go/bin/gorelease || go install golang.org/x/exp/cmd/gorelease@latest OUTPUT=$(gorelease 2>&1 || exit 0) echo "${OUTPUT}" OUTPUT="${OUTPUT//$'\n'/%0A}" diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 485eddc..7ff164c 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -14,12 +14,14 @@ concurrency: env: GO111MODULE: "on" + RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing. COV_GO_VERSION: 1.18.x # Version of Go to collect coverage + TARGET_DELTA_COV: 90 # Target coverage of changed lines, in percents jobs: test: strategy: matrix: - go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x ] + go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x ] runs-on: ubuntu-latest steps: - name: Install Go stable @@ -27,6 +29,7 @@ jobs: uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} + - name: Install Go tip if: matrix.go-version == 'tip' run: | @@ -36,8 +39,10 @@ jobs: tar -C ~/sdk/gotip -xzf gotip.tar.gz ~/sdk/gotip/bin/go version echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV + - name: Checkout code uses: actions/checkout@v2 + - name: Go cache uses: actions/cache@v2 with: @@ -50,25 +55,54 @@ jobs: key: ${{ runner.os }}-go-cache-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go-cache + + - name: Restore base test coverage + id: base-coverage + if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' + 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: Run test for base code + if: matrix.go-version == env.COV_GO_VERSION && env.RUN_BASE_COVERAGE == 'on' && steps.base-coverage.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != '' + run: | + git fetch origin master ${{ github.event.pull_request.base.sha }} + HEAD=$(git rev-parse HEAD) + git reset --hard ${{ github.event.pull_request.base.sha }} + (make test-unit && go tool cover -func=./unit.coverprofile > unit-base.txt) || echo "No test-unit in base" + git reset --hard $HEAD + - 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 + go tool cover -func=./unit.coverprofile > unit.txt TOTAL=$(grep 'total:' unit.txt) echo "${TOTAL}" echo "::set-output name=total::$TOTAL" + - name: Annotate missing test coverage id: annotate if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' run: | + curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.6/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz + gocovdiff_hash=$(git hash-object ./gocovdiff) + [ "$gocovdiff_hash" == "8e507e0d671d4d6dfb3612309b72b163492f28eb" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1) git fetch origin master ${{ github.event.pull_request.base.sha }} - curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.0.0/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && echo "6b8bec07488b84e46c1b8993353323effe863493013309fd3df4f2cba9a2bb29 gocovdiff" | shasum -c - REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt) + REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) echo "${REP}" REP="${REP//$'\n'/%0A}" cat gha-unit.txt + DIFF=$(test -e unit-base.txt && ./gocovdiff -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file") + DIFF="${DIFF//$'\n'/%0A}" + TOTAL=$(cat delta-cov-unit.txt) echo "::set-output name=rep::$REP" + echo "::set-output name=diff::$DIFF" + echo "::set-output name=total::$TOTAL" + - name: Comment Test Coverage continue-on-error: true if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' @@ -79,12 +113,23 @@ jobs: message: | ### Unit Test Coverage ${{ steps.test.outputs.total }} + ${{ steps.annotate.outputs.total }}
Coverage of changed lines ${{ steps.annotate.outputs.rep }}
+
Coverage diff with base branch + + ${{ steps.annotate.outputs.diff }} + +
+ + - name: Store base coverage + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }} + run: cp unit.txt unit-base.txt + - name: Upload code coverage if: matrix.go-version == env.COV_GO_VERSION uses: codecov/codecov-action@v1 diff --git a/.golangci.yml b/.golangci.yml index 9fa56cf..219b387 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,6 +39,12 @@ linters: - ireturn - exhaustruct - nonamedreturns + - nosnakecase + - structcheck + - varcheck + - deadcode + - testableexamples + - dupword issues: exclude-use-default: false @@ -50,5 +56,13 @@ issues: - noctx - funlen - dupl + - structcheck + - unused + - unparam + - nosnakecase path: "_test.go" + - linters: + - errcheck # Error checking omitted for brevity. + - gosec + path: "example_" diff --git a/Makefile b/Makefile index c181688..80dc683 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -#GOLANGCI_LINT_VERSION := "v1.46.2" # Optional configuration to pinpoint golangci-lint version. +#GOLANGCI_LINT_VERSION := "v1.50.0" # Optional configuration to pinpoint golangci-lint version. # The head of Makefile determines location of dev-go to include standard targets. GO ?= go diff --git a/go.mod b/go.mod index 7fb0782..1cc6eb4 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/squirrel v1.5.3 github.com/bool64/ctxd v1.1.3 - github.com/bool64/dev v0.2.13 + github.com/bool64/dev v0.2.22 github.com/jmoiron/sqlx v1.3.5 - github.com/stretchr/testify v1.7.2 + github.com/stretchr/testify v1.8.0 ) require ( diff --git a/go.sum b/go.sum index 4e3fafc..24af0c3 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4 github.com/bool64/ctxd v1.1.3 h1:YXnsXdiB0wTsyaR+PgRBDj8c0ny2lP4QxYb33i2nk7A= github.com/bool64/ctxd v1.1.3/go.mod h1:ZJBWwFBYTMSES2gWQ+Q8ajTEMR/C1vAsbNhbml+Qk1o= github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= -github.com/bool64/dev v0.2.13 h1:Q52eTk/VeYpgqxfBRvdM4dvxA+QzFt+d+w93eTc8Xa0= -github.com/bool64/dev v0.2.13/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8= +github.com/bool64/dev v0.2.22 h1:YJFKBRKplkt+0Emq/5Xk1Z5QRmMNzc1UOJkR3rxJksA= +github.com/bool64/dev v0.2.22/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= 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= @@ -25,12 +25,13 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A 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= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/swaggest/usecase v1.1.2 h1:2LfuSyjYtPtpHnxqPwV87/eunbhGBC5HKdRp8/fINBk= github.com/swaggest/usecase v1.1.2/go.mod h1:abZWuMFYujaeLDODqRySJZpWD/ugsnE3Wj9K6jUeCjo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/mapper.go b/mapper.go index 5c433f6..a57cc1e 100644 --- a/mapper.go +++ b/mapper.go @@ -2,6 +2,7 @@ package sqluct import ( "errors" + "fmt" "reflect" "sync" @@ -18,6 +19,7 @@ var ( // Mapper prepares select, insert and update statements. type Mapper struct { ReflectMapper *reflectx.Mapper + Dialect Dialect mu sync.Mutex types map[reflect.Type]*reflectx.StructMap @@ -38,7 +40,12 @@ func IgnoreOmitEmpty(o *Options) { o.IgnoreOmitEmpty = true } -// Columns is used to control which columns from the structure should be used. +// InsertIgnore enables ignoring of row conflict during INSERT. +func InsertIgnore(o *Options) { + o.InsertIgnore = true +} + +// Columns are used to control which columns from the structure should be used. func Columns(columns ...string) func(o *Options) { return func(o *Options) { o.Columns = columns @@ -50,7 +57,7 @@ func OrderDesc(o *Options) { o.OrderDesc = true } -// Options defines mapping parameters. +// Options defines mapping and query building parameters. type Options struct { // SkipZeroValues instructs mapper to ignore fields with zero values regardless of `omitempty` tag. SkipZeroValues bool @@ -66,6 +73,13 @@ type Options struct { // PrepareColumn allows control of column quotation or aliasing. PrepareColumn func(col string) string + + // InsertIgnore enables ignoring of row conflict during INSERT. + // Uses + // - INSERT IGNORE for MySQL, + // - INSERT ON IGNORE for SQLite3, + // - INSERT ... ON CONFLICT DO NOTHING for Postgres. + InsertIgnore bool } // Insert adds struct value or slice of struct values to squirrel.InsertBuilder. @@ -75,19 +89,39 @@ func (sm *Mapper) Insert(q squirrel.InsertBuilder, val interface{}, options ...f } v := reflect.Indirect(reflect.ValueOf(val)) + o := Options{} + + for _, option := range options { + option(&o) + } if v.Kind() == reflect.Slice { - return sm.sliceInsert(q, v, options...) + return sm.sliceInsert(q, v, o) } - cols, vals := sm.ColumnsValues(v, options...) + cols, vals := sm.columnsValues(v, o) q = q.Columns(cols...) q = q.Values(vals...) + if o.InsertIgnore { + switch sm.Dialect { + case DialectMySQL: + q = q.Options("IGNORE") + case DialectSQLite3: + q = q.Options("OR IGNORE") + case DialectPostgres: + q = q.Suffix("ON CONFLICT DO NOTHING") + case DialectUnknown: + panic("can not apply INSERT IGNORE for unknown dialect") + default: + panic(fmt.Sprintf("can not apply INSERT IGNORE for dialect %q", sm.Dialect)) + } + } + return q } -func (sm *Mapper) sliceInsert(q squirrel.InsertBuilder, v reflect.Value, options ...func(*Options)) squirrel.InsertBuilder { +func (sm *Mapper) sliceInsert(q squirrel.InsertBuilder, v reflect.Value, o Options) squirrel.InsertBuilder { var ( hCols = make(map[string]struct{}) heterogeneous = false @@ -96,7 +130,7 @@ func (sm *Mapper) sliceInsert(q squirrel.InsertBuilder, v reflect.Value, options for i := 0; i < v.Len(); i++ { item := v.Index(i) - cols, vals := sm.ColumnsValues(item, options...) + cols, vals := sm.columnsValues(item, o) if i == 0 { for _, c := range cols { @@ -119,27 +153,25 @@ func (sm *Mapper) sliceInsert(q squirrel.InsertBuilder, v reflect.Value, options } if heterogeneous { - return sm.heterogeneousInsert(q, v, hCols, options...) + return sm.heterogeneousInsert(q, v, hCols, o) } return qq } -func (sm *Mapper) heterogeneousInsert(q squirrel.InsertBuilder, v reflect.Value, hCols map[string]struct{}, options ...func(*Options)) squirrel.InsertBuilder { +func (sm *Mapper) heterogeneousInsert(q squirrel.InsertBuilder, v reflect.Value, hCols map[string]struct{}, o 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 - }) + o.SkipZeroValues = false + o.IgnoreOmitEmpty = true + o.Columns = cols for i := 0; i < v.Len(); i++ { item := v.Index(i) - cols, vals := sm.ColumnsValues(item, options...) + cols, vals := sm.columnsValues(item, o) if i == 0 { q = q.Columns(cols...) @@ -157,7 +189,13 @@ func (sm *Mapper) Update(q squirrel.UpdateBuilder, val interface{}, options ...f return q } - cols, vals := sm.ColumnsValues(reflect.ValueOf(val), options...) + o := Options{} + + for _, option := range options { + option(&o) + } + + cols, vals := sm.columnsValues(reflect.ValueOf(val), o) for i, col := range cols { q = q.Set(col, vals[i]) } @@ -171,7 +209,15 @@ func (sm *Mapper) Select(q squirrel.SelectBuilder, columns interface{}, options return q } - cols, _ := sm.ColumnsValues(reflect.ValueOf(columns), append(options, IgnoreOmitEmpty)...) + o := Options{} + + for _, option := range options { + option(&o) + } + + o.IgnoreOmitEmpty = true + + cols, _ := sm.columnsValues(reflect.ValueOf(columns), o) q = q.Columns(cols...) return q @@ -179,7 +225,13 @@ func (sm *Mapper) Select(q squirrel.SelectBuilder, columns interface{}, options // WhereEq maps struct values as conditions to squirrel.Eq. func (sm *Mapper) WhereEq(conditions interface{}, options ...func(*Options)) squirrel.Eq { - columns, values := sm.ColumnsValues(reflect.ValueOf(conditions), options...) + o := Options{} + + for _, option := range options { + option(&o) + } + + columns, values := sm.columnsValues(reflect.ValueOf(conditions), o) eq := make(squirrel.Eq, len(columns)) for i, column := range columns { @@ -197,16 +249,16 @@ func (sm *Mapper) WhereEq(conditions interface{}, options ...func(*Options)) squ // // Deprecated: use Col with DESC/ASC. func (sm *Mapper) Order(columns interface{}, options ...func(*Options)) string { - cols, _ := sm.ColumnsValues(reflect.ValueOf(columns), options...) - order := "" - orderDir := " ASC" - o := Options{} for _, option := range options { option(&o) } + cols, _ := sm.columnsValues(reflect.ValueOf(columns), o) + order := "" + orderDir := " ASC" + if o.OrderDesc { orderDir = " DESC" } @@ -222,16 +274,11 @@ func (sm *Mapper) Order(columns interface{}, options ...func(*Options)) string { return "" } -func (sm *Mapper) colType(v reflect.Value, options ...func(*Options)) (*reflectx.StructMap, Options, bool) { +func (sm *Mapper) colType(v reflect.Value) (*reflectx.StructMap, bool) { v = reflect.Indirect(v) k := v.Kind() t := v.Type() skipValues := false - o := Options{} - - for _, option := range options { - option(&o) - } if k == reflect.Slice || k == reflect.Array { t = t.Elem() @@ -245,7 +292,7 @@ func (sm *Mapper) colType(v reflect.Value, options ...func(*Options)) (*reflectx tm := sm.typeMap(t) - return tm, o, skipValues + return tm, skipValues } func (sm *Mapper) skip(fi *reflectx.FieldInfo, columns []string) bool { @@ -293,7 +340,17 @@ func isZero(colV reflect.Value, val interface{}) bool { // ColumnsValues extracts columns and values from provided struct value. func (sm *Mapper) ColumnsValues(v reflect.Value, options ...func(*Options)) ([]string, []interface{}) { - tm, o, skipValues := sm.colType(v, options...) + o := Options{} + + for _, option := range options { + option(&o) + } + + return sm.columnsValues(v, o) +} + +func (sm *Mapper) columnsValues(v reflect.Value, o Options) ([]string, []interface{}) { + tm, skipValues := sm.colType(v) columns := make([]string, 0, len(tm.Index)) values := make([]interface{}, 0, len(tm.Index)) @@ -332,8 +389,9 @@ func (sm *Mapper) ColumnsValues(v reflect.Value, options ...func(*Options)) ([]s // FindColumnName returns column name of a database entity field. // // Entity field is defined by pointer to owner structure and pointer to field in that structure. -// entity := MyEntity{} -// name, found := sm.FindColumnName(&entity, &entity.UpdatedAt) +// +// entity := MyEntity{} +// name, found := sm.FindColumnName(&entity, &entity.UpdatedAt) func (sm *Mapper) FindColumnName(structPtr, fieldPtr interface{}) (string, error) { if structPtr == nil || fieldPtr == nil { return "", errNilArgument diff --git a/mapper_test.go b/mapper_test.go index d2c5e55..7267818 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -598,3 +598,32 @@ func BenchmarkMapper_Select_ref(b *testing.B) { } } } + +func TestInsertIgnore(t *testing.T) { + s := sqluct.Storage{} + + assert.Panics(t, func() { + s.InsertStmt("table", Sample{}, sqluct.InsertIgnore) + }) + + s.Mapper = &sqluct.Mapper{} + s.Mapper.Dialect = sqluct.DialectMySQL + s.Format = squirrel.Question + assertStatement(t, "INSERT IGNORE INTO table (meta,b,c) VALUES (?,?,?)", s.InsertStmt("table", Sample{}, sqluct.InsertIgnore)) + + s.Mapper.Dialect = sqluct.DialectSQLite3 + s.Format = squirrel.Question + assertStatement(t, "INSERT OR IGNORE INTO table (meta,b,c) VALUES (?,?,?)", s.InsertStmt("table", Sample{}, sqluct.InsertIgnore)) + + s.Mapper.Dialect = sqluct.DialectPostgres + s.Format = squirrel.Dollar + assertStatement(t, "INSERT INTO table (meta,b,c) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING", s.InsertStmt("table", Sample{}, sqluct.InsertIgnore)) +} + +func assertStatement(t *testing.T, s string, qb sqluct.ToSQL) { + t.Helper() + + stmt, _, err := qb.ToSql() + assert.NoError(t, err) + assert.Equal(t, s, stmt) +} diff --git a/storage.go b/storage.go index f850ffc..cb6da3c 100644 --- a/storage.go +++ b/storage.go @@ -20,7 +20,7 @@ type ToSQL interface { type StringStatement string // ToSql implements query builder result. -func (s StringStatement) ToSql() (string, []interface{}, error) { // nolint // Method name matches ext. implementation. +func (s StringStatement) ToSql() (string, []interface{}, error) { //nolint // Method name matches ext. implementation. return string(s), nil, nil } @@ -54,6 +54,17 @@ type Storage struct { Trace func(ctx context.Context, stmt string, args []interface{}) (newCtx context.Context, onFinish func(error)) } +// Dialect defines SQL dialect. +type Dialect string + +// Supported dialects. +const ( + DialectUnknown = Dialect("") + DialectMySQL = Dialect("mysql") + DialectPostgres = Dialect("postgres") + DialectSQLite3 = Dialect("sqlite3") +) + // InTx runs callback in a transaction. // // If transaction already exists, it will reuse that. Otherwise it starts a new transaction and commit or rollback