Skip to content

Commit

Permalink
Merge pull request #102 from huandu/feature-more-cond
Browse files Browse the repository at this point in the history
fix #100 Add EXISTS/NOT EXISTS/ANY/SOME/ALL in `Cond`
  • Loading branch information
huandu authored Apr 1, 2023
2 parents c25b4f2 + 95d77d1 commit 20d1fd8
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 40 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Usage](#usage)
- [Basic usage](#basic-usage)
- [Pre-defined SQL builders](#pre-defined-sql-builders)
- [Build `WHERE` clause](#build-where-clause)
- [Build SQL for different systems](#build-sql-for-different-systems)
- [Using `Struct` as a light weight ORM](#using-struct-as-a-light-weight-orm)
- [Nested SQL](#nested-sql)
Expand Down Expand Up @@ -110,6 +111,59 @@ Following are some utility methods to deal with special cases.

To learn how to use builders, check out [examples on GoDoc](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#pkg-examples).

### Build `WHERE` clause

`WHERE` clause is the most important part of a SQL. We can use `Where` method to add one or more conditions to a builder.

To make building `WHERE` clause easier, there is an utility type called `Cond` to build condition. All builders which support `WHERE` clause have an anonymous `Cond` field so that we can call methods implemented by `Cond` on these builders.

```go
sb := sqlbuilder.Select("id").From("user")
sb.Where(
sb.In("status", 1, 2, 5),
sb.Or(
sb.Equal("name", "foo"),
sb.Like("email", "foo@%"),
),
)

sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)

// Output:
// SELECT id FROM user WHERE status IN (?, ?, ?) AND (name = ? OR email LIKE ?)
// [1 2 5 foo foo@%]
```

There are many methods for building conditions.

- [Cond.Equal](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Equal)/[Cond.E](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.E): `field = value`.
- [Cond.NotEqual](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotEqual)/[Cond.NE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NE): `field <> value`.
- [Cond.GreaterThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterThan)/[Cond.G](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.G): `field > value`.
- [Cond.GreaterEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterEqualThan)/[Cond.GE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GE): `field >= value`.
- [Cond.LessThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessThan)/[Cond.L](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.L): `field < value`.
- [Cond.LessEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessEqualThan)/[Cond.LE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LE): `field <= value`.
- [Cond.In](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.In): `field IN (value1, value2, ...)`.
- [Cond.NotIn](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotIn): `field NOT IN (value1, value2, ...)`.
- [Cond.Like](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Like): `field LIKE value`.
- [Cond.NotLike](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotLike): `field NOT LIKE value`.
- [Cond.Between](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Between): `field BETWEEN lower AND upper`.
- [Cond.NotBetween](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotBetween): `field NOT BETWEEN lower AND upper`.
- [Cond.IsNull](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNull): `field IS NULL`.
- [Cond.IsNotNull](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNotNull): `field IS NOT NULL`.
- [Cond.Exists](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Exists): `EXISTS (subquery)`.
- [Cond.NotExists](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotExists): `NOT EXISTS (subquery)`.
- [Cond.Any](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Any): `field op ANY (value1, value2, ...)`.
- [Cond.All](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.All): `field op ALL (value1, value2, ...)`.
- [Cond.Some](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Some): `field op SOME (value1, value2, ...)`.
- [Cond.Var](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Var): A placeholder for any value.

There are also some methods to combine conditions.

- [Cond.And](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.And): Combine conditions with `AND` operator.
- [Cond.Or](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Or): Combine conditions with `OR` operator.

### Build SQL for different systems

SQL syntax and parameter marks vary in different systems. In this package, we introduce a concept called "flavor" to smooth out these difference.
Expand Down
11 changes: 5 additions & 6 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package sqlbuilder

import (
"bytes"
"database/sql"
"fmt"
"sort"
Expand Down Expand Up @@ -98,7 +97,7 @@ func (args *Args) Compile(format string, initialValue ...interface{}) (query str
//
// See doc for `Compile` to learn details.
func (args *Args) CompileWithFlavor(format string, flavor Flavor, initialValue ...interface{}) (query string, values []interface{}) {
buf := &bytes.Buffer{}
buf := &strings.Builder{}
idx := strings.IndexRune(format, '$')
offset := 0
values = initialValue
Expand Down Expand Up @@ -161,7 +160,7 @@ func (args *Args) CompileWithFlavor(format string, flavor Flavor, initialValue .
return
}

func (args *Args) compileNamed(buf *bytes.Buffer, flavor Flavor, format string, values []interface{}) (string, []interface{}) {
func (args *Args) compileNamed(buf *strings.Builder, flavor Flavor, format string, values []interface{}) (string, []interface{}) {
i := 1

for ; i < len(format) && format[i] != '}'; i++ {
Expand All @@ -183,7 +182,7 @@ func (args *Args) compileNamed(buf *bytes.Buffer, flavor Flavor, format string,
return format, values
}

func (args *Args) compileDigits(buf *bytes.Buffer, flavor Flavor, format string, values []interface{}, offset int) (string, []interface{}, int) {
func (args *Args) compileDigits(buf *strings.Builder, flavor Flavor, format string, values []interface{}, offset int) (string, []interface{}, int) {
i := 1

for ; i < len(format) && '0' <= format[i] && format[i] <= '9'; i++ {
Expand All @@ -200,7 +199,7 @@ func (args *Args) compileDigits(buf *bytes.Buffer, flavor Flavor, format string,
return format, values, offset
}

func (args *Args) compileSuccessive(buf *bytes.Buffer, flavor Flavor, format string, values []interface{}, offset int) (string, []interface{}, int) {
func (args *Args) compileSuccessive(buf *strings.Builder, flavor Flavor, format string, values []interface{}, offset int) (string, []interface{}, int) {
if offset >= len(args.args) {
return format, values, offset
}
Expand All @@ -211,7 +210,7 @@ func (args *Args) compileSuccessive(buf *bytes.Buffer, flavor Flavor, format str
return format, values, offset + 1
}

func (args *Args) compileArg(buf *bytes.Buffer, flavor Flavor, values []interface{}, arg interface{}) []interface{} {
func (args *Args) compileArg(buf *strings.Builder, flavor Flavor, values []interface{}, arg interface{}) []interface{} {
switch a := arg.(type) {
case Builder:
var s string
Expand Down
175 changes: 157 additions & 18 deletions cond.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package sqlbuilder

import (
"fmt"
"strings"
)

Expand All @@ -15,17 +14,25 @@ type Cond struct {

// Equal represents "field = value".
func (c *Cond) Equal(field string, value interface{}) string {
return fmt.Sprintf("%s = %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" = ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// E is an alias of Equal.
func (c *Cond) E(field string, value interface{}) string {
return c.Equal(field, value)
}

// NotEqual represents "field != value".
// NotEqual represents "field <> value".
func (c *Cond) NotEqual(field string, value interface{}) string {
return fmt.Sprintf("%s <> %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" <> ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// NE is an alias of NotEqual.
Expand All @@ -35,7 +42,11 @@ func (c *Cond) NE(field string, value interface{}) string {

// GreaterThan represents "field > value".
func (c *Cond) GreaterThan(field string, value interface{}) string {
return fmt.Sprintf("%s > %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" > ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// G is an alias of GreaterThan.
Expand All @@ -45,7 +56,11 @@ func (c *Cond) G(field string, value interface{}) string {

// GreaterEqualThan represents "field >= value".
func (c *Cond) GreaterEqualThan(field string, value interface{}) string {
return fmt.Sprintf("%s >= %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" >= ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// GE is an alias of GreaterEqualThan.
Expand All @@ -55,7 +70,11 @@ func (c *Cond) GE(field string, value interface{}) string {

// LessThan represents "field < value".
func (c *Cond) LessThan(field string, value interface{}) string {
return fmt.Sprintf("%s < %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" < ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// L is an alias of LessThan.
Expand All @@ -65,7 +84,11 @@ func (c *Cond) L(field string, value interface{}) string {

// LessEqualThan represents "field <= value".
func (c *Cond) LessEqualThan(field string, value interface{}) string {
return fmt.Sprintf("%s <= %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" <= ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// LE is an alias of LessEqualThan.
Expand All @@ -81,7 +104,12 @@ func (c *Cond) In(field string, value ...interface{}) string {
vs = append(vs, c.Args.Add(v))
}

return fmt.Sprintf("%s IN (%s)", Escape(field), strings.Join(vs, ", "))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" IN (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteString(")")
return buf.String()
}

// NotIn represents "field NOT IN (value...)".
Expand All @@ -92,47 +120,158 @@ func (c *Cond) NotIn(field string, value ...interface{}) string {
vs = append(vs, c.Args.Add(v))
}

return fmt.Sprintf("%s NOT IN (%s)", Escape(field), strings.Join(vs, ", "))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" NOT IN (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteString(")")
return buf.String()
}

// Like represents "field LIKE value".
func (c *Cond) Like(field string, value interface{}) string {
return fmt.Sprintf("%s LIKE %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" LIKE ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// NotLike represents "field NOT LIKE value".
func (c *Cond) NotLike(field string, value interface{}) string {
return fmt.Sprintf("%s NOT LIKE %s", Escape(field), c.Args.Add(value))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" NOT LIKE ")
buf.WriteString(c.Args.Add(value))
return buf.String()
}

// IsNull represents "field IS NULL".
func (c *Cond) IsNull(field string) string {
return fmt.Sprintf("%s IS NULL", Escape(field))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" IS NULL")
return buf.String()
}

// IsNotNull represents "field IS NOT NULL".
func (c *Cond) IsNotNull(field string) string {
return fmt.Sprintf("%s IS NOT NULL", Escape(field))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" IS NOT NULL")
return buf.String()
}

// Between represents "field BETWEEN lower AND upper".
func (c *Cond) Between(field string, lower, upper interface{}) string {
return fmt.Sprintf("%s BETWEEN %s AND %s", Escape(field), c.Args.Add(lower), c.Args.Add(upper))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" BETWEEN ")
buf.WriteString(c.Args.Add(lower))
buf.WriteString(" AND ")
buf.WriteString(c.Args.Add(upper))
return buf.String()
}

// NotBetween represents "field NOT BETWEEN lower AND upper".
func (c *Cond) NotBetween(field string, lower, upper interface{}) string {
return fmt.Sprintf("%s NOT BETWEEN %s AND %s", Escape(field), c.Args.Add(lower), c.Args.Add(upper))
buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" NOT BETWEEN ")
buf.WriteString(c.Args.Add(lower))
buf.WriteString(" AND ")
buf.WriteString(c.Args.Add(upper))
return buf.String()
}

// Or represents OR logic like "expr1 OR expr2 OR expr3".
func (c *Cond) Or(orExpr ...string) string {
return fmt.Sprintf("(%s)", strings.Join(orExpr, " OR "))
buf := &strings.Builder{}
buf.WriteString("(")
buf.WriteString(strings.Join(orExpr, " OR "))
buf.WriteString(")")
return buf.String()
}

// And represents AND logic like "expr1 AND expr2 AND expr3".
func (c *Cond) And(andExpr ...string) string {
return fmt.Sprintf("(%s)", strings.Join(andExpr, " AND "))
buf := &strings.Builder{}
buf.WriteString("(")
buf.WriteString(strings.Join(andExpr, " AND "))
buf.WriteString(")")
return buf.String()
}

// Exists represents "EXISTS (subquery)".
func (c *Cond) Exists(subquery interface{}) string {
buf := &strings.Builder{}
buf.WriteString("EXISTS (")
buf.WriteString(c.Args.Add(subquery))
buf.WriteString(")")
return buf.String()
}

// NotExists represents "NOT EXISTS (subquery)".
func (c *Cond) NotExists(subquery interface{}) string {
buf := &strings.Builder{}
buf.WriteString("NOT EXISTS (")
buf.WriteString(c.Args.Add(subquery))
buf.WriteString(")")
return buf.String()
}

// Any represents "field op ANY (value...)".
func (c *Cond) Any(field, op string, value ...interface{}) string {
vs := make([]string, 0, len(value))

for _, v := range value {
vs = append(vs, c.Args.Add(v))
}

buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" ")
buf.WriteString(op)
buf.WriteString(" ANY (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteString(")")
return buf.String()
}

// All represents "field op ALL (value...)".
func (c *Cond) All(field, op string, value ...interface{}) string {
vs := make([]string, 0, len(value))

for _, v := range value {
vs = append(vs, c.Args.Add(v))
}

buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" ")
buf.WriteString(op)
buf.WriteString(" ALL (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteString(")")
return buf.String()
}

// Some represents "field op SOME (value...)".
func (c *Cond) Some(field, op string, value ...interface{}) string {
vs := make([]string, 0, len(value))

for _, v := range value {
vs = append(vs, c.Args.Add(v))
}

buf := &strings.Builder{}
buf.WriteString(Escape(field))
buf.WriteString(" ")
buf.WriteString(op)
buf.WriteString(" SOME (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteString(")")
return buf.String()
}

// Var returns a placeholder for value.
Expand Down
Loading

0 comments on commit 20d1fd8

Please sign in to comment.