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.
The Session concept
Section titled “The Session concept”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’sOpenreturns (e.g.sqlite.Open). It runs each statement directly on the connection or pool.*liteorm.BoundTx— a transaction, returned bydb.Begin(ctx)or handed to your closure byliteorm.Transaction. It runs each statement inside that transaction, under the same logger and observers as theDBit 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.
Context goes to every call
Section titled “Context goes to every call”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.
Deadlines and cancellation
Section titled “Deadlines and cancellation”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 2sWhen 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.
See also
Section titled “See also”- Transactions —
Transaction/TransactionRetryand the*BoundTxhandle. - Query or ORM? — both front-ends share the one Session.
- The query builder and the orm front-end — what you run on a session.
- Concurrency & locking — retrying a transaction on cancellation-adjacent failures.