///|
/// A boolean predicate tree — the parameter-safe way to express arbitrary
/// `AND`/`OR`/`NOT` groupings (SQLAlchemy's `and_()` / `or_()` / `not_()`), which the
/// flat `where_`/`eq` API (always joined by `AND`) cannot. Every comparison binds its
/// value as a placeholder, so a `(a = ? OR b = ?)` filter stays fully parameterised.
/// Attach one to a query with `Select::where_pred`.
pub(all) enum Predicate {
Cmp(col~ : String, op~ : String, val~ : Value)
InList(col~ : String, vals~ : Array[Value], negated~ : Bool)
Exists(sub~ : Select, negated~ : Bool)
And(Array[Predicate])
Or(Array[Predicate])
Not(Predicate)
}
///|
/// Render a predicate to its SQL fragment and the values it binds, in text order. A
/// multi-child `And`/`Or` is wrapped in parentheses so nesting composes correctly.
pub fn Predicate::build(self : Predicate) -> (String, Array[Value]) {
match self {
Cmp(col~, op~, val~) => {
let params : Array[Value] = []
let sql = cond_sql({ col, op, val, }, params)
(sql, params)
}
InList(col~, vals~, negated~) =>
// Empty `IN` matches nothing, empty `NOT IN` matches everything (SQLAlchemy's
// constant fold). `IN/NOT IN (NULL)` would get both wrong via three-valued
// NULL logic (`NOT IN (NULL)` is never true).
if vals.length() == 0 {
(if negated { "1 = 1" } else { "1 = 0" }, [])
} else {
let ph : Array[String] = []
for _v in vals {
ph.push("?")
}
let kw = if negated { " NOT IN (" } else { " IN (" }
(col + kw + ph.join(", ") + ")", vals)
}
Exists(sub~, negated~) => {
let (s, p) = sub.build()
((if negated { "NOT EXISTS (" } else { "EXISTS (" }) + s + ")", p)
}
And(ps) =>
if ps.length() == 0 {
("1 = 1", [])
} else {
predicate_combine(ps, " AND ")
}
Or(ps) =>
if ps.length() == 0 {
("1 = 0", [])
} else {
predicate_combine(ps, " OR ")
}
Not(p) => {
let (s, pa) = p.build()
("NOT (" + s + ")", pa)
}
}
}
///|
/// Join child predicates with `sep`, wrapping the group in parentheses when it has
/// more than one child, and concatenating their bound values in text order.
fn predicate_combine(
ps : Array[Predicate],
sep : String,
) -> (String, Array[Value]) {
let parts : Array[String] = []
let params : Array[Value] = []
for p in ps {
let (s, pa) = p.build()
parts.push(s)
for v in pa {
params.push(v)
}
}
let body = parts.join(sep)
(if parts.length() > 1 { "(" + body + ")" } else { body }, params)
}