beava._col
bv.col — column expressions
bv.col references a schema field; bv.lit wraps a Python constant. Together they build the operator-overloaded _Expr AST that powers where= predicates, with_columns derivations, and operator field selection in the chain DSL.
Overview
Every predicate in beava — every where= kwarg, every filter clause, every derived-column expression — is built from two primitives: bv.col(name) for a field reference, and bv.lit(value) for a constant. Combine them with Python operators (==, >, &, |, ~) to produce an _Expr AST node. The AST is inert at construction time; the server's expression parser evaluates it at push time.
bv.count( window="1h", where=bv.col("amount") > 100, )
pv.with_columns( is_premium=bv.col("plan") == bv.lit("premium"), )
Both helpers return _Expr nodes (subclasses _Col and _Literal). _Expr overloads Python's comparison, arithmetic, and bitwise operators to grow the AST; you never construct nodes by hand.
bv.col(name)
Reference a schema field by name. Returns an _Expr — an unbound expression node that participates in operator overloads. The lookup is unresolved until the expression reaches the server; bv.col("nonexistent") only fails at register / push time when the schema check runs.
Args
name— the field name as declared on the upstream event class. Composite keys are addressed by their per-column names; there is no nested-field accessor.
Returns
_Col — a frozen-dataclass _Expr node. Hashable, immutable, safe to reuse across multiple chain expressions.
Example
import beava as bv amount = bv.col("amount") expr = amount > 100 print(expr.to_expr_string()) # → '(amount > 100)'
bv.lit(value)
Wrap a Python constant as an explicit literal expression. The implicit form (bv.col("a") > 100) and the explicit form (bv.col("a") > bv.lit(100)) are wire-equivalent inside operator overloads — Python's dispatch coerces the right-hand side automatically. bv.lit is required when the literal stands on its own (a constant column in with_columns, the left-hand side of an arithmetic expression, or the only operand of a derivation).
bv.lit is the canonical way to write a comparison-against-constant in any where= kwarg — bare strings are rejected (see the where= contract).
Args
value— a Python scalar. Accepted types:int,float,str,bool,None. Other types (e.g.list,dict, custom classes) are not supported by the server's expression grammar.
Returns
_Literal — a frozen-dataclass _Expr node. Renders to the wire-grammar form: single-quoted strings, true/false for booleans, null for None.
Example
import beava as bv bv.lit(42).to_expr_string() # → '42' bv.lit("web").to_expr_string() # → "'web'" bv.lit(None).to_expr_string() # → 'null' bv.lit(True).to_expr_string() # → 'true'
Expression DSL
An _Expr grows by operator overload. Every overload returns a new _Expr — the AST is immutable, so re-using a node in multiple chains is safe.
Comparison
Each comparison operator emits a _BinOp node. Both operands are _Expr; bare Python literals on either side are auto-wrapped via _coerce.
Equality and inequality. bv.col("plan") == bv.lit("premium") renders as (plan == 'premium').
Ordered comparison. Numeric on both sides; the server rejects ordered comparison of strings.
Logical combinators
Python's and / or can't be overloaded — they short-circuit on truthiness. Beava overloads & / | instead and serializes them as the server-grammar keywords and / or. Always parenthesize — Python's bitwise operators bind tighter than comparison.
Logical AND. Wire form: (left and right).
Logical OR. Wire form: (left or right).
Logical NOT. Wire form: !(operand).
expr = (bv.col("amount") > 100) & (bv.col("plan") == "premium") expr.to_expr_string() # → "((amount > 100) and (plan == 'premium'))" negated = ~(bv.col("path") == "/health") negated.to_expr_string() # → "!((path == '/health'))"
Methods
Cast the operand to one of "str", "int", "float", "bool". Any other target raises ValueError at construction time (client-side guard). Wire form: cast(operand, target).
Null-check. Wire form: (operand == null). Combine with ~ for an "is not null" check: ~bv.col("x").isnull().
Render the AST to its wire-grammar JSON form. Stable string representation; safe to use in tests.
The where= contract
Every kwarg that takes a predicate — every aggregation operator's where=, every filter clause — accepts an _Expr and only an _Expr. Bare strings are rejected. The check lives in _serialize_where on every aggregation operator: anything that isn't None or an _Expr raises TypeError client-side, before a single byte hits the wire.
bv.count(where="amount > 0") # TypeError: where= must be an _Expr or None; # got str
bv.count(where=bv.col("amount") > 0) # wire: where = "(amount > 0)"
Why bare strings are blocked. A free-form predicate string would force the SDK to either ship a Python-side parser (hidden complexity, divergence risk against the server grammar) or pass the string through unchecked (typos surface only at push-time on the server). The _Expr AST removes both failure modes: it parses at construction, renders deterministically, and refactors statically.
Common questions
Why do I need bv.lit for constants? Polars does this automatically.
Inside an operator overload (e.g. bv.col("a") > 100), Python's dispatch does auto-coerce the right-hand side via _coerce. bv.lit is needed when the literal isn't on the right side of an overload — for instance, when assigning a constant column in with_columns (no operator to dispatch through). Pick whichever reads better; both render to the same wire form.
Can I use Python's and / or?
No — Python forbids overloading and / or (they short-circuit on truthiness, not on operand types). The SDK overloads & / | instead and serializes them as the server-grammar keywords and / or. Always parenthesize each side: (a == 1) & (b > 0), not a == 1 & b > 0 — Python's bitwise operators bind tighter than comparison.
Does .cast allow lossy conversions?
The client-side guard only validates the target name — one of "str", "int", "float", "bool". The server applies the actual conversion at push time and rejects values that can't be represented (e.g. cast("not-a-number", int)). Lossy numeric casts (float → int) follow the server's coercion rules; check the operator catalogue's "type coercion" notes for the exact behaviour per call-site.
Can I reuse the same _Expr in multiple operators?
Yes. _Expr subclasses are frozen dataclasses — the AST is immutable, so a single expression node can appear inside several aggregations safely. Build it once at module scope and pass it to each where= kwarg.
Where to go next
You know how to reference and constrain. The next thing to learn is what to apply expressions to:
Every aggregation primitive — count, sum, mean, n_unique, top_k, ewma, plus the windowed and time-decay operators — and their where= / field= conventions.
Per-entity feature tables built with group_by(...).agg(...). Composite keys, global tables, and the chain primitives that consume your bv.col / bv.lit expressions.