services
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2017 Jonathan Novak, Tai-Lin Chu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
# gocraft/dbr (database records)
|
||||
|
||||
[](https://godoc.org/github.com/gocraft/dbr)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_shield)
|
||||
[](https://goreportcard.com/report/github.com/gocraft/dbr)
|
||||
[](https://circleci.com/gh/gocraft/dbr)
|
||||
|
||||
gocraft/dbr provides additions to Go's database/sql for super fast performance and convenience.
|
||||
|
||||
```
|
||||
$ go get -u github.com/gocraft/dbr/v2
|
||||
```
|
||||
|
||||
```go
|
||||
import "github.com/gocraft/dbr/v2"
|
||||
```
|
||||
|
||||
## Driver support
|
||||
|
||||
* MySQL
|
||||
* PostgreSQL
|
||||
* SQLite3
|
||||
|
||||
## Examples
|
||||
|
||||
See [godoc](https://godoc.org/github.com/gocraft/dbr) for more examples.
|
||||
|
||||
### Open connections
|
||||
|
||||
```go
|
||||
// create a connection (e.g. "postgres", "mysql", or "sqlite3")
|
||||
conn, _ := Open("postgres", "...", nil)
|
||||
conn.SetMaxOpenConns(10)
|
||||
|
||||
// create a session for each business unit of execution (e.g. a web request or goworkers job)
|
||||
sess := conn.NewSession(nil)
|
||||
|
||||
// create a tx from sessions
|
||||
sess.Begin()
|
||||
```
|
||||
|
||||
### Create and use Tx
|
||||
|
||||
```go
|
||||
sess := mysqlSession
|
||||
tx, err := sess.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer tx.RollbackUnlessCommitted()
|
||||
|
||||
// do stuff...
|
||||
|
||||
tx.Commit()
|
||||
```
|
||||
|
||||
### SelectStmt loads data into structs
|
||||
|
||||
```go
|
||||
// columns are mapped by tag then by field
|
||||
type Suggestion struct {
|
||||
ID int64 // id, will be autoloaded by last insert id
|
||||
Title NullString `db:"subject"` // subjects are called titles now
|
||||
Url string `db:"-"` // ignored
|
||||
secret string // ignored
|
||||
}
|
||||
|
||||
// By default gocraft/dbr converts CamelCase property names to snake_case column_names.
|
||||
// You can override this with struct tags, just like with JSON tags.
|
||||
// This is especially helpful while migrating from legacy systems.
|
||||
var suggestions []Suggestion
|
||||
sess := mysqlSession
|
||||
sess.Select("*").From("suggestions").Load(&suggestions)
|
||||
```
|
||||
|
||||
### SelectStmt with where-value interpolation
|
||||
|
||||
```go
|
||||
// database/sql uses prepared statements, which means each argument
|
||||
// in an IN clause needs its own question mark.
|
||||
// gocraft/dbr, on the other hand, handles interpolation itself
|
||||
// so that you can easily use a single question mark paired with a
|
||||
// dynamically sized slice.
|
||||
|
||||
sess := mysqlSession
|
||||
ids := []int64{1, 2, 3, 4, 5}
|
||||
sess.Select("*").From("suggestions").Where("id IN ?", ids)
|
||||
```
|
||||
|
||||
### SelectStmt with joins
|
||||
|
||||
```go
|
||||
sess := mysqlSession
|
||||
sess.Select("*").From("suggestions").
|
||||
Join("subdomains", "suggestions.subdomain_id = subdomains.id")
|
||||
|
||||
sess.Select("*").From("suggestions").
|
||||
LeftJoin("subdomains", "suggestions.subdomain_id = subdomains.id")
|
||||
|
||||
// join multiple tables
|
||||
sess.Select("*").From("suggestions").
|
||||
Join("subdomains", "suggestions.subdomain_id = subdomains.id").
|
||||
Join("accounts", "subdomains.accounts_id = accounts.id")
|
||||
```
|
||||
|
||||
### SelectStmt with raw SQL
|
||||
|
||||
```go
|
||||
SelectBySql("SELECT `title`, `body` FROM `suggestions` ORDER BY `id` ASC LIMIT 10")
|
||||
```
|
||||
|
||||
### InsertStmt adds data from struct
|
||||
|
||||
```go
|
||||
type Suggestion struct {
|
||||
ID int64
|
||||
Title NullString
|
||||
CreatedAt time.Time
|
||||
}
|
||||
sugg := &Suggestion{
|
||||
Title: NewNullString("Gopher"),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
sess := mysqlSession
|
||||
sess.InsertInto("suggestions").
|
||||
Columns("title").
|
||||
Record(&sugg).
|
||||
Exec()
|
||||
|
||||
// id is set automatically
|
||||
fmt.Println(sugg.ID)
|
||||
```
|
||||
|
||||
### InsertStmt adds data from value
|
||||
|
||||
```go
|
||||
sess := mysqlSession
|
||||
sess.InsertInto("suggestions").
|
||||
Pair("title", "Gopher").
|
||||
Pair("body", "I love go.")
|
||||
```
|
||||
|
||||
|
||||
## Benchmark (2018-05-11)
|
||||
|
||||
```
|
||||
BenchmarkLoadValues/sqlx_10-8 5000 407318 ns/op 3913 B/op 164 allocs/op
|
||||
BenchmarkLoadValues/dbr_10-8 5000 372940 ns/op 3874 B/op 123 allocs/op
|
||||
BenchmarkLoadValues/sqlx_100-8 2000 584197 ns/op 30195 B/op 1428 allocs/op
|
||||
BenchmarkLoadValues/dbr_100-8 3000 558852 ns/op 22965 B/op 937 allocs/op
|
||||
BenchmarkLoadValues/sqlx_1000-8 1000 2319101 ns/op 289339 B/op 14031 allocs/op
|
||||
BenchmarkLoadValues/dbr_1000-8 1000 2310441 ns/op 210092 B/op 9040 allocs/op
|
||||
BenchmarkLoadValues/sqlx_10000-8 100 17004716 ns/op 3193997 B/op 140043 allocs/op
|
||||
BenchmarkLoadValues/dbr_10000-8 100 16150062 ns/op 2394698 B/op 90051 allocs/op
|
||||
BenchmarkLoadValues/sqlx_100000-8 10 170068209 ns/op 31679944 B/op 1400053 allocs/op
|
||||
BenchmarkLoadValues/dbr_100000-8 10 147202536 ns/op 23680625 B/op 900061 allocs/op
|
||||
```
|
||||
|
||||
## Thanks & Authors
|
||||
Inspiration from these excellent libraries:
|
||||
* [sqlx](https://github.com/jmoiron/sqlx) - various useful tools and utils for interacting with database/sql.
|
||||
* [Squirrel](https://github.com/lann/squirrel) - simple fluent query builder.
|
||||
|
||||
Authors:
|
||||
* Jonathan Novak -- [https://github.com/cypriss](https://github.com/cypriss)
|
||||
* Tai-Lin Chu -- [https://github.com/taylorchu](https://github.com/taylorchu)
|
||||
* Sponsored by [UserVoice](https://eng.uservoice.com)
|
||||
|
||||
Contributors:
|
||||
* Paul Bergeron -- [https://github.com/dinedal](https://github.com/dinedal) - SQLite dialect
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_large)
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
# gocraft/dbr (database records)
|
||||
|
||||
[](https://godoc.org/github.com/gocraft/dbr)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_shield)
|
||||
[](https://goreportcard.com/report/github.com/gocraft/dbr)
|
||||
[](https://circleci.com/gh/gocraft/dbr)
|
||||
|
||||
gocraft/dbr provides additions to Go's database/sql for super fast performance and convenience.
|
||||
|
||||
```
|
||||
$ go get -u github.com/gocraft/dbr/v2
|
||||
```
|
||||
|
||||
```go
|
||||
import "github.com/gocraft/dbr/v2"
|
||||
```
|
||||
|
||||
## Driver support
|
||||
|
||||
* MySQL
|
||||
* PostgreSQL
|
||||
* SQLite3
|
||||
|
||||
## Examples
|
||||
|
||||
See [godoc](https://godoc.org/github.com/gocraft/dbr) for more examples.
|
||||
|
||||
### Open connections
|
||||
|
||||
{{ "ExampleOpen" | example }}
|
||||
|
||||
### Create and use Tx
|
||||
|
||||
{{ "ExampleTx" | example }}
|
||||
|
||||
### SelectStmt loads data into structs
|
||||
|
||||
{{ "ExampleSelectStmt_Load" | example }}
|
||||
|
||||
### SelectStmt with where-value interpolation
|
||||
|
||||
{{ "ExampleSelectStmt_Where" | example }}
|
||||
|
||||
### SelectStmt with joins
|
||||
|
||||
{{ "ExampleSelectStmt_Join" | example }}
|
||||
|
||||
### SelectStmt with raw SQL
|
||||
|
||||
{{ "ExampleSelectBySql" | example }}
|
||||
|
||||
### InsertStmt adds data from struct
|
||||
|
||||
{{ "ExampleInsertStmt_Record" | example }}
|
||||
|
||||
### InsertStmt adds data from value
|
||||
|
||||
{{ "ExampleInsertStmt_Pair" | example }}
|
||||
|
||||
|
||||
## Benchmark (2018-05-11)
|
||||
|
||||
```
|
||||
BenchmarkLoadValues/sqlx_10-8 5000 407318 ns/op 3913 B/op 164 allocs/op
|
||||
BenchmarkLoadValues/dbr_10-8 5000 372940 ns/op 3874 B/op 123 allocs/op
|
||||
BenchmarkLoadValues/sqlx_100-8 2000 584197 ns/op 30195 B/op 1428 allocs/op
|
||||
BenchmarkLoadValues/dbr_100-8 3000 558852 ns/op 22965 B/op 937 allocs/op
|
||||
BenchmarkLoadValues/sqlx_1000-8 1000 2319101 ns/op 289339 B/op 14031 allocs/op
|
||||
BenchmarkLoadValues/dbr_1000-8 1000 2310441 ns/op 210092 B/op 9040 allocs/op
|
||||
BenchmarkLoadValues/sqlx_10000-8 100 17004716 ns/op 3193997 B/op 140043 allocs/op
|
||||
BenchmarkLoadValues/dbr_10000-8 100 16150062 ns/op 2394698 B/op 90051 allocs/op
|
||||
BenchmarkLoadValues/sqlx_100000-8 10 170068209 ns/op 31679944 B/op 1400053 allocs/op
|
||||
BenchmarkLoadValues/dbr_100000-8 10 147202536 ns/op 23680625 B/op 900061 allocs/op
|
||||
```
|
||||
|
||||
## Thanks & Authors
|
||||
Inspiration from these excellent libraries:
|
||||
* [sqlx](https://github.com/jmoiron/sqlx) - various useful tools and utils for interacting with database/sql.
|
||||
* [Squirrel](https://github.com/lann/squirrel) - simple fluent query builder.
|
||||
|
||||
Authors:
|
||||
* Jonathan Novak -- [https://github.com/cypriss](https://github.com/cypriss)
|
||||
* Tai-Lin Chu -- [https://github.com/taylorchu](https://github.com/taylorchu)
|
||||
* Sponsored by [UserVoice](https://eng.uservoice.com)
|
||||
|
||||
Contributors:
|
||||
* Paul Bergeron -- [https://github.com/dinedal](https://github.com/dinedal) - SQLite dialect
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_large)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package dbr
|
||||
|
||||
import "strings"
|
||||
|
||||
// Buffer collects strings, and values that are ready to be interpolated.
|
||||
// This is used internally to efficiently build SQL statement.
|
||||
type Buffer interface {
|
||||
WriteString(string) (int, error)
|
||||
String() string
|
||||
|
||||
WriteValue(v ...interface{}) (err error)
|
||||
Value() []interface{}
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
strings.Builder
|
||||
v []interface{}
|
||||
}
|
||||
|
||||
// NewBuffer creates a new Buffer.
|
||||
func NewBuffer() Buffer {
|
||||
return &buffer{}
|
||||
}
|
||||
|
||||
func (b *buffer) WriteValue(v ...interface{}) error {
|
||||
b.v = append(b.v, v...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *buffer) Value() []interface{} {
|
||||
return b.v
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package dbr
|
||||
|
||||
// Builder builds SQL in Dialect like MySQL, and PostgreSQL.
|
||||
// The raw SQL and values are stored in Buffer.
|
||||
//
|
||||
// The core of gocraft/dbr is interpolation, which can expand ? with arbitrary SQL.
|
||||
// If you need a feature that is not currently supported, you can build it
|
||||
// on your own (or use Expr).
|
||||
//
|
||||
// To do that, the value that you wish to be expanded with ? needs to
|
||||
// implement Builder.
|
||||
type Builder interface {
|
||||
Build(Dialect, Buffer) error
|
||||
}
|
||||
|
||||
// BuildFunc implements Builder.
|
||||
type BuildFunc func(Dialect, Buffer) error
|
||||
|
||||
// Build calls itself to build SQL.
|
||||
func (b BuildFunc) Build(d Dialect, buf Buffer) error {
|
||||
return b(d, buf)
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package dbr
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
openingSQLComment = "/*"
|
||||
closingSQLComment = "*/"
|
||||
space = " "
|
||||
newline = "\n"
|
||||
emptyString = ""
|
||||
)
|
||||
|
||||
// Comments represents a set of sql comments
|
||||
type Comments []string
|
||||
|
||||
// Append a new sql comment to a set of comments
|
||||
func (comments Comments) Append(comment string) Comments {
|
||||
comment = strings.Replace(comment, openingSQLComment, emptyString, -1)
|
||||
comment = strings.Replace(comment, closingSQLComment, emptyString, -1)
|
||||
comment = strings.TrimSpace(comment)
|
||||
comments = append(comments, comment)
|
||||
return comments
|
||||
}
|
||||
|
||||
// Build writes each comment in the form of "/* some comment */\n"
|
||||
func (comments Comments) Build(d Dialect, buf Buffer) error {
|
||||
for _, comment := range comments {
|
||||
words := []string{openingSQLComment, space, comment, space, closingSQLComment, newline}
|
||||
for _, str := range words {
|
||||
if _, err := buf.WriteString(str); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func buildCond(d Dialect, buf Buffer, pred string, cond ...Builder) error {
|
||||
for i, c := range cond {
|
||||
if i > 0 {
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(pred)
|
||||
buf.WriteString(" ")
|
||||
}
|
||||
buf.WriteString("(")
|
||||
err := c.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(")")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// And creates AND from a list of conditions.
|
||||
func And(cond ...Builder) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildCond(d, buf, "AND", cond...)
|
||||
})
|
||||
}
|
||||
|
||||
// Or creates OR from a list of conditions.
|
||||
func Or(cond ...Builder) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildCond(d, buf, "OR", cond...)
|
||||
})
|
||||
}
|
||||
|
||||
func buildCmp(d Dialect, buf Buffer, pred string, column string, value interface{}) error {
|
||||
buf.WriteString(d.QuoteIdent(column))
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(pred)
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(placeholder)
|
||||
|
||||
buf.WriteValue(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Eq is `=`.
|
||||
// When value is nil, it will be translated to `IS NULL`.
|
||||
// When value is a slice, it will be translated to `IN`.
|
||||
// Otherwise it will be translated to `=`.
|
||||
func Eq(column string, value interface{}) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
if value == nil {
|
||||
buf.WriteString(d.QuoteIdent(column))
|
||||
buf.WriteString(" IS NULL")
|
||||
return nil
|
||||
}
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Kind() == reflect.Slice {
|
||||
if v.Len() == 0 {
|
||||
buf.WriteString(d.EncodeBool(false))
|
||||
return nil
|
||||
}
|
||||
return buildCmp(d, buf, "IN", column, value)
|
||||
}
|
||||
return buildCmp(d, buf, "=", column, value)
|
||||
})
|
||||
}
|
||||
|
||||
// Neq is `!=`.
|
||||
// When value is nil, it will be translated to `IS NOT NULL`.
|
||||
// When value is a slice, it will be translated to `NOT IN`.
|
||||
// Otherwise it will be translated to `!=`.
|
||||
func Neq(column string, value interface{}) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
if value == nil {
|
||||
buf.WriteString(d.QuoteIdent(column))
|
||||
buf.WriteString(" IS NOT NULL")
|
||||
return nil
|
||||
}
|
||||
v := reflect.ValueOf(value)
|
||||
if v.Kind() == reflect.Slice {
|
||||
if v.Len() == 0 {
|
||||
buf.WriteString(d.EncodeBool(true))
|
||||
return nil
|
||||
}
|
||||
return buildCmp(d, buf, "NOT IN", column, value)
|
||||
}
|
||||
return buildCmp(d, buf, "!=", column, value)
|
||||
})
|
||||
}
|
||||
|
||||
// Gt is `>`.
|
||||
func Gt(column string, value interface{}) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildCmp(d, buf, ">", column, value)
|
||||
})
|
||||
}
|
||||
|
||||
// Gte is '>='.
|
||||
func Gte(column string, value interface{}) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildCmp(d, buf, ">=", column, value)
|
||||
})
|
||||
}
|
||||
|
||||
// Lt is '<'.
|
||||
func Lt(column string, value interface{}) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildCmp(d, buf, "<", column, value)
|
||||
})
|
||||
}
|
||||
|
||||
// Lte is `<=`.
|
||||
func Lte(column string, value interface{}) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildCmp(d, buf, "<=", column, value)
|
||||
})
|
||||
}
|
||||
|
||||
func buildLike(d Dialect, buf Buffer, column, pattern string, isNot bool, escape []string) error {
|
||||
buf.WriteString(d.QuoteIdent(column))
|
||||
if isNot {
|
||||
buf.WriteString(" NOT LIKE ")
|
||||
} else {
|
||||
buf.WriteString(" LIKE ")
|
||||
}
|
||||
buf.WriteString(d.EncodeString(pattern))
|
||||
if len(escape) > 0 {
|
||||
buf.WriteString(" ESCAPE ")
|
||||
buf.WriteString(d.EncodeString(escape[0]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Like is `LIKE`, with an optional `ESCAPE` clause
|
||||
func Like(column, value string, escape ...string) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildLike(d, buf, column, value, false, escape)
|
||||
})
|
||||
}
|
||||
|
||||
// NotLike is `NOT LIKE`, with an optional `ESCAPE` clause
|
||||
func NotLike(column, value string, escape ...string) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
return buildLike(d, buf, column, value, true, escape)
|
||||
})
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
// Package dbr provides additions to Go's database/sql for super fast performance and convenience.
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gocraft/dbr/v2/dialect"
|
||||
)
|
||||
|
||||
// Open creates a Connection.
|
||||
// log can be nil to ignore logging.
|
||||
func Open(driver, dsn string, log EventReceiver) (*Connection, error) {
|
||||
if log == nil {
|
||||
log = nullReceiver
|
||||
}
|
||||
conn, err := sql.Open(driver, dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d Dialect
|
||||
switch driver {
|
||||
case "mysql":
|
||||
d = dialect.MySQL
|
||||
case "postgres", "pgx":
|
||||
d = dialect.PostgreSQL
|
||||
case "sqlite3":
|
||||
d = dialect.SQLite3
|
||||
default:
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
return &Connection{DB: conn, EventReceiver: log, Dialect: d}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
placeholder = "?"
|
||||
)
|
||||
|
||||
// Connection wraps sql.DB with an EventReceiver
|
||||
// to send events, errors, and timings.
|
||||
type Connection struct {
|
||||
*sql.DB
|
||||
Dialect
|
||||
EventReceiver
|
||||
}
|
||||
|
||||
// Session represents a business unit of execution.
|
||||
//
|
||||
// All queries in gocraft/dbr are made in the context of a session.
|
||||
// This is because when instrumenting your app, it's important
|
||||
// to understand which business action the query took place in.
|
||||
//
|
||||
// A custom EventReceiver can be set.
|
||||
//
|
||||
// Timeout specifies max duration for an operation like Select.
|
||||
type Session struct {
|
||||
*Connection
|
||||
EventReceiver
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// GetTimeout returns current timeout enforced in session.
|
||||
func (sess *Session) GetTimeout() time.Duration {
|
||||
return sess.Timeout
|
||||
}
|
||||
|
||||
// NewSession instantiates a Session from Connection.
|
||||
// If log is nil, Connection EventReceiver is used.
|
||||
func (conn *Connection) NewSession(log EventReceiver) *Session {
|
||||
if log == nil {
|
||||
log = conn.EventReceiver // Use parent instrumentation
|
||||
}
|
||||
return &Session{Connection: conn, EventReceiver: log}
|
||||
}
|
||||
|
||||
// Ensure that tx and session are session runner
|
||||
var (
|
||||
_ SessionRunner = (*Tx)(nil)
|
||||
_ SessionRunner = (*Session)(nil)
|
||||
)
|
||||
|
||||
// SessionRunner can do anything that a Session can except start a transaction.
|
||||
// Both Session and Tx implements this interface.
|
||||
type SessionRunner interface {
|
||||
Select(column ...string) *SelectBuilder
|
||||
SelectBySql(query string, value ...interface{}) *SelectBuilder
|
||||
|
||||
InsertInto(table string) *InsertBuilder
|
||||
InsertBySql(query string, value ...interface{}) *InsertBuilder
|
||||
|
||||
Update(table string) *UpdateBuilder
|
||||
UpdateBySql(query string, value ...interface{}) *UpdateBuilder
|
||||
|
||||
DeleteFrom(table string) *DeleteBuilder
|
||||
DeleteBySql(query string, value ...interface{}) *DeleteBuilder
|
||||
}
|
||||
|
||||
type runner interface {
|
||||
GetTimeout() time.Duration
|
||||
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
|
||||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
func exec(ctx context.Context, runner runner, log EventReceiver, builder Builder, d Dialect) (sql.Result, error) {
|
||||
timeout := runner.GetTimeout()
|
||||
if timeout > 0 {
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
i := interpolator{
|
||||
Buffer: NewBuffer(),
|
||||
Dialect: d,
|
||||
IgnoreBinary: true,
|
||||
}
|
||||
err := i.encodePlaceholder(builder, true)
|
||||
query, value := i.String(), i.Value()
|
||||
if err != nil {
|
||||
return nil, log.EventErrKv("dbr.exec.interpolate", err, kvs{
|
||||
"sql": query,
|
||||
"args": fmt.Sprint(value),
|
||||
})
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
log.TimingKv("dbr.exec", time.Since(startTime).Nanoseconds(), kvs{
|
||||
"sql": query,
|
||||
})
|
||||
}()
|
||||
|
||||
traceImpl, hasTracingImpl := log.(TracingEventReceiver)
|
||||
if hasTracingImpl {
|
||||
ctx = traceImpl.SpanStart(ctx, "dbr.exec", query)
|
||||
defer traceImpl.SpanFinish(ctx)
|
||||
}
|
||||
|
||||
result, err := runner.ExecContext(ctx, query, value...)
|
||||
if err != nil {
|
||||
if hasTracingImpl {
|
||||
traceImpl.SpanError(ctx, err)
|
||||
}
|
||||
return result, log.EventErrKv("dbr.exec.exec", err, kvs{
|
||||
"sql": query,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func queryRows(ctx context.Context, runner runner, log EventReceiver, builder Builder, d Dialect) (string, *sql.Rows, error) {
|
||||
// discard the timeout set in the runner, the context should not be canceled
|
||||
// implicitly here but explicitly by the caller since the returned *sql.Rows
|
||||
// may still listening to the context
|
||||
i := interpolator{
|
||||
Buffer: NewBuffer(),
|
||||
Dialect: d,
|
||||
IgnoreBinary: true,
|
||||
}
|
||||
err := i.encodePlaceholder(builder, true)
|
||||
query, value := i.String(), i.Value()
|
||||
if err != nil {
|
||||
return query, nil, log.EventErrKv("dbr.select.interpolate", err, kvs{
|
||||
"sql": query,
|
||||
"args": fmt.Sprint(value),
|
||||
})
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
log.TimingKv("dbr.select", time.Since(startTime).Nanoseconds(), kvs{
|
||||
"sql": query,
|
||||
})
|
||||
}()
|
||||
|
||||
traceImpl, hasTracingImpl := log.(TracingEventReceiver)
|
||||
if hasTracingImpl {
|
||||
ctx = traceImpl.SpanStart(ctx, "dbr.select", query)
|
||||
defer traceImpl.SpanFinish(ctx)
|
||||
}
|
||||
|
||||
rows, err := runner.QueryContext(ctx, query, value...)
|
||||
if err != nil {
|
||||
if hasTracingImpl {
|
||||
traceImpl.SpanError(ctx, err)
|
||||
}
|
||||
return query, nil, log.EventErrKv("dbr.select.load.query", err, kvs{
|
||||
"sql": query,
|
||||
})
|
||||
}
|
||||
|
||||
return query, rows, nil
|
||||
}
|
||||
|
||||
func query(ctx context.Context, runner runner, log EventReceiver, builder Builder, d Dialect, dest interface{}) (int, error) {
|
||||
timeout := runner.GetTimeout()
|
||||
if timeout > 0 {
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
query, rows, err := queryRows(ctx, runner, log, builder, d)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
count, err := Load(rows, dest)
|
||||
if err != nil {
|
||||
return 0, log.EventErrKv("dbr.select.load.scan", err, kvs{
|
||||
"sql": query,
|
||||
})
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// DeleteStmt builds `DELETE ...`.
|
||||
type DeleteStmt struct {
|
||||
runner
|
||||
EventReceiver
|
||||
Dialect
|
||||
|
||||
raw
|
||||
|
||||
Table string
|
||||
WhereCond []Builder
|
||||
LimitCount int64
|
||||
|
||||
comments Comments
|
||||
}
|
||||
|
||||
type DeleteBuilder = DeleteStmt
|
||||
|
||||
func (b *DeleteStmt) Build(d Dialect, buf Buffer) error {
|
||||
if b.raw.Query != "" {
|
||||
return b.raw.Build(d, buf)
|
||||
}
|
||||
|
||||
if b.Table == "" {
|
||||
return ErrTableNotSpecified
|
||||
}
|
||||
|
||||
err := b.comments.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.WriteString("DELETE FROM ")
|
||||
buf.WriteString(d.QuoteIdent(b.Table))
|
||||
|
||||
if len(b.WhereCond) > 0 {
|
||||
buf.WriteString(" WHERE ")
|
||||
err := And(b.WhereCond...).Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if b.LimitCount >= 0 {
|
||||
buf.WriteString(" LIMIT ")
|
||||
buf.WriteString(strconv.FormatInt(b.LimitCount, 10))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFrom creates a DeleteStmt.
|
||||
func DeleteFrom(table string) *DeleteStmt {
|
||||
return &DeleteStmt{
|
||||
Table: table,
|
||||
LimitCount: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteFrom creates a DeleteStmt.
|
||||
func (sess *Session) DeleteFrom(table string) *DeleteStmt {
|
||||
b := DeleteFrom(table)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// DeleteFrom creates a DeleteStmt.
|
||||
func (tx *Tx) DeleteFrom(table string) *DeleteStmt {
|
||||
b := DeleteFrom(table)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// DeleteBySql creates a DeleteStmt from raw query.
|
||||
func DeleteBySql(query string, value ...interface{}) *DeleteStmt {
|
||||
return &DeleteStmt{
|
||||
raw: raw{
|
||||
Query: query,
|
||||
Value: value,
|
||||
},
|
||||
LimitCount: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteBySql creates a DeleteStmt from raw query.
|
||||
func (sess *Session) DeleteBySql(query string, value ...interface{}) *DeleteStmt {
|
||||
b := DeleteBySql(query, value...)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// DeleteBySql creates a DeleteStmt from raw query.
|
||||
func (tx *Tx) DeleteBySql(query string, value ...interface{}) *DeleteStmt {
|
||||
b := DeleteBySql(query, value...)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// Where adds a where condition.
|
||||
// query can be Builder or string. value is used only if query type is string.
|
||||
func (b *DeleteStmt) Where(query interface{}, value ...interface{}) *DeleteStmt {
|
||||
switch query := query.(type) {
|
||||
case string:
|
||||
b.WhereCond = append(b.WhereCond, Expr(query, value...))
|
||||
case Builder:
|
||||
b.WhereCond = append(b.WhereCond, query)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *DeleteStmt) Limit(n uint64) *DeleteStmt {
|
||||
b.LimitCount = int64(n)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *DeleteStmt) Comment(comment string) *DeleteStmt {
|
||||
b.comments = b.comments.Append(comment)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *DeleteStmt) Exec() (sql.Result, error) {
|
||||
return b.ExecContext(context.Background())
|
||||
}
|
||||
|
||||
func (b *DeleteStmt) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
return exec(ctx, b.runner, b.EventReceiver, b, b.Dialect)
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package dbr
|
||||
|
||||
import "time"
|
||||
|
||||
// Dialect abstracts database driver differences in encoding
|
||||
// types, and placeholders.
|
||||
type Dialect interface {
|
||||
QuoteIdent(id string) string
|
||||
|
||||
EncodeString(s string) string
|
||||
EncodeBool(b bool) string
|
||||
EncodeTime(t time.Time) string
|
||||
EncodeBytes(b []byte) string
|
||||
|
||||
Placeholder(n int) string
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package dialect
|
||||
|
||||
import "strings"
|
||||
|
||||
var (
|
||||
// MySQL dialect
|
||||
MySQL = mysql{}
|
||||
// PostgreSQL dialect
|
||||
PostgreSQL = postgreSQL{}
|
||||
// SQLite3 dialect
|
||||
SQLite3 = sqlite3{}
|
||||
)
|
||||
|
||||
const (
|
||||
timeFormat = "2006-01-02 15:04:05.000000"
|
||||
)
|
||||
|
||||
func quoteIdent(s, quote string) string {
|
||||
part := strings.SplitN(s, ".", 2)
|
||||
if len(part) == 2 {
|
||||
return quoteIdent(part[0], quote) + "." + quoteIdent(part[1], quote)
|
||||
}
|
||||
return quote + s + quote
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package dialect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mysql struct{}
|
||||
|
||||
func (d mysql) QuoteIdent(s string) string {
|
||||
return quoteIdent(s, "`")
|
||||
}
|
||||
|
||||
func (d mysql) EncodeString(s string) string {
|
||||
var buf strings.Builder
|
||||
|
||||
buf.WriteRune('\'')
|
||||
// https://dev.mysql.com/doc/refman/5.7/en/string-literals.html
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case 0:
|
||||
buf.WriteString(`\0`)
|
||||
case '\'':
|
||||
buf.WriteString(`\'`)
|
||||
case '"':
|
||||
buf.WriteString(`\"`)
|
||||
case '\b':
|
||||
buf.WriteString(`\b`)
|
||||
case '\n':
|
||||
buf.WriteString(`\n`)
|
||||
case '\r':
|
||||
buf.WriteString(`\r`)
|
||||
case '\t':
|
||||
buf.WriteString(`\t`)
|
||||
case 26:
|
||||
buf.WriteString(`\Z`)
|
||||
case '\\':
|
||||
buf.WriteString(`\\`)
|
||||
default:
|
||||
buf.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteRune('\'')
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (d mysql) EncodeBool(b bool) string {
|
||||
if b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (d mysql) EncodeTime(t time.Time) string {
|
||||
return `'` + t.UTC().Format(timeFormat) + `'`
|
||||
}
|
||||
|
||||
func (d mysql) EncodeBytes(b []byte) string {
|
||||
return fmt.Sprintf(`0x%x`, b)
|
||||
}
|
||||
|
||||
func (d mysql) Placeholder(_ int) string {
|
||||
return "?"
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package dialect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type postgreSQL struct{}
|
||||
|
||||
func (d postgreSQL) QuoteIdent(s string) string {
|
||||
return quoteIdent(s, `"`)
|
||||
}
|
||||
|
||||
func (d postgreSQL) EncodeString(s string) string {
|
||||
// http://www.postgresql.org/docs/9.2/static/sql-syntax-lexical.html
|
||||
return `'` + strings.Replace(s, `'`, `''`, -1) + `'`
|
||||
}
|
||||
|
||||
func (d postgreSQL) EncodeBool(b bool) string {
|
||||
if b {
|
||||
return "TRUE"
|
||||
}
|
||||
return "FALSE"
|
||||
}
|
||||
|
||||
func (d postgreSQL) EncodeTime(t time.Time) string {
|
||||
return MySQL.EncodeTime(t)
|
||||
}
|
||||
|
||||
func (d postgreSQL) EncodeBytes(b []byte) string {
|
||||
return fmt.Sprintf(`E'\\x%x'`, b)
|
||||
}
|
||||
|
||||
func (d postgreSQL) Placeholder(n int) string {
|
||||
return fmt.Sprintf("$%d", n+1)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package dialect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type sqlite3 struct{}
|
||||
|
||||
func (d sqlite3) QuoteIdent(s string) string {
|
||||
return quoteIdent(s, `"`)
|
||||
}
|
||||
|
||||
func (d sqlite3) EncodeString(s string) string {
|
||||
// https://www.sqlite.org/faq.html
|
||||
return `'` + strings.Replace(s, `'`, `''`, -1) + `'`
|
||||
}
|
||||
|
||||
func (d sqlite3) EncodeBool(b bool) string {
|
||||
// https://www.sqlite.org/lang_expr.html
|
||||
if b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (d sqlite3) EncodeTime(t time.Time) string {
|
||||
// https://www.sqlite.org/lang_datefunc.html
|
||||
return MySQL.EncodeTime(t)
|
||||
}
|
||||
|
||||
func (d sqlite3) EncodeBytes(b []byte) string {
|
||||
// https://www.sqlite.org/lang_expr.html
|
||||
return fmt.Sprintf(`X'%x'`, b)
|
||||
}
|
||||
|
||||
func (d sqlite3) Placeholder(_ int) string {
|
||||
return "?"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package dbr
|
||||
|
||||
import "errors"
|
||||
|
||||
// package errors
|
||||
var (
|
||||
ErrNotFound = errors.New("dbr: not found")
|
||||
ErrNotSupported = errors.New("dbr: not supported")
|
||||
ErrTableNotSpecified = errors.New("dbr: table not specified")
|
||||
ErrColumnNotSpecified = errors.New("dbr: column not specified")
|
||||
ErrInvalidPointer = errors.New("dbr: attempt to load into an invalid pointer")
|
||||
ErrPlaceholderCount = errors.New("dbr: wrong placeholder count")
|
||||
ErrInvalidSliceLength = errors.New("dbr: length of slice is 0. length must be >= 1")
|
||||
ErrCantConvertToTime = errors.New("dbr: can't convert to time.Time")
|
||||
ErrInvalidTimestring = errors.New("dbr: invalid time string")
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package dbr
|
||||
|
||||
import "context"
|
||||
|
||||
// EventReceiver gets events from dbr methods for logging purposes.
|
||||
type EventReceiver interface {
|
||||
Event(eventName string)
|
||||
EventKv(eventName string, kvs map[string]string)
|
||||
EventErr(eventName string, err error) error
|
||||
EventErrKv(eventName string, err error, kvs map[string]string) error
|
||||
Timing(eventName string, nanoseconds int64)
|
||||
TimingKv(eventName string, nanoseconds int64, kvs map[string]string)
|
||||
}
|
||||
|
||||
// TracingEventReceiver is an optional interface an EventReceiver type can implement
|
||||
// to allow tracing instrumentation
|
||||
type TracingEventReceiver interface {
|
||||
SpanStart(ctx context.Context, eventName, query string) context.Context
|
||||
SpanError(ctx context.Context, err error)
|
||||
SpanFinish(ctx context.Context)
|
||||
}
|
||||
|
||||
type kvs map[string]string
|
||||
|
||||
var nullReceiver = &NullEventReceiver{}
|
||||
|
||||
// NullEventReceiver is a sentinel EventReceiver.
|
||||
// Use it if the caller doesn't supply one.
|
||||
type NullEventReceiver struct{}
|
||||
|
||||
// Event receives a simple notification when various events occur.
|
||||
func (n *NullEventReceiver) Event(eventName string) {}
|
||||
|
||||
// EventKv receives a notification when various events occur along with
|
||||
// optional key/value data.
|
||||
func (n *NullEventReceiver) EventKv(eventName string, kvs map[string]string) {}
|
||||
|
||||
// EventErr receives a notification of an error if one occurs.
|
||||
func (n *NullEventReceiver) EventErr(eventName string, err error) error { return err }
|
||||
|
||||
// EventErrKv receives a notification of an error if one occurs along with
|
||||
// optional key/value data.
|
||||
func (n *NullEventReceiver) EventErrKv(eventName string, err error, kvs map[string]string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Timing receives the time an event took to happen.
|
||||
func (n *NullEventReceiver) Timing(eventName string, nanoseconds int64) {}
|
||||
|
||||
// TimingKv receives the time an event took to happen along with optional key/value data.
|
||||
func (n *NullEventReceiver) TimingKv(eventName string, nanoseconds int64, kvs map[string]string) {}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package dbr
|
||||
|
||||
type raw struct {
|
||||
Query string
|
||||
Value []interface{}
|
||||
}
|
||||
|
||||
// Expr allows raw expression to be used when current SQL syntax is
|
||||
// not supported by gocraft/dbr.
|
||||
func Expr(query string, value ...interface{}) Builder {
|
||||
return &raw{Query: query, Value: value}
|
||||
}
|
||||
|
||||
func (raw *raw) Build(_ Dialect, buf Buffer) error {
|
||||
buf.WriteString(raw.Query)
|
||||
buf.WriteValue(raw.Value...)
|
||||
return nil
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
module github.com/gocraft/dbr/v2
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3
|
||||
github.com/go-sql-driver/mysql v1.4.1
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.11.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package dbr
|
||||
|
||||
// I is quoted identifier
|
||||
type I string
|
||||
|
||||
// Build quotes string with dialect.
|
||||
func (i I) Build(d Dialect, buf Buffer) error {
|
||||
buf.WriteString(d.QuoteIdent(string(i)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// As creates an alias for expr.
|
||||
func (i I) As(alias string) Builder {
|
||||
return as(i, alias)
|
||||
}
|
||||
|
||||
func as(expr interface{}, alias string) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
buf.WriteString(placeholder)
|
||||
buf.WriteValue(expr)
|
||||
buf.WriteString(" AS ")
|
||||
buf.WriteString(d.QuoteIdent(alias))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InsertStmt builds `INSERT INTO ...`.
|
||||
type InsertStmt struct {
|
||||
runner
|
||||
EventReceiver
|
||||
Dialect
|
||||
|
||||
raw
|
||||
|
||||
Table string
|
||||
Column []string
|
||||
Value [][]interface{}
|
||||
ReturnColumn []string
|
||||
RecordID *int64
|
||||
comments Comments
|
||||
}
|
||||
|
||||
type InsertBuilder = InsertStmt
|
||||
|
||||
func (b *InsertStmt) Build(d Dialect, buf Buffer) error {
|
||||
if b.raw.Query != "" {
|
||||
return b.raw.Build(d, buf)
|
||||
}
|
||||
|
||||
if b.Table == "" {
|
||||
return ErrTableNotSpecified
|
||||
}
|
||||
|
||||
if len(b.Column) == 0 {
|
||||
return ErrColumnNotSpecified
|
||||
}
|
||||
|
||||
err := b.comments.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.WriteString("INSERT INTO ")
|
||||
buf.WriteString(d.QuoteIdent(b.Table))
|
||||
|
||||
var placeholderBuf strings.Builder
|
||||
placeholderBuf.WriteString("(")
|
||||
buf.WriteString(" (")
|
||||
for i, col := range b.Column {
|
||||
if i > 0 {
|
||||
buf.WriteString(",")
|
||||
placeholderBuf.WriteString(",")
|
||||
}
|
||||
buf.WriteString(d.QuoteIdent(col))
|
||||
placeholderBuf.WriteString(placeholder)
|
||||
}
|
||||
buf.WriteString(") VALUES ")
|
||||
placeholderBuf.WriteString(")")
|
||||
placeholderStr := placeholderBuf.String()
|
||||
|
||||
for i, tuple := range b.Value {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
buf.WriteString(placeholderStr)
|
||||
|
||||
buf.WriteValue(tuple...)
|
||||
}
|
||||
|
||||
if len(b.ReturnColumn) > 0 {
|
||||
buf.WriteString(" RETURNING ")
|
||||
for i, col := range b.ReturnColumn {
|
||||
if i > 0 {
|
||||
buf.WriteString(",")
|
||||
}
|
||||
buf.WriteString(d.QuoteIdent(col))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertInto creates an InsertStmt.
|
||||
func InsertInto(table string) *InsertStmt {
|
||||
return &InsertStmt{
|
||||
Table: table,
|
||||
}
|
||||
}
|
||||
|
||||
// InsertInto creates an InsertStmt.
|
||||
func (sess *Session) InsertInto(table string) *InsertStmt {
|
||||
b := InsertInto(table)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// InsertInto creates an InsertStmt.
|
||||
func (tx *Tx) InsertInto(table string) *InsertStmt {
|
||||
b := InsertInto(table)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// InsertBySql creates an InsertStmt from raw query.
|
||||
func InsertBySql(query string, value ...interface{}) *InsertStmt {
|
||||
return &InsertStmt{
|
||||
raw: raw{
|
||||
Query: query,
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// InsertBySql creates an InsertStmt from raw query.
|
||||
func (sess *Session) InsertBySql(query string, value ...interface{}) *InsertStmt {
|
||||
b := InsertBySql(query, value...)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// InsertBySql creates an InsertStmt from raw query.
|
||||
func (tx *Tx) InsertBySql(query string, value ...interface{}) *InsertStmt {
|
||||
b := InsertBySql(query, value...)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *InsertStmt) Columns(column ...string) *InsertStmt {
|
||||
b.Column = column
|
||||
return b
|
||||
}
|
||||
|
||||
// Comment adds a comment to prepended. All multi-line sql comment characters are stripped
|
||||
func (b *InsertStmt) Comment(comment string) *InsertStmt {
|
||||
b.comments = b.comments.Append(comment)
|
||||
return b
|
||||
}
|
||||
|
||||
// Values adds a tuple to be inserted.
|
||||
// The order of the tuple should match Columns.
|
||||
func (b *InsertStmt) Values(value ...interface{}) *InsertStmt {
|
||||
b.Value = append(b.Value, value)
|
||||
return b
|
||||
}
|
||||
|
||||
// Record adds a tuple for columns from a struct.
|
||||
//
|
||||
// If there is a field called "Id" or "ID" in the struct,
|
||||
// it will be set to LastInsertId.
|
||||
func (b *InsertStmt) Record(structValue interface{}) *InsertStmt {
|
||||
v := reflect.Indirect(reflect.ValueOf(structValue))
|
||||
|
||||
if v.Kind() == reflect.Struct {
|
||||
found := make([]interface{}, len(b.Column)+1)
|
||||
// ID is recommended by golint here
|
||||
s := newTagStore()
|
||||
s.findValueByName(v, append(b.Column, "id"), found, false)
|
||||
|
||||
value := found[:len(found)-1]
|
||||
for i, v := range value {
|
||||
if v != nil {
|
||||
value[i] = v.(reflect.Value).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
if v.CanSet() {
|
||||
switch idField := found[len(found)-1].(type) {
|
||||
case reflect.Value:
|
||||
if idField.Kind() == reflect.Int64 {
|
||||
b.RecordID = idField.Addr().Interface().(*int64)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Values(value...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Returning specifies the returning columns for postgres.
|
||||
func (b *InsertStmt) Returning(column ...string) *InsertStmt {
|
||||
b.ReturnColumn = column
|
||||
return b
|
||||
}
|
||||
|
||||
// Pair adds (column, value) to be inserted.
|
||||
// It is an error to mix Pair with Values and Record.
|
||||
func (b *InsertStmt) Pair(column string, value interface{}) *InsertStmt {
|
||||
b.Column = append(b.Column, column)
|
||||
switch len(b.Value) {
|
||||
case 0:
|
||||
b.Values(value)
|
||||
case 1:
|
||||
b.Value[0] = append(b.Value[0], value)
|
||||
default:
|
||||
panic("pair only allows one record to insert")
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *InsertStmt) Exec() (sql.Result, error) {
|
||||
return b.ExecContext(context.Background())
|
||||
}
|
||||
|
||||
func (b *InsertStmt) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
result, err := exec(ctx, b.runner, b.EventReceiver, b, b.Dialect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b.RecordID != nil {
|
||||
if id, err := result.LastInsertId(); err == nil {
|
||||
*b.RecordID = id
|
||||
}
|
||||
b.RecordID = nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *InsertStmt) LoadContext(ctx context.Context, value interface{}) error {
|
||||
_, err := query(ctx, b.runner, b.EventReceiver, b, b.Dialect, value)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *InsertStmt) Load(value interface{}) error {
|
||||
return b.LoadContext(context.Background(), value)
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type interpolator struct {
|
||||
Buffer
|
||||
Dialect
|
||||
IgnoreBinary bool
|
||||
N int
|
||||
}
|
||||
|
||||
// InterpolateForDialect replaces placeholder
|
||||
// in query with corresponding value in dialect.
|
||||
//
|
||||
// It can be also used for debugging custom Builder.
|
||||
//
|
||||
// Every time you call database/sql's db.Query("SELECT ...") method,
|
||||
// under the hood, the mysql driver will create a prepared statement,
|
||||
// execute it, and then throw it away. This has a big performance cost.
|
||||
//
|
||||
// gocraft/dbr doesn't use prepared statements.
|
||||
// We ported mysql's query escape functionality directly into our package,
|
||||
// which means we interpolate all of those question marks with
|
||||
// their arguments before they get to MySQL.
|
||||
// The result of this is that it's way faster, and just as secure.
|
||||
//
|
||||
// Check out these benchmarks from https://github.com/tyler-smith/golang-sql-benchmark.
|
||||
func InterpolateForDialect(query string, value []interface{}, d Dialect) (string, error) {
|
||||
i := interpolator{
|
||||
Buffer: NewBuffer(),
|
||||
Dialect: d,
|
||||
}
|
||||
err := i.interpolate(query, value, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return i.String(), nil
|
||||
}
|
||||
|
||||
var escapedPlaceholder = strings.Repeat(placeholder, 2)
|
||||
|
||||
func (i *interpolator) interpolate(query string, value []interface{}, topLevel bool) error {
|
||||
valueIndex := 0
|
||||
|
||||
for {
|
||||
index := strings.Index(query, placeholder)
|
||||
if index == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// escape placeholder by repeating it twice
|
||||
if strings.HasPrefix(query[index:], escapedPlaceholder) {
|
||||
i.WriteString(query[:index+1]) // Write placeholder once, not twice
|
||||
query = query[index+len(escapedPlaceholder):]
|
||||
continue
|
||||
}
|
||||
|
||||
if valueIndex >= len(value) {
|
||||
return ErrPlaceholderCount
|
||||
}
|
||||
|
||||
i.WriteString(query[:index])
|
||||
if _, ok := value[valueIndex].([]byte); ok && i.IgnoreBinary {
|
||||
i.WriteString(i.Placeholder(i.N))
|
||||
i.N++
|
||||
i.WriteValue(value[valueIndex])
|
||||
} else {
|
||||
err := i.encodePlaceholder(value[valueIndex], topLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
query = query[index+len(placeholder):]
|
||||
valueIndex++
|
||||
}
|
||||
|
||||
if valueIndex != len(value) {
|
||||
return ErrPlaceholderCount
|
||||
}
|
||||
|
||||
// placeholder not found; write remaining query
|
||||
i.WriteString(query)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
typeTime = reflect.TypeOf(time.Time{})
|
||||
)
|
||||
|
||||
func (i *interpolator) encodePlaceholder(value interface{}, topLevel bool) error {
|
||||
if builder, ok := value.(Builder); ok {
|
||||
pbuf := NewBuffer()
|
||||
err := builder.Build(i.Dialect, pbuf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paren := false
|
||||
switch value.(type) {
|
||||
case *SelectStmt, *union:
|
||||
paren = !topLevel
|
||||
}
|
||||
if paren {
|
||||
i.WriteString("(")
|
||||
}
|
||||
err = i.interpolate(pbuf.String(), pbuf.Value(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if paren {
|
||||
i.WriteString(")")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if valuer, ok := value.(driver.Valuer); ok {
|
||||
// get driver.Valuer's data
|
||||
var err error
|
||||
value, err = valuer.Value()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
i.WriteString("NULL")
|
||||
return nil
|
||||
}
|
||||
v := reflect.ValueOf(value)
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
i.WriteString(i.EncodeString(v.String()))
|
||||
return nil
|
||||
case reflect.Bool:
|
||||
i.WriteString(i.EncodeBool(v.Bool()))
|
||||
return nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i.WriteString(strconv.FormatInt(v.Int(), 10))
|
||||
return nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
i.WriteString(strconv.FormatUint(v.Uint(), 10))
|
||||
return nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
i.WriteString(strconv.FormatFloat(v.Float(), 'f', -1, 64))
|
||||
return nil
|
||||
case reflect.Struct:
|
||||
if v.Type() == typeTime {
|
||||
i.WriteString(i.EncodeTime(v.Interface().(time.Time)))
|
||||
return nil
|
||||
}
|
||||
case reflect.Slice:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 {
|
||||
// []byte
|
||||
i.WriteString(i.EncodeBytes(v.Bytes()))
|
||||
return nil
|
||||
}
|
||||
if v.Len() == 0 {
|
||||
// FIXME: support zero-length slice
|
||||
return ErrInvalidSliceLength
|
||||
}
|
||||
i.WriteString("(")
|
||||
for n := 0; n < v.Len(); n++ {
|
||||
if n > 0 {
|
||||
i.WriteString(",")
|
||||
}
|
||||
err := i.encodePlaceholder(v.Index(n).Interface(), topLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
i.WriteString(")")
|
||||
return nil
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
i.WriteString("NULL")
|
||||
return nil
|
||||
}
|
||||
return i.encodePlaceholder(v.Elem().Interface(), topLevel)
|
||||
}
|
||||
return ErrNotSupported
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package dbr
|
||||
|
||||
type joinType uint8
|
||||
|
||||
const (
|
||||
inner joinType = iota
|
||||
left
|
||||
right
|
||||
full
|
||||
)
|
||||
|
||||
func join(t joinType, table interface{}, on interface{}) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
buf.WriteString(" ")
|
||||
switch t {
|
||||
case left:
|
||||
buf.WriteString("LEFT ")
|
||||
case right:
|
||||
buf.WriteString("RIGHT ")
|
||||
case full:
|
||||
buf.WriteString("FULL ")
|
||||
}
|
||||
buf.WriteString("JOIN ")
|
||||
switch table := table.(type) {
|
||||
case string:
|
||||
buf.WriteString(d.QuoteIdent(table))
|
||||
default:
|
||||
buf.WriteString(placeholder)
|
||||
buf.WriteValue(table)
|
||||
}
|
||||
buf.WriteString(" ON ")
|
||||
switch on := on.(type) {
|
||||
case string:
|
||||
buf.WriteString(on)
|
||||
case Builder:
|
||||
buf.WriteString(placeholder)
|
||||
buf.WriteValue(on)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type interfaceLoader struct {
|
||||
v interface{}
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
func InterfaceLoader(value interface{}, concreteType interface{}) interface{} {
|
||||
return interfaceLoader{value, reflect.TypeOf(concreteType)}
|
||||
}
|
||||
|
||||
// Load loads any value from sql.Rows.
|
||||
//
|
||||
// value can be:
|
||||
//
|
||||
// 1. simple type like int64, string, etc.
|
||||
//
|
||||
// 2. sql.Scanner, which allows loading with custom types.
|
||||
//
|
||||
// 3. map; the first column from SQL result loaded to the key,
|
||||
// and the rest of columns will be loaded into the value.
|
||||
// This is useful to dedup SQL result with first column.
|
||||
//
|
||||
// 4. map of slice; like map, values with the same key are
|
||||
// collected with a slice.
|
||||
func Load(rows *sql.Rows, value interface{}) (int, error) {
|
||||
defer rows.Close()
|
||||
|
||||
column, err := rows.Columns()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ptr := make([]interface{}, len(column))
|
||||
|
||||
var v reflect.Value
|
||||
var elemType reflect.Type
|
||||
|
||||
if il, ok := value.(interfaceLoader); ok {
|
||||
v = reflect.ValueOf(il.v)
|
||||
elemType = il.typ
|
||||
} else {
|
||||
v = reflect.ValueOf(value)
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Ptr || v.IsNil() {
|
||||
return 0, ErrInvalidPointer
|
||||
}
|
||||
v = v.Elem()
|
||||
isScanner := v.Addr().Type().Implements(typeScanner)
|
||||
isSlice := v.Kind() == reflect.Slice && v.Type().Elem().Kind() != reflect.Uint8 && !isScanner
|
||||
isMap := v.Kind() == reflect.Map && !isScanner
|
||||
isMapOfSlices := isMap && v.Type().Elem().Kind() == reflect.Slice && v.Type().Elem().Elem().Kind() != reflect.Uint8
|
||||
if isMap {
|
||||
v.Set(reflect.MakeMap(v.Type()))
|
||||
}
|
||||
|
||||
s := newTagStore()
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var elem, keyElem reflect.Value
|
||||
|
||||
if elemType != nil {
|
||||
elem = reflectAlloc(elemType)
|
||||
} else if isMapOfSlices {
|
||||
elem = reflectAlloc(v.Type().Elem().Elem())
|
||||
} else if isSlice || isMap {
|
||||
elem = reflectAlloc(v.Type().Elem())
|
||||
} else {
|
||||
elem = v
|
||||
}
|
||||
|
||||
if isMap {
|
||||
err := s.findPtr(elem, column[1:], ptr[1:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
keyElem = reflectAlloc(v.Type().Key())
|
||||
err = s.findPtr(keyElem, column[:1], ptr[:1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
err := s.findPtr(elem, column, ptr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Before scanning, set nil pointer to dummy dest.
|
||||
// After that, reset pointers to nil for the next batch.
|
||||
for i := range ptr {
|
||||
if ptr[i] == nil {
|
||||
ptr[i] = dummyDest
|
||||
}
|
||||
}
|
||||
err = rows.Scan(ptr...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for i := range ptr {
|
||||
ptr[i] = nil
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
if isSlice {
|
||||
v.Set(reflect.Append(v, elem))
|
||||
} else if isMapOfSlices {
|
||||
s := v.MapIndex(keyElem)
|
||||
if !s.IsValid() {
|
||||
s = reflect.Zero(v.Type().Elem())
|
||||
}
|
||||
v.SetMapIndex(keyElem, reflect.Append(s, elem))
|
||||
} else if isMap {
|
||||
v.SetMapIndex(keyElem, elem)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count, rows.Err()
|
||||
}
|
||||
|
||||
func reflectAlloc(typ reflect.Type) reflect.Value {
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
return reflect.New(typ.Elem())
|
||||
}
|
||||
return reflect.New(typ).Elem()
|
||||
}
|
||||
|
||||
type dummyScanner struct{}
|
||||
|
||||
func (dummyScanner) Scan(interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
dummyDest sql.Scanner = dummyScanner{}
|
||||
typeScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Now is a value that serializes to the current time in UTC.
|
||||
var Now = nowSentinel{}
|
||||
|
||||
const timeFormat = "2006-01-02 15:04:05.000000"
|
||||
|
||||
type nowSentinel struct{}
|
||||
|
||||
// Value implements a valuer for compatibility
|
||||
func (n nowSentinel) Value() (driver.Value, error) {
|
||||
now := time.Now().UTC().Format(timeFormat)
|
||||
return now, nil
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package dbr
|
||||
|
||||
type direction bool
|
||||
|
||||
// orderby directions
|
||||
// most databases by default use asc
|
||||
const (
|
||||
asc direction = false
|
||||
desc = true
|
||||
)
|
||||
|
||||
func order(column string, dir direction) Builder {
|
||||
return BuildFunc(func(d Dialect, buf Buffer) error {
|
||||
// FIXME: no quote ident
|
||||
buf.WriteString(column)
|
||||
switch dir {
|
||||
case asc:
|
||||
buf.WriteString(" ASC")
|
||||
case desc:
|
||||
buf.WriteString(" DESC")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SelectStmt builds `SELECT ...`.
|
||||
type SelectStmt struct {
|
||||
runner
|
||||
EventReceiver
|
||||
Dialect
|
||||
|
||||
raw
|
||||
|
||||
IsDistinct bool
|
||||
|
||||
Column []interface{}
|
||||
Table interface{}
|
||||
JoinTable []Builder
|
||||
|
||||
WhereCond []Builder
|
||||
Group []Builder
|
||||
HavingCond []Builder
|
||||
Order []Builder
|
||||
Suffixes []Builder
|
||||
|
||||
LimitCount int64
|
||||
OffsetCount int64
|
||||
|
||||
comments Comments
|
||||
}
|
||||
|
||||
type SelectBuilder = SelectStmt
|
||||
|
||||
func (b *SelectStmt) Build(d Dialect, buf Buffer) error {
|
||||
if b.raw.Query != "" {
|
||||
return b.raw.Build(d, buf)
|
||||
}
|
||||
|
||||
if len(b.Column) == 0 {
|
||||
return ErrColumnNotSpecified
|
||||
}
|
||||
|
||||
err := b.comments.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.WriteString("SELECT ")
|
||||
|
||||
if b.IsDistinct {
|
||||
buf.WriteString("DISTINCT ")
|
||||
}
|
||||
|
||||
for i, col := range b.Column {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
switch col := col.(type) {
|
||||
case string:
|
||||
// FIXME: no quote ident
|
||||
buf.WriteString(col)
|
||||
default:
|
||||
buf.WriteString(placeholder)
|
||||
buf.WriteValue(col)
|
||||
}
|
||||
}
|
||||
|
||||
if b.Table != nil {
|
||||
buf.WriteString(" FROM ")
|
||||
switch table := b.Table.(type) {
|
||||
case string:
|
||||
// FIXME: no quote ident
|
||||
buf.WriteString(table)
|
||||
default:
|
||||
buf.WriteString(placeholder)
|
||||
buf.WriteValue(table)
|
||||
}
|
||||
if len(b.JoinTable) > 0 {
|
||||
for _, join := range b.JoinTable {
|
||||
err := join.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.WhereCond) > 0 {
|
||||
buf.WriteString(" WHERE ")
|
||||
err := And(b.WhereCond...).Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.Group) > 0 {
|
||||
buf.WriteString(" GROUP BY ")
|
||||
for i, group := range b.Group {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
err := group.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.HavingCond) > 0 {
|
||||
buf.WriteString(" HAVING ")
|
||||
err := And(b.HavingCond...).Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.Order) > 0 {
|
||||
buf.WriteString(" ORDER BY ")
|
||||
for i, order := range b.Order {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
err := order.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.LimitCount >= 0 {
|
||||
buf.WriteString(" LIMIT ")
|
||||
buf.WriteString(strconv.FormatInt(b.LimitCount, 10))
|
||||
}
|
||||
|
||||
if b.OffsetCount >= 0 {
|
||||
buf.WriteString(" OFFSET ")
|
||||
buf.WriteString(strconv.FormatInt(b.OffsetCount, 10))
|
||||
}
|
||||
|
||||
if len(b.Suffixes) > 0 {
|
||||
for _, suffix := range b.Suffixes {
|
||||
buf.WriteString(" ")
|
||||
err := suffix.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Select creates a SelectStmt.
|
||||
func Select(column ...interface{}) *SelectStmt {
|
||||
return &SelectStmt{
|
||||
Column: column,
|
||||
LimitCount: -1,
|
||||
OffsetCount: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSelect(a []string) []interface{} {
|
||||
b := make([]interface{}, len(a))
|
||||
for i := range a {
|
||||
b[i] = a[i]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Select creates a SelectStmt.
|
||||
func (sess *Session) Select(column ...string) *SelectStmt {
|
||||
b := Select(prepareSelect(column)...)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// Select creates a SelectStmt.
|
||||
func (tx *Tx) Select(column ...string) *SelectStmt {
|
||||
b := Select(prepareSelect(column)...)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// SelectBySql creates a SelectStmt from raw query.
|
||||
func SelectBySql(query string, value ...interface{}) *SelectStmt {
|
||||
return &SelectStmt{
|
||||
raw: raw{
|
||||
Query: query,
|
||||
Value: value,
|
||||
},
|
||||
LimitCount: -1,
|
||||
OffsetCount: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// SelectBySql creates a SelectStmt from raw query.
|
||||
func (sess *Session) SelectBySql(query string, value ...interface{}) *SelectStmt {
|
||||
b := SelectBySql(query, value...)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// SelectBySql creates a SelectStmt from raw query.
|
||||
func (tx *Tx) SelectBySql(query string, value ...interface{}) *SelectStmt {
|
||||
b := SelectBySql(query, value...)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// From specifies table to select from.
|
||||
// table can be Builder like SelectStmt, or string.
|
||||
func (b *SelectStmt) From(table interface{}) *SelectStmt {
|
||||
b.Table = table
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *SelectStmt) Distinct() *SelectStmt {
|
||||
b.IsDistinct = true
|
||||
return b
|
||||
}
|
||||
|
||||
// Where adds a where condition.
|
||||
// query can be Builder or string. value is used only if query type is string.
|
||||
func (b *SelectStmt) Where(query interface{}, value ...interface{}) *SelectStmt {
|
||||
switch query := query.(type) {
|
||||
case string:
|
||||
b.WhereCond = append(b.WhereCond, Expr(query, value...))
|
||||
case Builder:
|
||||
b.WhereCond = append(b.WhereCond, query)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Having adds a having condition.
|
||||
// query can be Builder or string. value is used only if query type is string.
|
||||
func (b *SelectStmt) Having(query interface{}, value ...interface{}) *SelectStmt {
|
||||
switch query := query.(type) {
|
||||
case string:
|
||||
b.HavingCond = append(b.HavingCond, Expr(query, value...))
|
||||
case Builder:
|
||||
b.HavingCond = append(b.HavingCond, query)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// GroupBy specifies columns for grouping.
|
||||
func (b *SelectStmt) GroupBy(col ...string) *SelectStmt {
|
||||
for _, group := range col {
|
||||
b.Group = append(b.Group, Expr(group))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *SelectStmt) OrderAsc(col string) *SelectStmt {
|
||||
b.Order = append(b.Order, order(col, asc))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *SelectStmt) OrderDesc(col string) *SelectStmt {
|
||||
b.Order = append(b.Order, order(col, desc))
|
||||
return b
|
||||
}
|
||||
|
||||
// OrderBy specifies columns for ordering.
|
||||
func (b *SelectStmt) OrderBy(col string) *SelectStmt {
|
||||
b.Order = append(b.Order, Expr(col))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *SelectStmt) Limit(n uint64) *SelectStmt {
|
||||
b.LimitCount = int64(n)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *SelectStmt) Offset(n uint64) *SelectStmt {
|
||||
b.OffsetCount = int64(n)
|
||||
return b
|
||||
}
|
||||
|
||||
// Suffix adds an expression to the end of the query. This is useful to add dialect-specific clauses like FOR UPDATE
|
||||
func (b *SelectStmt) Suffix(suffix string, value ...interface{}) *SelectStmt {
|
||||
b.Suffixes = append(b.Suffixes, Expr(suffix, value...))
|
||||
return b
|
||||
}
|
||||
|
||||
// Paginate fetches a page in a naive way for a small set of data.
|
||||
func (b *SelectStmt) Paginate(page, perPage uint64) *SelectStmt {
|
||||
b.Limit(perPage)
|
||||
b.Offset((page - 1) * perPage)
|
||||
return b
|
||||
}
|
||||
|
||||
// OrderDir is a helper for OrderAsc and OrderDesc.
|
||||
func (b *SelectStmt) OrderDir(col string, isAsc bool) *SelectStmt {
|
||||
if isAsc {
|
||||
b.OrderAsc(col)
|
||||
} else {
|
||||
b.OrderDesc(col)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *SelectStmt) Comment(comment string) *SelectStmt {
|
||||
b.comments = b.comments.Append(comment)
|
||||
return b
|
||||
}
|
||||
|
||||
// Join add inner-join.
|
||||
// on can be Builder or string.
|
||||
func (b *SelectStmt) Join(table, on interface{}) *SelectStmt {
|
||||
b.JoinTable = append(b.JoinTable, join(inner, table, on))
|
||||
return b
|
||||
}
|
||||
|
||||
// LeftJoin add left-join.
|
||||
// on can be Builder or string.
|
||||
func (b *SelectStmt) LeftJoin(table, on interface{}) *SelectStmt {
|
||||
b.JoinTable = append(b.JoinTable, join(left, table, on))
|
||||
return b
|
||||
}
|
||||
|
||||
// RightJoin add right-join.
|
||||
// on can be Builder or string.
|
||||
func (b *SelectStmt) RightJoin(table, on interface{}) *SelectStmt {
|
||||
b.JoinTable = append(b.JoinTable, join(right, table, on))
|
||||
return b
|
||||
}
|
||||
|
||||
// FullJoin add full-join.
|
||||
// on can be Builder or string.
|
||||
func (b *SelectStmt) FullJoin(table, on interface{}) *SelectStmt {
|
||||
b.JoinTable = append(b.JoinTable, join(full, table, on))
|
||||
return b
|
||||
}
|
||||
|
||||
// As creates alias for select statement.
|
||||
func (b *SelectStmt) As(alias string) Builder {
|
||||
return as(b, alias)
|
||||
}
|
||||
|
||||
// Rows executes the query and returns the rows returned, or any error encountered.
|
||||
func (b *SelectStmt) Rows() (*sql.Rows, error) {
|
||||
return b.RowsContext(context.Background())
|
||||
}
|
||||
|
||||
func (b *SelectStmt) RowsContext(ctx context.Context) (*sql.Rows, error) {
|
||||
_, rows, err := queryRows(ctx, b.runner, b.EventReceiver, b, b.Dialect)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (b *SelectStmt) LoadOneContext(ctx context.Context, value interface{}) error {
|
||||
count, err := query(ctx, b.runner, b.EventReceiver, b, b.Dialect, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOne loads SQL result into go variable that is not a slice.
|
||||
// Unlike Load, it returns ErrNotFound if the SQL result row count is 0.
|
||||
//
|
||||
// See https://godoc.org/github.com/gocraft/dbr#Load.
|
||||
func (b *SelectStmt) LoadOne(value interface{}) error {
|
||||
return b.LoadOneContext(context.Background(), value)
|
||||
}
|
||||
|
||||
func (b *SelectStmt) LoadContext(ctx context.Context, value interface{}) (int, error) {
|
||||
return query(ctx, b.runner, b.EventReceiver, b, b.Dialect, value)
|
||||
}
|
||||
|
||||
// Load loads multi-row SQL result into a slice of go variables.
|
||||
//
|
||||
// See https://godoc.org/github.com/gocraft/dbr#Load.
|
||||
func (b *SelectStmt) Load(value interface{}) (int, error) {
|
||||
return b.LoadContext(context.Background(), value)
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package dbr
|
||||
|
||||
// ReturnInt64 executes the SelectStmt and returns the value as an int64.
|
||||
func (b *SelectStmt) ReturnInt64() (int64, error) {
|
||||
var v int64
|
||||
err := b.LoadOne(&v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// ReturnInt64s executes the SelectStmt and returns the value as a slice of int64s.
|
||||
func (b *SelectStmt) ReturnInt64s() ([]int64, error) {
|
||||
var v []int64
|
||||
_, err := b.Load(&v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// ReturnUint64 executes the SelectStmt and returns the value as an uint64.
|
||||
func (b *SelectStmt) ReturnUint64() (uint64, error) {
|
||||
var v uint64
|
||||
err := b.LoadOne(&v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// ReturnUint64s executes the SelectStmt and returns the value as a slice of uint64s.
|
||||
func (b *SelectStmt) ReturnUint64s() ([]uint64, error) {
|
||||
var v []uint64
|
||||
_, err := b.Load(&v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// ReturnString executes the SelectStmt and returns the value as a string.
|
||||
func (b *SelectStmt) ReturnString() (string, error) {
|
||||
var v string
|
||||
err := b.LoadOne(&v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// ReturnStrings executes the SelectStmt and returns the value as a slice of strings.
|
||||
func (b *SelectStmt) ReturnStrings() ([]string, error) {
|
||||
var v []string
|
||||
_, err := b.Load(&v)
|
||||
return v, err
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Tx is a transaction created by Session.
|
||||
type Tx struct {
|
||||
EventReceiver
|
||||
Dialect
|
||||
*sql.Tx
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// GetTimeout returns timeout enforced in Tx.
|
||||
func (tx *Tx) GetTimeout() time.Duration {
|
||||
return tx.Timeout
|
||||
}
|
||||
|
||||
// BeginTx creates a transaction with TxOptions.
|
||||
func (sess *Session) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) {
|
||||
tx, err := sess.Connection.BeginTx(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, sess.EventErr("dbr.begin.error", err)
|
||||
}
|
||||
sess.Event("dbr.begin")
|
||||
|
||||
return &Tx{
|
||||
EventReceiver: sess.EventReceiver,
|
||||
Dialect: sess.Dialect,
|
||||
Tx: tx,
|
||||
Timeout: sess.GetTimeout(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Begin creates a transaction for the given session.
|
||||
func (sess *Session) Begin() (*Tx, error) {
|
||||
return sess.BeginTx(context.Background(), nil)
|
||||
}
|
||||
|
||||
// Commit finishes the transaction.
|
||||
func (tx *Tx) Commit() error {
|
||||
err := tx.Tx.Commit()
|
||||
if err != nil {
|
||||
return tx.EventErr("dbr.commit.error", err)
|
||||
}
|
||||
tx.Event("dbr.commit")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback cancels the transaction.
|
||||
func (tx *Tx) Rollback() error {
|
||||
err := tx.Tx.Rollback()
|
||||
if err != nil {
|
||||
return tx.EventErr("dbr.rollback", err)
|
||||
}
|
||||
tx.Event("dbr.rollback")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackUnlessCommitted rollsback the transaction unless
|
||||
// it has already been committed or rolled back.
|
||||
//
|
||||
// Useful to defer tx.RollbackUnlessCommitted(), so you don't
|
||||
// have to handle N failure cases.
|
||||
// Keep in mind the only way to detect an error on the rollback
|
||||
// is via the event log.
|
||||
func (tx *Tx) RollbackUnlessCommitted() {
|
||||
err := tx.Tx.Rollback()
|
||||
if err == sql.ErrTxDone {
|
||||
// ok
|
||||
} else if err != nil {
|
||||
tx.EventErr("dbr.rollback_unless_committed", err)
|
||||
} else {
|
||||
tx.Event("dbr.rollback")
|
||||
}
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
//
|
||||
// Your app can use these Null types instead of the defaults. The sole benefit you get is a MarshalJSON method that is not retarded.
|
||||
//
|
||||
|
||||
// NullString is a type that can be null or a string.
|
||||
type NullString struct {
|
||||
sql.NullString
|
||||
}
|
||||
|
||||
// NullFloat64 is a type that can be null or a float64.
|
||||
type NullFloat64 struct {
|
||||
sql.NullFloat64
|
||||
}
|
||||
|
||||
// NullInt64 is a type that can be null or an int.
|
||||
type NullInt64 struct {
|
||||
sql.NullInt64
|
||||
}
|
||||
|
||||
// NullTime is a type that can be null or a time.
|
||||
type NullTime struct {
|
||||
Time time.Time
|
||||
Valid bool // Valid is true if Time is not NULL
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (n NullTime) Value() (driver.Value, error) {
|
||||
if !n.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return n.Time, nil
|
||||
}
|
||||
|
||||
// NullBool is a type that can be null or a bool.
|
||||
type NullBool struct {
|
||||
sql.NullBool
|
||||
}
|
||||
|
||||
var nullString = []byte("null")
|
||||
|
||||
// MarshalJSON correctly serializes a NullString to JSON.
|
||||
func (n NullString) MarshalJSON() ([]byte, error) {
|
||||
if n.Valid {
|
||||
return json.Marshal(n.String)
|
||||
}
|
||||
return nullString, nil
|
||||
}
|
||||
|
||||
// MarshalJSON correctly serializes a NullInt64 to JSON.
|
||||
func (n NullInt64) MarshalJSON() ([]byte, error) {
|
||||
if n.Valid {
|
||||
return json.Marshal(n.Int64)
|
||||
}
|
||||
return nullString, nil
|
||||
}
|
||||
|
||||
// MarshalJSON correctly serializes a NullFloat64 to JSON.
|
||||
func (n NullFloat64) MarshalJSON() ([]byte, error) {
|
||||
if n.Valid {
|
||||
return json.Marshal(n.Float64)
|
||||
}
|
||||
return nullString, nil
|
||||
}
|
||||
|
||||
// MarshalJSON correctly serializes a NullTime to JSON.
|
||||
func (n NullTime) MarshalJSON() ([]byte, error) {
|
||||
if n.Valid {
|
||||
return json.Marshal(n.Time)
|
||||
}
|
||||
return nullString, nil
|
||||
}
|
||||
|
||||
// MarshalJSON correctly serializes a NullBool to JSON.
|
||||
func (n NullBool) MarshalJSON() ([]byte, error) {
|
||||
if n.Valid {
|
||||
return json.Marshal(n.Bool)
|
||||
}
|
||||
return nullString, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON correctly deserializes a NullString from JSON.
|
||||
func (n *NullString) UnmarshalJSON(b []byte) error {
|
||||
var s interface{}
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.Scan(s)
|
||||
}
|
||||
|
||||
// UnmarshalJSON correctly deserializes a NullInt64 from JSON.
|
||||
func (n *NullInt64) UnmarshalJSON(b []byte) error {
|
||||
var s json.Number
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
return n.Scan(nil)
|
||||
}
|
||||
return n.Scan(s)
|
||||
}
|
||||
|
||||
// UnmarshalJSON correctly deserializes a NullFloat64 from JSON.
|
||||
func (n *NullFloat64) UnmarshalJSON(b []byte) error {
|
||||
var s interface{}
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.Scan(s)
|
||||
}
|
||||
|
||||
// UnmarshalJSON correctly deserializes a NullTime from JSON.
|
||||
func (n *NullTime) UnmarshalJSON(b []byte) error {
|
||||
// scan for null
|
||||
if bytes.Equal(b, nullString) {
|
||||
return n.Scan(nil)
|
||||
}
|
||||
// scan for JSON timestamp
|
||||
var t time.Time
|
||||
if err := json.Unmarshal(b, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.Scan(t)
|
||||
}
|
||||
|
||||
// UnmarshalJSON correctly deserializes a NullBool from JSON.
|
||||
func (n *NullBool) UnmarshalJSON(b []byte) error {
|
||||
var s interface{}
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.Scan(s)
|
||||
}
|
||||
|
||||
// NewNullInt64 creates a NullInt64 with Scan().
|
||||
func NewNullInt64(v interface{}) (n NullInt64) {
|
||||
n.Scan(v)
|
||||
return
|
||||
}
|
||||
|
||||
// NewNullFloat64 creates a NullFloat64 with Scan().
|
||||
func NewNullFloat64(v interface{}) (n NullFloat64) {
|
||||
n.Scan(v)
|
||||
return
|
||||
}
|
||||
|
||||
// NewNullString creates a NullString with Scan().
|
||||
func NewNullString(v interface{}) (n NullString) {
|
||||
n.Scan(v)
|
||||
return
|
||||
}
|
||||
|
||||
// NewNullTime creates a NullTime with Scan().
|
||||
func NewNullTime(v interface{}) (n NullTime) {
|
||||
n.Scan(v)
|
||||
return
|
||||
}
|
||||
|
||||
// NewNullBool creates a NullBool with Scan().
|
||||
func NewNullBool(v interface{}) (n NullBool) {
|
||||
n.Scan(v)
|
||||
return
|
||||
}
|
||||
|
||||
// The `(*NullTime) Scan(interface{})` and `parseDateTime(string, *time.Location)`
|
||||
// functions are slightly modified versions of code from the github.com/go-sql-driver/mysql
|
||||
// package. They work with Postgres and MySQL databases. Potential future
|
||||
// drivers should ensure these will work for them, or come up with an alternative.
|
||||
//
|
||||
// Conforming with its licensing terms the copyright notice and link to the licence
|
||||
// are available below.
|
||||
//
|
||||
// Source: https://github.com/go-sql-driver/mysql/blob/527bcd55aab2e53314f1a150922560174b493034/utils.go#L452-L508
|
||||
|
||||
// Copyright notice from original developers:
|
||||
//
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
// The value type must be time.Time or string / []byte (formatted time-string),
|
||||
// otherwise Scan fails.
|
||||
func (n *NullTime) Scan(value interface{}) error {
|
||||
var err error
|
||||
|
||||
if value == nil {
|
||||
n.Time, n.Valid = time.Time{}, false
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case time.Time:
|
||||
n.Time, n.Valid = v, true
|
||||
return nil
|
||||
case []byte:
|
||||
n.Time, err = parseDateTime(string(v), time.UTC)
|
||||
n.Valid = (err == nil)
|
||||
return err
|
||||
case string:
|
||||
n.Time, err = parseDateTime(v, time.UTC)
|
||||
n.Valid = (err == nil)
|
||||
return err
|
||||
}
|
||||
|
||||
n.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDateTime(str string, loc *time.Location) (time.Time, error) {
|
||||
var t time.Time
|
||||
var err error
|
||||
|
||||
base := "0000-00-00 00:00:00.0000000"
|
||||
switch len(str) {
|
||||
case 10, 19, 21, 22, 23, 24, 25, 26:
|
||||
if str == base[:len(str)] {
|
||||
return t, err
|
||||
}
|
||||
t, err = time.Parse(timeFormat[:len(str)], str)
|
||||
default:
|
||||
err = ErrInvalidTimestring
|
||||
return t, err
|
||||
}
|
||||
|
||||
// Adjust location
|
||||
if err == nil && loc != time.UTC {
|
||||
y, mo, d := t.Date()
|
||||
h, mi, s := t.Clock()
|
||||
t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
|
||||
}
|
||||
|
||||
return t, err
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package dbr
|
||||
|
||||
type union struct {
|
||||
builder []Builder
|
||||
all bool
|
||||
}
|
||||
|
||||
// Union builds `... UNION ...`.
|
||||
func Union(builder ...Builder) interface {
|
||||
Builder
|
||||
As(string) Builder
|
||||
} {
|
||||
return &union{
|
||||
builder: builder,
|
||||
}
|
||||
}
|
||||
|
||||
// UnionAll builds `... UNION ALL ...`.
|
||||
func UnionAll(builder ...Builder) interface {
|
||||
Builder
|
||||
As(string) Builder
|
||||
} {
|
||||
return &union{
|
||||
builder: builder,
|
||||
all: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *union) Build(d Dialect, buf Buffer) error {
|
||||
for i, b := range u.builder {
|
||||
if i > 0 {
|
||||
buf.WriteString(" UNION ")
|
||||
if u.all {
|
||||
buf.WriteString("ALL ")
|
||||
}
|
||||
}
|
||||
err := b.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *union) As(alias string) Builder {
|
||||
return as(u, alias)
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// UpdateStmt builds `UPDATE ...`.
|
||||
type UpdateStmt struct {
|
||||
runner
|
||||
EventReceiver
|
||||
Dialect
|
||||
|
||||
raw
|
||||
|
||||
Table string
|
||||
Value map[string]interface{}
|
||||
WhereCond []Builder
|
||||
ReturnColumn []string
|
||||
LimitCount int64
|
||||
comments Comments
|
||||
}
|
||||
|
||||
type UpdateBuilder = UpdateStmt
|
||||
|
||||
func (b *UpdateStmt) Build(d Dialect, buf Buffer) error {
|
||||
if b.raw.Query != "" {
|
||||
return b.raw.Build(d, buf)
|
||||
}
|
||||
|
||||
if b.Table == "" {
|
||||
return ErrTableNotSpecified
|
||||
}
|
||||
|
||||
if len(b.Value) == 0 {
|
||||
return ErrColumnNotSpecified
|
||||
}
|
||||
|
||||
err := b.comments.Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.WriteString("UPDATE ")
|
||||
buf.WriteString(d.QuoteIdent(b.Table))
|
||||
buf.WriteString(" SET ")
|
||||
|
||||
i := 0
|
||||
for col, v := range b.Value {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
buf.WriteString(d.QuoteIdent(col))
|
||||
buf.WriteString(" = ")
|
||||
buf.WriteString(placeholder)
|
||||
|
||||
buf.WriteValue(v)
|
||||
i++
|
||||
}
|
||||
|
||||
if len(b.WhereCond) > 0 {
|
||||
buf.WriteString(" WHERE ")
|
||||
err := And(b.WhereCond...).Build(d, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.ReturnColumn) > 0 {
|
||||
buf.WriteString(" RETURNING ")
|
||||
for i, col := range b.ReturnColumn {
|
||||
if i > 0 {
|
||||
buf.WriteString(",")
|
||||
}
|
||||
buf.WriteString(d.QuoteIdent(col))
|
||||
}
|
||||
}
|
||||
|
||||
if b.LimitCount >= 0 {
|
||||
buf.WriteString(" LIMIT ")
|
||||
buf.WriteString(strconv.FormatInt(b.LimitCount, 10))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update creates an UpdateStmt.
|
||||
func Update(table string) *UpdateStmt {
|
||||
return &UpdateStmt{
|
||||
Table: table,
|
||||
Value: make(map[string]interface{}),
|
||||
LimitCount: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Update creates an UpdateStmt.
|
||||
func (sess *Session) Update(table string) *UpdateStmt {
|
||||
b := Update(table)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// Update creates an UpdateStmt.
|
||||
func (tx *Tx) Update(table string) *UpdateStmt {
|
||||
b := Update(table)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// UpdateBySql creates an UpdateStmt with raw query.
|
||||
func UpdateBySql(query string, value ...interface{}) *UpdateStmt {
|
||||
return &UpdateStmt{
|
||||
raw: raw{
|
||||
Query: query,
|
||||
Value: value,
|
||||
},
|
||||
Value: make(map[string]interface{}),
|
||||
LimitCount: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateBySql creates an UpdateStmt with raw query.
|
||||
func (sess *Session) UpdateBySql(query string, value ...interface{}) *UpdateStmt {
|
||||
b := UpdateBySql(query, value...)
|
||||
b.runner = sess
|
||||
b.EventReceiver = sess.EventReceiver
|
||||
b.Dialect = sess.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// UpdateBySql creates an UpdateStmt with raw query.
|
||||
func (tx *Tx) UpdateBySql(query string, value ...interface{}) *UpdateStmt {
|
||||
b := UpdateBySql(query, value...)
|
||||
b.runner = tx
|
||||
b.EventReceiver = tx.EventReceiver
|
||||
b.Dialect = tx.Dialect
|
||||
return b
|
||||
}
|
||||
|
||||
// Where adds a where condition.
|
||||
// query can be Builder or string. value is used only if query type is string.
|
||||
func (b *UpdateStmt) Where(query interface{}, value ...interface{}) *UpdateStmt {
|
||||
switch query := query.(type) {
|
||||
case string:
|
||||
b.WhereCond = append(b.WhereCond, Expr(query, value...))
|
||||
case Builder:
|
||||
b.WhereCond = append(b.WhereCond, query)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Returning specifies the returning columns for postgres.
|
||||
func (b *UpdateStmt) Returning(column ...string) *UpdateStmt {
|
||||
b.ReturnColumn = column
|
||||
return b
|
||||
}
|
||||
|
||||
// Set updates column with value.
|
||||
func (b *UpdateStmt) Set(column string, value interface{}) *UpdateStmt {
|
||||
b.Value[column] = value
|
||||
return b
|
||||
}
|
||||
|
||||
// SetMap specifies a map of (column, value) to update in bulk.
|
||||
func (b *UpdateStmt) SetMap(m map[string]interface{}) *UpdateStmt {
|
||||
for col, val := range m {
|
||||
b.Set(col, val)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *UpdateStmt) Limit(n uint64) *UpdateStmt {
|
||||
b.LimitCount = int64(n)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *UpdateStmt) Comment(comment string) *UpdateStmt {
|
||||
b.comments = b.comments.Append(comment)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *UpdateStmt) Exec() (sql.Result, error) {
|
||||
return b.ExecContext(context.Background())
|
||||
}
|
||||
|
||||
func (b *UpdateStmt) ExecContext(ctx context.Context) (sql.Result, error) {
|
||||
return exec(ctx, b.runner, b.EventReceiver, b, b.Dialect)
|
||||
}
|
||||
|
||||
func (b *UpdateStmt) LoadContext(ctx context.Context, value interface{}) error {
|
||||
_, err := query(ctx, b.runner, b.EventReceiver, b, b.Dialect, value)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *UpdateStmt) Load(value interface{}) error {
|
||||
return b.LoadContext(context.Background(), value)
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package dbr
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var NameMapping = camelCaseToSnakeCase
|
||||
|
||||
func isUpper(b byte) bool {
|
||||
return 'A' <= b && b <= 'Z'
|
||||
}
|
||||
|
||||
func isLower(b byte) bool {
|
||||
return 'a' <= b && b <= 'z'
|
||||
}
|
||||
|
||||
func isDigit(b byte) bool {
|
||||
return '0' <= b && b <= '9'
|
||||
}
|
||||
|
||||
func toLower(b byte) byte {
|
||||
if isUpper(b) {
|
||||
return b - 'A' + 'a'
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func camelCaseToSnakeCase(name string) string {
|
||||
var buf strings.Builder
|
||||
buf.Grow(len(name) * 2)
|
||||
|
||||
for i := 0; i < len(name); i++ {
|
||||
buf.WriteByte(toLower(name[i]))
|
||||
if i != len(name)-1 && isUpper(name[i+1]) &&
|
||||
(isLower(name[i]) || isDigit(name[i]) ||
|
||||
(i != len(name)-2 && isLower(name[i+2]))) {
|
||||
buf.WriteByte('_')
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
var (
|
||||
typeValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
|
||||
)
|
||||
|
||||
type tagStore struct {
|
||||
m map[reflect.Type][]string
|
||||
}
|
||||
|
||||
func newTagStore() *tagStore {
|
||||
return &tagStore{
|
||||
m: make(map[reflect.Type][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *tagStore) get(t reflect.Type) []string {
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
if _, ok := s.m[t]; !ok {
|
||||
l := make([]string, t.NumField())
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.PkgPath != "" && !field.Anonymous {
|
||||
// unexported
|
||||
continue
|
||||
}
|
||||
tag := field.Tag.Get("db")
|
||||
if tag == "-" {
|
||||
// ignore
|
||||
continue
|
||||
}
|
||||
if tag == "" {
|
||||
// no tag, but we can record the field name
|
||||
tag = NameMapping(field.Name)
|
||||
}
|
||||
l[i] = tag
|
||||
}
|
||||
s.m[t] = l
|
||||
}
|
||||
return s.m[t]
|
||||
}
|
||||
|
||||
func (s *tagStore) findPtr(value reflect.Value, name []string, ptr []interface{}) error {
|
||||
if value.CanAddr() && value.Addr().Type().Implements(typeScanner) {
|
||||
ptr[0] = value.Addr().Interface()
|
||||
return nil
|
||||
}
|
||||
switch value.Kind() {
|
||||
case reflect.Struct:
|
||||
s.findValueByName(value, name, ptr, true)
|
||||
return nil
|
||||
case reflect.Ptr:
|
||||
if value.IsNil() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
}
|
||||
return s.findPtr(value.Elem(), name, ptr)
|
||||
default:
|
||||
ptr[0] = value.Addr().Interface()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *tagStore) findValueByName(value reflect.Value, name []string, ret []interface{}, retPtr bool) {
|
||||
if value.Type().Implements(typeValuer) {
|
||||
return
|
||||
}
|
||||
switch value.Kind() {
|
||||
case reflect.Ptr:
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
s.findValueByName(value.Elem(), name, ret, retPtr)
|
||||
case reflect.Struct:
|
||||
l := s.get(value.Type())
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
tag := l[i]
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
fieldValue := value.Field(i)
|
||||
for i, want := range name {
|
||||
if want != tag {
|
||||
continue
|
||||
}
|
||||
if ret[i] == nil {
|
||||
if retPtr {
|
||||
ret[i] = fieldValue.Addr().Interface()
|
||||
} else {
|
||||
ret[i] = fieldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
s.findValueByName(fieldValue, name, ret, retPtr)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user