Skip to content

Raw SQL

The query builder covers a wide surface, but when you need SQL it doesn’t model — an engine-specific construct, a query hint, a hand-tuned statement — drop to query.Raw[T]. It runs your SQL with bound args and scans the rows into any result type T. This is the deliberate escape hatch, not a fallback for missing features: everything on the advanced SQL page is typed and validated, so reach for Raw only past that edge.

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

query.Raw[T] runs a statement and maps each row into a struct T — usually a small struct shaped to the projection, with db:"..." tags where the column names differ from the fields:

type catStat struct {
Category string `db:"category"`
Items int64 `db:"items"`
Total int64 `db:"total"`
}
stats, err := query.Raw[catStat](ctx, db,
`SELECT category, count(*) AS items, sum(stock) AS total
FROM products GROUP BY category ORDER BY total DESC`)

Args bind positionally with ? markers and are renumbered for the dialect, so your SQL stays parameterized on every backend — pass values as arguments, never interpolate them into the string.

Raw[T] expects T to be a struct. For a single-column scalar, use query.Pluck (a typed column) or query.PluckExpr / PluckExprFirst (a raw expression) instead — they scan into []V / V. See streaming & aggregates.

You rarely need a fully raw statement. Most one-off SQL fits inside a typed builder as a fragment, so you keep the typed predicates, ordering, and scanning around it:

  • Where("frag", args...) adds a raw, AND-joined predicate — for a condition that spans joined tables or uses SQL the typed predicates don’t cover.
  • OrderBy("...") / GroupBy("...") take raw terms (a collation, NULLS FIRST, a function).
  • Project("...") overrides the SELECT list; Expr("...") is the raw item inside an Into projection.
  • Having("frag", args...) and a raw Join("clause", args...) cover the remaining clauses.
recent, err := query.Select[Product](db).
Filter(query.Col[bool]("active").Eq(true)). // typed
Where("created_at > ?", cutoff). // raw fragment
OrderBy("created_at DESC NULLS LAST"). // raw ordering
All(ctx)

A raw fragment is not column-validated — a typo in a Where string fails at the database, not at build time, unlike a typed Filter predicate. Keep as much of the query typed as you can, and reserve raw fragments for the piece that genuinely needs them.

The builder deliberately has no API for engine-specific query hints (index hints, FORCE INDEX, optimizer directives, and the like). They’re non-portable by nature, so rather than model them per dialect, LiteORM leaves them to Raw — write the full statement with the hint inline and scan it into your struct. The same goes for any other backend-specific syntax the typed API doesn’t expose.

  • Advanced SQL — CTEs, window functions, set ops, and locking, all typed (try these before Raw).
  • Streaming & aggregatesPluck / PluckExpr for single-column raw reads.
  • Reading data — the typed predicates a raw Where composes with.
  • Errors — how a failing raw statement surfaces.