Expression filters¶
FilterRows accepts an expression predicate built with rypipe.expr.col.
Expressions construct the same filter spec dicts the Rust engine fuses,
declaratively and without any bytecode analysis:
from rypipe import col, FilterRows
source | FilterRows(col("amount") > 100)
source | FilterRows((col("age") >= 18) & col("name").startswith("A"))
Anything the spec language cannot express raises at construction time, so there is no silent fallback to per-row Python.
How it works¶
col("amount") > 100
│
▼
Predicate({"field": "amount", "op": ">", "value": "100"})
│
▼
FilterRows(predicate) → _plan_kwargs() → {"filter": spec}
│
▼
ExecutionPlan.filter = CompareLiteral { field: "amount", op: CompareOp::Gt, value: "100" }
An expression compares to a literal or another column; the result is a
Predicate whose _to_spec() produces the plan spec. Predicate objects
compose with & (and), | (or), and ~ (not), producing nested
{"and": [...]}, {"or": [...]}, {"not": ...} specs.
Supported operations¶
| Expression | Spec |
|---|---|
col("x") == 1, !=, >, <, >=, <= |
{"field": "x", "op": "==", "value": "1"} |
col("a") > col("b") |
{"field_a": "a", "op": ">", "field_b": "b"} |
col("s").startswith("A") |
{"op": "starts_with", ...} |
col("s").endswith("z") |
{"op": "ends_with", ...} |
col("s").contains("x") |
{"op": "contains", ...} |
col("s").matches(r"^ERR\d+$") |
{"op": "regex", ...} |
col("n").between(2, 6) |
{"and": [{">="}, {"<="}]} (inclusive) |
col("s").isin(["a", "b"]) |
{"op": "in", "values": [...]} |
col("s").not_in(["a"]) |
{"op": "not_in", "values": [...]} |
col("x").is_null() |
{"op": "is_null"} |
col("x").is_not_null() |
{"not": {"op": "is_null"}} |
col("x").is_type("int64") |
{"op": "is_type", "value": "int64"} |
p & q, p \| q, ~p |
{"and": ...}, {"or": ...}, {"not": ...} |
Literals may be int, float, str, or bool; they are coerced to the
string form specs carry, and the engine compares with native-typed numeric
promotion. matches() validates the pattern with re.compile at
construction time and the engine applies it as a regex search against the
string form of the value.
Plain callables fall back to Python¶
A plain lambda or function still works as a FilterRows predicate, but it
is opaque to fusion: it runs in Python per row after the table is parsed.
Earlier versions shipped an in-tree bytecode analyzer that tried to compile
lambdas into specs; that was removed in favor of this explicit expression
API. If you want lambda compilation, use the standalone
lambda-compiler package.
# Runs in Python; not fusable
f = FilterRows(lambda r: r["name"].strip().lower() == "alice")
# f._filter_spec is None (not fusable)
# f._predicate is the original lambda (runs in Python)
The filter still works correctly, just slower. For large files, rewrite the
predicate with col(...) or the keyword form.
Diagnostics¶
Inspect the spec to confirm a predicate is fusable:
f = FilterRows(col("amount") > 100)
print(f._filter_spec)
# {'field': 'amount', 'op': '>', 'value': '100'}
f2 = FilterRows(lambda r: r["name"].strip().lower() == "alice")
print(f2._filter_spec)
# None (Python fallback)
Recap¶
col(name)starts an expression; comparisons and methods producePredicateobjects that carry a fusable spec.Predicatecomposes with&,|,~into arbitrarily nested trees.- Regex (
matches) and inclusive ranges (between) are first-class. - Plain lambdas are never analyzed; they run in Python as a fallback.