///|
struct Cond {
col : String
op : String
val : Value
}
///|
/// Render one scalar comparison, pushing its bound value onto `params`. `= NULL` and
/// `!= NULL` never match under three-valued logic, so a NULL equality folds to
/// `IS NULL` / `IS NOT NULL` — SQLAlchemy's `== None` fold — and binds nothing.
fn cond_sql(c : Cond, params : Array[Value]) -> String {
if c.val is Null && (c.op == "=" || c.op == "!=" || c.op == "<>") {
return c.col + (if c.op == "=" { " IS NULL" } else { " IS NOT NULL" })
}
params.push(c.val)
c.col + " " + c.op + " ?"
}
///|
/// A `col IN (subquery)` / `col NOT IN (subquery)` predicate: the subquery is a
/// full `Select`, whose bound parameters splice in at the point the predicate is
/// rendered. Keeping subquery predicates separate from the scalar `conds` lets the
/// builder concatenate the two params streams in the exact order the SQL text emits
/// them (scalar predicates first, then subqueries), so binding stays index-aligned.
priv struct SubCond {
col : String
op : String
sub : Select
}
///|
/// A `col IN (?, ?, …)` / `col NOT IN (?, …)` predicate over an explicit value list
/// (as opposed to `SubCond`, whose right-hand side is a subquery). Each value binds
/// as its own placeholder, so a set-membership filter — the workhorse of an
/// N+1-avoiding batch load, `WHERE fk IN (all the parent keys)` — stays fully
/// parameterised.
priv struct InCond {
col : String
negated : Bool
vals : Array[Value]
}
///|
/// The WHERE clause of any statement — SELECT, UPDATE and DELETE share it, so the
/// three render identical SQL and bind their values in one order rather than three.
struct Where {
conds : Array[Cond]
in_conds : Array[InCond]
sub_conds : Array[SubCond]
preds : Array[Predicate]
}
///|
fn Where::new() -> Where {
{ conds: [], in_conds: [], sub_conds: [], preds: [], }
}
///|
fn Where::dup(self : Where) -> Where {
{
conds: self.conds.copy(),
in_conds: self.in_conds.copy(),
sub_conds: self.sub_conds.copy(),
preds: self.preds.copy(),
}
}
///|
/// The conjunction of every attached predicate, or `""` when there is none, pushing
/// bound values onto `params` in text order: scalar comparisons, then `IN (values)`,
/// then subqueries, then predicate trees.
fn Where::sql(self : Where, params : Array[Value]) -> String {
let parts : Array[String] = []
for c in self.conds {
parts.push(cond_sql(c, params))
}
for ic in self.in_conds {
// Empty `IN` matches nothing, empty `NOT IN` matches everything — a constant
// predicate, not `IN (NULL)` (which is never true under NULL logic).
if ic.vals.length() == 0 {
parts.push(if ic.negated { "1 = 1" } else { "1 = 0" })
} else {
let placeholders : Array[String] = []
for v in ic.vals {
placeholders.push("?")
params.push(v)
}
let op = if ic.negated { "NOT IN" } else { "IN" }
parts.push(ic.col + " " + op + " (" + placeholders.join(", ") + ")")
}
}
for sc in self.sub_conds {
let (sub_sql, sub_params) = sc.sub.build()
parts.push(sc.col + " " + sc.op + " (" + sub_sql + ")")
for p in sub_params {
params.push(p)
}
}
for pred in self.preds {
let (ps, pp) = pred.build()
parts.push(ps)
for p in pp {
params.push(p)
}
}
if parts.length() == 0 {
""
} else {
parts.join(" AND ")
}
}