├── .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 | [](https://godoc.org/github.com/Code-Hex/sqb) [](https://circleci.com/gh/Code-Hex/sqb) [](https://codecov.io/gh/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 | 
68 |
69 | 
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 |
--------------------------------------------------------------------------------