gormx

package module
v0.33.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package gormx provides GORM-based table helpers with pagination support.

Index

Constants

View Source
const Associations = clause.Associations

Associations is clause.Associations ("*"), the wildcard passed to Table.Preload to eager-load every association, or to Table.Omit to skip auto-saving every association on a write.

View Source
const DefaultDatabase = "default"

DefaultDatabase names the primary database. It is accepted interchangeably with the empty name, so gormx.Module(d) and gormx.Module(d, gormx.DefaultDatabase) register the same connection and gormx.Of(app) / gormx.Of(app, "default") both resolve it.

Variables

This section is empty.

Functions

func Expr added in v0.24.0

func Expr(expr string, args ...any) clause.Expr

Expr builds a raw SQL expression. Pass it as an UpdateMap value so the database computes the new value in-place (read-free), which keeps guarded updates atomic:

t.UpdateMap(ctx, map[string]any{"used": gormx.Expr("used + 1")}, "id = ?", id)

It is gorm.Expr.

func Inject added in v0.19.0

func Inject(db *gorm.DB, name ...string) host.Module

Inject registers an already-open *gorm.DB as a database, bypassing config loading and Init. Use it to supply a connection the caller built directly — typically a shared in-memory database in tests. The host still closes it and includes it in health checks. Pass an optional name for a named database.

func IsCheckConstraintViolation added in v0.29.0

func IsCheckConstraintViolation(err error) bool

IsCheckConstraintViolation reports whether err is a check-constraint violation.

func IsDuplicateKey added in v0.29.0

func IsDuplicateKey(err error) bool

IsDuplicateKey reports whether err is a unique-constraint (duplicate key) violation — e.g. inserting a row whose unique key already exists.

func IsForeignKeyViolation added in v0.29.0

func IsForeignKeyViolation(err error) bool

IsForeignKeyViolation reports whether err is a foreign-key constraint violation — e.g. referencing a parent row that does not exist, or deleting one still referenced under ON DELETE RESTRICT.

func IsRecordNotFound added in v0.29.0

func IsRecordNotFound(err error) bool

IsRecordNotFound reports whether err is a no-rows-found error. Note the Table getters (First/Find/Last/Take) already fold not-found into a (nil, nil) result, so this is mainly for callers working with raw gorm results.

func Lookup added in v0.23.0

func Lookup(app *host.App, name ...string) (*gorm.DB, bool)

Lookup returns the database connection registered under the optional name and whether one is registered. Use it where absence is an expected, recoverable condition (e.g. a background worker); prefer Of when the database must exist.

func Module added in v0.19.0

func Module(driver Driver, name ...string) host.Module

Module registers driver as a database, opening the connection during the host's init phase from the [database] config section (or [database.<name>] when named). Pass a built-in driver (postgres.Driver(), sqlite.Driver()) or any custom Driver; supply an already-open *gorm.DB with Inject instead.

host.MustNew().WithModule(gormx.Module(sqlite.Driver())).MustRun()

Pass an optional name to run several databases side by side, each retrieved with gormx.Of(app, name). Reach the default with gormx.Of(app).

func NewGormLogger added in v0.33.0

func NewGormLogger(sl *slog.Logger, levelOverride string) gormLogger.Interface

NewGormLogger builds a GORM logger that writes through the host's slog logger. levelOverride, when non-empty, sets the GORM log level explicitly; otherwise the level is derived from what the slog logger has enabled. This lets a database's SQL logging be tuned independently of the global log level — e.g. keep the app at debug while silencing per-query SQL traces with logLevel = "warn" in the [database] section. Valid overrides: debug (or info), warn, error, silent.

Built-in drivers pass Config.LogLevel here; custom Driver implementations can do the same to honor the per-database override.

func Of added in v0.19.0

func Of(app *host.App, name ...string) *gorm.DB

Of returns the database connection registered under the optional name, panicking if none is registered.

func UseTracing added in v0.15.0

func UseTracing(db *gorm.DB) error

UseTracing registers OpenTelemetry callbacks on db so every create, query, update, delete, row, and raw operation runs inside a client span carrying the table, the SQL template (placeholders, never bound values), and any error. Spans are no-ops until a global tracer provider is installed (e.g. via the otelx module), so registering unconditionally is cheap. The host applies it automatically to driver-opened connections.

Types

type BaseListRequest

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

BaseListRequest provides default no-op implementations of all ListRequest methods. Embed it in your own request struct and override only the methods you need. Filter rows by chaining Where/WhereIf on the Table before calling List; for request-driven filtering, implement the optional Scoper interface.

Example:

type MyRequest struct {
    db.BaseListRequest[MyEntity]
    TenantID string
}
func (r *MyRequest) Scope() func(*gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB { return db.Where("tenant_id = ?", r.TenantID) }
}

func NewBaseListRequest

func NewBaseListRequest[T any](pageSize int, orderBy string) BaseListRequest[T]

func (*BaseListRequest[T]) CreateNextPageToken

func (b *BaseListRequest[T]) CreateNextPageToken(_ []T) (*string, error)

func (*BaseListRequest[T]) CurrentPageData

func (b *BaseListRequest[T]) CurrentPageData() ([]any, error)

func (*BaseListRequest[T]) OrderBy

func (b *BaseListRequest[T]) OrderBy() string

func (*BaseListRequest[T]) PageSize

func (b *BaseListRequest[T]) PageSize() int

type BaseRepository

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

BaseRepository holds a database connection and provides RunTx for transaction management. Embed it in your own repository structs to inherit transaction support without implementing it yourself.

Example:

type OrderRepo struct {
    db.BaseRepository
    orders   *db.Table[Order]
    payments *db.Table[Payment]
}

func NewOrderRepo(h *mjhost.App) (*OrderRepo, error) {
    base, err := db.NewBaseRepository(h)
    if err != nil {
        return nil, err
    }
    return &OrderRepo{
        BaseRepository: *base,
        orders:         db.NewTableFor[Order](base),
        payments:       db.NewTableFor[Payment](base),
    }, nil
}

func (s *OrderService) PlaceOrder(ctx context.Context, ...) error {
    return s.repo.RunTx(ctx, func(ctx context.Context) error {
        ...
    })
}

func NewBaseRepository

func NewBaseRepository(db *gorm.DB) BaseRepository

func (*BaseRepository) RunTx

func (r *BaseRepository) RunTx(ctx context.Context, op func(context.Context) error) error

RunTx executes op inside a database transaction, rolling back on any error. If a transaction is already present in ctx, op runs within that transaction (propagation required — the outermost RunTx owns commit/rollback).

type Config added in v0.4.0

type Config struct {
	Host     string `mapstructure:"host"`
	Port     int    `mapstructure:"port"`
	User     string `mapstructure:"user"`
	Password string `mapstructure:"password"`
	Name     string `mapstructure:"name"`
	SSLMode  string `mapstructure:"sslMode"`
	// LogLevel optionally overrides the SQL-logging level for this database,
	// independent of the global log level. Leave empty to derive it from the
	// host logger (the default). Set it to "warn" or "error" to silence
	// per-query SQL traces while the app runs at debug. Valid values: debug
	// (or info), warn, error, silent.
	LogLevel string `mapstructure:"logLevel"`
}

Config is the database connection configuration. Individual [database] and [database.<name>] sections are loaded by Service.LoadConfig.

type CursorScoper added in v0.5.0

type CursorScoper interface {
	CursorScope() (func(*gorm.DB) *gorm.DB, error)
}

CursorScoper is an optional interface for ListRequest. If implemented, CursorScope() takes precedence over CurrentPageData().

type DB added in v0.24.0

type DB = gorm.DB

DB is gorm.DB, re-exported for callers who write Where/Order/Scope closures or implement Scoper — their func(*gormx.DB) *gormx.DB satisfies the func(*gorm.DB) *gorm.DB that gormx expects, since this is an alias rather than a new type.

type Driver added in v0.7.0

type Driver interface {
	Open(cfg Config, log *slog.Logger) (*gorm.DB, error)
}

Driver opens a *gorm.DB from a resolved Config. It is the extension point for plugging a database engine into the host. Implement Open and pass the value to gormx.Module (optionally named). The host owns config resolution and lifecycle (Close, health checks); a Driver is responsible only for dialing.

Use Postgres() or SQLite() for the built-in engines.

type ListRequest

type ListRequest[T any] interface {
	CurrentPageData() (where []any, err error)
	CreateNextPageToken(items []T) (*string, error)
	PageSize() int
	OrderBy() string
}

ListRequest is the interface Table.List requires. Use PageRequest[TEntity, TValue] for cursor-based pagination, or implement this interface directly for custom pagination strategies.

type OffsetPager added in v0.18.0

type OffsetPager interface {
	Offset() (offset int, ok bool)
}

OffsetPager is an optional interface for ListRequest. If implemented and Offset returns ok, Table.List uses offset pagination instead of cursor pagination: it skips offset rows, ignores the cursor entirely, and computes TotalCount so callers can render "page X of Y". Use this for user-driven page-number jumps.

type OrderByColumn added in v0.24.0

type OrderByColumn = clause.OrderByColumn

OrderByColumn is clause.OrderByColumn, the typed alternative to a raw "col DESC" string accepted by Table.Order.

type PageOptions added in v0.13.0

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

PageOptions configures a ListPage call. Build it fluently with NewPageOptions:

NewPageOptions[Order]().PageSize(20).OrderBy("created_at DESC")

For cursor-based pagination, also set Cursor (the current-page filter) and NextToken (builds the token for the following page):

NewPageOptions[Order]().
    PageSize(20).
    OrderBy("created_at DESC").
    Cursor(func() ([]any, error) {
        if token == "" {
            return nil, nil
        }
        return []any{"created_at < ?", token}, nil
    }).
    NextToken(func(items []Order) (*string, error) {
        s := items[len(items)-1].CreatedAt.Format(time.RFC3339)
        return &s, nil
    })

func NewPageOptions added in v0.13.0

func NewPageOptions[T any]() *PageOptions[T]

NewPageOptions returns an empty PageOptions ready to configure.

func (*PageOptions[T]) Cursor added in v0.13.0

func (o *PageOptions[T]) Cursor(fn func() ([]any, error)) *PageOptions[T]

Cursor sets the WHERE condition that filters to the current page; decode the page token here and return it as {query, args...}, or nil for the first page. It runs after the table's accumulated Where/WhereIf scopes.

func (*PageOptions[T]) NextToken added in v0.13.0

func (o *PageOptions[T]) NextToken(fn func(items []T) (*string, error)) *PageOptions[T]

NextToken sets the function that builds the next-page token from the page's items. When set, ListPage fetches one extra row to decide whether a next page exists.

func (*PageOptions[T]) OrderBy added in v0.13.0

func (o *PageOptions[T]) OrderBy(s string) *PageOptions[T]

OrderBy sets the ORDER BY clause and returns o for chaining.

func (*PageOptions[T]) PageSize added in v0.13.0

func (o *PageOptions[T]) PageSize(n int) *PageOptions[T]

PageSize sets the maximum number of rows returned and returns o for chaining.

type PageRequest

type PageRequest[TEntity any, TValue any] struct {
	*types.PagedResultRequest
	// contains filtered or unexported fields
}

PageRequest implements cursor-based pagination for Table.List. TEntity is the row type; TValue is the type of the cursor column (e.g. time.Time, int64). Using a typed cursor avoids the JSON float64 coercion issue that occurs with any.

Example:

req := db.NewPageRequest(
    pagedReq,                          // *types.PagedResultRequest from the HTTP handler
    "created_at",                      // cursor column
    func(e Order) time.Time { return e.CreatedAt },
).OrderDesc()

result, err := orderTable.Where("tenant_id = ?", tenantID).List(ctx, req)

func NewPageRequest

func NewPageRequest[TEntity any, TValue any](
	req *types.PagedResultRequest,
	cursorCol string,
	cursorFn func(TEntity) TValue,
) *PageRequest[TEntity, TValue]

NewPageRequest creates a PageRequest. cursorCol is the SQL column name used for ordering and cursor position (e.g. "id", "created_at"). cursorFn extracts the cursor value from a row; its return type must match TValue.

func (*PageRequest[TEntity, TValue]) CreateNextPageToken

func (r *PageRequest[TEntity, TValue]) CreateNextPageToken(items []TEntity) (*string, error)

func (*PageRequest[TEntity, TValue]) CurrentPageData

func (r *PageRequest[TEntity, TValue]) CurrentPageData() ([]any, error)

func (*PageRequest[TEntity, TValue]) Offset added in v0.18.0

func (r *PageRequest[TEntity, TValue]) Offset() (offset int, ok bool)

Offset reports the row offset for page-number pagination. ok is false when no Page is set, in which case Table.List uses cursor pagination instead. Page is 1-based; values below 1 are clamped to the first page.

func (*PageRequest[TEntity, TValue]) OrderBy

func (r *PageRequest[TEntity, TValue]) OrderBy() string

func (*PageRequest[TEntity, TValue]) OrderDesc

func (r *PageRequest[TEntity, TValue]) OrderDesc() *PageRequest[TEntity, TValue]

OrderDesc switches the sort order to descending and flips the cursor operator accordingly. By default results are ordered ascending.

func (*PageRequest[TEntity, TValue]) PageSize

func (r *PageRequest[TEntity, TValue]) PageSize() int

type Scoper added in v0.5.0

type Scoper interface {
	Scope() func(*gorm.DB) *gorm.DB
}

Scoper is an optional interface for ListRequest. If implemented, its Scope() is applied on top of the Table's accumulated Where/WhereIf scopes — use it for request-driven filtering that can't be expressed by chaining Where on the Table.

type Service added in v0.9.0

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

Service manages the lifecycle of a single *gorm.DB connection. It implements configx.Configurable, core.Initer, core.Closer, and core.HealthChecker, so the host drives config loading, dialing, and shutdown automatically. When a connection is provided up front (NewServiceFromDB), config loading and Init are no-ops, but Close and health checks still run.

func NewService added in v0.9.0

func NewService(name, section string, driver Driver) *Service

NewService creates a Service that opens a connection via driver during Init. name is used in health-check error messages; section is the config key (e.g. "database" or "database.secondary").

func NewServiceFromDB added in v0.9.0

func NewServiceFromDB(name string, db *gorm.DB) *Service

NewServiceFromDB creates a Service around an already-open *gorm.DB. The Service participates in Close and health checks but skips config loading and Init.

func (*Service) Close added in v0.9.0

func (s *Service) Close() error

Close implements core.Closer, closing the underlying SQL connection.

func (*Service) CloseOrder added in v0.19.0

func (s *Service) CloseOrder() int

CloseOrder closes the database late (as a backend), after the edges that use it (HTTP servers, subscribers) have drained.

func (*Service) DB added in v0.9.0

func (s *Service) DB() *gorm.DB

DB returns the underlying *gorm.DB, nil until Init is called.

func (*Service) Healthy added in v0.9.0

func (s *Service) Healthy(ctx context.Context) error

Healthy implements core.HealthChecker, pinging the underlying connection so the host's /readyz can report the database. A self-describing error names the database for multi-database setups.

func (*Service) Init added in v0.9.0

func (s *Service) Init() error

Init implements core.Initer. It is a no-op for injected connections.

func (*Service) ReadConfig added in v0.11.0

func (s *Service) ReadConfig(l configx.Reader) error

ReadConfig implements configx.Configurable. It is a no-op for injected connections.

func (*Service) SetClock added in v0.32.0

func (s *Service) SetClock(c core.TimeProvider)

SetClock sets the time source GORM uses to stamp CreatedAt/UpdatedAt, wiring it to db.Config.NowFunc. For driver-opened connections it takes effect during Init; for an already-open (injected) connection it applies immediately. A nil clock is ignored, leaving GORM's default (time.Now).

func (*Service) SetLogger added in v0.9.0

func (s *Service) SetLogger(l *slog.Logger)

SetLogger replaces the logger. A nil logger is ignored.

type Table

type Table[TEntity any] struct {
	// contains filtered or unexported fields
}

Table is a generic GORM wrapper for a single database table. Create one per entity type via NewTable. Call Preload to eager-load associations before executing a query. Call WhereIf to accumulate conditional WHERE clauses.

func NewTable

func NewTable[TEntity any](db *gorm.DB) *Table[TEntity]

NewTable creates a Table for TEntity backed by a database.

func NewTableFor

func NewTableFor[TEntity any](r *BaseRepository) *Table[TEntity]

NewTableFor creates a Table derived from the same database connection as the BaseRepository. Use this during repository construction so all tables share the same connection.

func (*Table[TEntity]) Aggregate added in v0.21.0

func (t *Table[TEntity]) Aggregate(ctx context.Context, expr string, dest any, where ...any) error

Aggregate runs an arbitrary SQL aggregate expression over the rows matching the optional where conditions and scans the scalar result into dest (a pointer). It is the general primitive behind Sum/Avg/Max/Min — reach for it directly for aggregates without a dedicated helper, e.g.:

var spread int
table.Aggregate(ctx, "MAX(price) - MIN(price)", &spread, "in_stock = ?", true)

Accepts the same GORM-style where conditions as Count and composes with accumulated Where/WhereIf scopes. The expression is raw SQL, so when it can be NULL (e.g. over an empty set) wrap it in COALESCE to give dest a default rather than failing the scan.

func (*Table[TEntity]) Avg added in v0.21.0

func (t *Table[TEntity]) Avg(ctx context.Context, column string, dest any, where ...any) error

Avg averages the numeric column across matching rows into dest (typically a *float64). An empty set yields zero rather than NULL (via COALESCE), so dest is always set.

func (*Table[TEntity]) Count

func (t *Table[TEntity]) Count(ctx context.Context, where ...any) (int64, error)

Count returns the number of rows matching the optional where conditions.

func (*Table[TEntity]) Create

func (t *Table[TEntity]) Create(ctx context.Context, item *TEntity) error

Create inserts a new row. GORM populates auto-generated fields (ID, CreatedAt, etc.) on item.

func (*Table[TEntity]) CreateMany

func (t *Table[TEntity]) CreateMany(ctx context.Context, items []*TEntity, batchSize int) (int64, error)

CreateMany inserts items in batches of batchSize and reports how many rows were inserted. Use for bulk inserts. The count equals len(items) on a plain insert, so it is mainly a check against a conflict clause silently skipping rows.

func (*Table[TEntity]) Delete added in v0.11.0

func (t *Table[TEntity]) Delete(ctx context.Context, conditions ...any) (int64, error)

Delete removes rows matching conditions and reports how many rows were deleted. Accepts the same GORM condition formats as Find.

The count distinguishes "the row was already gone" from "the delete failed": a zero return with a nil error means nothing matched, which is how a caller detects an idempotent re-delete or a WHERE guard that rejected the row. Callers that don't need the count can discard it.

On a soft-delete entity (one carrying gorm.DeletedAt) this is the number of rows marked deleted, not physically removed; chain Unscoped for a hard delete.

func (*Table[TEntity]) Distinct added in v0.20.0

func (t *Table[TEntity]) Distinct(args ...any) *Table[TEntity]

Distinct returns a copy of the Table that selects distinct rows, optionally over the given columns. With no args it applies SELECT DISTINCT to the selected columns.

func (*Table[TEntity]) Exec added in v0.24.0

func (t *Table[TEntity]) Exec(ctx context.Context, sql string, values ...any) (int64, error)

Exec runs a raw SQL statement that returns no rows (INSERT/UPDATE/DELETE/DDL) and reports how many rows were affected. Like Raw, the SQL is engine-specific, the Table's builder scopes do not apply, and it participates in the ctx transaction when present.

func (*Table[TEntity]) Exists added in v0.20.0

func (t *Table[TEntity]) Exists(ctx context.Context, where ...any) (bool, error)

Exists reports whether any row matches the optional where conditions (combined with the Table's accumulated scopes).

func (*Table[TEntity]) FindInBatches added in v0.24.0

func (t *Table[TEntity]) FindInBatches(ctx context.Context, batchSize int, fn func(batch []*TEntity) error) (int64, error)

FindInBatches streams matching rows to fn in batches of batchSize, so large result sets can be processed without loading every row into memory at once, and reports how many rows were processed in total across all batches. Chain Where/WhereIf/Order/Preload on the Table to scope the query first. fn is called once per batch; returning a non-nil error from it stops the iteration and surfaces that error, and the count then covers only the batches processed up to that point. The batch slice is GORM-managed and reused between calls — copy out any elements you need to keep beyond the current call.

func (*Table[TEntity]) First added in v0.20.0

func (t *Table[TEntity]) First(ctx context.Context, conditions ...any) (*TEntity, error)

First returns the first matching row ordered by primary key, or by the Table's accumulated Order when set, or nil if none matches (no error). Use Get when a missing row should be an error. Accepts GORM condition formats: primary key value, "col = ?", or map[string]any.

func (*Table[TEntity]) Get added in v0.20.0

func (t *Table[TEntity]) Get(ctx context.Context, conditions ...any) (*TEntity, error)

Get is like First but treats a missing row as an error: it returns an errorx.ErrNotFound (a NotFound-category *errorx.Error, subject set to the entity type name) when no row matches. Use Get when the row must exist — e.g. loading by primary key behind a request handler, where absence should surface as a 404 — and use First when absence is an expected outcome. Other database errors are returned unchanged. Match with errors.Is(err, errorx.ErrNotFound).

func (*Table[TEntity]) Group added in v0.20.0

func (t *Table[TEntity]) Group(name string) *Table[TEntity]

Group returns a copy of the Table with a GROUP BY clause. Combine with Having and Select for aggregate queries.

func (*Table[TEntity]) Having added in v0.20.0

func (t *Table[TEntity]) Having(query any, args ...any) *Table[TEntity]

Having returns a copy of the Table with a HAVING clause, filtering grouped rows. query and args accept the same formats as gorm.DB.Having.

func (*Table[TEntity]) Joins added in v0.20.0

func (t *Table[TEntity]) Joins(query string, args ...any) *Table[TEntity]

Joins returns a copy of the Table with a JOIN clause. query and args accept the same formats as gorm.DB.Joins: a raw join expression, or an association name for a struct join.

func (*Table[TEntity]) Last added in v0.20.0

func (t *Table[TEntity]) Last(ctx context.Context, conditions ...any) (*TEntity, error)

Last returns the last matching row ordered by primary key, or by the Table's accumulated Order (reversed) when set, or nil if none matches (no error). Accepts the same GORM condition formats as First.

func (*Table[TEntity]) Limit added in v0.20.0

func (t *Table[TEntity]) Limit(n int) *Table[TEntity]

Limit returns a copy of the Table capped to n rows. A negative n cancels a previously applied limit. The cap applies to multi-row reads (Find, ListAll); the single-row getters (First, Last, Take) already limit to one row.

func (*Table[TEntity]) List

func (t *Table[TEntity]) List(ctx context.Context, req ListRequest[TEntity]) (*types.PagedResult[TEntity], error)

List returns one page of results. TotalCount is always -1 (not computed). Filter and eager-load by chaining Where/WhereIf/Preload on the Table before calling; the request supplies page size, ordering, and the cursor. If the request implements Scoper, its Scope() is applied on top of those scopes. If it implements CursorScoper, its CursorScope() replaces CurrentPageData().

func (*Table[TEntity]) ListAll

func (t *Table[TEntity]) ListAll(ctx context.Context) ([]*TEntity, error)

ListAll returns all matching rows. Use only when the result set is known to be small. Chain Where/WhereIf/Order/Preload on the Table to filter, sort, and eager-load before calling; without an Order the row order is database-defined.

func (*Table[TEntity]) ListPage added in v0.13.0

func (t *Table[TEntity]) ListPage(ctx context.Context, opts *PageOptions[TEntity]) (*types.PagedResult[TEntity], error)

ListPage returns one page of results using the table's accumulated scopes. Chain Where/WhereIf/Preload on the Table to filter and eager-load before calling. Set Cursor/NextToken on opts for cursor-based pagination; otherwise NextPageToken is nil.

func (*Table[TEntity]) LockForUpdate added in v0.24.0

func (t *Table[TEntity]) LockForUpdate() *Table[TEntity]

LockForUpdate returns a copy of the Table whose reads emit SELECT … FOR UPDATE, taking a row-level pessimistic lock on the matched rows until the surrounding transaction commits or rolls back. Use it inside RunTx for read-modify-write sequences that can't be expressed as a single guarded UpdateMap, e.g.:

repo.RunTx(ctx, func(ctx context.Context) error {
    acct, err := t.LockForUpdate().Get(ctx, "id = ?", id) // row locked here
    if err != nil {
        return err
    }
    acct.Balance = recompute(acct.Balance)
    _, err = t.Update(ctx, acct)
    return err
})

Engine caveat: this is a Postgres/MySQL feature. The SQLite driver silently drops the locking clause (the query still runs, but no lock is taken), so on SQLite the lock provides no protection — write safety there comes from SQLite serializing writers at the transaction level instead. Outside a transaction the clause is a no-op on every engine, since any lock would be released immediately.

Prefer a single guarded UpdateMap (see its docs) when the change can be expressed as a SQL expression — it is atomic and portable without any locking.

func (*Table[TEntity]) LockForUpdateSkipLocked added in v0.31.0

func (t *Table[TEntity]) LockForUpdateSkipLocked() *Table[TEntity]

LockForUpdateSkipLocked is LockForUpdate with SKIP LOCKED: reads emit SELECT … FOR UPDATE SKIP LOCKED, taking row-level locks on the matched rows but skipping (rather than waiting on) any row another transaction already locks. Use it inside a transaction so competing workers each claim a disjoint set of rows — the standard way to fan a queue or outbox table out across several concurrent consumers without any two of them grabbing the same row.

Engine caveat: SKIP LOCKED is a Postgres/MySQL 8+ feature. The SQLite driver silently drops the locking clause, so no rows are skipped there and every worker sees the whole set — gate on db.Dialector.Name() and keep a single-consumer path for SQLite. Outside a transaction the clause is a no-op on every engine, since any lock would be released immediately.

func (*Table[TEntity]) Max added in v0.21.0

func (t *Table[TEntity]) Max(ctx context.Context, column string, dest any, where ...any) error

Max writes the largest value of the numeric column across matching rows into dest. An empty set yields zero rather than NULL (via COALESCE), so dest is always set.

func (*Table[TEntity]) Min added in v0.21.0

func (t *Table[TEntity]) Min(ctx context.Context, column string, dest any, where ...any) error

Min writes the smallest value of the numeric column across matching rows into dest. An empty set yields zero rather than NULL (via COALESCE), so dest is always set.

func (*Table[TEntity]) Offset added in v0.20.0

func (t *Table[TEntity]) Offset(n int) *Table[TEntity]

Offset returns a copy of the Table that skips the first n rows. A negative n cancels a previously applied offset. Pair with Order and Limit for manual offset paging; for page-number jumps prefer the OffsetPager path on List.

func (*Table[TEntity]) Omit added in v0.27.0

func (t *Table[TEntity]) Omit(columns ...string) *Table[TEntity]

Omit returns a copy of the Table that omits the given columns, wrapping GORM's Omit. On reads it excludes those columns from the SELECT; on Create/Save/Update it excludes them from the written columns. It is the complement of Select. Pass Associations ("*") to skip auto-saving all associations on a write.

func (*Table[TEntity]) OmitAssociations added in v0.32.0

func (t *Table[TEntity]) OmitAssociations() *Table[TEntity]

OmitAssociations returns a copy of the Table that skips auto-saving associations on a Create/Save/Update — only the entity's own columns are written. It is shorthand for Omit(Associations).

Use it when the entity was loaded with associations preloaded but only its own row changed: GORM would otherwise re-upsert each association with ON CONFLICT DO NOTHING, which is wasted work and, since it never updates an existing associated row, a trap if you expected those child changes to persist (save them explicitly instead).

func (*Table[TEntity]) Order added in v0.20.0

func (t *Table[TEntity]) Order(value any) *Table[TEntity]

Order returns a copy of the Table with an ORDER BY clause. Calls accumulate, so multiple Order calls produce ordering by the first column, then the second, etc. value accepts the same formats as gorm.DB.Order: a string ("created_at DESC") or a clause.OrderByColumn. The accumulated order applies to every read on the Table, including First, Last, Find, ListAll, and ListPage.

func (*Table[TEntity]) Pluck added in v0.24.0

func (t *Table[TEntity]) Pluck(ctx context.Context, column string, dest any, where ...any) error

Pluck collects the values of a single column into dest (a pointer to a slice), one element per matching row with duplicates preserved. Accepts optional GORM-style where conditions to scope the query. Use PluckDistinct to deduplicate.

func (*Table[TEntity]) PluckDistinct

func (t *Table[TEntity]) PluckDistinct(ctx context.Context, column string, dest any, where ...any) error

PluckDistinct collects unique values from column into dest (a pointer to a slice). Accepts optional GORM-style where conditions to scope the query.

func (*Table[TEntity]) Preload

func (t *Table[TEntity]) Preload(association string, args ...any) *Table[TEntity]

Preload returns a copy of the Table that eager-loads association on every query. args mirrors GORM's Preload: a SQL condition string + values, or a func(*gorm.DB) *gorm.DB, or nothing for an unconditional preload. Use clause.Associations ("*") to preload all. Multiple calls accumulate; preloads do not modify the original Table.

func (*Table[TEntity]) Project added in v0.21.0

func (t *Table[TEntity]) Project(ctx context.Context, dest any) error

Project runs the Table's query — including any chained Select/Where/Order/Group/Joins/etc scopes — but maps the rows into dest instead of the entity type. dest is a pointer to a slice of your result type. Use it for read models / DTOs that don't match the entity struct: pair it with Select to pick or compute the columns, which map onto dest's fields by name (or `gorm:"column:..."` tag). dest may also be a *[]map[string]any to receive rows as column→value maps when the shape isn't known ahead of time. Preloads are ignored — associations belong to the entity type, not the projection.

type tally struct {
    CampaignID uint
    Total      uint64
}
var rows []tally
err := table.Select("campaign_id, COALESCE(SUM(amount), 0) AS total").
    Where("is_confirmed = ?", true).
    Group("campaign_id").
    Project(ctx, &rows)

func (*Table[TEntity]) ProjectFirst added in v0.21.0

func (t *Table[TEntity]) ProjectFirst(ctx context.Context, dest any) (bool, error)

ProjectFirst is the single-row form of Project: it maps the first matching row into dest (a pointer to your result struct) and reports whether a row was found. No match leaves dest untouched and returns (false, nil) — the missing row is not an error. Chain Order on the Table to control which row "first" selects; otherwise it is the entity's primary key.

func (*Table[TEntity]) Raw added in v0.24.0

func (t *Table[TEntity]) Raw(ctx context.Context, dest any, sql string, values ...any) error

Raw runs a raw SQL query and scans the result into dest — a pointer to a struct, slice of structs, map, or scalar. Use it as an escape hatch for queries the Table builder can't express (CTEs, window functions, engine-specific syntax). The SQL is sent verbatim, so it is engine-specific, and the Table's accumulated Where/Order/ Preload scopes do NOT apply. It runs on the transaction in ctx when one is present (see RunTx).

var report []SalesRow
err := t.Raw(ctx, &report, "SELECT region, SUM(total) AS total FROM orders GROUP BY region")

func (*Table[TEntity]) Save

func (t *Table[TEntity]) Save(ctx context.Context, item *TEntity) (int64, error)

Save performs a full upsert — all fields are written — and reports how many rows were written. Use Update or UpdateMap for partial updates.

func (*Table[TEntity]) Select added in v0.20.0

func (t *Table[TEntity]) Select(query any, args ...any) *Table[TEntity]

Select returns a copy of the Table restricted to the given columns or expressions. query and args accept the same formats as gorm.DB.Select: column names, a comma list, or a raw expression with placeholders.

func (*Table[TEntity]) Sum added in v0.21.0

func (t *Table[TEntity]) Sum(ctx context.Context, column string, dest any, where ...any) error

Sum totals the numeric column across rows matching the optional where conditions, writing the result into dest (a pointer to a numeric type). No matching rows — or an empty table — yields zero rather than NULL (via COALESCE), so dest is always set.

func (*Table[TEntity]) Take added in v0.20.0

func (t *Table[TEntity]) Take(ctx context.Context, conditions ...any) (*TEntity, error)

Take returns a single matching row without any implicit ordering (the Table's accumulated Order still applies), or nil if none matches (no error). Use it when you want any one row and do not need primary-key ordering. Accepts the same GORM condition formats as First.

func (*Table[TEntity]) Unscoped added in v0.20.0

func (t *Table[TEntity]) Unscoped() *Table[TEntity]

Unscoped returns a copy of the Table that ignores GORM's soft-delete filter, so reads include soft-deleted rows and Delete becomes a permanent (hard) delete.

func (*Table[TEntity]) Update

func (t *Table[TEntity]) Update(ctx context.Context, item *TEntity) (int64, error)

Update performs a partial update — only non-zero fields in item are written — and reports how many rows were updated.

A zero count with a nil error means no row matched: the primary key on item is absent from the table, or an accumulated Where scope excluded it. Treat it as the not-found signal rather than assuming the write landed. Callers that don't need the count can discard it.

Only non-zero fields are written, so a field cleared to its zero value ("", 0, false) is skipped — use UpdateMap to write zero values explicitly.

func (*Table[TEntity]) UpdateColumn added in v0.24.0

func (t *Table[TEntity]) UpdateColumn(ctx context.Context, column string, value any, where ...any) (int64, error)

UpdateColumn sets a single column to value and reports how many rows were updated. Unlike UpdateMap it writes the raw column WITHOUT running model hooks or auto-updating tracked timestamps (e.g. UpdatedAt). Accepts optional GORM-style where conditions. Reach for it on bulk maintenance writes where hook/timestamp side effects are unwanted; prefer UpdateMap for ordinary updates.

func (*Table[TEntity]) UpdateColumns added in v0.24.0

func (t *Table[TEntity]) UpdateColumns(ctx context.Context, values map[string]any, where ...any) (int64, error)

UpdateColumns applies a column→value map and reports how many rows were updated. It is the multi-column form of UpdateColumn: like it, the raw columns are written WITHOUT model hooks or timestamp auto-updates — the no-side-effects counterpart to UpdateMap.

func (*Table[TEntity]) UpdateMap

func (t *Table[TEntity]) UpdateMap(ctx context.Context, values map[string]any, where ...any) (int64, error)

UpdateMap applies a column→value map as a partial update and reports how many rows were updated. Accepts optional GORM-style where conditions (same format as Find/Count) to scope which rows are updated.

The affected-rows count matters for capacity- or version-guarded updates whose WHERE clause may match no rows (e.g. an atomic counter increment bounded by a limit): a zero return means the guard rejected the update rather than a failure. Callers that don't need the count can discard it.

For lock-free atomic updates, pass a gormx.Expr value so the new value is computed by the database in-place rather than read-then-written, and put the precondition in the WHERE clause. The whole thing is a single atomic statement and is portable across engines (SQLite included):

n, err := t.UpdateMap(ctx,
    map[string]any{"used": gormx.Expr("used + 1")},
    "id = ? AND used < capacity", id)
// n == 0 means the row was missing or at capacity.

func (*Table[TEntity]) Upsert

func (t *Table[TEntity]) Upsert(ctx context.Context, item *TEntity) (int64, error)

Upsert inserts item or updates all columns if the primary key already exists, and reports how many rows were written. Uses a database-level ON CONFLICT clause, making it safe under concurrent writes. The count does not distinguish an insert from an update — both write one row.

func (*Table[TEntity]) Where added in v0.13.0

func (t *Table[TEntity]) Where(query any, args ...any) *Table[TEntity]

Where returns a copy of the Table with an additional WHERE clause. Calls accumulate; the original Table is not modified. Accepts the same formats as gorm.DB.Where.

func (*Table[TEntity]) WhereIf

func (t *Table[TEntity]) WhereIf(condition bool, where ...any) *Table[TEntity]

WhereIf returns a copy of the Table with an additional WHERE clause that is applied only when condition is true. Calls accumulate; the original Table is not modified.

Example:

results, err := table.
    WhereIf(req.TenantID != "", "tenant_id = ?", req.TenantID).
    WhereIf(req.Active, "active = true").
    Order("created_at DESC").
    ListAll(ctx)

Directories

Path Synopsis
postgres module
sqlite module

Jump to

Keyboard shortcuts

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