Files
bobiqq c231be0094
build-and-push / detect (push) Successful in 7s
build-and-push / build (${{ fromJSON(needs.detect.outputs.services) }}) (push) Failing after 3m33s
services
2026-08-26 11:04:23 +03:00

217 lines
5.4 KiB
Go

// 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
}