Help us shape the future of Go! Take the 2026 Go Developer Survey

orm

package
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 21 Imported by: 1

Documentation

Overview

Package orm is a generic query builder and model mapper over database/sql. Queries are typed (Query[T] returns T values), always use bound parameters, and validate column names against the model so input can never become SQL (A05). Models are plain structs; persistence functions take an Executor (*database.DB or *database.Tx), so there is no Active Record handle hidden in models.

Embed Model for an ID and timestamps, build reads with QueryOf, and write with Create, Update and Delete. Relations, scopes, soft deletes and encrypted columns (Encrypted) are declared on the struct. Tables come from package schema migrations.

An invalid model (not a struct, a bad table or column name, no primary key, a relation without its foreign key, an unknown orm tag option) panics on its first use, usually at boot: it is a programming error, not a runtime condition.

See https://github.com/AlexGalhardo/vukem/blob/main/docs/database.md.

Index

Examples

Constants

View Source
const DefaultMaxPerPage = 100

DefaultMaxPerPage bounds perPage in Paginate and CursorPaginate unless the query sets MaxPerPage, so a per_page taken from a request cannot make one call load a whole table.

Variables

View Source
var ErrConflict error = &conflictError{}

ErrConflict is returned (wrapped) when a write violates a unique constraint; it reports HTTP status 409 so handlers answer Conflict.

View Source
var ErrInvalidCursor = &errs.HTTPError{Status: 400, Message: "Invalid pagination cursor."}

ErrInvalidCursor is returned (400) when a cursor cannot be decoded.

View Source
var ErrLockOutsideTx = errors.New("orm: row locks need a transaction (run the query on a *database.Tx)")

ErrLockOutsideTx is returned when a locking query (LockForUpdate, ForShare) runs on a *database.DB instead of a *database.Tx: the lock would be released as soon as the statement ends.

View Source
var ErrNoEncrypter = errors.New("orm: encrypted columns need an encrypter (database.WithEncrypter)")

ErrNoEncrypter is returned when a model with Encrypted columns is written or read through an executor without an encrypter (see database.WithEncrypter). Values are never stored in plaintext instead.

View Source
var ErrNotFound = &errs.HTTPError{Status: 404, Message: "Record not found."}

ErrNotFound is returned when First/Find match no row. It matches errs.ErrNotFound, so handlers answer 404 by returning it.

View Source
var ErrNotLoaded = errors.New("orm: relation not loaded (use Query.With)")

ErrNotLoaded is returned when a relation is read without being eager loaded with Query.With. Relations never query lazily, so there are no hidden N+1 queries (strict mode).

Functions

func Attach added in v0.4.0

func Attach[T any](ctx context.Context, ex database.Executor, model *T, relation string, ids ...any) error

Attach links model to the related ids through the pivot table of the many-to-many relation (field name). Ids already attached are skipped.

func Columns

func Columns[T any]() []string

Columns returns the column names of model T in field order.

func Create

func Create[T any](ctx context.Context, ex database.Executor, model *T) error

Create inserts model, filling the auto-increment id and timestamps.

func CreateMany added in v0.24.0

func CreateMany[T any](ctx context.Context, ex database.Executor, models []T) error

CreateMany inserts models with multi-row INSERT statements, split into batches that stay under the dialect's bound-parameter limit. It fills timestamps and runs BeforeCreate on every model first, then AfterCreate once every row is stored.

Atomicity holds only for *database.DB and *database.Tx: on a DB the batches run in one transaction, so either every row is stored or none is; on a Tx they join it and the caller commits or rolls back. Any other executor (a wrapper) runs the statements directly, so batches written before a failure stay written and keep their ids.

AfterCreate runs after that transaction commits (a hook writing through the DB would otherwise deadlock on SQLite's single writer), so on a DB an AfterCreate error means the rows are stored and the caller still gets the error.

Auto-increment ids are written back when every model has a zero id (or every model sets its own; mixing both is an error). PostgreSQL and SQLite return them with RETURNING, in VALUES order; the ids must be strictly increasing (an ascending sequence), otherwise CreateMany fails instead of guessing which row got which id. MySQL cannot report the ids of a multi-row insert reliably, so there each row is its own INSERT. A model with no column besides its generated key is rejected.

func Delete

func Delete[T any](ctx context.Context, ex database.Executor, model *T) error

Delete removes model, or sets deleted_at when the model embeds SoftDeletes.

func Detach added in v0.4.0

func Detach[T any](ctx context.Context, ex database.Executor, model *T, relation string, ids ...any) error

Detach unlinks the related ids, or every related row when ids is empty.

func ForceDelete

func ForceDelete[T any](ctx context.Context, ex database.Executor, model *T) error

ForceDelete removes model even when it uses soft deletes.

func PrimaryKeyOf added in v0.20.0

func PrimaryKeyOf[T any]() string

PrimaryKeyOf returns the primary key column of model T.

func RelationsOf added in v0.28.0

func RelationsOf[T any]() []string

RelationsOf returns the relation field names of model T (the names Query.With takes), sorted.

func Restore

func Restore[T any](ctx context.Context, ex database.Executor, model *T) error

Restore clears deleted_at on a soft-deleted model.

func Sync added in v0.4.0

func Sync[T any](ctx context.Context, ex database.Executor, model *T, relation string, ids ...any) error

Sync makes ids the exact set of related rows (in a transaction): missing ids are attached, others detached.

func TableOf

func TableOf[T any]() string

TableOf returns the table name of model T.

func Update

func Update[T any](ctx context.Context, ex database.Executor, model *T) error

Update writes every column of model (except the primary key and created_at) and refreshes updated_at.

func Validate added in v0.4.0

func Validate(ctx context.Context, ex database.Executor, rules ...Rule) error

Validate runs rules and returns an *errs.ValidationError (422) listing the failing fields, or nil. Empty values (nil, "", zero numbers) are skipped: required-ness belongs to the z schema. The built-in messages follow the request locale ("validation.unique", "validation.exists"; see errs.WithTranslator, installed by i18n.Middleware).

func WithClock

func WithClock(ctx context.Context, now func() time.Time) context.Context

WithClock makes writes in ctx use now for timestamps (tests).

Types

type AfterCreator added in v0.21.0

type AfterCreator interface {
	AfterCreate(ctx context.Context) error
}

AfterCreator runs after INSERT.

type AfterUpdater

type AfterUpdater interface {
	AfterUpdate(ctx context.Context) error
}

AfterUpdater runs after UPDATE.

type BeforeCreator added in v0.21.0

type BeforeCreator interface {
	BeforeCreate(ctx context.Context) error
}

BeforeCreator runs before INSERT.

type BeforeDeleter

type BeforeDeleter interface {
	BeforeDelete(ctx context.Context) error
}

BeforeDeleter runs before DELETE (or soft delete).

type BeforeUpdater

type BeforeUpdater interface {
	BeforeUpdate(ctx context.Context) error
}

BeforeUpdater runs before UPDATE.

type BelongsTo added in v0.4.0

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

BelongsTo is a relation to the model referenced by a foreign key on this model: Post.User with column user_id. Override the column with the tag `orm:"fk:author_id"`.

func (BelongsTo[T]) Get added in v0.4.0

func (r BelongsTo[T]) Get() (*T, error)

Get returns the related model, nil when the foreign key points nowhere, or ErrNotLoaded.

func (BelongsTo[T]) IsZero added in v0.4.0

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

IsZero makes `json:",omitzero"` drop relations that were not loaded.

func (BelongsTo[T]) Loaded added in v0.4.0

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

Loaded reports whether the relation was eager loaded.

func (BelongsTo[T]) MarshalJSON added in v0.4.0

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

MarshalJSON encodes the related model (null when none or not loaded).

type CursorPage

type CursorPage[T any] struct {
	Items      []T    `json:"data"`
	PerPage    int    `json:"per_page"`
	NextCursor string `json:"next_cursor,omitempty"`
}

CursorPage is one page of keyset pagination. NextCursor is empty on the last page.

type Direction

type Direction string

Direction orders results.

const (
	Asc  Direction = "ASC"
	Desc Direction = "DESC"
)

Sort directions.

type Encrypted added in v0.18.0

type Encrypted[T any] struct {
	V T
}

Encrypted stores T as JSON encrypted with the executor's encrypter (AES-256-GCM via app.Crypt). The table and column are authenticated with the value, so a ciphertext copied to another column is rejected; values sealed with a previous key (APP_PREVIOUS_KEYS) still decrypt and are re-sealed with the current key on the next save.

Encrypted columns are written and read only through orm (Create, Update, Query.Update, queries and Pluck); passing an Encrypted value to database/sql directly fails. They cannot be filtered with Where because every encryption is randomized. NULL reads as the zero value. JSON encoding (API output) uses the plain V: tag the field `json:"-"` to hide it.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/AlexGalhardo/vukem/crypt"
	"github.com/AlexGalhardo/vukem/database"
	"github.com/AlexGalhardo/vukem/orm"
)

type Patient struct {
	orm.Model
	Name      string
	Diagnosis orm.Encrypted[string] `json:"-"`
}

func main() {
	ctx := context.Background()

	// Apps pass app.Crypt; app.DB() does it for you.
	enc, _ := crypt.New(bytes.Repeat([]byte{1}, crypt.KeySize))

	db, _ := database.Open(ctx, ":memory:", database.WithEncrypter(enc))
	defer db.Close()

	_, _ = db.ExecContext(ctx, `CREATE TABLE patients (id INTEGER PRIMARY KEY AUTOINCREMENT,
		created_at TIMESTAMP, updated_at TIMESTAMP, name TEXT, diagnosis TEXT)`)

	p := Patient{Name: "Ada", Diagnosis: orm.Encrypted[string]{V: "flu"}}
	if err := orm.Create(ctx, db, &p); err != nil {
		fmt.Println(err)
		return
	}

	var stored string
	_ = db.QueryRowContext(ctx, "SELECT diagnosis FROM patients").Scan(&stored)
	fmt.Println("stored plaintext:", stored == "flu" || stored == `"flu"`)

	got, _ := orm.QueryOf[Patient](db).Find(ctx, p.ID)
	fmt.Println("decrypted:", got.Diagnosis.V)
}
Output:
stored plaintext: false
decrypted: flu

func (Encrypted[T]) MarshalJSON added in v0.18.0

func (e Encrypted[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes the plain inner value.

func (*Encrypted[T]) Scan added in v0.18.0

func (*Encrypted[T]) Scan(any) error

Scan implements sql.Scanner and always fails: Encrypted columns are opened by orm queries.

func (*Encrypted[T]) UnmarshalJSON added in v0.18.0

func (e *Encrypted[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes into the inner value.

func (Encrypted[T]) Value added in v0.18.0

func (Encrypted[T]) Value() (driver.Value, error)

Value implements driver.Valuer and always fails: Encrypted values are sealed by orm writes, never sent to the database as plaintext.

type FieldInfo added in v0.20.0

type FieldInfo struct {
	// Column is the column name.
	Column string
	// Name is the Go field name.
	Name string
	// Index locates the field for reflect.Value.FieldByIndex.
	Index []int
	// Type is the Go field type.
	Type reflect.Type
	// Tag is the struct field tag.
	Tag reflect.StructTag
}

FieldInfo describes a mapped column of a model, for tools that work with any model (the admin panel, exporters).

func FieldsOf added in v0.20.0

func FieldsOf[T any]() []FieldInfo

FieldsOf returns the mapped columns of model T in field order.

type HasMany added in v0.4.0

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

HasMany is a relation to the models holding a foreign key to this one: User.Posts with posts.user_id. Tag: `orm:"fk:author_id"`.

func (HasMany[T]) Get added in v0.4.0

func (r HasMany[T]) Get() ([]T, error)

Get returns the related models or ErrNotLoaded.

func (HasMany[T]) IsZero added in v0.4.0

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

IsZero makes `json:",omitzero"` drop relations that were not loaded.

func (HasMany[T]) Loaded added in v0.4.0

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

Loaded reports whether the relation was eager loaded.

func (HasMany[T]) MarshalJSON added in v0.4.0

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

MarshalJSON encodes the related models ([] when loaded and empty).

type HasOne added in v0.4.0

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

HasOne is a relation to one model holding a foreign key to this one: User.Profile with profiles.user_id. Tag: `orm:"fk:owner_id"`.

func (HasOne[T]) Get added in v0.4.0

func (r HasOne[T]) Get() (*T, error)

Get returns the related model (nil when none) or ErrNotLoaded.

func (HasOne[T]) IsZero added in v0.4.0

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

IsZero makes `json:",omitzero"` drop relations that were not loaded.

func (HasOne[T]) Loaded added in v0.4.0

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

Loaded reports whether the relation was eager loaded.

func (HasOne[T]) MarshalJSON added in v0.4.0

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

MarshalJSON encodes the related model (null when none or not loaded).

type JSON

type JSON[T any] struct {
	V T
}

JSON stores T as JSON text in one column.

func (JSON[T]) MarshalJSON

func (j JSON[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes the inner value.

func (*JSON[T]) Scan

func (j *JSON[T]) Scan(src any) error

Scan implements sql.Scanner.

func (*JSON[T]) UnmarshalJSON

func (j *JSON[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes into the inner value.

func (JSON[T]) Value

func (j JSON[T]) Value() (driver.Value, error)

Value implements driver.Valuer.

type Keyer

type Keyer interface{ PrimaryKey() string }

Keyer overrides the primary key column (default "id").

type ManyToMany added in v0.4.0

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

ManyToMany relates models through a pivot table: Post.Tags with table post_tag (the two singular table names, sorted) and columns post_id and tag_id. Tag: `orm:"pivot:taggings,fk:post_id,rk:tag_id"`.

func (ManyToMany[T]) Get added in v0.4.0

func (r ManyToMany[T]) Get() ([]T, error)

Get returns the related models or ErrNotLoaded.

func (ManyToMany[T]) IsZero added in v0.4.0

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

IsZero makes `json:",omitzero"` drop relations that were not loaded.

func (ManyToMany[T]) Loaded added in v0.4.0

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

Loaded reports whether the relation was eager loaded.

func (ManyToMany[T]) MarshalJSON added in v0.4.0

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

MarshalJSON encodes the related models ([] when loaded and empty).

type Model

type Model struct {
	ID        int64     `db:"id"         json:"id"`
	CreatedAt time.Time `db:"created_at" json:"created_at"`
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

Model provides an auto-increment id and timestamps; embed it in models.

type NullTime added in v0.4.0

type NullTime struct {
	sql.Null[time.Time]
}

NullTime is a nullable timestamp that encodes as JSON null or an RFC 3339 time (sql.Null would encode as {"V":...,"Valid":...}).

func (NullTime) IsZero added in v0.4.0

func (n NullTime) IsZero() bool

IsZero reports a NULL value, so `json:",omitzero"` omits it.

func (NullTime) MarshalJSON added in v0.4.0

func (n NullTime) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (*NullTime) UnmarshalJSON added in v0.4.0

func (n *NullTime) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler.

type Page

type Page[T any] struct {
	Items    []T   `json:"data"`
	Total    int64 `json:"total"`
	Page     int   `json:"page"`
	PerPage  int   `json:"per_page"`
	LastPage int   `json:"last_page"`
}

Page is one page of results.

type Query

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

Query builds a SELECT for model T. Builders are values: every method returns a modified copy, so a base query can be shared and refined.

func QueryOf

func QueryOf[T any](ex database.Executor) Query[T]

QueryOf starts a query for model T on ex.

func (Query[T]) Chunk

func (q Query[T]) Chunk(ctx context.Context, size int, fn func([]T) error) error

Chunk processes rows in batches of size ordered by primary key; fn may stop early by returning an error. Batches are read by keyset (primary key greater than the last one seen), not OFFSET, so fn may delete or update the rows it receives without making Chunk skip rows. The query must not have its own OrderBy, Limit or Offset.

func (Query[T]) Count

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

Count returns the number of matching rows. It ignores LockForUpdate and ForShare: PostgreSQL rejects row locks on aggregates, so lock the rows with Get or Pluck when the count must stay stable.

func (Query[T]) CursorPaginate

func (q Query[T]) CursorPaginate(
	ctx context.Context,
	cursor string,
	perPage int,
	dir Direction,
) (CursorPage[T], error)

CursorPaginate returns perPage rows after cursor, ordered by the primary key in dir. Unlike Paginate it needs no COUNT and stays fast and stable on large or changing tables. Pass "" for the first page and the returned NextCursor for the following ones. The query must not have its own OrderBy: the keyset order is the primary key. perPage is clamped to 1..DefaultMaxPerPage (see MaxPerPage).

func (Query[T]) Delete

func (q Query[T]) Delete(ctx context.Context) (int64, error)

Delete removes every matching row (soft-deleting for SoftDeletes models unless WithTrashed/OnlyTrashed was used, which deletes for real) and returns the count.

func (Query[T]) Each

func (q Query[T]) Each(ctx context.Context) iter.Seq2[T, error]

Each streams rows without loading them all; breaking out of the loop closes the rows.

for post, err := range orm.QueryOf[Post](db).Each(ctx) { ... }

func (Query[T]) Exists

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

Exists reports whether any row matches (without row locks, see Count).

func (Query[T]) Find

func (q Query[T]) Find(ctx context.Context, id any) (T, error)

Find returns the row with the given primary key or ErrNotFound.

func (Query[T]) First

func (q Query[T]) First(ctx context.Context) (T, error)

First returns the first matching row or ErrNotFound.

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

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

ForShare appends FOR SHARE (LOCK IN SHARE MODE on MySQL, which MariaDB also understands): matching rows can be read but not changed by other transactions until this one ends. Like LockForUpdate it needs a *database.Tx and is a no-op on SQLite.

func (Query[T]) Get

func (q Query[T]) Get(ctx context.Context) ([]T, error)

Get returns every matching row.

func (Query[T]) Latest

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

Latest orders by created_at (or the primary key) descending.

func (Query[T]) Limit

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

Limit sets the maximum number of rows.

func (Query[T]) LockForUpdate added in v0.24.0

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

LockForUpdate appends FOR UPDATE: matching rows stay locked against other writers until the transaction ends. The query must run on a *database.Tx. On SQLite it is a no-op, because SQLite has no row locks and a write transaction (BEGIN IMMEDIATE) already locks the database.

func (Query[T]) MaxPerPage added in v0.28.0

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

MaxPerPage sets the largest perPage Paginate and CursorPaginate accept for this query (default DefaultMaxPerPage); a larger perPage is lowered to n, and Page.PerPage reports the value used. n < 1 is an error.

func (Query[T]) NoWait added in v0.24.0

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

NoWait makes the lock fail at once when a row is held by another transaction instead of waiting. Call it after LockForUpdate or ForShare. It is a no-op on SQLite, where it never fails: a second writer waits for the database lock (busy timeout) instead.

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 returns only soft-deleted rows.

func (Query[T]) OrWhere

func (q Query[T]) OrWhere(col string, args ...any) Query[T]

OrWhere is Where joined with OR.

func (Query[T]) OrderBy

func (q Query[T]) OrderBy(colName string, dir Direction) Query[T]

OrderBy adds an ORDER BY column (validated) and direction.

func (Query[T]) Paginate

func (q Query[T]) Paginate(ctx context.Context, page, perPage int) (Page[T], error)

Paginate returns page (1-based) with perPage items and the total count. perPage is clamped to 1..DefaultMaxPerPage (see MaxPerPage).

func (Query[T]) Pluck

func (q Query[T]) Pluck[V any](ctx context.Context, colName string) ([]V, error)

Pluck returns one column of every matching row as []V.

func (Query[T]) SQL

func (q Query[T]) SQL() (string, []any, error)

SQL returns the SELECT statement and its arguments (debugging, tests).

func (Query[T]) Scope

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

Scope applies reusable query functions: q.Scope(Published, ByAuthor(id)).

func (Query[T]) Select

func (q Query[T]) Select(cols ...string) Query[T]

Select limits the selected columns (validated against the model).

func (Query[T]) SkipLocked added in v0.24.0

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

SkipLocked makes the lock skip rows other transactions hold instead of waiting (work queues). Call it after LockForUpdate or ForShare. Like the lock itself it is a no-op on SQLite.

func (Query[T]) Update

func (q Query[T]) Update(ctx context.Context, set Set) (int64, error)

Update sets columns on every matching row and returns the count. updated_at is refreshed unless set explicitly.

func (Query[T]) Where

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

Where adds "col = value", or "col op value" when three arguments are given: Where("age", ">=", 18). Operators are allow-listed.

func (Query[T]) WhereBetween

func (q Query[T]) WhereBetween(colName string, low, high any) Query[T]

WhereBetween adds "col BETWEEN low AND high".

func (Query[T]) WhereGroup

func (q Query[T]) WhereGroup(fn func(g Query[T]) Query[T]) Query[T]

WhereGroup adds a parenthesised group built by fn.

func (Query[T]) WhereIn

func (q Query[T]) WhereIn(col string, values ...any) Query[T]

WhereIn adds "col IN (...)". An empty list matches nothing.

func (Query[T]) WhereNotIn

func (q Query[T]) WhereNotIn(col string, values ...any) Query[T]

WhereNotIn adds "col NOT IN (...)". An empty list matches everything.

func (Query[T]) WhereNotNull

func (q Query[T]) WhereNotNull(col string) Query[T]

WhereNotNull adds "col IS NOT NULL".

func (Query[T]) WhereNull

func (q Query[T]) WhereNull(col string) Query[T]

WhereNull adds "col IS NULL".

func (Query[T]) WhereRaw

func (q Query[T]) WhereRaw(sqlFrag string, args ...any) Query[T]

WhereRaw adds a raw condition. It must use "?" placeholders for every value; never concatenate input into sql. Placeholders are found the way the dialect lexes SQL (database.Rebind's rules): "?" inside literals, quoted identifiers, comments and PostgreSQL dollar-quoted bodies does not count, and "??" is a literal "?" on PostgreSQL. A fragment that ends inside a literal, identifier or comment (including a "--" comment without a newline) is rejected, since it would swallow the SQL appended after it, and so are explicit PostgreSQL "$n" parameters. Without an executor the fragment is lexed with SQLite's rules.

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

func (q Query[T]) With(relations ...string) Query[T]

With eager loads relations (by field name) for Get, First, Find, Paginate and CursorPaginate: one query per relation, batched with IN, so N rows cost 1 + len(relations) queries. Each does not eager load.

func (Query[T]) WithTrashed

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

WithTrashed includes soft-deleted rows.

type Rule added in v0.4.0

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

Rule is a validation rule that needs the database (unique, exists). z schemas validate shape and format; Rules run after parsing:

in, err := web.Parse(c, requests.RegisterSchema)
if err != nil {
	return err
}
if err := orm.Validate(c.Context(), db, orm.Unique[models.User]("email", in.Email)); err != nil {
	return err // 422 (or flashed errors for forms), like any validation error
}

func Exists added in v0.4.0

func Exists[T any](column string, value any) Rule

Exists passes when a (not soft-deleted) row of T has column = value.

func Unique added in v0.4.0

func Unique[T any](column string, value any) Rule

Unique passes when no row of T has column = value (soft-deleted rows count, as the database constraint does). Use Ignore(id) when updating.

func (Rule) Field added in v0.4.0

func (r Rule) Field(name string) Rule

Field reports the error on name instead of the column name; the built-in message names it too ("The contact has already been taken.").

func (Rule) Ignore added in v0.4.0

func (r Rule) Ignore(id any) Rule

Ignore excludes the row with primary key id (the model being updated).

func (Rule) Message added in v0.4.0

func (r Rule) Message(msg string) Rule

Message replaces the error message; it is used as is, not translated.

type Set

type Set map[string]any

Set is a column → value map for bulk updates.

type SoftDeletes

type SoftDeletes struct {
	DeletedAt NullTime `db:"deleted_at" json:"deleted_at,omitzero"`
}

SoftDeletes makes Delete set deleted_at instead of removing rows; queries exclude deleted rows unless WithTrashed or OnlyTrashed is used.

type Tabler

type Tabler interface{ Table() string }

Tabler overrides the table name (default: snake_case plural of the type).

Jump to

Keyboard shortcuts

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