rio

package module
v0.18.1 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 23 Imported by: 0

README

rio

rio gopher logo

Go Reference Go Release Test License

A generic ORM for Go with zero third-party dependencies in the core. Queries are immutable values, every write is an explicit call, and relations load only when asked.

users, err := rio.From[User]().
    Where("age >= ?", 18).
    OrderBy("created_at DESC").
    With("Posts", rio.RelWhere("published = ?", true)).
    All(ctx, db)

Getting started

Requires Go 1.27+. Install the core and one driver module:

go get github.com/go-rio/rio
go get github.com/go-rio/sqlite # or postgres, mysql, clickhouse
Module Driver
go-rio/postgres pgx (database/sql or native)
go-rio/mysql go-sql-driver/mysql
go-rio/sqlite modernc.org/sqlite, pure Go
go-rio/clickhouse native protocol, zero deps
package main

import (
    "context"
    "log"

    "github.com/go-rio/rio"
    "github.com/go-rio/sqlite"
)

type User struct {
    ID    int64
    Email string
    Age   int
}

func main() {
    ctx := context.Background()
    db, err := sqlite.Open("file:app.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    user := User{Email: "alice@example.com", Age: 30}
    if err := rio.Insert(ctx, db, &user); err != nil {
        log.Fatal(err)
    }

    loaded, err := rio.Find[User](ctx, db, user.ID)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("loaded %s", loaded.Email)
}

Schema migrations live in go-rio/migrate. Compile-only examples of the main entry points are in example_test.go.

Features

Principles
  • Immutable queries. Query[T] is a value: build once, validate, reuse concurrently, run against any DB or transaction.
  • Explicit everything. No lazy loading, no dirty tracking, no hidden transactions, no callbacks. What you call is what runs.
  • Typed relations. HasMany[T] and friends load in batched IN queries when you ask, and panic with guidance when you forgot to.
  • Fast paths where the driver has them. The PostgreSQL module runs on pgx natively: preloads share one round trip per layer and bulk inserts stream over COPY.

DESIGN.md records the architecture, the operation-semantics table, and what rio deliberately leaves out.

API surface
Area API
Query construction From[T], Where, Having, Join, OrderBy, GroupBy, Distinct, Limit, Offset, Scope
Query modifiers ForUpdate, ForShare, Final, WithTrashed, OnlyTrashed, AllRows
Query execution All, First, Sole, Find, Rows, Chunk, Count, Exists, Pluck[V], Sum/Min/Max/Avg[V], SQL
Cursor pagination OrderKeys, After, Before, CursorAt, Cursor.String, ParseCursor
Direct lookup and SQL Find[T], Raw[T], Exec, Query.Sub
Entity writes Insert, Update, Delete, ForceDelete, Restore, Upsert, FirstOrCreate, CreateOrFirst
Batch and set writes InsertAll, UpsertAll, UpdateAll, UpdateAllReturning, DeleteAll, DeleteAllReturning, ForceDeleteAll, RestoreAll
Relations With, WithCount, WhereHas, WhereHasNot, Attach, Detach, SyncRelation, ClearRelation
Validation and reuse Query.Validate, Query.Must, WithStmtCache, WithoutStmtCache
Handles and options New, NewNative, DB.Tx, DB.TxWith, Tx.Tx, WithQueryHook, WithoutArgs, WithClock, WithErrorTranslator, WithTableNamer, WithDriverHandle, DB.DescribeModel, DB.Dialect, WriteColumns
Queries

Query[T] carries no connection. Terminal methods take the context, a Queryer (*DB or *Tx), and any deferred arguments:

var adults = rio.From[User]().
    Where("age >= ?").
    OrderBy("created_at DESC").
    Limit(10).
    Must()

users, err := adults.All(ctx, db, 18)
emails, err := adults.Pluck[string](ctx, db, "email", 18)
total, err := adults.Sum[int64](ctx, db, "age", 18)
user, err := adults.With("Posts").Find(ctx, db, 42) // by primary key, under the query's clauses

Validate returns structural errors; Must panics instead and returns the query, for package variables. Neither touches the database. SQL(db, args...) renders the statement All would run, with its bound arguments, without executing it.

First adds LIMIT 1 only when no limit is set and never adds an order; Sole returns ErrMultipleRows past one row. Query.Find is the keyed First: WithTrashed, With, WithCount, and inline Where all apply, and composite key parts follow field declaration order. Limit and Offset render into the SQL, not as parameters.

Sum, Min, Max, and Avg aggregate a mapped column and return the zero value over no rows (sql.Null[V] tells the two apart). Distinct applies to entity rows, Pluck, Count (distinct primary keys), Sum, and Avg. Count, Pluck, and the aggregates refuse GroupBy/Having — projections go through Raw; Count and the aggregates also refuse Limit/Offset, while Exists honors them.

Parameters per fragment:

  • Where("age >= ?", 18) — inline, owns its arguments.
  • Where("age >= ?") — deferred, consumes terminal arguments in SQL order.
  • Where("active") — no placeholders.

Slices expand inside IN (?); an empty slice is an error. A one-column query embeds the same way: Where("id IN (?)", banned.Sub("user_id")) splices the subquery and its arguments in place — the caller writes the parentheses, and the subquery's own Where arguments must be inline. Missing or excess arguments fail before the driver sees the query. ?? emits a literal ? where the rendered SQL can carry one (PostgreSQL, ClickHouse); on MySQL and SQLite the rendered ? is the bind marker. Join/OrderBy/GroupBy take no placeholders, and RelWhere arguments are always inline.

Must caches stable scalar shapes per handle; slices, subqueries, and cursors bypass the cache. Rows streams without materializing the slice; it rejects With/WithCount and Before. WithStmtCache caches prepared statements per DB and per transaction (default 512 entries); the sqlite and mysql modules turn it on by default, and WithoutStmtCache opts out behind transaction- or statement-mode poolers. New panics on WithStmtCache with ClickHouse, which cannot prepare general queries.

Cursor pagination

OrderKeys declares ordering over mapped NOT NULL scalar columns, so rio can read key values back out of a row and issue a keyset cursor. A missing primary-key column is appended as tie-breaker — pages never skip or repeat. OrderKeys cannot mix with verbatim OrderBy:

q := rio.From[Post]().OrderKeys(
    rio.SortKey{Column: "score", Desc: true},
    rio.SortKey{Column: "created_at"},
) // + "id" appended automatically

page, err := q.Limit(20).All(ctx, db)
cur, err := q.CursorAt(&page[len(page)-1])
next, err := q.After(cur).Limit(20).All(ctx, db)
prev, err := q.Before(first).Limit(20).All(ctx, db) // first = CursorAt(&page[0])

Before runs the reversed query and turns the page around, so it always reads in OrderKeys order; After and Before cannot combine. Chunk(ctx, db, 500) walks the whole result in keyset pages (iter.Seq2[[]T, error]), releasing the connection between pages and applying With/WithCount per page; it follows OrderKeys, defaulting to the primary key, refuses Limit, Offset, After, and Before, and stops at the first short page.

Cursor.String/rio.ParseCursor round-trip a URL-safe token. Tokens carry values (bound as parameters) plus an ordering fingerprint — a forged token moves the window, never the query, and a cursor from different OrderKeys fails loudly. The zero Cursor is rejected; omit After for the first page.

Schema-drift lint

The read-only lint subpackage diffs model expectations against the live schema (PostgreSQL, MySQL, SQLite): missing tables and columns, nullability, primary keys, and type mismatches in known equivalence classes. Run it in CI or a startup probe:

report, err := lint.Check(ctx, db, User{}, Post{})
for _, f := range report.Findings {
    log.Printf("%s: %s", f.Severity, f.Message)
}
Transactions and row locks

*DB and *Tx both implement Queryer, so repository code runs unchanged in or out of a transaction:

err := db.Tx(ctx, func(tx *rio.Tx) error {
    users, err := adults.ForUpdate().All(ctx, tx, 21)
    if err != nil {
        return err
    }
    return updateUsers(ctx, tx, users)
})

Error rolls back, nil commits, a panic rolls back and re-panics, Tx on a *Tx opens a savepoint, and TxWith takes *sql.TxOptions (isolation level, read-only). Batch operations never start hidden transactions — wrap them in DB.Tx when all chunks must land together.

ForUpdate and ForShare take rio.NoWait or rio.SkipLocked; the queue-worker idiom is Where("state = ?", "queued").ForUpdate(rio.SkipLocked).Limit(1). SQLite elides row locks, ClickHouse rejects them.

Models and relations
Declaration Meaning
ID int64 conventional auto-increment primary key
rio:"column" column name; default is snake_case
rio:",pk" explicit primary key; repeat for composite
rio:",noautoincr" integer key without auto-generation
rio:",version" optimistic locking; conflicts return ErrStaleObject
rio:",softdelete" deletion timestamp driving soft-delete operations
rio:",json" encode and scan as JSON
rio:",omitzero" skip zero value on single-row insert so defaults apply
rio:",readonly" database-computed column: scanned and loaded back after Insert/Upsert, never written
rio:",countof:Posts" int64 target for WithCount("Posts")
rio:",nostamp" opt out of CreatedAt/UpdatedAt maintenance
rio:"-" ignored field
TableName() string override the pluralized table name

Table names are the snake_case plural of the struct name (Userusers, APIKeyapi_keys); rio.TableName exposes the derivation and WithTableNamer overrides it per handle. Embedded structs flatten by value; pointer embedding is rejected.

Relations are HasMany[T], HasOne[T], BelongsTo[T], ManyToMany[T]; fk:/ref:/join: tags override conventions. Relation APIs take Go field names (With("Posts.Comments")), column APIs take column names. Preloads run as separate key-set queries (one array parameter on PostgreSQL, an IN list elsewhere), never JOINs, and never lazily: Rows/Row on an unloaded container panic naming the With argument to add, Loaded reports the state, Set assembles one by hand, and JSON encodes an unloaded relation as null.

With takes RelWhere, RelOrderBy, RelLimit (per parent; needs window functions: PostgreSQL, MySQL 8+, SQLite 3.25+), and RelWithTrashed; options apply to the leaf of a dotted path. WithCount fills the countof target in one GROUP BY query and takes RelWhere and RelWithTrashed to count a subset; a filtered count never reuses a preload. WhereHas/WhereHasNot keep rows whose relation path has (no) matching row, through nested EXISTS.

Attach, Detach, SyncRelation, and ClearRelation write the join table of a ManyToMany relation and never upsert related entities: Attach is idempotent, Detach needs ids, SyncRelation makes the relation match its ids exactly inside a transaction, and ClearRelation unlinks every row.

Writes and errors

Insert backfills generated columns where the dialect can and stamps CreatedAt/UpdatedAt and a zero version before execution. Update writes every eligible field — zero values included — unless given a column whitelist, and checks the version column. db.WithoutStamps() and tx.WithoutStamps() stop generating both timestamps: a statement that writes the caller's row binds the struct's values as they are, and one rio composes itself (a column-list Update, UpdateAll, Delete, Restore) drops the UpdatedAt assignment rather than inventing a value. Delete becomes an UPDATE of the softdelete stamp on soft-delete models, ForceDelete deletes, Restore clears the stamp; queries hide trashed rows unless WithTrashed or OnlyTrashed. FirstOrCreate/CreateOrFirst re-read after ErrDuplicateKey.

Set-based writes require a condition (AllRows() opts out) and refuse Limit/Offset, GroupBy/Having, Join, ordering, preloads, and row locks — select the target rows in Where. UpdateAllReturning and DeleteAllReturning hand the affected rows back where the dialect has RETURNING (a soft delete returns the trashed state); MySQL rejects them.

Upsert supports conflict targets (OnConflict), update whitelists (DoUpdate), DoUpdateSet for expressions (rio.Expr("hits + excluded.hits")) or bound values, DoNothing, and KeepTrashed. In DoUpdateSet the incoming row is excluded on PostgreSQL and SQLite and _rio_new on MySQL; rio-maintained and readonly columns, and columns also named in DoUpdate, are rejected, and DoNothing cannot combine with either. A successful upsert leaves the row visible unless KeepTrashed.

InsertAll/UpsertAll chunk at the dialect bind limit; batch writes share one column list, so omitzero doesn't apply, and a batch mixing zero and explicit generated keys is refused. InsertAll backfills keys only where ordering is reliable (PostgreSQL by position, SQLite sorted by key, MySQL never); UpsertAll never backfills.

Condition Result
First/Find/Sole miss ErrNotFound (wraps sql.ErrNoRows)
All finds nothing empty slice, nil error
Sole finds several ErrMultipleRows
optimistic-lock conflict ErrStaleObject
set write without condition ErrMissingWhere
keyed operation on a model without a primary key ErrNoPrimaryKey
unique / FK violation ErrDuplicateKey / ErrForeignKeyViolated, driver error retained
operation the dialect cannot honor error matching errors.ErrUnsupported
NULL into non-nullable field error naming the column

Times are normalized to UTC, microsecond precision, and written back to the struct as they bind.

Dialect differences
Dialect Behavior
PostgreSQL RETURNING everywhere, including InsertAll backfill; row locks with NoWait/SkipLocked; preload key sets bind as one array (= ANY). The driver module adds a pgx-native channel: batched preload round trips and COPY-backed bulk inserts.
MySQL Insert backfills via LastInsertId; batch inserts don't backfill; no RETURNING. DoUpdate needs MySQL 8.0.19+ (no MariaDB); DoNothing works everywhere. Statement cache on by default.
SQLite Pure-Go driver. RETURNING where backfill needs it; row locks are no-ops. Statement cache, UTC time binding, and BEGIN IMMEDIATE on by default.
ClickHouse Reads, preloads, Insert, InsertAll. Rejects row locks, transactions, statement caching, synchronous update/delete, and conflict upserts — use ReplacingMergeTree + Final. No backfill; supply IDs yourself.
Handles and options

rio.New(*sql.DB, dialect, opts...) wraps a pool you configure; rio never tunes it. Dialects are the opaque built-in values rio.Postgres, rio.MySQL, rio.SQLite, and rio.ClickHouse; driver modules pick one, never implement one. Unwrap returns the *sql.DB, Dialect the dialect value, DescribeModel the resolved table and column mapping, and Close closes the statement cache and the pool.

Option Effect
WithQueryHook(h) observe statements (see Observability)
WithoutArgs() strip bind arguments from hook events
WithClock(fn) time source for stamps and soft deletes, for tests
WithErrorTranslator(fn) map driver errors to sentinels; driver modules install one
WithTableNamer(fn) rename tables per handle; must be pure and stable, since SQL caches per handle
WithDriverHandle(h) attach a driver-owned handle, read back by DB.DriverHandle
WithStmtCache(cap) / WithoutStmtCache() prepared-statement caches (see Queries)

NewNative builds a handle on a driver-native channel (NativeDB, NativeTx, NativeRows, with optional NativeBatcher, NativeCopier, and NativeLastInserter capabilities discovered by type assertion). It is driver-module SPI; applications construct through the driver module (postgres.OpenNative, sqlite.Open, clickhouse.Open).

Security

Values always bind as parameters. SQL fragments do not:

Input APIs Rule
Mapped columns Update columns, Set keys, Pluck, aggregates, Sub, OrderKeys, DoUpdate, DoUpdateSet validated against the model, quoted as identifiers
Conflict targets OnConflict quoted as identifiers
Relation paths With, WithCount, WhereHas, relation writes validated against the model's relations
SQL text Where, Having, Join, OrderBy, GroupBy, RelWhere, RelOrderBy, Expr, Raw, Exec rendered verbatim — constants only, never untrusted input

For runtime-selected identifiers, map external values onto generated constants: rio.WriteColumns(os.Stdout, "models", User{}, Post{}) emits table and column constants from the model mapping.

Observability

WithQueryHook observes every statement: operation, model, SQL, arguments, duration, row counts, error. Hooks cannot alter SQL. The context returned by BeforeQuery flows through the driver into AfterQuery, so tracing spans and deadlines propagate. WithoutArgs strips arguments from events.

QueryEvent.Op is a stable label (select, insert, update, delete, upsert, copy, raw, exec, begin, commit, rollback, savepoint); Phase marks statements rio derives itself (preload, count, probe). For row-returning queries AfterQuery fires once the rows are consumed, and a First/Find/Sole miss reports Err = nil.

Contributing

Read CONTRIBUTING.md for setup, the test suites, and the commit and comment conventions. The short version:

go test ./...
go test -race ./...
go vet ./...

Contributors

Thanks to everyone who has contributed.

Contributors

License

rio is released under the MIT License, © 2026-now TreeNewBee.

The rio gopher logo is inspired by the Go gopher, created by Renée French and licensed under CC BY 4.0.

Documentation

Overview

Package rio is a type-safe ORM built around immutable, connection-free query values. A query touches the database only when a terminal method receives a context and Queryer:

users, err := rio.From[User]().
	Where("age > ?", 18).
	OrderBy("created_at DESC").
	Limit(10).
	With("Posts").
	All(ctx, db)

Queryer is implemented by DB and Tx, so the same query can run inside or outside a transaction. Use ? placeholders for every dialect; slice arguments in IN (?) expressions are expanded at execution time, and a Query.Sub argument splices a subquery in place.

Models are ordinary structs. The rio tag configures column names, primary keys, optimistic locking, soft deletion, JSON, timestamp maintenance, omitted zero values, count targets, and relations. By convention ID is the primary key, CreatedAt and UpdatedAt are maintained timestamps, and a TableName method overrides the pluralized table name.

Relations load only when requested. Raw and Exec provide SQL escape hatches; Query.Validate and Query.Must validate reusable query templates; and WithStmtCache enables prepared-statement reuse. Sentinel errors support errors.Is while preserving translated driver errors in the chain.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound reports that no row matched. It wraps sql.ErrNoRows.
	ErrNotFound = fmt.Errorf("rio: record not found (%w)", sql.ErrNoRows)

	// ErrMultipleRows is returned by Sole when more than one row matches.
	ErrMultipleRows = errors.New("rio: expected exactly one row, found more")

	// ErrStaleObject reports an optimistic-lock conflict or deleted row.
	ErrStaleObject = errors.New("rio: stale object: version conflict or row deleted")

	// ErrMissingWhere reports a set-based write without conditions or AllRows.
	ErrMissingWhere = errors.New("rio: UpdateAll/DeleteAll without conditions; call AllRows() to affect the whole table")

	// ErrDuplicateKey reports a translated unique-constraint violation.
	ErrDuplicateKey = errors.New("rio: duplicate key violates unique constraint")

	// ErrForeignKeyViolated reports a foreign key constraint violation.
	ErrForeignKeyViolated = errors.New("rio: foreign key constraint violated")

	// ErrNoPrimaryKey reports an operation that requires a model primary key.
	ErrNoPrimaryKey = errors.New("rio: model has no primary key")
)

Functions

func Attach added in v0.2.0

func Attach[T any, K any](ctx context.Context, db Queryer, row *T, relation string, ids ...K) error

Attach links rows to a ManyToMany relation by inserting join-table rows. It is idempotent — existing links are left alone (ON CONFLICT DO NOTHING; a no-op assignment on MySQL), assuming the join table's standard composite unique key. Attaching zero ids is a no-op.

Large id sets are chunked to the dialect's bind-parameter ceiling; outside a transaction each chunk commits independently — wrap the call in db.Tx, or retry (idempotency makes a rerun converge).

func ClearRelation added in v0.16.0

func ClearRelation[T any](ctx context.Context, db Queryer, row *T, relation string) error

ClearRelation unlinks every row of a ManyToMany relation.

func Delete

func Delete[T any](ctx context.Context, db Queryer, row *T) error

Delete removes a row by primary key. Models with a softdelete column get an UPDATE setting the deletion timestamp instead; ForceDelete really deletes. The version column, when present, is checked like Update.

func Detach added in v0.2.0

func Detach[T any, K any](ctx context.Context, db Queryer, row *T, relation string, ids ...K) error

Detach unlinks rows from a ManyToMany relation; ids must be non-empty.

func Exec

func Exec(ctx context.Context, db Queryer, sqlText string, args ...any) (sql.Result, error)

Exec runs a hand-written statement through the shared pipeline and returns the driver result. The SQL is verbatim; never build it from untrusted input.

func Find

func Find[T any](ctx context.Context, db Queryer, key ...any) (*T, error)

Find fetches a row by primary key. Pass composite key parts in struct-field declaration order.

func ForceDelete

func ForceDelete[T any](ctx context.Context, db Queryer, row *T) error

ForceDelete removes a row even when its model supports soft deletion.

func Insert

func Insert[T any](ctx context.Context, db Queryer, row *T) error

Insert writes one row and backfills generated columns supported by the dialect. Zero omitzero fields use database defaults; zero auto-increment keys are omitted. It initializes timestamps and a zero version before execution, so a failed call may still modify those fields. Trigger changes not returned by the statement are not loaded.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	user := User{Email: "alice@example.com", Age: 30, Active: true}
	if err := rio.Insert(ctx, db, &user); err != nil {
		log.Fatal(err)
	}
	// ID is backfilled where the dialect generates it; CreatedAt is stamped.
	fmt.Println(user.ID, user.CreatedAt.IsZero())
}

func InsertAll

func InsertAll[T any](ctx context.Context, db Queryer, rows []T) error

InsertAll inserts rows in chunks within the dialect's bind limit. Chunks commit independently unless the caller supplies a transaction. It backfills generated keys only where ordering is reliable; omitzero does not apply.

On a native channel whose driver streams bulk loads (go-rio/postgres via COPY), an explicit-key batch goes through the copy protocol instead: one exchange, atomic as a whole, values bound through the same encoding.

func Restore

func Restore[T any](ctx context.Context, db Queryer, row *T) error

Restore clears the deletion timestamp of one soft-deleted row by primary key. The version column, when present, is checked and bumped like any other write.

func SyncRelation added in v0.3.0

func SyncRelation[T any, K any](ctx context.Context, db Queryer, row *T, relation string, ids ...K) error

SyncRelation makes a ManyToMany relation match ids exactly in a transaction. No ids clears the relation.

func TableName added in v0.15.0

func TableName(structName string) string

TableName derives the conventional table name for a struct type: User → users, APIKey → api_keys, Person → people.

func Update

func Update[T any](ctx context.Context, db Queryer, row *T, cols ...string) error

Update writes a row by primary key. Without cols it writes every eligible field, including zero values; otherwise it writes only cols and UpdatedAt. It enforces optimistic locking and may stamp the struct before a failed call.

func Upsert

func Upsert[T any](ctx context.Context, db Queryer, row *T, opts ...UpsertOption) error

Upsert inserts a row or updates it on unique-key conflict in one statement. Unless KeepTrashed is set, a successful update restores a soft-deleted row. Zero omitzero fields are excluded from both insert and the default update set; naming one explicitly in DoUpdate is an error.

PostgreSQL and SQLite backfill the conflict result. MySQL backfills an auto-increment key only on insert and cannot refresh a server-incremented version; reload before updating the same versioned struct. MySQL DoUpdate requires MySQL 8.0.19 or later and is not supported by MariaDB.

Timestamp and version initialization occurs before execution, so a failed call may still modify the struct.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	user := User{Email: "alice@example.com", Age: 31, Active: true}
	err := rio.Upsert(ctx, db, &user,
		rio.OnConflict("email"),
		rio.DoUpdate("age", "active"),
		rio.DoUpdateSet(rio.Set{"login_count": rio.Expr("users.login_count + 1")}),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(user.ID)
}

func UpsertAll

func UpsertAll[T any](ctx context.Context, db Queryer, rows []T, opts ...UpsertOption) error

UpsertAll applies Upsert conflict behavior in chunked multi-VALUES statements. It does not backfill generated values, and omitzero does not apply. MySQL DoUpdate requires MySQL 8.0.19 or later and excludes MariaDB.

func WriteColumns added in v0.3.0

func WriteColumns(w io.Writer, pkgName string, models ...any) error

WriteColumns generates Go source declaring column-name constants for the given models: per model, a <Name>Table constant (the convention- or TableName-derived name; a WithTableNamer handle may rename it at runtime) and a <Name>Cols struct value with one string field per mapped column.

Types

type BatchStatement added in v0.13.0

type BatchStatement struct {
	SQL  string
	Args []any
}

BatchStatement is one rendered statement of a batch: SQL in the dialect's placeholder form and its bind values, as NativeDB.Query receives them.

type BelongsTo

type BelongsTo[T any] struct {
	// contains filtered or unexported fields
}

BelongsTo holds the parent row referenced by a foreign key on this row. A NULL foreign key preloads as loaded-nil: Row returns nil, no panic.

func (BelongsTo[T]) Loaded

func (r BelongsTo[T]) Loaded() bool

Loaded reports whether the relation has been populated by With or Set.

func (BelongsTo[T]) MarshalJSON

func (r BelongsTo[T]) MarshalJSON() ([]byte, error)

MarshalJSON behaves like HasOne.MarshalJSON.

func (BelongsTo[T]) Row

func (r BelongsTo[T]) Row() *T

Row returns the loaded parent, or nil when the foreign key was NULL. It panics if the relation was never loaded.

func (*BelongsTo[T]) Set

func (r *BelongsTo[T]) Set(row *T)

Set marks the relation loaded. A nil row means "loaded, no parent".

func (*BelongsTo[T]) UnmarshalJSON

func (r *BelongsTo[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON behaves like HasOne.UnmarshalJSON.

type ColumnSchema added in v0.12.0

type ColumnSchema struct {
	// Name is the column name.
	Name string
	// Field is the Go struct field's name.
	Field string
	// GoType is the field's Go type.
	GoType reflect.Type
	// Nullable reports whether rio can scan a NULL into the field: a pointer
	// field, a sql.Scanner (which receives NULL itself), or the softdelete
	// column's NULL↔zero-time exception.
	Nullable bool
	// PrimaryKey marks the primary-key columns.
	PrimaryKey bool
	// JSON marks columns stored as serialized JSON text.
	JSON bool
	// Scanner marks columns delegated to the type's own sql.Scanner; their
	// database representation is undecidable to schema tooling.
	Scanner bool
}

ColumnSchema is one mapped column of a model, as rio's plan sees it — the expectation side of a schema comparison.

type Cursor added in v0.12.0

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

Cursor marks a position in a keyset-ordered result: the sort-key values of the row it points past, plus a fingerprint of the ordering that issued it. The zero Cursor is "no position" and After rejects it. Tokens are opaque but not tamper-proof: values bind as parameters, so a forged token can move the page window, never change the query.

func ParseCursor added in v0.12.0

func ParseCursor(s string) (Cursor, error)

ParseCursor decodes a token produced by String. Malformed input fails here; a token for a different ordering fails at After's fingerprint check.

func (Cursor) IsZero added in v0.12.0

func (c Cursor) IsZero() bool

IsZero reports whether the cursor marks no position.

func (Cursor) String added in v0.12.0

func (c Cursor) String() string

String encodes the cursor as a URL-safe token.

type DB

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

DB wraps a *sql.DB with a dialect. rio never replaces or tunes the connection pool — configure pooling on the *sql.DB you pass in.

func New

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

New wraps an existing *sql.DB. Panics on a nil db or dialect, and on WithStmtCache with a dialect that cannot prepare statements (ClickHouse).

func NewNative added in v0.8.0

func NewNative(nc NativeConfig, dialect Dialect, opts ...Option) *DB

NewNative constructs a *DB on a driver-native execution channel. Driver-module SPI: applications construct through the driver module (postgres.OpenNative). Close on the returned DB closes the SQLView first, then the channel. Panics if NativeConfig.DB or dialect is nil, or if opts include WithStmtCache — statement caching belongs to the native driver.

func (*DB) Close

func (d *DB) Close() error

Close closes the prepared-statement cache (if enabled) and the underlying *sql.DB.

func (*DB) DescribeModel added in v0.12.0

func (d *DB) DescribeModel(model any) (*TableSchema, error)

DescribeModel reports how this handle maps model: its resolved table name and column expectations. The model must be a mappable struct (or pointer to one); relations and countof targets are not columns and do not appear.

func (*DB) Dialect added in v0.13.0

func (d *DB) Dialect() Dialect

Dialect returns this handle's dialect identity — one of rio.Postgres, rio.MySQL, rio.SQLite, or rio.ClickHouse — a comparable value for dispatch.

func (*DB) DriverHandle added in v0.12.0

func (d *DB) DriverHandle() any

DriverHandle returns the driver-owned handle attached through WithDriverHandle or NativeConfig.Handle, or nil when none was attached.

func (*DB) Native added in v0.8.0

func (d *DB) Native() any

Native returns NativeConfig.Handle on the native channel and nil on the database/sql channel.

func (*DB) Tx

func (d *DB) Tx(ctx context.Context, fn func(tx *Tx) error) error

Tx runs fn in a transaction with default options.

Example
package main

import (
	"context"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	err := db.Tx(ctx, func(tx *rio.Tx) error {
		users, err := rio.From[User]().
			Where("active").
			ForUpdate(rio.SkipLocked).
			Limit(10).
			All(ctx, tx)
		if err != nil {
			return err // rolls back
		}
		for i := range users {
			users[i].Age++
			if err := rio.Update(ctx, tx, &users[i], "age"); err != nil {
				return err
			}
		}
		return nil // commits
	})
	if err != nil {
		log.Fatal(err)
	}
}

func (*DB) TxWith

func (d *DB) TxWith(ctx context.Context, opts *sql.TxOptions, fn func(tx *Tx) error) (err error)

TxWith runs fn in a transaction with the given options (isolation level, read-only).

func (*DB) Unwrap

func (d *DB) Unwrap() *sql.DB

Unwrap returns the underlying *sql.DB. On the native channel it is the driver module's database/sql view over the same pool (NativeConfig.SQLView), or nil when none was supplied; never tune pooling on that view.

func (*DB) WithoutStamps added in v0.18.0

func (d *DB) WithoutStamps() *DB

WithoutStamps returns a handle that generates no CreatedAt or UpdatedAt value. Statements carrying the caller's row (Insert, InsertAll, full-column Update, both Upsert branches) bind the struct's values as they are; the ones rio composes itself (column-list Update, UpdateAll, Delete, Restore) drop the assignment. Versions and softdelete stamps are unaffected. The handle shares its parent's pool and caches; its transactions inherit it.

type Dialect

type Dialect interface {
	// contains filtered or unexported methods
}

Dialect identifies one of the built-in SQL dialects. All methods are unexported: driver modules pick a built-in value, never implement one.

var (
	// Postgres renders $n placeholders, RETURNING, and ON CONFLICT (columns),
	// and binds a typed key slice as one array parameter.
	Postgres Dialect = postgresDialect{}
	// MySQL renders ? placeholders and ON DUPLICATE KEY UPDATE; it has no
	// RETURNING and no conflict target.
	MySQL Dialect = mysqlDialect{}
	// SQLite renders ? placeholders, RETURNING, and ON CONFLICT (columns);
	// ForUpdate is elided because the whole database locks, times bind as
	// sqliteTimeFormat text, and statements chunk under 999 parameters.
	SQLite Dialect = sqliteDialect{}
	// ClickHouse is append-only OLAP: no transactions, row locks, unique keys,
	// or generated keys, and every argument interpolates client-side; see
	// clickhouseDialect.caps for the exact surface.
	ClickHouse Dialect = clickhouseDialect{}
)

Built-in dialects: driver modules select one, and New and NewNative take it.

type Expr

type Expr string

Expr is a verbatim UpdateAll value for database-side expressions; never construct it from untrusted input.

type HasMany

type HasMany[T any] struct {
	// contains filtered or unexported fields
}

HasMany holds the "child rows pointing at this row" side of a one-to-many relation. "Not loaded" and "loaded, empty" are distinct states: rio never lazy-loads and never returns silently empty data.

func (HasMany[T]) Loaded

func (r HasMany[T]) Loaded() bool

Loaded reports whether the relation has been populated by With or Set.

func (HasMany[T]) MarshalJSON

func (r HasMany[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes unloaded relations as null and loaded ones as arrays.

func (HasMany[T]) Rows

func (r HasMany[T]) Rows() []T

Rows returns the loaded children, panicking if the relation was never loaded.

func (*HasMany[T]) Set

func (r *HasMany[T]) Set(rows []T)

Set marks the relation loaded with the given rows.

func (*HasMany[T]) UnmarshalJSON

func (r *HasMany[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts null (leaving the relation unloaded) or an array.

type HasOne

type HasOne[T any] struct {
	// contains filtered or unexported fields
}

HasOne holds the "single child row pointing at this row" side of a one-to-one relation.

func (HasOne[T]) Loaded

func (r HasOne[T]) Loaded() bool

Loaded reports whether the relation has been populated by With or Set.

func (HasOne[T]) MarshalJSON

func (r HasOne[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes unloaded as null; loaded-none also encodes as null.

func (HasOne[T]) Row

func (r HasOne[T]) Row() *T

Row returns the loaded child, or nil when the parent has none. It panics if the relation was never loaded.

func (*HasOne[T]) Set

func (r *HasOne[T]) Set(row *T)

Set marks the relation loaded. A nil row means "loaded, has none".

func (*HasOne[T]) UnmarshalJSON

func (r *HasOne[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts null (leaving the relation unloaded) or an object.

type LockOption added in v0.16.0

type LockOption uint8

LockOption refines a row lock: what happens when a row is already locked.

const (
	// NoWait fails immediately instead of waiting for a locked row.
	NoWait LockOption = iota + 1
	// SkipLocked leaves out rows another transaction holds locked.
	SkipLocked
)

type ManyToMany

type ManyToMany[T any] struct {
	// contains filtered or unexported fields
}

ManyToMany is HasMany across a join table.

func (ManyToMany[T]) Loaded

func (r ManyToMany[T]) Loaded() bool

Loaded reports whether the relation has been populated by With or Set.

func (ManyToMany[T]) MarshalJSON

func (r ManyToMany[T]) MarshalJSON() ([]byte, error)

MarshalJSON behaves like HasMany.MarshalJSON.

func (ManyToMany[T]) Rows

func (r ManyToMany[T]) Rows() []T

Rows returns the loaded rows, panicking when the relation was never loaded.

func (*ManyToMany[T]) Set

func (r *ManyToMany[T]) Set(rows []T)

Set marks the relation loaded with the given rows.

func (*ManyToMany[T]) UnmarshalJSON

func (r *ManyToMany[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON behaves like HasMany.UnmarshalJSON.

type NativeBatchResults added in v0.13.0

type NativeBatchResults interface {
	Rows() (rows NativeRows, done bool, err error)
	Close() error
}

NativeBatchResults yields each batched statement's rows in submission order. Rows returns the next statement's result — its NativeRows must be fully consumed and closed before the next call — and reports done when every statement's result has been handed out. Close releases the batch and surfaces any deferred protocol error; it must be called once, after consumption stops (early on failure is fine).

type NativeBatcher added in v0.13.0

type NativeBatcher interface {
	QueryBatch(ctx context.Context, stmts []BatchStatement) (NativeBatchResults, error)
}

NativeBatcher is an optional capability of a NativeDB or NativeTx: executing a group of independent row-returning statements in one driver round trip. Implementations queue every statement and flush once; results are consumed strictly in submission order.

type NativeCell added in v0.8.0

type NativeCell interface {
	sql.Scanner // fallback: accepts driver-canonical values
	ScanKind() NativeScanKind
	SetInt64(int64) error
	SetUint64(uint64) error
	SetFloat64(float64) error
	SetBool(bool) error
	SetString(string) error
	SetBytes([]byte) error
	SetTime(time.Time) error
	SetNull() error
	// contains filtered or unexported methods
}

NativeCell is the typed sink a NativeRows implementation feeds decoded column values into, one cell per column. Sealed: drivers consume it, never implement it, so rio may add Set methods in a minor version without breaking a driver.

Every Set method is exactly Scan with the interface boxing removed — SetInt64(v) behaves like Scan(int64(v)), SetNull like Scan(nil) — same conversion, overflow, and NULL rules, same error shapes, mismatched-kind fallback included. SetBytes never retains its argument. SetString stores its argument as-is, so hand over an owned string, never an unsafe view of driver memory. ScanKind reports the cell's strategy; pointer fields report their element's kind, and SetNull stores nil.

type NativeConfig added in v0.8.0

type NativeConfig struct {
	// DB is the native execution channel. Required.
	DB NativeDB

	// Handle is the driver-native pool handle, returned by
	// (*DB).DriverHandle and (*DB).Native.
	Handle any

	// SQLView is an optional database/sql view over the same pool, returned
	// by (*DB).Unwrap (nil without one). (*DB).Close closes it before DB.
	SQLView *sql.DB
}

NativeConfig carries what a driver module hands NewNative, all wired to the same underlying pool.

type NativeCopier added in v0.13.0

type NativeCopier interface {
	CopyIn(ctx context.Context, table []string, columns []string, next func() ([]any, error)) (int64, error)
}

NativeCopier is an optional capability of a NativeDB or NativeTx: bulk-loading rows through the driver's streaming copy protocol (PostgreSQL COPY FROM). table is the resolved, unquoted table name split into schema segments ([]string{"app", "users"}); the driver quotes each segment. next returns one row's bind values in columns order, (nil, nil) when the batch is exhausted, or a non-nil error to abort the copy; the returned slice is valid only until the next call, so encode it before pulling the next row.

type NativeDB added in v0.8.0

type NativeDB interface {
	Query(ctx context.Context, sql string, args []any) (NativeRows, error)
	Exec(ctx context.Context, sql string, args []any) (rowsAffected int64, err error)
	Begin(ctx context.Context, opts *sql.TxOptions) (NativeTx, error)
	Close() error
}

NativeDB is a driver-native execution channel: what rio needs from a driver pool. SQL arrives fully rendered in the dialect's placeholder form; args are the bind values rio would hand database/sql. Exec returns the driver's affected-row count. Begin maps *sql.TxOptions (possibly nil) onto the driver's transaction options. Close releases the channel's resources.

type NativeLastInserter added in v0.17.0

type NativeLastInserter interface {
	ExecLastInsert(ctx context.Context, sql string, args []any) (rowsAffected, lastInsertID int64, err error)
}

NativeLastInserter is an optional capability of a NativeDB or NativeTx: Exec that also reports the last inserted row id, which the SQLite dialect uses to backfill a lone auto-increment key. rio prefers it over Exec.

type NativeRows added in v0.8.0

type NativeRows interface {
	Columns() []string
	Next() bool
	Scan(dest ...any) error
	Err() error
	Close()
}

NativeRows is a driver-native result set. Close returns nothing; errors — including those Close itself discovers — converge in Err, which rio reads after Close. rio passes the same dest slots, in the same order, for every row of one result; each slot is either a NativeCell or a plain pointer (scan it as the driver natively would), so implementations may classify the dest list on the first Scan and reuse it.

type NativeScanKind added in v0.8.0

type NativeScanKind uint8

NativeScanKind names the plan-time scan strategy of one NativeCell, so a NativeRows implementation can pick a typed decode path per column. The enum can grow: treat any unrecognized kind as NativeKindScanner — the fallback is correct for every kind, only slower.

const (
	// NativeKindScanner is the fallback and zero value: pass the cell
	// itself to the driver's sql.Scanner path.
	NativeKindScanner NativeScanKind = iota
	// NativeKindInt marks an integer field; SetInt64 is its direct path.
	NativeKindInt
	// NativeKindUint marks an unsigned integer field; SetUint64 is its direct path.
	NativeKindUint
	// NativeKindFloat marks a float field; SetFloat64 is its direct path.
	NativeKindFloat
	// NativeKindBool marks a bool field; SetBool is its direct path.
	NativeKindBool
	// NativeKindString marks a string field; SetString is its direct path.
	NativeKindString
	// NativeKindBytes marks a []byte field; SetBytes is its direct path.
	NativeKindBytes
	// NativeKindTime marks a time.Time field; SetTime is its direct path.
	NativeKindTime
	// NativeKindJSON takes the column's raw JSON payload through SetBytes or
	// SetString, not a value decoded driver-side.
	NativeKindJSON
)

type NativeTx added in v0.8.0

type NativeTx interface {
	Query(ctx context.Context, sql string, args []any) (NativeRows, error)
	Exec(ctx context.Context, sql string, args []any) (int64, error)
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

NativeTx is one driver-native transaction. Once the transaction has ended — committed, rolled back, or destroyed by the driver on its own — Commit and Rollback must return an error satisfying errors.Is(err, sql.ErrTxDone), translating the driver's own sentinel where needed.

type Option

type Option func(*config)

Option configures a DB handle at construction time.

func WithClock

func WithClock(now func() time.Time) Option

WithClock replaces the time source used for CreatedAt/UpdatedAt and soft deletes; nil is ignored. Intended for tests.

func WithDriverHandle added in v0.12.0

func WithDriverHandle(h any) Option

WithDriverHandle attaches a driver-owned handle to the DB, retrievable through DB.DriverHandle. rio never touches the value; it exists for driver modules' typed accessors.

func WithErrorTranslator

func WithErrorTranslator(f func(error) error) Option

WithErrorTranslator installs a driver-specific error translator, mapping driver errors to rio sentinels (ErrDuplicateKey, ErrForeignKeyViolated). The go-rio driver modules install one automatically; the translator runs before the dialect's SQLSTATE fallback. Returning nil means "not mine".

func WithQueryHook

func WithQueryHook(h QueryHook) Option

WithQueryHook installs a read-only hook for executed statements and transaction control; a nil hook is ignored.

func WithStmtCache

func WithStmtCache(capacity ...int) Option

WithStmtCache enables bounded prepared-statement caches; the DB and each transaction own separate caches. The sqlite and mysql modules enable it by default; WithoutStmtCache opts out for transaction/statement-mode poolers. Schema-change errors evict entries and are not retried. New panics if used with ClickHouse, which cannot prepare general queries.

func WithTableNamer

func WithTableNamer(f func(structName string) string) Option

WithTableNamer overrides conventional table names for this handle; a model's TableName method still takes precedence. The function must be a pure, stable mapping — rendered SQL is cached per handle — so for dynamic tenancy construct one *DB per naming universe.

func WithoutArgs

func WithoutArgs() Option

WithoutArgs redacts bind arguments from QueryEvent before hooks see them.

func WithoutStmtCache added in v0.16.0

func WithoutStmtCache() Option

WithoutStmtCache disables the prepared-statement caches, overriding a driver module's default.

type Query

type Query[T any] struct {
	// contains filtered or unexported fields
}

Query is an immutable, connection-free query description safe for concurrent reuse; builder methods return derived values.

func From

func From[T any]() Query[T]

From starts a query for T's table.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	users, err := rio.From[User]().
		Where("age >= ?", 18).
		Where("active").
		OrderBy("created_at DESC").
		Limit(10).
		All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, u := range users {
		fmt.Println(u.Email)
	}
}

func (Query[T]) After added in v0.12.0

func (q Query[T]) After(c Cursor) Query[T]

After resumes past the position c marks: rows strictly after it in the OrderKeys ordering. c must come from CursorAt under the same OrderKeys; a different ordering fails loudly.

func (Query[T]) All

func (q Query[T]) All(ctx context.Context, db Queryer, args ...any) ([]T, error)

All runs the query and returns every matching row. args fill deferred placeholders from Where and Having fragments in final SQL order.

func (Query[T]) AllRows

func (q Query[T]) AllRows() Query[T]

AllRows is the explicit opt-in for UpdateAll/DeleteAll without conditions.

func (Query[T]) Avg added in v0.16.0

func (q Query[T]) Avg[V any](ctx context.Context, db Queryer, column string, args ...any) (V, error)

Avg averages a mapped column under the query's conditions; over no rows it returns V's zero value.

func (Query[T]) Before added in v0.16.0

func (q Query[T]) Before(c Cursor) Query[T]

Before selects the page ending at the position c marks: the rows strictly before it, still in OrderKeys order. It runs the reversed query and turns the page around, so Rows cannot stream it.

func (Query[T]) Chunk added in v0.16.0

func (q Query[T]) Chunk(ctx context.Context, db Queryer, size int, args ...any) iter.Seq2[[]T, error]

Chunk yields the matching rows in keyset pages of size rows: one bounded query per page, the connection released between pages, With and WithCount applied per page. Pages follow OrderKeys, defaulting to the primary key; Limit, Offset, After, and Before are refused.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	// One bounded query per page, in primary-key order, the connection
	// released between pages.
	for posts, err := range rio.From[Post]().Where("published = ?", true).Chunk(ctx, db, 500) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(len(posts))
	}
}

func (Query[T]) Count

func (q Query[T]) Count(ctx context.Context, db Queryer, args ...any) (int64, error)

Count returns the number of matching rows. GroupBy, Having, Limit, and Offset are rejected; use Raw for those queries.

func (Query[T]) CreateOrFirst

func (q Query[T]) CreateOrFirst(ctx context.Context, db Queryer, row *T, args ...any) error

CreateOrFirst inserts row or returns the existing match after a unique-key conflict.

func (Query[T]) CursorAt added in v0.16.0

func (q Query[T]) CursorAt(row *T) (Cursor, error)

CursorAt issues the cursor marking row's position under the query's OrderKeys: After the last row of a page for the next page, Before the first row for the previous one. The row must hold the values the database stores (any row rio scanned does).

func (Query[T]) DeleteAll

func (q Query[T]) DeleteAll(ctx context.Context, db Queryer, args ...any) (int64, error)

DeleteAll deletes matching rows, using soft deletion when configured. It requires conditions or AllRows.

func (Query[T]) DeleteAllReturning added in v0.16.0

func (q Query[T]) DeleteAllReturning(ctx context.Context, db Queryer, args ...any) ([]T, error)

DeleteAllReturning is DeleteAll returning the deleted rows, as stored after a soft delete. Dialects without RETURNING (MySQL) reject it.

func (Query[T]) Distinct added in v0.16.0

func (q Query[T]) Distinct() Query[T]

Distinct renders SELECT DISTINCT: entity rows deduplicate across joins, Pluck values deduplicate, Count counts distinct primary keys, and Sum and Avg aggregate distinct values.

func (Query[T]) Exists

func (q Query[T]) Exists(ctx context.Context, db Queryer, args ...any) (bool, error)

Exists reports whether any row matches.

func (Query[T]) Final added in v0.7.0

func (q Query[T]) Final() Query[T]

Final applies ClickHouse's FINAL modifier to the main SELECT. It does not affect preloads, WithCount, or WhereHas subqueries. Other dialects reject it.

func (Query[T]) Find added in v0.16.0

func (q Query[T]) Find(ctx context.Context, db Queryer, key ...any) (*T, error)

Find fetches a row by primary key under the query's clauses: WithTrashed, With, WithCount, and inline Where all apply, and Must caches the shape. Composite key parts follow struct-field declaration order; the package Find is the plain cached lookup.

func (Query[T]) First

func (q Query[T]) First(ctx context.Context, db Queryer, args ...any) (*T, error)

First returns the first matching row or ErrNotFound. It adds LIMIT 1 only when no limit was set and never adds an order.

func (Query[T]) FirstOrCreate

func (q Query[T]) FirstOrCreate(ctx context.Context, db Queryer, row *T, args ...any) error

FirstOrCreate returns the first match or inserts row. If a concurrent insert wins, it re-reads after ErrDuplicateKey; if that still misses, it returns the duplicate-key error, which may identify a hidden soft-deleted row.

func (Query[T]) ForShare added in v0.16.0

func (q Query[T]) ForShare(opts ...LockOption) Query[T]

ForShare renders SELECT ... FOR SHARE, with at most one LockOption. A no-op on SQLite; rejected on ClickHouse.

func (Query[T]) ForUpdate

func (q Query[T]) ForUpdate(opts ...LockOption) Query[T]

ForUpdate renders SELECT ... FOR UPDATE, with at most one LockOption. A no-op on SQLite; rejected on ClickHouse.

func (Query[T]) ForceDeleteAll

func (q Query[T]) ForceDeleteAll(ctx context.Context, db Queryer, args ...any) (int64, error)

ForceDeleteAll permanently deletes matching rows. It requires conditions or AllRows, including on soft-delete models.

func (Query[T]) GroupBy

func (q Query[T]) GroupBy(expr string) Query[T]

GroupBy appends a verbatim GROUP BY term; never build it from untrusted input.

func (Query[T]) Having

func (q Query[T]) Having(expr string, args ...any) Query[T]

Having adds an AND-ed HAVING condition. The expression is verbatim — never build it from untrusted input.

func (Query[T]) Join

func (q Query[T]) Join(clause string) Query[T]

Join appends a verbatim JOIN clause; entity queries still select only T's columns. Never build the clause from untrusted input.

func (Query[T]) Limit

func (q Query[T]) Limit(n int) Query[T]

Limit caps the result. The value is rendered into the SQL, not bound.

func (Query[T]) Max added in v0.16.0

func (q Query[T]) Max[V any](ctx context.Context, db Queryer, column string, args ...any) (V, error)

Max returns the largest value of a mapped column under the query's conditions; over no rows it returns V's zero value.

func (Query[T]) Min added in v0.16.0

func (q Query[T]) Min[V any](ctx context.Context, db Queryer, column string, args ...any) (V, error)

Min returns the smallest value of a mapped column under the query's conditions; over no rows it returns V's zero value.

func (Query[T]) Must added in v0.10.0

func (q Query[T]) Must() Query[T]

Must panics if Validate fails. The returned query carries a private render cache keyed per executing handle; a handle's entries are reclaimed with it.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

// adults is a package-level template: validated once by Must, reused
// concurrently, and executed with its deferred argument per call.
var adults = rio.From[User]().
	Where("age >= ?").
	OrderBy("created_at DESC").
	Limit(10).
	Must()

func main() {
	ctx := context.Background()
	users, err := adults.All(ctx, db, 18)
	if err != nil {
		log.Fatal(err)
	}
	emails, err := adults.Pluck[string](ctx, db, "email", 21)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(users), len(emails))
}

func (Query[T]) Offset

func (q Query[T]) Offset(n int) Query[T]

Offset skips n rows.

func (Query[T]) OnlyTrashed

func (q Query[T]) OnlyTrashed() Query[T]

OnlyTrashed selects only soft-deleted rows.

func (Query[T]) OrderBy

func (q Query[T]) OrderBy(expr string) Query[T]

OrderBy appends an ORDER BY term, verbatim SQL ("created_at DESC"); never build it from untrusted input.

func (Query[T]) OrderKeys added in v0.12.0

func (q Query[T]) OrderKeys(keys ...SortKey) Query[T]

OrderKeys sets the structured ordering cursor pagination requires, rendered as the query's ORDER BY; it cannot mix with verbatim OrderBy. Primary-key columns missing from keys are appended as tie-breakers, following the last declared direction.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	q := rio.From[Post]().
		Where("published = ?", true).
		OrderKeys(
			rio.SortKey{Column: "score", Desc: true},
			rio.SortKey{Column: "created_at"},
		) // "id" is appended as the tie-breaker

	page, err := q.Limit(20).All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	if len(page) == 0 {
		return
	}

	// Next page: the cursor at the last row.
	last, err := q.CursorAt(&page[len(page)-1])
	if err != nil {
		log.Fatal(err)
	}
	next, err := q.After(last).Limit(20).All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	// Previous page: the cursor at the first row; Before reads backwards
	// and turns the page around, so it arrives in OrderKeys order.
	first, err := q.CursorAt(&page[0])
	if err != nil {
		log.Fatal(err)
	}
	prev, err := q.Before(first).Limit(20).All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	// Tokens round-trip through URL-safe strings.
	token := last.String()
	parsed, err := rio.ParseCursor(token)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(next), len(prev), parsed.IsZero())
}

func (Query[T]) Pluck added in v0.10.0

func (q Query[T]) Pluck[V any](ctx context.Context, db Queryer, column string, args ...any) ([]V, error)

Pluck extracts a single column under the query's conditions. The column must be one of T's mapped columns — expressions go through Raw.

func (Query[T]) RestoreAll added in v0.4.0

func (q Query[T]) RestoreAll(ctx context.Context, db Queryer, args ...any) (int64, error)

RestoreAll restores matching soft-deleted rows. It requires conditions or AllRows.

func (Query[T]) Rows added in v0.2.0

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

Rows streams results and closes them on completion or early break. It yields a zero T with the first error. With and WithCount cannot be streamed.

func (Query[T]) SQL added in v0.16.0

func (q Query[T]) SQL(db Queryer, args ...any) (string, []any, error)

SQL renders the statement All would run on db, with its bound arguments, without executing it.

func (Query[T]) Scope added in v0.3.0

func (q Query[T]) Scope(fns ...func(Query[T]) Query[T]) Query[T]

Scope applies reusable query functions in order.

func (Query[T]) Sole

func (q Query[T]) Sole(ctx context.Context, db Queryer, args ...any) (*T, error)

Sole returns the only matching row, ErrNotFound for none, or ErrMultipleRows for more than one. It adds LIMIT 2 only when no limit is set.

func (Query[T]) Sub added in v0.16.0

func (q Query[T]) Sub(column string) Subquery

Sub embeds the query as a ? argument projecting one mapped column, for IN (?), EXISTS (?), and scalar comparisons; the caller writes the parentheses. The query's own Where arguments must be inline.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	// The subquery renders in place of the ? with its own arguments spliced
	// in; the caller writes the parentheses.
	authors := rio.From[Post]().Where("published = ?", true).Sub("user_id")
	users, err := rio.From[User]().Where("id IN (?)", authors).All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(users))
}

func (Query[T]) Sum added in v0.16.0

func (q Query[T]) Sum[V any](ctx context.Context, db Queryer, column string, args ...any) (V, error)

Sum totals a mapped column under the query's conditions; over no rows it returns V's zero value (use sql.Null[V] to tell the two apart).

func (Query[T]) UpdateAll

func (q Query[T]) UpdateAll(ctx context.Context, db Queryer, set Set, args ...any) (int64, error)

UpdateAll updates matching rows and returns the affected count. It requires conditions or AllRows. UpdatedAt is maintained unless explicitly assigned; set-based writes do not use optimistic locking.

func (Query[T]) UpdateAllReturning added in v0.16.0

func (q Query[T]) UpdateAllReturning(ctx context.Context, db Queryer, set Set, args ...any) ([]T, error)

UpdateAllReturning is UpdateAll returning the updated rows. Dialects without RETURNING (MySQL) reject it.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	// Set-based writes need a condition (or AllRows); the returning form
	// hands the affected rows back on dialects with RETURNING.
	deactivated, err := rio.From[User]().
		Where("age < ?", 18).
		UpdateAllReturning(ctx, db, rio.Set{"active": false})
	if err != nil {
		log.Fatal(err)
	}
	for _, u := range deactivated {
		fmt.Println(u.Email, u.Active)
	}
}

func (Query[T]) Validate added in v0.10.0

func (q Query[T]) Validate() error

Validate checks q without accessing a database. Deferred Where and Having arguments are checked by the terminal method under its dialect.

func (Query[T]) Where

func (q Query[T]) Where(expr string, args ...any) Query[T]

Where adds an AND-ed condition in SQL with ? placeholders; slice arguments expand inside IN (?). The expression is verbatim — never build it from untrusted input.

func (Query[T]) WhereHas added in v0.2.0

func (q Query[T]) WhereHas(path string, opts ...RelOption) Query[T]

WhereHas keeps rows whose relation path has a matching row. Nested paths nest EXISTS predicates, and RelWithTrashed applies to the leaf relation.

func (Query[T]) WhereHasNot added in v0.2.0

func (q Query[T]) WhereHasNot(path string, opts ...RelOption) Query[T]

WhereHasNot keeps rows whose relation path has no matching row.

func (Query[T]) With

func (q Query[T]) With(path string, opts ...RelOption) Query[T]

With preloads a relation with a separate query. Dot-separated paths preload nested relations; options apply to the leaf.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/go-rio/rio"
)

// db stands for a handle opened by a driver module, for example
// sqlite.Open("file:app.db"). The core has no driver, so these examples
// compile but do not run.
var db *rio.DB

// User is a model: ID is the auto-increment primary key by convention,
// CreatedAt is maintained on insert, PostCount is the WithCount("Posts")
// target, and Posts loads only through With.
type User struct {
	ID         int64
	Email      string
	Age        int
	Active     bool
	LoginCount int64
	PostCount  int64 `rio:",countof:Posts"`
	CreatedAt  time.Time

	Posts rio.HasMany[Post]
}

// Post belongs to a User through the conventional user_id foreign key.
type Post struct {
	ID        int64
	UserID    int64
	Title     string
	Published bool
	Score     int64
	CreatedAt time.Time
}

func main() {
	ctx := context.Background()
	users, err := rio.From[User]().
		With("Posts",
			rio.RelWhere("published = ?", true),
			rio.RelOrderBy("created_at DESC"),
			rio.RelLimit(3)).
		WithCount("Posts", rio.RelWhere("published = ?", true)).
		All(ctx, db)
	if err != nil {
		log.Fatal(err)
	}
	for _, u := range users {
		// Rows would panic had Posts not been loaded with With.
		fmt.Println(u.Email, u.PostCount, len(u.Posts.Rows()))
	}
}

func (Query[T]) WithCount added in v0.2.0

func (q Query[T]) WithCount(relation string, opts ...RelOption) Query[T]

WithCount fills the tagged int64 count target for a HasMany or ManyToMany relation using one GROUP BY query. RelWhere and RelWithTrashed narrow what is counted; a filtered count never reuses a preloaded relation.

func (Query[T]) WithTrashed

func (q Query[T]) WithTrashed() Query[T]

WithTrashed includes soft-deleted rows.

type QueryEvent

type QueryEvent struct {
	// Op is a stable statement label: "select", "insert", "update",
	// "delete", "upsert", "copy", "raw", "exec", "begin", "commit",
	// "rollback", "savepoint".
	Op string
	// Model is the Go struct name behind the statement, "" for Raw/Exec and
	// transaction control.
	Model string
	// Query is the rendered, dialect-form SQL.
	Query string
	// Args are the bind arguments, nil when the DB was built WithoutArgs.
	Args []any
	// Err is the translated execution error, nil on success; a write whose
	// Result.RowsAffected fails carries that failure here. After only.
	Err error
	// Duration is the execution wall time; for row-returning queries it runs
	// through row consumption. After only.
	Duration time.Duration
	// RowsAffected is the driver-reported count for writes, -1 when unknown
	// (row-returning queries, or a report failure carried in Err). After only.
	RowsAffected int64
	// RowsReturned is how many rows the statement handed back, -1 for
	// statements that return none. Count and Exists report their result-set
	// rows (one), not the value they carry. After only.
	RowsReturned int64
	// Phase labels statements rio itself derives — "preload" and "count"
	// for With/WithCount relation queries, "probe" for internal write
	// probes; "" for everything else.
	Phase string
}

QueryEvent describes one statement execution. Hooks receive the same event pointer in Before and After; After sees Err, Duration, RowsAffected, and RowsReturned filled in.

type QueryHook

type QueryHook interface {
	BeforeQuery(ctx context.Context, e *QueryEvent) context.Context
	AfterQuery(ctx context.Context, e *QueryEvent)
}

QueryHook observes statement execution. The context BeforeQuery returns is the execution context: the statement and its row consumption run under it, and AfterQuery receives it; returning nil leaves the incoming context in force. Hooks must not retain the event past the call and cannot alter the statement.

For row-returning queries AfterQuery fires once the rows are consumed, so Err includes scan and iteration failures. Exception: a First/Find/Sole miss reports Err = nil — ErrNotFound is a successfully executed query. Batched or streamed native execution still fires events per logical statement: every BeforeQuery runs before the one wire exchange, contexts chaining into the single execution context, and a mid-batch failure reports the remaining statements' AfterQuery with the same error.

The method set is fixed: new hook capabilities arrive as optional interfaces discovered by type assertion, never as methods added here.

type Queryer

type Queryer interface {
	// Tx runs fn inside a transaction (on *DB) or a savepoint (on *Tx),
	// committing when fn returns nil and rolling back when it returns an
	// error or panics.
	Tx(ctx context.Context, fn func(tx *Tx) error) error
	// contains filtered or unexported methods
}

Queryer is the execution target every rio entry point accepts: a *DB or a *Tx.

type RawQuery

type RawQuery[T any] struct {
	// contains filtered or unexported fields
}

RawQuery is hand-written SQL through the shared pipeline, scanning into any target shape — DTO structs, scalars, entities. It is a connection-free value; placeholders are ? with IN (?) expansion.

func Raw

func Raw[T any](sqlText string, args ...any) RawQuery[T]

Raw builds a raw query. Struct scanning matches by column name and errors on result columns with no matching field. Scanning half an entity and then calling Update writes zero values to the unselected columns — project into DTOs. The SQL is verbatim; never build it from untrusted input.

func (RawQuery[T]) All

func (r RawQuery[T]) All(ctx context.Context, db Queryer) ([]T, error)

All runs the query and scans every row.

func (RawQuery[T]) First

func (r RawQuery[T]) First(ctx context.Context, db Queryer) (*T, error)

First returns the first row or ErrNotFound. rio does not append LIMIT to hand-written SQL; add your own when it matters.

func (RawQuery[T]) Rows added in v0.9.0

func (r RawQuery[T]) Rows(ctx context.Context, db Queryer) iter.Seq2[T, error]

Rows streams rows without materializing them. Iteration stops on the first error (yielded with a zero T) and the rows close automatically, including on early break. Struct targets follow All's full-column-coverage rule.

func (RawQuery[T]) Sole

func (r RawQuery[T]) Sole(ctx context.Context, db Queryer) (*T, error)

Sole returns the single row, ErrNotFound when none match, and ErrMultipleRows when several do.

type RelOption

type RelOption func(*relQuery)

RelOption customizes how one preloaded relation is fetched.

func RelLimit added in v0.2.0

func RelLimit(n int) RelOption

RelLimit caps the preloaded rows per parent, not overall. Order within each parent follows RelOrderBy, defaulting to the target's primary key. Requires window functions (PostgreSQL, MySQL 8+, SQLite 3.25+). RelLimit(0) loads no children, not all of them.

func RelOrderBy added in v0.16.0

func RelOrderBy(expr string) RelOption

RelOrderBy orders the preloaded rows before they are grouped per parent. The term is included verbatim; never build it from untrusted input.

func RelWhere

func RelWhere(expr string, args ...any) RelOption

RelWhere restricts the preloaded rows. The condition runs inside the preload's own query, so it can only reference the related table's columns. The expression is included verbatim; never build it from untrusted input.

func RelWithTrashed

func RelWithTrashed() RelOption

RelWithTrashed includes soft-deleted rows in the preload when the target model declares a softdelete column.

type Set

type Set map[string]any

Set maps database column names to UpdateAll values. Expr values are inserted verbatim; never construct them from untrusted input.

type SortKey added in v0.12.0

type SortKey struct {
	Column string
	Desc   bool
}

SortKey is one column of a structured ordering. It names a mapped column — not verbatim SQL — so rio can read its value back out of rows to issue cursors.

type Subquery added in v0.16.0

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

Subquery is a query embedded as a ? argument: it renders in place of the placeholder, its own arguments spliced into the statement's, with the caller's parentheses around it. Query.Sub builds one.

type TableNamer

type TableNamer interface {
	TableName() string
}

TableNamer overrides the convention-derived table name for one model.

type TableSchema added in v0.12.0

type TableSchema struct {
	// Struct is the model's Go type name.
	Struct string
	// Table is the table name under this handle's naming.
	Table string
	// Columns are the mapped columns in plan order; the primary key is the
	// PrimaryKey subset in order.
	Columns []ColumnSchema
}

TableSchema is a model's mapping under one handle: the resolved table name (TableName override, then WithTableNamer, then convention) and every mapped column in plan order. Read-only.

type Tx

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

Tx is a transaction handle. Like *sql.Tx it is bound to one connection and must not be used concurrently.

func (*Tx) NativeTx added in v0.8.0

func (t *Tx) NativeTx() any

NativeTx returns the NativeTx SPI adapter this transaction runs on, or nil on the database/sql channel.

func (*Tx) Tx

func (t *Tx) Tx(ctx context.Context, fn func(tx *Tx) error) (err error)

Tx runs fn inside a savepoint: released when fn returns nil, rolled back when fn returns an error or panics, leaving the outer transaction usable.

func (*Tx) Unwrap

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

Unwrap returns the underlying *sql.Tx, or nil on the native channel.

func (*Tx) WithoutStamps added in v0.18.0

func (t *Tx) WithoutStamps() *Tx

WithoutStamps returns a view of this transaction that leaves CreatedAt and UpdatedAt to the caller; see DB.WithoutStamps.

type UpsertOption

type UpsertOption func(*upsertSpec)

UpsertOption shapes the conflict clause.

func DoNothing

func DoNothing() UpsertOption

DoNothing turns conflicts into no-ops without suppressing unrelated errors. It remains compatible with MariaDB and MySQL before 8.0.19.

func DoUpdate

func DoUpdate(cols ...string) UpsertOption

DoUpdate selects columns to overwrite on conflict. Without columns it uses every eligible column; explicit columns are deduplicated in model order.

func DoUpdateSet added in v0.16.0

func DoUpdateSet(set Set) UpsertOption

DoUpdateSet assigns columns on conflict: an Expr renders verbatim (the incoming row is "excluded" on PostgreSQL and SQLite, "_rio_new" on MySQL), any other value binds. Calls merge; columns rio maintains, readonly columns, and columns also named in DoUpdate are rejected.

func KeepTrashed

func KeepTrashed() UpsertOption

KeepTrashed preserves deleted_at on both insert and conflict-update paths.

func OnConflict

func OnConflict(cols ...string) UpsertOption

OnConflict names the unique-index columns. DoUpdate requires it on PostgreSQL and SQLite; MySQL reacts to any unique index.

Directories

Path Synopsis
Package lint compares rio model expectations against a live database schema and reports the drift: missing tables/columns, nullability and primary-key disagreements, and type mismatches the dialect's equivalence classes can rule on.
Package lint compares rio model expectations against a live database schema and reports the drift: missing tables/columns, nullability and primary-key disagreements, and type mismatches the dialect's equivalence classes can rule on.

Jump to

Keyboard shortcuts

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