Documentation
¶
Overview ¶
Package row maps SQL result rows onto Go values.
It is a thin layer over database/sql that removes the scanning boilerplate without becoming an ORM. Destinations are generic type parameters rather than interface{}, parameter binding is driven by a real SQL scanner instead of a search-and-replace, and errors carry a portable classification.
A minimal program:
db, err := sqlite.Open(ctx, ":memory:")
...
users, err := row.All[User](ctx, db,
`SELECT id, name FROM users WHERE org_id = :org`,
row.Args{"org": 7})
row supports Postgres (through github.com/jackc/pgx/v5) and SQLite (through modernc.org/sqlite). The root package imports neither, so a program compiles only the driver it actually uses.
Index ¶
- Variables
- func Affected(ctx context.Context, s Session, query string, args ...any) (int64, error)
- func All[T any](ctx context.Context, s Session, query string, args ...any) ([]T, error)
- func AllOf[T any](ctx context.Context, s Session, b Builder) ([]T, error)
- func AppendOrdinal(dst []byte, prefix byte, n int) []byte
- func Delete[T any](ctx context.Context, s Session, table string, v *T, opts ...WriteOption) error
- func Exec(ctx context.Context, s Session, query string, args ...any) (sql.Result, error)
- func ExecOf(ctx context.Context, s Session, b Builder) (sql.Result, error)
- func ExpandSlice(v any) ([]any, bool)
- func First[T any](ctx context.Context, s Session, query string, args ...any) (T, error)
- func FirstOf[T any](ctx context.Context, s Session, b Builder) (T, error)
- func InTx(ctx context.Context, s Session, fn func(context.Context, *Tx) error, ...) error
- func InTxRetry(ctx context.Context, s Session, p RetryPolicy, ...) error
- func Insert[T any](ctx context.Context, s Session, table string, v *T, opts ...WriteOption) error
- func InsertMany[T any](ctx context.Context, s Session, table string, vs []T, opts ...WriteOption) error
- func IsCode(err error, c Code) bool
- func Iter[T any](ctx context.Context, s Session, query string, args ...any) iter.Seq2[T, error]
- func IterOf[T any](ctx context.Context, s Session, b Builder) iter.Seq2[T, error]
- func One[T any](ctx context.Context, s Session, query string, args ...any) (T, error)
- func OneOf[T any](ctx context.Context, s Session, b Builder) (T, error)
- func QuoteWith(name string, q byte) string
- func SnakeCase(name string) string
- func Update[T any](ctx context.Context, s Session, table string, v *T, opts ...WriteOption) error
- type Args
- type Builder
- type Code
- type Conn
- type DB
- type Dialect
- type Error
- type Features
- type Hook
- type Option
- type QueryInfo
- type RetryPolicy
- type Session
- type Tx
- type TxOption
- type WriteOption
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultRetryPolicy = RetryPolicy{Attempts: 5, BaseDelay: 5 * time.Millisecond, MaxDelay: 200 * time.Millisecond}
DefaultRetryPolicy is a reasonable starting point: five attempts with exponential backoff from 5ms, capped at 200ms.
var ErrNoRows = sql.ErrNoRows
ErrNoRows is returned by One when a query produces no rows.
It is sql.ErrNoRows, not a copy of it, so code that already tests for sql.ErrNoRows keeps working and errors.Is matches either spelling.
Functions ¶
func Affected ¶
Affected runs a statement and reports how many rows it changed. It is Exec for the common case where the count is the only interesting part.
func All ¶
All runs a query and returns every row as a T.
T may be a struct, a pointer to a struct, a single scannable value such as int or time.Time, or map[string]any. An empty result yields a nil slice and no error.
Arguments are either a single row.Args map, a single struct, or a list of positional values, depending on how the statement spells its placeholders.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
users, err := row.All[User](ctx, db,
`SELECT id, name, email, active FROM users WHERE active = :active ORDER BY id`,
row.Args{"active": true})
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Println(u.ID, u.Name)
}
}
Output: 1 ada 2 grace
Example (InClause) ¶
A slice argument expands into a placeholder list, so IN needs no special handling: no separate expansion and rebinding step.
package main
import (
"context"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
names, err := row.All[string](ctx, db,
`SELECT name FROM users WHERE id IN (:ids) ORDER BY id`,
row.Args{"ids": []int64{1, 3}})
if err != nil {
log.Fatal(err)
}
fmt.Println(names)
}
Output: [ada alan]
Example (LeftJoin) ¶
A pointer to a nested struct stays nil when a LEFT JOIN matched nothing, instead of becoming a zeroed struct that looks like real data.
package main
import (
"context"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
if _, err := row.Exec(ctx, db, `CREATE TABLE orgs (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`); err != nil {
log.Fatal(err)
}
if _, err := row.Exec(ctx, db, `INSERT INTO orgs (id, name) VALUES (1, 'acme')`); err != nil {
log.Fatal(err)
}
if _, err := row.Exec(ctx, db, `ALTER TABLE users ADD COLUMN org_id INTEGER`); err != nil {
log.Fatal(err)
}
if _, err := row.Exec(ctx, db, `UPDATE users SET org_id = 1 WHERE id = 1`); err != nil {
log.Fatal(err)
}
type Org struct {
ID int64 `db:"id,pk"`
Name string
}
type Member struct {
ID int64 `db:"id,pk"`
Name string
Org *Org // reads org_id and org_name
}
members, err := row.All[Member](ctx, db, `
SELECT u.id AS id, u.name AS name, o.id AS org_id, o.name AS org_name
FROM users u LEFT JOIN orgs o ON o.id = u.org_id
WHERE u.id <= 2 ORDER BY u.id`)
if err != nil {
log.Fatal(err)
}
for _, m := range members {
if m.Org == nil {
fmt.Println(m.Name, "no org")
continue
}
fmt.Println(m.Name, m.Org.Name)
}
}
Output: ada acme grace no org
func AllOf ¶
AllOf runs a built query and returns every row as a T.
Example ¶
The builder is for queries whose shape depends on the request.
package main
import (
"context"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/qb"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
search := "a"
onlyActive := true
q := qb.Select("id", "name", "email", "active").From("users")
if search != "" {
q = q.Where(qb.Like{"name": "%" + search + "%"})
}
if onlyActive {
q = q.Where(qb.Eq{"active": true})
}
q = q.OrderBy("id")
users, err := row.AllOf[User](ctx, db, q)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Println(u.Name)
}
}
Output: ada grace
func AppendOrdinal ¶
AppendOrdinal appends prefix followed by the decimal form of n. Postgres dialects use it to emit "$1", "$2" and so on without allocating.
func ExpandSlice ¶
ExpandSlice reports whether v is a slice that a query should expand into a list of bind parameters, and if so returns its elements.
It is exported so that query builders apply exactly the same rule row does: []byte and string are scalars, and a type implementing driver.Valuer knows how to marshal itself and is passed through whole.
func First ¶
First runs a query and returns its first row, ignoring any others. It still returns ErrNoRows for an empty result.
func InTx ¶
func InTx(ctx context.Context, s Session, fn func(context.Context, *Tx) error, opts ...TxOption) error
InTx runs fn inside a transaction.
The transaction commits when fn returns nil and rolls back when it returns an error, which is returned unchanged. If fn panics, the transaction is rolled back and the panic continues to propagate — a panicking handler must never leave a transaction open.
Calling InTx with a *Tx nests: it opens a savepoint, so an inner failure undoes only the inner work. That composes, which means a function that needs a transaction can simply ask for one without knowing whether its caller already started one.
err := row.InTx(ctx, db, func(ctx context.Context, tx *row.Tx) error {
if err := row.Insert(ctx, tx, "users", &u); err != nil {
return err
}
return row.InTx(ctx, tx, func(ctx context.Context, tx *row.Tx) error {
return row.Insert(ctx, tx, "audit", &a) // its own savepoint
})
})
Example ¶
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
// The transaction rolls back because the callback returns an error, and
// the error is returned unchanged.
sentinel := errors.New("changed my mind")
err := row.InTx(ctx, db, func(ctx context.Context, tx *row.Tx) error {
u := User{ID: 9, Name: "temp", Email: "temp@example.com"}
if err := row.Insert(ctx, tx, "users", &u); err != nil {
return err
}
return sentinel
})
fmt.Println(errors.Is(err, sentinel))
n, err := row.One[int64](ctx, db, `SELECT count(*) FROM users`)
if err != nil {
log.Fatal(err)
}
fmt.Println(n)
}
Output: true 3
Example (Nested) ¶
A nested InTx opens a savepoint, so an inner failure undoes only the inner work and the outer transaction carries on.
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
err := row.InTx(ctx, db, func(ctx context.Context, tx *row.Tx) error {
u := User{ID: 10, Name: "kept", Email: "kept@example.com"}
if err := row.Insert(ctx, tx, "users", &u); err != nil {
return err
}
// This inner block fails and is rolled back on its own.
_ = row.InTx(ctx, tx, func(ctx context.Context, tx *row.Tx) error {
v := User{ID: 11, Name: "discarded", Email: "discarded@example.com"}
if err := row.Insert(ctx, tx, "users", &v); err != nil {
return err
}
return errors.New("inner failed")
})
return nil
})
if err != nil {
log.Fatal(err)
}
names, err := row.All[string](ctx, db, `SELECT name FROM users WHERE id >= 10 ORDER BY id`)
if err != nil {
log.Fatal(err)
}
fmt.Println(names)
}
Output: [kept]
func InTxRetry ¶
func InTxRetry(ctx context.Context, s Session, p RetryPolicy, fn func(context.Context, *Tx) error, opts ...TxOption) error
InTxRetry runs fn in a transaction, retrying when the engine reports a failure that a retry can fix: a serialization failure, a deadlock, or a locked SQLite database.
This is what SERIALIZABLE isolation requires in practice — the database is entitled to abort a transaction that cannot be ordered, and the application is expected to try again. fn must therefore be safe to run more than once.
func Insert ¶
Insert writes one struct as a row.
Every mapped column is written except those tagged readonly, which is how a database-generated column is declared:
type User struct {
ID int64 `db:"id,pk,readonly"`
Name string
CreatedAt time.Time `db:"created_at,readonly"`
}
u := User{Name: "ada"}
err := row.Insert(ctx, db, "users", &u, row.Returning("id", "created_at"))
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
u := User{ID: 4, Name: "edsger", Email: "edsger@example.com"}
if err := row.Insert(ctx, db, "users", &u); err != nil {
log.Fatal(err)
}
// Update writes back using the field tagged pk.
u.Name = "edsger dijkstra"
if err := row.Update(ctx, db, "users", &u, row.Only("name")); err != nil {
log.Fatal(err)
}
name, err := row.One[string](ctx, db, `SELECT name FROM users WHERE id = :id`, row.Args{"id": 4})
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
}
Output: edsger dijkstra
func InsertMany ¶
func InsertMany[T any](ctx context.Context, s Session, table string, vs []T, opts ...WriteOption) error
InsertMany writes a slice of structs in as few statements as possible.
Rows are batched into multi-row VALUES clauses, split so that no single statement exceeds the engine's parameter limit. An empty slice is a no-op.
The batching is not atomic on its own: wrap the call in InTx when all rows must land together.
func IsCode ¶
IsCode reports whether err was classified as the given code. It is the idiomatic way to branch on a constraint violation:
if row.IsCode(err, row.UniqueViolation) { ... }
Example ¶
Constraint violations are classified into portable codes, so branching on one needs no driver-specific error type.
package main
import (
"context"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
dup := User{ID: 99, Name: "impostor", Email: "ada@example.com"}
err := row.Insert(ctx, db, "users", &dup)
fmt.Println(row.IsCode(err, row.UniqueViolation))
fmt.Println(row.CodeOf(err))
}
Output: true unique_violation
func Iter ¶
Iter streams rows instead of collecting them, for result sets too large to hold in memory.
for u, err := range row.Iter[User](ctx, db, q) {
if err != nil {
return err
}
...
}
The underlying rows are closed when the loop ends, including when it breaks early or returns. At most one error is yielded, and it is always the last iteration.
Example ¶
Iter streams rows rather than collecting them, and closes the underlying result even when the loop breaks early.
package main
import (
"context"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
for u, err := range row.Iter[User](ctx, db, `SELECT id, name, email, active FROM users ORDER BY id`) {
if err != nil {
log.Fatal(err)
}
fmt.Println(u.Name)
if u.Name == "grace" {
break
}
}
}
Output: ada grace
func One ¶
One runs a query that must return exactly one row.
It returns ErrNoRows when the query matched nothing, and an error when it matched more than one row — an over-broad WHERE clause is a bug, not something to silently take the first of. Use First when extra rows are expected and unwanted.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
// User is a typical mapped struct. Column names come from the field names by
// default, so only the exceptions need a tag.
type User struct {
ID int64 `db:"id,pk"`
Name string
Email string
Active bool
}
func openExampleDB() *row.DB {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
_, err = row.Exec(ctx, db, `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
active BOOLEAN NOT NULL
)`)
if err != nil {
log.Fatal(err)
}
users := []User{
{1, "ada", "ada@example.com", true},
{2, "grace", "grace@example.com", true},
{3, "alan", "alan@example.com", false},
}
if err := row.InsertMany(ctx, db, "users", users); err != nil {
log.Fatal(err)
}
return db
}
func main() {
ctx := context.Background()
db := openExampleDB()
defer db.Close()
u, err := row.One[User](ctx, db,
`SELECT id, name, email, active FROM users WHERE id = :id`, row.Args{"id": 2})
if err != nil {
log.Fatal(err)
}
fmt.Println(u.Name)
// A query that matches nothing returns ErrNoRows.
_, err = row.One[User](ctx, db,
`SELECT id, name, email, active FROM users WHERE id = :id`, row.Args{"id": 99})
fmt.Println(errors.Is(err, row.ErrNoRows))
}
Output: grace true
func QuoteWith ¶
QuoteWith is a helper for dialects whose quoting rule is "wrap in q and double any occurrence of q inside". It covers "..." and `...` alike.
func SnakeCase ¶
SnakeCase converts a Go field name to the column name row will look for when a field carries no db tag.
It handles acronyms the way Go code actually spells them:
ID -> id UserID -> user_id HTTPCode -> http_code OAuth2Key -> oauth2_key CreatedAt -> created_at
Types ¶
type Args ¶
Args carries named parameters.
row.All[User](ctx, db, `SELECT * FROM users WHERE org = :org`, row.Args{"org": 7})
type Builder ¶
type Builder interface {
// BuildSQL renders the query in d's native form. The returned SQL already
// carries d's placeholders and the arguments are already positional, so
// row passes both straight to the driver.
BuildSQL(d Dialect) (query string, args []any, err error)
}
Builder is a query that can render itself for a specific dialect.
It is the seam between row and a query builder: row declares the interface and never imports a builder, so the two stay independent. The row/qb subpackage implements it, and so can anything else.
type Code ¶
type Code int
Code is a portable classification of a database error. Engines report failures with their own vocabulary — SQLSTATE strings on Postgres, integer result codes on SQLite — and a Code is what those map onto so that calling code can branch without importing a driver.
const ( // Unknown means the error was not recognised by the dialect. It carries no // claim about what went wrong; inspect the wrapped error. Unknown Code = iota // UniqueViolation: a UNIQUE or PRIMARY KEY constraint was violated. UniqueViolation // ForeignKeyViolation: a FOREIGN KEY constraint was violated. ForeignKeyViolation // NotNullViolation: a NOT NULL column was given a NULL. NotNullViolation // CheckViolation: a CHECK constraint failed. CheckViolation // Deadlock: the engine aborted this transaction to break a deadlock. Deadlock // SerializationFailure: the transaction could not be serialised against a // concurrent one and must be retried. SerializationFailure // Busy: the database is locked by another connection (SQLite). Busy // Timeout: a statement or lock wait exceeded its timeout. Timeout )
func CodeOf ¶
CodeOf reports the portable classification of err, or Unknown when err did not come from row or was not recognised by its dialect.
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn is a single database connection reserved from the pool. Use it when a sequence of statements must run on the same connection — session settings, temporary tables, advisory locks.
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a handle to a database. It wraps *sql.DB and is safe for concurrent use by multiple goroutines.
func New ¶
New wraps an existing *sql.DB.
Use it when the pool is configured elsewhere, or to reach a database row has no adapter for. The dialect must match the driver the pool was opened with.
func Open ¶
Open opens a database using the dialect's registered driver name.
It is the low-level form; the adapter packages provide friendlier constructors that also apply engine-appropriate defaults.
func (*DB) Conn ¶
Conn reserves a connection from the pool. The caller must Close it to return the connection.
type Dialect ¶
type Dialect interface {
// Name is a short stable identifier, e.g. "postgres" or "sqlite". It is
// used as part of the statement cache key, so distinct dialects must not
// share a name.
Name() string
// DriverName is the name the engine's driver registers with database/sql.
DriverName() string
// AppendPlaceholder appends the placeholder for the n-th bind parameter
// (1-based) to dst and returns the extended slice. Postgres appends "$n";
// SQLite appends "?".
AppendPlaceholder(dst []byte, n int) []byte
// QuoteIdent quotes a single identifier so it survives reserved words and
// unusual characters.
QuoteIdent(name string) string
// Features reports what the engine can do.
Features() Features
// ClassifyError maps a driver error onto a portable Code. It reports false
// when the error is not one it recognises, in which case the error is left
// unclassified rather than guessed at.
ClassifyError(err error) (Code, bool)
}
Dialect describes the handful of ways SQL engines differ from one another.
row's core is engine-agnostic: everything above this interface is shared by every supported database. Implementations live in subpackages (row/pg, row/sqlite) so that importing row does not drag in a driver.
type Error ¶
type Error struct {
// Op is the row operation that failed, e.g. "row.All" or "row.Insert".
Op string
// SQL is the statement as sent to the driver, after binding. It is empty
// for failures that happen before a statement exists.
SQL string
// Code classifies Err when the dialect recognised it, else Unknown.
Code Code
// Err is the underlying error.
Err error
}
Error is the error type row returns for every database failure. It records what row was doing and the statement it was doing it with, which is the context that raw driver errors leave out.
type Features ¶
type Features struct {
// Returning reports whether INSERT/UPDATE/DELETE ... RETURNING works.
Returning bool
// Savepoints reports whether SAVEPOINT / ROLLBACK TO / RELEASE work,
// which is what nested InTx calls are built on.
Savepoints bool
// Upsert reports whether INSERT ... ON CONFLICT works.
Upsert bool
// MaxPlaceholders is the largest number of bind parameters a single
// statement accepts, or 0 when the engine has no meaningful limit.
// InsertMany chunks its batches to stay under it.
MaxPlaceholders int
}
Features records optional engine capabilities. Zero value means "supports nothing optional", which is the safe default for a new dialect.
type Hook ¶
type Hook interface {
// BeforeQuery runs immediately before the statement is executed. The
// returned context replaces the one used for the call, which lets a hook
// attach a tracing span.
BeforeQuery(ctx context.Context, info *QueryInfo) context.Context
// AfterQuery runs once the statement has completed.
AfterQuery(ctx context.Context, info *QueryInfo)
}
Hook observes statement execution. Implementations must be safe for concurrent use and should not block.
type Option ¶
type Option func(*config)
Option configures a DB.
func Lax ¶
func Lax() Option
Lax stops row from treating an unmatched result column as an error.
By default a column with no corresponding struct field fails the scan, which catches typos and stale queries. Lax is the escape hatch for queries that deliberately select more than they map.
func WithHook ¶
WithHook registers an observer of statement execution. Hooks run in registration order.
func WithNameMapper ¶
WithNameMapper sets the function that derives a column name from a struct field name when the field carries no db tag. The default is SnakeCase.
func WithPlanCacheSize ¶
WithPlanCacheSize sets how many compiled statements to keep. Zero disables caching entirely, which is only useful when generating unbounded distinct SQL.
type QueryInfo ¶
type QueryInfo struct {
// Op is the row operation, e.g. "row.All".
Op string
// SQL is the statement as sent to the driver, after binding.
SQL string
// Args are the positional arguments as sent to the driver.
Args []any
// Started is when the statement was handed to the driver.
Started time.Time
// Duration is how long it took. Zero in BeforeQuery.
Duration time.Duration
// RowsAffected is set for Exec, and is -1 when the driver does not report
// it or the operation was a query.
RowsAffected int64
// Err is the resulting error, nil on success. Always nil in BeforeQuery.
Err error
}
QueryInfo describes one statement execution. It is passed to hooks before and after the statement runs; the fields filled in on the way out are Duration, RowsAffected and Err.
type RetryPolicy ¶
type RetryPolicy struct {
// Attempts is the total number of tries, including the first. Values below
// 1 are treated as 1.
Attempts int
// BaseDelay is the delay before the second attempt. It doubles each time,
// up to MaxDelay.
BaseDelay time.Duration
// MaxDelay caps the backoff. Zero means no cap.
MaxDelay time.Duration
}
RetryPolicy controls InTxRetry.
type Session ¶
type Session interface {
// Dialect reports the engine this session speaks. Query builders use it to
// render placeholders and quote identifiers.
Dialect() Dialect
// contains filtered or unexported methods
}
Session is what One, All, Iter and Exec accept. It is implemented by *DB, *Tx and *Conn.
The interface is sealed: its methods are unexported, so it cannot be implemented outside this package. That is deliberate — it is an internal dispatch mechanism, not an extension point. To fake a database in tests, point row at a real in-memory SQLite or at your own database/sql driver.
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx is a transaction, or — when opened inside another transaction — a savepoint within one.
type TxOption ¶
type TxOption func(*txConfig)
TxOption configures a transaction.
func Isolation ¶
func Isolation(level sql.IsolationLevel) TxOption
Isolation sets the transaction isolation level. Nested transactions inherit the outermost level; a savepoint cannot change it, and passing Isolation to a nested InTx is an error rather than a silently ignored request.
type WriteOption ¶
type WriteOption func(*writeConfig)
WriteOption configures Insert, InsertMany, Update and Delete.
func Omit ¶
func Omit(columns ...string) WriteOption
Omit excludes the named columns from the write.
func Only ¶
func Only(columns ...string) WriteOption
Only restricts the write to the named columns. Everything else is left alone, which is how a partial update is expressed.
func Returning ¶
func Returning(columns ...string) WriteOption
Returning adds a RETURNING clause and scans the result back into the value being written. It is how a database-generated id or timestamp gets back into the struct:
u := User{Name: "ada"}
err := row.Insert(ctx, db, "users", &u, row.Returning("id", "created_at"))
Both Postgres and SQLite support RETURNING.
Example ¶
Returning brings database-generated values back into the struct.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/r-52/row"
"github.com/r-52/row/sqlite"
)
func main() {
ctx := context.Background()
db, err := sqlite.Open(ctx, ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if _, err := row.Exec(ctx, db, `
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`); err != nil {
log.Fatal(err)
}
type Note struct {
// Both columns are filled in by the database, so neither is written.
ID int64 `db:"id,pk,readonly"`
Body string `db:"body"`
CreatedAt time.Time `db:"created_at,readonly"`
}
n := Note{Body: "hello"}
if err := row.Insert(ctx, db, "notes", &n, row.Returning("id", "created_at")); err != nil {
log.Fatal(err)
}
fmt.Println(n.ID, n.CreatedAt.IsZero())
}
Output: 1 false
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
crossengine
command
Command crossengine runs the same code against SQLite and PostgreSQL to show that a row program is portable between them.
|
Command crossengine runs the same code against SQLite and PostgreSQL to show that a row program is portable between them. |
|
internal
|
|
|
dbtest
Package dbtest runs one test body against every database row supports.
|
Package dbtest runs one test body against every database row supports. |
|
Package pg adapts row to PostgreSQL through github.com/jackc/pgx/v5.
|
Package pg adapts row to PostgreSQL through github.com/jackc/pgx/v5. |
|
Package qb builds SQL statements programmatically.
|
Package qb builds SQL statements programmatically. |
|
Package sqlite adapts row to SQLite through modernc.org/sqlite, a pure-Go translation of the SQLite C sources.
|
Package sqlite adapts row to SQLite through modernc.org/sqlite, a pure-Go translation of the SQLite C sources. |