Skip to content

Commit

Permalink
Add identifier referencer (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Jan 7, 2021
1 parent 6131e82 commit 605b794
Show file tree
Hide file tree
Showing 13 changed files with 633 additions and 47 deletions.
69 changes: 69 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -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
<details><summary>Benchmark diff with base branch</summary>
```
${{ steps.bench.outputs.result }}
```
</details>
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/...
Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
@@ -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 }}
<details><summary>Coverage diff with base branch</summary>
```diff
${{ steps.test.outputs.diff }}
```
</details>
- name: Upload code coverage
if: matrix.go-version == '1.15.x'
uses: codecov/codecov-action@v1
with:
file: ./unit.coverprofile
flags: unittests
40 changes: 0 additions & 40 deletions .github/workflows/test.yml

This file was deleted.

3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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]
```
55 changes: 55 additions & 0 deletions example_referencer_test.go
Original file line number Diff line number Diff line change
@@ -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]
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
24 changes: 24 additions & 0 deletions mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 605b794

Please sign in to comment.