queries

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2025 License: MPL-2.0 Imports: 11 Imported by: 0

README ¶

queries

checks pkg.go.dev goreportcard codecov

Convenience helpers for working with SQL queries.

🚀 Features

  • Builder: a strings.Builder wrapper with added support for placeholder verbs to easily build raw SQL queries conditionally.
  • Scanner: sql.DB.Query/QueryRow wrappers that automatically scan sql.Rows into the given struct. Inspired by golang/go#61637.
  • Interceptor: a driver.Driver wrapper to easily add instrumentation (logs, metrics, traces) to the database layer. Similar to gRPC interceptors.

📦 Install

Go 1.24+

go get go-simpler.org/queries

📋 Usage

Builder
columns := []string{"id", "name"}

var qb queries.Builder
qb.Appendf("SELECT %s FROM users", strings.Join(columns, ", "))

if role != nil { // "admin"
    qb.Appendf(" WHERE role = %$", *role)
}
if orderBy != nil { // "name"
    qb.Appendf(" ORDER BY %$", *orderBy)
}
if limit != nil { // 10
    qb.Appendf(" LIMIT %$", *limit)
}

query, args := qb.Build()
db.QueryContext(ctx, query, args...)
// Query: "SELECT id, name FROM users WHERE role = $1 ORDER BY $2 LIMIT $3"
// Args: ["admin", "name", 10]

The following database placeholders are supported:

  • ? (used by MySQL and SQLite)
  • $1, $2, ..., $N (used by PostgreSQL)
  • @p1, @p2, ..., @pN (used by Microsoft SQL Server)
  • :1, :2, ..., :N (used by Oracle Database):
Scanner
type User struct {
    ID   int    `sql:"id"`
    Name string `sql:"name"`
}

// single column, single row:
name, _ := queries.QueryRow[string](ctx, db, "SELECT name FROM users WHERE id = 1")

// single column, multiple rows:
names, _ := queries.Collect(queries.Query[string](ctx, db, "SELECT name FROM users"))

// multiple columns, single row:
user, _ := queries.QueryRow[User](ctx, db, "SELECT id, name FROM users WHERE id = 1")

// multiple columns, multiple rows:
for user, _ := range queries.Query[User](ctx, db, "SELECT id, name FROM users") {
    // ...
}
Interceptor
interceptor := queries.Interceptor{
    Driver: // database driver of your choice.
    ExecContext: func(ctx context.Context, query string, args []driver.NamedValue, execer driver.ExecerContext) (driver.Result, error) {
        slog.InfoContext(ctx, "ExecContext", "query", query)
        return execer.ExecContext(ctx, query, args)
    },
    QueryContext: func(ctx context.Context, query string, args []driver.NamedValue, queryer driver.QueryerContext) (driver.Rows, error) {
        slog.InfoContext(ctx, "QueryContext", "query", query)
        return queryer.QueryContext(ctx, query, args)
    },
}

sql.Register("interceptor", interceptor)
db, _ := sql.Open("interceptor", "dsn")

db.ExecContext(ctx, "INSERT INTO users VALUES (1, 'John Doe')")
// stderr: INFO ExecContext query="INSERT INTO users VALUES (1, 'John Doe')"

db.QueryContext(ctx, "SELECT id, name FROM users")
// stderr: INFO QueryContext query="SELECT id, name FROM users"

Integration tests cover the following databases and drivers:

See integration_test.go for details.

Documentation ¶

Overview ¶

Package queries implements convenience helpers for working with SQL queries.

Index ¶

Constants ¶

This section is empty.

Variables ¶

This section is empty.

Functions ¶

func Build ¶ added in v0.6.0

func Build(format string, a ...any) (query string, args []any)

Build is a shorthand for a new Builder + Builder.Appendf + Builder.Build.

func Collect ¶ added in v0.3.0

func Collect[T any](seq iter.Seq2[T, error]) ([]T, error)

Collect is a slices.Collect variant that collects values from an iter.Seq2[T, error]. If an error occurs during the collection, Collect stops the iteration and returns the error.

func Query ¶ added in v0.2.0

func Query[T any](ctx context.Context, q Queryer, query string, args ...any) iter.Seq2[T, error]

Query executes a query that returns rows, scans each row into a T, and returns an iterator over the Ts. If an error occurs, the iterator yields it as the second value, and the caller should then stop the iteration. Queryer can be either sql.DB or sql.Tx, the rest of the arguments are passed directly to [Queryer.QueryContext]. Query fully manages the lifecycle of the sql.Rows returned by [Queryer.QueryContext], so the caller does not have to.

The following Ts are supported:

  • int (any kind)
  • uint (any kind)
  • float (any kind)
  • bool
  • string
  • time.Time
  • sql.Scanner (implemented by sql.Null types)
  • any struct

See the sql.Rows.Scan documentation for the scanning rules. If the query has multiple columns, T must be a struct, other types can only be used for single-column queries. The fields of a struct T must have the `sql:"COLUMN"` tag, where COLUMN is the name of the corresponding column in the query. Untagged and unexported and fields are ignored.

If the caller prefers the result to be a slice rather than an iterator, Query can be combined with Collect.

func QueryRow ¶ added in v0.2.0

func QueryRow[T any](ctx context.Context, q Queryer, query string, args ...any) (T, error)

QueryRow is a Query variant for queries that are expected to return at most one row, so instead of an iterator, it returns a single T. Like sql.DB.QueryRow, QueryRow returns sql.ErrNoRows if the query selects no rows, otherwise it scans the first row and discards the rest. See the Query documentation for details on supported Ts.

Types ¶

type Builder ¶

type Builder struct {
	// contains filtered or unexported fields
}

Builder is a raw SQL query builder. The zero value is ready to use. Do not copy a non-zero Builder.

func (*Builder) Appendf ¶

func (b *Builder) Appendf(format string, a ...any)

Appendf formats according to the given format and appends the result to the query. It works like fmt.Appendf, meaning all the rules from the fmt package are applied. In addition, Appendf supports special verbs that automatically expand to database placeholders.

-----------------------------------------------
| Database               | Verb | Placeholder |
|------------------------|------|-------------|
| MySQL, MariaDB, SQLite | %?   | ?           |
| PostgreSQL             | %$   | $N          |
| Microsoft SQL Server   | %@   | @pN         |
| Oracle Database        | %:   | :N          |
-----------------------------------------------

Here, N is an auto-incrementing counter. For example, "%$, %$, %$" expands to "$1, $2, $3".

If a special verb includes the "+" flag, it automatically expands to multiple placeholders. For example, given the verb "%+?" and the argument []int{1, 2, 3}, Appendf writes "?, ?, ?" to the query and appends 1, 2, and 3 to the arguments. You may want to use this flag to build "WHERE IN (...)" clauses.

Make sure to always pass arguments from user input with placeholder verbs to avoid SQL injections.

func (*Builder) Build ¶ added in v0.6.0

func (b *Builder) Build() (query string, args []any)

Build returns the query and its arguments.

type Interceptor ¶ added in v0.2.0

type Interceptor struct {
	// Driver is an implementation of [driver.Driver].
	// It must also implement [driver.Pinger], [driver.ConnPrepareContext], and [driver.ConnBeginTx].
	Driver driver.Driver

	// ExecContext is a callback for [sql.DB.ExecContext] and [sql.Tx.ExecContext].
	// The implementation must call execer.ExecerContext(ctx, query, args) and return the result.
	// Note that if the driver does not implement [driver.ExecerContext], the callback will never be called.
	// In this case, consider implementing the PrepareContext callback instead.
	ExecContext func(ctx context.Context, query string, args []driver.NamedValue, execer driver.ExecerContext) (driver.Result, error)

	// QueryContext is a callback for [sql.DB.QueryContext] and [sql.Tx.QueryContext].
	// The implementation must call queryer.QueryContext(ctx, query, args) and return the result.
	// Note that if the driver does not implement [driver.QueryerContext], the callback will never be called.
	// In this case, consider implementing the PrepareContext callback instead.
	QueryContext func(ctx context.Context, query string, args []driver.NamedValue, queryer driver.QueryerContext) (driver.Rows, error)

	// PrepareContext is a callback for [sql.DB.PrepareContext] and [sql.Tx.PrepareContext].
	// The implementation must call preparer.ConnPrepareContext(ctx, query) and return the result.
	PrepareContext func(ctx context.Context, query string, preparer driver.ConnPrepareContext) (driver.Stmt, error)

	// BeginTx is a callback for [sql.DB.BeginTx].
	// The implementation must call beginner.BeginTx(ctx, opts) and return the result.
	BeginTx func(ctx context.Context, opts driver.TxOptions, beginner driver.ConnBeginTx) (driver.Tx, error)
}

Interceptor is a driver.Driver wrapper that allows to register callbacks for SQL queries. The main use case is to instrument code with logs, metrics, and traces without introducing an sql.DB wrapper. An interceptor must first be registered with sql.Register using the same name that is then passed to sql.Open:

interceptor := queries.Interceptor{...}
sql.Register("interceptor", interceptor)
db, err := sql.Open("interceptor", "dsn")

Only the Driver field must be set; all callbacks are optional.

Note that some drivers only partially implement driver.ExecerContext and driver.QueryerContext. A driver may return driver.ErrSkip, which sql.DB interprets as a signal to fall back to a prepared statement. For example, the go-sql-driver/mysql driver only executes a query within sql.DB.ExecContext or sql.DB.QueryContext if the query has no arguments. Otherwise, it prepares a driver.Stmt using driver.ConnPrepareContext, executes it, and closes it. In such cases, you may want to implement both the PrepareContext and ExecContext/QueryContext callbacks, even if you don't prepare statements manually via sql.DB.PrepareContext.

func (Interceptor) Open ¶ added in v0.2.0

func (Interceptor) Open(string) (driver.Conn, error)

Open implements driver.Driver.

func (Interceptor) OpenConnector ¶ added in v0.2.0

func (i Interceptor) OpenConnector(name string) (driver.Connector, error)

OpenConnector implements driver.DriverContext.

type Queryer ¶ added in v0.3.0

type Queryer interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}

Queryer is an interface implemented by sql.DB and sql.Tx.

Directories ¶

Path Synopsis
internal
assert
Package assert implements assertions for the standard testing package.
Package assert implements assertions for the standard testing package.
assert/EF
Package EF provides type aliases for the parent assert package.
Package EF provides type aliases for the parent assert package.
Package queriestest implements utilities for testing SQL queries.
Package queriestest implements utilities for testing SQL queries.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL