Skip to content

Context & session

Two small conventions run through the whole library and make it composable: every handle you pass to the front-ends is a Session, and every call that touches the database takes a context.Context. Because *DB and *BoundTx both satisfy Session, the same query builder and orm code runs unchanged on a connection or inside a transaction.

For exhaustive API detail, see the reference at pkg.go.dev/liteorm.org.

liteorm.Session is the handle the front-ends operate against. It is a small interface — a statement executor that also knows its dialect and logger:

type Session interface {
Querier // QueryContext / ExecContext
Dialect() dialect.Dialect
Logger() *slog.Logger
}

Two concrete types satisfy it, and nothing else needs to:

  • *liteorm.DB — the primary handle a backend’s Open returns (e.g. sqlite.Open). It runs each statement directly on the connection or pool.
  • *liteorm.BoundTx — a transaction, returned by db.Begin(ctx) or handed to your closure by liteorm.Transaction. It runs each statement inside that transaction, under the same logger and observers as the DB it came from.

Both the query and orm front-ends take a Session, never a concrete *DB:

func Select[T any](sess liteorm.Session) *query.SelectBuilder[T]
func NewRepo[T any](sess liteorm.Session) *orm.Repo[T]

Same code on a connection or a transaction

Section titled “Same code on a connection or a transaction”

That one interface is why your data-access code doesn’t fork on whether a transaction is in play. Write a function against liteorm.Session and it works identically either way — call it on the *DB for a standalone operation, or hand it the *BoundTx to fold it into a larger unit of work:

func topSpenders(ctx context.Context, sess liteorm.Session, n int) ([]Customer, error) {
return query.Select[Customer](sess).
OrderBy("lifetime_spend DESC").
Limit(n).
All(ctx)
}
// on a connection:
top, _ := topSpenders(ctx, db, 10)
// inside a transaction — identical function, transactional handle:
err := liteorm.Transaction(ctx, db, func(tx *liteorm.BoundTx) error {
top, err := topSpenders(ctx, tx, 10)
if err != nil {
return err
}
// ... more work on tx, all committed or rolled back together
return nil
})

A repository is just as portable — bind orm.NewRepo[T] to whichever session you have:

func transfer(ctx context.Context, sess liteorm.Session, from, to *Account) error {
accounts := orm.NewRepo[Account](sess)
if err := accounts.Update(ctx, from); err != nil {
return err
}
return accounts.Update(ctx, to)
}

Passing db runs each update on its own; passing a *BoundTx runs both in one transaction. The function body doesn’t change. See transactions for the transaction helpers, and query or orm? for mixing both front-ends on one session.

Every method that reaches the database takes a context.Context as its first argument — the query finishers (All, One, Count, Exists, Iter), every Repo method, AutoMigrate, the migration runner, Begin/Commit/Rollback. Pass the request’s context (or a derived one) down; don’t reach for context.Background() except at a true top level.

users, err := query.Select[User](db).Where("active").All(ctx)
err = orm.NewRepo[Order](db).Create(ctx, &order)
tx, err := db.Begin(ctx)

The context you pass is threaded to the underlying driver’s QueryContext / ExecContext, so it carries all the way to the database call.

Because the context reaches the driver, a deadline or cancellation on it propagates to the running statement:

ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
rows, err := query.Select[Report](db).All(ctx) // aborts if it overruns 2s

When the context is cancelled or its deadline passes, the in-flight query is cancelled and the call returns the context’s error (context.Canceled or context.DeadlineExceeded). The same holds inside a transaction: cancelling the context aborts the current statement, and you then Rollback. Streaming reads honor it continuously — a query.Iter sequence stops yielding once the context is done, so a cancelled request doesn’t keep pulling rows.

Give long-running or user-facing work a bounded context so a slow or abandoned query can’t hold a connection indefinitely.