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

A WHERE/HAVING fragment plus how it combines with the previous one.

pub type BoolExpr {
  BoolExpr(combine: Combine, condition: Condition)
}

Constructors

A set-combination appended to a query: UNION / UNION ALL with another query (Ecto’s union/union_all).

pub type Combination {
  Combination(op: SetOp, query: Query)
}

Constructors

pub type Combine {
  CombAnd
  CombOr
}

Constructors

  • CombAnd
  • CombOr

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

  • Cond(expr.Expr)
  • InSubquery(field: expr.Expr, subquery: Query, negate: Bool)
  • Exists(subquery: Query, negate: Bool)

    EXISTS (<subquery>) / NOT EXISTS (...) (Ecto’s exists(subquery(q))). The subquery is usually correlated — referencing an outer alias via expr.outer.

One named CTE attached to a query: WITH [RECURSIVE] <name> AS (<query>).

pub type Cte {
  Cte(name: String, query: Query, recursive: Bool)
}

Constructors

  • Cte(name: String, query: Query, recursive: Bool)
pub type From {
  From(source: Source, alias: String)
}

Constructors

  • From(source: Source, alias: String)
pub type Join {
  Join(
    kind: JoinKind,
    source: Source,
    alias: String,
    on: expr.Expr,
  )
}

Constructors

pub type JoinKind {
  InnerJoin
  LeftJoin
  RightJoin
  FullJoin
  CrossJoin
}

Constructors

  • InnerJoin
  • LeftJoin
  • RightJoin
  • FullJoin
  • CrossJoin
pub type OrderItem {
  Asc(expr.Expr)
  Desc(expr.Expr)
}

Constructors

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 standard WINDOW <name> AS (...) clause. Reference one from select with expr.over_named. The in-memory adapter ignores them.

    ctes

    Named common table expressions (Ecto’s with_cte), rendered as a single WITH [RECURSIVE] "name" AS (...), ... prefix before the statement. Reference one by name as an ordinary source. SELECT-only: update_all/delete_all ignore 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 like repo.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

pub type SetOp {
  Union
  UnionAll
}

Constructors

  • Union
  • UnionAll

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

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 asc(e: expr.Expr) -> OrderItem
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 desc(e: expr.Expr) -> OrderItem
pub fn distinct(query q: Query, on on: Bool) -> Query
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 group_by(
  query q: Query,
  by exprs: List(expr.Expr),
) -> Query
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 limit(query q: Query, to n: Int) -> Query
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 offset(query q: Query, by n: Int) -> Query
pub fn or_where(query q: Query, condition e: expr.Expr) -> Query

Add a WHERE condition, OR-combined with existing conditions.

pub fn order_by(
  query q: Query,
  by items: List(OrderItem),
) -> Query
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_.)

Search Document