row

package module
v0.0.0-...-1fa2acd Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 18 Imported by: 0

README

row

CI Go Reference MIT

A small, typed layer over database/sql for Go.

row takes care of the mechanical part of talking to a SQL database — turning result rows into Go values, binding parameters, managing transactions — and leaves the SQL to you. It is not an ORM, and it doesn't want to be. You write the query; row handles everything between the query and your structs.

users, err := row.All[User](ctx, db,
    `SELECT id, name FROM users WHERE org_id = :org AND id IN (:ids)`,
    row.Args{"org": 7, "ids": []int64{1, 2, 3}})

That's the whole idea. The rest of this page is the details.

Contents

Install

go get github.com/r-52/row

Requires Go 1.24 or later.

Then import the adapter for your database. The root package imports neither driver, so your binary only includes the one you actually use.

Database Import Driver
PostgreSQL github.com/r-52/row/pg jackc/pgx/v5
SQLite github.com/r-52/row/sqlite modernc.org/sqlite — pure Go, no cgo

Those two drivers are the only third-party dependencies in the project.

Getting started

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/r-52/row"
    "github.com/r-52/row/sqlite"
)

type User struct {
    ID     int64 `db:"id,pk"`
    Name   string
    Email  string
    Active bool
}

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 users (
            id     INTEGER PRIMARY KEY,
            name   TEXT NOT NULL,
            email  TEXT NOT NULL UNIQUE,
            active BOOLEAN NOT NULL
        )`); err != nil {
        log.Fatal(err)
    }

    u := User{ID: 1, Name: "ada", Email: "ada@example.com", Active: true}
    if err := row.Insert(ctx, db, "users", &u); err != nil {
        log.Fatal(err)
    }

    active, err := row.All[User](ctx, db,
        `SELECT id, name, email, active FROM users WHERE active = :active`,
        row.Args{"active": true})
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(active) // [{1 ada ada@example.com true}]
}

That program runs as written. Change ":memory:" to a filename such as "app.db" to keep the data. To talk to PostgreSQL instead, import github.com/r-52/row/pg and call pg.Open(ctx, dsn) — everything below that line stays exactly the same.

Reading rows

Four functions cover reading. Each takes the destination as a type parameter, so the compiler checks it for you.

one,   err := row.One[User](ctx, db, q, args)    // exactly one row
first, err := row.First[User](ctx, db, q, args)  // the first row, ignore the rest
users, err := row.All[User](ctx, db, q, args)    // every row
total, err := row.One[int64](ctx, db, `SELECT count(*) FROM users`)

for u, err := range row.Iter[User](ctx, db, q) { // stream, don't collect
    if err != nil {
        return err
    }
    ...
}

The destination type can be:

  • a struct or a *struct
  • a scalar — int, string, time.Time, []byte, sql.Null[T], or anything implementing sql.Scanner
  • map[string]any, when the columns aren't known ahead of time

One returns an error if the query matched more than one row. That's usually a sign the WHERE clause is broader than intended, and it seemed better to say so than to quietly take the first result. When extra rows are expected and you want the first anyway, use First.

Iter returns an iter.Seq2[T, error] and closes the underlying result when the loop ends — including when you break out early.

Two helpful strictnesses

Unmatched columns are an error. If a query selects a column no struct field maps to, row says so, and lists both the columns it couldn't place and the fields that were available:

row.All: no field in main.User for result column "emial"
  main.User has columns: active, email, id, name
  fix the query, add a db tag, or pass row.Lax() to ignore extra columns
  sql: SELECT id, name, email, active, id AS emial FROM users

This catches a renamed column or a typo at the first query rather than as a mysteriously empty field later. If you deliberately select more than you map, pass row.Lax() when opening the database.

NULL into a non-nullable Go type is an error. A NULL arriving at a plain string field would otherwise become "", which is indistinguishable from a real empty string. row tells you to use a *string or sql.Null[string] instead.

Parameters

There are three ways to write placeholders. row works out which one a statement uses; mixing two in one statement is an error.

// Named — the recommended default.
row.All[User](ctx, db, `... WHERE id = :id`, row.Args{"id": 7})

// Named, filled from a struct's fields.
row.All[User](ctx, db, `... WHERE id = :id AND org_id = :org_id`, someUser)

// Ordinal.
row.All[User](ctx, db, `... WHERE id = ?`, 7)

// Your database's native placeholders, passed through untouched.
row.All[User](ctx, db, `... WHERE id = $1`, 7)
Slices expand automatically

A slice argument becomes a placeholder list, so IN needs no special handling:

row.All[User](ctx, db, `SELECT * FROM users WHERE id IN (:ids)`,
    row.Args{"ids": []int64{1, 2, 3}})
// SELECT * FROM users WHERE id IN ($1,$2,$3)

An empty slice becomes IN (NULL), which matches nothing — the right answer for membership in an empty set, and valid SQL either way.

[]byte, string, and any type implementing driver.Valuer (PostgreSQL array types, for instance) are treated as single values and passed through whole.

Your SQL is read properly

row parses each statement with a real SQL scanner rather than searching for punctuation, so parameters are found and everything else is left alone:

  • 'strings' with '' escapes, and PostgreSQL E'\'' escape strings
  • $tag$ dollar-quoted bodies $tag$, including nested lookalike tags
  • "quoted" and `quoted` identifiers
  • -- line comments and /* nested /* block */ comments */
  • ::casts, which are not parameters
  • PostgreSQL's ?, ?| and ?& JSON operators, which are not placeholders

That last point is worth spelling out. In a statement that uses named parameters, a bare ? is always the JSON existence operator, never a placeholder — so this works as written:

row.All[Doc](ctx, db, `SELECT * FROM docs WHERE data ? 'key' AND org = :org`,
    row.Args{"org": 7})

Write ?? anywhere you want a literal question mark.

Parsed statements are cached, so a statement is scanned once however many times you run it.

Mapping structs

Column names come from field names by default, converted to snake case (UserIDuser_id, HTTPCodehttp_code). Add a db tag for the exceptions.

type User struct {
    ID        int64     `db:"id,pk"`               // primary key, used by Update and Delete
    Name      string                               // -> name
    CreatedAt time.Time `db:"created_at,readonly"` // read, never written
    Secret    string    `db:"-"`                   // ignored entirely
    Address   *Address                             // -> address_city, address_zip
    Audit     Audit     `db:",inline"`             // merged in, no prefix
    Home      Address   `db:",prefix=home_"`       // -> home_city, home_zip
}

Tag options: pk, readonly, inline, prefix=, and - to skip.

Embedded structs merge into the parent's namespace. Named struct fields get a prefix derived from their column name, which lines up with what SELECT a.city AS address_city produces. If two fields end up claiming the same column, row reports it and names both — no silent first-wins.

You can change the naming rule per database with row.WithNameMapper(myFunc). It's a per-DB setting, so two databases in the same process can use different conventions.

Joined structs can be nil

A *Struct field is left nil when every column belonging to it came back NULL:

type Member struct {
    ID   int64 `db:"id,pk"`
    Name string
    Org  *Org  // nil when the LEFT JOIN found no match
}

members, _ := 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`)

A zeroed Org{ID: 0, Name: ""} looks exactly like a real row that happens to have those values. nil doesn't, which is why it's worth the trouble.

Writing rows

err := row.Insert(ctx, db, "users", &u)
err = row.Insert(ctx, db, "users", &u, row.Returning("id", "created_at"))
err = row.InsertMany(ctx, db, "users", users) // batched automatically
err = row.Update(ctx, db, "users", &u)        // matches on the pk field
err = row.Update(ctx, db, "users", &u, row.Only("name", "email"))
err = row.Delete(ctx, db, "users", &u)

Returning scans the result back into your struct, which is how a database-generated id or timestamp finds its way home. Both PostgreSQL and SQLite support it. Mark generated columns readonly so they're never written:

type Note struct {
    ID        int64     `db:"id,pk,readonly"`
    Body      string
    CreatedAt time.Time `db:"created_at,readonly"`
}

n := Note{Body: "hello"}
err := row.Insert(ctx, db, "notes", &n, row.Returning("id", "created_at"))
// n.ID and n.CreatedAt are now filled in

InsertMany groups rows into multi-row VALUES clauses and splits them so no single statement exceeds the engine's parameter limit. It isn't atomic by itself — wrap it in InTx when all the rows need to land together.

Use Only and Omit to narrow which columns a write touches.

Transactions

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.Insert(ctx, tx, "audit", &a)
})

Commits when the function returns nil. Rolls back and returns your error unchanged when it doesn't. Rolls back and re-panics if it panics.

Nesting works

Calling InTx with a *Tx opens a SAVEPOINT instead of a new transaction, so an inner failure undoes only the inner work and the outer transaction carries on:

row.InTx(ctx, db, func(ctx context.Context, tx *row.Tx) error {
    if err := row.Insert(ctx, tx, "users", &u); err != nil {
        return err
    }
    // This block gets its own savepoint. If it fails, only it is undone.
    if err := row.InTx(ctx, tx, recordOptionalThing); err != nil {
        log.Printf("optional step failed: %v", err)
    }
    return nil
})

This means a function that needs a transaction can just ask for one, without knowing whether its caller already started one.

Retrying
err := row.InTxRetry(ctx, db, row.DefaultRetryPolicy, func(ctx context.Context, tx *row.Tx) error {
    ...
})

Retries with jittered exponential backoff, but only for failures a retry can actually fix — serialization failures, deadlocks, and a busy SQLite. Anything else is returned immediately, because retrying a unique-key violation just fails again. Under SERIALIZABLE isolation this is what the database expects you to do.

Errors

Constraint violations are classified into portable codes, so you can branch on one without importing a driver:

if err := row.Insert(ctx, db, "users", &u); err != nil {
    if row.IsCode(err, row.UniqueViolation) {
        return ErrEmailTaken
    }
    return err
}

The codes are UniqueViolation, ForeignKeyViolation, NotNullViolation, CheckViolation, Deadlock, SerializationFailure, Busy and Timeout. Each one is verified against a live server for both databases in the test suite, rather than transcribed from documentation.

An error row doesn't recognise stays Unknown — it never guesses.

row.ErrNoRows is sql.ErrNoRows, so errors.Is matches either spelling and existing code keeps working.

Error messages carry the operation and the statement that produced them:

row.Insert [unique_violation]: ERROR: duplicate key value violates unique constraint "users_email_key" (SQLSTATE 23505)
  sql: INSERT INTO "users" ("id", "name", "email", "active") VALUES ($1, $2, $3, $4)

Every error names the operation, the portable code where there is one, and the statement as it was actually sent.

Query builder

Most queries read better written out as SQL. The row/qb subpackage is for the ones whose shape changes with the request — a search form where each filter may or may not apply:

q := qb.Select("id", "name").From("users").Where(qb.Eq{"org_id": orgID})

if search != "" {
    q = q.Where(qb.ILike{"name": "%" + search + "%"})
}
if onlyActive {
    q = q.Where(qb.Eq{"active": true})
}

users, err := row.AllOf[User](ctx, db, q.OrderBy("created_at DESC").Limit(50))

Builders are immutable — every method returns a new value — so a partly-built query is safe to keep as a template and branch from.

Conditions include Eq, NotEq, Lt, Lte, Gt, Gte, Like, ILike, In, NotIn, IsNull, NotNull, Between, And, Or and Not. In an Eq, a nil becomes IS NULL and a slice becomes IN (...), because that's what you meant.

Every value becomes a bind parameter. qb.Raw is the escape hatch for anything the builder can't express, and is the one place text is passed through as-is:

qb.Raw("data @> ?::jsonb", filter)

UPDATE and DELETE without a WHERE clause are refused, since that's rarely what anyone meant to write. Say .Where(qb.Raw("1 = 1")) when it is.

Logging and tracing

db, err := pg.Open(ctx, dsn,
    row.WithHook(row.SlogHook(logger, slog.LevelDebug, 100*time.Millisecond)))

Statements slower than the threshold log at warn level, failures at error.

For tracing, implement row.Hook yourself. The context you return from BeforeQuery is the one used for the call, so a span opened there is available again in AfterQuery.

Performance

Compared with writing the scan loop out by hand (SQLite, in memory, Apple M3):

BenchmarkScan/row/1000-8            1855833 ns/op   685146 B/op   9794 allocs/op
BenchmarkScan/handwritten/1000-8    1382845 ns/op   556326 B/op   8787 allocs/op

Roughly 34% more time and 11% more allocations — about half a microsecond per row. That's the cost of reflection-driven mapping.

Worth knowing where that actually goes, though. Profiling the benchmark above attributes about three quarters of the allocations to the SQLite driver itself, and row's own share comes to exactly two per row: one to allocate the struct, one to hand it back as a T. So the number above says as much about the driver as it does about row, and it will look different on PostgreSQL.

If the overhead matters in a particular hot path, write those Scan calls by hand. db.SQL() returns the underlying *sql.DB, and row never hides it from you.

How it works

Worth reading before your first change. Each file has one job:

File Job
row.go DB, Conn, the Session interface, options
dialect.go the Dialect interface — the only thing databases differ by
lex.go the SQL scanner
bind.go compiling statements and binding arguments
cache.go the compiled-statement cache
mapper.go struct ⇄ column mapping, and the reflection cache
convert.go assigning driver values to Go fields
scan.go One, All, First, Iter
write.go Insert, Update, Delete, InsertMany
tx.go transactions, savepoints, retries
errors.go error types and portable codes
hook.go the observability interface
builder.go the Builder seam and the *Of executors
qb/ the query builder
pg/, sqlite/ the two dialect adapters

Two design notes that explain most of the structure:

Everything sits on database/sql. Connection pooling, transactions and prepared statements come from the standard library. A Dialect supplies only what genuinely differs between engines: placeholder syntax, identifier quoting, a feature list, and how to read the engine's error codes. That's why there is one code path rather than one per database.

Value conversion is ours rather than the standard library's, because row needs to see when every column of a nested struct came back NULL. That's what makes nil-able joined structs possible, and it's also where scan errors get the detail that makes them useful.

Contributing

Contributions are welcome, and small ones are just as welcome as large ones. CONTRIBUTING.md has the details; the short version:

git clone https://github.com/r-52/row
cd row
make test        # unit tests and SQLite integration — nothing else needed

make pg-up       # start PostgreSQL 17 in Docker
make test-pg     # run everything against both databases
make pg-down

Tests that touch real SQL go in integration/, where dbtest.Each runs them against both databases. That's deliberate: something that works on SQLite but not PostgreSQL is a bug in row, and it should surface as a red test rather than as a surprise later.

Adding another database is a nicely self-contained first project — a dialect is one type with six methods, and registering it in internal/dbtest runs the entire existing suite against it. The guide walks through it.

A failing test case is the most useful bug report there is, and usually most of the fix.

Non-goals

row maps rows. It deliberately doesn't do migrations, schema reflection, relation loading, caching, or code generation. There are good focused tools for each of those, and they compose better than one library trying to do everything.

License

MIT. See LICENSE.

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

Examples

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func Affected(ctx context.Context, s Session, query string, args ...any) (int64, error)

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

func All[T any](ctx context.Context, s Session, query string, args ...any) ([]T, error)

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

func AllOf[T any](ctx context.Context, s Session, b Builder) ([]T, error)

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

func AppendOrdinal(dst []byte, prefix byte, n int) []byte

AppendOrdinal appends prefix followed by the decimal form of n. Postgres dialects use it to emit "$1", "$2" and so on without allocating.

func Delete

func Delete[T any](ctx context.Context, s Session, table string, v *T, opts ...WriteOption) error

Delete removes the row matching the struct's primary key.

func Exec

func Exec(ctx context.Context, s Session, query string, args ...any) (sql.Result, error)

Exec runs a statement that returns no rows and reports the result.

func ExecOf

func ExecOf(ctx context.Context, s Session, b Builder) (sql.Result, error)

ExecOf runs a built statement that returns no rows.

func ExpandSlice

func ExpandSlice(v any) ([]any, bool)

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

func First[T any](ctx context.Context, s Session, query string, args ...any) (T, error)

First runs a query and returns its first row, ignoring any others. It still returns ErrNoRows for an empty result.

func FirstOf

func FirstOf[T any](ctx context.Context, s Session, b Builder) (T, error)

FirstOf runs a built query and returns its first row, ignoring any others.

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

func Insert[T any](ctx context.Context, s Session, table string, v *T, opts ...WriteOption) error

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

func IsCode(err error, c Code) bool

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

func Iter[T any](ctx context.Context, s Session, query string, args ...any) iter.Seq2[T, error]

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 IterOf

func IterOf[T any](ctx context.Context, s Session, b Builder) iter.Seq2[T, error]

IterOf streams the rows of a built query.

func One

func One[T any](ctx context.Context, s Session, query string, args ...any) (T, error)

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 OneOf

func OneOf[T any](ctx context.Context, s Session, b Builder) (T, error)

OneOf runs a built query that must return exactly one row.

func QuoteWith

func QuoteWith(name string, q byte) string

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

func SnakeCase(name string) string

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

func Update

func Update[T any](ctx context.Context, s Session, table string, v *T, opts ...WriteOption) error

Update writes a struct back to its row, matching on the fields tagged pk.

Primary keys and readonly columns are never in the SET clause. Use Only for a partial update:

err := row.Update(ctx, db, "users", &u, row.Only("name", "email"))

Types

type Args

type Args map[string]any

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

func CodeOf(err error) Code

CodeOf reports the portable classification of err, or Unknown when err did not come from row or was not recognised by its dialect.

func (Code) Retryable

func (c Code) Retryable() bool

Retryable reports whether a failure with this code is worth retrying with the same input. InTxRetry uses it to decide.

func (Code) String

func (c Code) String() string

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.

func (*Conn) Close

func (c *Conn) Close() error

Close returns the connection to the pool.

func (*Conn) Dialect

func (c *Conn) Dialect() Dialect

Dialect returns the dialect this connection speaks.

func (*Conn) SQL

func (c *Conn) SQL() *sql.Conn

SQL returns the underlying *sql.Conn.

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

func New(db *sql.DB, d Dialect, opts ...Option) *DB

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

func Open(d Dialect, dsn string, opts ...Option) (*DB, error)

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

func (db *DB) Close() error

Close closes the underlying pool.

func (*DB) Conn

func (db *DB) Conn(ctx context.Context) (*Conn, error)

Conn reserves a connection from the pool. The caller must Close it to return the connection.

func (*DB) Dialect

func (db *DB) Dialect() Dialect

Dialect returns the dialect this handle speaks.

func (*DB) Ping

func (db *DB) Ping(ctx context.Context) error

Ping verifies the database is reachable.

func (*DB) SQL

func (db *DB) SQL() *sql.DB

SQL returns the underlying *sql.DB, for pool tuning and for the occasional operation row does not cover. row never hides it.

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.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is lets errors.Is(err, row.ErrNoRows) and friends see through the wrapper even though Unwrap already handles the common case; it additionally allows matching on a bare Code via a sentinel-free comparison.

func (*Error) Unwrap

func (e *Error) Unwrap() error

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.

func SlogHook

func SlogHook(log *slog.Logger, level slog.Level, slow time.Duration) Hook

SlogHook returns a Hook that logs each statement to log at the given level.

Statements taking longer than slow are logged at warn level instead; pass a zero slow to disable that promotion.

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

func WithHook(h Hook) Option

WithHook registers an observer of statement execution. Hooks run in registration order.

func WithNameMapper

func WithNameMapper(f func(string) string) Option

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

func WithPlanCacheSize(n int) Option

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.

func (*Tx) Depth

func (t *Tx) Depth() int

Depth reports how deeply nested this transaction is; 0 is the outermost.

func (*Tx) Dialect

func (t *Tx) Dialect() Dialect

Dialect returns the dialect this transaction speaks.

func (*Tx) SQL

func (t *Tx) SQL() *sql.Tx

SQL returns the underlying *sql.Tx.

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.

func ReadOnly

func ReadOnly() TxOption

ReadOnly marks the transaction read-only, which lets the engine take cheaper locks and catches accidental writes.

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

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.

Jump to

Keyboard shortcuts

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