├── .codecov.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── custom.md ├── RELEASE_CHECKLIST.md ├── appveyor-prepare-mssql.ps1 ├── dependabot.yml ├── reform.png ├── shell.go └── workflows │ ├── ci.yaml │ └── cleanup.yaml ├── .gitignore ├── .golangci-required.yml ├── .golangci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── appveyor.yml ├── base.go ├── base_test.go ├── db.go ├── db_test.go ├── dialects ├── dialects.go ├── mssql │ └── mssql.go ├── mysql │ └── mysql.go ├── postgresql │ └── postgresql.go ├── sqlite3 │ └── sqlite3.go └── sqlserver │ └── sqlserver.go ├── doc.go ├── docker-compose.override.yml.apple-silicon ├── docker-compose.yml ├── docs ├── _config.yml └── test.md ├── go.mod ├── go.sum ├── internal ├── logger.go └── test │ ├── db.go │ └── models │ ├── bogus │ ├── bogus1.go │ ├── bogus10.go │ ├── bogus11.go │ ├── bogus2.go │ ├── bogus3.go │ ├── bogus4.go │ ├── bogus5.go │ ├── bogus6.go │ ├── bogus7.go │ ├── bogus8.go │ ├── bogus9.go │ └── bogus_ignore.go │ ├── extra.go │ ├── extra_reform.go │ ├── good.go │ └── good_reform.go ├── logger.go ├── parse ├── base.go ├── file.go ├── parse_test.go └── runtime.go ├── querier.go ├── querier_commands.go ├── querier_commands_test.go ├── querier_examples_test.go ├── querier_selects.go ├── querier_selects_test.go ├── querier_test.go ├── reform-db ├── base_test.go ├── cmd_exec.go ├── cmd_init.go ├── cmd_init_mssql.go ├── cmd_init_mysql.go ├── cmd_init_postgres.go ├── cmd_init_sqlite3.go ├── cmd_init_template.go ├── cmd_init_test.go ├── cmd_query.go ├── main.go ├── models.go └── models_reform.go ├── reform ├── main.go ├── template.go └── version_check.go ├── test └── sql │ ├── data.sql │ ├── mssql_create.sql │ ├── mssql_data.sql │ ├── mssql_drop.sql │ ├── mssql_init.sql │ ├── mssql_set.sql │ ├── mysql_create.sql │ ├── mysql_data.sql │ ├── mysql_drop.sql │ ├── mysql_init.sql │ ├── mysql_set.sql │ ├── postgres_create.sql │ ├── postgres_data.sql │ ├── postgres_drop.sql │ ├── postgres_init.sql │ ├── postgres_set.sql │ ├── sqlite3_create.sql │ ├── sqlite3_data.sql │ ├── sqlite3_drop.sql │ ├── sqlite3_init.sql │ └── sqlite3_set.sql ├── tools ├── go.mod ├── go.sum └── tools.go └── tx.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | codecov: 3 | branch: "main" 4 | 5 | coverage: 6 | range: "50...80" 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. 15 | 2. 16 | 3. 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/custom.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-reform/reform/db2c976d4b2fa1df1f9e16b4c314e18c5b475969/.github/PULL_REQUEST_TEMPLATE/custom.md -------------------------------------------------------------------------------- /.github/RELEASE_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | 3 | 1. Check tests, linters. 4 | 2. Check issues and pull requests, update milestones. 5 | 3. Bump version in `doc.go`. 6 | 4. Update CHANGELOG.md. 7 | 5. Make tag. 8 | 6. Push it! 9 | 7. Make [release](https://github.com/go-reform/reform/releases). 10 | 8. Refresh 11 | * `env GO111MODULE=on GOPROXY=https://proxy.golang.org go get -v gopkg.in/reform.v1@v1.M.P` 12 | * https://pkg.go.dev/gopkg.in/reform.v1@v1.M.P 13 | * https://godoc.org/gopkg.in/reform.v1 14 | * https://goreportcard.com/report/gopkg.in/reform.v1 15 | -------------------------------------------------------------------------------- /.github/appveyor-prepare-mssql.ps1: -------------------------------------------------------------------------------- 1 | # Based on http://www.appveyor.com/docs/services-databases 2 | 3 | [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null 4 | [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | Out-Null 5 | 6 | $instanceName = $env:REFORM_SQL_INSTANCE 7 | $uri = "ManagedComputer[@Name='$env:COMPUTERNAME']/ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Tcp']" 8 | $wmi = New-Object ('Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer') 9 | $tcp = $wmi.GetSmoObject($uri) 10 | $tcp.IsEnabled = $true 11 | $tcp.Alter() 12 | 13 | Start-Service "MSSQL`$$instanceName" 14 | 15 | Set-Service SQLBrowser -StartupType Manual 16 | Start-Service SQLBrowser 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | - package-ecosystem: "gomod" 9 | directory: "/tools" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/reform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-reform/reform/db2c976d4b2fa1df1f9e16b4c314e18c5b475969/.github/reform.png -------------------------------------------------------------------------------- /.github/shell.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | // Custom shell for GNU Make to measure command execution time. 7 | 8 | import ( 9 | "log" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | log.SetFlags(0) 18 | 19 | cmd := exec.Command("/bin/bash", os.Args[1:]...) 20 | cmd.Stdout = os.Stdout 21 | cmd.Stderr = os.Stderr 22 | 23 | start := time.Now() 24 | err := cmd.Run() 25 | d := time.Since(start) 26 | if d > time.Second { 27 | log.Printf("===> %s: %s", strings.Join(cmd.Args, " "), d) 28 | } 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | schedule: 5 | # Saturday 8:00, after cleanup 6 | - cron: "0 8 * * 6" 7 | push: 8 | branches: 9 | - main 10 | - hotfix/** 11 | - release/** 12 | pull_request: 13 | 14 | jobs: 15 | # test job 16 | test: 17 | name: Test 18 | timeout-minutes: 10 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: 24 | - ubuntu-20.04 25 | go-version: 26 | - 1.17.x 27 | - tip 28 | images: 29 | - { postgres: "postgres:9.5", mysql: "mysql:5.6", mssql: "mcr.microsoft.com/mssql/server:2017-latest" } 30 | - { postgres: "postgres:9.6", mysql: "mysql:5.7", mssql: "mcr.microsoft.com/mssql/server:2019-latest" } 31 | - { postgres: "postgres:10", mysql: "mysql:8.0" } 32 | - { postgres: "postgres:11" } 33 | - { postgres: "postgres:12" } 34 | - { postgres: "postgres:13" } 35 | 36 | runs-on: ${{ matrix.os }} 37 | 38 | env: 39 | # no `-mod=readonly` to test PRs made by @dependabot; 40 | # `make ci-check-changes` step below still checks what we need 41 | WORKDIR: "${{ github.workspace }}/gopath/src/gopkg.in/reform.v1" 42 | GOPATH: "${{ github.workspace }}/gopath" 43 | GOBIN: "${{ github.workspace }}/gopath/bin" 44 | GO111MODULE: "on" 45 | GOPROXY: "https://proxy.golang.org" 46 | GORACE: "halt_on_error=1" 47 | REFORM_POSTGRES_IMAGE: "${{ matrix.images.postgres }}" 48 | REFORM_MYSQL_IMAGE: "${{ matrix.images.mysql }}" 49 | REFORM_MSSQL_IMAGE: "${{ matrix.images.mssql }}" 50 | 51 | steps: 52 | # Cache Go modules, build cache, installed packages and GOPATH sources 53 | # to significantly decreases total CI time. See also cleanup.yaml. 54 | - name: Enable Go cache 55 | uses: actions/cache@v2 56 | with: 57 | path: | 58 | ~/.cache/go-build 59 | ${{ env.GOPATH }}/pkg 60 | ${{ env.GOPATH }}/src/github.com 61 | ${{ env.GOPATH }}/src/golang.org 62 | key: ${{ matrix.os }}-${{ matrix.go-version }} 63 | 64 | - name: Set up Go version ${{ matrix.go-version }} 65 | if: matrix.go-version != 'tip' 66 | uses: actions/setup-go@v2 67 | with: 68 | go-version: ${{ matrix.go-version }} 69 | 70 | - name: Set up Go tip 71 | if: matrix.go-version == 'tip' 72 | run: | 73 | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip 74 | cd $HOME/gotip/src 75 | ./make.bash 76 | echo "GOROOT=$HOME/gotip" >> $GITHUB_ENV 77 | echo "$HOME/gotip/bin" >> $GITHUB_PATH 78 | echo "$GOBIN" >> $GITHUB_PATH 79 | 80 | - name: Set GO_VERSION 81 | run: echo "GO_VERSION=$(go version)" >> $GITHUB_ENV 82 | 83 | - name: Check out code into GOPATH 84 | uses: actions/checkout@v2 85 | with: 86 | path: ${{ env.WORKDIR }} 87 | 88 | - name: Pull Docker images 89 | working-directory: ${{ env.WORKDIR }} 90 | run: docker-compose pull 91 | 92 | - name: Stop Ubuntu services 93 | run: sudo systemctl stop mysql 94 | 95 | # FIXME Is there a more ergonomic way? 96 | - name: Update Go language version in the module 97 | working-directory: ${{ env.WORKDIR }} 98 | run: go mod edit -go=$(go list -f '{{ $tag := 0 }}{{ range $tag = context.ReleaseTags }}{{ end }}{{ slice $tag 2 }}' runtime) 99 | 100 | - name: Download Go modules 101 | working-directory: ${{ env.WORKDIR }} 102 | run: go mod download 103 | 104 | - name: Check that it is still possible to install reform without modules 105 | working-directory: ${{ env.WORKDIR }} 106 | run: | 107 | env GO111MODULE=off go get -v -x ./... 108 | reform -version 109 | reform-db -version 110 | 111 | - name: Run init target 112 | working-directory: ${{ env.WORKDIR }} 113 | run: make init 114 | 115 | - name: Start development environment 116 | working-directory: ${{ env.WORKDIR }} 117 | run: make env-up-detach 118 | 119 | - name: Run test target 120 | working-directory: ${{ env.WORKDIR }} 121 | run: make test 122 | 123 | # TODO test again with updated deps 124 | 125 | - name: Clean Go test cache 126 | if: ${{ always() }} 127 | run: go clean -testcache 128 | 129 | # to ensure that all generators still work the same way 130 | - name: Check that there are no source code changes 131 | working-directory: ${{ env.WORKDIR }} 132 | run: make ci-check-changes 133 | 134 | - name: Upload coverage information 135 | working-directory: ${{ env.WORKDIR }} 136 | run: bash <(curl -s https://codecov.io/bash) -f coverage.txt -X fix -e GO_VERSION,REFORM_POSTGRES_IMAGE,REFORM_MYSQL_IMAGE,REFORM_MSSQL_IMAGE 137 | 138 | - name: Run debug commands on failure 139 | if: ${{ failure() }} 140 | run: | 141 | sudo apt-get install -qy tree 142 | env 143 | go version 144 | go env 145 | pwd 146 | tree -d 147 | ls -al 148 | docker --version 149 | docker-compose --version 150 | 151 | # lint job 152 | lint: 153 | name: Lint 154 | timeout-minutes: 10 155 | 156 | strategy: 157 | fail-fast: false 158 | matrix: 159 | os: 160 | - ubuntu-20.04 161 | go-version: 162 | - 1.17.x 163 | 164 | runs-on: ${{ matrix.os }} 165 | 166 | env: 167 | # no `-mod=readonly` to test PRs made by @dependabot; 168 | # `make ci-check-changes` step below still checks what we need 169 | WORKDIR: "${{ github.workspace }}/gopath/src/gopkg.in/reform.v1" 170 | GOPATH: "${{ github.workspace }}/gopath" 171 | GOBIN: "${{ github.workspace }}/gopath/bin" 172 | GO111MODULE: "on" 173 | GOPROXY: "https://proxy.golang.org" 174 | GORACE: "halt_on_error=1" 175 | REFORM_POSTGRES_IMAGE: "${{ matrix.images.postgres }}" 176 | REFORM_MYSQL_IMAGE: "${{ matrix.images.mysql }}" 177 | REFORM_MSSQL_IMAGE: "${{ matrix.images.mssql }}" 178 | 179 | steps: 180 | # Cache Go modules, build cache, installed packages and GOPATH sources 181 | # to significantly decreases total CI time. See also cleanup.yaml. 182 | - name: Enable Go cache 183 | uses: actions/cache@v2 184 | with: 185 | path: | 186 | ~/.cache/go-build 187 | ${{ env.GOPATH }}/pkg 188 | ${{ env.GOPATH }}/src/github.com 189 | ${{ env.GOPATH }}/src/golang.org 190 | key: ${{ matrix.os }}-${{ matrix.go-version }} 191 | 192 | - name: Set up Go version ${{ matrix.go-version }} 193 | if: matrix.go-version != 'tip' 194 | uses: actions/setup-go@v2 195 | with: 196 | go-version: ${{ matrix.go-version }} 197 | 198 | - name: Set up Go tip 199 | if: matrix.go-version == 'tip' 200 | run: | 201 | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip 202 | cd $HOME/gotip/src 203 | ./make.bash 204 | echo "GOROOT=$HOME/gotip" >> $GITHUB_ENV 205 | echo "$HOME/gotip/bin" >> $GITHUB_PATH 206 | echo "$GOBIN" >> $GITHUB_PATH 207 | 208 | - name: Set GO_VERSION 209 | run: echo "GO_VERSION=$(go version)" >> $GITHUB_ENV 210 | 211 | - name: Check out code into GOPATH 212 | uses: actions/checkout@v2 213 | with: 214 | path: ${{ env.WORKDIR }} 215 | 216 | # FIXME Is there a more ergonomic way? 217 | - name: Update Go language version in the module 218 | working-directory: ${{ env.WORKDIR }} 219 | run: go mod edit -go=$(go list -f '{{ $tag := 0 }}{{ range $tag = context.ReleaseTags }}{{ end }}{{ slice $tag 2 }}' runtime) 220 | 221 | - name: Download Go modules 222 | working-directory: ${{ env.WORKDIR }} 223 | run: go mod download 224 | 225 | - name: Run init target 226 | working-directory: ${{ env.WORKDIR }} 227 | run: make init 228 | 229 | - name: Run checks/linters 230 | working-directory: ${{ env.WORKDIR }} 231 | run: | 232 | # use GITHUB_TOKEN because only it has access to GitHub Checks API 233 | bin/golangci-lint run --config=.golangci-required.yml --out-format=line-number | env REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GITHUB_TOKEN }} bin/reviewdog -f=golangci-lint -name='Required linters' -reporter=github-check 234 | 235 | # use GO_REFORM_BOT_TOKEN for better reviewer's name 236 | bin/golangci-lint run --out-format=line-number | env REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GO_REFORM_BOT_TOKEN }} bin/reviewdog -f=golangci-lint -name='Optional linters' -reporter=github-pr-review 237 | bin/go-consistent -pedantic ./... | env REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GO_REFORM_BOT_TOKEN }} bin/reviewdog -f=go-consistent -name='go-consistent' -reporter=github-pr-review 238 | 239 | # to ensure that all generators still work the same way 240 | - name: Check that there are no source code changes 241 | working-directory: ${{ env.WORKDIR }} 242 | run: make ci-check-changes 243 | 244 | - name: Run debug commands on failure 245 | if: ${{ failure() }} 246 | run: | 247 | sudo apt-get install -qy tree 248 | env 249 | go version 250 | go env 251 | pwd 252 | tree -d 253 | ls -al 254 | docker --version 255 | docker-compose --version 256 | -------------------------------------------------------------------------------- /.github/workflows/cleanup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Cleanup 3 | on: 4 | schedule: 5 | # Saturday 7:00 6 | - cron: "0 7 * * 6" 7 | 8 | jobs: 9 | clean: 10 | name: Clean caches 11 | timeout-minutes: 5 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-20.04 18 | go-version: 19 | - 1.17.x 20 | - tip 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | env: 25 | WORKDIR: "${{ github.workspace }}/gopath/src/gopkg.in/reform.v1" 26 | GOPATH: "${{ github.workspace }}/gopath" 27 | GOBIN: "${{ github.workspace }}/gopath/bin" 28 | GO111MODULE: "on" 29 | GOPROXY: "https://proxy.golang.org" 30 | 31 | steps: 32 | # Cache Go modules, build cache, installed packages and GOPATH sources 33 | # to significantly decreases total CI time. See also cleanup.yaml. 34 | - name: Enable Go cache 35 | uses: actions/cache@v2 36 | with: 37 | path: | 38 | ~/.cache/go-build 39 | ${{ env.GOPATH }}/pkg 40 | ${{ env.GOPATH }}/src/github.com 41 | ${{ env.GOPATH }}/src/golang.org 42 | key: ${{ matrix.os }}-${{ matrix.go-version }} 43 | 44 | - name: Set up Go version ${{ matrix.go-version }} 45 | if: matrix.go-version != 'tip' 46 | uses: actions/setup-go@v2 47 | with: 48 | go-version: ${{ matrix.go-version }} 49 | 50 | - name: Set up Go tip 51 | if: matrix.go-version == 'tip' 52 | run: | 53 | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip 54 | cd $HOME/gotip/src 55 | ./make.bash 56 | echo "GOROOT=$HOME/gotip" >> $GITHUB_ENV 57 | echo "$HOME/gotip/bin" >> $GITHUB_PATH 58 | echo "$GOBIN" >> $GITHUB_PATH 59 | 60 | - name: Check out code into GOPATH 61 | uses: actions/checkout@v2 62 | with: 63 | path: ${{ env.WORKDIR }} 64 | 65 | - name: Clean Go cache 66 | run: | 67 | go clean -modcache 68 | go clean -cache 69 | rm -fr ${{ env.GOPATH }} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | /.idea/ 3 | 4 | /bin/ 5 | 6 | docker-compose.override.yml 7 | 8 | *.tmp.sql 9 | *.sqlite3 10 | 11 | *.cover 12 | coverage.txt 13 | -------------------------------------------------------------------------------- /.golangci-required.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # The most valuable linters; they are required to pass for PR to be merged. 3 | 4 | run: 5 | skip-dirs: 6 | - bogus 7 | 8 | linters-settings: 9 | govet: 10 | check-shadowing: true 11 | goimports: 12 | local-prefixes: gopkg.in/reform.v1 13 | 14 | linters: 15 | disable-all: true 16 | enable: 17 | - deadcode 18 | - depguard 19 | - goimports 20 | - gosec 21 | - govet 22 | - ineffassign 23 | - misspell 24 | - nolintlint 25 | - staticcheck 26 | - unused 27 | 28 | issues: 29 | exclude-use-default: false 30 | exclude: 31 | # gosec - we are making an ORM after all 32 | - "G201: SQL string formatting" 33 | - "G202: SQL string concatenation" 34 | 35 | # # golint - matches database/sql.Result.LastInsertId() 36 | # # > method LastInsertIdMethod should be LastInsertIDMethod 37 | # # > type `LastInsertIdMethod` should be `LastInsertIDMethod` 38 | # # > var `lastInsertIdMethod` should be `lastInsertIDMethod` 39 | # - "`?LastInsertIdMethod`? should be `?LastInsertIDMethod`?" 40 | 41 | # exclude-rules: 42 | # - path: internal/test/models/ 43 | # linters: 44 | # - golint 45 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Almost all linters; some of them are optional. 3 | 4 | run: 5 | skip-dirs: 6 | - bogus 7 | 8 | linters-settings: 9 | govet: 10 | check-shadowing: true 11 | goimports: 12 | local-prefixes: gopkg.in/reform.v1 13 | 14 | linters: 15 | enable-all: true 16 | disable: 17 | - goerr113 # reform v1 should not wrap errors to keep SemVer compatibility 18 | - golint # deprecated 19 | - gomnd # too annoying 20 | - interfacer # deprecated 21 | - lll # too annoying 22 | - maligned # deprecated 23 | - nlreturn # too annoying 24 | - scopelint # deprecated 25 | - wrapcheck # reform v1 should not wrap errors to keep SemVer compatibility 26 | - wsl # too annoying 27 | 28 | issues: 29 | exclude-use-default: false 30 | exclude: 31 | # gosec - we are making an ORM after all 32 | - "G201: SQL string formatting" 33 | - "G202: SQL string concatenation" 34 | 35 | # # golint - matches database/sql.Result.LastInsertId() 36 | # # > method LastInsertIdMethod should be LastInsertIDMethod 37 | # # > type `LastInsertIdMethod` should be `LastInsertIDMethod` 38 | # # > var `lastInsertIdMethod` should be `lastInsertIDMethod` 39 | # - "`?LastInsertIdMethod`? should be `?LastInsertIDMethod`?" 40 | 41 | exclude-rules: 42 | # - path: internal/test/models/ 43 | # linters: 44 | # - golint 45 | - path: _test\.go 46 | linters: 47 | - funlen # tests may be long 48 | - testpackage # senseless 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.6.0 (not released yet) 4 | 5 | * Go 1.17+ is now required. 6 | 7 | ## v1.5.1 (2021-08-27, https://github.com/go-reform/reform/milestones/v1.5.1) 8 | 9 | * `reform-db init` now correctly handles tables with composite primary keys ([#274](https://github.com/go-reform/reform/issues/274)). 10 | 11 | ## v1.5.0 (2020-12-08, https://github.com/go-reform/reform/milestones/v1.5.0) 12 | 13 | * Generated code now passes Go 1.15's `go vet`. See ([#245](https://github.com/go-reform/reform/issues/245)) 14 | and ([#269](https://github.com/go-reform/reform/issues/269)). 15 | * Removed hard-coded dependency on sqlite3 package. 16 | * Updated dependencies and testing infrastructure. 17 | 18 | ## v1.4.0 (2020-07-29, https://github.com/go-reform/reform/milestones/v1.4.0) 19 | 20 | * Go 1.13+ is now required. 21 | * Converted to Go module. [Non modules-aware tools like `dep`](https://github.com/golang/dep/issues/1962) 22 | are still supported until reform v2 (dependencies with Semantic Import Versioning paths are not used in v1). 23 | * Added [`context` support](https://pkg.go.dev/gopkg.in/reform.v1?tab=doc#hdr-Context). 24 | * Added [`Querier.Count`](https://godoc.org/gopkg.in/reform.v1#Querier.Count). 25 | Thanks to [Simon Kamenetskiy](https://github.com/skamenetskiy). 26 | * Added support for [github.com/jackc/pgx](https://github.com/jackc/pgx) v3 driver. 27 | * CI now uses GitHub Actions. 28 | 29 | ## v1.3.4 (2020-06-25, https://github.com/go-reform/reform/milestones/v1.3.4) 30 | 31 | * Make reform generator work with Go 1.15. 32 | * Replace syreclabs.com/go/faker with github.com/brianvoe/gofakeit. 33 | 34 | ## v1.3.3 (2018-12-11, https://github.com/go-reform/reform/milestones/v1.3.3) 35 | 36 | * Fix tests for Go 1.12. 37 | 38 | ## v1.3.2 (2018-07-23, https://github.com/go-reform/reform/milestones/v1.3.2) 39 | 40 | * Go 1.8+ is now required due to changes in github.com/lib/pq driver. 41 | * Fixes in tests for MySQL 8, Go 1.10+ and latest versions of drivers. 42 | 43 | ## v1.3.1 (2017-12-07, https://github.com/go-reform/reform/milestones/v1.3.1) 44 | 45 | * No user-visible changes. 46 | * Major changes in CI and development environment. 47 | 48 | ## v1.3.0 (2017-12-01, https://github.com/go-reform/reform/milestones/v1.3.0) 49 | 50 | * Go 1.7+ is now required. 51 | * Added `reform-db` command. 52 | * `init` subcommand may be used to generate Go model files for existing database schema. 53 | * `query` and `exec` subcommands may be used for accessing a database. 54 | * Fields with `reform` tag with value `"-"` are ignored now (just like with value `""` and without tag at all). 55 | * Added [`ErrTxDone`](https://godoc.org/gopkg.in/reform.v1#pkg-variables). 56 | * Added [`DB.DBInterface`](https://godoc.org/gopkg.in/reform.v1#DB.DBInterface). 57 | * Added [`Querier.UpdateView`](https://godoc.org/gopkg.in/reform.v1#Querier.UpdateView). 58 | * `reform` command with `-gofmt=false` flag still formats generated sources with go/format package, without invoking `gofmt`. 59 | Thanks to [João Pereira](https://github.com/joaodrp). 60 | * Added support for `sqlserver` variant of [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) driver. 61 | * Added support for Microsoft SQL Server for Linux. 62 | * We now have a logo! Huge thanks to Natalya Glebova for making it. 63 | 64 | ## v1.2.1 (2016-09-14, https://github.com/go-reform/reform/milestones/v1.2.1) 65 | 66 | * `reform` command now correctly handles non-exported types. 67 | * [`Querier.Insert`](https://godoc.org/gopkg.in/reform.v1#Querier.Insert) now correctly INSERTs records with set 68 | non-integer primary keys, even if dialect uses LastInsertId (MySQL, SQLite3). 69 | 70 | ## v1.2.0 (2016-08-10, https://github.com/go-reform/reform/milestones/v1.2.0) 71 | 72 | * Added support for Microsoft SQL Server. Huge thanks to [Aleksey Martynov](https://github.com/AlekseyMartynov). 73 | * Added [`Querier.InsertColumns`](https://godoc.org/gopkg.in/reform.v1#Querier.InsertColumns). 74 | * [`Querier.Insert`](https://godoc.org/gopkg.in/reform.v1#Querier.Insert) now correctly handles records with only primary key column. 75 | 76 | ## v1.1.2 (2016-07-20, https://github.com/go-reform/reform/milestones/v1.1.2) 77 | 78 | * `reform` command now correctly ignores type information when it's not used. 79 | This allows one to have fields of any custom types. The only exception is primary key fields, 80 | which are restricted to basic types (numbers and strings). 81 | * Package [`gopkg.in/reform.v1/parse`](https://godoc.org/gopkg.in/reform.v1/parse) is explicitly documented as internal. 82 | (It's wasn't really possible to use it.) 83 | 84 | ## v1.1.1 (2016-07-05, https://github.com/go-reform/reform/milestones/v1.1.1) 85 | 86 | * [`Querier.UpdateColumns`](https://godoc.org/gopkg.in/reform.v1#Querier.UpdateColumns) no longer allows to update 87 | primary key column. This behavior was allowed, but did not make any sense. 88 | * `reform` command now correctly handles pointers to custom types and slices. 89 | 90 | ## v1.1.0 (2016-07-01, https://github.com/go-reform/reform/milestones/v1.1.0) 91 | 92 | * Added [`Querier.InsertMulti`](https://godoc.org/gopkg.in/reform.v1#Querier.InsertMulti). 93 | * Added [`DBInterface`](https://godoc.org/gopkg.in/reform.v1#DBInterface), 94 | [`TXInterface`](https://godoc.org/gopkg.in/reform.v1#TXInterface), 95 | [`NewDBFromInterface`](https://godoc.org/gopkg.in/reform.v1#NewDBFromInterface), 96 | [`NewTXFromInterface`](https://godoc.org/gopkg.in/reform.v1#NewTXFromInterface). 97 | 98 | ## v1.0.0 (2016-06-22) 99 | 100 | * Moved to https://github.com/go-reform/reform repository. 101 | * Changed canonical import path. 102 | * Added versioning policy. 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alexey.palazhchenko@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Golden rule 4 | 5 | Speak up _before_ writing code. Comment on existing issue or create a new one. Discuss what 6 | you want to implement _before_ implementing it. 7 | 8 | 9 | ## Getting code 10 | 11 | Fork repository on GitHub and clone the source code: 12 | 13 | ``` 14 | git clone git@github.com:/reform.git 15 | cd reform 16 | make init 17 | ``` 18 | 19 | Thanks to Go modules, that will work in any directory. Make sure they are not disabled in your environment. 20 | 21 | Please read the "Versioning and branching policy" section in README, 22 | and send pull requests to the right branch. 23 | 24 | 25 | ## Makefile targets 26 | 27 | * Run `make` without arguments to see all Makefile targets. 28 | * Run `make env-up` or `make env-up-detach` to start databases with Docker Compose. 29 | * Run `make init` to install development tools. 30 | * Run `make test` to run all tests. 31 | * Run `make lint` to run linters. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Alexey Palazhchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reform 2 | 3 | [![Release](https://img.shields.io/github/release/go-reform/reform.svg)](https://github.com/go-reform/reform/releases/latest) 4 | [![PkgGoDev](https://pkg.go.dev/badge/gopkg.in/reform.v1)](https://pkg.go.dev/gopkg.in/reform.v1) 5 | [![CI](https://github.com/go-reform/reform/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/go-reform/reform/actions) 6 | [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/srwa0cuwf91qpjge/branch/main?svg=true)](https://ci.appveyor.com/project/AlekSi/reform/branch/main) 7 | [![Coverage Report](https://codecov.io/gh/go-reform/reform/branch/main/graph/badge.svg)](https://codecov.io/gh/go-reform/reform) 8 | [![Go Report Card](https://goreportcard.com/badge/gopkg.in/reform.v1)](https://goreportcard.com/report/gopkg.in/reform.v1) 9 | 10 | Reform gopher logo 11 | 12 | A better ORM for Go and `database/sql`. 13 | 14 | It uses non-empty interfaces, code generation (`go generate`), and initialization-time reflection 15 | as opposed to `interface{}`, type system sidestepping, and runtime reflection. It will be kept simple. 16 | 17 | Supported SQL dialects: 18 | 19 | | RDBMS | Library and drivers | Status 20 | | ----- | ------------------- | ------ 21 | | PostgreSQL | [github.com/lib/pq](https://github.com/lib/pq) (`postgres`) | Stable. Tested with all [supported](https://www.postgresql.org/support/versioning/) versions. 22 | | | [github.com/jackc/pgx/stdlib](https://github.com/jackc/pgx) (`pgx` v3) | Stable. Tested with all [supported](https://www.postgresql.org/support/versioning/) versions. 23 | | MySQL | [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) (`mysql`) | Stable. Tested with all [supported](https://www.mysql.com/support/supportedplatforms/database.html) versions. 24 | | SQLite3 | [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (`sqlite3`) | Stable. 25 | | Microsoft SQL Server | [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) (`sqlserver`, `mssql`) | Stable.
Tested on Windows with: SQL2008R2SP2, SQL2012SP1, SQL2014, SQL2016.
On Linux with: `mcr.microsoft.com/mssql/server:2017-latest` and `mcr.microsoft.com/mssql/server:2019-latest` [Docker images](https://hub.docker.com/_/microsoft-mssql-server). 26 | 27 | Notes: 28 | * [`clientFoundRows=true` flag](https://github.com/go-sql-driver/mysql#clientfoundrows) is required for `mysql` driver. 29 | * `mssql` driver is [deprecated](https://github.com/denisenkom/go-mssqldb#deprecated) (but not `sqlserver` driver). 30 | 31 | 32 | ## Quickstart 33 | 34 | 1. Make sure you are using Go 1.17+, and Go modules support is enabled. 35 | Install or update `reform` package, `reform` and `reform-db` commands with: 36 | ``` 37 | go get -v gopkg.in/reform.v1/... 38 | ``` 39 | 40 | If you are not using Go modules yet, you can use dep to vendor desired version of reform, 41 | and then install commands with: 42 | ``` 43 | go install -v ./vendor/gopkg.in/reform.v1/... 44 | ``` 45 | 46 | You can also install the latest stable version of reform without using Go modules thanks to 47 | [gopkg.in redirection](https://gopkg.in/reform.v1), but please note that this will not use the stable 48 | versions of the database drivers: 49 | ``` 50 | env GO111MODULE=off go get -u -v gopkg.in/reform.v1/... 51 | ``` 52 | 53 | Canonical import path is `gopkg.in/reform.v1`; using `github.com/go-reform/reform` will not work. 54 | 55 | See note about versioning and branches below. 56 | 57 | 2. Use `reform-db` command to generate models for your existing database schema. For example: 58 | ``` 59 | reform-db -db-driver=sqlite3 -db-source=example.sqlite3 init 60 | ``` 61 | 62 | 3. Update generated models or write your own – `struct` representing a table or view row. For example, 63 | store this in file `person.go`: 64 | ```go 65 | //go:generate reform 66 | 67 | //reform:people 68 | type Person struct { 69 | ID int32 `reform:"id,pk"` 70 | Name string `reform:"name"` 71 | Email *string `reform:"email"` 72 | CreatedAt time.Time `reform:"created_at"` 73 | UpdatedAt *time.Time `reform:"updated_at"` 74 | } 75 | ``` 76 | 77 | Magic comment `//reform:people` links this model to `people` table or view in SQL database. 78 | The first value in field's `reform` tag is a column name. `pk` marks primary key. 79 | Use value `-` or omit tag completely to skip a field. 80 | Use pointers (recommended) or `sql.NullXXX` types for nullable fields. 81 | 82 | 4. Run `reform [package or directory]` or `go generate [package or file]`. This will create `person_reform.go` 83 | in the same package with type `PersonTable` and methods on `Person`. 84 | 85 | 5. See [documentation](https://godoc.org/gopkg.in/reform.v1) how to use it. Simple example: 86 | 87 | ```go 88 | // Get *sql.DB as usual. PostgreSQL example: 89 | sqlDB, err := sql.Open("postgres", "postgres://127.0.0.1:5432/database") 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | defer sqlDB.Close() 94 | 95 | // Use new *log.Logger for logging. 96 | logger := log.New(os.Stderr, "SQL: ", log.Flags()) 97 | 98 | // Create *reform.DB instance with simple logger. 99 | // Any Printf-like function (fmt.Printf, log.Printf, testing.T.Logf, etc) can be used with NewPrintfLogger. 100 | // Change dialect for other databases. 101 | db := reform.NewDB(sqlDB, postgresql.Dialect, reform.NewPrintfLogger(logger.Printf)) 102 | 103 | // Save record (performs INSERT or UPDATE). 104 | person := &Person{ 105 | Name: "Alexey Palazhchenko", 106 | Email: pointer.ToString("alexey.palazhchenko@gmail.com"), 107 | } 108 | if err := db.Save(person); err != nil { 109 | log.Fatal(err) 110 | } 111 | 112 | // ID is filled by Save. 113 | person2, err := db.FindByPrimaryKeyFrom(PersonTable, person.ID) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | fmt.Println(person2.(*Person).Name) 118 | 119 | // Delete record. 120 | if err = db.Delete(person); err != nil { 121 | log.Fatal(err) 122 | } 123 | 124 | // Find records by IDs. 125 | persons, err := db.FindAllFrom(PersonTable, "id", 1, 2) 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | for _, p := range persons { 130 | fmt.Println(p) 131 | } 132 | ``` 133 | 134 | 135 | ## Background 136 | 137 | reform was born during summer 2014 out of frustrations with existing Go ORMs. All of them have a method 138 | `Save(record interface{})` which can be used like this: 139 | 140 | ```go 141 | orm.Save(User{Name: "gopher"}) 142 | orm.Save(&User{Name: "gopher"}) 143 | orm.Save(nil) 144 | orm.Save("Batman!!") 145 | ``` 146 | 147 | Now you can say that last invocation is obviously invalid, and that it's not hard to make an ORM to accept both 148 | first and second versions. But there are two problems: 149 | 150 | 1. Compiler can't check it. Method's signature in `godoc` will not tell us how to use it. 151 | We are essentially working against those tools by sidestepping type system. 152 | 2. First version is still invalid, since one would expect `Save()` method to set record's primary key after `INSERT`, 153 | but this change will be lost due to passing by value. 154 | 155 | First proprietary version of reform was used in production even before `go generate` announcement. 156 | This free and open-source version is the fourth milestone on the road to better and idiomatic API. 157 | 158 | 159 | ## Versioning and branching policy 160 | 161 | We are following [Semantic Versioning](http://semver.org/spec/v2.0.0.html), 162 | using [gopkg.in](https://gopkg.in) and filling a [changelog](CHANGELOG.md). 163 | All v1 releases are SemVer-compatible; breaking changes will not be applied. 164 | 165 | We use tags `v1.M.m` for releases, branch `main` (default on GitHub) for the next minor release development, 166 | and `release/1.M` branches for patch release development. (It was more complicated before 1.4.0 release.) 167 | 168 | Major version 2 is currently not planned. 169 | 170 | 171 | ## Additional packages 172 | 173 | * [github.com/AlekSi/pointer](https://github.com/AlekSi/pointer) is very useful for working with reform structs with pointers. 174 | 175 | 176 | ## Caveats and limitations 177 | 178 | * There should be zero `pk` fields for Struct and exactly one `pk` field for Record. 179 | Composite primary keys are not supported ([#114](https://github.com/go-reform/reform/issues/114)). 180 | * `pk` field can't be a pointer (`== nil` [doesn't work](https://golang.org/doc/faq#nil_error)). 181 | * Database row can't have a Go's zero value (0, empty string, etc.) in primary key column. 182 | 183 | 184 | ## License 185 | 186 | Code is covered by standard MIT-style license. Copyright (c) 2016-2020 Alexey Palazhchenko. 187 | See [LICENSE](LICENSE) for details. Note that generated code is covered by the terms of your choice. 188 | 189 | The reform gopher was drawn by Natalya Glebova. Please use it only as reform logo. 190 | It is based on the original design by Renée French, released under [Creative Commons Attribution 3.0 USA license](https://creativecommons.org/licenses/by/3.0/). 191 | 192 | 193 | ## Contributing 194 | 195 | See [Contributing Guidelines](.github/CONTRIBUTING.md). 196 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '{build}' 3 | 4 | clone_folder: c:\gopath\src\gopkg.in\reform.v1 5 | 6 | environment: 7 | # no `-mod=readonly` to test PRs made by @dependabot; 8 | # `make ci-check-changes` step below still checks what we need 9 | GOPATH: c:\gopath 10 | GO111MODULE: "on" 11 | GOPROXY: https://proxy.golang.org 12 | GORACE: halt_on_error=1 13 | 14 | matrix: 15 | - REFORM_MAKE_TARGET: sqlite3 16 | 17 | - REFORM_SQL_INSTANCE: SQL2008R2SP2 18 | REFORM_MAKE_TARGET: win-mssql 19 | - REFORM_SQL_INSTANCE: SQL2008R2SP2 20 | REFORM_MAKE_TARGET: win-sqlserver 21 | 22 | - REFORM_SQL_INSTANCE: SQL2012SP1 23 | REFORM_MAKE_TARGET: win-mssql 24 | - REFORM_SQL_INSTANCE: SQL2012SP1 25 | REFORM_MAKE_TARGET: win-sqlserver 26 | 27 | - REFORM_SQL_INSTANCE: SQL2014 28 | REFORM_MAKE_TARGET: win-mssql 29 | - REFORM_SQL_INSTANCE: SQL2014 30 | REFORM_MAKE_TARGET: win-sqlserver 31 | 32 | - REFORM_SQL_INSTANCE: SQL2016 33 | REFORM_MAKE_TARGET: win-mssql 34 | - REFORM_SQL_INSTANCE: SQL2016 35 | REFORM_MAKE_TARGET: win-sqlserver 36 | 37 | install: 38 | - if defined REFORM_SQL_INSTANCE powershell -file .github\appveyor-prepare-mssql.ps1 39 | - set PATH=C:\msys64\usr\bin;C:\msys64\mingw64\bin;%GOPATH%\bin;%PATH% 40 | - pacman -Rsc --noconfirm gcc 41 | 42 | - go env 43 | - go version 44 | 45 | build_script: 46 | - go mod download 47 | 48 | # check that it is still possible to install reform without modules 49 | - env GO111MODULE=off go get -v ./... 50 | - reform -version 51 | - reform-db -version 52 | 53 | - make init 54 | 55 | test_script: 56 | - make test-unit 57 | - make %REFORM_MAKE_TARGET% 58 | - make merge-cover 59 | 60 | # TODO test again with updated deps 61 | 62 | - make ci-check-changes 63 | 64 | on_success: 65 | - curl -s -o codecov https://codecov.io/bash 66 | - bash codecov -f coverage.txt -X fix -e REFORM_MAKE_TARGET,REFORM_SQL_INSTANCE 67 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package reform 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "reflect" 8 | ) 9 | 10 | var ( 11 | // ErrNoRows is returned from various methods when query produced no rows. 12 | ErrNoRows = sql.ErrNoRows 13 | 14 | // ErrTxDone is returned from Commit() and Rollback() TX methods when transaction is already 15 | // committed or rolled back. 16 | ErrTxDone = sql.ErrTxDone 17 | 18 | // ErrNoPK is returned from various methods when primary key is required and not set. 19 | ErrNoPK = errors.New("reform: no primary key") 20 | ) 21 | 22 | // View represents SQL database view or table. 23 | type View interface { 24 | // Schema returns a schema name in SQL database. 25 | Schema() string 26 | 27 | // Name returns a view or table name in SQL database. 28 | Name() string 29 | 30 | // Columns returns a new slice of column names for that view or table in SQL database. 31 | Columns() []string 32 | 33 | // NewStruct makes a new struct for that view or table. 34 | NewStruct() Struct 35 | } 36 | 37 | // Table represents SQL database table with single-column primary key. 38 | // It extends View. 39 | type Table interface { 40 | View 41 | 42 | // NewRecord makes a new record for that table. 43 | NewRecord() Record 44 | 45 | // PKColumnIndex returns an index of primary key column for that table in SQL database. 46 | PKColumnIndex() uint 47 | } 48 | 49 | // Struct represents a row in SQL database view or table. 50 | type Struct interface { 51 | // String returns a string representation of this struct or record. 52 | String() string 53 | 54 | // Values returns a slice of struct or record field values. 55 | // Returned interface{} values are never untyped nils. 56 | Values() []interface{} 57 | 58 | // Pointers returns a slice of pointers to struct or record fields. 59 | // Returned interface{} values are never untyped nils. 60 | Pointers() []interface{} 61 | 62 | // View returns View object for that struct. 63 | View() View 64 | } 65 | 66 | // Record represents a row in SQL database table with single-column primary key. 67 | type Record interface { 68 | Struct 69 | 70 | // Table returns Table object for that record. 71 | Table() Table 72 | 73 | // PKValue returns a value of primary key for that record. 74 | // Returned interface{} value is never untyped nil. 75 | PKValue() interface{} 76 | 77 | // PKPointer returns a pointer to primary key field for that record. 78 | // Returned interface{} value is never untyped nil. 79 | PKPointer() interface{} 80 | 81 | // HasPK returns true if record has non-zero primary key set, false otherwise. 82 | HasPK() bool 83 | 84 | // SetPK sets record primary key, if possible. 85 | // 86 | // Deprecated: prefer direct field assignment where possible. 87 | SetPK(pk interface{}) 88 | } 89 | 90 | // BeforeInserter is an optional interface for Record which is used by Querier.Insert. 91 | // It can be used to set record's timestamp fields, convert timezones, change data precision, etc. 92 | // Returning error aborts operation. 93 | type BeforeInserter interface { 94 | BeforeInsert() error 95 | } 96 | 97 | // BeforeUpdater is an optional interface for Record which is used by Querier.Update and Querier.UpdateColumns. 98 | // It can be used to set record's timestamp fields, convert timezones, change data precision, etc. 99 | // Returning error aborts operation. 100 | type BeforeUpdater interface { 101 | BeforeUpdate() error 102 | } 103 | 104 | // AfterFinder is an optional interface for Record which is used by Querier's finders and selectors. 105 | // It can be used to convert timezones, change data precision, etc. 106 | // Returning error aborts operation. 107 | type AfterFinder interface { 108 | AfterFind() error 109 | } 110 | 111 | // DBTX is an interface for database connection or transaction. 112 | // It's implemented by *sql.DB, *sql.Tx, *DB, *TX, and *Querier. 113 | type DBTX interface { 114 | // Exec executes a query without returning any rows. 115 | // The args are for any placeholder parameters in the query. 116 | Exec(query string, args ...interface{}) (sql.Result, error) 117 | 118 | // Query executes a query that returns rows, typically a SELECT. 119 | // The args are for any placeholder parameters in the query. 120 | Query(query string, args ...interface{}) (*sql.Rows, error) 121 | 122 | // QueryRow executes a query that is expected to return at most one row. 123 | // QueryRow always returns a non-nil value. Errors are deferred until Row's Scan method is called. 124 | // If the query selects no rows, the *Row's Scan will return ErrNoRows. 125 | // Otherwise, the *Row's Scan scans the first selected row and discards the rest. 126 | QueryRow(query string, args ...interface{}) *sql.Row 127 | } 128 | 129 | // DBTXContext is an interface for database connection or transaction with context support. 130 | // It's implemented by *sql.DB, *sql.Tx, *sql.Conn, *DB, *TX, and *Querier. 131 | type DBTXContext interface { 132 | // ExecContext executes a query without returning any rows. 133 | // The args are for any placeholder parameters in the query. 134 | ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) 135 | 136 | // QueryContext executes a query that returns rows, typically a SELECT. 137 | // The args are for any placeholder parameters in the query. 138 | QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) 139 | 140 | // QueryRowContext executes a query that is expected to return at most one row. 141 | // QueryRowContext always returns a non-nil value. Errors are deferred until Row's Scan method is called. 142 | // If the query selects no rows, the *Row's Scan will return ErrNoRows. 143 | // Otherwise, the *Row's Scan scans the first selected row and discards the rest. 144 | QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row 145 | } 146 | 147 | // LastInsertIdMethod is a method of receiving primary key of last inserted row. 148 | type LastInsertIdMethod int 149 | 150 | const ( 151 | // LastInsertId is method using sql.Result.LastInsertId(). 152 | LastInsertId LastInsertIdMethod = iota 153 | 154 | // Returning is method using "RETURNING id" SQL syntax. 155 | Returning 156 | 157 | // OutputInserted is method using "OUTPUT INSERTED.id" SQL syntax. 158 | OutputInserted 159 | ) 160 | 161 | // SelectLimitMethod is a method of limiting the number of rows in a query result. 162 | type SelectLimitMethod int 163 | 164 | const ( 165 | // Limit is a method using "LIMIT N" SQL syntax. 166 | Limit SelectLimitMethod = iota 167 | 168 | // SelectTop is a method using "SELECT TOP N" SQL syntax. 169 | SelectTop 170 | ) 171 | 172 | // DefaultValuesMethod is a method of inserting of row with all default values. 173 | type DefaultValuesMethod int 174 | 175 | const ( 176 | // DefaultValues is a method using "DEFAULT VALUES" 177 | DefaultValues DefaultValuesMethod = iota 178 | 179 | // EmptyLists is a method using "() VALUES ()" 180 | EmptyLists 181 | ) 182 | 183 | // Dialect represents differences in various SQL dialects. 184 | type Dialect interface { 185 | // String returns dialect name. 186 | String() string 187 | 188 | // Placeholder returns representation of placeholder parameter for given index, 189 | // typically "?" or "$1". 190 | Placeholder(index int) string 191 | 192 | // Placeholders returns representation of placeholder parameters for given start index and count, 193 | // typically []{"?", "?"} or []{"$1", "$2"}. 194 | Placeholders(start, count int) []string 195 | 196 | // QuoteIdentifier returns quoted database identifier, 197 | // typically "identifier" or `identifier`. 198 | QuoteIdentifier(identifier string) string 199 | 200 | // LastInsertIdMethod returns a method of receiving primary key of last inserted row. 201 | LastInsertIdMethod() LastInsertIdMethod 202 | 203 | // SelectLimitMethod returns a method of limiting the number of rows in a query result. 204 | SelectLimitMethod() SelectLimitMethod 205 | 206 | // DefaultValuesMethod returns a method of inserting of row with all default values. 207 | DefaultValuesMethod() DefaultValuesMethod 208 | } 209 | 210 | // SetPK sets record's primary key, if possible. 211 | // 212 | // Deprecated: prefer direct field assignment where possible. 213 | func SetPK(r Record, pk interface{}) { 214 | fV := reflect.ValueOf(r.Pointers()[r.Table().PKColumnIndex()]).Elem() 215 | pkV := reflect.ValueOf(pk) 216 | if t := fV.Type(); t.ConvertibleTo(pkV.Type()) { 217 | fV.Set(pkV.Convert(t)) 218 | } 219 | } 220 | 221 | // check interfaces 222 | var ( 223 | _ DBTX = (*sql.DB)(nil) 224 | _ DBTX = (*sql.Tx)(nil) 225 | _ DBTXContext = (*sql.DB)(nil) 226 | _ DBTXContext = (*sql.Tx)(nil) 227 | _ DBTXContext = (*sql.Conn)(nil) 228 | ) 229 | -------------------------------------------------------------------------------- /base_test.go: -------------------------------------------------------------------------------- 1 | package reform_test 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | _ "github.com/denisenkom/go-mssqldb" 13 | _ "github.com/go-sql-driver/mysql" 14 | _ "github.com/jackc/pgx/stdlib" 15 | _ "github.com/lib/pq" 16 | _ "github.com/mattn/go-sqlite3" 17 | 18 | "github.com/stretchr/testify/assert" 19 | "github.com/stretchr/testify/require" 20 | "github.com/stretchr/testify/suite" 21 | 22 | "gopkg.in/reform.v1" 23 | "gopkg.in/reform.v1/dialects/mssql" //nolint:staticcheck 24 | "gopkg.in/reform.v1/dialects/postgresql" 25 | "gopkg.in/reform.v1/dialects/sqlite3" 26 | "gopkg.in/reform.v1/dialects/sqlserver" 27 | "gopkg.in/reform.v1/internal/test" 28 | . "gopkg.in/reform.v1/internal/test/models" 29 | ) 30 | 31 | // DB is a global connection pool shared by tests and examples. 32 | // 33 | // Deprecated: do not add new tests using it as using a global pool makes tests more brittle. 34 | var DB *reform.DB 35 | 36 | func TestMain(m *testing.M) { 37 | flag.Parse() 38 | 39 | if testing.Short() { 40 | log.Print("Not setting DB in short mode") 41 | } else { 42 | DB = test.ConnectToTestDB() 43 | } 44 | 45 | os.Exit(m.Run()) 46 | } 47 | 48 | // checkForeignKeys checks that foreign keys are still enforced for sqlite3. 49 | func checkForeignKeys(t testing.TB, q *reform.Querier) { 50 | t.Helper() 51 | 52 | if q.Dialect != sqlite3.Dialect { 53 | return 54 | } 55 | 56 | var enabled bool 57 | err := q.QueryRow("PRAGMA foreign_keys").Scan(&enabled) 58 | require.NoError(t, err) 59 | require.True(t, enabled) 60 | } 61 | 62 | // withIdentityInsert executes an action with MS SQL IDENTITY_INSERT enabled for a table 63 | func withIdentityInsert(t testing.TB, q *reform.Querier, table string, action func()) { 64 | t.Helper() 65 | 66 | if q.Dialect != mssql.Dialect && q.Dialect != sqlserver.Dialect { //nolint:staticcheck 67 | action() 68 | return 69 | } 70 | 71 | query := fmt.Sprintf("SET IDENTITY_INSERT %s %%s", q.QuoteIdentifier(table)) 72 | 73 | _, err := q.Exec(fmt.Sprintf(query, "ON")) 74 | require.NoError(t, err) 75 | 76 | action() 77 | 78 | _, err = q.Exec(fmt.Sprintf(query, "OFF")) 79 | require.NoError(t, err) 80 | } 81 | 82 | func insertPersonWithID(t testing.TB, q *reform.Querier, str reform.Struct) error { 83 | t.Helper() 84 | 85 | var err error 86 | withIdentityInsert(t, q, "people", func() { err = q.Insert(str) }) 87 | return err 88 | } 89 | 90 | // setupDB creates new database connection pool. 91 | func setupDB(t testing.TB) *reform.DB { 92 | t.Helper() 93 | 94 | if testing.Short() { 95 | t.Skip("skipping in short mode") 96 | } 97 | 98 | db := test.ConnectToTestDB() 99 | pl := reform.NewPrintfLogger(t.Logf) 100 | pl.LogTypes = true 101 | db.Logger = pl 102 | db.Querier = db.WithTag("test:%s", t.Name()) 103 | 104 | checkForeignKeys(t, db.Querier) 105 | return db 106 | } 107 | 108 | // setupTX creates new database connection pool and starts a new transaction. 109 | func setupTX(t testing.TB) (*reform.DB, *reform.TX) { 110 | t.Helper() 111 | 112 | db := setupDB(t) 113 | 114 | tx, err := db.Begin() 115 | require.NoError(t, err) 116 | return db, tx 117 | } 118 | 119 | // teardown closes database connection pool. 120 | func teardown(t testing.TB, db *reform.DB) { 121 | t.Helper() 122 | 123 | err := db.DBInterface().(*sql.DB).Close() 124 | require.NoError(t, err) 125 | } 126 | 127 | // Deprecated: do not add new test to this suite, use Go subtests instead. 128 | // TODO Remove. 129 | type ReformSuite struct { 130 | suite.Suite 131 | tx *reform.TX 132 | q *reform.Querier 133 | } 134 | 135 | func TestReformSuite(t *testing.T) { 136 | suite.Run(t, new(ReformSuite)) 137 | } 138 | 139 | // SetupTest configures global connection pool and starts a new transaction. 140 | func (s *ReformSuite) SetupTest() { 141 | if testing.Short() { 142 | s.T().Skip("skipping in short mode") 143 | } 144 | 145 | pl := reform.NewPrintfLogger(s.T().Logf) 146 | pl.LogTypes = true 147 | DB.Logger = pl 148 | DB.Querier = DB.WithTag("test:%s", s.T().Name()) 149 | 150 | checkForeignKeys(s.T(), DB.Querier) 151 | 152 | tx, err := DB.Begin() 153 | s.Require().NoError(err) 154 | s.tx = tx 155 | s.q = tx.Querier 156 | } 157 | 158 | // TearDownTest rollbacks transaction created by SetupTest. 159 | func (s *ReformSuite) TearDownTest() { 160 | if s.tx == nil { 161 | panic(s.T().Name() + ": tx is nil") 162 | } 163 | if s.q == nil { 164 | panic(s.T().Name() + ": q is nil") 165 | } 166 | 167 | checkForeignKeys(s.T(), s.q) 168 | s.Require().NoError(s.tx.Rollback()) 169 | 170 | DB.Logger = nil 171 | DB.Querier = DB.WithTag("") 172 | } 173 | 174 | func (s *ReformSuite) RestartTransaction() { 175 | s.TearDownTest() 176 | s.SetupTest() 177 | } 178 | 179 | func (s *ReformSuite) TestStringer() { 180 | person, err := s.q.FindByPrimaryKeyFrom(PersonTable, 1) 181 | s.NoError(err) 182 | expected := "ID: 1 (int32), GroupID: 65534 (*int32), Name: `Denis Mills` (string), Email: (*string), CreatedAt: 2009-11-10 23:00:00 +0000 UTC (time.Time), UpdatedAt: (*time.Time)" 183 | s.Equal(expected, person.String()) 184 | 185 | project, err := s.q.FindByPrimaryKeyFrom(ProjectTable, "baron") 186 | s.NoError(err) 187 | expected = "Name: `Vicious Baron` (string), ID: `baron` (string), Start: 2014-06-01 00:00:00 +0000 UTC (time.Time), End: 2016-02-21 00:00:00 +0000 UTC (*time.Time)" 188 | s.Equal(expected, project.String()) 189 | } 190 | 191 | func (s *ReformSuite) TestNeverNil() { 192 | project := new(Project) 193 | 194 | for i, v := range project.Values() { 195 | if v == nil { 196 | s.Fail("Value is nil", "%s %#v", ProjectTable.Columns()[i], v) 197 | } 198 | } 199 | 200 | for i, v := range project.Pointers() { 201 | if v == nil { 202 | s.Fail("Pointer is nil", "%s %#v", ProjectTable.Columns()[i], v) 203 | } 204 | } 205 | 206 | v := project.PKValue() 207 | if v == nil { 208 | s.Fail("PKValue is nil") 209 | } 210 | 211 | v = project.PKPointer() 212 | if v == nil { 213 | s.Fail("PKPointer is nil") 214 | } 215 | } 216 | 217 | func (s *ReformSuite) TestHasPK() { 218 | person := new(Person) 219 | project := new(Project) 220 | s.False(person.HasPK()) 221 | s.False(project.HasPK()) 222 | 223 | person.ID = 1 224 | project.ID = "id" 225 | s.True(person.HasPK()) 226 | s.True(project.HasPK()) 227 | } 228 | 229 | func (s *ReformSuite) TestPlaceholders() { 230 | if s.q.Dialect != postgresql.Dialect { 231 | s.T().Skip("PostgreSQL-specific test") 232 | } 233 | 234 | s.Equal([]string{"$1", "$2", "$3", "$4", "$5"}, s.q.Placeholders(1, 5)) 235 | s.Equal([]string{"$2", "$3", "$4", "$5", "$6"}, s.q.Placeholders(2, 5)) 236 | } 237 | 238 | func (s *ReformSuite) TestTimezones() { 239 | t1 := time.Now().Round(0) 240 | t2 := t1.UTC() 241 | vlat, err := time.LoadLocation("Asia/Vladivostok") 242 | s.NoError(err) 243 | tVLAT := t1.In(vlat) 244 | hst, err := time.LoadLocation("US/Hawaii") 245 | s.NoError(err) 246 | tHST := t1.In(hst) 247 | 248 | { 249 | q := fmt.Sprintf(`INSERT INTO people (id, name, created_at) VALUES `+ 250 | `(11, '11', %s), (12, '12', %s), (13, '13', %s), (14, '14', %s)`, 251 | s.q.Placeholder(1), s.q.Placeholder(2), s.q.Placeholder(3), s.q.Placeholder(4)) 252 | 253 | withIdentityInsert(s.T(), s.q, "people", func() { 254 | _, err := s.q.Exec(q, t1, t2, tVLAT, tHST) 255 | s.NoError(err) 256 | }) 257 | 258 | q = `SELECT created_at, created_at FROM people WHERE id IN (11, 12, 13, 14) ORDER BY id` 259 | rows, err := s.q.Query(q) 260 | s.NoError(err) 261 | 262 | for _, t := range []time.Time{t1, t2, tVLAT, tHST} { 263 | var createdS string 264 | var createdT time.Time 265 | rows.Next() 266 | err = rows.Scan(&createdS, &createdT) 267 | s.NoError(err) 268 | log.Printf("%s read from database as %q and %s", t, createdS, createdT) 269 | } 270 | 271 | s.NoError(rows.Err()) 272 | s.NoError(rows.Close()) 273 | } 274 | 275 | { 276 | q := fmt.Sprintf(`INSERT INTO projects (id, name, start) VALUES `+ 277 | `('11', '11', %s), ('12', '12', %s), ('13', '13', %s), ('14', '14', %s)`, 278 | s.q.Placeholder(1), s.q.Placeholder(2), s.q.Placeholder(3), s.q.Placeholder(4)) 279 | _, err := s.q.Exec(q, t1, t2, tVLAT, tHST) 280 | s.NoError(err) 281 | 282 | q = `SELECT start, start FROM projects WHERE id IN ('11', '12', '13', '14') ORDER BY id` 283 | rows, err := s.q.Query(q) 284 | s.NoError(err) 285 | 286 | for _, t := range []time.Time{t1, t2, tVLAT, tHST} { 287 | var startS string 288 | var startT time.Time 289 | rows.Next() 290 | err = rows.Scan(&startS, &startT) 291 | s.NoError(err) 292 | log.Printf("%s read from database as %q and %s", t, startS, startT) 293 | } 294 | 295 | s.NoError(rows.Err()) 296 | s.NoError(rows.Close()) 297 | } 298 | } 299 | 300 | // database/sql.(*Rows).Columns() is not currently used, but may be useful in the future. 301 | // Test is in place to track drivers supporting it. 302 | func (s *ReformSuite) TestColumns() { 303 | rows, err := s.q.SelectRows(PersonTable, "WHERE name = "+s.q.Placeholder(1)+" ORDER BY id", "Elfrieda Abbott") 304 | s.NoError(err) 305 | s.Require().NotNil(rows) 306 | s.NoError(rows.Err()) 307 | 308 | columns, err := rows.Columns() 309 | s.NoError(err) 310 | s.Equal(PersonTable.Columns(), columns) 311 | s.NoError(rows.Close()) 312 | } 313 | 314 | //nolint:staticcheck 315 | func TestSetPK(t *testing.T) { 316 | t.Parallel() 317 | 318 | var person Person 319 | person.SetPK(int32(1)) 320 | assert.EqualValues(t, 1, person.ID) 321 | person.SetPK(int64(2)) 322 | assert.EqualValues(t, 2, person.ID) 323 | person.SetPK(Integer(3)) 324 | assert.EqualValues(t, 3, person.ID) 325 | 326 | var project Project 327 | project.SetPK("baron") 328 | assert.EqualValues(t, "baron", project.ID) 329 | project.SetPK(1) 330 | assert.EqualValues(t, "baron", project.ID) 331 | 332 | var extra Extra 333 | extra.SetPK(int32(1)) 334 | assert.EqualValues(t, 1, extra.ID) 335 | extra.SetPK(int64(2)) 336 | assert.EqualValues(t, 2, extra.ID) 337 | extra.SetPK(Integer(3)) 338 | assert.EqualValues(t, 3, extra.ID) 339 | } 340 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package reform 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "time" 7 | ) 8 | 9 | // DBInterface is a subset of *sql.DB used by reform. 10 | // Can be used together with NewDBFromInterface for easier integration with existing code or for passing test doubles. 11 | // 12 | // It may grow and shrink over time to include only needed *sql.DB methods, 13 | // and is excluded from SemVer compatibility guarantees. 14 | type DBInterface interface { 15 | DBTXContext 16 | BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) 17 | 18 | // Deprecated: do not use, it will be removed in v1.6. 19 | DBTX 20 | // Deprecated: do not use, it will be removed in v1.6. 21 | Begin() (*sql.Tx, error) 22 | } 23 | 24 | // check interface 25 | var _ DBInterface = (*sql.DB)(nil) 26 | 27 | // DB represents a connection to SQL database. 28 | type DB struct { 29 | *Querier 30 | db DBInterface 31 | } 32 | 33 | // NewDB creates new DB object for given SQL database connection. 34 | // Logger can be nil. 35 | func NewDB(db *sql.DB, dialect Dialect, logger Logger) *DB { 36 | return NewDBFromInterface(db, dialect, logger) 37 | } 38 | 39 | // NewDBFromInterface creates new DB object for given DBInterface. 40 | // Can be used for easier integration with existing code or for passing test doubles. 41 | // Logger can be nil. 42 | func NewDBFromInterface(db DBInterface, dialect Dialect, logger Logger) *DB { 43 | return &DB{ 44 | Querier: newQuerier(context.Background(), db, "", dialect, logger), 45 | db: db, 46 | } 47 | } 48 | 49 | // DBInterface returns DBInterface associated with a given DB object. 50 | func (db *DB) DBInterface() DBInterface { 51 | return db.db 52 | } 53 | 54 | // Begin starts transaction with Querier's context and default options. 55 | func (db *DB) Begin() (*TX, error) { 56 | return db.BeginTx(db.Querier.ctx, nil) 57 | } 58 | 59 | // BeginTx starts transaction with given context and options (can be nil). 60 | func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*TX, error) { 61 | db.logBefore("BEGIN", nil) 62 | start := time.Now() 63 | tx, err := db.db.BeginTx(ctx, opts) 64 | db.logAfter("BEGIN", nil, time.Since(start), err) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return newTX(ctx, tx, db.Dialect, db.Logger), nil 69 | } 70 | 71 | // InTransaction wraps function execution in transaction with Querier's context and default options, 72 | // rolling back it in case of error or panic, committing otherwise. 73 | func (db *DB) InTransaction(f func(t *TX) error) error { 74 | return db.InTransactionContext(db.Querier.ctx, nil, f) 75 | } 76 | 77 | // InTransactionContext wraps function execution in transaction with given context and options (can be nil), 78 | // rolling back it in case of error or panic, committing otherwise. 79 | func (db *DB) InTransactionContext(ctx context.Context, opts *sql.TxOptions, f func(t *TX) error) error { 80 | tx, err := db.BeginTx(ctx, opts) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | var committed bool 86 | defer func() { 87 | if !committed { 88 | // always return f() or Commit() error, not possible Rollback() error 89 | _ = tx.Rollback() 90 | } 91 | }() 92 | 93 | err = f(tx) 94 | if err == nil { 95 | err = tx.Commit() 96 | } 97 | if err == nil { 98 | committed = true 99 | } 100 | return err 101 | } 102 | 103 | // check interfaces 104 | var ( 105 | _ DBTX = (*DB)(nil) 106 | _ DBTXContext = (*DB)(nil) 107 | ) 108 | -------------------------------------------------------------------------------- /db_test.go: -------------------------------------------------------------------------------- 1 | package reform_test 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/brianvoe/gofakeit/v6" 10 | "github.com/jackc/pgx" 11 | "github.com/jackc/pgx/stdlib" 12 | "github.com/lib/pq" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | 16 | "gopkg.in/reform.v1" 17 | "gopkg.in/reform.v1/dialects/postgresql" 18 | . "gopkg.in/reform.v1/internal/test/models" 19 | ) 20 | 21 | func TestBeginCommit(t *testing.T) { 22 | db, tx := setupTX(t) 23 | defer teardown(t, db) 24 | 25 | person := &Person{ID: 42, Email: pointer.ToString(gofakeit.Email())} 26 | 27 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person)) 28 | assert.NoError(t, tx.Commit()) 29 | assert.Equal(t, tx.Commit(), reform.ErrTxDone) 30 | assert.Equal(t, tx.Rollback(), reform.ErrTxDone) 31 | assert.NoError(t, db.Reload(person)) 32 | assert.NoError(t, db.Delete(person)) 33 | } 34 | 35 | func TestBeginRollback(t *testing.T) { 36 | db, tx := setupTX(t) 37 | defer teardown(t, db) 38 | 39 | person := &Person{ID: 42, Email: pointer.ToString(gofakeit.Email())} 40 | 41 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person)) 42 | assert.NoError(t, tx.Rollback()) 43 | assert.Equal(t, tx.Commit(), reform.ErrTxDone) 44 | assert.Equal(t, tx.Rollback(), reform.ErrTxDone) 45 | assert.Equal(t, db.Reload(person), reform.ErrNoRows) 46 | } 47 | 48 | // This behavior is checked for documentation purposes only. reform does not rely on it. 49 | func TestErrorInTransaction(t *testing.T) { 50 | db := setupDB(t) 51 | defer teardown(t, db) 52 | if db.Dialect == postgresql.Dialect { 53 | t.Skipf("%s works differently, see TestAbortedTransaction", db.Dialect) 54 | } 55 | 56 | person1 := &Person{ID: 42, Email: pointer.ToString(gofakeit.Email())} 57 | person2 := &Person{ID: 43, Email: pointer.ToString(gofakeit.Email())} 58 | 59 | // commit works 60 | tx, err := db.Begin() 61 | require.NoError(t, err) 62 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person1)) 63 | assert.Error(t, insertPersonWithID(t, tx.Querier, person1)) // duplicate PK 64 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person2)) // INSERT works 65 | assert.NoError(t, tx.Commit()) 66 | assert.Equal(t, tx.Commit(), reform.ErrTxDone) 67 | assert.Equal(t, tx.Rollback(), reform.ErrTxDone) 68 | assert.NoError(t, db.Reload(person1)) 69 | assert.NoError(t, db.Reload(person2)) 70 | assert.NoError(t, db.Delete(person1)) 71 | assert.NoError(t, db.Delete(person2)) 72 | 73 | // rollback works 74 | tx, err = db.Begin() 75 | require.NoError(t, err) 76 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person1)) 77 | assert.Error(t, insertPersonWithID(t, tx.Querier, person1)) // duplicate PK 78 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person2)) // INSERT works 79 | assert.NoError(t, tx.Rollback()) 80 | assert.Equal(t, tx.Commit(), reform.ErrTxDone) 81 | assert.Equal(t, tx.Rollback(), reform.ErrTxDone) 82 | assert.EqualError(t, db.Reload(person1), reform.ErrNoRows.Error()) 83 | assert.EqualError(t, db.Reload(person2), reform.ErrNoRows.Error()) 84 | } 85 | 86 | // This behavior is checked for documentation purposes only. reform does not rely on it. 87 | // http://postgresql.nabble.com/Current-transaction-is-aborted-commands-ignored-until-end-of-transaction-block-td5109252.html 88 | func TestAbortedTransaction(t *testing.T) { 89 | db := setupDB(t) 90 | defer teardown(t, db) 91 | if db.Dialect != postgresql.Dialect { 92 | t.Skipf("%s works differently, see TestErrorInTransaction", db.Dialect) 93 | } 94 | 95 | person1 := &Person{ID: 42, Email: pointer.ToString(gofakeit.Email())} 96 | person2 := &Person{ID: 43, Email: pointer.ToString(gofakeit.Email())} 97 | 98 | // commit fails 99 | tx, err := db.Begin() 100 | require.NoError(t, err) 101 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person1)) 102 | assert.Contains(t, insertPersonWithID(t, tx.Querier, person1).Error(), `duplicate key value violates unique constraint "people_pkey"`) 103 | assert.Contains(t, insertPersonWithID(t, tx.Querier, person2).Error(), `current transaction is aborted, commands ignored until end of transaction block`) 104 | err = tx.Commit() 105 | require.Error(t, err) 106 | 107 | switch db.DBInterface().(*sql.DB).Driver().(type) { 108 | case *pq.Driver: 109 | assert.Equal(t, pq.ErrInFailedTransaction, err) 110 | case *stdlib.Driver: 111 | assert.Equal(t, pgx.ErrTxCommitRollback, err) 112 | default: 113 | t.Fatalf("unexpected driver, error %v", err) 114 | } 115 | assert.Equal(t, tx.Rollback(), reform.ErrTxDone) 116 | assert.EqualError(t, db.Reload(person1), reform.ErrNoRows.Error()) 117 | assert.EqualError(t, db.Reload(person2), reform.ErrNoRows.Error()) 118 | 119 | // rollback works 120 | tx, err = db.Begin() 121 | require.NoError(t, err) 122 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person1)) 123 | assert.Contains(t, insertPersonWithID(t, tx.Querier, person1).Error(), `duplicate key value violates unique constraint "people_pkey"`) 124 | assert.Contains(t, insertPersonWithID(t, tx.Querier, person2).Error(), `current transaction is aborted, commands ignored until end of transaction block`) 125 | assert.NoError(t, tx.Rollback()) 126 | assert.Equal(t, tx.Commit(), reform.ErrTxDone) 127 | assert.Equal(t, tx.Rollback(), reform.ErrTxDone) 128 | assert.EqualError(t, db.Reload(person1), reform.ErrNoRows.Error()) 129 | assert.EqualError(t, db.Reload(person2), reform.ErrNoRows.Error()) 130 | } 131 | 132 | func TestInTransaction(t *testing.T) { 133 | db := setupDB(t) 134 | defer teardown(t, db) 135 | 136 | person := &Person{ID: 42, Email: pointer.ToString(gofakeit.Email())} 137 | 138 | // error in closure 139 | err := db.InTransaction(func(tx *reform.TX) error { 140 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person)) 141 | return errors.New("epic error") 142 | }) 143 | assert.EqualError(t, err, "epic error") 144 | assert.Equal(t, db.Reload(person), reform.ErrNoRows) 145 | 146 | // panic in closure 147 | assert.Panics(t, func() { 148 | err = db.InTransaction(func(tx *reform.TX) error { 149 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person)) 150 | panic("epic panic!") 151 | }) 152 | }) 153 | assert.Equal(t, db.Reload(person), reform.ErrNoRows) 154 | 155 | // duplicate PK in closure 156 | err = db.InTransaction(func(tx *reform.TX) error { 157 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person)) 158 | err = insertPersonWithID(t, tx.Querier, person) 159 | assert.Error(t, err) 160 | return err 161 | }) 162 | assert.Error(t, err) 163 | assert.Equal(t, db.Reload(person), reform.ErrNoRows) 164 | 165 | // no error 166 | err = db.InTransaction(func(tx *reform.TX) error { 167 | assert.NoError(t, insertPersonWithID(t, tx.Querier, person)) 168 | return nil 169 | }) 170 | assert.NoError(t, err) 171 | assert.NoError(t, db.Reload(person)) 172 | assert.NoError(t, db.Delete(person)) 173 | } 174 | -------------------------------------------------------------------------------- /dialects/dialects.go: -------------------------------------------------------------------------------- 1 | // Package dialects implements reform.Dialect selector. 2 | package dialects 3 | 4 | import ( 5 | "strings" 6 | 7 | "gopkg.in/reform.v1" 8 | "gopkg.in/reform.v1/dialects/mssql" //nolint:staticcheck 9 | "gopkg.in/reform.v1/dialects/mysql" 10 | "gopkg.in/reform.v1/dialects/postgresql" 11 | "gopkg.in/reform.v1/dialects/sqlite3" 12 | "gopkg.in/reform.v1/dialects/sqlserver" 13 | ) 14 | 15 | // ForDriver returns reform Dialect for given driver string, or nil. 16 | func ForDriver(driver string) reform.Dialect { 17 | // for sqlite3_with_sleep 18 | if strings.HasPrefix(driver, "sqlite3") { 19 | return sqlite3.Dialect 20 | } 21 | 22 | switch driver { 23 | case "postgres", "pgx": 24 | return postgresql.Dialect 25 | case "mysql": 26 | return mysql.Dialect 27 | case "mssql": 28 | return mssql.Dialect //nolint:staticcheck 29 | case "sqlserver": 30 | return sqlserver.Dialect 31 | default: 32 | return nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dialects/mssql/mssql.go: -------------------------------------------------------------------------------- 1 | // Package mssql implements reform.Dialect for Microsoft SQL Server (mssql driver). 2 | // 3 | // Deprecated: Use sqlserver dialect instead. https://github.com/denisenkom/go-mssqldb#deprecated 4 | package mssql // import "gopkg.in/reform.v1/dialects/mssql" 5 | 6 | import ( 7 | "gopkg.in/reform.v1" 8 | ) 9 | 10 | type mssql struct{} 11 | 12 | func (mssql) String() string { 13 | return "mssql" 14 | } 15 | 16 | func (mssql) Placeholder(index int) string { 17 | return "?" 18 | } 19 | 20 | func (mssql) Placeholders(start, count int) []string { 21 | res := make([]string, count) 22 | for i := 0; i < count; i++ { 23 | res[i] = "?" 24 | } 25 | return res 26 | } 27 | 28 | func (mssql) QuoteIdentifier(identifier string) string { 29 | return "[" + identifier + "]" 30 | } 31 | 32 | func (mssql) LastInsertIdMethod() reform.LastInsertIdMethod { 33 | return reform.OutputInserted 34 | } 35 | 36 | func (mssql) SelectLimitMethod() reform.SelectLimitMethod { 37 | return reform.SelectTop 38 | } 39 | 40 | func (mssql) DefaultValuesMethod() reform.DefaultValuesMethod { 41 | return reform.DefaultValues 42 | } 43 | 44 | // Dialect implements reform.Dialect for Microsoft SQL Server. 45 | // 46 | // Deprecated: Use sqlserver.Dialect instead. https://github.com/denisenkom/go-mssqldb#deprecated 47 | var Dialect mssql 48 | 49 | // check interface 50 | var _ reform.Dialect = Dialect 51 | -------------------------------------------------------------------------------- /dialects/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | // Package mysql implements reform.Dialect for MySQL. 2 | package mysql // import "gopkg.in/reform.v1/dialects/mysql" 3 | 4 | import ( 5 | "gopkg.in/reform.v1" 6 | ) 7 | 8 | type mysql struct{} 9 | 10 | func (mysql) String() string { 11 | return "mysql" 12 | } 13 | 14 | func (mysql) Placeholder(index int) string { 15 | return "?" 16 | } 17 | 18 | func (mysql) Placeholders(start, count int) []string { 19 | res := make([]string, count) 20 | for i := 0; i < count; i++ { 21 | res[i] = "?" 22 | } 23 | return res 24 | } 25 | 26 | func (mysql) QuoteIdentifier(identifier string) string { 27 | return "`" + identifier + "`" 28 | } 29 | 30 | func (mysql) LastInsertIdMethod() reform.LastInsertIdMethod { 31 | return reform.LastInsertId 32 | } 33 | 34 | func (mysql) SelectLimitMethod() reform.SelectLimitMethod { 35 | return reform.Limit 36 | } 37 | 38 | func (mysql) DefaultValuesMethod() reform.DefaultValuesMethod { 39 | return reform.EmptyLists 40 | } 41 | 42 | // Dialect implements reform.Dialect for MySQL. 43 | var Dialect mysql 44 | 45 | // check interface 46 | var _ reform.Dialect = Dialect 47 | -------------------------------------------------------------------------------- /dialects/postgresql/postgresql.go: -------------------------------------------------------------------------------- 1 | // Package postgresql implements reform.Dialect for PostgreSQL. 2 | package postgresql // import "gopkg.in/reform.v1/dialects/postgresql" 3 | 4 | import ( 5 | "strconv" 6 | 7 | "gopkg.in/reform.v1" 8 | ) 9 | 10 | type postgresql struct{} 11 | 12 | func (postgresql) String() string { 13 | return "postgresql" 14 | } 15 | 16 | func (postgresql) Placeholder(index int) string { 17 | return "$" + strconv.Itoa(index) 18 | } 19 | 20 | func (postgresql) Placeholders(start, count int) []string { 21 | res := make([]string, count) 22 | for i := 0; i < count; i++ { 23 | res[i] = "$" + strconv.Itoa(start+i) 24 | } 25 | return res 26 | } 27 | 28 | func (postgresql) QuoteIdentifier(identifier string) string { 29 | return `"` + identifier + `"` 30 | } 31 | 32 | func (postgresql) LastInsertIdMethod() reform.LastInsertIdMethod { 33 | return reform.Returning 34 | } 35 | 36 | func (postgresql) SelectLimitMethod() reform.SelectLimitMethod { 37 | return reform.Limit 38 | } 39 | 40 | func (postgresql) DefaultValuesMethod() reform.DefaultValuesMethod { 41 | return reform.DefaultValues 42 | } 43 | 44 | // Dialect implements reform.Dialect for PostgreSQL. 45 | var Dialect postgresql 46 | 47 | // check interface 48 | var _ reform.Dialect = Dialect 49 | -------------------------------------------------------------------------------- /dialects/sqlite3/sqlite3.go: -------------------------------------------------------------------------------- 1 | // Package sqlite3 implements reform.Dialect for SQLite3. 2 | package sqlite3 // import "gopkg.in/reform.v1/dialects/sqlite3" 3 | 4 | import ( 5 | "gopkg.in/reform.v1" 6 | ) 7 | 8 | type sqlite3 struct{} 9 | 10 | func (sqlite3) String() string { 11 | return "sqlite3" 12 | } 13 | 14 | func (sqlite3) Placeholder(index int) string { 15 | return "?" 16 | } 17 | 18 | func (sqlite3) Placeholders(start, count int) []string { 19 | res := make([]string, count) 20 | for i := 0; i < count; i++ { 21 | res[i] = "?" 22 | } 23 | return res 24 | } 25 | 26 | func (sqlite3) QuoteIdentifier(identifier string) string { 27 | return `"` + identifier + `"` 28 | } 29 | 30 | func (sqlite3) LastInsertIdMethod() reform.LastInsertIdMethod { 31 | return reform.LastInsertId 32 | } 33 | 34 | func (sqlite3) SelectLimitMethod() reform.SelectLimitMethod { 35 | return reform.Limit 36 | } 37 | 38 | func (sqlite3) DefaultValuesMethod() reform.DefaultValuesMethod { 39 | return reform.DefaultValues 40 | } 41 | 42 | // Dialect implements reform.Dialect for SQLite3. 43 | var Dialect sqlite3 44 | 45 | // check interface 46 | var _ reform.Dialect = Dialect 47 | -------------------------------------------------------------------------------- /dialects/sqlserver/sqlserver.go: -------------------------------------------------------------------------------- 1 | // Package sqlserver implements reform.Dialect for Microsoft SQL Server (sqlserver driver). 2 | package sqlserver // import "gopkg.in/reform.v1/dialects/sqlserver" 3 | 4 | import ( 5 | "strconv" 6 | 7 | "gopkg.in/reform.v1" 8 | ) 9 | 10 | type sqlserver struct{} 11 | 12 | func (sqlserver) String() string { 13 | return "sqlserver" 14 | } 15 | 16 | func (sqlserver) Placeholder(index int) string { 17 | return "@P" + strconv.Itoa(index) 18 | } 19 | 20 | func (sqlserver) Placeholders(start, count int) []string { 21 | res := make([]string, count) 22 | for i := 0; i < count; i++ { 23 | res[i] = "@P" + strconv.Itoa(1+i) 24 | } 25 | return res 26 | } 27 | 28 | func (sqlserver) QuoteIdentifier(identifier string) string { 29 | return "[" + identifier + "]" 30 | } 31 | 32 | func (sqlserver) LastInsertIdMethod() reform.LastInsertIdMethod { 33 | return reform.OutputInserted 34 | } 35 | 36 | func (sqlserver) SelectLimitMethod() reform.SelectLimitMethod { 37 | return reform.SelectTop 38 | } 39 | 40 | func (sqlserver) DefaultValuesMethod() reform.DefaultValuesMethod { 41 | return reform.DefaultValues 42 | } 43 | 44 | // Dialect implements reform.Dialect for Microsoft SQL Server. 45 | var Dialect sqlserver 46 | 47 | // check interface 48 | var _ reform.Dialect = Dialect 49 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package reform is a better ORM for Go, based on non-empty interfaces and code generation. 2 | // 3 | // See README (https://github.com/go-reform/reform/blob/main/README.md) for quickstart information. 4 | // 5 | // 6 | // Context 7 | // 8 | // Querier object, embedded into DB and TX types, contains context which is used by all its methods. 9 | // It defaults to context.Background() and can be changed with WithContext method: 10 | // 11 | // // for a single call 12 | // projects, err := DB.WithContext(ctx).SelectAllFrom(ProjectTable, "") 13 | // 14 | // // for several calls 15 | // q := DB.WithContext(ctx) 16 | // projects, err := q.SelectAllFrom(ProjectTable, "") 17 | // persons, err := q.SelectAllFrom(PersonTable, "") 18 | // 19 | // Methods Exec, Query, and QueryRow use the same context. 20 | // Methods ExecContext, QueryContext, and QueryRowContext are just compatibility wrappers for 21 | // Querier.WithContext(ctx).Exec/Query/QuyeryRow to satisfy various standard interfaces. 22 | // 23 | // DB object methods Begin and InTransaction start transaction with the same context. 24 | // Methods BeginTx and InTransactionContext start transaction with a given context without changing 25 | // DB's context: 26 | // 27 | // var projects, persons []Struct 28 | // err := DB.InTransactionContext(ctx, nil, func(tx *reform.TX) error { 29 | // var e error 30 | // 31 | // // uses ctx 32 | // if projects, e = tx.SelectAllFrom(ProjectTable, ""); e != nil { 33 | // return e 34 | // } 35 | // 36 | // // uses ctx too 37 | // if persons, e = tx.SelectAllFrom(PersonTable, ""); e != nil { 38 | // return e 39 | // } 40 | // 41 | // return nil 42 | // } 43 | // 44 | // Note that several different contexts can be used: 45 | // 46 | // DB.InTransactionContext(ctx1, nil, func(tx *reform.TX) error { 47 | // _, _ = tx.SelectAllFrom(PersonTable, "") // uses ctx1 48 | // _, _ = tx.WithContext(ctx2).SelectAllFrom(PersonTable, "") // uses ctx2 49 | // ... 50 | // }) 51 | // 52 | // In theory, ctx1 and ctx2 can be entirely unrelated. Although that construct is occasionally useful, 53 | // the behavior on context cancelation is entirely driver-defined; some drivers may just close the whole 54 | // connection, effectively canceling unrelated ctx2 on ctx1 cancelation. For that reason mixing several 55 | // contexts is not recommended. 56 | // 57 | // 58 | // Tagging 59 | // 60 | // reform allows one to add tags (comments) to generated queries with WithTag Querier method. 61 | // They can be used to track queries from RDBMS logs and tools back to application code. For example, this code: 62 | // id := "baron" 63 | // project, err := DB.WithTag("GetProject:%v", id).FindByPrimaryKeyFrom(ProjectTable, id) 64 | // will generate the following query: 65 | // SELECT /* GetProject:baron */ "projects"."name", "projects"."id", "projects"."start", "projects"."end" FROM "projects" WHERE "projects"."id" = ? LIMIT 1 66 | // Please keep in mind that dynamic tags can affect RDBMS query cache. Consult your RDBMS documentation for details. 67 | // Some known links: 68 | // MySQL / Percona Server: https://www.percona.com/doc/percona-server/5.7/performance/query_cache_enhance.html#ignoring-comments 69 | // Microsoft SQL Server: https://msdn.microsoft.com/en-us/library/cc293623.aspx 70 | // 71 | // 72 | // Short example 73 | // 74 | // This example shows some reform features. 75 | // It uses https://github.com/AlekSi/pointer to get pointers to values of build-in types. 76 | package reform // import "gopkg.in/reform.v1" 77 | 78 | // Version defines reform version. 79 | const Version = "v1.6.0-dev" 80 | -------------------------------------------------------------------------------- /docker-compose.override.yml.apple-silicon: -------------------------------------------------------------------------------- 1 | --- 2 | # Rename to `docker-compose.override.yml` to make it work on Apple Silicon Macs. 3 | 4 | services: 5 | mysql: 6 | platform: linux/amd64 7 | mssql: 8 | image: mcr.microsoft.com/azure-sql-edge 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.7' 3 | 4 | services: 5 | postgres: 6 | image: ${REFORM_POSTGRES_IMAGE:-postgres:12} 7 | container_name: reform_postgres 8 | environment: 9 | - TZ=Europe/Moscow 10 | - POSTGRES_HOST_AUTH_METHOD=trust 11 | ports: 12 | - 127.0.0.1:5432:5432 13 | healthcheck: 14 | test: psql -U postgres --command='SELECT 1' 15 | interval: 1s 16 | timeout: 1s 17 | retries: 1 18 | 19 | mysql: 20 | image: ${REFORM_MYSQL_IMAGE:-mysql:5.7} 21 | container_name: reform_mysql 22 | environment: 23 | - TZ=Europe/Moscow 24 | - MYSQL_ALLOW_EMPTY_PASSWORD=1 25 | - MYSQL_ROOT_HOST=% 26 | ports: 27 | - 127.0.0.1:3306:3306 28 | healthcheck: 29 | test: echo 'SELECT 1' | mysql 30 | interval: 1s 31 | timeout: 1s 32 | retries: 1 33 | 34 | mssql: 35 | image: ${REFORM_MSSQL_IMAGE:-mcr.microsoft.com/mssql/server:2017-latest} 36 | container_name: reform_mssql 37 | environment: 38 | - ACCEPT_EULA=Y 39 | - SA_PASSWORD=reform-password123 40 | ports: 41 | - 127.0.0.1:1433:1433 42 | healthcheck: 43 | test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$$SA_PASSWORD" -Q "SELECT 1" || exit 1 44 | interval: 1s 45 | timeout: 1s 46 | retries: 1 47 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /docs/test.md: -------------------------------------------------------------------------------- 1 | # Test page 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gopkg.in/reform.v1 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/AlekSi/pointer v1.2.0 7 | github.com/brianvoe/gofakeit/v6 v6.14.3 8 | github.com/denisenkom/go-mssqldb v0.10.0 9 | github.com/go-sql-driver/mysql v1.6.0 10 | github.com/jackc/pgx v3.6.2+incompatible 11 | github.com/lib/pq v1.8.0 12 | github.com/mattn/go-sqlite3 v1.14.0 13 | github.com/stretchr/testify v1.7.0 14 | ) 15 | 16 | require ( 17 | github.com/cockroachdb/apd v1.1.0 // indirect 18 | github.com/davecgh/go-spew v1.1.0 // indirect 19 | github.com/gofrs/uuid v4.2.0+incompatible // indirect 20 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect 21 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect 22 | github.com/pkg/errors v0.9.1 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/shopspring/decimal v1.3.1 // indirect 25 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect 26 | golang.org/x/text v0.3.0 // indirect 27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= 2 | github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= 3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | github.com/brianvoe/gofakeit/v6 v6.14.3 h1:ohzXoFmAX3Kc9TgYAXrEp0xGfseCDfdJ4Zuz0HWs/88= 6 | github.com/brianvoe/gofakeit/v6 v6.14.3/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= 7 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 8 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 9 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8= 12 | github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 13 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 14 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 15 | github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= 16 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 17 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 18 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 19 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= 20 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= 21 | github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= 22 | github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= 23 | github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= 24 | github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 25 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 26 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 27 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 28 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 32 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 35 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= 38 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 40 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 41 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 45 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 49 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | -------------------------------------------------------------------------------- /internal/logger.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "gopkg.in/reform.v1" 9 | ) 10 | 11 | // Logger is our custom logger with Debugf method. 12 | type Logger struct { 13 | printf reform.Printf 14 | debug bool 15 | } 16 | 17 | // NewLogger creates a new logger. 18 | func NewLogger(prefix string, debug bool) *Logger { 19 | var flags int 20 | if debug { 21 | flags = log.Ldate | log.Lmicroseconds | log.Lshortfile 22 | } 23 | 24 | l := log.New(os.Stderr, prefix, flags) 25 | return &Logger{ 26 | printf: func(format string, args ...interface{}) { 27 | _ = l.Output(3, fmt.Sprintf(format, args...)) 28 | }, 29 | debug: debug, 30 | } 31 | } 32 | 33 | // Debugf prints message only when Logger debug flag is set to true. 34 | func (l *Logger) Debugf(format string, args ...interface{}) { 35 | if l.debug { 36 | l.printf(format, args...) 37 | } 38 | } 39 | 40 | // Printf calls l.Output to print to the logger. 41 | // Arguments are handled in the manner of fmt.Printf. 42 | func (l *Logger) Printf(format string, args ...interface{}) { 43 | l.printf(format, args...) 44 | } 45 | 46 | // Fatalf is equivalent to l.Printf() followed by a call to os.Exit(1) (or panic for debug logger). 47 | func (l *Logger) Fatalf(format string, args ...interface{}) { 48 | l.printf(format, args...) 49 | if l.debug { 50 | // panic instead of os.Exit(1) to see output (SQL queries, failed assertions, etc.) in tests 51 | panic(fmt.Sprintf(format, args...)) 52 | } 53 | os.Exit(1) 54 | } 55 | -------------------------------------------------------------------------------- /internal/test/db.go: -------------------------------------------------------------------------------- 1 | // Package test provides shared testing utilities. 2 | package test 3 | 4 | import ( 5 | "database/sql" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | sqlite3Driver "github.com/mattn/go-sqlite3" 13 | 14 | "gopkg.in/reform.v1" 15 | "gopkg.in/reform.v1/dialects" 16 | "gopkg.in/reform.v1/dialects/mssql" //nolint:staticcheck 17 | "gopkg.in/reform.v1/dialects/mysql" 18 | "gopkg.in/reform.v1/dialects/postgresql" 19 | "gopkg.in/reform.v1/dialects/sqlite3" 20 | "gopkg.in/reform.v1/dialects/sqlserver" 21 | ) 22 | 23 | //nolint:gochecknoglobals 24 | var ( 25 | sqlite3RegisterOnce sync.Once 26 | inspectOnce sync.Once 27 | ) 28 | 29 | // ConnectToTestDB returns open and prepared connection to test DB. 30 | func ConnectToTestDB() *reform.DB { 31 | driver := strings.TrimSpace(os.Getenv("REFORM_TEST_DRIVER")) 32 | source := strings.TrimSpace(os.Getenv("REFORM_TEST_SOURCE")) 33 | if driver == "" || source == "" { 34 | log.Fatal("no driver or source, set REFORM_TEST_DRIVER and REFORM_TEST_SOURCE") 35 | } 36 | 37 | // register custom function "sleep" for context tests 38 | if driver == "sqlite3" { 39 | driver = "sqlite3_with_sleep" 40 | 41 | sqlite3RegisterOnce.Do(func() { 42 | sleep := func(nsec int64) (int64, error) { 43 | time.Sleep(time.Duration(nsec)) 44 | return nsec, nil 45 | } 46 | sql.Register(driver, &sqlite3Driver.SQLiteDriver{ 47 | ConnectHook: func(conn *sqlite3Driver.SQLiteConn) error { 48 | return conn.RegisterFunc("sleep", sleep, false) 49 | }, 50 | }) 51 | }) 52 | } 53 | 54 | db, err := sql.Open(driver, source) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | // Use single connection so various session-related variables work. 60 | // For example: "PRAGMA foreign_keys" for SQLite3, "SET IDENTITY_INSERT" for MS SQL, etc. 61 | db.SetMaxIdleConns(1) 62 | db.SetMaxOpenConns(1) 63 | db.SetConnMaxLifetime(0) 64 | 65 | if err = db.Ping(); err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | now := time.Now() 70 | 71 | // select dialect for driver 72 | dialect := dialects.ForDriver(driver) 73 | switch dialect { 74 | case postgresql.Dialect: 75 | inspectOnce.Do(func() { 76 | log.Printf("driver = %q, source = %q", driver, source) 77 | 78 | log.Printf("time.Now() = %s", now) 79 | log.Printf("time.Now().UTC() = %s", now.UTC()) 80 | 81 | var version, tz string 82 | if err = db.QueryRow("SHOW server_version").Scan(&version); err != nil { 83 | log.Fatal(err) 84 | } 85 | if err = db.QueryRow("SHOW TimeZone").Scan(&tz); err != nil { 86 | log.Fatal(err) 87 | } 88 | log.Printf("PostgreSQL version = %q", version) 89 | log.Printf("PostgreSQL TimeZone = %q", tz) 90 | }) 91 | 92 | case mysql.Dialect: 93 | inspectOnce.Do(func() { 94 | log.Printf("driver = %q, source = %q", driver, source) 95 | 96 | log.Printf("time.Now() = %s", now) 97 | log.Printf("time.Now().UTC() = %s", now.UTC()) 98 | 99 | q := "SELECT @@version, @@sql_mode, @@autocommit, @@time_zone" 100 | var version, mode, autocommit, tz string 101 | if err = db.QueryRow(q).Scan(&version, &mode, &autocommit, &tz); err != nil { 102 | log.Fatal(err) 103 | } 104 | log.Printf("MySQL version = %q", version) 105 | log.Printf("MySQL sql_mode = %q", mode) 106 | log.Printf("MySQL autocommit = %q", autocommit) 107 | log.Printf("MySQL time_zone = %q", tz) 108 | }) 109 | 110 | case sqlite3.Dialect: 111 | if _, err = db.Exec("PRAGMA foreign_keys = ON"); err != nil { 112 | log.Fatal(err) 113 | } 114 | 115 | inspectOnce.Do(func() { 116 | log.Printf("driver = %q, source = %q", driver, source) 117 | 118 | log.Printf("time.Now() = %s", now) 119 | log.Printf("time.Now().UTC() = %s", now.UTC()) 120 | 121 | var version, sourceID string 122 | if err = db.QueryRow("SELECT sqlite_version(), sqlite_source_id()").Scan(&version, &sourceID); err != nil { 123 | log.Fatal(err) 124 | } 125 | log.Printf("SQLite3 version = %q", version) 126 | log.Printf("SQLite3 source = %q", sourceID) 127 | }) 128 | 129 | case mssql.Dialect: //nolint:staticcheck 130 | fallthrough 131 | case sqlserver.Dialect: 132 | inspectOnce.Do(func() { 133 | log.Printf("driver = %q, source = %q", driver, source) 134 | 135 | log.Printf("time.Now() = %s", now) 136 | log.Printf("time.Now().UTC() = %s", now.UTC()) 137 | 138 | var version string 139 | var options uint16 140 | if err = db.QueryRow("SELECT @@VERSION, @@OPTIONS").Scan(&version, &options); err != nil { 141 | log.Fatal(err) 142 | } 143 | xact := "ON" 144 | if options&0x4000 == 0 { 145 | xact = "OFF" 146 | } 147 | log.Printf("MS SQL VERSION = %s", version) 148 | log.Printf("MS SQL OPTIONS = %#4x (XACT_ABORT %s)", options, xact) 149 | }) 150 | 151 | default: 152 | log.Fatalf("reform: no dialect for driver %s", driver) 153 | } 154 | 155 | return reform.NewDB(db, dialect, nil) 156 | } 157 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus1.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | type ( 6 | // BogusType is an exported type used for testing. 7 | BogusType string 8 | 9 | // BogusType is a non-exported type used for testing. 10 | bogusType string 11 | ) 12 | 13 | // Bogus1 is used for testing. reform:bogus 14 | type Bogus1 struct { 15 | BogusType `reform:"bogus"` // anonymous field with "reform:" tag should generate error 16 | } 17 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus10.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus10 is used for testing. reform:bogus 6 | type Bogus10 struct { 7 | Bogus1 string `reform:"bogus1,pk"` 8 | Bogus2 string `reform:"bogus2,pk"` // field with "reform:" tag with duplicate pk label should generate error 9 | } 10 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus11.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus11 is used for testing. reform:bogus 6 | type Bogus11 struct { 7 | Bogus []string `reform:"bogus,pk"` // slice field with "reform:" tag and pk label should generate error 8 | } 9 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus2.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus2 is used for testing. reform:bogus 6 | type Bogus2 struct { 7 | bogusType `reform:"bogus"` // anonymous field with "reform:" tag should generate error 8 | } 9 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus3.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus3 is used for testing. reform:bogus 6 | type Bogus3 struct { 7 | bogus string `reform:"bogus"` // non-exported field with "reform:" tag should generate error 8 | } 9 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus4.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus4 is used for testing. reform:bogus 6 | type Bogus4 struct { 7 | Bogus string `reform:",pk"` // field with "reform:" tag without column name should generate error 8 | } 9 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus5.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus5 is used for testing. reform:bogus 6 | type Bogus5 struct { 7 | Bogus string `reform:"bogus,foo"` // field with "reform:" tag with unexpected value should generate error 8 | } 9 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus6.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus6 is used for testing. reform:bogus 6 | type Bogus6 struct { 7 | // struct without fields with "reform:" tag should generate error 8 | Bogus string 9 | } 10 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus7.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus7 is used for testing. reform:bogus 6 | type Bogus7 struct { 7 | Bogus *string `reform:"bogus,pk"` // pointer field with "reform:" tag and pk label should generate error 8 | } 9 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus8.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus8 is used for testing. reform:bogus 6 | type Bogus8 struct { 7 | Bogus *string `reform:"bogus,omitempty"` // pointer field with "reform:" tag and omitempty label should generate error 8 | } 9 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus9.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // Bogus9 is used for testing. reform:bogus 6 | type Bogus9 struct { 7 | Bogus1 string `reform:"bogus,pk"` 8 | Bogus2 string `reform:"bogus"` // field with "reform:" tag with duplicate column name should generate error 9 | } 10 | -------------------------------------------------------------------------------- /internal/test/models/bogus/bogus_ignore.go: -------------------------------------------------------------------------------- 1 | package bogus 2 | 3 | //go:generate reform 4 | 5 | // BogusIgnore is used for testing. 6 | // Struct without "reform:" magic comment should be ignored by ParseFile. 7 | type BogusIgnore struct { 8 | Bogus string `reform:"bogus"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/test/models/extra.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | //go:generate reform 4 | 5 | // types for testing 6 | type ( 7 | Integer int32 8 | String string 9 | Bytes []byte 10 | Uint8s []uint8 11 | ) 12 | 13 | //reform:extra 14 | type Extra struct { 15 | ID Integer `reform:"id,pk"` 16 | // UUID uuid.UUID `reform:"uuid"` 17 | Name *String `reform:"name"` 18 | 19 | Ignored1 string 20 | Ignored2 string `reform:""` 21 | Ignored3 string `reform:"-"` 22 | 23 | Byte byte `reform:"byte"` 24 | Uint8 uint8 `reform:"uint8"` 25 | ByteP *byte `reform:"bytep"` 26 | Uint8P *uint8 `reform:"uint8p"` 27 | Bytes []byte `reform:"bytes"` 28 | Uint8s []uint8 `reform:"uint8s"` 29 | BytesA [512]byte `reform:"bytesa"` 30 | Uint8sA [512]uint8 `reform:"uint8sa"` 31 | BytesT Bytes `reform:"bytest"` 32 | Uint8sT Uint8s `reform:"uint8st"` 33 | } 34 | 35 | //reform:not_exported 36 | type notExported struct { 37 | ID string `reform:"id,pk"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/test/models/extra_reform.go: -------------------------------------------------------------------------------- 1 | // Code generated by gopkg.in/reform.v1. DO NOT EDIT. 2 | 3 | package models 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "gopkg.in/reform.v1" 10 | "gopkg.in/reform.v1/parse" 11 | ) 12 | 13 | type extraTableType struct { 14 | s parse.StructInfo 15 | z []interface{} 16 | } 17 | 18 | // Schema returns a schema name in SQL database (""). 19 | func (v *extraTableType) Schema() string { 20 | return v.s.SQLSchema 21 | } 22 | 23 | // Name returns a view or table name in SQL database ("extra"). 24 | func (v *extraTableType) Name() string { 25 | return v.s.SQLName 26 | } 27 | 28 | // Columns returns a new slice of column names for that view or table in SQL database. 29 | func (v *extraTableType) Columns() []string { 30 | return []string{ 31 | "id", 32 | "name", 33 | "byte", 34 | "uint8", 35 | "bytep", 36 | "uint8p", 37 | "bytes", 38 | "uint8s", 39 | "bytesa", 40 | "uint8sa", 41 | "bytest", 42 | "uint8st", 43 | } 44 | } 45 | 46 | // NewStruct makes a new struct for that view or table. 47 | func (v *extraTableType) NewStruct() reform.Struct { 48 | return new(Extra) 49 | } 50 | 51 | // NewRecord makes a new record for that table. 52 | func (v *extraTableType) NewRecord() reform.Record { 53 | return new(Extra) 54 | } 55 | 56 | // PKColumnIndex returns an index of primary key column for that table in SQL database. 57 | func (v *extraTableType) PKColumnIndex() uint { 58 | return uint(v.s.PKFieldIndex) 59 | } 60 | 61 | // ExtraTable represents extra view or table in SQL database. 62 | var ExtraTable = &extraTableType{ 63 | s: parse.StructInfo{ 64 | Type: "Extra", 65 | SQLName: "extra", 66 | Fields: []parse.FieldInfo{ 67 | {Name: "ID", Type: "Integer", Column: "id"}, 68 | {Name: "Name", Type: "*String", Column: "name"}, 69 | {Name: "Byte", Type: "uint8", Column: "byte"}, 70 | {Name: "Uint8", Type: "uint8", Column: "uint8"}, 71 | {Name: "ByteP", Type: "*uint8", Column: "bytep"}, 72 | {Name: "Uint8P", Type: "*uint8", Column: "uint8p"}, 73 | {Name: "Bytes", Type: "[]uint8", Column: "bytes"}, 74 | {Name: "Uint8s", Type: "[]uint8", Column: "uint8s"}, 75 | {Name: "BytesA", Type: "[512]uint8", Column: "bytesa"}, 76 | {Name: "Uint8sA", Type: "[512]uint8", Column: "uint8sa"}, 77 | {Name: "BytesT", Type: "Bytes", Column: "bytest"}, 78 | {Name: "Uint8sT", Type: "Uint8s", Column: "uint8st"}, 79 | }, 80 | PKFieldIndex: 0, 81 | }, 82 | z: new(Extra).Values(), 83 | } 84 | 85 | // String returns a string representation of this struct or record. 86 | func (s Extra) String() string { 87 | res := make([]string, 12) 88 | res[0] = "ID: " + reform.Inspect(s.ID, true) 89 | res[1] = "Name: " + reform.Inspect(s.Name, true) 90 | res[2] = "Byte: " + reform.Inspect(s.Byte, true) 91 | res[3] = "Uint8: " + reform.Inspect(s.Uint8, true) 92 | res[4] = "ByteP: " + reform.Inspect(s.ByteP, true) 93 | res[5] = "Uint8P: " + reform.Inspect(s.Uint8P, true) 94 | res[6] = "Bytes: " + reform.Inspect(s.Bytes, true) 95 | res[7] = "Uint8s: " + reform.Inspect(s.Uint8s, true) 96 | res[8] = "BytesA: " + reform.Inspect(s.BytesA, true) 97 | res[9] = "Uint8sA: " + reform.Inspect(s.Uint8sA, true) 98 | res[10] = "BytesT: " + reform.Inspect(s.BytesT, true) 99 | res[11] = "Uint8sT: " + reform.Inspect(s.Uint8sT, true) 100 | return strings.Join(res, ", ") 101 | } 102 | 103 | // Values returns a slice of struct or record field values. 104 | // Returned interface{} values are never untyped nils. 105 | func (s *Extra) Values() []interface{} { 106 | return []interface{}{ 107 | s.ID, 108 | s.Name, 109 | s.Byte, 110 | s.Uint8, 111 | s.ByteP, 112 | s.Uint8P, 113 | s.Bytes, 114 | s.Uint8s, 115 | s.BytesA, 116 | s.Uint8sA, 117 | s.BytesT, 118 | s.Uint8sT, 119 | } 120 | } 121 | 122 | // Pointers returns a slice of pointers to struct or record fields. 123 | // Returned interface{} values are never untyped nils. 124 | func (s *Extra) Pointers() []interface{} { 125 | return []interface{}{ 126 | &s.ID, 127 | &s.Name, 128 | &s.Byte, 129 | &s.Uint8, 130 | &s.ByteP, 131 | &s.Uint8P, 132 | &s.Bytes, 133 | &s.Uint8s, 134 | &s.BytesA, 135 | &s.Uint8sA, 136 | &s.BytesT, 137 | &s.Uint8sT, 138 | } 139 | } 140 | 141 | // View returns View object for that struct. 142 | func (s *Extra) View() reform.View { 143 | return ExtraTable 144 | } 145 | 146 | // Table returns Table object for that record. 147 | func (s *Extra) Table() reform.Table { 148 | return ExtraTable 149 | } 150 | 151 | // PKValue returns a value of primary key for that record. 152 | // Returned interface{} value is never untyped nil. 153 | func (s *Extra) PKValue() interface{} { 154 | return s.ID 155 | } 156 | 157 | // PKPointer returns a pointer to primary key field for that record. 158 | // Returned interface{} value is never untyped nil. 159 | func (s *Extra) PKPointer() interface{} { 160 | return &s.ID 161 | } 162 | 163 | // HasPK returns true if record has non-zero primary key set, false otherwise. 164 | func (s *Extra) HasPK() bool { 165 | return s.ID != ExtraTable.z[ExtraTable.s.PKFieldIndex] 166 | } 167 | 168 | // SetPK sets record primary key, if possible. 169 | // 170 | // Deprecated: prefer direct field assignment where possible: s.ID = pk. 171 | func (s *Extra) SetPK(pk interface{}) { 172 | reform.SetPK(s, pk) 173 | } 174 | 175 | // check interfaces 176 | var ( 177 | _ reform.View = ExtraTable 178 | _ reform.Struct = (*Extra)(nil) 179 | _ reform.Table = ExtraTable 180 | _ reform.Record = (*Extra)(nil) 181 | _ fmt.Stringer = (*Extra)(nil) 182 | ) 183 | 184 | type notExportedTableType struct { 185 | s parse.StructInfo 186 | z []interface{} 187 | } 188 | 189 | // Schema returns a schema name in SQL database (""). 190 | func (v *notExportedTableType) Schema() string { 191 | return v.s.SQLSchema 192 | } 193 | 194 | // Name returns a view or table name in SQL database ("not_exported"). 195 | func (v *notExportedTableType) Name() string { 196 | return v.s.SQLName 197 | } 198 | 199 | // Columns returns a new slice of column names for that view or table in SQL database. 200 | func (v *notExportedTableType) Columns() []string { 201 | return []string{ 202 | "id", 203 | } 204 | } 205 | 206 | // NewStruct makes a new struct for that view or table. 207 | func (v *notExportedTableType) NewStruct() reform.Struct { 208 | return new(notExported) 209 | } 210 | 211 | // NewRecord makes a new record for that table. 212 | func (v *notExportedTableType) NewRecord() reform.Record { 213 | return new(notExported) 214 | } 215 | 216 | // PKColumnIndex returns an index of primary key column for that table in SQL database. 217 | func (v *notExportedTableType) PKColumnIndex() uint { 218 | return uint(v.s.PKFieldIndex) 219 | } 220 | 221 | // notExportedTable represents not_exported view or table in SQL database. 222 | var notExportedTable = ¬ExportedTableType{ 223 | s: parse.StructInfo{ 224 | Type: "notExported", 225 | SQLName: "not_exported", 226 | Fields: []parse.FieldInfo{ 227 | {Name: "ID", Type: "string", Column: "id"}, 228 | }, 229 | PKFieldIndex: 0, 230 | }, 231 | z: new(notExported).Values(), 232 | } 233 | 234 | // String returns a string representation of this struct or record. 235 | func (s notExported) String() string { 236 | res := make([]string, 1) 237 | res[0] = "ID: " + reform.Inspect(s.ID, true) 238 | return strings.Join(res, ", ") 239 | } 240 | 241 | // Values returns a slice of struct or record field values. 242 | // Returned interface{} values are never untyped nils. 243 | func (s *notExported) Values() []interface{} { 244 | return []interface{}{ 245 | s.ID, 246 | } 247 | } 248 | 249 | // Pointers returns a slice of pointers to struct or record fields. 250 | // Returned interface{} values are never untyped nils. 251 | func (s *notExported) Pointers() []interface{} { 252 | return []interface{}{ 253 | &s.ID, 254 | } 255 | } 256 | 257 | // View returns View object for that struct. 258 | func (s *notExported) View() reform.View { 259 | return notExportedTable 260 | } 261 | 262 | // Table returns Table object for that record. 263 | func (s *notExported) Table() reform.Table { 264 | return notExportedTable 265 | } 266 | 267 | // PKValue returns a value of primary key for that record. 268 | // Returned interface{} value is never untyped nil. 269 | func (s *notExported) PKValue() interface{} { 270 | return s.ID 271 | } 272 | 273 | // PKPointer returns a pointer to primary key field for that record. 274 | // Returned interface{} value is never untyped nil. 275 | func (s *notExported) PKPointer() interface{} { 276 | return &s.ID 277 | } 278 | 279 | // HasPK returns true if record has non-zero primary key set, false otherwise. 280 | func (s *notExported) HasPK() bool { 281 | return s.ID != notExportedTable.z[notExportedTable.s.PKFieldIndex] 282 | } 283 | 284 | // SetPK sets record primary key, if possible. 285 | // 286 | // Deprecated: prefer direct field assignment where possible: s.ID = pk. 287 | func (s *notExported) SetPK(pk interface{}) { 288 | reform.SetPK(s, pk) 289 | } 290 | 291 | // check interfaces 292 | var ( 293 | _ reform.View = notExportedTable 294 | _ reform.Struct = (*notExported)(nil) 295 | _ reform.Table = notExportedTable 296 | _ reform.Record = (*notExported)(nil) 297 | _ fmt.Stringer = (*notExported)(nil) 298 | ) 299 | 300 | func init() { 301 | parse.AssertUpToDate(&ExtraTable.s, new(Extra)) 302 | parse.AssertUpToDate(¬ExportedTable.s, new(notExported)) 303 | } 304 | -------------------------------------------------------------------------------- /internal/test/models/good.go: -------------------------------------------------------------------------------- 1 | //nolint:stylecheck 2 | package models 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/AlekSi/pointer" 8 | 9 | "gopkg.in/reform.v1" 10 | ) 11 | 12 | //go:generate reform 13 | 14 | type ( 15 | //reform:people 16 | Person struct { 17 | ID int32 `reform:"id,pk"` 18 | GroupID *int32 `reform:"group_id"` 19 | Name string `reform:"name"` 20 | Email *string `reform:"email"` 21 | CreatedAt time.Time `reform:"created_at"` 22 | UpdatedAt *time.Time `reform:"updated_at"` 23 | } 24 | ) 25 | 26 | // BeforeInsert sets CreatedAt if it's not set, 27 | // then converts to UTC, truncates to second and strips monotonic clock reading from both CreatedAt and UpdatedAt. 28 | func (p *Person) BeforeInsert() error { 29 | if p.CreatedAt.IsZero() { 30 | p.CreatedAt = time.Now() 31 | } 32 | 33 | p.CreatedAt = p.CreatedAt.UTC().Truncate(time.Second).AddDate(0, 0, 0) 34 | if p.UpdatedAt != nil { 35 | p.UpdatedAt = pointer.ToTime(p.UpdatedAt.UTC().Truncate(time.Second).AddDate(0, 0, 0)) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // BeforeUpdate sets CreatedAt if it's not set, 42 | // sets UpdatedAt, 43 | // then converts to UTC, truncates to second and strips monotonic clock reading from both CreatedAt and UpdatedAt. 44 | func (p *Person) BeforeUpdate() error { 45 | now := time.Now() 46 | 47 | if p.CreatedAt.IsZero() { 48 | p.CreatedAt = now 49 | } 50 | 51 | p.UpdatedAt = &now 52 | 53 | p.CreatedAt = p.CreatedAt.UTC().Truncate(time.Second).AddDate(0, 0, 0) 54 | p.UpdatedAt = pointer.ToTime(p.UpdatedAt.UTC().Truncate(time.Second).AddDate(0, 0, 0)) 55 | 56 | return nil 57 | } 58 | 59 | // AfterFind converts to UTC and truncates to second both CreatedAt and UpdatedAt. 60 | func (p *Person) AfterFind() error { 61 | p.CreatedAt = p.CreatedAt.UTC().Truncate(time.Second) 62 | if p.UpdatedAt != nil { 63 | p.UpdatedAt = pointer.ToTime(p.UpdatedAt.UTC().Truncate(time.Second)) 64 | } 65 | return nil 66 | } 67 | 68 | // Project represents row in table projects 69 | // (reform:projects). 70 | type Project struct { 71 | Name string `reform:"name"` 72 | ID string `reform:"id,pk"` 73 | Start time.Time `reform:"start"` 74 | End *time.Time `reform:"end"` 75 | } 76 | 77 | // BeforeInsert converts to UTC, truncates to day and strips monotonic clock reading from both Start and End. 78 | func (p *Project) BeforeInsert() error { 79 | p.Start = p.Start.UTC().Truncate(24*time.Hour).AddDate(0, 0, 0) 80 | if p.End != nil { 81 | p.End = pointer.ToTime(p.End.UTC().Truncate(24*time.Hour).AddDate(0, 0, 0)) 82 | } 83 | return nil 84 | } 85 | 86 | // BeforeUpdate converts to UTC, truncates to day and strips monotonic clock reading from both Start and End. 87 | func (p *Project) BeforeUpdate() error { 88 | p.Start = p.Start.UTC().Truncate(24*time.Hour).AddDate(0, 0, 0) 89 | if p.End != nil { 90 | p.End = pointer.ToTime(p.End.UTC().Truncate(24*time.Hour).AddDate(0, 0, 0)) 91 | } 92 | return nil 93 | } 94 | 95 | // AfterFind converts to UTC both Start and End. 96 | func (p *Project) AfterFind() error { 97 | p.Start = p.Start.UTC() 98 | if p.End != nil { 99 | p.End = pointer.ToTime(p.End.UTC()) 100 | } 101 | return nil 102 | } 103 | 104 | // PersonProject represents row in table person_project. reform:person_project 105 | type PersonProject struct { 106 | PersonID int32 `reform:"person_id"` 107 | ProjectID string `reform:"project_id"` 108 | } 109 | 110 | // reform:id_only 111 | type IDOnly struct { 112 | ID int32 `reform:"id,pk"` 113 | } 114 | 115 | //reform:constraints 116 | type Constraints struct { 117 | I int32 `reform:"i"` 118 | ID string `reform:"id,pk"` 119 | } 120 | 121 | //reform:composite_pk 122 | type CompositePk struct { 123 | I int32 `reform:"i"` 124 | Name string `reform:"name"` 125 | J string `reform:"j"` 126 | } 127 | 128 | //reform:legacy.people 129 | type LegacyPerson struct { 130 | ID int32 `reform:"id,pk"` 131 | Name *string `reform:"name"` 132 | } 133 | 134 | // check interfaces 135 | var ( 136 | _ reform.BeforeInserter = (*Person)(nil) 137 | _ reform.BeforeUpdater = (*Person)(nil) 138 | _ reform.AfterFinder = (*Person)(nil) 139 | _ reform.BeforeInserter = (*Project)(nil) 140 | _ reform.BeforeUpdater = (*Project)(nil) 141 | _ reform.AfterFinder = (*Project)(nil) 142 | ) 143 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package reform 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Inspect returns suitable for logging representation of a query argument. 11 | func Inspect(arg interface{}, addType bool) string { 12 | var s string 13 | v := reflect.ValueOf(arg) 14 | switch v.Kind() { 15 | case reflect.Ptr: 16 | if v.IsNil() { 17 | s = "" 18 | } else { 19 | s = Inspect(v.Elem().Interface(), false) 20 | } 21 | 22 | case reflect.String: 23 | s = fmt.Sprintf("%#q", arg) 24 | 25 | default: 26 | s = fmt.Sprintf("%v", arg) 27 | } 28 | 29 | if addType { 30 | s += fmt.Sprintf(" (%T)", arg) 31 | } 32 | return s 33 | } 34 | 35 | // Logger is responsible to log queries before and after their execution. 36 | type Logger interface { 37 | // Before logs query before execution. 38 | Before(query string, args []interface{}) 39 | 40 | // After logs query after execution. 41 | After(query string, args []interface{}, d time.Duration, err error) 42 | } 43 | 44 | // Printf is a (fmt.Printf|log.Printf|testing.T.Logf)-like function. 45 | type Printf func(format string, args ...interface{}) 46 | 47 | // PrintfLogger is a simple query logger. 48 | type PrintfLogger struct { 49 | LogTypes bool 50 | printf Printf 51 | } 52 | 53 | // NewPrintfLogger creates a new simple query logger for any Printf-like function. 54 | func NewPrintfLogger(printf Printf) *PrintfLogger { 55 | return &PrintfLogger{false, printf} 56 | } 57 | 58 | // Before logs query before execution. 59 | func (pl *PrintfLogger) Before(query string, args []interface{}) { 60 | // fast path 61 | if args == nil { 62 | pl.printf(">>> %s", query) 63 | return 64 | } 65 | 66 | ss := make([]string, len(args)) 67 | for i, arg := range args { 68 | ss[i] = Inspect(arg, pl.LogTypes) 69 | } 70 | 71 | pl.printf(">>> %s [%s]", query, strings.Join(ss, ", ")) 72 | } 73 | 74 | // After logs query after execution. 75 | func (pl *PrintfLogger) After(query string, args []interface{}, d time.Duration, err error) { 76 | // fast path 77 | if args == nil { 78 | msg := fmt.Sprintf("%s %s", query, d) 79 | if err != nil { 80 | msg += ": " + err.Error() 81 | } 82 | pl.printf("<<< %s", msg) 83 | return 84 | } 85 | 86 | ss := make([]string, len(args)) 87 | for i, arg := range args { 88 | ss[i] = Inspect(arg, pl.LogTypes) 89 | } 90 | 91 | msg := fmt.Sprintf("%s [%s] %s", query, strings.Join(ss, ", "), d) 92 | if err != nil { 93 | msg += ": " + err.Error() 94 | } 95 | pl.printf("<<< %s", msg) 96 | } 97 | 98 | // check interface 99 | var _ Logger = (*PrintfLogger)(nil) 100 | -------------------------------------------------------------------------------- /parse/base.go: -------------------------------------------------------------------------------- 1 | // Package parse implements parsing of Go structs in files and runtime. 2 | // 3 | // This package, despite containing exported types, methods and functions, 4 | // is an internal part of implementation of 'reform' command, also used by generated files, 5 | // and not a part of public stable API. 6 | package parse // import "gopkg.in/reform.v1/parse" 7 | 8 | import ( 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // FieldInfo represents information about struct field. 15 | type FieldInfo struct { 16 | Name string // field name as defined in source file, e.g. Name 17 | Type string // field type as defined in source file, e.g. string; always present for primary key, may be absent otherwise 18 | Column string // SQL database column name from "reform:" struct field tag, e.g. name 19 | } 20 | 21 | // fieldInfoInSync returns true if FieldInfo fields that are set by both file and runtime parser are equal. 22 | func fieldInfoInSync(fi1, fi2 *FieldInfo) bool { 23 | if fi1 == nil { 24 | panic("fi1 is nil") 25 | } 26 | if fi2 == nil { 27 | panic("fi2 is nil") 28 | } 29 | 30 | return fi1.Name == fi2.Name && 31 | fi1.Type == fi2.Type && 32 | fi1.Column == fi2.Column 33 | } 34 | 35 | // GoString returns struct field information as Go code string. 36 | func (fi *FieldInfo) GoString() string { 37 | return fmt.Sprintf("{Name: %q, Type: %q, Column: %q}", fi.Name, fi.Type, fi.Column) 38 | } 39 | 40 | // StructInfo represents information about struct. 41 | type StructInfo struct { 42 | Type string // struct type as defined in source file, e.g. User 43 | SQLSchema string // SQL database schema name from magic "reform:" comment, e.g. public 44 | SQLName string // SQL database view or table name from magic "reform:" comment, e.g. users 45 | Fields []FieldInfo // fields info 46 | PKFieldIndex int // index of primary key field in Fields, -1 if none 47 | } 48 | 49 | // structInfoInSync returns true if FieldInfo fields that are set by both file and runtime parser are equal. 50 | func structInfoInSync(si1, si2 *StructInfo) bool { 51 | if si1 == nil { 52 | panic("si1 is nil") 53 | } 54 | if si2 == nil { 55 | panic("si2 is nil") 56 | } 57 | 58 | inSync := si1.Type == si2.Type && 59 | si1.SQLSchema == si2.SQLSchema && 60 | si1.SQLName == si2.SQLName && 61 | si1.PKFieldIndex == si2.PKFieldIndex 62 | if !inSync { 63 | return false 64 | } 65 | 66 | if len(si1.Fields) != len(si2.Fields) { 67 | return false 68 | } 69 | for i := range si1.Fields { 70 | if inSync = fieldInfoInSync(&si1.Fields[i], &si2.Fields[i]); !inSync { 71 | return false 72 | } 73 | } 74 | return true 75 | } 76 | 77 | // GoString returns struct information as Go code string. 78 | func (s *StructInfo) GoString() string { 79 | res := "parse.StructInfo{\n" 80 | 81 | res += fmt.Sprintf("\tType: %q,\n", s.Type) 82 | if s.SQLSchema != "" { 83 | res += fmt.Sprintf("\tSQLSchema: %q,\n", s.SQLSchema) 84 | } 85 | res += fmt.Sprintf("\tSQLName: %q,\n", s.SQLName) 86 | 87 | res += "\tFields: []parse.FieldInfo{\n" 88 | for _, f := range s.Fields { 89 | res += fmt.Sprintf("\t\t%s,\n", f.GoString()) 90 | } 91 | res += "\t},\n" 92 | 93 | res += fmt.Sprintf("\tPKFieldIndex: %d,\n", s.PKFieldIndex) 94 | 95 | res += "}" 96 | return res 97 | } 98 | 99 | // Columns returns a new slice of column names. 100 | func (s *StructInfo) Columns() []string { 101 | res := make([]string, len(s.Fields)) 102 | for i, f := range s.Fields { 103 | res[i] = f.Column 104 | } 105 | return res 106 | } 107 | 108 | // ColumnsGoString returns column names as Go code string. 109 | func (s *StructInfo) ColumnsGoString() string { 110 | res := make([]string, len(s.Fields)) 111 | for i, f := range s.Fields { 112 | res[i] = strconv.Quote(f.Column) 113 | } 114 | return "[]string{\n\t" + strings.Join(res, ",\n\t") + ",\n}" 115 | } 116 | 117 | // IsTable returns true if this object represent information for table, false for view. 118 | func (s *StructInfo) IsTable() bool { 119 | return s.PKFieldIndex >= 0 120 | } 121 | 122 | // PKField returns a primary key field, panics for views. 123 | func (s *StructInfo) PKField() FieldInfo { 124 | if !s.IsTable() { 125 | panic("reform: not a table") 126 | } 127 | return s.Fields[s.PKFieldIndex] 128 | } 129 | 130 | // AssertUpToDate checks that given StructInfo matches given object. 131 | // It is used during program initialization to check that generated files are up-to-date. 132 | func AssertUpToDate(si *StructInfo, obj interface{}) { 133 | msg := fmt.Sprintf(`reform: 134 | %s struct information is not up-to-date. 135 | Typically this means that %s type definition was changed, but 'reform' command / 'go generate' was not run. 136 | 137 | `, si.Type, si.Type) 138 | si2, err := Object(obj, si.SQLSchema, si.SQLName) 139 | if err != nil { 140 | panic(msg + err.Error()) 141 | } 142 | if !structInfoInSync(si, si2) { 143 | panic(msg) 144 | } 145 | } 146 | 147 | // parseStructFieldTag is used by both file and runtime parsers 148 | func parseStructFieldTag(tag string) (sqlName string, isPK bool) { 149 | parts := strings.Split(tag, ",") 150 | if len(parts) == 0 || len(parts) > 2 { 151 | return 152 | } 153 | 154 | if len(parts) == 2 { 155 | switch parts[1] { 156 | case "pk": 157 | isPK = true 158 | default: 159 | return 160 | } 161 | } 162 | 163 | sqlName = parts[0] 164 | return 165 | } 166 | 167 | // checkFields is used by both file and runtime parsers 168 | func checkFields(res *StructInfo) error { 169 | if len(res.Fields) == 0 { 170 | return fmt.Errorf(`reform: %s has no fields with "reform:" tag, it is not allowed`, res.Type) 171 | } 172 | 173 | dupes := make(map[string]string) 174 | for _, f := range res.Fields { 175 | if f2, ok := dupes[f.Column]; ok { 176 | return fmt.Errorf(`reform: %s has field %s with "reform:" tag with duplicate column name %s (used by %s), it is not allowed`, 177 | res.Type, f.Name, f.Column, f2) 178 | } 179 | dupes[f.Column] = f.Name 180 | } 181 | 182 | return nil 183 | } 184 | -------------------------------------------------------------------------------- /parse/file.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "reflect" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var magicReformComment = regexp.MustCompile(`reform:([0-9A-Za-z_\.]+)`) 14 | 15 | func fileGoType(x ast.Expr) string { 16 | switch t := x.(type) { 17 | case *ast.StarExpr: 18 | return "*" + fileGoType(t.X) 19 | case *ast.SelectorExpr: 20 | return fileGoType(t.X) + "." + t.Sel.String() 21 | case *ast.Ident: 22 | s := t.String() 23 | if s == "byte" { 24 | return "uint8" 25 | } 26 | return s 27 | case *ast.ArrayType: 28 | return "[" + fileGoType(t.Len) + "]" + fileGoType(t.Elt) 29 | case *ast.BasicLit: 30 | return t.Value 31 | case nil: 32 | return "" 33 | default: 34 | panic(fmt.Sprintf("reform: fileGoType: unhandled '%s' (%#v). Please report this bug.", x, x)) 35 | } 36 | } 37 | 38 | func commentText(g *ast.CommentGroup) string { 39 | // this code used to just call g.Text(), but the behavior of this method changed in Go 1.15: 40 | // https://go-review.googlesource.com/c/go/+/224737 41 | res := make([]string, len(g.List)) 42 | for i, c := range g.List { 43 | res[i] = c.Text 44 | } 45 | return strings.Join(res, " ") 46 | } 47 | 48 | func parseStructTypeSpec(ts *ast.TypeSpec, str *ast.StructType) (*StructInfo, error) { 49 | res := &StructInfo{ 50 | Type: ts.Name.Name, 51 | PKFieldIndex: -1, 52 | } 53 | 54 | var n int 55 | for _, f := range str.Fields.List { 56 | // consider only fields with "reform:" tag 57 | if f.Tag == nil { 58 | continue 59 | } 60 | tag := f.Tag.Value 61 | if len(tag) < 3 { 62 | continue 63 | } 64 | tag = reflect.StructTag(tag[1 : len(tag)-1]).Get("reform") // strip quotes 65 | if tag == "" || tag == "-" { 66 | continue 67 | } 68 | 69 | // check for anonymous fields 70 | if len(f.Names) == 0 { 71 | return nil, fmt.Errorf(`reform: %s has anonymous field %s with "reform:" tag, it is not allowed`, res.Type, f.Type) 72 | } 73 | if len(f.Names) != 1 { 74 | panic(fmt.Sprintf("reform: %d names: %#v. Please report this bug.", len(f.Names), f.Names)) 75 | } 76 | 77 | // check for exported name 78 | name := f.Names[0] 79 | if !name.IsExported() { 80 | return nil, fmt.Errorf(`reform: %s has non-exported field %s with "reform:" tag, it is not allowed`, res.Type, name.Name) 81 | } 82 | 83 | // parse tag and type 84 | column, isPK := parseStructFieldTag(tag) 85 | if column == "" { 86 | return nil, fmt.Errorf(`reform: %s has field %s with invalid "reform:" tag value, it is not allowed`, res.Type, name.Name) 87 | } 88 | typ := fileGoType(f.Type) 89 | if isPK { 90 | if strings.HasPrefix(typ, "*") { 91 | return nil, fmt.Errorf(`reform: %s has pointer field %s with with "pk" label in "reform:" tag, it is not allowed`, res.Type, name.Name) 92 | } 93 | if strings.HasPrefix(typ, "[") { 94 | return nil, fmt.Errorf(`reform: %s has slice field %s with with "pk" label in "reform:" tag, it is not allowed`, res.Type, name.Name) 95 | } 96 | if res.PKFieldIndex >= 0 { 97 | return nil, fmt.Errorf(`reform: %s has field %s with with duplicate "pk" label in "reform:" tag (first used by %s), it is not allowed`, res.Type, name.Name, res.Fields[res.PKFieldIndex].Name) 98 | } 99 | } 100 | 101 | res.Fields = append(res.Fields, FieldInfo{ 102 | Name: name.Name, 103 | Type: typ, 104 | Column: column, 105 | }) 106 | if isPK { 107 | res.PKFieldIndex = n 108 | } 109 | n++ 110 | } 111 | 112 | if len(res.Fields) == 0 { 113 | return nil, fmt.Errorf(`reform: %s has no fields with "reform:" tag, it is not allowed`, res.Type) 114 | } 115 | 116 | if err := checkFields(res); err != nil { 117 | return nil, err 118 | } 119 | 120 | return res, nil 121 | } 122 | 123 | // File parses given file and returns found structs information. 124 | func File(path string) ([]StructInfo, error) { 125 | // parse file 126 | fset := token.NewFileSet() 127 | fileNode, err := parser.ParseFile(fset, path, nil, parser.ParseComments) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | // consider only top-level struct type declarations with magic comment 133 | var res []StructInfo 134 | for _, decl := range fileNode.Decls { 135 | // ast.Print(fset, decl) 136 | 137 | gd, ok := decl.(*ast.GenDecl) 138 | if !ok { 139 | continue 140 | } 141 | for _, spec := range gd.Specs { 142 | ts, ok := spec.(*ast.TypeSpec) 143 | if !ok { 144 | continue 145 | } 146 | // ast.Print(fset, ts) 147 | 148 | // magic comment may be attached to "type Foo struct" (TypeSpec) 149 | // or to "type (" (GenDecl) 150 | doc := ts.Doc 151 | if doc == nil && len(gd.Specs) == 1 { 152 | doc = gd.Doc 153 | } 154 | if doc == nil { 155 | continue 156 | } 157 | // ast.Print(fset, doc) 158 | 159 | sm := magicReformComment.FindStringSubmatch(commentText(doc)) 160 | if len(sm) < 2 { 161 | continue 162 | } 163 | parts := strings.SplitN(sm[1], ".", 2) 164 | var schema string 165 | if len(parts) == 2 { 166 | schema = parts[0] 167 | } 168 | table := parts[len(parts)-1] 169 | 170 | str, ok := ts.Type.(*ast.StructType) 171 | if !ok { 172 | continue 173 | } 174 | if str.Incomplete { 175 | continue 176 | } 177 | // ast.Print(fset, str) 178 | 179 | s, err := parseStructTypeSpec(ts, str) 180 | if err != nil { 181 | return nil, err 182 | } 183 | s.SQLSchema = schema 184 | s.SQLName = table 185 | res = append(res, *s) 186 | } 187 | } 188 | 189 | return res, nil 190 | } 191 | -------------------------------------------------------------------------------- /parse/runtime.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | func objectGoType(t reflect.Type, structT reflect.Type) string { 10 | if t.Kind() == reflect.Ptr { 11 | return "*" + objectGoType(t.Elem(), structT) 12 | } 13 | 14 | s := t.String() 15 | 16 | // drop package name from qualified identifier if type is defined in the same package 17 | if strings.Contains(s, ".") && t.PkgPath() == structT.PkgPath() { 18 | s = strings.Join(strings.Split(s, ".")[1:], ".") 19 | } 20 | 21 | return s 22 | } 23 | 24 | // Object extracts struct information from given object. 25 | func Object(obj interface{}, schema, table string) (res *StructInfo, err error) { 26 | // convert any panic to error 27 | defer func() { 28 | p := recover() 29 | switch p := p.(type) { 30 | case error: 31 | err = p 32 | case nil: 33 | // nothing 34 | default: 35 | err = fmt.Errorf("%s", p) 36 | } 37 | }() 38 | 39 | t := reflect.ValueOf(obj).Elem().Type() 40 | res = &StructInfo{ 41 | Type: t.Name(), 42 | SQLSchema: schema, 43 | SQLName: table, 44 | PKFieldIndex: -1, 45 | } 46 | 47 | var n int 48 | for i := 0; i < t.NumField(); i++ { 49 | f := t.Field(i) 50 | tag := f.Tag.Get("reform") 51 | if tag == "" || tag == "-" { 52 | continue 53 | } 54 | 55 | // check for anonymous fields 56 | if f.Anonymous { 57 | return nil, fmt.Errorf(`reform: %s has anonymous field %s with "reform:" tag, it is not allowed`, res.Type, f.Name) 58 | } 59 | 60 | // check for exported name 61 | if f.PkgPath != "" { 62 | return nil, fmt.Errorf(`reform: %s has non-exported field %s with "reform:" tag, it is not allowed`, res.Type, f.Name) 63 | } 64 | 65 | // parse tag and type 66 | column, isPK := parseStructFieldTag(tag) 67 | if column == "" { 68 | return nil, fmt.Errorf(`reform: %s has field %s with invalid "reform:" tag value, it is not allowed`, res.Type, f.Name) 69 | } 70 | typ := objectGoType(f.Type, t) 71 | if isPK { 72 | if strings.HasPrefix(typ, "*") { 73 | return nil, fmt.Errorf(`reform: %s has pointer field %s with with "pk" label in "reform:" tag, it is not allowed`, res.Type, f.Name) 74 | } 75 | if strings.HasPrefix(typ, "[") { 76 | return nil, fmt.Errorf(`reform: %s has slice field %s with with "pk" label in "reform:" tag, it is not allowed`, res.Type, f.Name) 77 | } 78 | if res.PKFieldIndex >= 0 { 79 | return nil, fmt.Errorf(`reform: %s has field %s with with duplicate "pk" label in "reform:" tag (first used by %s), it is not allowed`, res.Type, f.Name, res.Fields[res.PKFieldIndex].Name) 80 | } 81 | } 82 | 83 | res.Fields = append(res.Fields, FieldInfo{ 84 | Name: f.Name, 85 | Type: typ, 86 | Column: column, 87 | }) 88 | if isPK { 89 | res.PKFieldIndex = n 90 | } 91 | n++ 92 | } 93 | 94 | if err = checkFields(res); err != nil { 95 | return nil, err 96 | } 97 | 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /querier.go: -------------------------------------------------------------------------------- 1 | package reform 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | // Querier performs queries and commands. 11 | type Querier struct { 12 | ctx context.Context 13 | dbtxCtx DBTXContext 14 | tag string 15 | Dialect 16 | Logger Logger 17 | } 18 | 19 | func newQuerier(ctx context.Context, dbtxCtx DBTXContext, tag string, dialect Dialect, logger Logger) *Querier { 20 | return &Querier{ 21 | ctx: ctx, 22 | dbtxCtx: dbtxCtx, 23 | tag: tag, 24 | Dialect: dialect, 25 | Logger: logger, 26 | } 27 | } 28 | 29 | func (q *Querier) clone() *Querier { 30 | return newQuerier(q.ctx, q.dbtxCtx, q.tag, q.Dialect, q.Logger) 31 | } 32 | 33 | func (q *Querier) logBefore(query string, args []interface{}) { 34 | if q.Logger != nil { 35 | q.Logger.Before(query, args) 36 | } 37 | } 38 | 39 | func (q *Querier) logAfter(query string, args []interface{}, d time.Duration, err error) { 40 | if q.Logger != nil { 41 | q.Logger.After(query, args, d, err) 42 | } 43 | } 44 | 45 | func (q *Querier) startQuery(command string) string { 46 | if q.tag == "" { 47 | return command 48 | } 49 | return command + " /* " + q.tag + " */" 50 | } 51 | 52 | // Tag returns Querier's tag. Default tag is empty. 53 | func (q *Querier) Tag() string { 54 | return q.tag 55 | } 56 | 57 | // WithTag returns a copy of Querier with set tag. Returned Querier is tied to the same DB or TX. 58 | // See Tagging section in documentation for details. 59 | func (q *Querier) WithTag(format string, args ...interface{}) *Querier { 60 | newQ := q.clone() 61 | if len(args) == 0 { 62 | newQ.tag = format 63 | } else { 64 | newQ.tag = fmt.Sprintf(format, args...) 65 | } 66 | return newQ 67 | } 68 | 69 | // QualifiedView returns quoted qualified view name. 70 | func (q *Querier) QualifiedView(view View) string { 71 | v := q.QuoteIdentifier(view.Name()) 72 | if view.Schema() != "" { 73 | v = q.QuoteIdentifier(view.Schema()) + "." + v 74 | } 75 | return v 76 | } 77 | 78 | // Context returns Querier's context. Default context is context.Background(). 79 | func (q *Querier) Context() context.Context { 80 | return q.ctx 81 | } 82 | 83 | // WithContext returns a copy of Querier with set context. Returned Querier is tied to the same DB or TX. 84 | // See Context section in documentation for details. 85 | func (q *Querier) WithContext(ctx context.Context) *Querier { 86 | newQ := q.clone() 87 | newQ.ctx = ctx 88 | return newQ 89 | } 90 | 91 | // QualifiedColumns returns a slice of quoted qualified column names for given view. 92 | func (q *Querier) QualifiedColumns(view View) []string { 93 | v := q.QualifiedView(view) 94 | res := view.Columns() 95 | for i := 0; i < len(res); i++ { 96 | res[i] = v + "." + q.QuoteIdentifier(res[i]) 97 | } 98 | return res 99 | } 100 | 101 | // Exec executes a query without returning any rows. 102 | // The args are for any placeholder parameters in the query. 103 | func (q *Querier) Exec(query string, args ...interface{}) (sql.Result, error) { 104 | q.logBefore(query, args) 105 | start := time.Now() 106 | res, err := q.dbtxCtx.ExecContext(q.ctx, query, args...) 107 | q.logAfter(query, args, time.Since(start), err) 108 | return res, err 109 | } 110 | 111 | // ExecContext just calls q.WithContext(ctx).Exec(query, args...), and that form should be used instead. 112 | // This method exists to satisfy various standard interfaces for advanced use-cases. 113 | func (q *Querier) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { 114 | return q.WithContext(ctx).Exec(query, args...) 115 | } 116 | 117 | // Query executes a query that returns rows, typically a SELECT. 118 | // The args are for any placeholder parameters in the query. 119 | func (q *Querier) Query(query string, args ...interface{}) (*sql.Rows, error) { 120 | q.logBefore(query, args) 121 | start := time.Now() 122 | rows, err := q.dbtxCtx.QueryContext(q.ctx, query, args...) 123 | q.logAfter(query, args, time.Since(start), err) 124 | return rows, err 125 | } 126 | 127 | // QueryContext just calls q.WithContext(ctx).Query(query, args...), and that form should be used instead. 128 | // This method exists to satisfy various standard interfaces for advanced use-cases. 129 | func (q *Querier) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { 130 | return q.WithContext(ctx).Query(query, args...) 131 | } 132 | 133 | // QueryRow executes a query that is expected to return at most one row. 134 | // QueryRow always returns a non-nil value. Errors are deferred until Row's Scan method is called. 135 | func (q *Querier) QueryRow(query string, args ...interface{}) *sql.Row { 136 | q.logBefore(query, args) 137 | start := time.Now() 138 | row := q.dbtxCtx.QueryRowContext(q.ctx, query, args...) 139 | q.logAfter(query, args, time.Since(start), nil) 140 | return row 141 | } 142 | 143 | // QueryRowContext just calls q.WithContext(ctx).QueryRow(query, args...), and that form should be used instead. 144 | // This method exists to satisfy various standard interfaces for advanced use-cases. 145 | func (q *Querier) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { 146 | return q.WithContext(ctx).QueryRow(query, args...) 147 | } 148 | 149 | // check interfaces 150 | var ( 151 | _ DBTX = (*Querier)(nil) 152 | _ DBTXContext = (*Querier)(nil) 153 | ) 154 | -------------------------------------------------------------------------------- /querier_examples_test.go: -------------------------------------------------------------------------------- 1 | package reform_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/AlekSi/pointer" 13 | "github.com/brianvoe/gofakeit/v6" 14 | 15 | "gopkg.in/reform.v1" 16 | "gopkg.in/reform.v1/dialects/postgresql" 17 | . "gopkg.in/reform.v1/internal/test/models" 18 | ) 19 | 20 | // This example should be synced with README. 21 | 22 | func Example() { 23 | // Use reform.NewDB to create DB. 24 | 25 | // Save record (performs INSERT or UPDATE). 26 | person := &Person{ 27 | Name: "Alexey Palazhchenko", 28 | Email: pointer.ToString("alexey.palazhchenko@gmail.com"), 29 | } 30 | if err := DB.Save(person); err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | // ID is filled by Save. 35 | person2, err := DB.FindByPrimaryKeyFrom(PersonTable, person.ID) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | fmt.Println(person2.(*Person).Name) 40 | 41 | // Delete record. 42 | if err = DB.Delete(person); err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | // Find records by IDs. 47 | persons, err := DB.FindAllFrom(PersonTable, "id", 1, 2) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | for _, p := range persons { 52 | fmt.Println(p) 53 | } 54 | // Output: 55 | // Alexey Palazhchenko 56 | // ID: 1 (int32), GroupID: 65534 (*int32), Name: `Denis Mills` (string), Email: (*string), CreatedAt: 2009-11-10 23:00:00 +0000 UTC (time.Time), UpdatedAt: (*time.Time) 57 | // ID: 2 (int32), GroupID: 65534 (*int32), Name: `Garrick Muller` (string), Email: `muller_garrick@example.com` (*string), CreatedAt: 2009-12-12 12:34:56 +0000 UTC (time.Time), UpdatedAt: (*time.Time) 58 | } 59 | 60 | func ExampleNewDB() { 61 | // Get *sql.DB as usual. PostgreSQL example: 62 | sqlDB, err := sql.Open("postgres", "postgres://127.0.0.1:5432/database") 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | defer sqlDB.Close() 67 | 68 | // Use new *log.Logger for logging. 69 | logger := log.New(os.Stderr, "SQL: ", log.Flags()) 70 | 71 | // Create *reform.DB instance with simple logger. 72 | // Any Printf-like function (fmt.Printf, log.Printf, testing.T.Logf, etc) can be used with NewPrintfLogger. 73 | // Change dialect for other databases. 74 | _ = reform.NewDB(sqlDB, postgresql.Dialect, reform.NewPrintfLogger(logger.Printf)) 75 | } 76 | 77 | func ExampleQuerier_WithTag() { 78 | id := "baron" 79 | _, _ = DB.WithTag("GetProject:%v", id).FindByPrimaryKeyFrom(ProjectTable, id) 80 | } 81 | 82 | func ExampleQuerier_WithContext() { 83 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 84 | defer cancel() 85 | _, _ = DB.WithContext(ctx).SelectAllFrom(ProjectTable, "") 86 | } 87 | 88 | func ExampleQuerier_SelectRows() { 89 | tail := fmt.Sprintf("WHERE created_at < %s ORDER BY id", DB.Placeholder(1)) 90 | y2010 := time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC) 91 | rows, err := DB.SelectRows(PersonTable, tail, y2010) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | defer rows.Close() 96 | 97 | for { 98 | var person Person 99 | if err = DB.NextRow(&person, rows); err != nil { 100 | break 101 | } 102 | fmt.Println(person) 103 | } 104 | if err != reform.ErrNoRows { 105 | log.Fatal(err) 106 | } 107 | // Output: 108 | // ID: 1 (int32), GroupID: 65534 (*int32), Name: `Denis Mills` (string), Email: (*string), CreatedAt: 2009-11-10 23:00:00 +0000 UTC (time.Time), UpdatedAt: (*time.Time) 109 | // ID: 2 (int32), GroupID: 65534 (*int32), Name: `Garrick Muller` (string), Email: `muller_garrick@example.com` (*string), CreatedAt: 2009-12-12 12:34:56 +0000 UTC (time.Time), UpdatedAt: (*time.Time) 110 | } 111 | 112 | func ExampleQuerier_SelectOneTo() { 113 | var person Person 114 | tail := fmt.Sprintf("WHERE created_at < %s ORDER BY id", DB.Placeholder(1)) 115 | y2010 := time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC) 116 | if err := DB.SelectOneTo(&person, tail, y2010); err != nil { 117 | log.Fatal(err) 118 | } 119 | fmt.Println(person) 120 | // Output: 121 | // ID: 1 (int32), GroupID: 65534 (*int32), Name: `Denis Mills` (string), Email: (*string), CreatedAt: 2009-11-10 23:00:00 +0000 UTC (time.Time), UpdatedAt: (*time.Time) 122 | } 123 | 124 | var persons = []reform.Struct{ 125 | &Person{ 126 | Name: "Alexey Palazhchenko", 127 | Email: pointer.ToString("alexey.palazhchenko@gmail.com"), 128 | }, 129 | &Person{ 130 | Name: gofakeit.Name(), 131 | Email: pointer.ToString(gofakeit.Email()), 132 | }, 133 | &Person{ 134 | Name: gofakeit.Name(), 135 | Email: pointer.ToString(gofakeit.Email()), 136 | }, 137 | &Person{ 138 | Name: gofakeit.Name(), 139 | Email: pointer.ToString(gofakeit.Email()), 140 | }, 141 | &Person{ 142 | Name: gofakeit.Name(), 143 | Email: pointer.ToString(gofakeit.Email()), 144 | }, 145 | } 146 | 147 | func ExampleQuerier_InsertMulti() { 148 | // insert up to 3 structs at once 149 | const batchSize = 3 150 | for i := 0; i < len(persons)/batchSize+1; i++ { 151 | low := i * batchSize 152 | high := (i + 1) * batchSize 153 | if high > len(persons) { 154 | high = len(persons) 155 | } 156 | batch := persons[low:high] 157 | 158 | if err := DB.InsertMulti(batch...); err != nil { 159 | log.Fatal(err) 160 | } 161 | fmt.Printf("Inserted %d persons\n", len(batch)) 162 | } 163 | 164 | // note that ID is not filled 165 | fmt.Println(persons[0].(*Person).ID, persons[0].(*Person).Name) 166 | // Output: 167 | // Inserted 3 persons 168 | // Inserted 2 persons 169 | // 0 Alexey Palazhchenko 170 | } 171 | 172 | func ExampleQuerier_Query() { 173 | columns := DB.QualifiedColumns(PersonTable) 174 | columns = append(columns, DB.QualifiedColumns(PersonProjectView)...) 175 | columns = append(columns, DB.QualifiedColumns(ProjectTable)...) 176 | query := fmt.Sprintf(` 177 | SELECT %s 178 | FROM people 179 | INNER JOIN person_project ON people.id = person_project.person_id 180 | INNER JOIN projects ON person_project.project_id = projects.id 181 | ORDER BY person_id, project_id; 182 | `, strings.Join(columns, ", ")) 183 | rows, err := DB.Query(query) 184 | if err != nil { 185 | log.Fatal(err) 186 | } 187 | defer rows.Close() 188 | 189 | for rows.Next() { 190 | var person Person 191 | var personProject PersonProject 192 | var project Project 193 | pointers := person.Pointers() 194 | pointers = append(pointers, personProject.Pointers()...) 195 | pointers = append(pointers, project.Pointers()...) 196 | if err = rows.Scan(pointers...); err != nil { 197 | log.Print(err) 198 | } 199 | if err = person.AfterFind(); err != nil { 200 | log.Fatal(err) 201 | } 202 | if err = project.AfterFind(); err != nil { 203 | log.Fatal(err) 204 | } 205 | fmt.Printf("%s - %s\n", person.Name, project.Name) 206 | } 207 | if err = rows.Err(); err != nil { 208 | log.Fatal(err) 209 | } 210 | // Output: 211 | // Noble Schumm - Vicious Baron 212 | // Elfrieda Abbott - Vicious Baron 213 | // Elfrieda Abbott - Thirsty Queen 214 | // Elfrieda Abbott - Vicious Baron 215 | // Elfrieda Abbott - Thirsty Queen 216 | // Elfrieda Abbott - Kosher Traveler 217 | } 218 | -------------------------------------------------------------------------------- /querier_selects.go: -------------------------------------------------------------------------------- 1 | package reform 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // NextRow scans next result row from rows to str. If str implements AfterFinder, it also calls AfterFind(). 10 | // It is caller's responsibility to call rows.Close(). 11 | // 12 | // If there is no next result row, it returns ErrNoRows. It also may return rows.Err(), rows.Scan() 13 | // and AfterFinder errors. 14 | // 15 | // See SelectRows example for idiomatic usage. 16 | func (q *Querier) NextRow(str Struct, rows *sql.Rows) error { 17 | var err error 18 | next := rows.Next() 19 | if !next { 20 | err = rows.Err() 21 | if err == nil { 22 | err = ErrNoRows 23 | } 24 | return err 25 | } 26 | 27 | if err = rows.Scan(str.Pointers()...); err != nil { 28 | return err 29 | } 30 | 31 | if af, ok := str.(AfterFinder); ok { 32 | err = af.AfterFind() 33 | } 34 | return err 35 | } 36 | 37 | // selectQuery returns full SELECT query for given view and tail. 38 | func (q *Querier) selectQuery(view View, tail string, limit1 bool) string { 39 | query := q.startQuery("SELECT") 40 | 41 | if limit1 && q.SelectLimitMethod() == SelectTop { 42 | query += " TOP 1" 43 | } 44 | 45 | return fmt.Sprintf("%s %s FROM %s %s", 46 | query, strings.Join(q.QualifiedColumns(view), ", "), q.QualifiedView(view), tail) 47 | } 48 | 49 | // SelectOneTo queries str's View with tail and args and scans first result to str. 50 | // If str implements AfterFinder, it also calls AfterFind(). 51 | // 52 | // If there are no rows in result, it returns ErrNoRows. It also may return QueryRow(), Scan() 53 | // and AfterFinder errors. 54 | func (q *Querier) SelectOneTo(str Struct, tail string, args ...interface{}) error { 55 | query := q.selectQuery(str.View(), tail, true) 56 | if err := q.QueryRow(query, args...).Scan(str.Pointers()...); err != nil { 57 | return err 58 | } 59 | 60 | if af, ok := str.(AfterFinder); ok { 61 | return af.AfterFind() 62 | } 63 | return nil 64 | } 65 | 66 | // SelectOneFrom queries view with tail and args and scans first result to new Struct str. 67 | // If str implements AfterFinder, it also calls AfterFind(). 68 | // 69 | // If there are no rows in result, it returns nil, ErrNoRows. It also may return QueryRow(), Scan() 70 | // and AfterFinder errors. 71 | func (q *Querier) SelectOneFrom(view View, tail string, args ...interface{}) (Struct, error) { 72 | str := view.NewStruct() 73 | if err := q.SelectOneTo(str, tail, args...); err != nil { 74 | return nil, err 75 | } 76 | return str, nil 77 | } 78 | 79 | // SelectRows queries view with tail and args and returns rows. They can then be iterated with NextRow(). 80 | // It is caller's responsibility to call rows.Close(). 81 | // 82 | // In case of error rows will be nil. Error is never ErrNoRows. 83 | // 84 | // See example for idiomatic usage. 85 | func (q *Querier) SelectRows(view View, tail string, args ...interface{}) (*sql.Rows, error) { 86 | query := q.selectQuery(view, tail, false) 87 | return q.Query(query, args...) 88 | } 89 | 90 | // SelectAllFrom queries view with tail and args and returns a slice of new Structs. 91 | // If view's Struct implements AfterFinder, it also calls AfterFind(). 92 | // 93 | // In case of query error slice will be nil. If error is encountered during iteration, 94 | // partial result and error will be returned. Error is never ErrNoRows. 95 | func (q *Querier) SelectAllFrom(view View, tail string, args ...interface{}) (structs []Struct, err error) { 96 | var rows *sql.Rows 97 | rows, err = q.SelectRows(view, tail, args...) 98 | if err != nil { 99 | return 100 | } 101 | defer func() { 102 | e := rows.Close() 103 | if err == nil { 104 | err = e 105 | } 106 | }() 107 | 108 | for { 109 | str := view.NewStruct() 110 | if err = q.NextRow(str, rows); err != nil { 111 | break 112 | } 113 | 114 | structs = append(structs, str) 115 | } 116 | if err == ErrNoRows { 117 | err = nil 118 | } 119 | return 120 | } 121 | 122 | // findTail returns a tail of SELECT query for given view, column and arg. 123 | func (q *Querier) findTail(view string, column string, arg interface{}, limit1 bool) (tail string, needArg bool) { 124 | qi := q.QuoteIdentifier(view) + "." + q.QuoteIdentifier(column) 125 | if arg == nil { 126 | tail = fmt.Sprintf("WHERE %s IS NULL", qi) 127 | } else { 128 | tail = fmt.Sprintf("WHERE %s = %s", qi, q.Placeholder(1)) 129 | needArg = true 130 | } 131 | 132 | if limit1 && q.SelectLimitMethod() == Limit { 133 | tail += " LIMIT 1" 134 | } 135 | 136 | return 137 | } 138 | 139 | // FindOneTo queries str's View with column and arg and scans first result to str. 140 | // If str implements AfterFinder, it also calls AfterFind(). 141 | // 142 | // If there are no rows in result, it returns ErrNoRows. It also may return QueryRow(), Scan() 143 | // and AfterFinder errors. 144 | func (q *Querier) FindOneTo(str Struct, column string, arg interface{}) error { 145 | tail, needArg := q.findTail(str.View().Name(), column, arg, true) 146 | if needArg { 147 | return q.SelectOneTo(str, tail, arg) 148 | } 149 | return q.SelectOneTo(str, tail) 150 | } 151 | 152 | // FindOneFrom queries view with column and arg and scans first result to new Struct str. 153 | // If str implements AfterFinder, it also calls AfterFind(). 154 | // 155 | // If there are no rows in result, it returns nil, ErrNoRows. It also may return QueryRow(), Scan() 156 | // and AfterFinder errors. 157 | func (q *Querier) FindOneFrom(view View, column string, arg interface{}) (Struct, error) { 158 | tail, needArg := q.findTail(view.Name(), column, arg, true) 159 | if needArg { 160 | return q.SelectOneFrom(view, tail, arg) 161 | } 162 | return q.SelectOneFrom(view, tail) 163 | } 164 | 165 | // FindRows queries view with column and arg and returns rows. They can then be iterated with NextRow(). 166 | // It is caller's responsibility to call rows.Close(). 167 | // 168 | // In case of error rows will be nil. Error is never ErrNoRows. 169 | // 170 | // See SelectRows example for idiomatic usage. 171 | func (q *Querier) FindRows(view View, column string, arg interface{}) (*sql.Rows, error) { 172 | tail, needArg := q.findTail(view.Name(), column, arg, false) 173 | if needArg { 174 | return q.SelectRows(view, tail, arg) 175 | } 176 | return q.SelectRows(view, tail) 177 | } 178 | 179 | // FindAllFrom queries view with column and args and returns a slice of new Structs. 180 | // If view's Struct implements AfterFinder, it also calls AfterFind(). 181 | // 182 | // In case of query error slice will be nil. If error is encountered during iteration, 183 | // partial result and error will be returned. Error is never ErrNoRows. 184 | func (q *Querier) FindAllFrom(view View, column string, args ...interface{}) ([]Struct, error) { 185 | p := strings.Join(q.Placeholders(1, len(args)), ", ") 186 | qi := q.QualifiedView(view) + "." + q.QuoteIdentifier(column) 187 | tail := fmt.Sprintf("WHERE %s IN (%s)", qi, p) 188 | return q.SelectAllFrom(view, tail, args...) 189 | } 190 | 191 | // FindByPrimaryKeyTo queries record's Table with primary key and scans first result to record. 192 | // If record implements AfterFinder, it also calls AfterFind(). 193 | // 194 | // If there are no rows in result, it returns ErrNoRows. It also may return QueryRow(), Scan() 195 | // and AfterFinder errors. 196 | func (q *Querier) FindByPrimaryKeyTo(record Record, pk interface{}) error { 197 | table := record.Table() 198 | return q.FindOneTo(record, table.Columns()[table.PKColumnIndex()], pk) 199 | } 200 | 201 | // FindByPrimaryKeyFrom queries table with primary key and scans first result to new Record. 202 | // If record implements AfterFinder, it also calls AfterFind(). 203 | // 204 | // If there are no rows in result, it returns nil, ErrNoRows. It also may return QueryRow(), Scan() 205 | // and AfterFinder errors. 206 | func (q *Querier) FindByPrimaryKeyFrom(table Table, pk interface{}) (Record, error) { 207 | record := table.NewRecord() 208 | if err := q.FindOneTo(record, table.Columns()[table.PKColumnIndex()], pk); err != nil { 209 | return nil, err 210 | } 211 | return record, nil 212 | } 213 | 214 | // Reload is a shortcut for FindByPrimaryKeyTo for given record. 215 | func (q *Querier) Reload(record Record) error { 216 | return q.FindByPrimaryKeyTo(record, record.PKValue()) 217 | } 218 | 219 | // Count queries view with tail and args and returns a number (COUNT(*)) of matching rows. 220 | func (q *Querier) Count(view View, tail string, args ...interface{}) (int, error) { 221 | query := fmt.Sprintf("%s COUNT(*) FROM %s %s", q.startQuery("SELECT"), q.QualifiedView(view), tail) 222 | var count int 223 | if err := q.QueryRow(query, args...).Scan(&count); err != nil { 224 | return 0, err 225 | } 226 | return count, nil 227 | } 228 | -------------------------------------------------------------------------------- /querier_test.go: -------------------------------------------------------------------------------- 1 | package reform_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "database/sql/driver" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | mssqlDriver "github.com/denisenkom/go-mssqldb" 12 | mysqlDriver "github.com/go-sql-driver/mysql" 13 | "github.com/jackc/pgx" 14 | "github.com/jackc/pgx/stdlib" 15 | "github.com/lib/pq" 16 | sqlite3Driver "github.com/mattn/go-sqlite3" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | 20 | "gopkg.in/reform.v1" 21 | "gopkg.in/reform.v1/dialects/mssql" //nolint:staticcheck 22 | "gopkg.in/reform.v1/dialects/mysql" 23 | "gopkg.in/reform.v1/dialects/postgresql" 24 | "gopkg.in/reform.v1/dialects/sqlite3" 25 | "gopkg.in/reform.v1/dialects/sqlserver" 26 | ) 27 | 28 | type ctxKey string 29 | 30 | func sleepQuery(t testing.TB, q *reform.Querier, d time.Duration) string { 31 | switch q.Dialect { 32 | case postgresql.Dialect: 33 | return fmt.Sprintf("SELECT pg_sleep(%f)", d.Seconds()) 34 | case mysql.Dialect: 35 | return fmt.Sprintf("SELECT SLEEP(%f)", d.Seconds()) 36 | case sqlite3.Dialect: 37 | return fmt.Sprintf("SELECT sleep(%d)", d.Nanoseconds()) 38 | case mssql.Dialect: //nolint:staticcheck 39 | fallthrough 40 | case sqlserver.Dialect: 41 | sec := int(d.Seconds()) 42 | msec := (d - time.Duration(sec)*time.Second) / time.Millisecond 43 | return fmt.Sprintf("WAITFOR DELAY '00:00:%02d.%03d'", sec, msec) 44 | default: 45 | t.Fatalf("No sleep for %s.", q.Dialect) 46 | return "" 47 | } 48 | } 49 | 50 | func TestExecWithContext(t *testing.T) { 51 | db, tx := setupTX(t) 52 | defer teardown(t, db) 53 | 54 | assert.Equal(t, context.Background(), db.Context()) 55 | assert.Equal(t, context.Background(), tx.Context()) 56 | 57 | dbDriver := db.DBInterface().(*sql.DB).Driver() 58 | const sleep = 200 * time.Millisecond 59 | const ctxTimeout = 100 * time.Millisecond 60 | query := sleepQuery(t, tx.Querier, sleep) 61 | ctx, cancel := context.WithTimeout(context.WithValue(context.Background(), ctxKey("k"), "exec"), ctxTimeout) 62 | defer cancel() 63 | 64 | q := tx.WithContext(ctx) 65 | assert.Equal(t, ctx, q.Context()) 66 | start := time.Now() 67 | _, err := q.Exec(query) 68 | dur := time.Since(start) 69 | 70 | switch dbDriver.(type) { 71 | case *sqlite3Driver.SQLiteDriver: 72 | // sqlite3 driver does not support query cancelation 73 | assert.Equal(t, context.DeadlineExceeded, err) 74 | assert.True(t, dur >= sleep, "sqlite3: failed comparison: dur >= sleep") 75 | assert.True(t, dur >= ctxTimeout, "sqlite3: failed comparison: dur >= ctxTimeout") 76 | 77 | default: 78 | assert.Error(t, err) 79 | assert.True(t, dur < sleep, "failed comparison: dur < sleep") 80 | assert.True(t, dur > ctxTimeout, "failed comparison: dur > ctxTimeout") 81 | 82 | // check specific error type 83 | switch dbDriver.(type) { 84 | case *pq.Driver: 85 | require.EqualError(t, err, "pq: canceling statement due to user request") 86 | pgErr := err.(*pq.Error) 87 | assert.Equal(t, "ERROR", pgErr.Severity) 88 | assert.Equal(t, pq.ErrorCode("57014"), pgErr.Code) 89 | assert.Equal(t, "ProcessInterrupts", pgErr.Routine) 90 | case *stdlib.Driver: 91 | assert.Equal(t, context.DeadlineExceeded, err) 92 | case *mysqlDriver.MySQLDriver: 93 | assert.Equal(t, context.DeadlineExceeded, err) 94 | case *mssqlDriver.Driver: 95 | assert.Equal(t, context.DeadlineExceeded, err) 96 | default: 97 | t.Fatalf("q.Exec: unhandled driver %T. err = %s", dbDriver, err) 98 | } 99 | } 100 | 101 | // context should not be modified 102 | assert.Equal(t, context.Background(), db.Context()) 103 | assert.Equal(t, context.Background(), tx.Context()) 104 | 105 | // check q with expired timeout 106 | var res int 107 | err = q.QueryRow("SELECT 1").Scan(&res) 108 | assert.Equal(t, context.DeadlineExceeded, err) 109 | require.Equal(t, 0, res) 110 | 111 | // check the same tx without timeout 112 | err = tx.QueryRow("SELECT 1").Scan(&res) 113 | switch dbDriver.(type) { 114 | case *pq.Driver: 115 | require.EqualError(t, err, "pq: current transaction is aborted, commands ignored until end of transaction block") 116 | pgErr := err.(*pq.Error) 117 | assert.Equal(t, "ERROR", pgErr.Severity) 118 | assert.Equal(t, pq.ErrorCode("25P02"), pgErr.Code) 119 | assert.Equal(t, "exec_simple_query", pgErr.Routine) 120 | 121 | assert.Equal(t, 0, res) 122 | 123 | case *stdlib.Driver: 124 | assert.EqualError(t, err, "ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE 25P02)") 125 | pgErr := err.(pgx.PgError) 126 | assert.Equal(t, "ERROR", pgErr.Severity) 127 | assert.Equal(t, "25P02", pgErr.Code) 128 | assert.Equal(t, "exec_parse_message", pgErr.Routine) 129 | 130 | assert.Equal(t, 0, res) 131 | 132 | case *mysqlDriver.MySQLDriver: 133 | assert.Equal(t, driver.ErrBadConn, err) 134 | assert.Equal(t, 0, res) 135 | 136 | case *sqlite3Driver.SQLiteDriver: 137 | assert.NoError(t, err) 138 | assert.Equal(t, 1, res) 139 | 140 | case *mssqlDriver.Driver: 141 | assert.Equal(t, driver.ErrBadConn, err) 142 | assert.Equal(t, 0, res) 143 | 144 | default: 145 | t.Fatalf("tx.QueryRow: unhandled driver %T. err = %s", dbDriver, err) 146 | } 147 | 148 | err = tx.Rollback() 149 | switch dbDriver.(type) { 150 | case *pq.Driver: 151 | assert.NoError(t, err) 152 | case *stdlib.Driver: 153 | assert.NoError(t, err) 154 | case *mysqlDriver.MySQLDriver: 155 | assert.Equal(t, mysqlDriver.ErrInvalidConn, err) 156 | case *sqlite3Driver.SQLiteDriver: 157 | assert.NoError(t, err) 158 | case *mssqlDriver.Driver: 159 | assert.Equal(t, driver.ErrBadConn, err) 160 | default: 161 | t.Fatalf("tx.Rollback: unhandled driver %T. err = %s", dbDriver, err) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /reform-db/base_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | 8 | "gopkg.in/reform.v1" 9 | "gopkg.in/reform.v1/internal" 10 | "gopkg.in/reform.v1/internal/test" 11 | ) 12 | 13 | type ReformDBSuite struct { 14 | suite.Suite 15 | db *reform.DB 16 | } 17 | 18 | func TestReformDBSuite(t *testing.T) { 19 | suite.Run(t, new(ReformDBSuite)) 20 | } 21 | 22 | func (s *ReformDBSuite) SetupSuite() { 23 | if testing.Short() { 24 | s.T().Skip("skipping in short mode") 25 | } 26 | 27 | logger = internal.NewLogger("reform-db-test: ", true) 28 | 29 | s.db = test.ConnectToTestDB() 30 | s.db.Querier = s.db.WithTag("reform-db-test") 31 | } 32 | 33 | func (s *ReformDBSuite) SetupTest() { 34 | pl := reform.NewPrintfLogger(s.T().Logf) 35 | pl.LogTypes = true 36 | s.db.Logger = pl 37 | } 38 | -------------------------------------------------------------------------------- /reform-db/cmd_exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | 10 | "gopkg.in/reform.v1" 11 | ) 12 | 13 | var ( 14 | execFlags = flag.NewFlagSet("exec", flag.ExitOnError) 15 | execSplitF = execFlags.Bool("split", false, "Split statements by semicolon; does not handles them in string literals") 16 | ) 17 | 18 | func init() { 19 | execFlags.Usage = func() { 20 | fmt.Fprintf(os.Stderr, "`exec` command executes SQL queries from given files or stdin.\n\n") 21 | fmt.Fprintf(os.Stderr, "Usage:\n") 22 | fmt.Fprintf(os.Stderr, " %s [global flags] exec [exec flags] [file names]\n\n", os.Args[0]) 23 | fmt.Fprintf(os.Stderr, "Global flags:\n") 24 | flag.PrintDefaults() 25 | fmt.Fprintf(os.Stderr, "\nExec flags:\n") 26 | execFlags.PrintDefaults() 27 | 28 | // TODO mention -split flag 29 | fmt.Fprintf(os.Stderr, ` 30 | Each file's content is executed as a single query. If it contains multiple 31 | statements, make sure SQL driver supports them. If file names are not given, 32 | a query is read from stdin until EOF, then executed. 33 | `) 34 | } 35 | } 36 | 37 | // readFiles reads queries from given files, or from stdin, if files are not given 38 | func readFiles(files []string, split bool) (queries []string) { 39 | // read stdin 40 | if len(files) == 0 { 41 | logger.Debugf("no files are given, reading stdin") 42 | b, err := ioutil.ReadAll(os.Stdin) 43 | if err != nil { 44 | logger.Fatalf("failed to read stdin: %s", err) 45 | } 46 | b = bytes.TrimSpace(b) 47 | if len(b) > 0 { 48 | if split { 49 | s := bytes.Split(b, []byte(";")) 50 | for _, ss := range s { 51 | ss = bytes.TrimSpace(ss) 52 | if len(ss) > 0 { 53 | queries = append(queries, string(ss)) 54 | } 55 | } 56 | } else { 57 | queries = append(queries, string(b)) 58 | } 59 | } 60 | 61 | return 62 | } 63 | 64 | // read files 65 | for _, f := range files { 66 | logger.Debugf("reading file %s", f) 67 | b, err := ioutil.ReadFile(f) //nolint:gosec 68 | if err != nil { 69 | logger.Fatalf("failed to read file %s: %s", f, err) 70 | } 71 | b = bytes.TrimSpace(b) 72 | if len(b) > 0 { 73 | if split { 74 | s := bytes.Split(b, []byte(";")) 75 | for _, ss := range s { 76 | ss = bytes.TrimSpace(ss) 77 | if len(ss) > 0 { 78 | queries = append(queries, string(ss)) 79 | } 80 | } 81 | } else { 82 | queries = append(queries, string(b)) 83 | } 84 | } 85 | } 86 | 87 | return 88 | } 89 | 90 | // cmdExec implements exec command. 91 | func cmdExec(db *reform.DB, files []string) { 92 | queries := readFiles(files, *execSplitF) 93 | for _, q := range queries { 94 | if _, err := db.Exec(q); err != nil { 95 | logger.Fatalf("failed to execute %s: %s", q, err) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /reform-db/cmd_init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "go/build" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "gopkg.in/reform.v1" 14 | "gopkg.in/reform.v1/dialects/mssql" //nolint:staticcheck 15 | "gopkg.in/reform.v1/dialects/mysql" 16 | "gopkg.in/reform.v1/dialects/postgresql" 17 | "gopkg.in/reform.v1/dialects/sqlite3" 18 | "gopkg.in/reform.v1/dialects/sqlserver" 19 | "gopkg.in/reform.v1/parse" 20 | ) 21 | 22 | var ( 23 | initFlags = flag.NewFlagSet("init", flag.ExitOnError) 24 | gofmtF = initFlags.Bool("gofmt", true, "Format with gofmt") 25 | ) 26 | 27 | func init() { 28 | initFlags.Usage = func() { 29 | fmt.Fprintf(os.Stderr, "`init` generates Go model files for existing database schema.\n\n") 30 | fmt.Fprintf(os.Stderr, "Usage:\n") 31 | fmt.Fprintf(os.Stderr, " %s [global flags] init [init flags] [directory]\n\n", os.Args[0]) 32 | fmt.Fprintf(os.Stderr, "Global flags:\n") 33 | flag.PrintDefaults() 34 | fmt.Fprintf(os.Stderr, "\nInit flags:\n") 35 | initFlags.PrintDefaults() 36 | fmt.Fprintf(os.Stderr, ` 37 | It uses information_schema or similar RDBMS mechanism to inspect database 38 | structure. For each table, it generates a single file with single struct type 39 | definition with fields, types, and tags. Generated code then should be checked 40 | and edited manually. 41 | `) 42 | } 43 | } 44 | 45 | func gofmt(path string) { 46 | if *gofmtF { 47 | cmd := exec.Command("gofmt", "-s", "-w", path) 48 | logger.Debugf(strings.Join(cmd.Args, " ")) 49 | b, err := cmd.CombinedOutput() 50 | if err != nil { 51 | logger.Fatalf("gofmt error: %s", err) 52 | } 53 | logger.Debugf("gofmt output: %s", b) 54 | } 55 | } 56 | 57 | type typeFunc func(sqlType string, nullable bool) (typ string, pack string, comment string) 58 | 59 | // maybePointer returns *typ if nullable, typ otherwise. 60 | func maybePointer(typ string, nullable bool) string { 61 | if nullable { 62 | return "*" + typ 63 | } 64 | return typ 65 | } 66 | 67 | // convertName converts snake_case name of table or column to CamelCase name of type or field. 68 | // It also handles "_id" to "ID" conversion as a typical special case. 69 | func convertName(sqlName string) string { 70 | fields := strings.Fields(strings.ReplaceAll(sqlName, "_", " ")) 71 | res := make([]string, len(fields)) 72 | for i, f := range fields { 73 | if f == "id" { 74 | res[i] = "ID" 75 | } else { 76 | res[i] = strings.Title(f) 77 | } 78 | } 79 | return strings.Join(res, "") 80 | } 81 | 82 | // getPrimaryKeyColumn returns single primary key column for given table, or nil. 83 | func getPrimaryKeyColumn(db *reform.DB, catalog, schema, tableName string) *keyColumnUsage { 84 | using := []string{ 85 | "table_catalog", "table_schema", "table_name", 86 | "constraint_catalog", "constraint_schema", "constraint_name", 87 | } 88 | if db.Dialect == mysql.Dialect { 89 | // MySQL doesn't have table_catalog in table_constraints 90 | using = using[1:] 91 | } 92 | for i, u := range using { 93 | using[i] = fmt.Sprintf("key_column_usage.%s = table_constraints.%s", u, u) 94 | } 95 | q := fmt.Sprintf( 96 | `SELECT column_name, ordinal_position FROM information_schema.key_column_usage 97 | INNER JOIN information_schema.table_constraints ON %s 98 | WHERE key_column_usage.table_catalog = %s AND 99 | key_column_usage.table_schema = %s AND 100 | key_column_usage.table_name = %s AND 101 | constraint_type = 'PRIMARY KEY' 102 | ORDER BY ordinal_position DESC`, 103 | strings.Join(using, " AND "), db.Placeholder(1), db.Placeholder(2), db.Placeholder(3), 104 | ) 105 | 106 | // get only the first row (with the maximum ordinal_position) 107 | row := db.QueryRow(q, catalog, schema, tableName) 108 | var key keyColumnUsage 109 | if err := row.Scan(key.Pointers()...); err != nil { 110 | if errors.Is(err, reform.ErrNoRows) { 111 | return nil 112 | } 113 | logger.Fatalf("%s", err) 114 | } 115 | 116 | if key.OrdinalPosition > 1 { 117 | logger.Printf( 118 | "Composite primary keys are not supported (found %d columns), skipping it for table %s.", 119 | key.OrdinalPosition, tableName, 120 | ) 121 | return nil 122 | } 123 | 124 | return &key 125 | } 126 | 127 | // initModelsInformationSchema returns structs from database with information_schema. 128 | func initModelsInformationSchema(db *reform.DB, tablesTail string, typeFunc typeFunc) (structs []StructData) { 129 | tables, err := db.SelectAllFrom(tableView, tablesTail) 130 | if err != nil { 131 | logger.Fatalf("%s", err) 132 | } 133 | 134 | for _, t := range tables { 135 | imports := make(map[string]struct{}) 136 | table := t.(*table) 137 | str := parse.StructInfo{ 138 | Type: convertName(table.TableName), 139 | SQLName: table.TableName, 140 | PKFieldIndex: -1, 141 | } 142 | var comments []string 143 | 144 | key := getPrimaryKeyColumn(db, table.TableCatalog, table.TableSchema, table.TableName) 145 | 146 | tail := fmt.Sprintf( 147 | `WHERE table_catalog = %s AND table_schema = %s AND table_name = %s ORDER BY ordinal_position`, 148 | db.Placeholder(1), db.Placeholder(2), db.Placeholder(3), 149 | ) 150 | columns, err := db.SelectAllFrom(columnView, tail, table.TableCatalog, table.TableSchema, table.TableName) 151 | if err != nil { 152 | logger.Fatalf("%s", err) 153 | } 154 | for i, c := range columns { 155 | column := c.(*column) 156 | typ, pack, comment := typeFunc(column.Type, bool(column.IsNullable)) 157 | if pack != "" { 158 | imports[pack] = struct{}{} 159 | } 160 | comments = append(comments, comment) 161 | str.Fields = append(str.Fields, parse.FieldInfo{ 162 | Name: convertName(column.Name), 163 | Type: typ, 164 | Column: column.Name, 165 | }) 166 | 167 | if key != nil && key.ColumnName == column.Name { 168 | str.PKFieldIndex = i 169 | } 170 | } 171 | 172 | structs = append(structs, StructData{ 173 | Imports: imports, 174 | StructInfo: str, 175 | FieldComments: comments, 176 | }) 177 | } 178 | 179 | return 180 | } 181 | 182 | // cmdInit implements init command. 183 | func cmdInit(db *reform.DB, dir string) { 184 | var structs []StructData 185 | switch db.Dialect { 186 | case postgresql.Dialect: 187 | // catalog is a currently selected database (reform-database, postgres, template0, etc.) 188 | // schema is a PostgreSQL schema (public, pg_catalog, information_schema, etc.) 189 | structs = initModelsInformationSchema(db, `WHERE table_schema = current_schema()`, goTypePostgres) 190 | case mysql.Dialect: 191 | // catalog is always "def" 192 | // schema is a database name (reform-database, information_schema, performance_schema, mysql, sys, etc.) 193 | structs = initModelsInformationSchema(db, `WHERE table_schema = DATABASE()`, goTypeMySQL) 194 | case sqlite3.Dialect: 195 | // SQLite is special 196 | structs = initModelsSQLite3(db) 197 | case mssql.Dialect: //nolint:staticcheck 198 | fallthrough 199 | case sqlserver.Dialect: 200 | // catalog is a currently selected database (reform-database, master, etc.) 201 | // schema is MS SQL schema (dbo, guest, sys, information_schema, etc.) 202 | structs = initModelsInformationSchema(db, `WHERE table_schema = SCHEMA_NAME()`, goTypeMSSQL) 203 | default: 204 | logger.Fatalf("unhandled dialect %s", db.Dialect) 205 | } 206 | 207 | // detect package name by importing package or from directory name 208 | var packageName string 209 | pack, err := build.ImportDir(dir, 0) 210 | if err == nil { 211 | packageName = pack.Name 212 | } else { 213 | s := strings.Split(filepath.Base(dir), ".")[0] 214 | packageName = strings.ReplaceAll(s, "-", "_") 215 | } 216 | 217 | for _, s := range structs { 218 | logger.Debugf("%#v", s) 219 | 220 | f, err := os.Create(filepath.Join(dir, strings.ToLower(s.SQLName)+".go")) 221 | if err != nil { 222 | logger.Fatalf("%s", err) 223 | } 224 | 225 | logger.Debugf("Writing %s ...", f.Name()) 226 | if _, err = f.WriteString("package " + packageName + "\n"); err != nil { 227 | logger.Fatalf("%s", err) 228 | } 229 | if err = prologTemplate.Execute(f, s); err != nil { 230 | logger.Fatalf("%s", err) 231 | } 232 | if err = structTemplate.Execute(f, s); err != nil { 233 | logger.Fatalf("%s", err) 234 | } 235 | 236 | if err = f.Close(); err != nil { 237 | logger.Fatalf("%s", err) 238 | } 239 | } 240 | 241 | gofmt(dir) 242 | } 243 | -------------------------------------------------------------------------------- /reform-db/cmd_init_mssql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // goTypeMSSQL converts given SQL type to Go type. https://msdn.microsoft.com/en-us/library/ms187752.aspx 8 | func goTypeMSSQL(sqlType string, nullable bool) (typ string, pack string, comment string) { 9 | switch sqlType { 10 | case "tinyint": 11 | return maybePointer("uint8", nullable), "", "" // unsigned 12 | case "smallint": 13 | return maybePointer("int16", nullable), "", "" 14 | case "int": 15 | return maybePointer("int32", nullable), "", "" 16 | case "bigint": 17 | return maybePointer("int64", nullable), "", "" 18 | 19 | case "decimal", "numeric": 20 | return maybePointer("string", nullable), "", "" 21 | 22 | case "real": 23 | return maybePointer("float32", nullable), "", "" 24 | case "float": 25 | return maybePointer("float64", nullable), "", "" 26 | 27 | case "date", "time", "datetime", "datetime2", "smalldatetime": 28 | return maybePointer("time.Time", nullable), "time", "" 29 | 30 | case "char", "varchar", "text": 31 | fallthrough 32 | case "nchar", "nvarchar", "ntext": 33 | return maybePointer("string", nullable), "", "" 34 | 35 | case "binary", "varbinary": 36 | return "[]byte", "", "" // never a pointer 37 | 38 | case "bit": 39 | return maybePointer("bool", nullable), "", "" 40 | 41 | default: 42 | // logger.Fatalf("unhandled MSSQL type %q", sqlType) 43 | return "[]byte", "", fmt.Sprintf("// FIXME unhandled database type %q", sqlType) // never a pointer 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /reform-db/cmd_init_mysql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // goTypeMySQL converts given SQL type to Go type. https://dev.mysql.com/doc/refman/5.7/en/data-types.html 8 | func goTypeMySQL(sqlType string, nullable bool) (typ string, pack string, comment string) { 9 | switch sqlType { 10 | case "tinyint": 11 | return maybePointer("int8", nullable), "", "" 12 | case "smallint": 13 | return maybePointer("int16", nullable), "", "" 14 | case "mediumint", "int": 15 | return maybePointer("int32", nullable), "", "" 16 | case "bigint": 17 | return maybePointer("int64", nullable), "", "" 18 | 19 | case "decimal": 20 | return maybePointer("string", nullable), "", "" 21 | 22 | case "float": 23 | return maybePointer("float32", nullable), "", "" 24 | case "double": 25 | return maybePointer("float64", nullable), "", "" 26 | 27 | case "year", "date", "time", "datetime", "timestamp": 28 | return maybePointer("time.Time", nullable), "time", "" 29 | 30 | case "char", "varchar": 31 | fallthrough 32 | case "tinytext", "mediumtext", "text", "longtext": 33 | return maybePointer("string", nullable), "", "" 34 | 35 | case "binary", "varbinary": 36 | fallthrough 37 | case "tinyblob", "mediumblob", "blob", "longblob": 38 | return "[]byte", "", "" // never a pointer 39 | 40 | case "bool": 41 | return maybePointer("bool", nullable), "", "" 42 | 43 | default: 44 | // logger.Fatalf("unhandled MySQL type %q", sqlType) 45 | return "[]byte", "", fmt.Sprintf("// FIXME unhandled database type %q", sqlType) // never a pointer 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /reform-db/cmd_init_postgres.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // goTypePostgres converts given SQL type to Go type. https://www.postgresql.org/docs/current/static/datatype.html 8 | func goTypePostgres(sqlType string, nullable bool) (typ string, pack string, comment string) { 9 | switch sqlType { 10 | case "smallint", "smallserial": 11 | return maybePointer("int16", nullable), "", "" 12 | case "integer", "serial": 13 | return maybePointer("int32", nullable), "", "" 14 | case "bigint", "bigserial": 15 | return maybePointer("int64", nullable), "", "" 16 | 17 | case "decimal", "numeric": 18 | return maybePointer("string", nullable), "", "" 19 | 20 | case "real": 21 | return maybePointer("float32", nullable), "", "" 22 | case "double precision": 23 | return maybePointer("float64", nullable), "", "" 24 | 25 | case "character varying", "varchar", "character", "char", "text": 26 | return maybePointer("string", nullable), "", "" 27 | 28 | case "bytea": 29 | return "[]byte", "", "" // never a pointer 30 | 31 | case "date", "time", "time with time zone", "timestamp", "timestamp with time zone": 32 | return maybePointer("time.Time", nullable), "time", "" 33 | // interval can't be mapped to time.Duration: https://github.com/lib/pq/issues/78 34 | 35 | case "boolean": 36 | return maybePointer("bool", nullable), "", "" 37 | 38 | default: 39 | // logger.Fatalf("unhandled PostgreSQL type %q", sqlType) 40 | return "[]byte", "", fmt.Sprintf("// FIXME unhandled database type %q", sqlType) // never a pointer 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /reform-db/cmd_init_sqlite3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "gopkg.in/reform.v1" 8 | "gopkg.in/reform.v1/parse" 9 | ) 10 | 11 | // goTypeSQLite3 converts given SQL type to Go type. https://www.sqlite.org/datatype3.html 12 | func goTypeSQLite3(sqlType string, nullable bool) (typ string, pack string, comment string) { 13 | // SQLite3 has quite unique dynamic type system with storage classes and type affinities. 14 | // In short: 15 | // * table columns don't have rigid types; 16 | // * value has storage class (null, integer, real, text, blob), which defines how value is stored on disk; 17 | // * table column has type affinity (text, numeric, integer, real, blob), which defines preferred storage class 18 | // for values in this column; 19 | // * table column declared type in CREATE TABLE defines column affinity with a set of rules; 20 | // * table_info returns column declared type; 21 | // * we try to mirror SQLite's set of rules of defining column affinity from declared type to define Go type; 22 | // * we also extend this set of rules with some common SQL data types; 23 | // * it's not 100% accurate (because SQLite is dynamically typed), but it follows actual and best practices. 24 | 25 | sqlType = strings.ToLower(sqlType) 26 | 27 | // SQLite rules 1-4 28 | switch { 29 | case strings.Contains(sqlType, "int"): 30 | return maybePointer("int64", nullable), "", "" 31 | 32 | case strings.Contains(sqlType, "char"): 33 | fallthrough 34 | case strings.Contains(sqlType, "clob"): 35 | fallthrough 36 | case strings.Contains(sqlType, "text"): 37 | return maybePointer("string", nullable), "", "" 38 | 39 | case strings.Contains(sqlType, "blob"): 40 | fallthrough 41 | case sqlType == "": 42 | return "[]byte", "", "" // never a pointer 43 | 44 | case strings.Contains(sqlType, "real"): 45 | fallthrough 46 | case strings.Contains(sqlType, "floa"): 47 | fallthrough 48 | case strings.Contains(sqlType, "doub"): //nolint:misspell 49 | return maybePointer("float64", nullable), "", "" 50 | } 51 | 52 | // common SQL data types 53 | switch { 54 | // numeric, decimal, etc. 55 | case strings.Contains(sqlType, "num"): 56 | fallthrough 57 | case strings.Contains(sqlType, "dec"): 58 | return maybePointer("string", nullable), "", "" 59 | 60 | // bool, boolean, etc. 61 | case strings.Contains(sqlType, "bool"): 62 | return maybePointer("bool", nullable), "", "" 63 | 64 | // date, datetime, timestamp, etc. 65 | case strings.Contains(sqlType, "date"): 66 | fallthrough 67 | case strings.Contains(sqlType, "time"): 68 | return maybePointer("time.Time", nullable), "time", "" 69 | 70 | default: 71 | // logger.Fatalf("unhandled SQLite3 type %q", sqlType) 72 | return "[]byte", "", fmt.Sprintf("// FIXME unhandled database type %q", sqlType) // never a pointer 73 | } 74 | } 75 | 76 | // initModelsSQLite3 returns structs from SQLite3 database. 77 | func initModelsSQLite3(db *reform.DB) (structs []StructData) { 78 | tables, err := db.SelectAllFrom(sqliteMasterView, "WHERE type IN (?, ?)", "table", "view") 79 | if err != nil { 80 | logger.Fatalf("%s", err) 81 | } 82 | 83 | for _, table := range tables { 84 | imports := make(map[string]struct{}) 85 | tableName := table.(*sqliteMaster).Name 86 | if tableName == "sqlite_sequence" { 87 | continue 88 | } 89 | 90 | str := parse.StructInfo{ 91 | Type: convertName(tableName), 92 | SQLName: tableName, 93 | PKFieldIndex: -1, 94 | } 95 | var comments []string 96 | 97 | rows, err := db.Query("PRAGMA table_info(" + tableName + ")") // no placeholders for PRAGMA 98 | if err != nil { 99 | // https://github.com/go-reform/reform/issues/180 100 | logger.Printf("Skipping %s: %s.", tableName, err) 101 | continue 102 | } 103 | for { 104 | var column sqliteTableInfo 105 | if err = db.NextRow(&column, rows); err != nil { 106 | break 107 | } 108 | switch column.PK { 109 | case 0: // not PK 110 | // nothing 111 | case 1: 112 | str.PKFieldIndex = len(str.Fields) 113 | default: // composite PK 114 | str.PKFieldIndex = -1 115 | } 116 | typ, pack, comment := goTypeSQLite3(column.Type, !column.NotNull) 117 | if pack != "" { 118 | imports[pack] = struct{}{} 119 | } 120 | comments = append(comments, comment) 121 | str.Fields = append(str.Fields, parse.FieldInfo{ 122 | Name: convertName(column.Name), 123 | Type: typ, 124 | Column: column.Name, 125 | }) 126 | } 127 | if err != reform.ErrNoRows { 128 | logger.Fatalf("%s", err) 129 | } 130 | if err = rows.Close(); err != nil { 131 | logger.Fatalf("%s", err) 132 | } 133 | 134 | structs = append(structs, StructData{ 135 | Imports: imports, 136 | StructInfo: str, 137 | FieldComments: comments, 138 | }) 139 | } 140 | 141 | return 142 | } 143 | -------------------------------------------------------------------------------- /reform-db/cmd_init_template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "text/template" 5 | 6 | "gopkg.in/reform.v1/parse" 7 | ) 8 | 9 | // StructData contains struct information for template. 10 | type StructData struct { 11 | Imports map[string]struct{} 12 | parse.StructInfo 13 | FieldComments []string 14 | } 15 | 16 | var ( 17 | prologTemplate = template.Must(template.New("prolog").Parse(` 18 | import ( 19 | {{- range $i, $_ := .Imports }} 20 | "{{ $i }}" 21 | {{- end }} 22 | ) 23 | `)) 24 | 25 | structTemplate = template.Must(template.New("struct").Parse(` 26 | //go:generate reform 27 | 28 | // {{ .Type }} represents a row in {{ .SQLName }} table. 29 | //reform:{{ .SQLName }} 30 | type {{ .Type }} struct { 31 | {{- range $i, $f := .Fields }} 32 | {{ $f.Name }} {{ $f.Type }} ` + "`" + `reform:"{{ $f.Column }}{{ if eq $i $.PKFieldIndex }},pk{{ end }}"` + "`" + ` {{ index $.FieldComments $i }} 33 | {{- end }} 34 | } 35 | `)) 36 | ) 37 | -------------------------------------------------------------------------------- /reform-db/cmd_init_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "gopkg.in/reform.v1/dialects/sqlite3" 10 | "gopkg.in/reform.v1/parse" 11 | ) 12 | 13 | func (s *ReformDBSuite) TestInit() { 14 | good, err := parse.File("../internal/test/models/good.go") 15 | s.Require().NoError(err) 16 | s.Require().Len(good, 7) 17 | 18 | people := good[0] 19 | projects := good[1] 20 | personProject := good[2] 21 | idOnly := good[3] 22 | constraints := good[4] 23 | compositePK := good[5] 24 | 25 | // patch difference we don't handle 26 | people.Type = strings.ReplaceAll(people.Type, "Person", "People") 27 | projects.Type = strings.ReplaceAll(projects.Type, "Project", "Projects") 28 | if s.db.Dialect == sqlite3.Dialect { 29 | people.Fields[0].Type = strings.ReplaceAll(people.Fields[0].Type, "int32", "int64") 30 | people.Fields[1].Type = strings.ReplaceAll(people.Fields[1].Type, "int32", "int64") 31 | personProject.Fields[0].Type = strings.ReplaceAll(personProject.Fields[0].Type, "int32", "int64") 32 | idOnly.Fields[0].Type = strings.ReplaceAll(idOnly.Fields[0].Type, "int32", "int64") 33 | constraints.Fields[0].Type = strings.ReplaceAll(constraints.Fields[0].Type, "int32", "int64") 34 | compositePK.Fields[0].Type = strings.ReplaceAll(compositePK.Fields[0].Type, "int32", "int64") 35 | } 36 | 37 | dir, err := ioutil.TempDir("", "ReformDBTestInit") 38 | s.Require().NoError(err) 39 | s.T().Log(dir) 40 | 41 | cmdInit(s.db, dir) 42 | 43 | fis, err := ioutil.ReadDir(dir) 44 | s.Require().NoError(err) 45 | s.Require().Len(fis, 6) 46 | 47 | ff := filepath.Join(dir, "people.go") 48 | actual, err := parse.File(ff) 49 | s.Require().NoError(err) 50 | s.Require().Len(actual, 1) 51 | s.Require().Equal(people, actual[0]) 52 | 53 | ff = filepath.Join(dir, "projects.go") 54 | actual, err = parse.File(ff) 55 | s.Require().NoError(err) 56 | s.Require().Len(actual, 1) 57 | s.Require().Equal(projects, actual[0]) 58 | 59 | ff = filepath.Join(dir, "person_project.go") 60 | actual, err = parse.File(ff) 61 | s.Require().NoError(err) 62 | s.Require().Len(actual, 1) 63 | s.Require().Equal(personProject, actual[0]) 64 | 65 | ff = filepath.Join(dir, "id_only.go") 66 | actual, err = parse.File(ff) 67 | s.Require().NoError(err) 68 | s.Require().Len(actual, 1) 69 | s.Require().Equal(idOnly, actual[0]) 70 | 71 | ff = filepath.Join(dir, "constraints.go") 72 | actual, err = parse.File(ff) 73 | s.Require().NoError(err) 74 | s.Require().Len(actual, 1) 75 | s.Require().Equal(constraints, actual[0]) 76 | 77 | ff = filepath.Join(dir, "composite_pk.go") 78 | actual, err = parse.File(ff) 79 | s.Require().NoError(err) 80 | s.Require().Len(actual, 1) 81 | s.Require().Equal(compositePK, actual[0]) 82 | 83 | err = os.RemoveAll(dir) 84 | s.Require().NoError(err) 85 | } 86 | -------------------------------------------------------------------------------- /reform-db/cmd_query.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "gopkg.in/reform.v1" 12 | ) 13 | 14 | var ( 15 | queryFlags = flag.NewFlagSet("query", flag.ExitOnError) 16 | querySplitF = queryFlags.Bool("split", false, "Split statements by semicolon; does not handles them in string literals") 17 | ) 18 | 19 | func init() { 20 | queryFlags.Usage = func() { 21 | fmt.Fprintf(os.Stderr, "`query` command executes SQL queries from given files or stdin, and returns results.\n\n") 22 | fmt.Fprintf(os.Stderr, "Usage:\n") 23 | fmt.Fprintf(os.Stderr, " %s [global flags] query [query flags] [file names]\n\n", os.Args[0]) 24 | fmt.Fprintf(os.Stderr, "Global flags:\n") 25 | flag.PrintDefaults() 26 | fmt.Fprintf(os.Stderr, "\nQuery flags:\n") 27 | queryFlags.PrintDefaults() 28 | 29 | // TODO mention -split flag 30 | fmt.Fprintf(os.Stderr, ` 31 | Each file's content is executed as a single query. If it contains multiple 32 | statements, make sure SQL driver supports them. If file names are not given, 33 | a query is read from stdin until EOF, then executed. 34 | `) 35 | } 36 | } 37 | 38 | // cmdQuery implements query command. 39 | func cmdQuery(db *reform.DB, files []string) { 40 | queries := readFiles(files, *querySplitF) 41 | for _, q := range queries { 42 | rows, err := db.Query(q) 43 | if err != nil { 44 | logger.Fatalf("failed to query %s: %s", q, err) 45 | } 46 | columns, err := rows.Columns() 47 | if err != nil { 48 | logger.Fatalf("failed to get columns for %s: %s", q, err) 49 | } 50 | logger.Debugf("result columns: %v", columns) 51 | 52 | // write table header 53 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.Debug) 54 | if _, err = fmt.Fprintln(w, strings.Join(columns, "\t")); err != nil { 55 | logger.Fatalf("%s", err) 56 | } 57 | for i, c := range columns { 58 | columns[i] = strings.Repeat("-", len(c)) 59 | } 60 | if _, err = fmt.Fprintln(w, strings.Join(columns, "\t")); err != nil { 61 | logger.Fatalf("%s", err) 62 | } 63 | 64 | // read all rows, scan each field to []byte 65 | for rows.Next() { 66 | line := make([][]byte, len(columns)) 67 | dests := make([]interface{}, len(line)) 68 | for i := range dests { 69 | dests[i] = &line[i] 70 | } 71 | if err = rows.Scan(dests...); err != nil { 72 | logger.Fatalf("%s", err) 73 | } 74 | fmt.Fprintf(w, "%s\n", bytes.Join(line, []byte("\t"))) 75 | } 76 | if err = rows.Err(); err != nil { 77 | logger.Fatalf("%s", err) 78 | } 79 | 80 | if err = w.Flush(); err != nil { 81 | logger.Fatalf("%s", err) 82 | } 83 | if err = rows.Close(); err != nil { 84 | logger.Fatalf("%s", err) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /reform-db/main.go: -------------------------------------------------------------------------------- 1 | // Package reform-db implements reform-db command. 2 | package main 3 | 4 | import ( 5 | "database/sql" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | _ "github.com/denisenkom/go-mssqldb" 14 | _ "github.com/go-sql-driver/mysql" 15 | _ "github.com/jackc/pgx/stdlib" 16 | _ "github.com/lib/pq" 17 | _ "github.com/mattn/go-sqlite3" 18 | 19 | "gopkg.in/reform.v1" 20 | "gopkg.in/reform.v1/dialects" 21 | "gopkg.in/reform.v1/internal" 22 | ) 23 | 24 | var ( 25 | logger *internal.Logger 26 | 27 | debugF = flag.Bool("debug", false, "Enable debug logging") 28 | driverF = flag.String("db-driver", "", "Database driver (required)") 29 | sourceF = flag.String("db-source", "", "Database connection string (required)") 30 | waitF = flag.Duration("db-wait", 0, "Wait for database connection to be established, retrying every second") 31 | versionF = flag.Bool("version", false, "Print version and exit") 32 | ) 33 | 34 | func init() { 35 | flag.Usage = func() { 36 | fmt.Fprintf(os.Stderr, "reform-db - a better ORM tool. %s.\n\n", reform.Version) 37 | fmt.Fprintf(os.Stderr, "Usage:\n") 38 | fmt.Fprintf(os.Stderr, " %s [global flags] [command] [command flags] [arguments]\n\n", os.Args[0]) 39 | fmt.Fprintf(os.Stderr, "Global flags:\n") 40 | flag.PrintDefaults() 41 | fmt.Fprintf(os.Stderr, "\nCommands (run reform-db [command] -h for more information):\n") 42 | fmt.Fprintf(os.Stderr, " exec - executes SQL queries from given files or stdin\n") 43 | fmt.Fprintf(os.Stderr, " query - executes SQL queries from given files or stdin, and returns results\n") 44 | fmt.Fprintf(os.Stderr, " init - generates Go model files for existing database schema\n\n") 45 | fmt.Fprintf(os.Stderr, "Registered database drivers: %s.\n", strings.Join(sql.Drivers(), ", ")) 46 | } 47 | } 48 | 49 | func getDB() *reform.DB { 50 | if *driverF == "" || *sourceF == "" { 51 | logger.Fatalf("Please set both -db-driver and -db-source flags.") 52 | } 53 | sqlDB, err := sql.Open(*driverF, *sourceF) 54 | if err != nil { 55 | logger.Fatalf("Failed to connect to %s %q: %s", *driverF, *sourceF, err) 56 | } 57 | 58 | // Use single connection so various session-related variables work. 59 | // For example: "PRAGMA foreign_keys" for SQLite3, "SET IDENTITY_INSERT" for MS SQL, etc. 60 | sqlDB.SetMaxIdleConns(1) 61 | sqlDB.SetMaxOpenConns(1) 62 | sqlDB.SetConnMaxLifetime(0) 63 | 64 | start := time.Now() 65 | for { 66 | err = sqlDB.Ping() 67 | if err == nil { 68 | break 69 | } 70 | 71 | if time.Since(start) > *waitF { 72 | logger.Fatalf("Failed to ping database: %s.", err) 73 | } 74 | 75 | logger.Debugf("Failed to ping database: %s.", err) 76 | time.Sleep(time.Second) 77 | } 78 | 79 | logger.Debugf("Connected to database.") 80 | dialect := dialects.ForDriver(*driverF) 81 | return reform.NewDB(sqlDB, dialect, reform.NewPrintfLogger(logger.Debugf)) 82 | } 83 | 84 | func main() { 85 | flag.Parse() 86 | 87 | if *versionF { 88 | fmt.Println(reform.Version) 89 | os.Exit(0) 90 | } 91 | 92 | if flag.NArg() == 0 { 93 | flag.Usage() 94 | os.Exit(1) 95 | } 96 | 97 | *driverF = strings.TrimSpace(*driverF) 98 | *sourceF = strings.TrimSpace(*sourceF) 99 | 100 | logger = internal.NewLogger("reform-db: ", *debugF) 101 | 102 | // flagsets used below are created with ExitOnError, so Parse() is not expected to return errors 103 | switch flag.Arg(0) { 104 | case "exec": 105 | if err := execFlags.Parse(flag.Args()[1:]); err != nil { 106 | panic(err) 107 | } 108 | cmdExec(getDB(), execFlags.Args()) 109 | 110 | case "query": 111 | if err := queryFlags.Parse(flag.Args()[1:]); err != nil { 112 | panic(err) 113 | } 114 | cmdQuery(getDB(), queryFlags.Args()) 115 | 116 | case "init": 117 | if err := initFlags.Parse(flag.Args()[1:]); err != nil { 118 | panic(err) 119 | } 120 | 121 | if initFlags.NArg() > 1 { 122 | logger.Fatalf("Expected zero or one argument for %q, got %d", "init", initFlags.NArg()) 123 | } 124 | 125 | dir := initFlags.Arg(0) 126 | var err error 127 | if dir == "" { 128 | if dir, err = os.Getwd(); err != nil { 129 | logger.Fatalf("%s", err) 130 | } 131 | } 132 | if dir, err = filepath.Abs(dir); err != nil { 133 | logger.Fatalf("%s", err) 134 | } 135 | fi, err := os.Stat(dir) 136 | if os.IsNotExist(err) { 137 | logger.Fatalf("%q should be existing directory", dir) 138 | } 139 | if err != nil { 140 | logger.Fatalf("%s", err) 141 | } 142 | if !fi.IsDir() { 143 | logger.Fatalf("%q should be existing directory", dir) 144 | } 145 | 146 | cmdInit(getDB(), dir) 147 | 148 | default: 149 | flag.Usage() 150 | logger.Fatalf("Unexpected command %q", flag.Arg(0)) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /reform-db/models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | //go:generate reform 9 | 10 | type yesNo bool 11 | 12 | func (yn *yesNo) Scan(src interface{}) error { 13 | var str string 14 | switch s := src.(type) { 15 | case string: 16 | str = s 17 | case []byte: 18 | str = string(s) 19 | default: 20 | return fmt.Errorf("unexpected type %T (%#v)", src, src) 21 | } 22 | 23 | switch str { 24 | case "YES": 25 | *yn = true 26 | case "NO": 27 | *yn = false 28 | default: 29 | return fmt.Errorf("unexpected %q", str) 30 | } 31 | return nil 32 | } 33 | 34 | // check interface 35 | var _ sql.Scanner = (*yesNo)(nil) 36 | 37 | //reform:information_schema.tables 38 | type table struct { 39 | TableCatalog string `reform:"table_catalog"` 40 | TableSchema string `reform:"table_schema"` 41 | TableName string `reform:"table_name"` 42 | TableType string `reform:"table_type"` 43 | } 44 | 45 | //reform:information_schema.columns 46 | type column struct { 47 | TableCatalog string `reform:"table_catalog"` 48 | TableSchema string `reform:"table_schema"` 49 | TableName string `reform:"table_name"` 50 | Name string `reform:"column_name"` 51 | IsNullable yesNo `reform:"is_nullable"` 52 | Type string `reform:"data_type"` 53 | } 54 | 55 | //reform:information_schema.key_column_usage 56 | type keyColumnUsage struct { 57 | ColumnName string `reform:"column_name"` 58 | OrdinalPosition int `reform:"ordinal_position"` 59 | } 60 | 61 | //reform:sqlite_master 62 | type sqliteMaster struct { 63 | Name string `reform:"name"` 64 | } 65 | 66 | // TODO This "dummy" table name is ugly. We should do better. 67 | // See https://github.com/go-reform/reform/issues/107. 68 | //reform:dummy 69 | type sqliteTableInfo struct { 70 | CID int `reform:"cid"` 71 | Name string `reform:"name"` 72 | Type string `reform:"type"` 73 | NotNull bool `reform:"notnull"` 74 | DefaultValue *string `reform:"dflt_value"` 75 | PK int `reform:"pk"` 76 | } 77 | -------------------------------------------------------------------------------- /reform/main.go: -------------------------------------------------------------------------------- 1 | // Package reform implements reform command. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "go/build" 8 | "go/format" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "reflect" 14 | "strings" 15 | 16 | "gopkg.in/reform.v1" 17 | "gopkg.in/reform.v1/internal" 18 | "gopkg.in/reform.v1/parse" 19 | ) 20 | 21 | var ( 22 | logger *internal.Logger 23 | 24 | debugF = flag.Bool("debug", false, "Enable debug logging") 25 | gofmtF = flag.Bool("gofmt", true, "Format with gofmt") 26 | versionF = flag.Bool("version", false, "Print version and exit") 27 | ) 28 | 29 | func processFile(path, file, pack string) error { 30 | logger.Debugf("processFile: path=%q file=%q pack=%q", path, file, pack) 31 | 32 | structs, err := parse.File(filepath.Join(path, file)) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | logger.Debugf("%#v", structs) 38 | if len(structs) == 0 { 39 | return nil 40 | } 41 | 42 | ext := filepath.Ext(file) 43 | base := strings.TrimSuffix(file, ext) 44 | f, err := os.Create(filepath.Join(path, base+"_reform"+ext)) 45 | if err != nil { 46 | return err 47 | } 48 | defer f.Close() //nolint // real Close() error checked at the end if we reach it 49 | 50 | if _, err = f.WriteString("// Code generated by gopkg.in/reform.v1. DO NOT EDIT.\n\n"); err != nil { 51 | return err 52 | } 53 | if _, err = f.WriteString("package " + pack + "\n"); err != nil { 54 | return err 55 | } 56 | if err = prologTemplate.Execute(f, nil); err != nil { 57 | return err 58 | } 59 | 60 | sds := make([]StructData, 0, len(structs)) 61 | for _, str := range structs { 62 | // decide about view/table suffix 63 | t := strings.ToLower(str.Type[0:1]) + str.Type[1:] 64 | v := str.Type 65 | if str.IsTable() { 66 | t += "TableType" 67 | v += "Table" 68 | } else { 69 | t += "ViewType" 70 | v += "View" 71 | } 72 | 73 | sd := StructData{ 74 | StructInfo: str, 75 | TableType: t, 76 | TableVar: v, 77 | } 78 | sds = append(sds, sd) 79 | 80 | if err = structTemplate.Execute(f, &sd); err != nil { 81 | return err 82 | } 83 | } 84 | 85 | if err = initTemplate.Execute(f, sds); err != nil { 86 | return err 87 | } 88 | 89 | return f.Close() 90 | } 91 | 92 | func gofmt(path string) { 93 | if *gofmtF { 94 | cmd := exec.Command("gofmt", "-s", "-w", path) 95 | logger.Debugf(strings.Join(cmd.Args, " ")) 96 | b, err := cmd.CombinedOutput() 97 | if err != nil { 98 | logger.Fatalf("gofmt error: %s", err) 99 | } 100 | logger.Debugf("gofmt output: %s", b) 101 | } 102 | } 103 | 104 | func goformat(filePath string) { 105 | if !*gofmtF { 106 | in, err := ioutil.ReadFile(filePath) //nolint:gosec 107 | if err != nil { 108 | logger.Fatalf("go/format read error: %s", err) 109 | } 110 | out, err := format.Source(in) 111 | if err != nil { 112 | logger.Fatalf("go/format formatting error: %s", err) 113 | } 114 | if !reflect.DeepEqual(in, out) { 115 | if err := ioutil.WriteFile(filePath, out, 0o644); err != nil { //nolint:gosec 116 | logger.Fatalf("go/format write error: %s", err) 117 | } 118 | } 119 | } 120 | } 121 | 122 | func main() { 123 | flag.Usage = func() { 124 | fmt.Fprintf(os.Stderr, "reform - a better ORM generator. %s.\n\n", reform.Version) 125 | fmt.Fprintf(os.Stderr, "Usage:\n\n") 126 | fmt.Fprintf(os.Stderr, " %s [flags] [packages or directories]\n\n", os.Args[0]) 127 | fmt.Fprintf(os.Stderr, " go generate [flags] [packages or files] (with '//go:generate reform' in files)\n\n") 128 | fmt.Fprintf(os.Stderr, "Flags:\n") 129 | flag.PrintDefaults() 130 | } 131 | flag.Parse() 132 | 133 | if *versionF { 134 | fmt.Println(reform.Version) 135 | os.Exit(0) 136 | } 137 | 138 | logger = internal.NewLogger("reform: ", *debugF) 139 | 140 | logger.Debugf("Environment:") 141 | for _, pair := range os.Environ() { 142 | if strings.HasPrefix(pair, "GO") { 143 | logger.Debugf("\t%s", pair) 144 | } 145 | } 146 | 147 | wd, err := os.Getwd() 148 | if err != nil { 149 | logger.Fatalf("%s", err) 150 | } 151 | logger.Debugf("wd: %s", wd) 152 | logger.Debugf("args: %v", flag.Args()) 153 | 154 | // process arguments 155 | for _, arg := range flag.Args() { 156 | // import arg as directory or package path 157 | var pack *build.Package 158 | s, err := os.Stat(arg) 159 | if err == nil && s.IsDir() { 160 | pack, err = build.ImportDir(arg, 0) 161 | } 162 | if os.IsNotExist(err) { 163 | err = nil 164 | } 165 | if pack == nil && err == nil { 166 | pack, err = build.Import(arg, wd, 0) 167 | } 168 | if err != nil { 169 | logger.Fatalf("%s: %s", arg, err) 170 | } 171 | 172 | logger.Debugf("%#v", pack) 173 | 174 | var changed bool 175 | for _, f := range pack.GoFiles { 176 | err = processFile(pack.Dir, f, pack.Name) 177 | if err != nil { 178 | logger.Fatalf("%s %s: %s", arg, f, err) 179 | } 180 | goformat(filepath.Join(pack.Dir, f)) 181 | changed = true 182 | } 183 | 184 | if changed { 185 | gofmt(pack.Dir) 186 | } 187 | } 188 | 189 | // process go generate environment 190 | file := os.Getenv("GOFILE") 191 | pack := os.Getenv("GOPACKAGE") 192 | if file != "" && pack != "" { 193 | err := processFile(wd, file, pack) 194 | if err != nil { 195 | logger.Fatalf("%s", err) 196 | } 197 | goformat(file) 198 | gofmt(wd) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /reform/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "text/template" 5 | 6 | "gopkg.in/reform.v1/parse" 7 | ) 8 | 9 | // StructData represents struct info for XXX_reform.go file generation. 10 | type StructData struct { 11 | parse.StructInfo 12 | TableType string 13 | TableVar string 14 | } 15 | 16 | //nolint:gochecknoglobals 17 | var ( 18 | prologTemplate = template.Must(template.New("prolog").Parse(` 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "gopkg.in/reform.v1" 24 | "gopkg.in/reform.v1/parse" 25 | ) 26 | `)) 27 | 28 | structTemplate = template.Must(template.New("struct").Parse(` 29 | type {{ .TableType }} struct { 30 | s parse.StructInfo 31 | z []interface{} 32 | } 33 | 34 | // Schema returns a schema name in SQL database ("{{ .SQLSchema }}"). 35 | func (v *{{ .TableType }}) Schema() string { 36 | return v.s.SQLSchema 37 | } 38 | 39 | // Name returns a view or table name in SQL database ("{{ .SQLName }}"). 40 | func (v *{{ .TableType }}) Name() string { 41 | return v.s.SQLName 42 | } 43 | 44 | // Columns returns a new slice of column names for that view or table in SQL database. 45 | func (v *{{ .TableType }}) Columns() []string { 46 | return {{ .ColumnsGoString }} 47 | } 48 | 49 | // NewStruct makes a new struct for that view or table. 50 | func (v *{{ .TableType }}) NewStruct() reform.Struct { 51 | return new({{ .Type }}) 52 | } 53 | 54 | {{- if .IsTable }} 55 | 56 | // NewRecord makes a new record for that table. 57 | func (v *{{ .TableType }}) NewRecord() reform.Record { 58 | return new({{ .Type }}) 59 | } 60 | 61 | // PKColumnIndex returns an index of primary key column for that table in SQL database. 62 | func (v *{{ .TableType }}) PKColumnIndex() uint { 63 | return uint(v.s.PKFieldIndex) 64 | } 65 | 66 | {{- end }} 67 | 68 | // {{ .TableVar }} represents {{ .SQLName }} view or table in SQL database. 69 | var {{ .TableVar }} = &{{ .TableType }} { 70 | s: {{ .GoString }}, 71 | z: new({{ .Type }}).Values(), 72 | } 73 | 74 | // String returns a string representation of this struct or record. 75 | func (s {{ .Type }}) String() string { 76 | res := make([]string, {{ len .Fields }}) 77 | {{- range $i, $f := .Fields }} 78 | res[{{ $i }}] = "{{ $f.Name }}: " + reform.Inspect(s.{{ $f.Name }}, true) 79 | {{- end }} 80 | return strings.Join(res, ", ") 81 | } 82 | 83 | // Values returns a slice of struct or record field values. 84 | // Returned interface{} values are never untyped nils. 85 | func (s *{{ .Type }}) Values() []interface{} { 86 | return []interface{}{ {{- range .Fields }} 87 | s.{{ .Name }}, {{- end }} 88 | } 89 | } 90 | 91 | // Pointers returns a slice of pointers to struct or record fields. 92 | // Returned interface{} values are never untyped nils. 93 | func (s *{{ .Type }}) Pointers() []interface{} { 94 | return []interface{}{ {{- range .Fields }} 95 | &s.{{ .Name }}, {{- end }} 96 | } 97 | } 98 | 99 | // View returns View object for that struct. 100 | func (s *{{ .Type }}) View() reform.View { 101 | return {{ .TableVar }} 102 | } 103 | 104 | {{- if .IsTable }} 105 | 106 | // Table returns Table object for that record. 107 | func (s *{{ .Type }}) Table() reform.Table { 108 | return {{ .TableVar }} 109 | } 110 | 111 | // PKValue returns a value of primary key for that record. 112 | // Returned interface{} value is never untyped nil. 113 | func (s *{{ .Type }}) PKValue() interface{} { 114 | return s.{{ .PKField.Name }} 115 | } 116 | 117 | // PKPointer returns a pointer to primary key field for that record. 118 | // Returned interface{} value is never untyped nil. 119 | func (s *{{ .Type }}) PKPointer() interface{} { 120 | return &s.{{ .PKField.Name }} 121 | } 122 | 123 | // HasPK returns true if record has non-zero primary key set, false otherwise. 124 | func (s *{{ .Type }}) HasPK() bool { 125 | return s.{{ .PKField.Name }} != {{ .TableVar }}.z[{{ .TableVar }}.s.PKFieldIndex] 126 | } 127 | 128 | // SetPK sets record primary key, if possible. 129 | // 130 | // Deprecated: prefer direct field assignment where possible: s.{{ .PKField.Name }} = pk. 131 | func (s *{{ .Type }}) SetPK(pk interface{}) { 132 | reform.SetPK(s, pk) 133 | } 134 | 135 | {{- end }} 136 | 137 | // check interfaces 138 | var ( 139 | _ reform.View = {{ .TableVar }} 140 | _ reform.Struct = (*{{ .Type }})(nil) 141 | {{- if .IsTable }} 142 | _ reform.Table = {{ .TableVar }} 143 | _ reform.Record = (*{{ .Type }})(nil) 144 | {{- end }} 145 | _ fmt.Stringer = (*{{ .Type }})(nil) 146 | ) 147 | `)) 148 | 149 | initTemplate = template.Must(template.New("init").Parse(` 150 | func init() { 151 | {{- range $i, $sd := . }} 152 | parse.AssertUpToDate(&{{ $sd.TableVar }}.s, new({{ $sd.Type }})) 153 | {{- end }} 154 | } 155 | `)) 156 | ) 157 | -------------------------------------------------------------------------------- /reform/version_check.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.17 2 | // +build !go1.17 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | "runtime" 9 | ) 10 | 11 | func init() { 12 | log.Fatalf("reform requires Go 1.17+, but was compiled with %s.", runtime.Version()) 13 | } 14 | -------------------------------------------------------------------------------- /test/sql/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO people (id, name, email, created_at) VALUES (1, 'Denis Mills', NULL, '2009-11-10 23:00:00'); 2 | INSERT INTO people (id, name, email, created_at) VALUES (2, 'Garrick Muller', 'muller_garrick@example.com', '2009-12-12 12:34:56'); 3 | 4 | INSERT INTO people (id, name, email, created_at) VALUES (101, 'Noble Schumm', NULL, '2013-01-01 00:00:00'); 5 | INSERT INTO people (id, name, email, created_at) VALUES (102, 'Elfrieda Abbott', 'elfrieda_abbott@example.org', '2014-01-01 00:00:00'); 6 | INSERT INTO people (id, name, email, created_at) VALUES (103, 'Elfrieda Abbott', NULL, '2014-01-01 00:00:00'); 7 | 8 | -- ANSI quotes for keyword "end" 9 | INSERT INTO projects (id, name, start, "end") VALUES ('baron', 'Vicious Baron', '2014-06-01', '2016-02-21'); 10 | INSERT INTO projects (id, name, start, "end") VALUES ('queen', 'Thirsty Queen', '2016-01-15', NULL); 11 | INSERT INTO projects (id, name, start, "end") VALUES ('traveler', 'Kosher Traveler', '2016-02-01', NULL); 12 | INSERT INTO projects (id, name, start, "end") VALUES ('lightfoot', 'Sweet Lightfoot', '2016-01-01', NULL); 13 | INSERT INTO projects (id, name, start, "end") VALUES ('walker', 'Eager Walker', '2015-01-01', NULL); 14 | 15 | INSERT INTO person_project (project_id, person_id) VALUES ('baron', 101); 16 | INSERT INTO person_project (project_id, person_id) VALUES ('baron', 102); 17 | INSERT INTO person_project (project_id, person_id) VALUES ('baron', 103); 18 | 19 | INSERT INTO person_project (project_id, person_id) VALUES ('queen', 102); 20 | INSERT INTO person_project (project_id, person_id) VALUES ('queen', 103); 21 | 22 | INSERT INTO person_project (project_id, person_id) VALUES ('traveler', 103); 23 | -------------------------------------------------------------------------------- /test/sql/mssql_create.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE [reform-database]; 2 | -------------------------------------------------------------------------------- /test/sql/mssql_data.sql: -------------------------------------------------------------------------------- 1 | -- no MS SQL specific data 2 | -------------------------------------------------------------------------------- /test/sql/mssql_drop.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE [reform-database]; 2 | -------------------------------------------------------------------------------- /test/sql/mssql_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE [people] ( 2 | [id] int identity(1, 1) PRIMARY KEY, 3 | [group_id] int DEFAULT 65534, 4 | [name] varchar(255) NOT NULL, 5 | [email] varchar(255), 6 | [created_at] datetime2 NOT NULL, 7 | [updated_at] datetime2 8 | ); 9 | 10 | CREATE TABLE [projects] ( 11 | [name] varchar(255) NOT NULL, 12 | [id] varchar(255) PRIMARY KEY, 13 | [start] date NOT NULL, 14 | [end] date 15 | ); 16 | 17 | CREATE TABLE [person_project] ( 18 | [person_id] int NOT NULL REFERENCES [people] ON DELETE CASCADE, 19 | [project_id] varchar(255) NOT NULL REFERENCES [projects] ON DELETE CASCADE, 20 | UNIQUE ([person_id], [project_id]) 21 | ); 22 | 23 | CREATE TABLE id_only ( 24 | [id] int identity(1, 1) PRIMARY KEY 25 | ); 26 | 27 | CREATE TABLE constraints ( 28 | [i] int identity(1, 1) NOT NULL, 29 | [id] varchar(255) PRIMARY KEY, 30 | UNIQUE ([i]) 31 | ); 32 | 33 | CREATE TABLE composite_pk ( 34 | [i] int identity(1, 1) NOT NULL, 35 | [name] varchar(255) NOT NULL, 36 | [j] varchar(255) NOT NULL, 37 | PRIMARY KEY ([i], [j]) 38 | ); 39 | 40 | -- to allow insert test data with IDs 41 | SET IDENTITY_INSERT people ON; 42 | -------------------------------------------------------------------------------- /test/sql/mssql_set.sql: -------------------------------------------------------------------------------- 1 | DBCC CHECKIDENT ('people', RESEED, 1000); 2 | -------------------------------------------------------------------------------- /test/sql/mysql_create.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE `reform-database`; 2 | -------------------------------------------------------------------------------- /test/sql/mysql_data.sql: -------------------------------------------------------------------------------- 1 | -- no MySQL specific data 2 | -------------------------------------------------------------------------------- /test/sql/mysql_drop.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS `reform-database`; 2 | -------------------------------------------------------------------------------- /test/sql/mysql_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE people ( 2 | id int NOT NULL AUTO_INCREMENT, 3 | group_id int DEFAULT 65534, 4 | name varchar(255) NOT NULL, 5 | email varchar(255), 6 | created_at datetime NOT NULL, 7 | updated_at datetime, 8 | -- TODO doesn't work, file an issue for github.com/go-sql-driver/mysql 9 | -- created_at timestamp NOT NULL, 10 | -- updated_at timestamp, 11 | PRIMARY KEY (id) 12 | ); 13 | 14 | CREATE TABLE projects ( 15 | name varchar(255) NOT NULL, 16 | id varchar(255) NOT NULL, 17 | start date NOT NULL, 18 | end date, 19 | PRIMARY KEY (id) 20 | ); 21 | 22 | -- https://dev.mysql.com/doc/refman/5.7/en/create-table.html 23 | -- MySQL parses but ignores “inline REFERENCES specifications” (as defined in the SQL standard) 24 | -- where the references are defined as part of the column specification. MySQL accepts REFERENCES 25 | -- clauses only when specified as part of a separate FOREIGN KEY specification. 26 | 27 | CREATE TABLE person_project ( 28 | person_id int NOT NULL, 29 | project_id varchar(255) NOT NULL, 30 | UNIQUE (person_id, project_id), 31 | FOREIGN KEY (person_id) REFERENCES people (id) ON DELETE CASCADE, 32 | FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE 33 | ); 34 | 35 | CREATE TABLE id_only ( 36 | id int NOT NULL AUTO_INCREMENT, 37 | PRIMARY KEY (id) 38 | ); 39 | 40 | CREATE TABLE constraints ( 41 | i int NOT NULL AUTO_INCREMENT, 42 | id varchar(255) NOT NULL, 43 | PRIMARY KEY (id), 44 | UNIQUE (i) 45 | ); 46 | 47 | CREATE TABLE composite_pk ( 48 | i int NOT NULL AUTO_INCREMENT, 49 | name varchar(255) NOT NULL, 50 | j varchar(255) NOT NULL, 51 | PRIMARY KEY (i, j) 52 | ); 53 | -------------------------------------------------------------------------------- /test/sql/mysql_set.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE people AUTO_INCREMENT = 1000; 2 | -------------------------------------------------------------------------------- /test/sql/postgres_create.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE "reform-database"; 2 | -------------------------------------------------------------------------------- /test/sql/postgres_data.sql: -------------------------------------------------------------------------------- 1 | -- PostgreSQL specific data 2 | INSERT INTO legacy.people (id, name) VALUES (1001, 'Amelia Heathcote'); 3 | INSERT INTO legacy.people (id, name) VALUES (1002, 'Anastacio Ledner'); 4 | INSERT INTO legacy.people (id, name) VALUES (1003, 'Dena Cummings'); 5 | -------------------------------------------------------------------------------- /test/sql/postgres_drop.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS "reform-database"; 2 | -------------------------------------------------------------------------------- /test/sql/postgres_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE people ( 2 | id serial PRIMARY KEY, 3 | group_id integer DEFAULT 65534, 4 | name varchar NOT NULL, 5 | email varchar, 6 | created_at timestamp with time zone NOT NULL, 7 | updated_at timestamp with time zone 8 | -- created_at timestamp without time zone NOT NULL, 9 | -- updated_at timestamp without time zone 10 | ); 11 | 12 | CREATE TABLE projects ( 13 | name varchar NOT NULL, 14 | id varchar PRIMARY KEY, 15 | start date NOT NULL, 16 | "end" date 17 | ); 18 | 19 | CREATE TABLE person_project ( 20 | person_id integer NOT NULL REFERENCES people ON DELETE CASCADE, 21 | project_id varchar NOT NULL REFERENCES projects ON DELETE CASCADE, 22 | UNIQUE (person_id, project_id) 23 | ); 24 | 25 | CREATE TABLE id_only ( 26 | id serial PRIMARY KEY 27 | ); 28 | 29 | CREATE TABLE constraints ( 30 | i serial, 31 | id varchar PRIMARY KEY, 32 | UNIQUE (i) 33 | ); 34 | 35 | CREATE TABLE composite_pk ( 36 | i serial, 37 | name varchar NOT NULL, 38 | j varchar NOT NULL, 39 | PRIMARY KEY (i, j) 40 | ); 41 | 42 | CREATE SCHEMA legacy; 43 | 44 | CREATE TABLE legacy.people ( 45 | id serial PRIMARY KEY, 46 | name varchar 47 | ); 48 | -------------------------------------------------------------------------------- /test/sql/postgres_set.sql: -------------------------------------------------------------------------------- 1 | SELECT setval('people_id_seq', 1000); 2 | -------------------------------------------------------------------------------- /test/sql/sqlite3_create.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-reform/reform/db2c976d4b2fa1df1f9e16b4c314e18c5b475969/test/sql/sqlite3_create.sql -------------------------------------------------------------------------------- /test/sql/sqlite3_data.sql: -------------------------------------------------------------------------------- 1 | -- no sqlite3 specific data 2 | -------------------------------------------------------------------------------- /test/sql/sqlite3_drop.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-reform/reform/db2c976d4b2fa1df1f9e16b4c314e18c5b475969/test/sql/sqlite3_drop.sql -------------------------------------------------------------------------------- /test/sql/sqlite3_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE people ( 2 | id integer NOT NULL PRIMARY KEY AUTOINCREMENT, -- https://sqlite.org/lang_createtable.html#rowid 3 | group_id int DEFAULT 65534, 4 | name varchar NOT NULL, 5 | email varchar, 6 | created_at datetime NOT NULL, 7 | updated_at datetime 8 | ); 9 | 10 | CREATE TABLE projects ( 11 | name varchar NOT NULL, 12 | id varchar NOT NULL PRIMARY KEY, 13 | start date NOT NULL, 14 | end date 15 | ); 16 | 17 | CREATE TABLE person_project ( 18 | person_id integer NOT NULL REFERENCES people ON DELETE CASCADE, 19 | project_id varchar NOT NULL REFERENCES projects ON DELETE CASCADE, 20 | UNIQUE (person_id, project_id) 21 | ); 22 | 23 | CREATE TABLE id_only ( 24 | id integer NOT NULL PRIMARY KEY AUTOINCREMENT 25 | ); 26 | 27 | CREATE TABLE constraints ( 28 | i integer NOT NULL, -- no AUTOINCREMENT - it works only for PRIMARY KEY: https://www.sqlite.org/autoinc.html 29 | id varchar NOT NULL PRIMARY KEY, 30 | UNIQUE (i) 31 | ); 32 | 33 | CREATE TABLE composite_pk ( 34 | i integer NOT NULL, 35 | name varchar NOT NULL, 36 | j varchar NOT NULL, 37 | PRIMARY KEY (i, j) 38 | ); 39 | -------------------------------------------------------------------------------- /test/sql/sqlite3_set.sql: -------------------------------------------------------------------------------- 1 | UPDATE sqlite_sequence SET seq = 1000 WHERE name = 'people'; 2 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools // import "gopkg.in/reform.v1/tools" 5 | 6 | import ( 7 | _ "github.com/AlekSi/gocoverutil" 8 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 9 | _ "github.com/quasilyte/go-consistent" 10 | _ "github.com/reviewdog/reviewdog/cmd/reviewdog" 11 | _ "mvdan.cc/gofumpt" 12 | ) 13 | 14 | //go:generate go build -v -o ../bin/gocoverutil github.com/AlekSi/gocoverutil 15 | //go:generate go build -v -o ../bin/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint 16 | //go:generate go build -v -o ../bin/go-consistent github.com/quasilyte/go-consistent 17 | //go:generate go build -v -o ../bin/reviewdog github.com/reviewdog/reviewdog/cmd/reviewdog 18 | //go:generate go build -v -o ../bin/gofumpt mvdan.cc/gofumpt 19 | -------------------------------------------------------------------------------- /tx.go: -------------------------------------------------------------------------------- 1 | package reform 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "time" 7 | ) 8 | 9 | // TXInterface is a subset of *sql.Tx used by reform. 10 | // Can be used together with NewTXFromInterface for easier integration with existing code or for passing test doubles. 11 | // 12 | // It may grow and shrink over time to include only needed *sql.Tx methods, 13 | // and is excluded from SemVer compatibility guarantees. 14 | type TXInterface interface { 15 | DBTXContext 16 | Commit() error 17 | Rollback() error 18 | 19 | // Deprecated: do not use, it will be removed in v1.6. 20 | DBTX 21 | } 22 | 23 | // check interface 24 | var _ TXInterface = (*sql.Tx)(nil) 25 | 26 | // TX represents a SQL database transaction. 27 | type TX struct { 28 | *Querier 29 | tx TXInterface 30 | } 31 | 32 | // NewTX creates new TX object for given SQL database transaction. 33 | // Logger can be nil. 34 | func NewTX(tx *sql.Tx, dialect Dialect, logger Logger) *TX { 35 | return NewTXFromInterface(tx, dialect, logger) 36 | } 37 | 38 | // NewTXFromInterface creates new TX object for given TXInterface. 39 | // Can be used for easier integration with existing code or for passing test doubles. 40 | // Logger can be nil. 41 | func NewTXFromInterface(tx TXInterface, dialect Dialect, logger Logger) *TX { 42 | return newTX(context.Background(), tx, dialect, logger) 43 | } 44 | 45 | func newTX(ctx context.Context, tx TXInterface, dialect Dialect, logger Logger) *TX { 46 | return &TX{ 47 | Querier: newQuerier(ctx, tx, "", dialect, logger), 48 | tx: tx, 49 | } 50 | } 51 | 52 | // Commit commits the transaction. 53 | func (tx *TX) Commit() error { 54 | tx.logBefore("COMMIT", nil) 55 | start := time.Now() 56 | err := tx.tx.Commit() 57 | tx.logAfter("COMMIT", nil, time.Since(start), err) 58 | return err 59 | } 60 | 61 | // Rollback aborts the transaction. 62 | func (tx *TX) Rollback() error { 63 | tx.logBefore("ROLLBACK", nil) 64 | start := time.Now() 65 | err := tx.tx.Rollback() 66 | tx.logAfter("ROLLBACK", nil, time.Since(start), err) 67 | return err 68 | } 69 | 70 | // check interfaces 71 | var ( 72 | _ DBTX = (*TX)(nil) 73 | _ DBTXContext = (*TX)(nil) 74 | ) 75 | --------------------------------------------------------------------------------