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

orm

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 13 Imported by: 0

README

Lemmego ORM

An independent SQL persistence module built directly on database/sql.

It stands alone. Its only dependencies are database/sql and two small naming libraries (go-pluralize, strcase) — shared with the CLI and migration modules so that table names agree. It imports no other Lemmego package, and nothing here needs GPA, a connector, or the framework:

db := orm.Open(sqlDB, orm.Postgres())

is the whole setup. Two optional modules build on it, and neither is required: gpaorm exposes it through the GPA contracts, and ormconnector wires it into the application lifecycle. The dependency direction is one-way — they import the ORM, never the reverse.

Quick start

type User struct {
    ID        int       `orm:"primaryKey;autoIncrement"`
    Email     string    `orm:"column:email_address"`
    CreatedAt time.Time
    Posts     []Post    `orm:"hasMany:UserID"`
}

sqlDB, _ := sql.Open("sqlite3", "app.db")
db := orm.Open(sqlDB, orm.SQLite())

users, err := db.Model[User]().
    Where(orm.Eq("active", true)).
    With("Posts").
    OrderBy(orm.Desc("created_at")).
    Limit(20).
    All(ctx)

Queries

Model[T] and Raw[T] are generic methods on both DB and Tx, so a transaction is a drop-in replacement for the connection:

db.Model[User]()                    tx.Model[User]()
db.Raw[Report](query, args...)      tx.Raw[Report](query, args...)

Builder methods return copies, so a partially built query can be shared and branched without aliasing.

DB.Transaction is the normal way to run a transaction: it commits, rolls back and recovers from panics. DB.Begin (and DB.BeginTx for isolation levels) returns a *Tx the caller must finish itself, for code that has to hand the handle around.

Category Methods
Filter Where, Having, GroupBy, Distinct, Scope
Shape Select, OrderBy, Limit, Offset, Table
Join Join[U], LeftJoin[U], JoinTable, LeftJoinTable
Read All, First, Find, Count, Exists, Paginate
Transaction DB.Transaction, DB.Begin, DB.BeginTx
Typed read Pluck[V], As[R], Sum[V], Avg[V], Min[V], Max[V]
Write Create, CreateBatch, Update, Delete, UpdateAll, DeleteAll
Upsert Upsert, InsertIgnore
Soft delete WithTrashed, OnlyTrashed, Restore, ForceDelete, ForceDeleteAll
Relations With, and db.Relation[P, C]
Predicates

Two ways to build the same Predicate, producing byte-identical SQL:

// String constructors — no code generation needed.
orm.Eq / Ne / Gt / Gte / Lt / Lte / In / NotIn / Like / NotLike
orm.IsNull / IsNotNull / Between / And / Or / Not / RawPredicate

// Typed descriptors — compile-time column and value checking.
var UserFields = struct {
    ID     orm.Field[User, int]
    Active orm.Field[User, bool]
}{
    ID:     orm.NewField[User, int]("id"),
    Active: orm.NewField[User, bool]("active"),
}

db.Model[User]().Where(UserFields.Active.Eq(true))

Descriptors are ordinary Go values, so they can be hand-written. cmd/ormgen generates them:

go run github.com/lemmego/orm/cmd/ormgen ./internal/models

An application using ormconnector gets the same generator as an orm:fields CLI command. The generator logic lives in the importable ormgen package, so go/ast is not pulled in by importing the ORM itself.

or, more usually, from a directive in the models package:

//go:generate go run github.com/lemmego/orm/cmd/ormgen .

It writes orm_fields.go containing a <Model>Fields value and a <Model>Table constant per model. By default it generates for exported structs carrying an orm or db tag; -type Name,Other selects specific types and -all includes every exported struct.

The generator uses the ORM's own naming and tag rules, so a generated descriptor always resolves to the same column the runtime does. It honours embedded structs, orm:"-", the db tag fallback, and a literal TableName() override, and it skips relation fields.

Result-type changes

Query[T] is not locked to returning []T:

emails, err := db.Model[User]().Pluck[string](ctx, "email_address")
total, err  := db.Model[Order]().Where(...).Sum[int64](ctx, "cents")

rows, err := db.Model[User]().
    Join[Post](orm.RawPredicate(`"posts"."user_id" = "users"."id"`)).
    As[UserPostRow]().
    All(ctx)

Relationships

hasOne, hasMany and belongsTo are declared in struct tags. The tag names the field holding the foreign key, not the column:

type User struct {
    ID    int    `orm:"primaryKey;autoIncrement"`
    Posts []Post `orm:"hasMany:UserID"`   // UserID lives on Post
}

type Post struct {
    ID       int       `orm:"primaryKey;autoIncrement"`
    UserID   int
    Author   *User     `orm:"belongsTo:UserID"`   // UserID lives on Post
    Comments []Comment `orm:"hasMany:PostID"`
}

With eager-loads, including nested paths, at a cost of one query per relation level regardless of row count:

users, err := db.Model[User]().With("Posts.Comments").All(ctx)

Association writes take the database handle as the receiver, keeping models free of any connection:

db.Relation[User, Post](&user, "Posts").Add(ctx, &post)     // insert or relink
db.Relation[User, Post](&user, "Posts").Remove(ctx, &post)  // clears the FK, never deletes
db.Relation[User, Post](&user, "Posts").Load(ctx)
db.Relation[User, Post](&user, "Posts").Query().Where(...)  // further filtering
Many-to-many

Declare the join table; the pivot columns default to the conventional <singular model>_id on each side and can be overridden with joinForeignKey: and joinTargetKey:.

type Article struct {
    ID   int   `orm:"primaryKey;autoIncrement"`
    Tags []Tag `orm:"many2many:article_tags"`   // article_id, tag_id
}

Eager loading costs one pivot query plus one target query per level. The association methods mirror Laravel's, with key-based variants for when the related rows are known by identifier rather than loaded:

tags := db.Relation[Article, Tag](&article, "Tags")

tags.Attach(ctx, &go, &sql)   // idempotent: existing links are not duplicated
tags.Detach(ctx, &sql)        // Detach(ctx) with no arguments clears everything
tags.Sync(ctx, &go, &orm)     // make the set exactly this
tags.Toggle(ctx, &go)         // flip membership

tags.AttachKeys(ctx, 1, 2)    // and DetachKeys / SyncKeys / ToggleKeys

Add and Remove delegate to Attach and Detach on a many-to-many, so the handle behaves sensibly whichever verb you reach for.

Soft deletes

A model is soft-deletable when it has a nullable timestamp named DeletedAt, or any nullable timestamp tagged orm:"softDelete":

type Article struct {
    ID        int        `orm:"primaryKey;autoIncrement"`
    DeletedAt *time.Time            // or sql.NullTime
}

The column must be *time.Time or sql.NullTime. A bare time.Time is rejected, because its zero value still writes a real timestamp and deleted_at IS NULL would then match nothing — quietly hiding every row.

Reads exclude trashed rows by default, and so do Count, Exists, Paginate, UpdateAll and DeleteAll:

db.Model[Article]().All(ctx)                  // visible rows only
db.Model[Article]().WithTrashed().All(ctx)    // everything
db.Model[Article]().OnlyTrashed().All(ctx)    // just the trashed

db.Model[Article]().Delete(ctx, &article)       // marks deleted_at
db.Model[Article]().Restore(ctx, &article)      // clears it
db.Model[Article]().ForceDelete(ctx, &article)  // really removes the row

DeleteAll soft-deletes. ForceDeleteAll removes rows, but still honours the default scope — combine it with WithTrashed() to purge everything.

The soft-delete column is resolved once from the model and carried through As[R], so a projection cannot accidentally read trashed rows.

Automatic timestamps

Fields named CreatedAt and UpdatedAt of a time type are maintained automatically, as is any field tagged autoCreateTime or autoUpdateTime. CreatedAt is only filled when unset, so an explicitly assigned value (or one set by a BeforeCreate hook) survives; UpdatedAt is always refreshed on update.

The type check is deliberate: a model carrying its own CreatedAt int64 keeps full manual control rather than having the ORM overwrite it.

Stamps are taken in UTC. A naive column (Postgres TIMESTAMP, MySQL DATETIME) stores no offset, so writing local time and reading it back returns a value shifted by the machine's zone offset. Normalising to UTC makes the round trip exact whether or not the column is timezone-aware. Values you assign yourself are stored as given.

Pagination and scopes

page, err := db.Model[Article]().OrderBy(orm.Desc("created_at")).Paginate(ctx, 2, 20)
// page.Items, page.Total, page.TotalPages, page.HasNext(), page.HasPrev()

Total counts the whole result set, ignoring any limit or offset already on the query. Aggregates drop ORDER BY: it is meaningless for a count, and Postgres rejects SELECT COUNT(*) ... ORDER BY id outright.

Scopes name a reusable filter and compose:

func Popular() orm.Scope[Article] {
    return func(q *orm.Query[Article]) *orm.Query[Article] {
        return q.Where(orm.Gte("views", 1000))
    }
}

db.Model[Article]().Scope(Popular(), Recent()).All(ctx)

Upserts

Upsert inserts, or updates the named columns when a row with the same conflict columns already exists. With no update columns named, every mapped column except the conflict columns and the generated key is overwritten.

db.Model[User]().Upsert(ctx, &user, []string{"email"}, "name", "updated_at")
db.Model[User]().InsertIgnore(ctx, &user, "email")   // insert, or leave it alone

The three databases spell this differently and the dialect hides the difference: ON CONFLICT (...) DO UPDATE on Postgres and SQLite, ON DUPLICATE KEY UPDATE on MySQL. Two MySQL differences are worth knowing: it keys the conflict off any unique index rather than the columns you name, and it has no DO NOTHING, so InsertIgnore compiles to the conventional self-assignment no-op.

A generated key is not written back by an upsert: the statement may update an existing row rather than insert one, so there is no new key to report.

Transactions and savepoints

err := db.Transaction(ctx, func(tx *orm.Tx) error {
    if _, err := tx.Model[User]().Create(ctx, &user); err != nil {
        return err
    }
    if err := tx.Savepoint(ctx, "after_user"); err != nil {
        return err
    }
    if _, err := tx.Model[Post]().Create(ctx, &post); err != nil {
        return tx.RollbackTo(ctx, "after_user")   // keep the user, drop the post
    }
    return tx.ReleaseSavepoint(ctx, "after_user")
})

A transaction is deliberately not bound to one entity type, so a single unit of work spans as many models as it needs. AfterCommit registers callbacks that run only once the commit succeeds and cannot roll it back.

Savepoint names are written into the SQL text rather than bound as parameters, so they are validated as plain identifiers; anything else returns ErrInvalidIdentifier. CheckIdentifier is exported for callers that build names themselves.

Query logging

Install a logger and every statement the ORM runs is reported — including the extra queries eager loading issues, which are usually the ones you want to see:

db := orm.Open(sqlDB, orm.Postgres(), orm.WithSlogLogger(slog.Default()))

// or handle the events yourself
db = orm.Open(sqlDB, orm.Postgres(), orm.WithLogger(
    func(ctx context.Context, e orm.QueryEvent) {
        if e.Duration > 100*time.Millisecond {
            log.Printf("slow: %s", e)
        }
    }))

QueryEvent carries the SQL, bound arguments, duration, error, and rows affected — the last is -1 for a read, since rows are counted as they are scanned, long after the statement returns.

The logger is called synchronously on the query path, so hand off to a channel if the work is expensive. With no logger installed nothing is wrapped and the uninstrumented path costs nothing.

Mapping

  • Tags: orm:"column:name", primaryKey, autoIncrement, -, and the relation forms above. A db:"..." tag is honoured as a fallback, with modifiers such as ,omitempty stripped, so CLI-generated models map without edits.
  • Table names are strcase.ToSnake plus go-pluralize, matching /cli and /migration. Implement TableName() string to override.
  • Fields that cannot round-trip through one column (nested structs, slices of structs) are skipped rather than mapped.
  • SELECT names its columns explicitly, so a column present in the table but absent from the struct is simply not read.
  • sql.Scanner and driver.Valuer are honoured. NULL read into a non-pointer field yields the zero value instead of failing the scan.
  • Raw queries tolerate columns the destination does not declare; model queries do not, so a mistyped Select is reported rather than silently dropped.

Writes, hooks and errors

Generated keys are written back onto the entity after insert, via RETURNING where the dialect supports it and LastInsertId where it does not.

UpdateAll and DeleteAll refuse to run without a predicate, returning ErrUnsafeMutation.

Lifecycle order:

BeforeCreate/BeforeUpdate/BeforeDelete
SQL statement
AfterCreate/AfterUpdate/AfterDelete
transaction commit
AfterCommit callbacks

An after-statement hook error is returned to the caller, and rolls back when inside DB.Transaction. After-commit callbacks run only once the commit has succeeded and cannot roll back.

Driver errors are normalised into *ConstraintError carrying a kind, and the table and columns where the driver reports them:

if orm.IsUniqueViolation(err) { ... }

var constraint *orm.ConstraintError
if errors.As(err, &constraint) {
    log.Println(constraint.Kind, constraint.Table, constraint.Columns)
}

Sentinels: ErrNotFound, ErrClosed, ErrUnsafeMutation, ErrUnsupported, ErrNoRowsAffected, ErrConstraint.

GPA

The gpaorm module implements the GPA persistence contracts on top of this ORM, so an application written against gpa.Repository[T] can run on it. It is entirely optional: the ORM never imports GPA, and everything above works without it.

Where the two overlap, the ORM owns the behaviour and the adapter delegates — savepoints, for instance, are implemented on orm.Tx and gpaorm calls them, rather than carrying a second copy of the SQL.

Composite primary keys

A model may declare several primaryKey columns. Update and Delete then address the row by all of them, and Find takes one value per column in declaration order:

type Enrollment struct {
    StudentID int `orm:"primaryKey"`
    CourseID  int `orm:"primaryKey"`
    Grade     string
}

enrollment, err := db.Model[Enrollment]().Find(ctx, studentID, courseID)

ModelInfo.PrimaryKeys holds them all; ModelInfo.PrimaryKey is the single-key convenience and is nil for a composite key, so code cannot use it by accident. Relations join on one column, so a composite-key model needs references: to name which — otherwise eager loading reports the problem rather than guessing.

Not implemented yet

Row locking and subqueries; the GPA adapter reports both as unsupported rather than ignoring them.

Verification

cd orm
go build ./... && go vet ./... && go test ./... && go test -race ./...
GOWORK=off go test ./...   # validate as a published module

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound       = errors.New("orm: record not found")
	ErrClosed         = errors.New("orm: transaction is closed")
	ErrUnsafeMutation = errors.New("orm: update or delete requires a predicate")
	ErrUnsupported    = errors.New("orm: unsupported feature")
	ErrNoRowsAffected = errors.New("orm: no rows affected")
	ErrConstraint     = errors.New("orm: constraint violated")
)
View Source
var ErrInvalidIdentifier = errors.New("orm: invalid identifier")

ErrInvalidIdentifier reports a name that cannot be safely interpolated into SQL.

Functions

func CheckIdentifier

func CheckIdentifier(name string) error

CheckIdentifier rejects anything that is not a plain SQL identifier.

Some names — savepoints above all — cannot be passed as bound parameters and must be written into the statement text, so they are validated rather than trusted.

func CheckRowsAffected

func CheckRowsAffected(result interface{ RowsAffected() (int64, error) }) error

func ColumnName

func ColumnName(fieldName string) string

ColumnName returns the column a struct field maps to by default.

func IsCheckViolation

func IsCheckViolation(err error) bool

func IsForeignKeyViolation

func IsForeignKeyViolation(err error) bool

func IsNotNullViolation

func IsNotNullViolation(err error) bool

func IsUniqueViolation

func IsUniqueViolation(err error) bool

These report the constraint class of an error produced anywhere in the ORM. They are named "...Violation" to stay clear of the predicate constructors in expr.go, where IsNull and IsNotNull build SQL rather than inspect errors.

func PivotColumnFor

func PivotColumnFor(structName string) string

PivotColumnFor returns the conventional pivot column for a model name.

func TableNameFor

func TableNameFor(structName string) string

TableNameFor returns the table a model type name maps to by default. It does not consult TableNamer, which only a concrete type can answer.

Types

type AfterCreateHook

type AfterCreateHook interface{ AfterCreate(context.Context) error }

type AfterDeleteHook

type AfterDeleteHook interface{ AfterDelete(context.Context) error }

type AfterUpdateHook

type AfterUpdateHook interface{ AfterUpdate(context.Context) error }

type BeforeCreateHook

type BeforeCreateHook interface{ BeforeCreate(context.Context) error }

type BeforeDeleteHook

type BeforeDeleteHook interface{ BeforeDelete(context.Context) error }

type BeforeUpdateHook

type BeforeUpdateHook interface{ BeforeUpdate(context.Context) error }

type ConstraintError

type ConstraintError struct {
	Kind       ConstraintKind
	Constraint string
	Table      string
	Columns    []string
	Cause      error
}

ConstraintError is the normalised form of a driver constraint violation. Dialect.ClassifyError builds it; the fields it can fill vary by driver.

func (*ConstraintError) Error

func (e *ConstraintError) Error() string

func (*ConstraintError) Is

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

func (*ConstraintError) Unwrap

func (e *ConstraintError) Unwrap() error

type ConstraintKind

type ConstraintKind int

ConstraintKind classifies a constraint violation reported by the driver.

const (
	ConstraintUnknown ConstraintKind = iota
	ConstraintUnique
	ConstraintForeignKey
	ConstraintNotNull
	ConstraintCheck
)

func (ConstraintKind) String

func (k ConstraintKind) String() string

type DB

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

func Open

func Open(db *sql.DB, dialect Dialect, options ...Option) *DB

func (*DB) Begin

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

Begin starts a transaction the caller is responsible for finishing.

Transaction is the safer choice and should be preferred: it commits, rolls back and handles panics for you. Begin exists for callers that must hand the handle around, such as an adapter implementing an external contract.

func (*DB) BeginTx

func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

func (*DB) Close

func (db *DB) Close() error

func (*DB) Dialect

func (db *DB) Dialect() Dialect

func (*DB) Exec

func (db *DB) Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

func (*DB) Logger

func (db *DB) Logger() Logger

Logger returns the installed statement logger, or nil.

func (*DB) Model

func (db *DB) Model[T any]() *Query[T]

Model starts a typed query. Raw starts a typed hand-written query.

These are generic methods, which Go 1.27 permits on concrete types. That is why DB and Tx are concrete and why there is no shared interface over them: a method with its own type parameters can never satisfy an interface, so a Querier abstraction carrying Model[T] would not compile.

func (*DB) Raw

func (db *DB) Raw[T any](query string, args ...any) *RawQuery[T]

func (*DB) Relation

func (db *DB) Relation[Parent, Child any](parent *Parent, name string) *RelationHandle[Parent, Child]

Relation opens a handle onto the named association of parent.

func (*DB) SQLDB

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

func (*DB) Transaction

func (db *DB) Transaction(ctx context.Context, fn func(*Tx) error) error

Transaction runs fn inside a transaction, rolling back on error or panic. It is deliberately not bound to one entity type, so a single transaction can span as many models as the unit of work needs.

type Dialect

type Dialect interface {
	Name() string
	Quote(string) string
	Placeholder(int) string

	// SupportsReturning reports whether INSERT ... RETURNING is available.
	// Where it is not, generated keys come from LastInsertId instead.
	SupportsReturning() bool

	// ClassifyError maps a driver error onto a normalised ORM error such as
	// *ConstraintError. It returns the original error when unrecognised.
	ClassifyError(error) error

	// UpsertClause renders the suffix that turns an INSERT into an upsert.
	// conflict names the columns that identify an existing row; update names
	// the columns to overwrite, and being empty means "leave the row alone".
	UpsertClause(conflict, update []string) (string, error)
}

Dialect controls SQL syntax and error semantics that differ between supported databases.

Dialect is an interface, so it must never gain methods with their own type parameters: Go 1.27 generic methods exist on concrete types only and cannot participate in interface satisfaction.

func MySQL

func MySQL() Dialect

func Postgres

func Postgres() Dialect

func SQLite

func SQLite() Dialect

type Field

type Field[Model, Value any] struct {
	// contains filtered or unexported fields
}

Field is a model field descriptor. The Value type parameter prevents comparing, say, a bool column against a string. Descriptors are ordinary Go values: hand-write them now, generate them later.

func NewField

func NewField[Model, Value any](column string) Field[Model, Value]

func (Field[Model, Value]) Asc

func (f Field[Model, Value]) Asc() Ordering

func (Field[Model, Value]) Between

func (f Field[Model, Value]) Between(low, high Value) Predicate

func (Field[Model, Value]) Column

func (f Field[Model, Value]) Column() string

Column returns the underlying column name.

func (Field[Model, Value]) Desc

func (f Field[Model, Value]) Desc() Ordering

func (Field[Model, Value]) Eq

func (f Field[Model, Value]) Eq(value Value) Predicate

func (Field[Model, Value]) Gt

func (f Field[Model, Value]) Gt(value Value) Predicate

func (Field[Model, Value]) Gte

func (f Field[Model, Value]) Gte(value Value) Predicate

func (Field[Model, Value]) In

func (f Field[Model, Value]) In(values ...Value) Predicate

func (Field[Model, Value]) IsNotNull

func (f Field[Model, Value]) IsNotNull() Predicate

func (Field[Model, Value]) IsNull

func (f Field[Model, Value]) IsNull() Predicate

func (Field[Model, Value]) Lt

func (f Field[Model, Value]) Lt(value Value) Predicate

func (Field[Model, Value]) Lte

func (f Field[Model, Value]) Lte(value Value) Predicate

func (Field[Model, Value]) Ne

func (f Field[Model, Value]) Ne(value Value) Predicate

func (Field[Model, Value]) NotIn

func (f Field[Model, Value]) NotIn(values ...Value) Predicate

type FieldInfo

type FieldInfo struct {
	Name          string
	Column        string
	Index         []int
	Type          reflect.Type
	PrimaryKey    bool
	AutoIncrement bool
}

type Logger

type Logger func(context.Context, QueryEvent)

Logger receives every statement the ORM executes, including the extra queries eager loading issues.

It is called synchronously on the query path, so a slow logger slows every query. Hand off to a channel or a buffered writer if the work is expensive.

type MappingError

type MappingError struct {
	Model  string
	Column string
	Cause  error
}

MappingError reports a column that could not be mapped onto a model field.

func (*MappingError) Error

func (e *MappingError) Error() string

func (*MappingError) Unwrap

func (e *MappingError) Unwrap() error

type ModelInfo

type ModelInfo struct {
	Type     reflect.Type
	Table    string
	Fields   []FieldInfo
	ByColumn map[string]FieldInfo
	ByName   map[string]FieldInfo
	// PrimaryKeys holds every primary-key column in declaration order.
	// PrimaryKey is the single key, or nil when the model has none or a
	// composite one.
	PrimaryKeys []FieldInfo
	PrimaryKey  *FieldInfo

	Relations  []RelationInfo
	ByRelation map[string]RelationInfo

	// Lifecycle columns, detected by tag or by conventional name. Nil when
	// the model does not declare one.
	SoftDelete *FieldInfo
	CreatedAt  *FieldInfo
	UpdatedAt  *FieldInfo
}

func ModelInfoFor

func ModelInfoFor[T any]() (*ModelInfo, error)

func (*ModelInfo) Column

func (m *ModelInfo) Column(fieldName string) (string, bool)

Column returns the column name for a struct field name.

func (*ModelInfo) HasCompositeKey

func (m *ModelInfo) HasCompositeKey() bool

HasCompositeKey reports whether the model is keyed on more than one column.

func (*ModelInfo) HasSoftDelete

func (m *ModelInfo) HasSoftDelete() bool

HasSoftDelete reports whether reads should hide soft-deleted rows.

type Numeric

type Numeric interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
		~float32 | ~float64
}

Numeric constrains the aggregate helpers that only make sense over numbers.

type Operation

type Operation string

Operation distinguishes the two kinds of statement the ORM issues.

const (
	OpQuery Operation = "query"
	OpExec  Operation = "exec"
)

type Option

type Option func(*DB)

func WithDialect

func WithDialect(dialect Dialect) Option

func WithLogger

func WithLogger(logger Logger) Option

WithLogger installs a statement logger.

func WithSlogLogger

func WithSlogLogger(logger *slog.Logger) Option

WithSlogLogger logs statements through log/slog, at debug level for successes and error level for failures.

type Ordering

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

Ordering is one ORDER BY term.

func Asc

func Asc(column string) Ordering

func Desc

func Desc(column string) Ordering

func RawOrdering

func RawOrdering(sql string) Ordering

RawOrdering embeds literal ORDER BY SQL, for expressions the builder cannot express (COALESCE, CASE, NULLS LAST).

type Page

type Page[T any] struct {
	Items      []T
	Page       int
	PerPage    int
	Total      int64
	TotalPages int
}

Page is one page of results plus the totals needed to render a pager.

func (*Page[T]) HasNext

func (p *Page[T]) HasNext() bool

HasNext and HasPrev report whether neighbouring pages exist.

func (*Page[T]) HasPrev

func (p *Page[T]) HasPrev() bool

type Predicate

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

Predicate is the single currency of the query DSL. String constructors (orm.Eq) and generated field descriptors (UserFields.Active.Eq) both build one, so there is exactly one Where signature and one SQL compiler.

Predicates stay structured until compile time because quoting and placeholder numbering both depend on the dialect, which is not known when the predicate is constructed.

func And

func And(predicates ...Predicate) Predicate

And groups predicates with AND. Or groups them with OR.

func Between

func Between(column string, low, high any) Predicate

func Eq

func Eq(column string, value any) Predicate

String-based constructors. These take a column name and are the path the GPA adapter uses, since GPA delivers column names as runtime strings.

func Gt

func Gt(column string, value any) Predicate

func Gte

func Gte(column string, value any) Predicate

func In

func In(column string, values ...any) Predicate

func IsNotNull

func IsNotNull(column string) Predicate

func IsNull

func IsNull(column string) Predicate

func Like

func Like(column string, pattern string) Predicate

func Lt

func Lt(column string, value any) Predicate

func Lte

func Lte(column string, value any) Predicate

func Ne

func Ne(column string, value any) Predicate

func Not

func Not(predicate Predicate) Predicate

Not negates a predicate.

func NotIn

func NotIn(column string, values ...any) Predicate

func NotLike

func NotLike(column string, pattern string) Predicate

func Or

func Or(predicates ...Predicate) Predicate

func RawPredicate

func RawPredicate(sql string, args ...any) Predicate

RawPredicate embeds literal SQL. Placeholders are written as ? and are renumbered per dialect; identifiers are passed through untouched.

func (Predicate) And

func (p Predicate) And(other Predicate) Predicate

func (Predicate) IsZero

func (p Predicate) IsZero() bool

IsZero reports whether the predicate would contribute no SQL.

func (Predicate) Or

func (p Predicate) Or(other Predicate) Predicate

type Query

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

Query is an immutable, chainable query over T. Every builder method returns a copy, so a partially built query can be shared and branched safely.

func (*Query[T]) All

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

All executes the query and eager-loads any requested relations.

func (*Query[T]) As

func (q *Query[T]) As[R any]() *Query[R]

As reprojects the query into a different result type while keeping the source table, predicates, joins and ordering. This is what makes joins and report DTOs usable without dropping to raw SQL: Query[T] would otherwise be permanently locked to returning []T.

func (*Query[T]) Avg

func (q *Query[T]) Avg[V Numeric](ctx context.Context, column string) (V, error)

func (*Query[T]) Count

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

Count reports how many rows match. quoteColumn leaves "*" alone, so this renders as COUNT(*).

func (*Query[T]) Create

func (q *Query[T]) Create(ctx context.Context, entity *T) (sql.Result, error)

Create inserts one entity and writes any generated key back onto it.

func (*Query[T]) CreateBatch

func (q *Query[T]) CreateBatch(ctx context.Context, entities []*T) (sql.Result, error)

CreateBatch inserts many entities in a single multi-row statement.

func (*Query[T]) Delete

func (q *Query[T]) Delete(ctx context.Context, entity *T) (sql.Result, error)

Delete removes the entity. When the model declares a soft-delete column the row is marked rather than removed; ForceDelete always removes it.

func (*Query[T]) DeleteAll

func (q *Query[T]) DeleteAll(ctx context.Context) (sql.Result, error)

DeleteAll removes every matching row. Like UpdateAll it refuses to run without a predicate.

func (*Query[T]) DeleteSQL

func (q *Query[T]) DeleteSQL(entity *T) (string, []any, error)

func (*Query[T]) Distinct

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

func (*Query[T]) Exists

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

func (*Query[T]) Find

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

Find looks a row up by primary key. A composite key takes one value per column, in declaration order.

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]) ForceDelete

func (q *Query[T]) ForceDelete(ctx context.Context, entity *T) (sql.Result, error)

ForceDelete issues a real DELETE even for a soft-deletable model.

func (*Query[T]) ForceDeleteAll

func (q *Query[T]) ForceDeleteAll(ctx context.Context) (sql.Result, error)

ForceDeleteAll removes matching rows outright, even for a soft-deletable model. It carries the same predicate requirement as DeleteAll.

func (*Query[T]) GroupBy

func (q *Query[T]) GroupBy(columns ...string) *Query[T]

func (*Query[T]) Having

func (q *Query[T]) Having(predicates ...Predicate) *Query[T]

func (*Query[T]) InsertIgnore

func (q *Query[T]) InsertIgnore(ctx context.Context, entity *T, conflict ...string) (sql.Result, error)

InsertIgnore inserts the entity, doing nothing if it already exists.

func (*Query[T]) InsertSQL

func (q *Query[T]) InsertSQL(entity *T) (string, []any, error)

func (*Query[T]) Join

func (q *Query[T]) Join[U any](on Predicate) *Query[T]

Join adds an INNER JOIN against the table of U. LeftJoin adds a LEFT JOIN. The type parameter supplies the joined table name, so the join stays in sync with the model rather than a hand-written string.

func (*Query[T]) JoinTable

func (q *Query[T]) JoinTable(table string, on Predicate) *Query[T]

JoinTable and LeftJoinTable join a table by name, for the cases where the joined side has no model to infer it from — a lookup table, or a caller such as the GPA adapter that receives the table as a runtime string.

func (*Query[T]) LeftJoin

func (q *Query[T]) LeftJoin[U any](on Predicate) *Query[T]

func (*Query[T]) LeftJoinTable

func (q *Query[T]) LeftJoinTable(table string, on Predicate) *Query[T]

func (*Query[T]) Limit

func (q *Query[T]) Limit(limit int) *Query[T]

func (*Query[T]) Max

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

func (*Query[T]) Min

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

func (*Query[T]) Offset

func (q *Query[T]) Offset(offset int) *Query[T]

func (*Query[T]) OnlyTrashed

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

func (*Query[T]) OrderBy

func (q *Query[T]) OrderBy(orderings ...Ordering) *Query[T]

func (*Query[T]) Paginate

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

Paginate returns one page of results together with the total row count. Pages are 1-based.

func (*Query[T]) Pluck

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

Pluck reads a single column across all matching rows.

func (*Query[T]) Restore

func (q *Query[T]) Restore(ctx context.Context, entity *T) (sql.Result, error)

Restore clears the soft-delete marker on an entity.

func (*Query[T]) SQL

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

SQL renders the SELECT statement and its arguments.

func (*Query[T]) Scope

func (q *Query[T]) Scope(scopes ...Scope[T]) *Query[T]

Scope applies reusable query modifiers, so common filters can be named once and composed.

func (*Query[T]) Select

func (q *Query[T]) Select(columns ...string) *Query[T]

Select restricts the projection to the named columns.

func (*Query[T]) Sum

func (q *Query[T]) Sum[V Numeric](ctx context.Context, column string) (V, error)

Sum, Avg, Min and Max evaluate an aggregate over the current predicates.

func (*Query[T]) Table

func (q *Query[T]) Table(name string) *Query[T]

Table overrides the table this query targets.

func (*Query[T]) Update

func (q *Query[T]) Update(ctx context.Context, entity *T) (sql.Result, error)

func (*Query[T]) UpdateAll

func (q *Query[T]) UpdateAll(ctx context.Context, values map[string]any) (sql.Result, error)

UpdateAll applies a column/value map to every matching row.

A predicate is mandatory: an unfiltered UPDATE is almost always a mistake, and ErrUnsafeMutation makes it an explicit opt-in rather than a silent table rewrite.

func (*Query[T]) UpdateSQL

func (q *Query[T]) UpdateSQL(entity *T, columns ...string) (string, []any, error)

UpdateSQL writes every non-key column, or only the named ones.

func (*Query[T]) Upsert

func (q *Query[T]) Upsert(ctx context.Context, entity *T, conflict []string, update ...string) (sql.Result, error)

Upsert inserts the entity, or updates the named columns when a row with the same conflict columns already exists.

With no update columns, every mapped column except the conflict columns and the auto-increment key is overwritten. Use InsertIgnore to insert only.

A generated key is not written back: the statement may update an existing row rather than insert, so there is no new key to report.

func (*Query[T]) Where

func (q *Query[T]) Where(predicates ...Predicate) *Query[T]

Where appends predicates, combined with AND.

func (*Query[T]) With

func (q *Query[T]) With(paths ...string) *Query[T]

With eager-loads relations, including nested paths such as "Posts.Comments".

func (*Query[T]) WithTrashed

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

WithTrashed includes soft-deleted rows. OnlyTrashed returns just those.

type QueryEvent

type QueryEvent struct {
	Operation Operation
	SQL       string
	Args      []any
	Duration  time.Duration
	Err       error

	// Rows is the number of rows affected by an exec, or -1 when the figure
	// is unavailable — which it always is for a query, since rows are counted
	// as they are scanned, long after the statement returns.
	Rows int64
}

QueryEvent describes one executed statement.

func (QueryEvent) String

func (e QueryEvent) String() string

String renders the event as a single readable line.

type RawQuery

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

RawQuery is a typed handle over hand-written SQL.

func (*RawQuery[T]) All

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

All scans the result set. Columns the destination does not declare are discarded rather than treated as an error, since raw SQL routinely returns more than the struct models.

func (*RawQuery[T]) First

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

func (*RawQuery[T]) SQL

func (q *RawQuery[T]) SQL() (string, []any)

type RelationHandle

type RelationHandle[Parent, Child any] struct {
	// contains filtered or unexported fields
}

RelationHandle operates on one association of one parent record.

The receiver is the database handle rather than the model, so models stay plain structs: a struct that carries a live connection cannot be safely serialised, copied between goroutines, or built in a test without a database.

func (*RelationHandle[Parent, Child]) Add

func (h *RelationHandle[Parent, Child]) Add(ctx context.Context, children ...*Child) error

Add associates children with the parent, inserting them when they are new and updating the foreign key when they already exist.

func (*RelationHandle[Parent, Child]) Attach

func (h *RelationHandle[Parent, Child]) Attach(ctx context.Context, children ...*Child) error

Attach links children to the parent. It is idempotent: keys already present in the pivot are skipped rather than duplicated.

func (*RelationHandle[Parent, Child]) AttachKeys

func (h *RelationHandle[Parent, Child]) AttachKeys(ctx context.Context, keys ...any) error

AttachKeys, DetachKeys, SyncKeys and ToggleKeys are the key-based forms, for when the related rows are known by identifier rather than loaded.

func (*RelationHandle[Parent, Child]) Count

func (h *RelationHandle[Parent, Child]) Count(ctx context.Context) (int64, error)

func (*RelationHandle[Parent, Child]) Detach

func (h *RelationHandle[Parent, Child]) Detach(ctx context.Context, children ...*Child) error

Detach unlinks children. Calling it with none detaches everything.

func (*RelationHandle[Parent, Child]) DetachKeys

func (h *RelationHandle[Parent, Child]) DetachKeys(ctx context.Context, keys ...any) error

func (*RelationHandle[Parent, Child]) Load

func (h *RelationHandle[Parent, Child]) Load(ctx context.Context) error

Load fetches the association and assigns it onto the parent.

func (*RelationHandle[Parent, Child]) Query

func (h *RelationHandle[Parent, Child]) Query() *Query[Child]

Query returns a query over the related rows, ready for further filtering.

func (*RelationHandle[Parent, Child]) Remove

func (h *RelationHandle[Parent, Child]) Remove(ctx context.Context, children ...*Child) error

Remove dissociates children by clearing the foreign key. It never deletes rows: dropping a record is a separate, explicit decision.

func (*RelationHandle[Parent, Child]) Sync

func (h *RelationHandle[Parent, Child]) Sync(ctx context.Context, children ...*Child) error

Sync makes the association exactly the given set, attaching what is missing and detaching what is no longer wanted.

func (*RelationHandle[Parent, Child]) SyncKeys

func (h *RelationHandle[Parent, Child]) SyncKeys(ctx context.Context, keys ...any) error

func (*RelationHandle[Parent, Child]) Toggle

func (h *RelationHandle[Parent, Child]) Toggle(ctx context.Context, children ...*Child) error

Toggle attaches keys that are absent and detaches those already present.

func (*RelationHandle[Parent, Child]) ToggleKeys

func (h *RelationHandle[Parent, Child]) ToggleKeys(ctx context.Context, keys ...any) error

type RelationInfo

type RelationInfo struct {
	Name   string
	Kind   RelationKind
	Target reflect.Type
	Index  []int

	// ForeignKeyField names the struct field holding the foreign key. For
	// HasOne and HasMany it lives on Target; for BelongsTo it lives on the
	// declaring model.
	ForeignKeyField string

	// ReferencesField names the key field on the opposite side. Empty means
	// the primary key.
	ReferencesField string

	// Pivot describes the join table for ManyToMany. Populated at parse time
	// so the shape stays stable once many-to-many lands, but not yet used.
	PivotTable      string
	PivotForeignKey string
	PivotTargetKey  string
}

RelationInfo describes one association declared on a model.

Target metadata is resolved lazily, never during collectFields: two models that reference each other (User.Posts and Post.User) would otherwise recurse forever through modelInfoFor.

type RelationKind

type RelationKind int

RelationKind enumerates the relationship shapes the mapper understands.

const (
	RelationNone RelationKind = iota
	HasOne
	HasMany
	BelongsTo
	ManyToMany
)

func (RelationKind) String

func (k RelationKind) String() string

type Scope

type Scope[T any] func(*Query[T]) *Query[T]

Scope is a reusable query modifier.

type TableNamer

type TableNamer interface{ TableName() string }

TableNamer lets a model override the inflected table name.

type TagInfo

type TagInfo struct {
	Column        string
	PrimaryKey    bool
	AutoIncrement bool
	Skip          bool

	SoftDelete     bool
	AutoCreateTime bool
	AutoUpdateTime bool

	Relation        RelationKind
	RelationKey     string
	References      string
	PivotTable      string
	PivotForeignKey string
	PivotTargetKey  string
}

TagInfo is the parsed form of an orm struct tag.

func ParseTag

func ParseTag(value string) TagInfo

ParseTag parses an orm struct tag value.

type Tx

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

func (*Tx) AfterCommit

func (tx *Tx) AfterCommit(callback func(context.Context) error) error

AfterCommit registers a callback that runs only once the database commit has succeeded. It cannot roll the transaction back.

func (*Tx) Commit

func (tx *Tx) Commit() error

func (*Tx) CommitContext

func (tx *Tx) CommitContext(ctx context.Context) error

func (*Tx) Dialect

func (tx *Tx) Dialect() Dialect

func (*Tx) Exec

func (tx *Tx) Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

func (*Tx) Model

func (tx *Tx) Model[T any]() *Query[T]

Model and Raw mirror the DB methods so a transaction is a drop-in replacement for the connection in any unit of work.

func (*Tx) Raw

func (tx *Tx) Raw[T any](query string, args ...any) *RawQuery[T]

func (*Tx) Relation

func (tx *Tx) Relation[Parent, Child any](parent *Parent, name string) *RelationHandle[Parent, Child]

Relation mirrors DB.Relation inside a transaction.

func (*Tx) ReleaseSavepoint

func (tx *Tx) ReleaseSavepoint(ctx context.Context, name string) error

ReleaseSavepoint discards a savepoint, keeping the work done since it.

func (*Tx) Rollback

func (tx *Tx) Rollback() error

func (*Tx) RollbackTo

func (tx *Tx) RollbackTo(ctx context.Context, name string) error

RollbackTo undoes everything done since the named savepoint.

func (*Tx) Savepoint

func (tx *Tx) Savepoint(ctx context.Context, name string) error

Savepoint marks a point inside the transaction that RollbackTo can return to, allowing part of a unit of work to be undone without discarding all of it.

Savepoint names cannot be bound as parameters, so they are validated as plain identifiers rather than interpolated blindly.

type UnsupportedFeatureError

type UnsupportedFeatureError struct {
	Dialect string
	Feature string
}

func (*UnsupportedFeatureError) Error

func (e *UnsupportedFeatureError) Error() string

func (*UnsupportedFeatureError) Is

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

Directories

Path Synopsis
cmd
ormgen command
Command ormgen generates typed field descriptors for Lemmego ORM models.
Command ormgen generates typed field descriptors for Lemmego ORM models.
internal
genfixture
Package genfixture holds models used to exercise the ormgen generator.
Package genfixture holds models used to exercise the ormgen generator.
Package ormgen generates typed field descriptors for ORM models.
Package ormgen generates typed field descriptors for ORM models.

Jump to

Keyboard shortcuts

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