sorm

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 20 Imported by: 0

README

sorm

The first Go ORM with a real Unit of Work.

Load a graph of entities, mutate them with plain Go assignments, call SaveChanges — sorm computes the minimal diff, orders the writes by their foreign-key dependencies and applies everything in a single transaction. No other Go library does this.

s := sorm.NewSession(db)
user, _ := sorm.Track[models.User](s).
    Where(u.Email.Eq("alice@example.com")).
    With(u.Posts.Include()).
    One(ctx)

user.Active = false          // plain assignments —
user.Posts[0].Title = "new"  // no setters, no dirty flags

err := s.SaveChanges(ctx)    // diff → topo-sort → batch → one transaction

Highlights

Unit of Work identity map, reflection-free snapshot diffing (26 ns/entity, 0 allocs), UPDATEs carry only changed columns, graph inserts with FK fixup via RETURNING
Optimistic concurrency a sorm:"version" field makes every UPDATE/DELETE carry a version predicate; conflicts surface as a typed *ConflictError
Type-safe queries generated column descriptors: Where(u.Age.Gte(18)) is checked by the compiler; predicates are values you compose in plain if statements
Zero-value honesty Where(u.Active.Eq(false)) and Set(u.Age.Set(0)) are real conditions — the classic GORM footgun is impossible by construction
Relations hasMany / belongsTo / hasOne / many2many; eager loading with filters, child ordering and arbitrary nesting; EXISTS filters in both directions
Projections GroupBy / Having, typed aggregates, relation-based and arbitrary joins, scanning into your own structs
Migrations embedded diff engine (Atlas SDK, no external CLI): declarative Apply/Plan and versioned files with Diff/Up/Down, checksums and an advisory lock against racing replicas
Three databases PostgreSQL (pgx, single-roundtrip batches), MySQL, SQLite — one code path, pluggable adapters
Production plumbing RunInTx with transient-error retries, typed ConstraintErrors, Instrument middleware, OpenTelemetry tracing (otelsorm)

The escape hatches are first-class: ToSQL() shows the exact SQL of any query, Raw/RawAs scan raw SQL into your types with strict column checks.

Benchmarks against GORM, Ent and raw database/sql live in benchmarks/: reads within ~9% of raw and 1.8× faster than GORM; single-field updates faster than both — with optimistic concurrency included.

Quick start

1. Describe your models — plain structs, no interfaces to implement:

package models

//go:generate go run github.com/dvislobokov/sorm/cmd/sorm gen .

type User struct {
    ID      int64   `sorm:"pk,auto"`
    Email   string  `sorm:"unique"`
    Active  bool
    Version int64   `sorm:"version"`
    Posts   []*Post `sorm:"hasMany:AuthorID"`
}

type Post struct {
    ID       int64  `sorm:"pk,auto"`
    AuthorID int64  `sorm:"fk:User.ID"`
    Author   *User  `sorm:"belongsTo:AuthorID"`
    Title    string
}

2. Generate the typed layer: go generate ./... produces a compact, review-friendly sormgen package.

3. Connect through a driver adapter:

pool, _ := pgxpool.New(ctx, dsn)
db := pgxd.Wrap(pool)                      // PostgreSQL
// db := sqld.Wrap(sdb, lite.Dialect{})    // SQLite
// db := sqld.Wrap(sdb, my.Dialect{})      // MySQL

4. Create the schema — from code or with versioned files:

migrate.Apply(ctx, sdb, "postgres")        // diff models vs database, apply
sorm migrate diff -dev-dsn <empty scratch db> add_users ./models
sorm migrate up -dsn <production dsn>

5. Query and mutate — see the session example above; without tracking:

users, _ := sorm.Query[models.User](db).
    Where(u.Active.Eq(true), u.Posts.Any(p.Title.HasPrefix("Go"))).
    OrderBy(u.Email.Asc()).Limit(20).
    All(ctx)

Documentation

The full guide lives in docs/guide:

  1. Getting started
  2. Schema definition — tags, types, indexes
  3. Queries — predicates, streaming, raw SQL
  4. Sessions & Unit of Work — tracking, SaveChanges, transactions
  5. Relations — eager loading, nesting, many-to-many
  6. Projections — aggregates, GROUP BY, joins
  7. Migrations — declarative & versioned
  8. Observability & errors — logging, tracing, typed errors
  9. Multiple databases — adapters and dialects

Plus the API reference.

Design history (in Russian): concept · detailed design · competitive analysis · EF Core gap analysis.

Examples

  • examples/chat — the showcase application: a chat service on Echo with clean layering (transport / service / repository), a dedicated "chat" DB schema, JSON documents with typed accessors, PG arrays, custom scalars, audit via RunInTx, migrations + seed on startup, srog logging and sconf configuration.

Philosophy

sorm does not hide SQL — it removes the manual bookkeeping of change tracking. Its rules were distilled from other libraries' failure modes: no silently dropped conditions, errors are return values only, ctx is mandatory, builders are immutable, UPDATE without WHERE refuses to run unless you say AllRows(), and every query can be inspected with ToSQL().

Status

Under active development; the API may change before v1. Every commit runs the full test suite against PostgreSQL 17, MySQL 8 and SQLite (including the race detector) in CI.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrCyclicGraph = errors.New("sorm: cyclic dependency between new entities")

ErrCyclicGraph — a cycle between new entities during SaveChanges (e.g. mutual references between two Added objects). Not supported in the MVP.

View Source
var ErrNotFound = errors.New("sorm: not found")

ErrNotFound — the single "not found" semantics across the entire API (One, future Find by PK).

Functions

func Add

func Add[E any](s *Session, entities ...*E)

Add registers new entities for INSERT. FKs of a new graph are set via navigation (p.Author = u) — the runtime fills in the FK column value after the parent is inserted.

func ClonePtr

func ClonePtr[V any](p *V) *V

func CloneTimePtr

func CloneTimePtr(p *time.Time) *time.Time

CloneTimePtr copies a *time.Time, dropping the monotonic component.

func IsUniqueViolation

func IsUniqueViolation(err error) bool

IsUniqueViolation — a shorthand check for a uniqueness violation.

func JSONScan

func JSONScan[T any](dst *T) sql.Scanner

JSONScan wraps a destination for reading a JSON column: it unmarshals []byte/string; SQL NULL leaves the destination at its zero value.

func JSONSnapshot

func JSONSnapshot(v any) []byte

JSONSnapshot marshals a value for snapshot diffing (json.Marshal is deterministic: map keys are sorted). nil-ish values snapshot as nil.

func JSONValue

func JSONValue(v any) driver.Valuer

JSONValue wraps a value for writing into a JSON column: it marshals on Value(). A nil pointer/map/slice becomes SQL NULL.

func PtrEq

func PtrEq[V comparable](a, b *V) bool

func QueryNameFromContext

func QueryNameFromContext(ctx context.Context) string

QueryNameFromContext returns the query name attached by WithQueryName or a builder's Named, or "" if none.

func Register

func Register[E any](m Meta[E])

Register registers an entity's meta. Called from the generated package's init().

func RegisterTable

func RegisterTable(def TableDef)

RegisterTable is called from the generated package's init().

func Remove

func Remove[E any](s *Session, entities ...*E)

Remove marks entities for DELETE. An entity must be tracked or have a populated PK (checked in SaveChanges).

func RunInTx

func RunInTx(ctx context.Context, db DB, fn func(tx Tx) error) error

RunInTx runs fn in a transaction: commit on nil, rollback on error. If the adapter classifies the error as transient (deadlock, serialization failure), the transaction is retried with exponential backoff — up to 3 retries. fn may be called multiple times: side effects outside the DB must be idempotent.

Sessions work naturally inside: NewSession(tx).SaveChanges flushes within this transaction without opening a nested one.

func SQLTypeFor

func SQLTypeFor(dialect string, c ColumnDef) string

SQLTypeFor — the column's SQL type for a dialect ("postgres","mysql","sqlite"). Single mapping point for the DDL generator and migrations.

func ScalarSnapshot

func ScalarSnapshot(v driver.Valuer) any

ScalarSnapshot normalizes a custom scalar for snapshot diffing: the driver.Value is comparable after []byte → string (driver.Value is one of nil, bool, int64, float64, string, time.Time, []byte). Used by generated snapshot/diff code — the Go type itself does not have to be comparable (decimal.Decimal is not).

func TimePtrEq

func TimePtrEq(a, b *time.Time) bool

func UnregisterTable

func UnregisterTable(name string)

UnregisterTable removes a table definition (mainly for migration tests).

func WithQueryName

func WithQueryName(ctx context.Context, name string) context.Context

WithQueryName returns a context carrying a logical query/operation name. Every database operation executed under this context is attributed to it.

Types

type AfterLoader added in v0.4.0

type AfterLoader interface {
	AfterLoad(ctx context.Context) error
}

AfterLoader is implemented by entities that post-process themselves after materialization (computed fields, decryption, ...).

type AggExpr

type AggExpr[E any, V comparable] struct {
	// contains filtered or unexported fields
}

AggExpr is an aggregate expression with value type V: comparisons yield Pred[E] for Having, As(...) yields a result column.

func Avg

func Avg[E any](c AnyCol) AggExpr[E, float64]

func Count

func Count[E any](c AnyCol) AggExpr[E, int64]

func CountAll

func CountAll[E any]() AggExpr[E, int64]

func CountDistinct

func CountDistinct[E any](c AnyCol) AggExpr[E, int64]

CountDistinct — count(DISTINCT col); portable across all dialects.

func Max

func Max[E any, V comparable](c ColV[V]) AggExpr[E, V]

func Min

func Min[E any, V comparable](c ColV[V]) AggExpr[E, V]

func NewAgg

func NewAgg[E any, V comparable](parts ...AggPart) AggExpr[E, V]

NewAgg assembles a custom aggregate expression from parts. E is the root entity of the projection; V is the result type used by Having comparisons.

// string_agg(name, ', ')  — how pgagg.StringAgg is built:
sorm.NewAgg[E, string](
    sorm.AggDialect("postgres"),
    sorm.AggRaw("string_agg("), sorm.AggCol(col), sorm.AggRaw(", "),
    sorm.AggArg(", "), sorm.AggRaw(")"),
)

func Sum

func Sum[E any, V comparable](c ColV[V]) AggExpr[E, V]

func (AggExpr[E, V]) Eq

func (a AggExpr[E, V]) Eq(v V) Pred[E]

func (AggExpr[E, V]) Gt

func (a AggExpr[E, V]) Gt(v V) Pred[E]

func (AggExpr[E, V]) Gte

func (a AggExpr[E, V]) Gte(v V) Pred[E]

func (AggExpr[E, V]) Lt

func (a AggExpr[E, V]) Lt(v V) Pred[E]

func (AggExpr[E, V]) Lte

func (a AggExpr[E, V]) Lte(v V) Pred[E]

type AggPart

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

AggPart is one fragment of a custom aggregate expression.

func AggArg

func AggArg(v any) AggPart

AggArg emits a bind-parameter placeholder for v.

func AggCol

func AggCol(c AnyCol) AggPart

AggCol emits a column reference (qualified inside projections).

func AggDialect

func AggDialect(name string) AggPart

AggDialect guards the expression: rendering on any other dialect is a build error returned when the query executes.

func AggLit

func AggLit(s string) AggPart

AggLit emits a safely quoted string literal — for spots where the SQL grammar forbids placeholders (e.g. MySQL's GROUP_CONCAT ... SEPARATOR).

func AggRaw

func AggRaw(sql string) AggPart

AggRaw emits a raw SQL fragment verbatim.

type AnyCol

type AnyCol interface {
	ColName() string
	// contains filtered or unexported methods
}

AnyCol is a column of any entity. Used in projections after a JOIN ("relaxed mode"): table membership in the FROM/JOIN set is validated when the query is built.

type ArrayCol

type ArrayCol[E any, V comparable] struct {
	// contains filtered or unexported fields
}

ArrayCol is the descriptor of a native PostgreSQL array column (`sorm:"array"` on []string, []int64, ...). Array predicates render PG operators and are guarded: building them on another dialect is a build error returned when the query executes. A nil slice maps to NULL.

func NewArrayCol

func NewArrayCol[E any, V comparable](table, name string) ArrayCol[E, V]

func (ArrayCol[E, V]) ColName

func (c ArrayCol[E, V]) ColName() string

func (ArrayCol[E, V]) Contains

func (c ArrayCol[E, V]) Contains(vs ...V) Pred[E]

Contains — col @> ARRAY[vs]: the column contains every listed element.

func (ArrayCol[E, V]) Has

func (c ArrayCol[E, V]) Has(v V) Pred[E]

Has — a single-element Contains, reads better for the common case.

func (ArrayCol[E, V]) IsNotNull

func (c ArrayCol[E, V]) IsNotNull() Pred[E]

func (ArrayCol[E, V]) IsNull

func (c ArrayCol[E, V]) IsNull() Pred[E]

func (ArrayCol[E, V]) Overlaps

func (c ArrayCol[E, V]) Overlaps(vs ...V) Pred[E]

Overlaps — col && ARRAY[vs]: the column shares at least one element.

func (ArrayCol[E, V]) Set

func (c ArrayCol[E, V]) Set(vs []V) Assign[E]

func (ArrayCol[E, V]) SetNull

func (c ArrayCol[E, V]) SetNull() Assign[E]

type Assign

type Assign[E any] struct {
	// contains filtered or unexported fields
}

Assign is a typed column assignment (Update.Set, future INSERT DSL).

type BatchItem

type BatchItem struct {
	SQL  string
	Args []any
	// IDCount > 0: a multi-row INSERT with auto-PK for IDCount rows. The
	// adapter obtains the ids via RETURNING (if the dialect supports it) or
	// LastInsertId arithmetic (MySQL — first id of the batch, SQLite — last)
	// and calls OnIDs with a slice of length IDCount in VALUES row order.
	IDCount int
	OnIDs   func(ids []int64)
	// Check is called with the number of affected rows (optimistic concurrency).
	Check func(rowsAffected int64) error
}

BatchItem — a single statement of a write batch.

type BeforeSaver added in v0.4.0

type BeforeSaver interface {
	BeforeSave(ctx context.Context, op SaveOp) error
}

BeforeSaver is implemented by entities that want a say before being inserted, updated or deleted by a session flush.

type BelongsTo

type BelongsTo[C, P any] struct {
	// contains filtered or unexported fields
}

BelongsTo — many-to-one relation descriptor (child → parent). Accessor functions are generated by `sorm gen`.

func NewBelongsTo

func NewBelongsTo[C, P any](
	fkCol string,
	childFK func(*C) any,
	setParent func(*C, *P),
) BelongsTo[C, P]

func (BelongsTo[C, P]) Include

func (r BelongsTo[C, P]) Include(opts ...ChildOpt[P]) IncludeSpec[C]

Include — eager loading of the parent: one query WHERE pk IN (children's fks) and distribution to children. Pred[P] options filter the parents; a child whose parent is filtered out keeps a nil navigation. IncludeSpec[P] options — nested loading of the parent's relations.

func (BelongsTo[C, P]) Is

func (r BelongsTo[C, P]) Is(preds ...Pred[P]) Pred[C]

Is filters the CHILD by parent attributes: EXISTS (SELECT 1 FROM parents WHERE parents.pk = children.fk AND preds).

type BytesCol

type BytesCol[E any] struct {
	// contains filtered or unexported fields
}

BytesCol: []byte is not comparable, hence a separate descriptor without In (SQL `= $1` on bytea is valid; IN is semantically unnecessary).

func NewBytesCol

func NewBytesCol[E any](table, name string) BytesCol[E]

func (BytesCol[E]) ColName

func (c BytesCol[E]) ColName() string

func (BytesCol[E]) Eq

func (c BytesCol[E]) Eq(v []byte) Pred[E]

func (BytesCol[E]) IsNotNull

func (c BytesCol[E]) IsNotNull() Pred[E]

func (BytesCol[E]) IsNull

func (c BytesCol[E]) IsNull() Pred[E]

func (BytesCol[E]) Neq

func (c BytesCol[E]) Neq(v []byte) Pred[E]

func (BytesCol[E]) Set

func (c BytesCol[E]) Set(v []byte) Assign[E]

func (BytesCol[E]) SetNull

func (c BytesCol[E]) SetNull() Assign[E]

SetNull assigns SQL NULL.

type ChildOpt

type ChildOpt[C any] interface {
	// contains filtered or unexported methods
}

ChildOpt — an Include option: a child predicate (Pred[C]), child ordering (Order[C]), or a nested spec (IncludeSpec[C] — analogous to ThenInclude):

q.With(u.Posts.Include(
    p.Title.HasPrefix("Go"),      // child filter
    p.CreatedAt.Desc(),           // child order
    p.Comments.Include(),         // nested loading
))

type Col

type Col[E any, V comparable] struct {
	// contains filtered or unexported fields
}

Col is a column descriptor with equality comparisons. The descriptor type (Col/OrdCol/StrCol/BytesCol) is chosen by the generator based on the field's Go type. For nullable fields (*string etc.) the descriptor is generated for the base type plus IsNull/IsNotNull — predicates do not require pointers.

Eq(false) and Eq(0) are full-fledged conditions: predicates have no notion of a zero value.

func NewCol

func NewCol[E any, V comparable](table, name string) Col[E, V]

func (Col[E, V]) Asc

func (c Col[E, V]) Asc() Order[E]

func (Col[E, V]) ColName

func (c Col[E, V]) ColName() string

ColName is the column name in the database.

func (Col[E, V]) Desc

func (c Col[E, V]) Desc() Order[E]

func (Col[E, V]) Eq

func (c Col[E, V]) Eq(v V) Pred[E]

func (Col[E, V]) In

func (c Col[E, V]) In(vs ...V) Pred[E]

func (Col[E, V]) IsNotNull

func (c Col[E, V]) IsNotNull() Pred[E]

func (Col[E, V]) IsNull

func (c Col[E, V]) IsNull() Pred[E]

func (Col[E, V]) Neq

func (c Col[E, V]) Neq(v V) Pred[E]

func (Col[E, V]) NotIn

func (c Col[E, V]) NotIn(vs ...V) Pred[E]

func (Col[E, V]) Set

func (c Col[E, V]) Set(v V) Assign[E]

Set is a typed assignment for set-based Update.

func (Col[E, V]) SetNull

func (c Col[E, V]) SetNull() Assign[E]

SetNull assigns SQL NULL (for nullable columns; a NOT NULL column will come back as a typed ConstraintError from the database).

type ColOf

type ColOf[E any] interface {
	AnyCol
	// contains filtered or unexported methods
}

ColOf is a column descriptor of exactly entity E (for Field/GroupBy on the root table — with type inference).

type ColOfV

type ColOfV[E any, V comparable] interface {
	ColOf[E]
	// contains filtered or unexported methods
}

ColOfV is a column descriptor of E with a value type (for type-safe ColEq).

type ColV

type ColV[V comparable] interface {
	AnyCol
	// contains filtered or unexported methods
}

ColV is a column of any entity with a known value type (aggregates and ColEq stay typed over V).

type ColumnDef

type ColumnDef struct {
	Name     string
	GoKind   string // "bool","string","int*","uint*","float32/64","time","bytes"
	Nullable bool
	Unique   bool
	PK       bool
	Auto     bool
	SQLType  string // override from the type: tag
	RefTable string // FK: target table
	RefCol   string // FK: target column
}

ColumnDef — a column description.

type ConflictError

type ConflictError struct {
	Table string
	PK    any
}

ConflictError — optimistic concurrency: an UPDATE/DELETE affected 0 rows, meaning the row was changed or deleted concurrently after loading.

func (*ConflictError) Error

func (e *ConflictError) Error() string

type ConstraintError

type ConstraintError struct {
	Kind       ConstraintKind
	Constraint string // constraint/column name, if the driver reported it
	Err        error
}

ConstraintError — a DB constraint violation translated by the driver adapter into a typed error: a handler can tell "email taken" (409) from a failure (500) without parsing driver codes.

var ce *sorm.ConstraintError
if errors.As(err, &ce) && ce.Kind == sorm.ConstraintUnique { ... }

func (*ConstraintError) Error

func (e *ConstraintError) Error() string

func (*ConstraintError) Unwrap

func (e *ConstraintError) Unwrap() error

type ConstraintKind

type ConstraintKind int

ConstraintKind — the kind of violated DB constraint.

const (
	ConstraintUnique ConstraintKind = iota + 1
	ConstraintForeignKey
	ConstraintNotNull
	ConstraintCheck
)

func (ConstraintKind) String

func (k ConstraintKind) String() string

type DB

type DB interface {
	Dialect() dialect.Dialect
	Query(ctx context.Context, sql string, args ...any) (Rows, error)
	// Exec returns the number of affected rows.
	Exec(ctx context.Context, sql string, args ...any) (int64, error)
	// ExecBatch executes statements in order: pgx — in one roundtrip
	// (pgx.Batch), database/sql — sequentially on the current connection.
	ExecBatch(ctx context.Context, items []BatchItem) error
	Begin(ctx context.Context) (Tx, error)
}

DB — sorm's driver abstraction. The runtime does not depend on a specific driver: the adapters are sorm/driver/pgxd (pgx, PostgreSQL) and sorm/driver/sqld (database/sql: MySQL, SQLite).

func InSchema

func InSchema(db DB, schema string) DB

InSchema binds a connection to a database schema: every table rendered by sorm becomes schema-qualified ("billing"."orders"). Models stay schema-agnostic — the same entities can live in different schemas via different wrappers over one pool (per-schema multi-tenancy):

db := sorm.InSchema(pgxd.Wrap(pool), "billing")
c  := sormgen.NewContext(db)   // the context inherits the schema

On MySQL a "schema" is a database name (`billing`.`orders`); on SQLite it is an attached database name. Raw/RawAs SQL is the caller's text and is not rewritten. For migrations see migrate.WithSchema.

func Instrument

func Instrument(db DB, fn InstrumentFunc) DB

Instrument wraps a DB with a middleware function. Works with any adapter; transactions are instrumented with the same fn.

db = sorm.Instrument(db, func(ctx context.Context, op sorm.Op, next func(context.Context) error) error {
    start := time.Now()
    err := next(ctx)
    slog.Info("sql", "kind", op.Kind, "sql", op.SQL, "dur", time.Since(start), "err", err)
    return err
})

func Primary added in v0.5.0

func Primary(db DB) DB

Primary returns the write connection behind db: unwraps WithReplicas through any wrapper stack; plain connections come back unchanged.

func Replica added in v0.5.0

func Replica(db DB) DB

Replica returns a read replica behind db (round-robin); plain connections come back unchanged.

func WithReplicas added in v0.5.0

func WithReplicas(primary DB, replicas ...DB) DB

WithReplicas splits reads and writes: untracked SELECTs go to the replicas round-robin, everything else stays on the primary.

db := sorm.WithReplicas(pgxd.Wrap(primary),
    pgxd.Wrap(replica1), pgxd.Wrap(replica2))

Routing rules:

  • Query (untracked reads) → next replica
  • Exec / ExecBatch / Begin / RunInTx → primary
  • Sessions and generated Contexts → primary entirely (read-your- writes: a tracked snapshot from a lagging replica would produce stale diffs and false version conflicts)
  • ForUpdate / ForUpdateSkipLocked → primary (locks on a replica are meaningless)

Explicit overrides: sorm.Primary(db) pins the primary for a single query, sorm.Replica(db) pins a replica (both are no-ops for plain connections). Health checking and failover are the pool's job — the resolver only routes.

Composes with other wrappers; recommended order is instrumentation outside, InSchema in the middle, the resolver inside.

type DeleteBuilder

type DeleteBuilder[E any] struct {
	// contains filtered or unexported fields
}

func Delete

func Delete[E any](db DB) DeleteBuilder[E]

Delete is a set-based DELETE. Same rules: without Where, AllRows() is required.

func (DeleteBuilder[E]) AllRows

func (q DeleteBuilder[E]) AllRows() DeleteBuilder[E]

func (DeleteBuilder[E]) Exec

func (q DeleteBuilder[E]) Exec(ctx context.Context) (int64, error)

func (DeleteBuilder[E]) Hard added in v0.4.0

func (q DeleteBuilder[E]) Hard() DeleteBuilder[E]

Hard forces a real DELETE for soft-delete entities: the rows are gone, including already soft-deleted ones matching the predicates (purge).

func (DeleteBuilder[E]) Named

func (q DeleteBuilder[E]) Named(name string) DeleteBuilder[E]

Named labels the statement for instrumentation (sorm.query.name).

func (DeleteBuilder[E]) ToSQL

func (q DeleteBuilder[E]) ToSQL() (string, []any, error)

func (DeleteBuilder[E]) Where

func (q DeleteBuilder[E]) Where(ps ...Pred[E]) DeleteBuilder[E]

type FromBuilder

type FromBuilder[E any] struct {
	// contains filtered or unexported fields
}

func From

func From[E any](db DB) FromBuilder[E]

From starts a projection query from the table of entity E.

func (FromBuilder[E]) GroupBy

func (q FromBuilder[E]) GroupBy(cols ...ColOf[E]) FromBuilder[E]

func (FromBuilder[E]) Having

func (q FromBuilder[E]) Having(ps ...Pred[E]) FromBuilder[E]

func (FromBuilder[E]) Join

func (q FromBuilder[E]) Join(specs ...JoinSpec[E]) FromBuilder[E]

Join adds JOIN specifications (created by relation methods or by the free functions LeftJoinOn/InnerJoinOn/CrossJoin).

func (FromBuilder[E]) Limit

func (q FromBuilder[E]) Limit(n int) FromBuilder[E]

func (FromBuilder[E]) Named

func (q FromBuilder[E]) Named(name string) FromBuilder[E]

Named labels the projection for instrumentation (sorm.query.name).

func (FromBuilder[E]) Offset

func (q FromBuilder[E]) Offset(n int) FromBuilder[E]

func (FromBuilder[E]) OrderBy

func (q FromBuilder[E]) OrderBy(os ...Order[E]) FromBuilder[E]

func (FromBuilder[E]) Where

func (q FromBuilder[E]) Where(ps ...Pred[E]) FromBuilder[E]

func (FromBuilder[E]) WithDeleted added in v0.4.0

func (q FromBuilder[E]) WithDeleted() FromBuilder[E]

WithDeleted disables the ROOT table soft-delete filter. Joined tables are not filtered implicitly — add explicit ON/WHERE predicates there.

type HasMany

type HasMany[E, C any] struct {
	// contains filtered or unexported fields
}

HasMany — one-to-many relation descriptor. It knows both types, so every "two-type" operation (Include, Any) is a method on it: builder methods cannot introduce new type parameters.

Accessor functions are generated by `sorm gen` — the runtime uses no reflection.

func NewHasMany

func NewHasMany[E, C any](
	fkCol string,
	parentKey func(*E) any,
	childKey func(*C) any,
	initSlice func(*E),
	appendChild func(*E, *C),
) HasMany[E, C]

func (HasMany[E, C]) Any

func (r HasMany[E, C]) Any(preds ...Pred[C]) Pred[E]

Any filters the PARENT by its children: EXISTS (SELECT 1 FROM children WHERE fk = parent.pk AND preds). Returns a Pred[E]; feed it into a regular Where.

func (HasMany[E, C]) Include

func (r HasMany[E, C]) Include(opts ...ChildOpt[C]) IncludeSpec[E]

Include — eager loading of children (split strategy: a separate query WHERE fk IN (pks) and in-memory distribution to parents). Options: Pred[C] (child filter), Order[C] (their order), IncludeSpec[C] (nested loading — analogous to ThenInclude).

func (HasMany[E, C]) InnerJoin

func (r HasMany[E, C]) InnerJoin(preds ...Pred[C]) JoinSpec[E]

InnerJoin is an INNER JOIN of the child table over the relation's FK.

func (HasMany[E, C]) LeftJoin

func (r HasMany[E, C]) LeftJoin(preds ...Pred[C]) JoinSpec[E]

LeftJoin is a LEFT JOIN of the child table over the relation's FK; preds are added to ON.

func (HasMany[E, C]) None

func (r HasMany[E, C]) None(preds ...Pred[C]) Pred[E]

None — NOT EXISTS: parents that have no matching children.

type HasOne

type HasOne[E, C any] struct {
	// contains filtered or unexported fields
}

HasOne — one-to-one relation (FK on the child side C, *C navigation on E). A missing child after Include is indistinguishable from "not loaded" (the pointer stays nil) — unlike hasMany, where an empty slice ≠ nil.

func NewHasOne

func NewHasOne[E, C any](
	fkCol string,
	parentKey func(*E) any,
	childKey func(*C) any,
	setChild func(*E, *C),
) HasOne[E, C]

func (HasOne[E, C]) Any

func (r HasOne[E, C]) Any(preds ...Pred[C]) Pred[E]

Any filters the parent by the child (EXISTS); None — absence of a child.

func (HasOne[E, C]) Include

func (r HasOne[E, C]) Include(opts ...ChildOpt[C]) IncludeSpec[E]

Include — eager loading of the single child (WHERE fk IN (pks)).

func (HasOne[E, C]) None

func (r HasOne[E, C]) None(preds ...Pred[C]) Pred[E]

type IncludeSpec

type IncludeSpec[E any] struct {
	// contains filtered or unexported fields
}

IncludeSpec — an eager loading specification closed over the parent entity; created by relation descriptor methods, executed by the builder.

type IndexDef

type IndexDef struct {
	Name    string
	Columns []string    // simple ASC columns (equivalent to Parts without Desc/Expr)
	Parts   []IndexPart // extended form: sort order and expressions
	Unique  bool
	Type    string // index type: "gin"/"brin" (PG, USING), "fulltext" (MySQL)
	Where   string // partial index (PG, SQLite); raw SQL condition
}

IndexDef — a table index. Simple (including composite) indexes are declared with `index:`/`uniqueIndex:` tags; custom ones via an optional model method:

func (Post) Indexes() []sorm.IndexDef {
    return []sorm.IndexDef{
        {Name: "idx_posts_fts", Type: "gin",
         Parts: []sorm.IndexPart{{Expr: "to_tsvector('russian', title)"}}},
        {Name: "idx_posts_recent",
         Parts: []sorm.IndexPart{{Column: "created_at", Desc: true}},
         Where: "views > 0"},
    }
}

`sorm gen` merges the tags and the method into the TableDef.

func (IndexDef) IndexParts

func (ix IndexDef) IndexParts() []IndexPart

IndexParts — the normalized list of index elements (for generators).

type IndexPart

type IndexPart struct {
	Column string // column name (mutually exclusive with Expr)
	Expr   string // raw expression: to_tsvector(...), lower(email), ...
	Desc   bool
}

IndexPart — an index element: a column or an expression.

type InstrumentFunc

type InstrumentFunc func(ctx context.Context, op Op, next func(ctx context.Context) error) error

InstrumentFunc wraps every DB operation: logging, metrics, tracing (an OpenTelemetry span around next). It must call next exactly once and return its error (possibly wrapped).

type JSONArr

type JSONArr[E any] struct {
	// contains filtered or unexported fields
}

JSONArr is a typed accessor for an array JSON field.

func NewJSONArr

func NewJSONArr[E any](table, col, path string) JSONArr[E]

func (JSONArr[E]) Contains

func (j JSONArr[E]) Contains(v any) Pred[E]

Contains reports whether the array contains v (an element or a sub-array). PostgreSQL and MySQL; a build error on SQLite.

func (JSONArr[E]) IsNotNull

func (j JSONArr[E]) IsNotNull() Pred[E]

func (JSONArr[E]) IsNull

func (j JSONArr[E]) IsNull() Pred[E]

type JSONBool

type JSONBool[E any] struct {
	// contains filtered or unexported fields
}

JSONBool is a typed accessor for a boolean JSON field.

func NewJSONBool

func NewJSONBool[E any](table, col, path string) JSONBool[E]

func (JSONBool[E]) Eq

func (j JSONBool[E]) Eq(v bool) Pred[E]

func (JSONBool[E]) IsFalse

func (j JSONBool[E]) IsFalse() Pred[E]

func (JSONBool[E]) IsNotNull

func (j JSONBool[E]) IsNotNull() Pred[E]

func (JSONBool[E]) IsNull

func (j JSONBool[E]) IsNull() Pred[E]

func (JSONBool[E]) IsTrue

func (j JSONBool[E]) IsTrue() Pred[E]

type JSONCol

type JSONCol[E any] struct {
	// contains filtered or unexported fields
}

JSONCol is the descriptor of a JSON column with content predicates. Dialect support: Path works everywhere; Contains — PostgreSQL (@>) and MySQL (JSON_CONTAINS), a build error on SQLite; HasKey works everywhere.

func NewJSONCol

func NewJSONCol[E any](table, name string) JSONCol[E]

func (JSONCol[E]) ColName

func (c JSONCol[E]) ColName() string

func (JSONCol[E]) Contains

func (c JSONCol[E]) Contains(v any) Pred[E]

Contains reports whether the column's JSON document contains v (PostgreSQL @>, MySQL JSON_CONTAINS). Not available on SQLite.

func (JSONCol[E]) HasKey

func (c JSONCol[E]) HasKey(key string) Pred[E]

HasKey reports whether the top-level document has the given key.

func (JSONCol[E]) IsNotNull

func (c JSONCol[E]) IsNotNull() Pred[E]

func (JSONCol[E]) IsNull

func (c JSONCol[E]) IsNull() Pred[E]

func (JSONCol[E]) Path

func (c JSONCol[E]) Path(path string) JSONPath[E]

Path addresses a nested value with dot notation ("a.b.c"). Extraction is textual: comparisons behave like string comparisons of the unquoted value. Segments must match [A-Za-z0-9_]+ — anything else is a build error surfaced when the query executes.

func (JSONCol[E]) Set

func (c JSONCol[E]) Set(v any) Assign[E]

Set is a typed assignment for set-based Update (the value is marshaled).

func (JSONCol[E]) SetNull

func (c JSONCol[E]) SetNull() Assign[E]

SetNull assigns SQL NULL.

type JSONNum

type JSONNum[E any, V int64 | float64] struct {
	// contains filtered or unexported fields
}

JSONNum is a typed accessor for a numeric JSON field. Comparisons are numeric on every dialect: PG casts the text extraction to numeric, MySQL and SQLite compare the extracted value natively.

func NewJSONNum

func NewJSONNum[E any, V int64 | float64](table, col, path string) JSONNum[E, V]

func (JSONNum[E, V]) Eq

func (j JSONNum[E, V]) Eq(v V) Pred[E]

func (JSONNum[E, V]) Gt

func (j JSONNum[E, V]) Gt(v V) Pred[E]

func (JSONNum[E, V]) Gte

func (j JSONNum[E, V]) Gte(v V) Pred[E]

func (JSONNum[E, V]) IsNotNull

func (j JSONNum[E, V]) IsNotNull() Pred[E]

func (JSONNum[E, V]) IsNull

func (j JSONNum[E, V]) IsNull() Pred[E]

func (JSONNum[E, V]) Lt

func (j JSONNum[E, V]) Lt(v V) Pred[E]

func (JSONNum[E, V]) Lte

func (j JSONNum[E, V]) Lte(v V) Pred[E]

func (JSONNum[E, V]) Neq

func (j JSONNum[E, V]) Neq(v V) Pred[E]

type JSONPath

type JSONPath[E any] struct {
	// contains filtered or unexported fields
}

JSONPath is a dot-notation path into a JSON column.

func (JSONPath[E]) Eq

func (p JSONPath[E]) Eq(v string) Pred[E]

func (JSONPath[E]) In

func (p JSONPath[E]) In(vs ...string) Pred[E]

func (JSONPath[E]) IsNotNull

func (p JSONPath[E]) IsNotNull() Pred[E]

func (JSONPath[E]) IsNull

func (p JSONPath[E]) IsNull() Pred[E]

IsNull is true when the path is absent or holds JSON null.

func (JSONPath[E]) Neq

func (p JSONPath[E]) Neq(v string) Pred[E]

type JSONStr

type JSONStr[E any] struct {
	// contains filtered or unexported fields
}

JSONStr is a typed accessor for a string JSON field.

func NewJSONStr

func NewJSONStr[E any](table, col, path string) JSONStr[E]

func (JSONStr[E]) Eq

func (j JSONStr[E]) Eq(v string) Pred[E]

func (JSONStr[E]) In

func (j JSONStr[E]) In(vs ...string) Pred[E]

func (JSONStr[E]) IsNotNull

func (j JSONStr[E]) IsNotNull() Pred[E]

func (JSONStr[E]) IsNull

func (j JSONStr[E]) IsNull() Pred[E]

func (JSONStr[E]) Like

func (j JSONStr[E]) Like(p string) Pred[E]

func (JSONStr[E]) Neq

func (j JSONStr[E]) Neq(v string) Pred[E]

type JoinOn

type JoinOn[C, E any] struct {
	// contains filtered or unexported fields
}

JoinOn is the join condition between C (being joined) and E (already in the query). The column value types must match — enforced by the compiler.

func ColEq

func ColEq[C, E any, V comparable](joined ColOfV[C, V], existing ColOfV[E, V]) JoinOn[C, E]

type JoinSpec

type JoinSpec[E any] struct {
	// contains filtered or unexported fields
}

func CrossJoin

func CrossJoin[C, E any]() JoinSpec[E]

CrossJoin is a Cartesian product with entity C's table.

func InnerJoinOn

func InnerJoinOn[C, E any](on JoinOn[C, E], preds ...Pred[C]) JoinSpec[E]

func LeftJoinOn

func LeftJoinOn[C, E any](on JoinOn[C, E], preds ...Pred[C]) JoinSpec[E]

LeftJoinOn / InnerJoinOn is an arbitrary JOIN of entity C's table; preds on C are added to ON.

type ManyToMany

type ManyToMany[E, C any] struct {
	// contains filtered or unexported fields
}

ManyToMany — many-to-many relation through an implicit join table (`many2many:join_table` tag). Reads via Include/Any; writes via explicit Link/Unlink: sorm does not guess collection diffs, linking is an operation.

func NewManyToMany

func NewManyToMany[E, C any](
	joinTable, parentCol, childCol string,
	initSlice func(*E),
	appendChild func(*E, *C),
) ManyToMany[E, C]

func (ManyToMany[E, C]) Any

func (r ManyToMany[E, C]) Any(preds ...Pred[C]) Pred[E]

Any filters E by related C: EXISTS over the join table; preds narrow C with a correlated subquery.

func (ManyToMany[E, C]) Include

func (r ManyToMany[E, C]) Include(opts ...ChildOpt[C]) IncludeSpec[E]

Include — eager loading of related C: pairs from the join table + loading children with a single IN query (chunked), then distribution to parents. Order[C] options set the order of children within each parent.

func (r ManyToMany[E, C]) Link(ctx context.Context, db DB, parent *E, children ...*C) error

Link associates parent with children (INSERT into the join table). Both sides must be persisted. Linking twice yields a ConstraintError (the join table's composite PK).

func (r ManyToMany[E, C]) Unlink(ctx context.Context, db DB, parent *E, children ...*C) error

Unlink breaks the links between parent and children (DELETE from the join table).

type Meta

type Meta[E any] struct {
	Table      string
	PK         string // PK column name
	Auto       bool   // PK is DB-generated (identity) → INSERT ... RETURNING
	VersionCol string // "" — no optimistic concurrency

	SelectCols []string // order matches Scan
	InsertCols []string // without auto-PK; order matches InsertValues

	// Scan returns pointers to fields in SelectCols order.
	Scan func(*E) []any
	// InsertValues returns values in InsertCols order.
	InsertValues func(*E) []any
	// ValuesFor returns column values by SelectCols indexes (bridge diff → UPDATE).
	ValuesFor func(*E, []int) []any
	// Snapshot/Diff — snapshot tracking (PR #4). The snapshot is a typed
	// generated struct, boxed at the meta boundary.
	Snapshot func(*E) any
	Diff     func(any, *E) []int
	// SetPK sets the auto-PK after RETURNING.
	SetPK func(*E, int64)
	// PKValue — the PK value (identity map key).
	PKValue func(*E) any
	// GetVersion/SetVersion — only when VersionCol != "".
	GetVersion func(*E) int64
	SetVersion func(*E, int64)
	// SoftDeleteCol — the column of `sorm:"softDelete"` (a *time.Time);
	// "" disables soft deletion. When set: queries filter the column
	// IS NULL, session Remove and set-based Delete become UPDATEs.
	SoftDeleteCol string
	// SetDeleted stamps the soft-delete field (only when SoftDeleteCol != "").
	SetDeleted func(*E, time.Time)
	// TouchCreate/TouchUpdate — auto-timestamps (sorm:"autoCreate"/"autoUpdate").
	// Nil when the entity has no such fields. TouchCreate stamps zero-valued
	// autoCreate fields on INSERT (a manually set value wins); TouchUpdate
	// stamps autoUpdate fields on INSERT and on every effective UPDATE.
	TouchCreate func(*E, time.Time)
	TouchUpdate func(*E, time.Time)
	// Refs — belongsTo navigations of this entity: edges for per-instance
	// toposort and FK fixup of new graphs.
	Refs []Ref[E]
	// RefTables — tables referenced by FKs (deletion order).
	RefTables []string
}

Meta describes an entity to the runtime. In production code it is generated by `sorm gen`; handwritten meta is fine for tests and transition periods. All functions are per-field, no reflection — the only runtime reflection is a single type lookup in the registry when building a query.

func MetaOf

func MetaOf[E any]() *Meta[E]

MetaOf returns the registered meta of an entity — for tests and advanced scenarios (instrumentation, custom executors). Panics if no meta is registered.

type Op

type Op struct {
	Kind string // "query" | "exec" | "batch" | "begin" | "commit" | "rollback"
	SQL  string // query text ("" for begin/commit/rollback; first statement for batch)
	Args []any
	// Statements — all statements of the batch (Kind == "batch").
	Statements []string
}

Op — an operation description for instrumentation.

type OrdCol

type OrdCol[E any, V comparable] struct{ Col[E, V] }

OrdCol adds ordered comparisons. The constraint is comparable, not cmp.Ordered: time.Time is ordered in SQL but not Ordered in Go; the generator guarantees correct selection (OrdCol only for numbers, strings, and time.Time).

func NewOrdCol

func NewOrdCol[E any, V comparable](table, name string) OrdCol[E, V]

func (OrdCol[E, V]) Between

func (c OrdCol[E, V]) Between(lo, hi V) Pred[E]

func (OrdCol[E, V]) Gt

func (c OrdCol[E, V]) Gt(v V) Pred[E]

func (OrdCol[E, V]) Gte

func (c OrdCol[E, V]) Gte(v V) Pred[E]

func (OrdCol[E, V]) Lt

func (c OrdCol[E, V]) Lt(v V) Pred[E]

func (OrdCol[E, V]) Lte

func (c OrdCol[E, V]) Lte(v V) Pred[E]

type Order

type Order[E any] struct {
	// contains filtered or unexported fields
}

Order is a sort element, closed over the entity.

type Pred

type Pred[E any] struct {
	// contains filtered or unexported fields
}

Pred is an immutable condition parameterized by the entity: a condition on one entity cannot be passed into a query on another (compile-time error). agg=true means the condition contains an aggregate and is only valid in Having.

func And

func And[E any](ps ...Pred[E]) Pred[E]

And combines conditions with a conjunction. And() with no arguments is TRUE.

func EqQ added in v0.4.0

func EqQ[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

Scalar comparisons: col <op> (SELECT ...). The subquery must yield a single row (usually an aggregate) — more rows is a database error.

func GtQ added in v0.4.0

func GtQ[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

func GteQ added in v0.4.0

func GteQ[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

func InQuery added in v0.4.0

func InQuery[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

InQuery — col IN (SELECT ...). Value types must match.

func LtQ added in v0.4.0

func LtQ[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

func LteQ added in v0.4.0

func LteQ[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

func NeqQ added in v0.4.0

func NeqQ[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

func Not

func Not[E any](p Pred[E]) Pred[E]

func NotInQuery added in v0.4.0

func NotInQuery[E any, V comparable](c ColOfV[E, V], sub SubQ[V]) Pred[E]

NotInQuery — col NOT IN (SELECT ...). Beware NULLs in the subquery: SQL's NOT IN over a set containing NULL matches nothing.

func Or

func Or[E any](ps ...Pred[E]) Pred[E]

Or combines conditions with a disjunction. Or() with no arguments is FALSE.

type ProjQuery

type ProjQuery[R any] struct {
	// contains filtered or unexported fields
}

func Project

func Project[R any, E any](q FromBuilder[E], exprs ...SelectExpr[E]) ProjQuery[R]

Project performs a projection into struct R. The match between expression aliases and R's fields (`sorm:` tag or snake_case) is checked strictly before the query.

func (ProjQuery[R]) All

func (q ProjQuery[R]) All(ctx context.Context) ([]*R, error)

func (ProjQuery[R]) One

func (q ProjQuery[R]) One(ctx context.Context) (*R, error)

func (ProjQuery[R]) ToSQL

func (q ProjQuery[R]) ToSQL() (string, []any, error)

type QueryBuilder

type QueryBuilder[E any] struct {
	// contains filtered or unexported fields
}

func Query

func Query[E any](db DB) QueryBuilder[E]

Query starts a typed query over entity E. The builder is immutable: every method returns a copy, so reusing a base builder is safe (no state leaks between queries).

func Track

func Track[E any](s *Session) QueryBuilder[E]

Track is the same query builder, but materialized entities go into the tracker. Reloading the same row returns the already-tracked pointer; database data does not overwrite local changes (EF semantics).

func (QueryBuilder[E]) All

func (q QueryBuilder[E]) All(ctx context.Context) ([]*E, error)

All runs the query without tracking. An empty result is an empty slice, nil error.

func (QueryBuilder[E]) Count

func (q QueryBuilder[E]) Count(ctx context.Context) (int64, error)

func (QueryBuilder[E]) ForUpdate added in v0.4.0

func (q QueryBuilder[E]) ForUpdate() QueryBuilder[E]

ForUpdate locks the selected rows until the transaction ends (SELECT ... FOR UPDATE). Meaningful inside RunInTx / an open Tx; PostgreSQL and MySQL only — building on SQLite is an error.

func (QueryBuilder[E]) ForUpdateSkipLocked added in v0.4.0

func (q QueryBuilder[E]) ForUpdateSkipLocked() QueryBuilder[E]

ForUpdateSkipLocked locks the selected rows, silently skipping rows already locked by other transactions — the standard queue-worker pattern (PostgreSQL / MySQL 8+).

func (QueryBuilder[E]) Iter

func (q QueryBuilder[E]) Iter(ctx context.Context) iter.Seq2[*E, error]

Iter streams the result: rows are yielded as they are read, without loading the whole result set into memory. Incompatible with With (eager loading requires the full set of parents) — in that case the iterator yields a single error.

for u, err := range sorm.Query[models.User](db).Iter(ctx) {
    if err != nil { return err }
    ...
}

func (QueryBuilder[E]) Limit

func (q QueryBuilder[E]) Limit(n int) QueryBuilder[E]

func (QueryBuilder[E]) Named

func (q QueryBuilder[E]) Named(name string) QueryBuilder[E]

Named labels the query for instrumentation: spans and metrics carry it as sorm.query.name. Equivalent to wrapping the context in WithQueryName (an explicit WithQueryName on the context wins over Named).

func (QueryBuilder[E]) Offset

func (q QueryBuilder[E]) Offset(n int) QueryBuilder[E]

func (QueryBuilder[E]) One

func (q QueryBuilder[E]) One(ctx context.Context) (*E, error)

One returns the first row or ErrNotFound.

func (QueryBuilder[E]) OnlyDeleted added in v0.4.0

func (q QueryBuilder[E]) OnlyDeleted() QueryBuilder[E]

OnlyDeleted returns exclusively soft-deleted rows (a trash-bin view).

func (QueryBuilder[E]) OrderBy

func (q QueryBuilder[E]) OrderBy(os ...Order[E]) QueryBuilder[E]

func (QueryBuilder[E]) ToSQL

func (q QueryBuilder[E]) ToSQL() (string, []any)

ToSQL returns the final SQL and arguments — inspection instead of magic.

func (QueryBuilder[E]) Where

func (q QueryBuilder[E]) Where(ps ...Pred[E]) QueryBuilder[E]

Where adds conditions; multiple Where calls and multiple arguments are ANDed.

func (QueryBuilder[E]) With

func (q QueryBuilder[E]) With(specs ...IncludeSpec[E]) QueryBuilder[E]

With adds eager loading of relations (specs are created by the Include method on relation descriptors: gen.User.Posts.Include(...)).

func (QueryBuilder[E]) WithDeleted added in v0.4.0

func (q QueryBuilder[E]) WithDeleted() QueryBuilder[E]

WithDeleted disables the soft-delete filter: the query sees every row, deleted or not. No-op for entities without a softDelete field.

type RawQuery

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

func Raw

func Raw[E any](db DB, sql string, args ...any) RawQuery[E]

Raw is the escape hatch into raw SQL with scanning into entities via meta. Matching is strict by column name: any mismatch yields a *ScanError with the lists, not silently skipped fields.

func RawAs

func RawAs[R any](db DB, sql string, args ...any) RawQuery[R]

RawAs is raw SQL with scanning into an arbitrary struct R (aggregates, CTEs, window functions — anything that is not the shape of an entity). Mapping: the `sorm:"col"` tag or snake_case of the field name. The scan plan is built once per type and cached.

func (RawQuery[T]) All

func (q RawQuery[T]) All(ctx context.Context) ([]*T, error)

func (RawQuery[T]) Named

func (q RawQuery[T]) Named(name string) RawQuery[T]

Named labels the query for instrumentation (sorm.query.name).

func (RawQuery[T]) One

func (q RawQuery[T]) One(ctx context.Context) (*T, error)

type Ref

type Ref[E any] struct {
	FKCol   string
	NotNull bool
	// Nav — pointer to the parent as any (nil-safe: no typed-nil leaks).
	Nav func(*E) any
	// NavPK — the parent's PK; valid only when Nav != nil.
	NavPK func(*E) any
	// SetFK sets the FK from the parent's PK (fixup after the parent is inserted).
	SetFK func(*E, any)
	// FKIsZero — the FK column was not set manually.
	FKIsZero func(*E) bool
}

Ref — a many-to-one navigation reference generated for an FK column.

type Rows

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

Rows — a minimal result cursor shared by pgx and database/sql.

type SaveOp added in v0.4.0

type SaveOp int

SaveOp tells a BeforeSave hook which operation is being planned.

const (
	SaveInsert SaveOp = iota
	SaveUpdate
	SaveDelete
)

func (SaveOp) String added in v0.4.0

func (o SaveOp) String() string

type ScalarCol

type ScalarCol[E any, V any] struct {
	// contains filtered or unexported fields
}

ScalarCol is the descriptor of a custom-scalar column: a named Go type implementing driver.Valuer + sql.Scanner (decimal.Decimal, money types, encrypted strings). V is intentionally unconstrained — the type does not have to be Go-comparable; every comparison happens in SQL, where the column's real type (numeric, ...) is ordered.

func NewScalarCol

func NewScalarCol[E any, V any](table, name string) ScalarCol[E, V]

func (ScalarCol[E, V]) Asc

func (c ScalarCol[E, V]) Asc() Order[E]

func (ScalarCol[E, V]) ColName

func (c ScalarCol[E, V]) ColName() string

func (ScalarCol[E, V]) Desc

func (c ScalarCol[E, V]) Desc() Order[E]

func (ScalarCol[E, V]) Eq

func (c ScalarCol[E, V]) Eq(v V) Pred[E]

func (ScalarCol[E, V]) Gt

func (c ScalarCol[E, V]) Gt(v V) Pred[E]

func (ScalarCol[E, V]) Gte

func (c ScalarCol[E, V]) Gte(v V) Pred[E]

func (ScalarCol[E, V]) In

func (c ScalarCol[E, V]) In(vs ...V) Pred[E]

func (ScalarCol[E, V]) IsNotNull

func (c ScalarCol[E, V]) IsNotNull() Pred[E]

func (ScalarCol[E, V]) IsNull

func (c ScalarCol[E, V]) IsNull() Pred[E]

func (ScalarCol[E, V]) Lt

func (c ScalarCol[E, V]) Lt(v V) Pred[E]

func (ScalarCol[E, V]) Lte

func (c ScalarCol[E, V]) Lte(v V) Pred[E]

func (ScalarCol[E, V]) Neq

func (c ScalarCol[E, V]) Neq(v V) Pred[E]

func (ScalarCol[E, V]) NotIn

func (c ScalarCol[E, V]) NotIn(vs ...V) Pred[E]

func (ScalarCol[E, V]) Set

func (c ScalarCol[E, V]) Set(v V) Assign[E]

func (ScalarCol[E, V]) SetNull

func (c ScalarCol[E, V]) SetNull() Assign[E]

type ScanError

type ScanError struct {
	Missing []string // result columns that have no destination
	Extra   []string // expected but absent from the result
}

ScanError — a mismatch between result columns and destination fields (Raw/RawAs/Project).

func (*ScanError) Error

func (e *ScanError) Error() string

type SelectExpr

type SelectExpr[E any] struct {
	// contains filtered or unexported fields
}

SelectExpr is a column of the projection result.

func As

func As[E any, V comparable](a AggExpr[E, V], alias string) SelectExpr[E]

As is an aggregate with an alias.

func Field

func Field[E any](c ColOf[E]) SelectExpr[E]

Field is a column of the root entity (E is inferred); the result name = the column name.

func FieldAs

func FieldAs[E any](c ColOf[E], alias string) SelectExpr[E]

FieldAs is a column with an alias (e.g. on a name collision after a JOIN).

func FieldOf

func FieldOf[E any](c AnyCol) SelectExpr[E]

FieldOf is a column of a joined entity ("relaxed mode", E is explicit; table membership is validated at build time).

func FieldOfAs

func FieldOfAs[E any](c AnyCol, alias string) SelectExpr[E]

FieldOfAs is the same with an alias.

type Session

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

Session — Unit of Work: identity map + snapshot change tracking. Entities loaded via Track are mutated with plain Go code; SaveChanges computes the minimal diff and applies it in batches within a single transaction.

Not thread-safe (like DbContext in EF); lives for one unit of work.

func NewSession

func NewSession(db DB) *Session

func (*Session) DB

func (s *Session) DB() DB

DB returns the connection or transaction the session runs on.

func (*Session) SaveChanges

func (s *Session) SaveChanges(ctx context.Context) error

SaveChanges opens a transaction, applies the diff, and commits. Order: DELETE (children before parents) → UPDATE (changed columns only) → INSERT by dependency level (RETURNING → FK fixup of children). DELETE+UPDATE go in one pgx.Batch (single roundtrip), each insert level in another.

func (*Session) SaveChangesTx

func (s *Session) SaveChangesTx(ctx context.Context, tx Tx) error

SaveChangesTx applies the diff inside an external transaction; commit is up to the caller. Note: tracker state is updated immediately after a successful flush.

type Set

type Set[E any] struct {
	// contains filtered or unexported fields
}

Set is typed access to one entity within a unit of work — the EF Core DbSet analog. Queries started from a set are tracked: materialized entities land in the session's identity map, plain Go mutations are picked up by SaveChanges. The generated Context wires a set per entity.

c := sormgen.NewContext(db)
users, err := c.Users.Where(u.Age.Gt(18)).All(ctx)
users[0].Name = "new"                 // tracked — no manual Track
c.Orders.Add(&models.Order{...})
err = c.SaveChanges(ctx)

func NewSet

func NewSet[E any](s *Session) Set[E]

NewSet binds a set to a session. Normally called by the generated NewContext, not by hand.

func (Set[E]) Add

func (s Set[E]) Add(entities ...*E)

Add registers new entities for INSERT on the session.

func (Set[E]) All

func (s Set[E]) All(ctx context.Context) ([]*E, error)

func (Set[E]) Count

func (s Set[E]) Count(ctx context.Context) (int64, error)

func (Set[E]) Find

func (s Set[E]) Find(ctx context.Context, pk any) (*E, error)

Find returns the entity by primary key: first from the identity map (no SQL if already tracked — EF Find semantics), otherwise a tracked SELECT by PK. ErrNotFound if the row does not exist.

func (Set[E]) Iter

func (s Set[E]) Iter(ctx context.Context) iter.Seq2[*E, error]

func (Set[E]) Limit

func (s Set[E]) Limit(n int) QueryBuilder[E]

func (Set[E]) Named

func (s Set[E]) Named(name string) QueryBuilder[E]

func (Set[E]) NoTracking

func (s Set[E]) NoTracking() QueryBuilder[E]

NoTracking starts an untracked read-only query: no snapshots, no identity map — cheaper for large result sets you won't mutate.

func (Set[E]) OrderBy

func (s Set[E]) OrderBy(os ...Order[E]) QueryBuilder[E]

func (Set[E]) Query

func (s Set[E]) Query() QueryBuilder[E]

Query starts a tracked query (identity map, EF semantics: a reloaded row returns the already-tracked pointer, local changes win).

func (Set[E]) Remove

func (s Set[E]) Remove(entities ...*E)

Remove marks entities for DELETE on the session.

func (Set[E]) Where

func (s Set[E]) Where(ps ...Pred[E]) QueryBuilder[E]

Shortcuts to Query() so a set reads like a query root.

func (Set[E]) With

func (s Set[E]) With(specs ...IncludeSpec[E]) QueryBuilder[E]

type StrCol

type StrCol[E any] struct{ OrdCol[E, string] }

StrCol adds string predicates. HasPrefix/HasSuffix/Contains arguments are literals, LIKE special characters are escaped; Like/ILike take a ready-made pattern.

func NewStrCol

func NewStrCol[E any](table, name string) StrCol[E]

func (StrCol[E]) Contains

func (c StrCol[E]) Contains(s string) Pred[E]

func (StrCol[E]) HasPrefix

func (c StrCol[E]) HasPrefix(s string) Pred[E]

func (StrCol[E]) HasSuffix

func (c StrCol[E]) HasSuffix(s string) Pred[E]

func (StrCol[E]) ILike

func (c StrCol[E]) ILike(pattern string) Pred[E]

func (StrCol[E]) Like

func (c StrCol[E]) Like(pattern string) Pred[E]

type SubQ added in v0.4.0

type SubQ[V comparable] struct {
	// contains filtered or unexported fields
}

Typed subqueries. SubQ[V] is a subquery producing values of type V — the compiler will not let an int64 subquery meet a string column. Arguments of the outer query and the subquery share one placeholder sequence: everything renders into a single writer.

// users who authored a flagged post: id IN (SELECT author_id FROM posts WHERE ...)
flagged := sorm.Pick(p.AuthorID, sorm.Query[models.Post](nil).Where(p.Flagged.Eq(true)))
users, err := sorm.Query[models.User](db).Where(sorm.InQuery(u.ID, flagged)).All(ctx)

// balance above the average: balance > (SELECT avg(balance) FROM users)
avg := sorm.PickScalar(sorm.Avg[models.User](u.Balance), sorm.Query[models.User](nil))
rich, err := sorm.Query[models.User](db).Where(sorm.GtQ(u.Balance, avg)).All(ctx)

The inner builder is usually created with a nil db — rendering is dialect-driven by the OUTER query, so the same subquery value works on any connection. Correlated subqueries (referencing the outer row) are covered by relation predicates (Any/None/Is); Pick/PickScalar are for самостоятельные подзапросы.

func Pick added in v0.4.0

func Pick[E any, V comparable](c ColOfV[E, V], q QueryBuilder[E]) SubQ[V]

Pick selects a single typed column from a query — the IN-subquery shape. ORDER BY/LIMIT/OFFSET of the builder are preserved if set.

func PickScalar added in v0.4.0

func PickScalar[E any, V comparable](a AggExpr[E, V], q QueryBuilder[E]) SubQ[V]

PickScalar selects a single aggregate value — the scalar-subquery shape for comparisons: avg, max, count(*), ...

type TableDef

type TableDef struct {
	Name    string
	Columns []ColumnDef
	Indexes []IndexDef
}

TableDef — a dialect-neutral description of an entity's table. Generated by `sorm gen` and registered alongside the meta; used by migrations (sorm/migrate, `sorm schema`) as the source of the desired schema.

func Tables

func Tables() []TableDef

Tables returns all registered definitions (deterministic order).

type Tx

type Tx interface {
	DB
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

Tx — a transaction; implements DB (queries/batches inside the transaction).

type UpdateBuilder

type UpdateBuilder[E any] struct {
	// contains filtered or unexported fields
}

func Update

func Update[E any](db DB) UpdateBuilder[E]

Update is a set-based UPDATE without a session (analogous to ExecuteUpdate in EF Core). Type safety comes from descriptors: Set takes an Assign[E] from Col.Set(v). Set(false) and Set(0) are full-fledged assignments.

UPDATE without Where is an error unless explicitly allowed via AllRows() (anti-footgun). For versioned entities the version column is incremented automatically — open sessions will properly catch the conflict.

func (UpdateBuilder[E]) AllRows

func (q UpdateBuilder[E]) AllRows() UpdateBuilder[E]

AllRows explicitly allows an UPDATE over the whole table.

func (UpdateBuilder[E]) Exec

func (q UpdateBuilder[E]) Exec(ctx context.Context) (int64, error)

func (UpdateBuilder[E]) Named

func (q UpdateBuilder[E]) Named(name string) UpdateBuilder[E]

Named labels the statement for instrumentation (sorm.query.name).

func (UpdateBuilder[E]) Set

func (q UpdateBuilder[E]) Set(as ...Assign[E]) UpdateBuilder[E]

func (UpdateBuilder[E]) ToSQL

func (q UpdateBuilder[E]) ToSQL() (string, []any, error)

func (UpdateBuilder[E]) Where

func (q UpdateBuilder[E]) Where(ps ...Pred[E]) UpdateBuilder[E]

func (UpdateBuilder[E]) WithDeleted added in v0.4.0

func (q UpdateBuilder[E]) WithDeleted() UpdateBuilder[E]

WithDeleted lets the UPDATE touch soft-deleted rows too.

type UpsertBuilder added in v0.4.0

type UpsertBuilder[E any] struct {
	// contains filtered or unexported fields
}

func Upsert added in v0.4.0

func Upsert[E any](db DB) UpsertBuilder[E]

Upsert is a set-based INSERT ... ON CONFLICT (PostgreSQL/SQLite) / ON DUPLICATE KEY (MySQL):

n, err := sorm.Upsert[models.User](db).
    Rows(u1, u2).
    OnConflict(gen.User.Email).             // conflict target (PG/SQLite)
    DoUpdate(gen.User.Name, gen.User.Age).  // set these from the new values
    Exec(ctx)

Semantics and caveats:

  • OnConflict is required on PostgreSQL/SQLite and ignored on MySQL (MySQL fires on ANY unique key — that is the engine's rule).
  • DoUpdate columns are updated from the values being inserted (excluded.col / VALUES(col)); the version column of optimistic concurrency is bumped automatically on the update path.
  • DoNothing() skips conflicting rows instead of updating them.
  • autoCreate/autoUpdate timestamps are stamped on the inserted values.
  • Auto-generated PKs are NOT written back into the entities (unlike Session inserts) — this is a set-based statement; reload if you need the keys. Exec returns the driver-reported affected count, whose meaning differs per engine (MySQL counts an update as 2).

func (UpsertBuilder[E]) DoNothing added in v0.4.0

func (q UpsertBuilder[E]) DoNothing() UpsertBuilder[E]

DoNothing skips conflicting rows entirely.

func (UpsertBuilder[E]) DoUpdate added in v0.4.0

func (q UpsertBuilder[E]) DoUpdate(cols ...ColOf[E]) UpsertBuilder[E]

DoUpdate lists the columns to overwrite with the incoming values when the row already exists.

func (UpsertBuilder[E]) Exec added in v0.4.0

func (q UpsertBuilder[E]) Exec(ctx context.Context) (int64, error)

func (UpsertBuilder[E]) Named added in v0.4.0

func (q UpsertBuilder[E]) Named(name string) UpsertBuilder[E]

Named labels the statement for instrumentation (sorm.query.name).

func (UpsertBuilder[E]) OnConflict added in v0.4.0

func (q UpsertBuilder[E]) OnConflict(cols ...ColOf[E]) UpsertBuilder[E]

OnConflict sets the conflict target (a unique column or unique-index column set). Required on PostgreSQL/SQLite; ignored on MySQL.

func (UpsertBuilder[E]) Rows added in v0.4.0

func (q UpsertBuilder[E]) Rows(es ...*E) UpsertBuilder[E]

Rows adds entities to insert.

func (UpsertBuilder[E]) ToSQL added in v0.4.0

func (q UpsertBuilder[E]) ToSQL() (string, []any, error)

Directories

Path Synopsis
cmd
sorm command
Command sorm is the code generator and migration CLI.
Command sorm is the code generator and migration CLI.
Package dialect defines the minimal surface of differences between databases.
Package dialect defines the minimal surface of differences between databases.
lite
Package lite implements the SQLite dialect.
Package lite implements the SQLite dialect.
my
Package my implements the MySQL/MariaDB dialect.
Package my implements the MySQL/MariaDB dialect.
pg
Package pg implements the PostgreSQL dialect.
Package pg implements the PostgreSQL dialect.
driver
pgxd
Package pgxd is the pgx v5 adapter (PostgreSQL): one roundtrip per batch via pgx.Batch.
Package pgxd is the pgx v5 adapter (PostgreSQL): one roundtrip per batch via pgx.Batch.
sqld
Package sqld is the database/sql adapter (MySQL, SQLite).
Package sqld is the database/sql adapter (MySQL, SQLite).
examples
chat/cmd/chat command
Chat — the sorm showcase application: Echo + clean layering (transport → service → repository → sorm), everything in a dedicated "chat" database schema.
Chat — the sorm showcase application: Echo + clean layering (transport → service → repository → sorm), everything in a dedicated "chat" database schema.
chat/internal/config
Package config — typed application settings via sconf: YAML file + environment variables (CHAT_ prefix, __ as the section separator) + command-line flags, later sources override earlier ones.
Package config — typed application settings via sconf: YAML file + environment variables (CHAT_ prefix, __ as the section separator) + command-line flags, later sources override earlier ones.
chat/internal/models
Package models is the persistence model of the chat example.
Package models is the persistence model of the chat example.
chat/internal/repository
Package repository is the data-access layer.
Package repository is the data-access layer.
chat/internal/service
Package service is the business layer: it composes repositories, makes multi-write flows atomic with sorm.RunInTx (the audit entry commits or rolls back together with the action) and logs via srog.
Package service is the business layer: it composes repositories, makes multi-write flows atomic with sorm.RunInTx (the audit entry commits or rolls back together with the action) and logs via srog.
chat/internal/transport/httpapi
Package httpapi is the transport layer: Echo handlers over the chat service.
Package httpapi is the transport layer: Echo handlers over the chat service.
internal
codegen
Package codegen generates the sormgen package from the parsed schema: typed column descriptors and meta with per-field Scan/Snapshot/Diff (zero reflection at runtime) — compact and diff-friendly.
Package codegen generates the sormgen package from the parsed schema: typed column descriptors and meta with per-field Scan/Snapshot/Diff (zero reflection at runtime) — compact and diff-friendly.
ddl
Package ddl generates the canonical SQL schema (CREATE TABLE) from the parsed models schema — input for a file-based Atlas workflow (`atlas migrate diff --to file://schema.sql`) and schema documentation.
Package ddl generates the canonical SQL schema (CREATE TABLE) from the parsed models schema — input for a file-based Atlas workflow (`atlas migrate diff --to file://schema.sql`) and schema documentation.
parse
Package parse parses the models package: it finds entity structs (marked by exactly one field with a `sorm:"pk..."` tag), classifies fields by Go type, and validates the schema before generation.
Package parse parses the models package: it finds entity structs (marked by exactly one field with a `sorm:"pk..."` tag), classifies fields by Go type, and validates the schema before generation.
pgmodels
Package pgmodels is a PostgreSQL-only test schema: native array columns are rejected by the DDL generator on other dialects, so they live apart from the portable testmodels.
Package pgmodels is a PostgreSQL-only test schema: native array columns are rejected by the DDL generator on other dialects, so they live apart from the portable testmodels.
testmodels
Package testmodels is a test schema covering every field kind the generator supports: basic types, nullable pointers, time.Time, []byte, a version field, and navigations.
Package testmodels is a test schema covering every field kind the generator supports: basic types, nullable pointers, time.Time, []byte, a version field, and navigations.
Package migrate provides declarative migrations from application code, powered by the Atlas engine (ariga.io/atlas as a Go dependency, no external CLI):
Package migrate provides declarative migrations from application code, powered by the Atlas engine (ariga.io/atlas as a Go dependency, no external CLI):
Package myagg provides MySQL-specific aggregate functions for sorm projections.
Package myagg provides MySQL-specific aggregate functions for sorm projections.
Package otelsorm provides OpenTelemetry tracing AND metrics for sorm on top of sorm.Instrument:
Package otelsorm provides OpenTelemetry tracing AND metrics for sorm on top of sorm.Instrument:
Package pgagg provides PostgreSQL-specific aggregate functions for sorm projections.
Package pgagg provides PostgreSQL-specific aggregate functions for sorm projections.
Package sormtest is the testing toolkit for projects built on sorm.
Package sormtest is the testing toolkit for projects built on sorm.

Jump to

Keyboard shortcuts

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