queries

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 30, 2025 License: MPL-2.0 Imports: 8 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.23+

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

db.QueryContext(ctx, qb.Query(), qb.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 MSSQL)
Scanner
type User struct {
    ID   int    `sql:"id"`
    Name string `sql:"name"`
}

for user, _ := range queries.Query[User](ctx, db, "SELECT id, name FROM users") {
    // user.ID, user.Name
}
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"

[!note] To keep the implementation simple, only ExecContext and QueryContext callbacks are supported. If you need to intercept other database operations, such as sql.DB.BeginTx, consider using ngrok/sqlmw instead.

Integration tests cover the following databases and drivers:

🚧 TODOs

  • Add missing documentation.
  • Add more tests for different databases and drivers. See https://go.dev/wiki/SQLDrivers.
  • Add examples for tested databases and drivers.
  • Add benchmarks.

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 Query ¶ added in v0.2.0

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

TODO: document me.

func QueryRow ¶ added in v0.2.0

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

TODO: document me.

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. Do not reuse a single Builder for multiple queries.

func (*Builder) Appendf ¶

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

Appendf formats according to the given format and appends the result to the query. It works like fmt.Appendf, i.e. all rules from the fmt package are applied. In addition, Appendf supports %?, %$, and %@ verbs, which are automatically expanded to the query placeholders ?, $N, and @pN, where N is the auto-incrementing counter. The corresponding arguments can then be accessed with the Builder.Args method. IMPORTANT: to avoid SQL injections, make sure to pass arguments from user input with placeholder verbs. Always test your queries.

Placeholder verbs to database placeholders:

  • MySQL, SQLite: %? -> ?
  • PostgreSQL: %$ -> $N
  • MSSQL: %@ -> @pN

func (*Builder) Args ¶

func (b *Builder) Args() []any

Args returns the argument slice.

func (*Builder) Query ¶ added in v0.2.0

func (b *Builder) Query() string

Query returns the query string. If the query is invalid, e.g. too few/many arguments are given or different placeholders are used, Query panics.

type Interceptor ¶ added in v0.2.0

type Interceptor struct {
	// Driver is a database driver.
	// It must implement [driver.ExecerContext] and [driver.QueryerContext] (most drivers do).
	// Required.
	Driver driver.Driver

	// ExecContext is a callback for both [sql.DB.ExecContext] and [sql.Tx.ExecContext].
	// The implementation must call execer.ExecerContext(ctx, query, args) and return the result.
	// Optional.
	ExecContext func(ctx context.Context, query string, args []driver.NamedValue, execer driver.ExecerContext) (driver.Result, error)

	// QueryContext is a callback for both [sql.DB.QueryContext] and [sql.Tx.QueryContext].
	// The implementation must call queryer.QueryContext(ctx, query, args) and return the result.
	// Optional.
	QueryContext func(ctx context.Context, query string, args []driver.NamedValue, queryer driver.QueryerContext) (driver.Rows, error)
}

Interceptor is a driver.Driver wrapper that allows to register callbacks for database queries. It must first be registered with sql.Register with the same name that is then passed to sql.Open:

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

func (Interceptor) Open ¶ added in v0.2.0

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

Open implements driver.Driver. Do not use it directly.

func (Interceptor) OpenConnector ¶ added in v0.2.0

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

OpenConnector implements driver.DriverContext. Do not use it directly.

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.

Jump to

Keyboard shortcuts

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