lode/query
The query value and its builder functions — the macro-free replacement for
Ecto’s from u in User, where: ..., select: ... DSL.
Bindings are positional integers: the from source is binding 0, each join
adds the next index. Typed per-schema field accessors (see lode/query/expr)
carry their own binding index, so query expressions are compile-checked.
query.from(“users”, “u”) // binding 0 = u |> query.where(expr.gt(user_age, 18)) |> query.where(expr.eq(user_active, True)) |> query.order_by([query.asc(expr.col(user_name))]) |> query.limit(10) |> query.select([expr.col(user_id), expr.col(user_name)])
Types
One column assignment in an update_all SET. SetValue is a literal
(col = $n); SetExpr is an arbitrary expression (col = <expr>),
including references to the row’s own columns (Col(0, "...")). Build with
set/set_expr/inc/push/pull.
pub type Assignment {
SetValue(column: String, value: value.Value)
SetExpr(column: String, expr: expr.Expr)
}
Constructors
-
SetValue(column: String, value: value.Value) -
SetExpr(column: String, expr: expr.Expr)
A single WHERE/HAVING condition: a plain boolean expression, or a
<field> IN (<subquery>) / NOT IN membership test against another query
(Ecto’s where: x in subquery(q)). The subquery is a Query, which is why
this lives here rather than in lode/query/expr — an expression can’t carry
a query without an import cycle.
pub type Condition {
Cond(expr.Expr)
InSubquery(field: expr.Expr, subquery: Query, negate: Bool)
Exists(subquery: Query, negate: Bool)
}
Constructors
pub type JoinKind {
InnerJoin
LeftJoin
RightJoin
FullJoin
CrossJoin
}
Constructors
-
InnerJoin -
LeftJoin -
RightJoin -
FullJoin -
CrossJoin
pub type Query {
Query(
from: From,
joins: List(Join),
wheres: List(BoolExpr),
selection: Selection,
group_bys: List(expr.Expr),
havings: List(BoolExpr),
order_bys: List(OrderItem),
limit: option.Option(Int),
offset: option.Option(Int),
distinct: Bool,
prefix: option.Option(String),
lock: option.Option(String),
combinations: List(Combination),
windows: List(WindowDef),
ctes: List(Cte),
preloads: List(preload.Preload),
)
}
Constructors
-
Query( from: From, joins: List(Join), wheres: List(BoolExpr), selection: Selection, group_bys: List(expr.Expr), havings: List(BoolExpr), order_bys: List(OrderItem), limit: option.Option(Int), offset: option.Option(Int), distinct: Bool, prefix: option.Option(String), lock: option.Option(String), combinations: List(Combination), windows: List(WindowDef), ctes: List(Cte), preloads: List(preload.Preload), )Arguments
- prefix
-
Optional Postgres schema/namespace prefix (Ecto’s query
:prefix). When set, every table in the query (FROM and joins) renders as"prefix"."table". The in-memory adapter ignores it. - lock
-
Row lock clause (Ecto’s
lock:), e.g."FOR UPDATE". Rendered verbatim at the end of the SELECT; the in-memory adapter ignores it. - combinations
-
Set-combinations (
UNION/UNION ALL) appended after this query. - windows
-
Named window declarations (Ecto’s
windows:), rendered as a standardWINDOW <name> AS (...)clause. Reference one fromselectwithexpr.over_named. The in-memory adapter ignores them. - ctes
-
Named common table expressions (Ecto’s
with_cte), rendered as a singleWITH [RECURSIVE] "name" AS (...), ...prefix before the statement. Reference one by name as an ordinary source. SELECT-only:update_all/delete_allignore them. The in-memory adapter does not evaluate them. - preloads
-
Associations to load onto the result rows (Ecto’s query
preload:). Adapters ignore this — the repo applies it after loading, batched per association exactly likerepo.preload.
What the query returns. SelectStar (the default) selects all columns of all
bindings; SelectExprs selects specific expressions in order.
NOTE: SelectStar over a JOIN emits a bare SELECT *, so same-named columns
from different bindings (e.g. users.id and posts.id) come back under the
same name-keyed result key and one clobbers the other. The join-safe path is
explicit SelectExprs of expr.col_as(field, prefix) projections, which
alias each column to prefix__name; split the row back with
schema.load_prefixed.
pub type Selection {
SelectStar
SelectExprs(List(expr.Expr))
}
Constructors
-
SelectStar -
SelectExprs(List(expr.Expr))
A FROM source: a table name, or a nested subquery (Ecto’s
from(x in subquery(inner))). A subquery source queries the result of
another query — e.g. aggregating over a limit/distinct-bounded set,
which a bare aggregate can’t express.
pub type Source {
Table(String)
Subquery(Query)
}
Constructors
-
Table(String) -
Subquery(Query)
A named window declaration: WINDOW <name> AS (PARTITION BY ... ORDER BY ...).
pub type WindowDef {
WindowDef(
name: String,
partition_by: List(expr.Expr),
order_by: List(expr.WindowOrder),
)
}
Constructors
-
WindowDef( name: String, partition_by: List(expr.Expr), order_by: List(expr.WindowOrder), )
Values
pub fn add_join(query q: Query, join j: Join) -> Query
Append a fully-built Join (used by association.join).
pub fn alias_for(
query q: Query,
binding binding: Int,
) -> Result(String, Nil)
The alias for a binding index, scanning from then joins in order.
pub fn clear_preloads(query q: Query) -> Query
Drop any attached preloads. repo.all uses this to run the underlying query
(the join-preload adds its own join/select) without recursing on preloads.
pub fn first(query q: Query, by field: expr.Expr) -> Query
Order by field ascending and take the first row (Ecto’s first/2): sets
the order to field and limit 1, replacing any existing order.
pub fn from(source source: String, alias alias: String) -> Query
Start a query from a source table with an explicit binding alias.
pub fn from_subquery(
subquery subquery: Query,
alias alias: String,
) -> Query
Start a query from a subquery (Ecto’s from(x in subquery(inner))). The
outer query reads the inner query’s result rows, so load them with a schema
matching the inner query’s projection.
pub fn having(query q: Query, condition e: expr.Expr) -> Query
Add a HAVING condition (AND-combined).
pub fn inc(
column column: String,
by amount: value.Value,
) -> Assignment
col = col + <amount> (Ecto’s inc:). A negative amount decrements.
pub fn join(
query q: Query,
kind kind: JoinKind,
source source: String,
alias alias: String,
on on: expr.Expr,
) -> Query
pub fn join_subquery(
query q: Query,
kind kind: JoinKind,
subquery subquery: Query,
alias alias: String,
on on: expr.Expr,
) -> Query
Join against a subquery source (Ecto’s join: x in subquery(inner)):
<kind> JOIN (<subquery>) AS alias ON <on>. Reference the joined subquery’s
columns by its alias at the next binding (expr.Col(last_binding + 1, ...)).
pub fn last(query q: Query, by field: expr.Expr) -> Query
Order by field descending and take the last row (Ecto’s last/2).
pub fn last_binding(q: Query) -> Int
The binding index of the most recently added join (the from is 0).
pub fn lock(query q: Query, clause clause: String) -> Query
Add a row lock clause (Ecto’s lock:), rendered verbatim at the end of the
SELECT — e.g. query.lock(q, "FOR UPDATE"). Use inside a repo.transaction.
The in-memory adapter ignores it.
pub fn or_where(query q: Query, condition e: expr.Expr) -> Query
Add a WHERE condition, OR-combined with existing conditions.
pub fn prefix(query q: Query, schema schema: String) -> Query
Set the Postgres schema prefix for this query (Ecto’s query :prefix). Every
table in the query renders as "<schema>"."<table>" — used for schema-based
multi-tenancy. The in-memory adapter ignores it.
pub fn preload(
query q: Query,
preloads preloads: List(preload.Preload),
) -> Query
Attach preloads to the query (Ecto’s from(...) |> preload(...)). When the
query runs through repo.all/repo.one, the named associations are loaded
onto the results in one batched query per association — equivalent to a
separate repo.preload call, just declared with the query.
pub fn pull(
column column: String,
value value: value.Value,
) -> Assignment
col = array_remove(col, <value>) (Ecto’s pull:), for an array column.
pub fn push(
column column: String,
value value: value.Value,
) -> Assignment
col = array_append(col, <value>) (Ecto’s push:), for an array column.
pub fn select(
query q: Query,
fields exprs: List(expr.Expr),
) -> Query
Select specific expressions (replaces any prior selection).
pub fn select_merge(
query q: Query,
fields exprs: List(expr.Expr),
) -> Query
Append more expressions to the current selection (like select_merge).
pub fn set(
column column: String,
to value: value.Value,
) -> Assignment
col = <value> (a literal).
pub fn set_expr(
column column: String,
to expr: expr.Expr,
) -> Assignment
col = <expr> — set a column from an expression, e.g. another column or a
function of one (Ecto’s set: [c: fragment(...)]). Reference the updated
row’s columns with expr.Col(0, "...").
pub fn set_values(
pairs pairs: List(#(String, value.Value)),
) -> List(Assignment)
Wrap literal column = value pairs as Assignments (the repo.update_all
convenience path).
pub fn union(query q: Query, with other: Query) -> Query
Combine with another query via UNION (Ecto’s union), de-duplicating
rows. An outer limit/offset applies to the whole result; to order a
union, wrap it with from_subquery and order the outer query (a UNION’s
output columns are not table-qualified, so a direct order_by can’t name
them).
pub fn union_all(query q: Query, with other: Query) -> Query
Combine with another query via UNION ALL (Ecto’s union_all), keeping
duplicate rows.
pub fn where(query q: Query, condition e: expr.Expr) -> Query
Add a WHERE condition, AND-combined with any existing conditions.
pub fn where_exists(
query q: Query,
subquery subquery: Query,
) -> Query
Add a WHERE EXISTS (<subquery>) condition (AND-combined). The subquery is
typically correlated — referencing this query’s binding aliases via
expr.outer(alias, name):
query.from(“posts”, “p”) |> query.where_exists( query.from(“comments”, “c”) |> query.where(expr.BinOp( expr.Eq, expr.Col(0, “post_id”), expr.outer(“p”, “id”))), )
pub fn where_in_subquery(
query q: Query,
field field: expr.Expr,
subquery subquery: Query,
) -> Query
Add a WHERE <field> IN (<subquery>) condition, AND-combined (Ecto’s
where: f in subquery(q)). field is an expression (typically
expr.col(...)); subquery should select the single column to match.
pub fn where_not_exists(
query q: Query,
subquery subquery: Query,
) -> Query
Add a WHERE NOT EXISTS (<subquery>) condition (AND-combined). See
where_exists.
pub fn where_not_in_subquery(
query q: Query,
field field: expr.Expr,
subquery subquery: Query,
) -> Query
Add a WHERE <field> NOT IN (<subquery>) condition, AND-combined.
pub fn window(
query q: Query,
name name: String,
partition_by partition_by: List(expr.Expr),
order_by order_by: List(expr.WindowOrder),
) -> Query
Declare a named window (Ecto’s windows: [w: [partition_by: ...]]) and
reference it from select with expr.over_named(call, "w"):
query.from(“scores”, “s”) |> query.window(“w”, partition_by: [expr.col(team())], order_by: []) |> query.select([expr.as_(expr.over_named(expr.rank(), “w”), “r”)])
Renders between HAVING and ORDER BY. SQL-only — the in-memory adapter does
not evaluate windows. Frames (ROWS BETWEEN ...) are not modeled.
pub fn with_cte(
query q: Query,
name name: String,
as_ body: Query,
recursive recursive: Bool,
) -> Query
Attach a named common table expression (Ecto’s with_cte/3), rendered as
WITH "name" AS (<body>) before the statement — WITH RECURSIVE when any
attached CTE has recursive: True (one keyword covers the whole list, as in
both engines’ SQL). The CTE’s parameters render before the main statement’s,
taking the first $n/?n numbers. Reference the CTE by name as an ordinary
source: query.from(source: name, ...) or a join.
A recursive CTE’s body is base |> query.union_all(step) where the step
references the CTE’s own name:
let base = query.from(“nodes”, “n”) |> query.where(expr.is_nil(node_parent())) let step = query.from(“nodes”, “n”) |> query.join(query.InnerJoin, “tree”, “t”, expr.eq_col(node_parent(), tree_id())) query.from(“tree”, “t”) |> query.with_cte(name: “tree”, as_: base |> query.union_all(step), recursive: True) // WITH RECURSIVE “tree” AS (SELECT … UNION ALL SELECT …) SELECT …
Inside a CTE body, union operands render WITHOUT parentheses (SQLite rejects
parenthesized compound operands, and the recursive form requires the bare
shape) — so a CTE body’s union operands must not carry their own
order_by/limit. SELECT-only: update_all/delete_all ignore attached
CTEs. SQL-only — the in-memory adapter does not evaluate CTEs. Column
aliases (WITH name (cols) AS) are not modeled: name the columns with an
explicit select in the body. Don’t combine with query.prefix on the
query that reads FROM the CTE (the prefix would schema-qualify the CTE name).
(The label is as_ because as is a Gleam keyword — same as expr.as_.)