├── .circleci └── config.yml ├── .codecov.yml ├── .gitignore ├── LICENSE ├── README.md ├── aliases.go ├── aliases_test.go ├── benchmark ├── benchmark_test.go ├── go.mod └── go.sum ├── comparisoner.go ├── comparisoner_test.go ├── conjunction.go ├── conjunction_test.go ├── go.mod ├── go.sum ├── internal ├── pool │ ├── buffer.go │ ├── bytes_buffer.go │ ├── pool.go │ └── string_builder.go └── slice │ ├── slice.go │ └── slice_test.go ├── options.go ├── order_by.go ├── order_by_test.go ├── sqb.go ├── sqb_test.go ├── stmt ├── columns.go ├── columns_test.go ├── compare.go ├── compare_test.go ├── condition.go ├── condition_test.go ├── conjunction.go ├── conjunction_test.go ├── error.go ├── error_test.go ├── stmt.go ├── table.go ├── table_test.go └── utils_for_test.go └── utils_for_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | variables: 4 | go: &go circleci/golang:1.13.4 5 | context: &context org-context 6 | working_directory: &working_directory /go/src/github.com/Code-Hex/sqb 7 | common_steps: 8 | restore_cache_modules: &restore_cache_modules 9 | keys: 10 | - go-mod-v1-{{ checksum "go.sum" }} 11 | 12 | jobs: 13 | mod: 14 | docker: 15 | - image: *go 16 | environment: 17 | GOFLAGS: -mod=vendor 18 | working_directory: *working_directory 19 | steps: 20 | - checkout 21 | - restore_cache: *restore_cache_modules 22 | - run: 23 | name: Install dependencies 24 | command: | 25 | go mod vendor 26 | 27 | - save_cache: 28 | key: go-mod-v1-{{ checksum "go.sum" }} 29 | paths: 30 | - "./vendor" 31 | 32 | vet: 33 | docker: 34 | - image: *go 35 | environment: 36 | GOFLAGS: -mod=vendor 37 | working_directory: *working_directory 38 | steps: 39 | - checkout 40 | - restore_cache: *restore_cache_modules 41 | - run: 42 | name: vet 43 | command: go vet ./... 44 | 45 | test: 46 | docker: 47 | - image: *go 48 | environment: 49 | GOFLAGS: -mod=vendor 50 | working_directory: *working_directory 51 | steps: 52 | - checkout 53 | - restore_cache: *restore_cache_modules 54 | - run: 55 | name: Test 56 | command: go test -race -coverpkg=./... -coverprofile=coverage.txt ./... 57 | - run: 58 | name: Upload coverages to codecov 59 | command: | 60 | bash <(curl -s https://codecov.io/bash) 61 | 62 | workflows: 63 | version: 2 64 | test-workflow: 65 | jobs: 66 | - mod: 67 | context: *context 68 | filters: 69 | tags: 70 | only: /.*/ 71 | - vet: 72 | context: *context 73 | requires: 74 | - mod 75 | filters: 76 | tags: 77 | only: /.*/ 78 | - test: 79 | context: *context 80 | requires: 81 | - mod 82 | filters: 83 | tags: 84 | only: /.*/ 85 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | # At CircleCi, the PR is merged into `master` before the testsuite is run. 3 | # This allows CodeCov to adjust the resulting coverage diff, s.t. it matches 4 | # with the GitHub diff. 5 | # https://github.com/codecov/support/issues/363 6 | # https://docs.codecov.io/docs/comparing-commits 7 | allow_coverage_offsets: true 8 | ignore: 9 | - "internal/pool" 10 | coverage: 11 | precision: 2 12 | round: down 13 | range: "90...95" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Code-Hex 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 | # sqb - SQL Query Builder 2 | 3 | > ⚡ Blazing fast, Flexible, SQL Query Builder for Go 4 | 5 | [![GoDoc](https://godoc.org/github.com/Code-Hex/sqb?status.svg)](https://godoc.org/github.com/Code-Hex/sqb) [![CircleCI](https://circleci.com/gh/Code-Hex/sqb.svg?style=svg&circle-token=0ff0570576e90eb3a10e017f7ca1279748565daf)](https://circleci.com/gh/Code-Hex/sqb) [![codecov](https://codecov.io/gh/Code-Hex/sqb/branch/master/graph/badge.svg?token=xjioT8q5f5)](https://codecov.io/gh/Code-Hex/sqb) [![Go Report Card](https://goreportcard.com/badge/github.com/Code-Hex/sqb)](https://goreportcard.com/report/github.com/Code-Hex/sqb) 6 | 7 | ## Features 8 | 9 | - High performance. 10 | - Easy to use. 11 | - Powerful, Flexible. You can define stmt for yourself. 12 | - Supported MySQL, PostgreSQL, Spanner statement. 13 | 14 | ## Synopsis 15 | 16 | When used normally 17 | 18 | ```go 19 | const sqlstr = "SELECT * FROM tables WHERE ?" 20 | builder := sqb.New(sqlstr).Bind(sqb.Eq("category", 1)) 21 | query, args, err := builder.Build() 22 | // query => "SELECT * FROM tables WHERE category = ?", 23 | // args => []interface{}{1} 24 | ``` 25 | 26 |
27 | When you want to use build cache 28 | 29 | 30 | ```go 31 | const sqlstr = "SELECT * FROM tables WHERE ? AND ?" 32 | cached := sqb.New(sqlstr).Bind(sqb.Eq("category", 1)) 33 | 34 | for _, col := range columns { 35 | builder := cached.Bind(sqb.Eq(col, "value")) 36 | query, args, err := builder.Build() 37 | // query => "SELECT * FROM tables WHERE category = ? AND " + col + " = ?", 38 | // args => []interface{}{1, "value"} 39 | } 40 | ``` 41 |
42 | 43 |
44 | Error case 45 | 46 | 47 | ```go 48 | const sqlstr = "SELECT * FROM tables WHERE ? OR ?" 49 | builder := sqb.New(sqlstr).Bind(sqb.Eq("category", 1)) 50 | query, args, err := builder.Build() 51 | // query => "", 52 | // args => nil 53 | // err => "number of bindVars exceeds replaceable statements" 54 | ``` 55 |
56 | 57 | ## Install 58 | 59 | Use `go get` to install this package. 60 | 61 | go get -u github.com/Code-Hex/sqb 62 | 63 | ## Performance 64 | 65 | sqb is the fastest and least-memory used among currently known SQL Query builder in the benchmark. The data of chart using simple [benchmark](https://github.com/Code-Hex/sqb/blob/a3e54e8ed6bf41df28cac174e503b15d03c76b4b/benchmark/benchmark_test.go). 66 | 67 | ![time](https://user-images.githubusercontent.com/6500104/71240191-d7e74a00-234b-11ea-8070-03ffaa8f849f.png) 68 | 69 | ![benchmark](https://user-images.githubusercontent.com/6500104/71240411-62c84480-234c-11ea-926b-d31418148d26.png) 70 | -------------------------------------------------------------------------------- /aliases.go: -------------------------------------------------------------------------------- 1 | package sqb 2 | 3 | import ( 4 | "github.com/Code-Hex/sqb/stmt" 5 | ) 6 | 7 | type ( 8 | // Columns is an alias of stmt.Columns. 9 | Columns = stmt.Columns 10 | // Limit is an alias of stmt.Limit. 11 | Limit = stmt.Limit 12 | // Offset is an alias of stmt.Offset. 13 | Offset = stmt.Offset 14 | // String is an alias of stmt.String. 15 | String = stmt.String 16 | // Numeric is an alias of stmt.Numeric. 17 | Numeric = stmt.Numeric 18 | ) 19 | -------------------------------------------------------------------------------- /aliases_test.go: -------------------------------------------------------------------------------- 1 | package sqb_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Code-Hex/sqb" 7 | ) 8 | 9 | func TestColumns(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | c sqb.Columns 13 | want string 14 | wantErr bool 15 | }{ 16 | { 17 | name: "valid", 18 | c: sqb.Columns{"hello"}, 19 | want: "SELECT hello FROM table", 20 | wantErr: false, 21 | }, 22 | { 23 | name: "valid columns", 24 | c: sqb.Columns{"hello", "world", "sqb"}, 25 | want: "SELECT hello, world, sqb FROM table", 26 | wantErr: false, 27 | }, 28 | { 29 | name: "invalid", 30 | c: sqb.Columns{}, 31 | want: "", 32 | wantErr: true, 33 | }, 34 | } 35 | const sqlstr = "SELECT ? FROM table" 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | builder := sqb.New().Bind(tt.c) 39 | got, _, err := builder.Build(sqlstr) 40 | if (err != nil) != tt.wantErr { 41 | t.Errorf("Columns error = %v, wantErr %v", err, tt.wantErr) 42 | } 43 | if tt.want != got { 44 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestString(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | s sqb.String 54 | want string 55 | wantErr bool 56 | }{ 57 | { 58 | name: "valid", 59 | s: sqb.String("hello"), 60 | want: "SELECT * FROM hello", 61 | wantErr: false, 62 | }, 63 | { 64 | name: "invalid", 65 | s: sqb.String(""), 66 | want: "", 67 | wantErr: true, 68 | }, 69 | } 70 | const sqlstr = "SELECT * FROM ?" 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | builder := sqb.New().Bind(tt.s) 74 | got, _, err := builder.Build(sqlstr) 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("String error = %v, wantErr %v", err, tt.wantErr) 77 | } 78 | if tt.want != got { 79 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestNumeric(t *testing.T) { 86 | tests := []struct { 87 | name string 88 | n sqb.Numeric 89 | want string 90 | }{ 91 | { 92 | name: "valid", 93 | n: sqb.Numeric(10), 94 | want: "SELECT * FROM hello LIMIT 10", 95 | }, 96 | { 97 | name: "valid zero", 98 | n: sqb.Numeric(0), 99 | want: "SELECT * FROM hello LIMIT 0", 100 | }, 101 | } 102 | const sqlstr = "SELECT * FROM hello LIMIT ?" 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | builder := sqb.New().Bind(tt.n) 106 | got, _, err := builder.Build(sqlstr) 107 | if err != nil { 108 | t.Errorf("unexpected error = %v", err) 109 | } 110 | if tt.want != got { 111 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func TestLimit(t *testing.T) { 118 | tests := []struct { 119 | name string 120 | l sqb.Limit 121 | want string 122 | wantErr bool 123 | }{ 124 | { 125 | name: "valid", 126 | l: sqb.Limit(100), 127 | want: "SELECT * FROM table LIMIT 100", 128 | wantErr: false, 129 | }, 130 | { 131 | name: "valid 0", 132 | l: sqb.Limit(0), 133 | want: "SELECT * FROM table LIMIT 0", 134 | wantErr: false, 135 | }, 136 | } 137 | const sqlstr = "SELECT * FROM table ?" 138 | for _, tt := range tests { 139 | t.Run(tt.name, func(t *testing.T) { 140 | builder := sqb.New().Bind(tt.l) 141 | got, _, err := builder.Build(sqlstr) 142 | if err != nil { 143 | t.Fatalf("unexpected error: %v", err) 144 | } 145 | if tt.want != got { 146 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | func TestOffset(t *testing.T) { 153 | tests := []struct { 154 | name string 155 | o sqb.Offset 156 | want string 157 | wantErr bool 158 | }{ 159 | { 160 | name: "valid", 161 | o: sqb.Offset(100), 162 | want: "SELECT * FROM table LIMIT 1 OFFSET 100", 163 | wantErr: false, 164 | }, 165 | { 166 | name: "valid 0", 167 | o: sqb.Offset(0), 168 | want: "SELECT * FROM table LIMIT 1 OFFSET 0", 169 | wantErr: false, 170 | }, 171 | } 172 | const sqlstr = "SELECT * FROM table LIMIT 1 ?" 173 | for _, tt := range tests { 174 | t.Run(tt.name, func(t *testing.T) { 175 | builder := sqb.New().Bind(tt.o) 176 | got, _, err := builder.Build(sqlstr) 177 | if err != nil { 178 | t.Fatalf("unexpected error: %v", err) 179 | } 180 | if tt.want != got { 181 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 182 | } 183 | }) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /benchmark/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Code-Hex/sqb" 7 | sq "github.com/Masterminds/squirrel" 8 | "github.com/huandu/go-sqlbuilder" 9 | ) 10 | 11 | func BenchmarkSqbAndFromMap(b *testing.B) { 12 | const want = "SELECT * FROM users WHERE (col1 = ? AND col2 = ? AND col3 = ?)" 13 | for i := 0; i < b.N; i++ { 14 | builder := sqb.New() 15 | sql, _, err := builder.Bind( 16 | sqb.Paren( 17 | sqb.AndFromMap(sqb.Eq, map[string]interface{}{ 18 | "col1": "world", 19 | "col2": 100, 20 | "col3": true, 21 | }), 22 | ), 23 | ).Build("SELECT * FROM users WHERE ?") 24 | if err != nil { 25 | b.Fatal(err) 26 | } 27 | if want != sql { 28 | b.Fatalf("\nwant %q\ngot %q", want, sql) 29 | } 30 | } 31 | } 32 | 33 | func BenchmarkSqbAnd(b *testing.B) { 34 | const want = "SELECT * FROM users WHERE (col1 = ? AND col2 = ? AND col3 = ?)" 35 | for i := 0; i < b.N; i++ { 36 | builder := sqb.New() 37 | sql, _, err := builder.Bind( 38 | sqb.Paren( 39 | sqb.And( 40 | sqb.Eq("col1", "world"), 41 | sqb.Eq("col2", 100), 42 | sqb.Eq("col3", true), 43 | ), 44 | ), 45 | ).Build("SELECT * FROM users WHERE ?") 46 | if err != nil { 47 | b.Fatal(err) 48 | } 49 | if want != sql { 50 | b.Fatalf("\nwant %q\ngot %q", want, sql) 51 | } 52 | } 53 | } 54 | 55 | func BenchmarkSquirrel(b *testing.B) { 56 | const want = "SELECT * FROM users WHERE (col1 = ? AND col2 = ? AND col3 = ?)" 57 | for i := 0; i < b.N; i++ { 58 | users := sq.Select("*").From("users") 59 | active := users.Where( 60 | sq.And{ 61 | sq.Eq{"col1": "world"}, 62 | sq.Eq{"col2": 100}, 63 | sq.Eq{"col3": true}, 64 | }, 65 | ) 66 | sql, _, err := active.ToSql() 67 | if err != nil { 68 | b.Fatal(err) 69 | } 70 | if want != sql { 71 | b.Fatalf("\nwant %q\ngot %q", want, sql) 72 | } 73 | } 74 | } 75 | 76 | func BenchmarkSqlbuilder(b *testing.B) { 77 | const want = "SELECT * FROM users WHERE (col1 = ? AND col2 = ? AND col3 = ?)" 78 | for i := 0; i < b.N; i++ { 79 | sb := sqlbuilder.NewSelectBuilder() 80 | sb.Select("*") 81 | sb.From("users") 82 | sb.Where( 83 | sb.And( 84 | sb.Equal("col1", "world"), 85 | sb.Equal("col2", 100), 86 | sb.Equal("col3", true), 87 | ), 88 | ) 89 | sql, _ := sb.Build() 90 | if want != sql { 91 | b.Fatalf("\nwant %q\ngot %q", want, sql) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /benchmark/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Code-Hex/sqb/benchmark 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Code-Hex/sqb v0.0.2 7 | github.com/Masterminds/squirrel v1.1.0 8 | github.com/go-sql-driver/mysql v1.4.1 // indirect 9 | github.com/huandu/go-sqlbuilder v1.5.1 10 | github.com/lib/pq v1.2.0 // indirect 11 | github.com/mattn/go-sqlite3 v1.11.0 // indirect 12 | google.golang.org/appengine v1.6.5 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /benchmark/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Code-Hex/sqb v0.0.2 h1:qBLN/KgAR9PivErl/wkEDbgATs6w85vY27K6UyTqrow= 2 | github.com/Code-Hex/sqb v0.0.2/go.mod h1:4rr67X36xZ0I3/8Wi5UYDZDpI/+vN7VMxdZP+n/jKWE= 3 | github.com/Masterminds/squirrel v1.1.0 h1:baP1qLdoQCeTw3ifCdOq2dkYc6vGcmRdaociKLbEJXs= 4 | github.com/Masterminds/squirrel v1.1.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 8 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 9 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 10 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 11 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 12 | github.com/huandu/go-sqlbuilder v1.5.1 h1:u6plAT25uWpbOYK7+9szj2ppq5L9gbj7/Z35NUB2GCg= 13 | github.com/huandu/go-sqlbuilder v1.5.1/go.mod h1:cM38aLPrMXaGxsUkHFh1e2skthPnQRPK7h8//X5LQMc= 14 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 15 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 16 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 17 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 18 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 19 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 20 | github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= 21 | github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 25 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 26 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 27 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 28 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 29 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 30 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 31 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= 32 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 33 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 34 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 35 | -------------------------------------------------------------------------------- /comparisoner.go: -------------------------------------------------------------------------------- 1 | package sqb 2 | 3 | import ( 4 | "github.com/Code-Hex/sqb/stmt" 5 | ) 6 | 7 | // ConditionalFunc indicates function of conditional. 8 | type ConditionalFunc func(column string, value interface{}) *stmt.Condition 9 | 10 | // Eq creates condition `column = ?`. 11 | func Eq(column string, value interface{}) *stmt.Condition { 12 | return Op("=", column, value) 13 | } 14 | 15 | // Ge creates condition `column >= ?`. 16 | func Ge(column string, value interface{}) *stmt.Condition { 17 | return Op(">=", column, value) 18 | } 19 | 20 | // Gt creates condition `column > ?`. 21 | func Gt(column string, value interface{}) *stmt.Condition { 22 | return Op(">", column, value) 23 | } 24 | 25 | // Le creates condition `column <= ?`. 26 | func Le(column string, value interface{}) *stmt.Condition { 27 | return Op("<=", column, value) 28 | } 29 | 30 | // Lt creates condition `column < ?`. 31 | func Lt(column string, value interface{}) *stmt.Condition { 32 | return Op("<", column, value) 33 | } 34 | 35 | // Ne creates condition `column != ?`. 36 | func Ne(column string, value interface{}) *stmt.Condition { 37 | return Op("!=", column, value) 38 | } 39 | 40 | // Op creates flexible compare operation. 41 | func Op(op, column string, value interface{}) *stmt.Condition { 42 | return &stmt.Condition{ 43 | Column: column, 44 | Compare: &stmt.CompOp{ 45 | Op: op, 46 | Value: value, 47 | }, 48 | } 49 | } 50 | 51 | // Like creates condition `column LIKE ?`. 52 | func Like(column string, value interface{}) *stmt.Condition { 53 | return &stmt.Condition{ 54 | Column: column, 55 | Compare: &stmt.CompLike{ 56 | Negative: false, 57 | Value: value, 58 | }, 59 | } 60 | } 61 | 62 | // NotLike creates condition `column NOT LIKE ?`. 63 | func NotLike(column string, value interface{}) *stmt.Condition { 64 | return &stmt.Condition{ 65 | Column: column, 66 | Compare: &stmt.CompLike{ 67 | Negative: true, 68 | Value: value, 69 | }, 70 | } 71 | } 72 | 73 | // Between creates condition `column BETWEEN ? AND ?`. 74 | func Between(column string, left, right interface{}) *stmt.Condition { 75 | return &stmt.Condition{ 76 | Column: column, 77 | Compare: &stmt.CompBetween{ 78 | Negative: false, 79 | Left: left, 80 | Right: right, 81 | }, 82 | } 83 | } 84 | 85 | // NotBetween creates condition `column NOT BETWEEN ? AND ?`. 86 | func NotBetween(column string, left, right interface{}) *stmt.Condition { 87 | return &stmt.Condition{ 88 | Column: column, 89 | Compare: &stmt.CompBetween{ 90 | Negative: true, 91 | Left: left, 92 | Right: right, 93 | }, 94 | } 95 | } 96 | 97 | // In creates condition `column IN (?, ?, ?, ...)`. 98 | func In(column string, args ...interface{}) *stmt.Condition { 99 | return &stmt.Condition{ 100 | Column: column, 101 | Compare: &stmt.CompIn{ 102 | Negative: false, 103 | Values: args, 104 | }, 105 | } 106 | } 107 | 108 | // NotIn creates condition `column NOT IN (?, ?, ?, ...)`. 109 | func NotIn(column string, args ...interface{}) *stmt.Condition { 110 | return &stmt.Condition{ 111 | Column: column, 112 | Compare: &stmt.CompIn{ 113 | Negative: true, 114 | Values: args, 115 | }, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /comparisoner_test.go: -------------------------------------------------------------------------------- 1 | package sqb_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/Code-Hex/sqb" 9 | "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | func TestConditional(t *testing.T) { 13 | tests := []struct { 14 | f sqb.ConditionalFunc 15 | want string 16 | }{ 17 | { 18 | f: sqb.Eq, 19 | want: "=", 20 | }, 21 | { 22 | f: sqb.Ge, 23 | want: ">=", 24 | }, 25 | { 26 | f: sqb.Gt, 27 | want: ">", 28 | }, 29 | { 30 | f: sqb.Le, 31 | want: "<=", 32 | }, 33 | { 34 | f: sqb.Lt, 35 | want: "<", 36 | }, 37 | { 38 | f: sqb.Ne, 39 | want: "!=", 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.want, func(t *testing.T) { 44 | b := &BuildCapture{ 45 | buf: strings.Builder{}, 46 | Args: []interface{}{}, 47 | } 48 | wantArg := 1 49 | expr := tt.f("col", wantArg) 50 | if err := expr.Write(b); err != nil { 51 | t.Fatalf("unexpected error: %v", err) 52 | } 53 | 54 | wantQuery := fmt.Sprintf("col %s ?", tt.want) 55 | if got := b.buf.String(); wantQuery != got { 56 | t.Errorf("\nwant: %q\ngot: %q", wantQuery, got) 57 | } 58 | if diff := cmp.Diff([]interface{}{wantArg}, b.Args); diff != "" { 59 | t.Errorf("args (-want, +got)\n%s", diff) 60 | } 61 | }) 62 | } 63 | } 64 | func TestLike(t *testing.T) { 65 | type args struct { 66 | column string 67 | value interface{} 68 | } 69 | tests := []struct { 70 | name string 71 | args args 72 | want string 73 | wantArgs []interface{} 74 | }{ 75 | { 76 | name: "valid", 77 | args: args{ 78 | column: "col", 79 | value: "abc%", 80 | }, 81 | want: "col LIKE ?", 82 | wantArgs: []interface{}{"abc%"}, 83 | }, 84 | } 85 | for _, tt := range tests { 86 | t.Run(tt.name, func(t *testing.T) { 87 | b := &BuildCapture{ 88 | buf: strings.Builder{}, 89 | Args: []interface{}{}, 90 | } 91 | expr := sqb.Like(tt.args.column, tt.args.value) 92 | if err := expr.Write(b); err != nil { 93 | t.Fatalf("unexpected error: %v", err) 94 | } 95 | if got := b.buf.String(); tt.want != got { 96 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 97 | } 98 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 99 | t.Errorf("args (-want, +got)\n%s", diff) 100 | } 101 | }) 102 | } 103 | } 104 | 105 | func TestNotLike(t *testing.T) { 106 | type args struct { 107 | column string 108 | value interface{} 109 | } 110 | tests := []struct { 111 | name string 112 | args args 113 | want string 114 | wantArgs []interface{} 115 | }{ 116 | { 117 | name: "valid", 118 | args: args{ 119 | column: "col", 120 | value: "abc%", 121 | }, 122 | want: "col NOT LIKE ?", 123 | wantArgs: []interface{}{"abc%"}, 124 | }, 125 | } 126 | for _, tt := range tests { 127 | t.Run(tt.name, func(t *testing.T) { 128 | b := &BuildCapture{ 129 | buf: strings.Builder{}, 130 | Args: []interface{}{}, 131 | } 132 | expr := sqb.NotLike(tt.args.column, tt.args.value) 133 | if err := expr.Write(b); err != nil { 134 | t.Fatalf("unexpected error: %v", err) 135 | } 136 | if got := b.buf.String(); tt.want != got { 137 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 138 | } 139 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 140 | t.Errorf("args (-want, +got)\n%s", diff) 141 | } 142 | }) 143 | } 144 | } 145 | 146 | func TestIn(t *testing.T) { 147 | type args struct { 148 | column string 149 | args []interface{} 150 | } 151 | tests := []struct { 152 | name string 153 | args args 154 | want string 155 | wantArgs []interface{} 156 | }{ 157 | { 158 | name: "valid", 159 | args: args{ 160 | column: "col", 161 | args: []interface{}{ 162 | []uint8("hello"), 163 | }, 164 | }, 165 | want: "col IN (?)", 166 | wantArgs: []interface{}{ 167 | []uint8("hello"), 168 | }, 169 | }, 170 | } 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | b := &BuildCapture{ 174 | buf: strings.Builder{}, 175 | Args: []interface{}{}, 176 | } 177 | expr := sqb.In(tt.args.column, tt.args.args...) 178 | if err := expr.Write(b); err != nil { 179 | t.Fatalf("unexpected error: %v", err) 180 | } 181 | if got := b.buf.String(); tt.want != got { 182 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 183 | } 184 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 185 | t.Errorf("args (-want, +got)\n%s", diff) 186 | } 187 | }) 188 | } 189 | } 190 | 191 | func TestNotIn(t *testing.T) { 192 | type args struct { 193 | column string 194 | args []interface{} 195 | } 196 | tests := []struct { 197 | name string 198 | args args 199 | want string 200 | wantArgs []interface{} 201 | }{ 202 | { 203 | name: "valid", 204 | args: args{ 205 | column: "col", 206 | args: []interface{}{ 207 | []uint8("hello"), 208 | }, 209 | }, 210 | want: "col NOT IN (?)", 211 | wantArgs: []interface{}{ 212 | []uint8("hello"), 213 | }, 214 | }, 215 | } 216 | for _, tt := range tests { 217 | t.Run(tt.name, func(t *testing.T) { 218 | b := &BuildCapture{ 219 | buf: strings.Builder{}, 220 | Args: []interface{}{}, 221 | } 222 | expr := sqb.NotIn(tt.args.column, tt.args.args...) 223 | if err := expr.Write(b); err != nil { 224 | t.Fatalf("unexpected error: %v", err) 225 | } 226 | if got := b.buf.String(); tt.want != got { 227 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 228 | } 229 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 230 | t.Errorf("args (-want, +got)\n%s", diff) 231 | } 232 | }) 233 | } 234 | } 235 | 236 | func TestBetween(t *testing.T) { 237 | type args struct { 238 | column string 239 | left interface{} 240 | right interface{} 241 | } 242 | tests := []struct { 243 | name string 244 | args args 245 | want string 246 | wantArgs []interface{} 247 | }{ 248 | { 249 | name: "valid", 250 | args: args{ 251 | column: "col", 252 | left: 1, 253 | right: 2, 254 | }, 255 | want: "col BETWEEN ? AND ?", 256 | wantArgs: []interface{}{ 257 | 1, 2, 258 | }, 259 | }, 260 | } 261 | for _, tt := range tests { 262 | t.Run(tt.name, func(t *testing.T) { 263 | b := &BuildCapture{ 264 | buf: strings.Builder{}, 265 | Args: []interface{}{}, 266 | } 267 | expr := sqb.Between(tt.args.column, tt.args.left, tt.args.right) 268 | if err := expr.Write(b); err != nil { 269 | t.Fatalf("unexpected error: %v", err) 270 | } 271 | if got := b.buf.String(); tt.want != got { 272 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 273 | } 274 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 275 | t.Errorf("args (-want, +got)\n%s", diff) 276 | } 277 | }) 278 | } 279 | } 280 | 281 | func TestNotBetween(t *testing.T) { 282 | type args struct { 283 | column string 284 | left interface{} 285 | right interface{} 286 | } 287 | tests := []struct { 288 | name string 289 | args args 290 | want string 291 | wantArgs []interface{} 292 | }{ 293 | { 294 | name: "valid", 295 | args: args{ 296 | column: "col", 297 | left: 1, 298 | right: 2, 299 | }, 300 | want: "col NOT BETWEEN ? AND ?", 301 | wantArgs: []interface{}{ 302 | 1, 2, 303 | }, 304 | }, 305 | } 306 | for _, tt := range tests { 307 | t.Run(tt.name, func(t *testing.T) { 308 | b := &BuildCapture{ 309 | buf: strings.Builder{}, 310 | Args: []interface{}{}, 311 | } 312 | expr := sqb.NotBetween(tt.args.column, tt.args.left, tt.args.right) 313 | if err := expr.Write(b); err != nil { 314 | t.Fatalf("unexpected error: %v", err) 315 | } 316 | if got := b.buf.String(); tt.want != got { 317 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 318 | } 319 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 320 | t.Errorf("args (-want, +got)\n%s", diff) 321 | } 322 | }) 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /conjunction.go: -------------------------------------------------------------------------------- 1 | package sqb 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/Code-Hex/sqb/stmt" 7 | ) 8 | 9 | // Paren creates the expression with parentheses. 10 | func Paren(expr stmt.Expr) *stmt.Paren { 11 | return &stmt.Paren{ 12 | Expr: expr, 13 | } 14 | } 15 | 16 | // And creates statement for the AND boolean expression. 17 | // If you want to know more details, See at stmt.And. 18 | func And(left, right stmt.Expr, exprs ...stmt.Expr) *stmt.And { 19 | ret := &stmt.And{ 20 | Left: left, 21 | Right: right, 22 | } 23 | for _, expr := range exprs { 24 | ret = &stmt.And{ 25 | Left: ret, 26 | Right: expr, 27 | } 28 | } 29 | return ret 30 | } 31 | 32 | // Or creates statement for the OR boolean expression with parentheses. 33 | // If you want to know more details, See at stmt.Or. 34 | func Or(left, right stmt.Expr, exprs ...stmt.Expr) *stmt.Or { 35 | ret := &stmt.Or{ 36 | Left: left, 37 | Right: right, 38 | } 39 | for _, expr := range exprs { 40 | ret = &stmt.Or{ 41 | Left: ret, 42 | Right: expr, 43 | } 44 | } 45 | return ret 46 | } 47 | 48 | // AndFromMap Creates a concatenated string of AND boolean expression from a map. 49 | // 50 | // If there is no first argument then occurs panic. 51 | // If map length is zero it returns nil. 52 | // If map length is 1 it returns *stmt.Condition created by ConditionalFunc. 53 | func AndFromMap(f ConditionalFunc, m map[string]interface{}) stmt.Expr { 54 | if f == nil { 55 | panic("unspecified function") 56 | } 57 | if len(m) < 2 { 58 | // Length is zero 59 | for key, val := range m { 60 | return f(key, val) 61 | } 62 | // return nil if length is zero 63 | return nil 64 | } 65 | exprs := convertMapToStmts(f, m) 66 | return And(exprs[0], exprs[1], exprs[2:]...) 67 | } 68 | 69 | // OrFromMap Creates a concatenated string of OR boolean expression from a map. 70 | // 71 | // If there is no first argument then occurs panic. 72 | // If map length is zero it returns nil. 73 | // If map length is 1 it returns *stmt.Condition created by ConditionalFunc. 74 | func OrFromMap(f ConditionalFunc, m map[string]interface{}) stmt.Expr { 75 | if f == nil { 76 | panic("unspecified function") 77 | } 78 | if len(m) < 2 { 79 | // Length is zero 80 | for key, val := range m { 81 | return f(key, val) 82 | } 83 | // return nil if length is zero 84 | return nil 85 | } 86 | exprs := convertMapToStmts(f, m) 87 | return Or(exprs[0], exprs[1], exprs[2:]...) 88 | } 89 | 90 | func convertMapToStmts(f ConditionalFunc, m map[string]interface{}) []stmt.Expr { 91 | i, keys := 0, make([]string, len(m)) 92 | for key := range m { 93 | keys[i] = key 94 | i++ 95 | } 96 | // This is to guarantee the order 97 | // when concatenating strings 98 | sort.Strings(keys) 99 | 100 | exprs := make([]stmt.Expr, len(m)) 101 | for idx, key := range keys { 102 | exprs[idx] = f(key, m[key]) 103 | } 104 | return exprs 105 | } 106 | -------------------------------------------------------------------------------- /conjunction_test.go: -------------------------------------------------------------------------------- 1 | package sqb_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Code-Hex/sqb" 9 | "github.com/Code-Hex/sqb/stmt" 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestAnd(t *testing.T) { 14 | type args struct { 15 | left stmt.Expr 16 | right stmt.Expr 17 | exprs []stmt.Expr 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want string 23 | wantArgs []interface{} 24 | }{ 25 | { 26 | name: "simple", 27 | args: args{ 28 | left: sqb.Eq("category", "music"), 29 | right: sqb.Eq("category", "home appliances"), 30 | }, 31 | want: "category = ? AND category = ?", 32 | wantArgs: []interface{}{"music", "home appliances"}, 33 | }, 34 | { 35 | name: "complex", 36 | args: args{ 37 | left: sqb.Eq("category", "music"), 38 | right: sqb.Eq("category", "home appliances"), 39 | exprs: []stmt.Expr{ 40 | sqb.Ne("sub_category", 1), 41 | sqb.Le("sub_category", 2), 42 | }, 43 | }, 44 | want: "category = ? AND category = ? AND sub_category != ? AND sub_category <= ?", 45 | wantArgs: []interface{}{"music", "home appliances", 1, 2}, 46 | }, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | b := &BuildCapture{ 51 | buf: strings.Builder{}, 52 | Args: []interface{}{}, 53 | } 54 | expr := sqb.And(tt.args.left, tt.args.right, tt.args.exprs...) 55 | if err := expr.Write(b); err != nil { 56 | t.Fatalf("unexpected error: %v", err) 57 | } 58 | if got := b.buf.String(); tt.want != got { 59 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 60 | } 61 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 62 | t.Errorf("args (-want, +got)\n%s", diff) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestOr(t *testing.T) { 69 | type args struct { 70 | left stmt.Expr 71 | right stmt.Expr 72 | exprs []stmt.Expr 73 | } 74 | tests := []struct { 75 | name string 76 | args args 77 | want string 78 | wantArgs []interface{} 79 | }{ 80 | { 81 | name: "simple", 82 | args: args{ 83 | left: sqb.Eq("category", "music"), 84 | right: sqb.Eq("category", "home appliances"), 85 | }, 86 | want: "(category = ? OR category = ?)", 87 | wantArgs: []interface{}{"music", "home appliances"}, 88 | }, 89 | { 90 | name: "complex", 91 | args: args{ 92 | left: sqb.Eq("category", "music"), 93 | right: sqb.Eq("category", "home appliances"), 94 | exprs: []stmt.Expr{ 95 | sqb.Ge("sub_category", 1), 96 | sqb.Lt("sub_category", 2), 97 | }, 98 | }, 99 | want: "(((category = ? OR category = ?) OR sub_category >= ?) OR sub_category < ?)", 100 | wantArgs: []interface{}{"music", "home appliances", 1, 2}, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | b := &BuildCapture{ 106 | buf: strings.Builder{}, 107 | Args: []interface{}{}, 108 | } 109 | expr := sqb.Or(tt.args.left, tt.args.right, tt.args.exprs...) 110 | if err := expr.Write(b); err != nil { 111 | t.Fatalf("unexpected error: %v", err) 112 | } 113 | if got := b.buf.String(); tt.want != got { 114 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 115 | } 116 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 117 | t.Errorf("args (-want, +got)\n%s", diff) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func TestAndFromMap(t *testing.T) { 124 | type args struct { 125 | f sqb.ConditionalFunc 126 | m map[string]interface{} 127 | } 128 | tests := []struct { 129 | name string 130 | args args 131 | want string 132 | wantArgs []interface{} 133 | }{ 134 | { 135 | name: "an argument", 136 | args: args{ 137 | f: sqb.Gt, 138 | m: map[string]interface{}{ 139 | "col": 2, 140 | }, 141 | }, 142 | want: "col > ?", 143 | wantArgs: []interface{}{2}, 144 | }, 145 | { 146 | name: "two arguments", 147 | args: args{ 148 | f: sqb.Ne, 149 | m: map[string]interface{}{ 150 | "col": 2, 151 | "colcol": time.Time{}, 152 | }, 153 | }, 154 | want: "col != ? AND colcol != ?", 155 | wantArgs: []interface{}{2, time.Time{}}, 156 | }, 157 | { 158 | name: "three arguments", 159 | args: args{ 160 | f: sqb.Eq, 161 | m: map[string]interface{}{ 162 | "col": 2, 163 | "colcol": time.Time{}, 164 | "colcolcol": []byte("hello"), 165 | }, 166 | }, 167 | want: "col = ? AND colcol = ? AND colcolcol = ?", 168 | wantArgs: []interface{}{2, time.Time{}, []byte("hello")}, 169 | }, 170 | } 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | b := &BuildCapture{ 174 | buf: strings.Builder{}, 175 | Args: []interface{}{}, 176 | } 177 | expr := sqb.AndFromMap(tt.args.f, tt.args.m) 178 | if err := expr.Write(b); err != nil { 179 | t.Fatalf("unexpected error: %v", err) 180 | } 181 | if got := b.buf.String(); tt.want != got { 182 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 183 | } 184 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 185 | t.Errorf("args (-want, +got)\n%s", diff) 186 | } 187 | }) 188 | } 189 | } 190 | func TestAndByMap_nil_panic(t *testing.T) { 191 | got := sqb.AndFromMap(sqb.Lt, map[string]interface{}{}) 192 | if got != nil { 193 | t.Fatalf("expected nil") 194 | } 195 | defer func() { 196 | if v := recover(); v == nil { 197 | panic("expected panic") 198 | } 199 | }() 200 | sqb.AndFromMap(nil, map[string]interface{}{}) 201 | } 202 | 203 | func TestOrFromMap(t *testing.T) { 204 | type args struct { 205 | f sqb.ConditionalFunc 206 | m map[string]interface{} 207 | } 208 | tests := []struct { 209 | name string 210 | args args 211 | want string 212 | wantArgs []interface{} 213 | }{ 214 | { 215 | name: "an argument", 216 | args: args{ 217 | f: sqb.Gt, 218 | m: map[string]interface{}{ 219 | "col": 2, 220 | }, 221 | }, 222 | want: "col > ?", 223 | wantArgs: []interface{}{2}, 224 | }, 225 | { 226 | name: "two arguments", 227 | args: args{ 228 | f: sqb.Ne, 229 | m: map[string]interface{}{ 230 | "col": 2, 231 | "colcol": time.Time{}, 232 | }, 233 | }, 234 | want: "(col != ? OR colcol != ?)", 235 | wantArgs: []interface{}{2, time.Time{}}, 236 | }, 237 | { 238 | name: "three arguments", 239 | args: args{ 240 | f: sqb.Eq, 241 | m: map[string]interface{}{ 242 | "col": 2, 243 | "colcol": time.Time{}, 244 | "colcolcol": []byte("hello"), 245 | }, 246 | }, 247 | want: "((col = ? OR colcol = ?) OR colcolcol = ?)", 248 | wantArgs: []interface{}{2, time.Time{}, []byte("hello")}, 249 | }, 250 | } 251 | for _, tt := range tests { 252 | t.Run(tt.name, func(t *testing.T) { 253 | b := &BuildCapture{ 254 | buf: strings.Builder{}, 255 | Args: []interface{}{}, 256 | } 257 | expr := sqb.OrFromMap(tt.args.f, tt.args.m) 258 | if err := expr.Write(b); err != nil { 259 | t.Fatalf("unexpected error: %v", err) 260 | } 261 | if got := b.buf.String(); tt.want != got { 262 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 263 | } 264 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 265 | t.Errorf("args (-want, +got)\n%s", diff) 266 | } 267 | }) 268 | } 269 | } 270 | 271 | func TestOrByMap_nil_panic(t *testing.T) { 272 | got := sqb.OrFromMap(sqb.Lt, map[string]interface{}{}) 273 | if got != nil { 274 | t.Fatalf("expected nil") 275 | } 276 | defer func() { 277 | if v := recover(); v == nil { 278 | panic("expected panic") 279 | } 280 | }() 281 | sqb.OrFromMap(nil, map[string]interface{}{}) 282 | } 283 | 284 | func TestParen(t *testing.T) { 285 | tests := []struct { 286 | name string 287 | args stmt.Expr 288 | want string 289 | wantArgs []interface{} 290 | }{ 291 | { 292 | name: "valid", 293 | args: sqb.Eq("col", true), 294 | want: "(col = ?)", 295 | wantArgs: []interface{}{true}, 296 | }, 297 | { 298 | name: "valid AND", 299 | args: sqb.And( 300 | sqb.Eq("col", true), 301 | sqb.Ne("col2", 10), 302 | ), 303 | want: "(col = ? AND col2 != ?)", 304 | wantArgs: []interface{}{true, 10}, 305 | }, 306 | } 307 | for _, tt := range tests { 308 | t.Run(tt.name, func(t *testing.T) { 309 | b := &BuildCapture{ 310 | buf: strings.Builder{}, 311 | Args: []interface{}{}, 312 | } 313 | expr := sqb.Paren(tt.args) 314 | if err := expr.Write(b); err != nil { 315 | t.Fatalf("unexpected error: %v", err) 316 | } 317 | if got := b.buf.String(); tt.want != got { 318 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 319 | } 320 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 321 | t.Errorf("args (-want, +got)\n%s", diff) 322 | } 323 | }) 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Code-Hex/sqb 2 | 3 | go 1.13 4 | 5 | require github.com/google/go-cmp v0.3.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 2 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 3 | -------------------------------------------------------------------------------- /internal/pool/buffer.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | // Buffer is the interface that wraps the basic 4 | // Reset, Cap and WriteString method. 5 | type Buffer interface { 6 | Reset() 7 | Cap() int 8 | WriteString(string) (int, error) 9 | String() string 10 | } 11 | -------------------------------------------------------------------------------- /internal/pool/bytes_buffer.go: -------------------------------------------------------------------------------- 1 | // +build !go1.10 2 | 3 | package pool 4 | 5 | import ( 6 | "bytes" 7 | "sync" 8 | ) 9 | 10 | var _ Buffer = (*bytes.Buffer)(nil) 11 | 12 | var globalPool = sync.Pool{ 13 | New: func() interface{} { 14 | return &Builder{ 15 | buf: new(bytes.Buffer), 16 | args: make([]interface{}, 0, 3), 17 | } 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /internal/pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import "strconv" 4 | 5 | // These variables are the same as defined variables at sqb.go. 6 | const ( 7 | // Question represents a '?' placeholder parameter. 8 | Question = iota 9 | // Dollar represents a '$1', '$2'... placeholder parameters. 10 | Dollar 11 | // AtMark represents a '@1', '@2'... placeholder parameters. 12 | AtMark 13 | ) 14 | 15 | // Builder is the interface that wraps the basic 16 | // Reset, Cap and WriteString method. 17 | type Builder struct { 18 | Placeholder int 19 | 20 | buf Buffer 21 | args []interface{} 22 | counter int 23 | } 24 | 25 | // WritePlaceholder writes placeholder. 26 | func (b *Builder) WritePlaceholder() { 27 | switch b.Placeholder { 28 | case AtMark: 29 | b.counter++ 30 | b.buf.WriteString("@") 31 | b.buf.WriteString(strconv.Itoa(b.counter)) 32 | case Dollar: 33 | b.counter++ 34 | b.buf.WriteString("$") 35 | b.buf.WriteString(strconv.Itoa(b.counter)) 36 | case Question: 37 | fallthrough 38 | default: 39 | b.buf.WriteString("?") 40 | } 41 | } 42 | 43 | // String returns appended the contents. 44 | func (b *Builder) String() string { 45 | return b.buf.String() 46 | } 47 | 48 | // Args return appended args. 49 | func (b *Builder) Args() []interface{} { 50 | return b.args 51 | } 52 | 53 | // WriteString appends the contents of s to Buffer. 54 | // Builder.buf.WriteString doesn't have the potential to return 55 | // an error. But have the potential to panic. 56 | // 57 | // strings.Builder 58 | // https://golang.org/src/strings/builder.go?s=3425:3477#L110 59 | // 60 | // bytes.Buffer 61 | // https://golang.org/pkg/bytes/#Buffer.WriteString 62 | func (b *Builder) WriteString(s string) { 63 | b.buf.WriteString(s) 64 | } 65 | 66 | // AppendArgs appends the args. 67 | func (b *Builder) AppendArgs(args ...interface{}) { 68 | b.args = append(b.args, args...) 69 | } 70 | 71 | // Reset resets Builder. 72 | func (b *Builder) Reset() { 73 | b.args = []interface{}{} 74 | b.counter = 0 75 | // Proper usage of a sync.Pool requires each entry to have approximately 76 | // the same memory cost. To obtain this property when the stored type 77 | // contains a variably-sized buffer, we add a hard limit on the maximum buffer 78 | // to place back in the pool. 79 | // 80 | // See https://golang.org/issue/23199 81 | if b.buf.Cap() > limit { 82 | return 83 | } 84 | b.buf.Reset() 85 | } 86 | 87 | // Get allocates a new strings.Builder or grabs a cached one. 88 | func Get() *Builder { 89 | return globalPool.Get().(*Builder) 90 | } 91 | 92 | const limit = 64 << 10 93 | 94 | // Put saves used Builder; avoids an allocation per invocation. 95 | func Put(b *Builder) { 96 | b.Reset() 97 | globalPool.Put(b) 98 | } 99 | -------------------------------------------------------------------------------- /internal/pool/string_builder.go: -------------------------------------------------------------------------------- 1 | // +build go1.10 2 | 3 | package pool 4 | 5 | import ( 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | var _ Buffer = (*strings.Builder)(nil) 11 | 12 | var globalPool = sync.Pool{ 13 | New: func() interface{} { 14 | return &Builder{ 15 | buf: new(strings.Builder), 16 | args: make([]interface{}, 0, 3), 17 | } 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /internal/slice/slice.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | import ( 4 | "database/sql/driver" 5 | "reflect" 6 | ) 7 | 8 | // Flatten do flatten the input slice. 9 | func Flatten(args []interface{}) []interface{} { 10 | ret := make([]interface{}, 0, len(args)) 11 | for _, arg := range args { 12 | if driver.IsValue(arg) { 13 | ret = append(ret, arg) 14 | continue 15 | } 16 | 17 | v := reflect.ValueOf(arg) 18 | kind := v.Kind() 19 | if kind == reflect.Array || kind == reflect.Slice { 20 | ret = appendList(ret, v) 21 | } else { 22 | ret = append(ret, arg) 23 | } 24 | } 25 | return ret 26 | } 27 | 28 | func appendList(args []interface{}, v reflect.Value) []interface{} { 29 | vlen := v.Len() 30 | for i := 0; i < vlen; i++ { 31 | vv := v.Index(i) 32 | val := vv.Interface() 33 | 34 | if driver.IsValue(val) { 35 | args = append(args, val) 36 | continue 37 | } 38 | 39 | if vv.Kind() == reflect.Interface { 40 | vv = vv.Elem() 41 | } 42 | 43 | kind := vv.Kind() 44 | if kind == reflect.Array || kind == reflect.Slice { 45 | args = appendList(args, vv) 46 | } else { 47 | args = append(args, val) 48 | } 49 | } 50 | return args 51 | } 52 | -------------------------------------------------------------------------------- /internal/slice/slice_test.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestFlatten(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | args []interface{} 14 | want []interface{} 15 | }{ 16 | { 17 | name: "normal", 18 | args: []interface{}{ 19 | "hello", 20 | 1234, 21 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 22 | }, 23 | want: []interface{}{ 24 | "hello", 25 | 1234, 26 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 27 | }, 28 | }, 29 | { 30 | name: "contain []byte", 31 | args: []interface{}{ 32 | "hello", 33 | 1234, 34 | []byte("hello"), 35 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 36 | }, 37 | want: []interface{}{ 38 | "hello", 39 | 1234, 40 | []byte("hello"), 41 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 42 | }, 43 | }, 44 | { 45 | name: "contain []int8", 46 | args: []interface{}{ 47 | "hello", 48 | 1234, 49 | []uint8("hello"), 50 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 51 | }, 52 | want: []interface{}{ 53 | "hello", 54 | 1234, 55 | []uint8("hello"), 56 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 57 | }, 58 | }, 59 | { 60 | name: "contain []int", 61 | args: []interface{}{ 62 | "hello", 63 | 1234, 64 | []int{1, 2, 3}, 65 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 66 | }, 67 | want: []interface{}{ 68 | "hello", 69 | 1234, 70 | 1, 2, 3, 71 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 72 | }, 73 | }, 74 | { 75 | name: "contain []string", 76 | args: []interface{}{ 77 | "hello", 78 | 1234, 79 | []string{"a", "b", "c"}, 80 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 81 | }, 82 | want: []interface{}{ 83 | "hello", 84 | 1234, 85 | "a", "b", "c", 86 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 87 | }, 88 | }, 89 | { 90 | name: "contain []int64", 91 | args: []interface{}{ 92 | "hello", 93 | 1234, 94 | []int64{1, 2, 3}, 95 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 96 | }, 97 | want: []interface{}{ 98 | "hello", 99 | 1234, 100 | int64(1), int64(2), int64(3), 101 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 102 | }, 103 | }, 104 | { 105 | name: "contain []interface{}", 106 | args: []interface{}{ 107 | "hello", 108 | 1234, 109 | []interface{}{"a", 1, "b", 2}, 110 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 111 | }, 112 | want: []interface{}{ 113 | "hello", 114 | 1234, 115 | "a", 1, "b", 2, 116 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 117 | }, 118 | }, 119 | { 120 | name: "contain [][]int", 121 | args: []interface{}{ 122 | "hello", 123 | 1234, 124 | [][]int{ 125 | {1, 2, 3}, 126 | {5, 6, 7}, 127 | }, 128 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 129 | }, 130 | want: []interface{}{ 131 | "hello", 132 | 1234, 133 | 1, 2, 3, 134 | 5, 6, 7, 135 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 136 | }, 137 | }, 138 | { 139 | name: "contain [][]interface{}", 140 | args: []interface{}{ 141 | "hello", 142 | 1234, 143 | [][]interface{}{ 144 | {"a", 1}, 145 | {"b", 2}, 146 | }, 147 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 148 | }, 149 | want: []interface{}{ 150 | "hello", 151 | 1234, 152 | "a", 1, "b", 2, 153 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 154 | }, 155 | }, 156 | { 157 | name: "complex", 158 | args: []interface{}{ 159 | interface{}(1), 160 | [][]interface{}{ 161 | { 162 | 100, 163 | []interface{}{ 164 | "hello", 165 | []byte("world"), 166 | }, 167 | 200, 168 | }, 169 | {"b", 2}, 170 | }, 171 | [2]int{1, 2}, 172 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 173 | }, 174 | want: []interface{}{ 175 | 1, 176 | 100, 177 | "hello", 178 | []byte("world"), 179 | 200, 180 | "b", 2, 181 | 1, 2, 182 | time.Date(2019, 11, 4, 0, 0, 0, 0, time.UTC), 183 | }, 184 | }, 185 | } 186 | for _, tt := range tests { 187 | t.Run(tt.name, func(t *testing.T) { 188 | got := Flatten(tt.args) 189 | if diff := cmp.Diff(tt.want, got); diff != "" { 190 | t.Errorf("(-want, +got)\n%s\n%#v", diff, got) 191 | } 192 | }) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package sqb 2 | -------------------------------------------------------------------------------- /order_by.go: -------------------------------------------------------------------------------- 1 | package sqb 2 | 3 | import "github.com/Code-Hex/sqb/stmt" 4 | 5 | // OrderBy Creates an unary expression for ORDER BY. 6 | // If you want to know more details, See at stmt.OrderBy. 7 | func OrderBy(column string, desc bool) *stmt.OrderBy { 8 | return &stmt.OrderBy{ 9 | Column: column, 10 | Desc: desc, 11 | } 12 | } 13 | 14 | // OrderByList Creates an expression for ORDER BY from multiple *stmt.OrderBy. 15 | // 16 | // This function creates like ", DESC". The first argument 17 | // is required. If you want to know more details, See at stmt.OrderBy. 18 | func OrderByList(expr *stmt.OrderBy, exprs ...*stmt.OrderBy) *stmt.OrderBy { 19 | ret := expr 20 | for _, o := range exprs { 21 | tmp := &stmt.OrderBy{ 22 | Column: o.Column, 23 | Desc: o.Desc, 24 | } 25 | ret.Next = tmp 26 | ret = tmp 27 | } 28 | return expr 29 | } 30 | -------------------------------------------------------------------------------- /order_by_test.go: -------------------------------------------------------------------------------- 1 | package sqb_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/Code-Hex/sqb" 8 | "github.com/Code-Hex/sqb/stmt" 9 | ) 10 | 11 | func TestOrderBy(t *testing.T) { 12 | type args struct { 13 | column string 14 | desc bool 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want string 20 | }{ 21 | { 22 | name: "ASC", 23 | args: args{ 24 | column: "column", 25 | desc: false, 26 | }, 27 | want: "column", 28 | }, 29 | { 30 | name: "DESC", 31 | args: args{ 32 | column: "column", 33 | desc: true, 34 | }, 35 | want: "column DESC", 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | b := &BuildCapture{ 41 | buf: strings.Builder{}, 42 | Args: []interface{}{}, 43 | } 44 | expr := sqb.OrderBy(tt.args.column, tt.args.desc) 45 | if err := expr.Write(b); err != nil { 46 | t.Fatalf("unexpected error: %v", err) 47 | } 48 | if got := b.buf.String(); tt.want != got { 49 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func TestOrderByList(t *testing.T) { 56 | type args struct { 57 | expr *stmt.OrderBy 58 | exprs []*stmt.OrderBy 59 | } 60 | tests := []struct { 61 | name string 62 | args args 63 | want string 64 | }{ 65 | { 66 | name: "unary", 67 | args: args{ 68 | expr: sqb.OrderBy("column", false), 69 | }, 70 | want: "column", 71 | }, 72 | { 73 | name: "list", 74 | args: args{ 75 | expr: sqb.OrderBy("column1", true), 76 | exprs: []*stmt.OrderBy{ 77 | sqb.OrderBy("column2", true), 78 | sqb.OrderBy("column3", false), 79 | }, 80 | }, 81 | want: "column1 DESC, column2 DESC, column3", 82 | }, 83 | } 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | b := &BuildCapture{ 87 | buf: strings.Builder{}, 88 | Args: []interface{}{}, 89 | } 90 | expr := sqb.OrderByList(tt.args.expr, tt.args.exprs...) 91 | if err := expr.Write(b); err != nil { 92 | t.Fatalf("unexpected error: %v", err) 93 | } 94 | if got := b.buf.String(); tt.want != got { 95 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /sqb.go: -------------------------------------------------------------------------------- 1 | package sqb 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/Code-Hex/sqb/internal/pool" 8 | "github.com/Code-Hex/sqb/stmt" 9 | ) 10 | 11 | // There is build logic using placeholder in internal/pool.go. 12 | const ( 13 | // Question represents a '?' placeholder parameter. 14 | Question = iota 15 | // Dollar represents a '$1', '$2'... placeholder parameters. 16 | Dollar 17 | // AtMark represents a '@1', '@2'... placeholder parameters. 18 | AtMark 19 | ) 20 | 21 | // Option represents options to build sql query. 22 | type Option func(b *Builder) 23 | 24 | // SetPlaceholder sets placeholder. 25 | // 26 | // Default value is zero uses Question '?' as a placeholder. 27 | func SetPlaceholder(placeholder int) Option { 28 | return func(b *Builder) { 29 | b.placeholder = placeholder 30 | } 31 | } 32 | 33 | // Builder builds sql query string. 34 | type Builder struct { 35 | placeholder int 36 | stmt []stmt.Expr 37 | } 38 | 39 | // New returns sql query builder. 40 | func New(opts ...Option) *Builder { 41 | b := &Builder{} 42 | for _, opt := range opts { 43 | opt(b) 44 | } 45 | return b 46 | } 47 | 48 | // Bind binds expression to bindVars. returns copied *Builder which 49 | // bound expression. 50 | func (b *Builder) Bind(expr stmt.Expr) *Builder { 51 | // copy 52 | ret := *b 53 | copy(ret.stmt, b.stmt) 54 | // append to copied builder 55 | ret.stmt = append(b.stmt, expr) 56 | return &ret 57 | } 58 | 59 | // Build builds sql query string, returning the built query string 60 | // and a new arg list that can be executed by a database. The `query` should 61 | // use the `?` bindVar. The return value uses the `?` bindVar. 62 | func (b *Builder) Build(baseQuery string) (string, []interface{}, error) { 63 | q := baseQuery 64 | 65 | buf := pool.Get() 66 | defer pool.Put(buf) 67 | 68 | buf.Placeholder = b.placeholder 69 | 70 | // '?' <- bindVar 71 | var bindVars, offset int 72 | for i := strings.IndexByte(q, '?'); i != -1; i = strings.IndexByte(q, '?') { 73 | if bindVars >= len(b.stmt) { 74 | // If number of statements is less than bindVars, returns an error; 75 | return "", nil, errors.New("number of bindVars exceeds replaceable statements") 76 | } 77 | 78 | buf.WriteString(q[:i]) 79 | if err := b.stmt[bindVars].Write(buf); err != nil { 80 | return "", nil, err 81 | } 82 | bindVars++ 83 | offset += i + 1 84 | q = baseQuery[offset:] 85 | } 86 | buf.WriteString(q) 87 | 88 | return buf.String(), buf.Args(), nil 89 | } 90 | -------------------------------------------------------------------------------- /sqb_test.go: -------------------------------------------------------------------------------- 1 | package sqb_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/Code-Hex/sqb" 8 | "github.com/Code-Hex/sqb/stmt" 9 | "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | func TestBuilder_Build(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | sql string 16 | options []sqb.Option 17 | stmts []stmt.Expr 18 | want string 19 | wantArgs []interface{} 20 | wantErr bool 21 | }{ 22 | { 23 | name: "valid where between", 24 | sql: "SELECT * FROM tables WHERE ?", 25 | stmts: []stmt.Expr{ 26 | sqb.Between("name", 100, 200), 27 | }, 28 | want: "SELECT * FROM tables WHERE name BETWEEN ? AND ?", 29 | wantArgs: []interface{}{100, 200}, 30 | wantErr: false, 31 | }, 32 | { 33 | name: "valid where not between", 34 | sql: "SELECT * FROM tables WHERE ?", 35 | stmts: []stmt.Expr{ 36 | sqb.NotBetween("name", 100, 200), 37 | }, 38 | want: "SELECT * FROM tables WHERE name NOT BETWEEN ? AND ?", 39 | wantArgs: []interface{}{100, 200}, 40 | wantErr: false, 41 | }, 42 | { 43 | name: "valid condition", 44 | sql: "SELECT * FROM tables WHERE ? AND ?", 45 | stmts: []stmt.Expr{ 46 | sqb.Eq("name", "taro"), 47 | sqb.Ne("category", 10), 48 | }, 49 | want: "SELECT * FROM tables WHERE name = ? AND category != ?", 50 | wantArgs: []interface{}{"taro", 10}, 51 | wantErr: false, 52 | }, 53 | { 54 | name: "valid conject twice", 55 | sql: "SELECT * FROM tables WHERE ?", 56 | stmts: []stmt.Expr{ 57 | sqb.And( 58 | sqb.Or( 59 | sqb.Eq("category", 1), 60 | sqb.Eq("category", 2), 61 | ), 62 | sqb.Or( 63 | sqb.NotIn("brand", []string{ 64 | "apple", "sony", "google", 65 | }), 66 | sqb.NotLike("name", "abc%"), 67 | ), 68 | ), 69 | }, 70 | want: "SELECT * FROM tables WHERE (category = ? OR category = ?) AND (brand NOT IN (?, ?, ?) OR name NOT LIKE ?)", 71 | wantArgs: []interface{}{1, 2, "apple", "sony", "google", "abc%"}, 72 | wantErr: false, 73 | }, 74 | { 75 | name: "valid conject twice with postgresql", 76 | sql: "SELECT * FROM tables WHERE ?", 77 | options: []sqb.Option{ 78 | sqb.SetPlaceholder(sqb.Dollar), 79 | }, 80 | stmts: []stmt.Expr{ 81 | sqb.And( 82 | sqb.Or( 83 | sqb.Eq("category", 1), 84 | sqb.Eq("category", 2), 85 | ), 86 | sqb.Or( 87 | sqb.NotIn("brand", []string{ 88 | "apple", "sony", "google", 89 | }), 90 | sqb.NotLike("name", "abc%"), 91 | ), 92 | ), 93 | }, 94 | want: "SELECT * FROM tables WHERE (category = $1 OR category = $2) AND (brand NOT IN ($3, $4, $5) OR name NOT LIKE $6)", 95 | wantArgs: []interface{}{1, 2, "apple", "sony", "google", "abc%"}, 96 | wantErr: false, 97 | }, 98 | { 99 | name: "valid conject twice with spanner", 100 | sql: "SELECT * FROM tables WHERE ?", 101 | options: []sqb.Option{ 102 | sqb.SetPlaceholder(sqb.AtMark), 103 | }, 104 | stmts: []stmt.Expr{ 105 | sqb.And( 106 | sqb.Or( 107 | sqb.Eq("category", 1), 108 | sqb.Eq("category", 2), 109 | ), 110 | sqb.Or( 111 | sqb.NotIn("brand", []string{ 112 | "apple", "sony", "google", 113 | }), 114 | sqb.NotLike("name", "abc%"), 115 | ), 116 | ), 117 | }, 118 | want: "SELECT * FROM tables WHERE (category = @1 OR category = @2) AND (brand NOT IN (@3, @4, @5) OR name NOT LIKE @6)", 119 | wantArgs: []interface{}{1, 2, "apple", "sony", "google", "abc%"}, 120 | wantErr: false, 121 | }, 122 | { 123 | name: "invalid bindVars exceeds replaceable statements", 124 | sql: "SELECT * FROM tables WHERE ?", 125 | stmts: []stmt.Expr{}, 126 | want: "", 127 | wantArgs: nil, 128 | wantErr: true, 129 | }, 130 | { 131 | name: "invalid build error", 132 | sql: "SELECT * FROM tables WHERE ?", 133 | stmts: []stmt.Expr{ 134 | &ExprMock{ 135 | WriteMock: func(stmt.Builder) error { 136 | return errors.New("error") 137 | }, 138 | }, 139 | }, 140 | want: "", 141 | wantArgs: nil, 142 | wantErr: true, 143 | }, 144 | } 145 | for _, tt := range tests { 146 | t.Run(tt.name, func(t *testing.T) { 147 | b := sqb.New(tt.options...) 148 | for _, expr := range tt.stmts { 149 | b = b.Bind(expr) 150 | } 151 | got, args, err := b.Build(tt.sql) 152 | if (err != nil) != tt.wantErr { 153 | t.Errorf("Builder.Build() error = %v, wantErr %v", err, tt.wantErr) 154 | return 155 | } 156 | if got != tt.want { 157 | t.Errorf("sql\ngot = %q\nwant %q", got, tt.want) 158 | } 159 | if diff := cmp.Diff(tt.wantArgs, args); diff != "" { 160 | t.Errorf("args (-want, +got)\n%s", diff) 161 | } 162 | }) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /stmt/columns.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import "errors" 4 | 5 | // Columns represents columns field. 6 | type Columns []string 7 | 8 | // Write writes a string with concatenated column names. 9 | // i.e. "col1", "col1, col2, col3" 10 | func (c Columns) Write(b Builder) error { 11 | switch len(c) { 12 | case 0: 13 | return errors.New("unspecified columns") 14 | case 1: 15 | b.WriteString(c[0]) 16 | return nil 17 | } 18 | b.WriteString(c[0]) 19 | for _, column := range c[1:] { 20 | b.WriteString(", ") 21 | b.WriteString(column) 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /stmt/columns_test.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestColumns_Write(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | c Columns 12 | want string 13 | wantErr bool 14 | }{ 15 | { 16 | name: "invalid no column", 17 | c: Columns{}, 18 | want: "", 19 | wantErr: true, 20 | }, 21 | { 22 | name: "valid a column", 23 | c: Columns{"col"}, 24 | want: "col", 25 | wantErr: false, 26 | }, 27 | { 28 | name: "valid some columns", 29 | c: Columns{"col1", "col2", "col3"}, 30 | want: "col1, col2, col3", 31 | wantErr: false, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | b := &BuildCapture{ 37 | buf: strings.Builder{}, 38 | Args: []interface{}{}, 39 | } 40 | if err := tt.c.Write(b); (err != nil) != tt.wantErr { 41 | t.Errorf("Columns.Write() error = %v, wantErr %v", err, tt.wantErr) 42 | } 43 | if !tt.wantErr { 44 | if got := b.buf.String(); tt.want != got { 45 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 46 | } 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /stmt/compare.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/Code-Hex/sqb/internal/slice" 7 | ) 8 | 9 | var ( 10 | _ Comparisoner = (*CompOp)(nil) 11 | _ Comparisoner = (*CompLike)(nil) 12 | _ Comparisoner = (*CompBetween)(nil) 13 | _ Comparisoner = (*CompIn)(nil) 14 | ) 15 | 16 | // CompOp represents condition for using operators. 17 | // 18 | // Op field should contain "=", ">=", ">", "<=", "<", "!=", "IS", "IS NOT" 19 | // Value field should set the value to use for comparison. 20 | type CompOp struct { 21 | Op string 22 | Value interface{} 23 | } 24 | 25 | // WriteComparison implemented Comparisoner interface. 26 | func (c *CompOp) WriteComparison(b Builder) error { 27 | b.WriteString(c.Op) 28 | b.WriteString(" ") 29 | b.WritePlaceholder() 30 | b.AppendArgs(c.Value) 31 | return nil 32 | } 33 | 34 | // CompLike represents condition for using "LIKE". 35 | // 36 | // If enabled Negative field, it's meaning use "NOT LIKE". 37 | // Value field should set the value to use for comparison. 38 | type CompLike struct { 39 | Negative bool 40 | Value interface{} 41 | } 42 | 43 | // WriteComparison implemented Comparisoner interface. 44 | func (c *CompLike) WriteComparison(b Builder) error { 45 | if c.Negative { 46 | b.WriteString("NOT ") 47 | } 48 | b.WriteString("LIKE ") 49 | b.WritePlaceholder() 50 | b.AppendArgs(c.Value) 51 | return nil 52 | } 53 | 54 | // CompBetween represents condition for using "BETWEEN". 55 | // 56 | // If enabled Negative field, it's meaning use "NOT BETWEEN". 57 | // This struct will convert to be like "BETWEEN left_expr AND right_expr". 58 | type CompBetween struct { 59 | Negative bool 60 | Left interface{} 61 | Right interface{} 62 | } 63 | 64 | // WriteComparison implemented Comparisoner interface. 65 | func (c *CompBetween) WriteComparison(b Builder) error { 66 | if c.Left == nil { 67 | return errors.New("unset Left Value in CompBetween") 68 | } 69 | if c.Right == nil { 70 | return errors.New("unset Right Value in CompBetween") 71 | } 72 | if c.Negative { 73 | b.WriteString("NOT ") 74 | } 75 | b.WriteString("BETWEEN ") 76 | b.WritePlaceholder() 77 | b.WriteString(" AND ") 78 | b.WritePlaceholder() 79 | b.AppendArgs(c.Left, c.Right) 80 | return nil 81 | } 82 | 83 | // CompIn represents condition for using "IN". 84 | // 85 | // If enabled Negative field, it's meaning use "NOT IN". 86 | // Values field should set list to use for comparison. 87 | // This struct will convert to be like "IN (?, ?, ?)". 88 | type CompIn struct { 89 | Negative bool 90 | Values []interface{} 91 | } 92 | 93 | // WriteComparison implemented Comparisoner interface. 94 | func (c *CompIn) WriteComparison(b Builder) error { 95 | if c.Negative { 96 | b.WriteString("NOT ") 97 | } 98 | b.WriteString("IN (") 99 | args := slice.Flatten(c.Values) 100 | if err := makePlaceholders(b, args); err != nil { 101 | return err 102 | } 103 | b.WriteString(")") 104 | b.AppendArgs(args...) 105 | return nil 106 | } 107 | 108 | func makePlaceholders(b Builder, args []interface{}) error { 109 | const sep = ", " 110 | switch len(args) { 111 | case 0: 112 | return errors.New("it should be passed at least more than 1") 113 | case 1: 114 | b.WritePlaceholder() 115 | return nil 116 | } 117 | 118 | b.WritePlaceholder() 119 | for range args[1:] { 120 | b.WriteString(sep) 121 | b.WritePlaceholder() 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /stmt/compare_test.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | // "=", ">=", ">", "<=", "<", "!=", "IS", "IS NOT" 13 | func TestCompOp_WriteComparison(t *testing.T) { 14 | tests := []string{ 15 | "=", ">=", ">", "<=", "<", "!=", "IS", "IS NOT", 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt, func(t *testing.T) { 19 | c := &CompOp{ 20 | Op: tt, 21 | Value: 1, 22 | } 23 | b := &BuildCapture{ 24 | buf: strings.Builder{}, 25 | Args: []interface{}{}, 26 | } 27 | if err := c.WriteComparison(b); err != nil { 28 | t.Fatalf("CompOp.WriteComparison() error = %v", err) 29 | } 30 | got := b.buf.String() 31 | want := fmt.Sprintf("%s ?", tt) // = ?, >= ?, IS ?, etc... 32 | if want != got { 33 | t.Errorf("\nwant: %q\ngot: %q", want, got) 34 | } 35 | 36 | wantArgs := []interface{}{1} 37 | if diff := cmp.Diff(wantArgs, b.Args); diff != "" { 38 | t.Errorf("args (-want, +got)\n%s", diff) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestCompLike_WriteComparison(t *testing.T) { 45 | tests := []struct { 46 | name string 47 | c *CompLike 48 | want string 49 | wantArgs []interface{} 50 | }{ 51 | { 52 | name: "LIKE", 53 | c: &CompLike{ 54 | Negative: false, 55 | Value: "abc%", 56 | }, 57 | want: "LIKE ?", 58 | wantArgs: []interface{}{"abc%"}, 59 | }, 60 | { 61 | name: "NOT LIKE", 62 | c: &CompLike{ 63 | Negative: true, 64 | Value: "abc%", 65 | }, 66 | want: "NOT LIKE ?", 67 | wantArgs: []interface{}{"abc%"}, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | b := &BuildCapture{ 73 | buf: strings.Builder{}, 74 | Args: []interface{}{}, 75 | } 76 | if err := tt.c.WriteComparison(b); err != nil { 77 | t.Fatalf("CompLike.WriteComparison() error = %v", err) 78 | } 79 | if got := b.buf.String(); tt.want != got { 80 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 81 | } 82 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 83 | t.Errorf("args (-want, +got)\n%s", diff) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestCompBetween_WriteComparison(t *testing.T) { 90 | tests := []struct { 91 | name string 92 | c *CompBetween 93 | want string 94 | wantArgs []interface{} 95 | wantErr bool 96 | }{ 97 | { 98 | name: "valid BETWEEN", 99 | c: &CompBetween{ 100 | Negative: false, 101 | Left: 100, 102 | Right: 200, 103 | }, 104 | want: "BETWEEN ? AND ?", 105 | wantArgs: []interface{}{100, 200}, 106 | wantErr: false, 107 | }, 108 | { 109 | name: "valid NOT BETWEEN", 110 | c: &CompBetween{ 111 | Negative: true, 112 | Left: time.Date(2014, 2, 1, 0, 0, 0, 0, time.UTC), 113 | Right: time.Date(2014, 2, 28, 0, 0, 0, 0, time.UTC), 114 | }, 115 | want: "NOT BETWEEN ? AND ?", 116 | wantArgs: []interface{}{ 117 | time.Date(2014, 2, 1, 0, 0, 0, 0, time.UTC), 118 | time.Date(2014, 2, 28, 0, 0, 0, 0, time.UTC), 119 | }, 120 | wantErr: false, 121 | }, 122 | { 123 | name: "invalid Left value", 124 | c: &CompBetween{ 125 | Negative: false, 126 | Left: nil, 127 | Right: 200, 128 | }, 129 | want: "", 130 | wantArgs: []interface{}{}, 131 | wantErr: true, 132 | }, 133 | { 134 | name: "invalid Right value", 135 | c: &CompBetween{ 136 | Negative: true, 137 | Left: 100, 138 | Right: nil, 139 | }, 140 | want: "", 141 | wantArgs: []interface{}{}, 142 | wantErr: true, 143 | }, 144 | } 145 | for _, tt := range tests { 146 | t.Run(tt.name, func(t *testing.T) { 147 | b := &BuildCapture{ 148 | buf: strings.Builder{}, 149 | Args: []interface{}{}, 150 | } 151 | if err := tt.c.WriteComparison(b); (err != nil) != tt.wantErr { 152 | t.Errorf("CompBetween.WriteComparison() error = %v, wantErr %v", err, tt.wantErr) 153 | } 154 | if !tt.wantErr { 155 | if got := b.buf.String(); tt.want != got { 156 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 157 | } 158 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 159 | t.Errorf("args (-want, +got)\n%s", diff) 160 | } 161 | } 162 | }) 163 | } 164 | } 165 | 166 | func TestCompIn_WriteComparison(t *testing.T) { 167 | tests := []struct { 168 | name string 169 | c *CompIn 170 | want string 171 | wantArgs []interface{} 172 | wantErr bool 173 | }{ 174 | { 175 | name: "valid IN", 176 | c: &CompIn{ 177 | Negative: false, 178 | Values: []interface{}{1}, 179 | }, 180 | want: "IN (?)", 181 | wantArgs: []interface{}{1}, 182 | wantErr: false, 183 | }, 184 | { 185 | name: "valid nested list", 186 | c: &CompIn{ 187 | Negative: false, 188 | Values: []interface{}{ 189 | []int{1, 2}, 190 | []interface{}{ 191 | 100, 192 | []interface{}{"hello", []byte("world")}, 193 | 500, 194 | }, 195 | }, 196 | }, 197 | want: "IN (?, ?, ?, ?, ?, ?)", 198 | wantArgs: []interface{}{ 199 | 1, 2, 200 | 100, 201 | "hello", []byte("world"), 202 | 500, 203 | }, 204 | wantErr: false, 205 | }, 206 | { 207 | name: "valid NOT IN", 208 | c: &CompIn{ 209 | Negative: true, 210 | Values: []interface{}{1, 2, 3}, 211 | }, 212 | want: "NOT IN (?, ?, ?)", 213 | wantArgs: []interface{}{1, 2, 3}, 214 | wantErr: false, 215 | }, 216 | { 217 | name: "invalid", 218 | c: &CompIn{ 219 | Negative: false, 220 | Values: []interface{}{}, 221 | }, 222 | want: "", 223 | wantArgs: []interface{}{}, 224 | wantErr: true, 225 | }, 226 | } 227 | for _, tt := range tests { 228 | t.Run(tt.name, func(t *testing.T) { 229 | b := &BuildCapture{ 230 | buf: strings.Builder{}, 231 | Args: []interface{}{}, 232 | } 233 | if err := tt.c.WriteComparison(b); (err != nil) != tt.wantErr { 234 | t.Errorf("CompIn.WriteComparison() error = %v, wantErr %v", err, tt.wantErr) 235 | } 236 | if !tt.wantErr { 237 | if got := b.buf.String(); tt.want != got { 238 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 239 | } 240 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 241 | t.Errorf("args (-want, +got)\n%s", diff) 242 | } 243 | } 244 | }) 245 | } 246 | } 247 | 248 | func Test_makePlaceholders(t *testing.T) { 249 | makeArgs := func(i int) []interface{} { 250 | ret := make([]interface{}, i) 251 | for idx := range ret { 252 | ret[idx] = 0 253 | } 254 | return ret 255 | } 256 | tests := []struct { 257 | name string 258 | args int 259 | want string 260 | wantErr bool 261 | }{ 262 | { 263 | name: "invalid 0 arguments", 264 | args: 0, 265 | wantErr: true, 266 | }, 267 | { 268 | name: "valid 1 arguments", 269 | args: 1, 270 | want: "?", 271 | wantErr: false, 272 | }, 273 | { 274 | name: "valid 2 arguments", 275 | args: 2, 276 | want: "?, ?", 277 | wantErr: false, 278 | }, 279 | { 280 | name: "valid 5 arguments", 281 | args: 5, 282 | want: "?, ?, ?, ?, ?", 283 | wantErr: false, 284 | }, 285 | } 286 | for _, tt := range tests { 287 | t.Run(tt.name, func(t *testing.T) { 288 | b := &BuildCapture{ 289 | buf: strings.Builder{}, 290 | Args: []interface{}{}, 291 | } 292 | args := makeArgs(tt.args) 293 | if err := makePlaceholders(b, args); (err != nil) != tt.wantErr { 294 | t.Errorf("makePlaceholders() error = %v, wantErr %v", err, tt.wantErr) 295 | } 296 | if got := b.buf.String(); tt.want != got { 297 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 298 | } 299 | }) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /stmt/condition.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var _ Expr = (*Condition)(nil) 8 | 9 | // Condition represents condition for using Comparisoner interface. 10 | // 11 | // this struct creates " " 12 | // indicates Comparisoner interface. 13 | type Condition struct { 14 | Column string 15 | Compare Comparisoner 16 | } 17 | 18 | // Write implements Expr interface. 19 | // 20 | // For example: 21 | // category = "music" 22 | // category != "music" 23 | // category LIKE "music" 24 | // category NOT LIKE "music" 25 | // category IN ("music", "video") 26 | // category NOT IN ("music", "video") 27 | func (c *Condition) Write(b Builder) error { 28 | b.WriteString(c.Column) 29 | if c.Compare == nil { 30 | return errors.New("unset Compare in condition") 31 | } 32 | b.WriteString(" ") 33 | return c.Compare.WriteComparison(b) 34 | } 35 | -------------------------------------------------------------------------------- /stmt/condition_test.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestCondition_Write(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | c *Condition 15 | want string 16 | wantArgs []interface{} 17 | wantErr bool 18 | }{ 19 | { 20 | name: "valid", 21 | c: &Condition{ 22 | Column: "name", 23 | Compare: &CompOp{ 24 | Op: "=", 25 | Value: "taro", 26 | }, 27 | }, 28 | want: "name = ?", 29 | wantArgs: []interface{}{"taro"}, 30 | wantErr: false, 31 | }, 32 | { 33 | name: "invalid nil compare", 34 | c: &Condition{ 35 | Column: "name", 36 | Compare: nil, 37 | }, 38 | want: "", 39 | wantArgs: []interface{}{}, 40 | wantErr: true, 41 | }, 42 | { 43 | name: "invalid write compare", 44 | c: &Condition{ 45 | Column: "name", 46 | Compare: &ComparisonerMock{ 47 | WriteComparisonMock: func(Builder) error { 48 | return errors.New("error") 49 | }, 50 | }, 51 | }, 52 | want: "", 53 | wantArgs: []interface{}{}, 54 | wantErr: true, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | b := &BuildCapture{ 60 | buf: strings.Builder{}, 61 | Args: []interface{}{}, 62 | } 63 | if err := tt.c.Write(b); (err != nil) != tt.wantErr { 64 | t.Errorf("Condition.Write() error = %v, wantErr %v", err, tt.wantErr) 65 | } 66 | if !tt.wantErr { 67 | if got := b.buf.String(); tt.want != got { 68 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 69 | } 70 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 71 | t.Errorf("args (-want, +got)\n%s", diff) 72 | } 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /stmt/conjunction.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import "errors" 4 | 5 | var ( 6 | _ Expr = (*Paren)(nil) 7 | _ Expr = (*Or)(nil) 8 | _ Expr = (*And)(nil) 9 | ) 10 | 11 | // Paren represents a parenthesized expression. 12 | type Paren struct { 13 | Expr Expr 14 | } 15 | 16 | // Write writes the expression with parentheses. 17 | func (p *Paren) Write(b Builder) error { 18 | if p.Expr == nil { 19 | return errors.New("unset Expr in Paren") 20 | } 21 | b.WriteString("(") 22 | if err := p.Expr.Write(b); err != nil { 23 | return err 24 | } 25 | b.WriteString(")") 26 | return nil 27 | } 28 | 29 | // Or represents an OR boolean expression. 30 | type Or struct { 31 | Left Expr 32 | Right Expr 33 | } 34 | 35 | // Write writes the OR boolean expression with parentheses. 36 | // Currently, the OR operator is the only one that's lower precedence 37 | // than AND on most of databases. 38 | func (o *Or) Write(b Builder) error { 39 | if o.Left == nil { 40 | return errors.New("unset Left Expr in OR") 41 | } 42 | if o.Right == nil { 43 | return errors.New("unset Right Expr in OR") 44 | } 45 | b.WriteString("(") 46 | if err := o.Left.Write(b); err != nil { 47 | return err 48 | } 49 | b.WriteString(" OR ") 50 | if err := o.Right.Write(b); err != nil { 51 | return err 52 | } 53 | b.WriteString(")") 54 | return nil 55 | } 56 | 57 | // And represents an And boolean expression. 58 | type And struct { 59 | Left Expr 60 | Right Expr 61 | } 62 | 63 | // Write writes the AND boolean expression. 64 | func (a *And) Write(b Builder) error { 65 | if a.Left == nil { 66 | return errors.New("unset Left Expr in And") 67 | } 68 | if a.Right == nil { 69 | return errors.New("unset Right Expr in And") 70 | } 71 | if err := a.Left.Write(b); err != nil { 72 | return err 73 | } 74 | b.WriteString(" AND ") 75 | return a.Right.Write(b) 76 | } 77 | -------------------------------------------------------------------------------- /stmt/conjunction_test.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestParen_Write(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | p *Paren 15 | want string 16 | wantArgs []interface{} 17 | wantErr bool 18 | }{ 19 | { 20 | name: "valid", 21 | p: &Paren{ 22 | Expr: &Condition{ 23 | Column: "hello", 24 | Compare: &CompOp{ 25 | Op: "=", 26 | Value: 10, 27 | }, 28 | }, 29 | }, 30 | want: "(hello = ?)", 31 | wantArgs: []interface{}{10}, 32 | wantErr: false, 33 | }, 34 | { 35 | name: "invalid nil", 36 | p: &Paren{ 37 | Expr: nil, 38 | }, 39 | want: "", 40 | wantArgs: []interface{}{}, 41 | wantErr: true, 42 | }, 43 | { 44 | name: "invalid", 45 | p: &Paren{ 46 | Expr: &ExprMock{ 47 | WriteMock: func(b Builder) error { 48 | return errors.New("error") 49 | }, 50 | }, 51 | }, 52 | want: "", 53 | wantArgs: []interface{}{}, 54 | wantErr: true, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | b := &BuildCapture{ 60 | buf: strings.Builder{}, 61 | Args: []interface{}{}, 62 | } 63 | if err := tt.p.Write(b); (err != nil) != tt.wantErr { 64 | t.Errorf("Paren.Write() error = %v, wantErr %v", err, tt.wantErr) 65 | } 66 | if !tt.wantErr { 67 | if got := b.buf.String(); tt.want != got { 68 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 69 | } 70 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 71 | t.Errorf("args (-want, +got)\n%s", diff) 72 | } 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func TestOr_Write(t *testing.T) { 79 | tests := []struct { 80 | name string 81 | o *Or 82 | want string 83 | wantArgs []interface{} 84 | wantErr bool 85 | }{ 86 | { 87 | name: "valid", 88 | o: &Or{ 89 | Left: &Condition{ 90 | Column: "hello", 91 | Compare: &CompLike{ 92 | Negative: false, 93 | Value: "world", 94 | }, 95 | }, 96 | Right: &Condition{ 97 | Column: "world", 98 | Compare: &CompBetween{ 99 | Negative: false, 100 | Left: 10, 101 | Right: 300, 102 | }, 103 | }, 104 | }, 105 | want: "(hello LIKE ? OR world BETWEEN ? AND ?)", 106 | wantArgs: []interface{}{ 107 | "world", 108 | 10, 300, 109 | }, 110 | wantErr: false, 111 | }, 112 | { 113 | name: "invalid Left is nil", 114 | o: &Or{ 115 | Left: nil, 116 | }, 117 | want: "", 118 | wantArgs: []interface{}{}, 119 | wantErr: true, 120 | }, 121 | { 122 | name: "invalid Left", 123 | o: &Or{ 124 | Left: &ExprMock{ 125 | WriteMock: func(Builder) error { 126 | return errors.New("error") 127 | }, 128 | }, 129 | Right: &ExprMock{ 130 | WriteMock: func(Builder) error { 131 | return nil 132 | }, 133 | }, 134 | }, 135 | want: "", 136 | wantArgs: []interface{}{}, 137 | wantErr: true, 138 | }, 139 | { 140 | name: "invalid Right is nil", 141 | o: &Or{ 142 | Left: &ExprMock{}, 143 | Right: nil, 144 | }, 145 | want: "", 146 | wantArgs: []interface{}{}, 147 | wantErr: true, 148 | }, 149 | { 150 | name: "invalid Right", 151 | o: &Or{ 152 | Left: &ExprMock{ 153 | WriteMock: func(Builder) error { 154 | return nil 155 | }, 156 | }, 157 | Right: &ExprMock{ 158 | WriteMock: func(Builder) error { 159 | return errors.New("error") 160 | }, 161 | }, 162 | }, 163 | want: "", 164 | wantArgs: []interface{}{}, 165 | wantErr: true, 166 | }, 167 | } 168 | for _, tt := range tests { 169 | t.Run(tt.name, func(t *testing.T) { 170 | b := &BuildCapture{ 171 | buf: strings.Builder{}, 172 | Args: []interface{}{}, 173 | } 174 | if err := tt.o.Write(b); (err != nil) != tt.wantErr { 175 | t.Errorf("Or.Write() error = %v, wantErr %v", err, tt.wantErr) 176 | } 177 | if !tt.wantErr { 178 | if got := b.buf.String(); tt.want != got { 179 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 180 | } 181 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 182 | t.Errorf("args (-want, +got)\n%s", diff) 183 | } 184 | } 185 | }) 186 | } 187 | } 188 | 189 | func TestAnd_Write(t *testing.T) { 190 | tests := []struct { 191 | name string 192 | a *And 193 | want string 194 | wantArgs []interface{} 195 | wantErr bool 196 | }{ 197 | { 198 | name: "valid", 199 | a: &And{ 200 | Left: &Condition{ 201 | Column: "hello", 202 | Compare: &CompLike{ 203 | Negative: false, 204 | Value: "world", 205 | }, 206 | }, 207 | Right: &Condition{ 208 | Column: "world", 209 | Compare: &CompBetween{ 210 | Negative: false, 211 | Left: 10, 212 | Right: 300, 213 | }, 214 | }, 215 | }, 216 | want: "hello LIKE ? AND world BETWEEN ? AND ?", 217 | wantArgs: []interface{}{ 218 | "world", 219 | 10, 300, 220 | }, 221 | wantErr: false, 222 | }, 223 | { 224 | name: "invalid Left is nil", 225 | a: &And{ 226 | Left: nil, 227 | }, 228 | want: "", 229 | wantArgs: []interface{}{}, 230 | wantErr: true, 231 | }, 232 | { 233 | name: "invalid Left", 234 | a: &And{ 235 | Left: &ExprMock{ 236 | WriteMock: func(Builder) error { 237 | return errors.New("error") 238 | }, 239 | }, 240 | Right: &ExprMock{ 241 | WriteMock: func(Builder) error { 242 | return nil 243 | }, 244 | }, 245 | }, 246 | want: "", 247 | wantArgs: []interface{}{}, 248 | wantErr: true, 249 | }, 250 | { 251 | name: "invalid Right is nil", 252 | a: &And{ 253 | Left: &ExprMock{}, 254 | Right: nil, 255 | }, 256 | want: "", 257 | wantArgs: []interface{}{}, 258 | wantErr: true, 259 | }, 260 | { 261 | name: "invalid Right", 262 | a: &And{ 263 | Left: &ExprMock{ 264 | WriteMock: func(Builder) error { 265 | return nil 266 | }, 267 | }, 268 | Right: &ExprMock{ 269 | WriteMock: func(Builder) error { 270 | return errors.New("error") 271 | }, 272 | }, 273 | }, 274 | want: "", 275 | wantArgs: []interface{}{}, 276 | wantErr: true, 277 | }, 278 | } 279 | for _, tt := range tests { 280 | t.Run(tt.name, func(t *testing.T) { 281 | b := &BuildCapture{ 282 | buf: strings.Builder{}, 283 | Args: []interface{}{}, 284 | } 285 | if err := tt.a.Write(b); (err != nil) != tt.wantErr { 286 | t.Errorf("And.Write() error = %v, wantErr %v", err, tt.wantErr) 287 | } 288 | if !tt.wantErr { 289 | if got := b.buf.String(); tt.want != got { 290 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 291 | } 292 | if diff := cmp.Diff(tt.wantArgs, b.Args); diff != "" { 293 | t.Errorf("args (-want, +got)\n%s", diff) 294 | } 295 | } 296 | }) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /stmt/error.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | var _ error = (*BuildError)(nil) 4 | 5 | // BuildError is the error type usually returned by functions in the stmt 6 | // package. It describes the current operation, occurred of 7 | // an error. 8 | type BuildError struct { 9 | Op string 10 | Err error 11 | } 12 | 13 | func (b *BuildError) Error() string { 14 | if b == nil { 15 | return "" 16 | } 17 | return b.Op + ": " + b.Err.Error() 18 | } 19 | 20 | // Unwrap unwraps the wrapped error. 21 | // This method is implemented to satisfy an interface of errors.Unwap. 22 | func (b *BuildError) Unwrap() error { 23 | return b.Err 24 | } 25 | -------------------------------------------------------------------------------- /stmt/error_test.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestBuildError_Unwrap(t *testing.T) { 9 | e := &BuildError{ 10 | Err: errors.New("error"), 11 | } 12 | if got := e.Unwrap(); e.Err != got { 13 | t.Fatalf("want %v, but got %v", e.Err, got) 14 | } 15 | } 16 | 17 | func TestBuildError_Error(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | b *BuildError 21 | want string 22 | }{ 23 | { 24 | name: "nil", 25 | b: nil, 26 | want: "", 27 | }, 28 | { 29 | name: "nil", 30 | b: &BuildError{ 31 | Op: "ope", 32 | Err: errors.New("error"), 33 | }, 34 | want: "ope: error", 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := tt.b.Error(); got != tt.want { 40 | t.Errorf("BuildError.Error() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /stmt/stmt.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | // Builder an interface used to build SQL queries. 4 | // 5 | // WriteString method uses the passed string is writing to the query builder. 6 | // AppendArgs method uses for pass arguments corresponding to variables. 7 | type Builder interface { 8 | WritePlaceholder() 9 | WriteString(string) 10 | AppendArgs(args ...interface{}) 11 | } 12 | 13 | // Expr implemented Write method. 14 | // 15 | // This interface represents an expression. 16 | type Expr interface { 17 | Write(Builder) error 18 | } 19 | 20 | // Comparisoner implemented WriteComparison method. 21 | // 22 | // This interface represents a conditional expression. 23 | type Comparisoner interface { 24 | WriteComparison(b Builder) error 25 | } 26 | -------------------------------------------------------------------------------- /stmt/table.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | ) 7 | 8 | // String is able to replace bindvars with string. 9 | // 10 | // i.e. "SELECT * FROM ?" => "SELECT * FROM string" 11 | type String string 12 | 13 | // Write writes the string. 14 | func (s String) Write(b Builder) error { 15 | if s == "" { 16 | return errors.New("unspecified string") 17 | } 18 | b.WriteString(string(s)) 19 | return nil 20 | } 21 | 22 | // Numeric is able to replace bindvars with numeric. 23 | // 24 | // i.e. "LIMIT ?" => "LIMIT numeric" 25 | type Numeric int64 26 | 27 | // Write writes the numeric. 28 | func (n Numeric) Write(b Builder) error { 29 | b.WriteString(strconv.FormatInt(int64(n), 10)) 30 | return nil 31 | } 32 | 33 | // Limit represents "LIMIT ". 34 | type Limit int64 35 | 36 | // Write writes the number of limitations that the Limit has. 37 | func (l Limit) Write(b Builder) error { 38 | b.WriteString("LIMIT ") 39 | return Numeric(l).Write(b) 40 | } 41 | 42 | // Offset represents "OFFSET ". 43 | type Offset int64 44 | 45 | // Write writes the number of offsets that the Offset has. 46 | func (o Offset) Write(b Builder) error { 47 | b.WriteString("OFFSET ") 48 | return Numeric(o).Write(b) 49 | } 50 | 51 | // OrderBy represents "", " DESC". 52 | // If there is Next, it represents like ", DESC". 53 | type OrderBy struct { 54 | Column string 55 | Desc bool 56 | Next *OrderBy 57 | } 58 | 59 | // Write writes expression for "ORDER BY". 60 | func (o *OrderBy) Write(b Builder) error { 61 | b.WriteString(o.Column) 62 | if o.Desc { 63 | b.WriteString(" DESC") 64 | } 65 | if o.Next != nil { 66 | b.WriteString(", ") 67 | return o.Next.Write(b) 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /stmt/table_test.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestString_Write(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | s String 12 | want string 13 | wantErr bool 14 | }{ 15 | { 16 | name: "valid", 17 | s: String("table"), 18 | want: "table", 19 | wantErr: false, 20 | }, 21 | { 22 | name: "invalid", 23 | s: String(""), 24 | want: "", 25 | wantErr: true, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | b := &BuildCapture{ 31 | buf: strings.Builder{}, 32 | Args: []interface{}{}, 33 | } 34 | err := tt.s.Write(b) 35 | if (err != nil) != tt.wantErr { 36 | t.Errorf("String.Write() error = %v, wantErr %v", err, tt.wantErr) 37 | } 38 | if !tt.wantErr { 39 | if got := b.buf.String(); tt.want != got { 40 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 41 | } 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestNumeric_Write(t *testing.T) { 48 | tests := []struct { 49 | name string 50 | n Numeric 51 | want string 52 | wantErr bool 53 | }{ 54 | { 55 | name: "valid", 56 | n: Numeric(100), 57 | want: "100", 58 | }, 59 | { 60 | name: "valid zero", 61 | n: Numeric(0), 62 | want: "0", 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | b := &BuildCapture{ 68 | buf: strings.Builder{}, 69 | Args: []interface{}{}, 70 | } 71 | err := tt.n.Write(b) 72 | if err != nil { 73 | t.Fatalf("String.Write() unexpected error: %v", err) 74 | } 75 | if !tt.wantErr { 76 | if got := b.buf.String(); tt.want != got { 77 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 78 | } 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestLimit_Write(t *testing.T) { 85 | tests := []struct { 86 | name string 87 | l Limit 88 | want string 89 | }{ 90 | { 91 | name: "valid", 92 | l: Limit(10), 93 | want: "LIMIT 10", 94 | }, 95 | } 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | b := &BuildCapture{ 99 | buf: strings.Builder{}, 100 | Args: []interface{}{}, 101 | } 102 | if err := tt.l.Write(b); err != nil { 103 | t.Fatalf("Limit.Write() unexpected error: %v", err) 104 | } 105 | if got := b.buf.String(); tt.want != got { 106 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestOffset_Write(t *testing.T) { 113 | tests := []struct { 114 | name string 115 | o Offset 116 | want string 117 | }{ 118 | { 119 | name: "valid", 120 | o: Offset(10), 121 | want: "OFFSET 10", 122 | }, 123 | } 124 | for _, tt := range tests { 125 | t.Run(tt.name, func(t *testing.T) { 126 | b := &BuildCapture{ 127 | buf: strings.Builder{}, 128 | Args: []interface{}{}, 129 | } 130 | if err := tt.o.Write(b); err != nil { 131 | t.Fatalf("Offset.Write() unexpected error: %v", err) 132 | } 133 | if got := b.buf.String(); tt.want != got { 134 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 135 | } 136 | }) 137 | } 138 | } 139 | 140 | func TestOrderBy_Write(t *testing.T) { 141 | tests := []struct { 142 | name string 143 | o *OrderBy 144 | want string 145 | }{ 146 | { 147 | name: "valid unary ASC", 148 | o: &OrderBy{ 149 | Column: "column", 150 | Desc: false, 151 | Next: nil, 152 | }, 153 | want: "column", 154 | }, 155 | { 156 | name: "valid unary DESC", 157 | o: &OrderBy{ 158 | Column: "column", 159 | Desc: true, 160 | Next: nil, 161 | }, 162 | want: "column DESC", 163 | }, 164 | { 165 | name: "valid has Next", 166 | o: &OrderBy{ 167 | Column: "column1", 168 | Desc: false, 169 | Next: &OrderBy{ 170 | Column: "column2", 171 | Desc: true, 172 | Next: nil, 173 | }, 174 | }, 175 | want: "column1, column2 DESC", 176 | }, 177 | } 178 | for _, tt := range tests { 179 | t.Run(tt.name, func(t *testing.T) { 180 | b := &BuildCapture{ 181 | buf: strings.Builder{}, 182 | Args: []interface{}{}, 183 | } 184 | if err := tt.o.Write(b); err != nil { 185 | t.Fatalf("From.Write() unexpected error: %v", err) 186 | } 187 | if got := b.buf.String(); tt.want != got { 188 | t.Errorf("\nwant: %q\ngot: %q", tt.want, got) 189 | } 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /stmt/utils_for_test.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import "strings" 4 | 5 | var _ Builder = (*BuildCapture)(nil) 6 | 7 | type BuildCapture struct { 8 | buf strings.Builder 9 | Args []interface{} 10 | } 11 | 12 | func (b *BuildCapture) WritePlaceholder() { 13 | b.buf.WriteString("?") 14 | } 15 | 16 | func (b *BuildCapture) WriteString(s string) { 17 | b.buf.WriteString(s) 18 | } 19 | 20 | func (b *BuildCapture) AppendArgs(args ...interface{}) { 21 | b.Args = append(b.Args, args...) 22 | } 23 | 24 | var _ Expr = (*ExprMock)(nil) 25 | 26 | type ExprMock struct { 27 | WriteMock func(Builder) error 28 | } 29 | 30 | func (e *ExprMock) Write(b Builder) error { 31 | return e.WriteMock(b) 32 | } 33 | 34 | var _ Comparisoner = (*ComparisonerMock)(nil) 35 | 36 | type ComparisonerMock struct { 37 | WriteComparisonMock func(Builder) error 38 | } 39 | 40 | func (c *ComparisonerMock) WriteComparison(b Builder) error { 41 | return c.WriteComparisonMock(b) 42 | } 43 | -------------------------------------------------------------------------------- /utils_for_test.go: -------------------------------------------------------------------------------- 1 | package sqb_test 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/Code-Hex/sqb/stmt" 7 | ) 8 | 9 | var _ stmt.Builder = (*BuildCapture)(nil) 10 | 11 | type BuildCapture struct { 12 | buf strings.Builder 13 | Args []interface{} 14 | } 15 | 16 | func (b *BuildCapture) WritePlaceholder() { 17 | b.buf.WriteString("?") 18 | } 19 | 20 | func (b *BuildCapture) WriteString(s string) { 21 | b.buf.WriteString(s) 22 | } 23 | 24 | func (b *BuildCapture) AppendArgs(args ...interface{}) { 25 | b.Args = append(b.Args, args...) 26 | } 27 | 28 | var _ stmt.Expr = (*ExprMock)(nil) 29 | 30 | type ExprMock struct { 31 | WriteMock func(stmt.Builder) error 32 | } 33 | 34 | func (e *ExprMock) Write(b stmt.Builder) error { 35 | return e.WriteMock(b) 36 | } 37 | --------------------------------------------------------------------------------