Skip to content

Commit

Permalink
fix #47. add Union and UnionAll methods
Browse files Browse the repository at this point in the history
  • Loading branch information
huandu committed Jul 28, 2020
1 parent a4f634e commit e5f5aaa
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 39 deletions.
79 changes: 41 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
# SQL builder for Go #
# SQL builder for Go

[![Build Status](https://travis-ci.org/huandu/go-sqlbuilder.svg?branch=master)](https://travis-ci.org/huandu/go-sqlbuilder)
[![GoDoc](https://godoc.org/github.com/huandu/go-sqlbuilder?status.svg)](https://godoc.org/github.com/huandu/go-sqlbuilder)
[![GoDoc](https://pkg.go.dev/github.com/huandu/go-sqlbuilder?status.svg)](https://pkg.go.dev/github.com/huandu/go-sqlbuilder)
[![Go Report](https://goreportcard.com/badge/github.com/huandu/go-sqlbuilder)](https://goreportcard.com/report/github.com/huandu/go-sqlbuilder)
[![Coverage Status](https://coveralls.io/repos/github/huandu/go-sqlbuilder/badge.svg?branch=master)](https://coveralls.io/github/huandu/go-sqlbuilder?branch=master)

Package `sqlbuilder` provides a set of flexible and powerful SQL string builders. The only goal of this package is to build SQL string with arguments which can be used in `DB#Query` or `DB#Exec` defined in package `database/sql`.

## Install ##
## Install

Use `go get` to install this package.

go get -u github.com/huandu/go-sqlbuilder
```shell
go get -u github.com/huandu/go-sqlbuilder
```

## Usage ##
## Usage

### Basic usage ###
### Basic usage

Here is a sample to demonstrate how to build a SELECT query.

Expand All @@ -37,17 +39,18 @@ fmt.Println(args)

Following builders are implemented right now. API document and examples are provided in the `godoc` document.

* [Struct](https://godoc.org/github.com/huandu/go-sqlbuilder#Struct): Builder factory for a struct.
* [CreateTableBuilder](https://godoc.org/github.com/huandu/go-sqlbuilder#CreateTableBuilder): Builder for CREATE TABLE.
* [SelectBuilder](https://godoc.org/github.com/huandu/go-sqlbuilder#SelectBuilder): Builder for SELECT.
* [InsertBuilder](https://godoc.org/github.com/huandu/go-sqlbuilder#InsertBuilder): Builder for INSERT.
* [UpdateBuilder](https://godoc.org/github.com/huandu/go-sqlbuilder#UpdateBuilder): Builder for UPDATE.
* [DeleteBuilder](https://godoc.org/github.com/huandu/go-sqlbuilder#DeleteBuilder): Builder for DELETE.
* [Buildf](https://godoc.org/github.com/huandu/go-sqlbuilder#Buildf): Freestyle builder using `fmt.Sprintf`-like syntax.
* [Build](https://godoc.org/github.com/huandu/go-sqlbuilder#Build): Advanced freestyle builder using special syntax defined in [Args#Compile](https://godoc.org/github.com/huandu/go-sqlbuilder#Args.Compile).
* [BuildNamed](https://godoc.org/github.com/huandu/go-sqlbuilder#BuildNamed): Advanced freestyle builder using `${key}` to refer the value of a map by key.
- [Struct](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Struct): Builder factory for a struct.
- [CreateTableBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CreateTableBuilder): Builder for CREATE TABLE.
- [SelectBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#SelectBuilder): Builder for SELECT.
- [InsertBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#InsertBuilder): Builder for INSERT.
- [UpdateBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UpdateBuilder): Builder for UPDATE.
- [DeleteBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#DeleteBuilder): Builder for DELETE.
- [UnionBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UnionBuilder): Builder for UNION and UNION ALL.
- [Buildf](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Buildf): Freestyle builder using `fmt.Sprintf`-like syntax.
- [Build](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Build): Advanced freestyle builder using special syntax defined in [Args#Compile](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Args.Compile).
- [BuildNamed](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#BuildNamed): Advanced freestyle builder using `${key}` to refer the value of a map by key.

### Build SQL for MySQL or PostgreSQL ###
### Build SQL for MySQL or PostgreSQL

Parameter markers are different in MySQL and PostgreSQL. This package provides some methods to set the type of markers (we call it "flavor") in all builders.

Expand All @@ -61,7 +64,7 @@ To be more verbose, we can use `PostgreSQL.NewSelectBuilder()` to create a `Sele

Right now, there are only two flavors, `MySQL` and `PostgreSQL`. Open new issue to me to ask for a new flavor if you find it necessary.

### Using `Struct` as a light weight ORM ###
### Using `Struct` as a light weight ORM

`Struct` stores type information and struct fields of a struct. It's a factory of builders. We can use `Struct` methods to create initialized SELECT/INSERT/UPDATE/DELETE builders to work with the struct. It can help us to save time and avoid human-error on writing column names.

Expand All @@ -80,7 +83,7 @@ type ATable struct {
}
```

Read [examples](https://godoc.org/github.com/huandu/go-sqlbuilder#Struct) for `Struct` to learn details of how to use it.
Read [examples](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Struct) for `Struct` to learn details of how to use it.

What's more, we can use `Struct` as a kind of zero-config ORM. While most ORM implementations requires several prerequisite configs to work with database connections, `Struct` doesn't require any config and work well with any SQL driver which works with `database/sql`. `Struct` doesn't call any `database/sql` API; It just creates right SQL with arguments for `DB#Query`/`DB#Exec` or a slice of address of struct fields for `Rows#Scan`/`Row#Scan`.

Expand Down Expand Up @@ -128,7 +131,7 @@ func ExampleStruct() {
}
```

### Nested SQL ###
### Nested SQL

It's quite straight forward to create a nested SQL: use a builder as an argument to nest it.

Expand All @@ -155,7 +158,7 @@ fmt.Println(args)
// [4 1]
```

### Use `sql.Named` in a builder ###
### Use `sql.Named` in a builder

The function `sql.Named` defined in `database/sql` can create a named argument in SQL. It's necessary if we want to reuse an argument several times in one SQL. It's still quite simple to use named arguments in a builder: use it as an argument.

Expand Down Expand Up @@ -183,15 +186,15 @@ fmt.Println(args)
// [{{} start 1514458225} {{} end 1514544625}]
```

### Argument modifiers ###
### Argument modifiers

There are several modifiers for arguments.

* `List(arg)` represents a list of arguments. If `arg` is a slice or array, e.g. a slice with 3 ints, it will be compiled to `?, ?, ?` and flattened in the final arguments as 3 ints. It's a tool for convenience. We can use it in the `IN` expression or `VALUES` of `INSERT INTO`.
* `Named(name, arg)` represents a named argument. It only works with `Build` or `BuildNamed` to define a named placeholder using syntax `${name}`.
* `Raw(expr)` marks an `expr` as a plain string in SQL rather than an argument. When we build a builder, the value of raw expressions are copied in SQL string directly without leaving any `?` in SQL.
- `List(arg)` represents a list of arguments. If `arg` is a slice or array, e.g. a slice with 3 ints, it will be compiled to `?, ?, ?` and flattened in the final arguments as 3 ints. It's a tool for convenience. We can use it in the `IN` expression or `VALUES` of `INSERT INTO`.
- `Named(name, arg)` represents a named argument. It only works with `Build` or `BuildNamed` to define a named placeholder using syntax `${name}`.
- `Raw(expr)` marks an `expr` as a plain string in SQL rather than an argument. When we build a builder, the value of raw expressions are copied in SQL string directly without leaving any `?` in SQL.

### Freestyle builder ###
### Freestyle builder

A builder is only a way to record arguments. If we want to build a long SQL with lots of special syntax (e.g. special comments for a database proxy), simply use `Buildf` to format a SQL string using a `fmt.Sprintf`-like syntax.

Expand All @@ -209,16 +212,16 @@ fmt.Println(args)
// [1 2]
```

### Using special syntax to build SQL ###
### Using special syntax to build SQL

Package `sqlbuilder` defines special syntax to represent an uncompiled SQL internally. If we want to take advantage of the syntax to build customized tools, we can use `Build` to compile it with arguments.

The format string uses special syntax to represent arguments.

* `$?` refers successive arguments passed in the call. It works similar as `%v` in `fmt.Sprintf`.
* `$0` `$1` ... `$n` refers nth-argument passed in the call. Next `$?` will use arguments n+1.
* `${name}` refers a named argument created by `Named` with `name`.
* `$$` is a `"$"` string.
- `$?` refers successive arguments passed in the call. It works similar as `%v` in `fmt.Sprintf`.
- `$0` `$1` ... `$n` refers nth-argument passed in the call. Next `$?` will use arguments n+1.
- `${name}` refers a named argument created by `Named` with `name`.
- `$$` is a `"$"` string.

```go
sb := sqlbuilder.NewSelectBuilder()
Expand All @@ -238,11 +241,11 @@ fmt.Println(args)

If we just want to use `${name}` syntax to refer named arguments, use `BuildNamed` instead. It disables all special syntax but `${name}` and `$$`.

### Interpolate `args` in the `sql` ###
### Interpolate `args` in the `sql`

Some SQL drivers doesn't actually implement `StmtExecContext#ExecContext`. They will fail when `len(args) > 0`. The only solution is to interpolate `args` in the `sql`, and execute the interpolated query with the driver.

*Security warning*: I try my best to escape special characters in interpolate methods, but it's still less secure than `Stmt` implemented by SQL servers.
_Security warning_: I try my best to escape special characters in interpolate methods, but it's still less secure than `Stmt` implemented by SQL servers.

This feature is inspired by interpolation feature in package `github.com/go-sql-driver/mysql`.

Expand Down Expand Up @@ -293,19 +296,19 @@ fmt.Println(err)
// <nil>
```

## FAQ ##
## FAQ

### What's the difference between this package and `squirrel` ###
### What's the difference between this package and `squirrel`

Package [squirrel](https://github.com/Masterminds/squirrel) is another SQL builder package with outstanding design and high code quality.
Comparing with `squirrel`, `go-sqlbuilder` is much more extensible with more built-in features.

Here are details.

* API design: The core of `go-sqlbuilder` is `Builder` and `Args`. Nearly all features are built on top of them. If we want to extend this package, e.g. support `EXPLAIN`, we can use `Build("EXPLAIN $?", builder)` to add `EXPLAIN` in front of any SQL.
* ORM: Package `squirrel` doesn't provide ORM directly. There is another package [structable](https://github.com/Masterminds/structable), which is based on `squirrel`, designed for ORM.
* No design pitfalls: There is no design pitfalls like `squirrel.Eq{"mynumber": []uint8{1,2,3}}`. I'm proud of it. :)
- API design: The core of `go-sqlbuilder` is `Builder` and `Args`. Nearly all features are built on top of them. If we want to extend this package, e.g. support `EXPLAIN`, we can use `Build("EXPLAIN $?", builder)` to add `EXPLAIN` in front of any SQL.
- ORM: Package `squirrel` doesn't provide ORM directly. There is another package [structable](https://github.com/Masterminds/structable), which is based on `squirrel`, designed for ORM.
- No design pitfalls: There is no design pitfalls like `squirrel.Eq{"mynumber": []uint8{1,2,3}}`. I'm proud of it. :)

## License ##
## License

This package is licensed under MIT license. See LICENSE for details.
2 changes: 2 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type compiledBuilder struct {
format string
}

var _ Builder = new(compiledBuilder)

func (cb *compiledBuilder) Build() (sql string, args []interface{}) {
return cb.args.Compile(cb.format)
}
Expand Down
2 changes: 2 additions & 0 deletions delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type DeleteBuilder struct {
args *Args
}

var _ Builder = new(DeleteBuilder)

// DeleteFrom sets table name in DELETE.
func (db *DeleteBuilder) DeleteFrom(table string) *DeleteBuilder {
db.table = Escape(table)
Expand Down
14 changes: 14 additions & 0 deletions flavor.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ func (f Flavor) NewUpdateBuilder() *UpdateBuilder {
return b
}

// Union unions all builders together using UNION operator with flavor.
func (f Flavor) Union(builders ...Builder) *UnionBuilder {
b := newUnionBuilder(unionDistinct, builders...)
b.SetFlavor(f)
return b
}

// UnionAll unions all builders together using UNION ALL operator with flavor.
func (f Flavor) UnionAll(builders ...Builder) *UnionBuilder {
b := newUnionBuilder(unionAll, builders...)
b.SetFlavor(f)
return b
}

// Quote adds quote for name to make sure the name can be used safely
// as table name or field name.
//
Expand Down
2 changes: 2 additions & 0 deletions insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type InsertBuilder struct {
args *Args
}

var _ Builder = new(InsertBuilder)

// InsertInto sets table name in INSERT.
func (ib *InsertBuilder) InsertInto(table string) *InsertBuilder {
ib.table = Escape(table)
Expand Down
5 changes: 4 additions & 1 deletion select.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type SelectBuilder struct {
args *Args
}

var _ Builder = new(SelectBuilder)

// Distinct marks this SELECT as DISTINCT.
func (sb *SelectBuilder) Distinct() *SelectBuilder {
sb.distinct = true
Expand Down Expand Up @@ -233,14 +235,15 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
buf.WriteString(" LIMIT ")
buf.WriteString(strconv.Itoa(sb.limit))
}

if MySQL == flavor && sb.limit >= 0 || PostgreSQL == flavor {
if sb.offset >= 0 {
buf.WriteString(" OFFSET ")
buf.WriteString(strconv.Itoa(sb.offset))
}
}

return sb.Args.CompileWithFlavor(buf.String(), flavor, initialArg...)
return sb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}

// SetFlavor sets the flavor of compiled sql.
Expand Down
146 changes: 146 additions & 0 deletions union.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package sqlbuilder

import (
"bytes"
"strconv"
"strings"
)

const (
unionDistinct = " UNION " // Default union type is DISTINCT.
unionAll = " UNION ALL "
)

// UnionBuilder is a builder to build UNION.
type UnionBuilder struct {
format string
builders []Builder
orderByCols []string
order string
limit int
offset int

args *Args
}

var _ Builder = new(UnionBuilder)

// Union unions all builders together using UNION operator.
func Union(builders ...Builder) *UnionBuilder {
return DefaultFlavor.Union(builders...)
}

// UnionAll unions all builders together using UNION ALL operator.
func UnionAll(builders ...Builder) *UnionBuilder {
return DefaultFlavor.UnionAll(builders...)
}

func newUnionBuilder(opt string, builders ...Builder) *UnionBuilder {
args := &Args{}
vars := make([]string, 0, len(builders))

for _, b := range builders {
vars = append(vars, args.Add(b))
}

return &UnionBuilder{
format: strings.Join(vars, opt),
builders: builders,
limit: -1,
offset: -1,

args: args,
}
}

// OrderBy sets columns of ORDER BY in SELECT.
func (ub *UnionBuilder) OrderBy(col ...string) *UnionBuilder {
ub.orderByCols = col
return ub
}

// Asc sets order of ORDER BY to ASC.
func (ub *UnionBuilder) Asc() *UnionBuilder {
ub.order = "ASC"
return ub
}

// Desc sets order of ORDER BY to DESC.
func (ub *UnionBuilder) Desc() *UnionBuilder {
ub.order = "DESC"
return ub
}

// Limit sets the LIMIT in SELECT.
func (ub *UnionBuilder) Limit(limit int) *UnionBuilder {
ub.limit = limit
return ub
}

// Offset sets the LIMIT offset in SELECT.
func (ub *UnionBuilder) Offset(offset int) *UnionBuilder {
ub.offset = offset
return ub
}

// String returns the compiled SELECT string.
func (ub *UnionBuilder) String() string {
s, _ := ub.Build()
return s
}

// Build returns compiled SELECT string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UnionBuilder) Build() (sql string, args []interface{}) {
return ub.BuildWithFlavor(ub.args.Flavor)
}

// BuildWithFlavor returns compiled SELECT string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := &bytes.Buffer{}

if len(ub.builders) > 1 {
buf.WriteRune('(')
}

buf.WriteString(ub.format)

if len(ub.builders) > 1 {
buf.WriteRune(')')
}

if len(ub.orderByCols) > 0 {
buf.WriteString(" ORDER BY ")
buf.WriteString(strings.Join(ub.orderByCols, ", "))

if ub.order != "" {
buf.WriteRune(' ')
buf.WriteString(ub.order)
}
}

if ub.limit >= 0 {
buf.WriteString(" LIMIT ")
buf.WriteString(strconv.Itoa(ub.limit))
}

if MySQL == flavor && ub.limit >= 0 || PostgreSQL == flavor {
if ub.offset >= 0 {
buf.WriteString(" OFFSET ")
buf.WriteString(strconv.Itoa(ub.offset))
}
}

return ub.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}

// SetFlavor sets the flavor of compiled sql.
func (ub *UnionBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = ub.args.Flavor
ub.args.Flavor = flavor
return
}
Loading

0 comments on commit e5f5aaa

Please sign in to comment.