sploit for arena-battle
build-and-push / detect (push) Successful in 10s
build-and-push / build (${{ fromJSON(needs.detect.outputs.services) }}) (push) Successful in 51s

This commit is contained in:
2026-08-26 11:29:05 +03:00
parent 593500424a
commit 5d765a09db
1436 changed files with 539800 additions and 14 deletions
+20
View File
@@ -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
View File
@@ -0,0 +1,173 @@
# gocraft/dbr (database records)
[![GoDoc](https://godoc.org/github.com/gocraft/dbr?status.png)](https://godoc.org/github.com/gocraft/dbr)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fgocraft%2Fdbr.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_shield)
[![Go Report Card](https://goreportcard.com/badge/github.com/gocraft/dbr)](https://goreportcard.com/report/github.com/gocraft/dbr)
[![CircleCI](https://circleci.com/gh/gocraft/dbr.svg?style=svg)](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
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fgocraft%2Fdbr.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_large)
@@ -0,0 +1,90 @@
# gocraft/dbr (database records)
[![GoDoc](https://godoc.org/github.com/gocraft/dbr?status.png)](https://godoc.org/github.com/gocraft/dbr)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fgocraft%2Fdbr.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_shield)
[![Go Report Card](https://goreportcard.com/badge/github.com/gocraft/dbr)](https://goreportcard.com/report/github.com/gocraft/dbr)
[![CircleCI](https://circleci.com/gh/gocraft/dbr.svg?style=svg)](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
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fgocraft%2Fdbr.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fgocraft%2Fdbr?ref=badge_large)
+32
View File
@@ -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
View File
@@ -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)
}
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
}
@@ -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 "?"
}
@@ -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)
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
}