# iceberg.expressions

> A module of iceberg's API.

Row filters: unbound and bound predicates, binding, projection, evaluators.

An expression tree lives in an arena (`Expr.nodes`) for the same reason types
do — Mojo values are not recursive. `Expr.root` is the top node.

The pieces, in the order a scan uses them:

1. `parse_filter` reads the JSON S-expression DSL (the same one
   iceberg-rs.mojo's Rust bridge speaks, so oracle plans are directly
   comparable) into an *unbound* tree of column names and untyped literals.
2. `bind` resolves names to field ids and converts each literal to the
   column's type, so `["=", "id", 3]` on a `long` column produces a long datum
   rather than an int one — a mistyped datum silently matches nothing.
3. `rewrite_not` pushes negation down to the leaves.
4. `project_inclusive` / `project_strict` push a bound predicate through a
   partition transform. Inclusive projection is the classic one: it may keep a
   file that has no matching row, but never drops one that has.
5. `ManifestEvaluator` applies the projected predicate to a manifest's
   partition field summaries; `InclusiveMetricsEvaluator` applies the original
   predicate to a data file's per-column bounds and counts.

## Structs

### `ExprNode`

```mojo
struct ExprNode
```

One node: a boolean connective, a constant, or a predicate on a term.

Implements: `Copyable`, `Movable`

Fields:

- `op: UInt8`
- `left: Int`
- `right: Int`
- `name: String`: Unbound term: the column name.
- `field_id: Int`: Bound term: the field id. -1 while unbound.
- `prim: UInt8`: Bound term: the column's primitive kind.
- `lits: List[Datum]`: Literals; `in`/`not-in` may hold many, comparisons exactly one.
- `raw_lits: List[String]`: Unbound literals as raw JSON text, typed at bind time.

### `Expr`

```mojo
struct Expr
```

An expression tree in an arena.

Implements: `Copyable`, `Movable`

Fields:

- `nodes: List[ExprNode]`
- `root: Int`

#### `__init__`

```mojo
def __init__(out self)
```

#### `add`

```mojo
def add(mut self, var n: ExprNode) -> Int
```

#### `constant`

```mojo
def constant(mut self, v: Bool) -> Int
```

#### `connective`

```mojo
def connective(mut self, op: UInt8, a: Int, b: Int) -> Int
```

#### `unbound`

```mojo
def unbound(mut self, op: UInt8, var name: String, var raw: List[String]) -> Int
```

#### `bound`

```mojo
def bound(mut self, op: UInt8, field_id: Int, prim: UInt8, var lits: List[Datum], var name: String = "") -> Int
```

`name` is carried through binding purely so that `text()` stays round-trippable: a residual is reported as DSL, and DSL names columns.

#### `is_true`

```mojo
def is_true(self, i: Int) -> Bool
```

#### `is_false`

```mojo
def is_false(self, i: Int) -> Bool
```

#### `text`

```mojo
def text(self, i: Int) -> String
```

Round-trippable DSL text — what a scan reports as its residual.

### `FieldSummary`

```mojo
struct FieldSummary
```

A manifest list's `partitions` entry: what one partition field holds across every file in that manifest.

Implements: `Copyable`, `Movable`

Fields:

- `contains_null: Bool`
- `contains_nan: Bool`
- `has_contains_nan: Bool`
- `lower_bound: List[UInt8]`
- `has_lower: Bool`
- `upper_bound: List[UInt8]`
- `has_upper: Bool`

#### `empty`

```mojo
def empty() -> Self
```

### `ManifestEvaluator`

```mojo
struct ManifestEvaluator
```

Applies a partition predicate to a manifest's field summaries.

Constructed from a *row* filter: the filter is projected inclusively
through the manifest's own partition spec first, so the evaluator only ever
sees predicates on partition field ids.

Implements: `Copyable`, `Movable`

Fields:

- `expr: Expr`
- `spec: PartitionSpec`
- `part_type: Schema`

#### `__init__`

```mojo
def __init__(out self, row_filter: Expr, spec: PartitionSpec, schema: Schema)
```

#### `eval`

```mojo
def eval(self, summaries: List[FieldSummary]) -> Bool
```

True when this manifest might contain a matching row.

### `ColumnMetrics`

```mojo
struct ColumnMetrics
```

Per-column statistics carried by a `data_file` entry.

Implements: `Copyable`, `Movable`

Fields:

- `field_id: Int`
- `value_count: Int64`
- `has_value_count: Bool`
- `null_value_count: Int64`
- `has_null_value_count: Bool`
- `nan_value_count: Int64`
- `has_nan_value_count: Bool`
- `lower_bound: List[UInt8]`
- `has_lower: Bool`
- `upper_bound: List[UInt8]`
- `has_upper: Bool`

#### `blank`

```mojo
def blank(field_id: Int) -> Self
```

### `InclusiveMetricsEvaluator`

```mojo
struct InclusiveMetricsEvaluator
```

Applies a bound row filter to one data file's column metrics.

Implements: `Copyable`, `Movable`

Fields:

- `expr: Expr`
- `schema: Schema`

#### `__init__`

```mojo
def __init__(out self, row_filter: Expr, schema: Schema)
```

#### `eval`

```mojo
def eval(self, record_count: Int64, metrics: List[ColumnMetrics]) -> Bool
```

### `ResidualEvaluator`

```mojo
struct ResidualEvaluator
```

What is left of a filter once a partition is known to be selected.

If the strict projection says the whole partition matches, the residual is
`true` and no per-row check is needed. Otherwise the original filter stands.

Implements: `Copyable`, `Movable`

Fields:

- `expr: Expr`
- `strict: Expr`
- `inclusive: Expr`
- `spec: PartitionSpec`
- `conjuncts: List[Int]`: The top-level `and` spine of `expr`, as node indices.
- `conjunct_strict: List[Expr]`: Each conjunct's own strict projection, one per entry of `conjuncts`.

#### `__init__`

```mojo
def __init__(out self, row_filter: Expr, spec: PartitionSpec, schema: Schema)
```

#### `residual_for`

```mojo
def residual_for(self, partition: List[Datum]) -> Expr
```

The residual filter for one partition tuple.

A conjunct whose *own* strict projection holds for this partition is
satisfied by every row in it, so dropping it leaves the filter's
meaning unchanged — and with it goes the column the reader would
otherwise have decoded only to check it. On a month-partitioned table,
`pickup >= '2024-06-01' and pickup < '2024-07-01' and distance > 1`
reduces to `distance > 1`, and the timestamp column is never read: over
3.5M rows that is a 24.7 ms column decode this used to pay per file for
an answer the partition already gave. Reducing only when the *whole*
filter is strictly satisfied — which is what this did before — leaves
that on the table for every query that mixes a partition-aligned range
with a predicate on data.

#### `selects`

```mojo
def selects(self, partition: List[Datum]) -> Bool
```

Whether the inclusive projection keeps this partition tuple.

## Functions

### `op_name`

```mojo
def op_name(op: UInt8) -> String
```

### `negate_op`

```mojo
def negate_op(op: UInt8) -> UInt8
```

### `is_predicate`

```mojo
def is_predicate(op: UInt8) -> Bool
```

### `parse_filter`

```mojo
def parse_filter(text: String) -> Expr
```

Parse the filter DSL into an unbound expression.

The grammar is iceberg-rs.mojo's, so the same filter string can be handed
to the Rust bridge and to this reader and the plans compared directly.

### `literal_for`

```mojo
def literal_for(store: TypeStore, type_idx: Int, raw_json: String) -> Datum
```

Convert one raw JSON literal to the column's type.

Widening conversions the spec allows are done here: an int literal on a
`long` column becomes a long, a string on a `date`/`timestamp` column is
parsed as ISO-8601, and a number on those columns is taken as the stored
integer.

### `bind`

```mojo
def bind(var e: Expr, schema: Schema, case_sensitive: Bool = True) -> Expr
```

Resolve names to field ids and type every literal against the schema.

A predicate on a column the schema does not have becomes `false` (a row can
never match a column that is not there) — except `is-null`, which becomes
`true`, since a missing column reads as all nulls.

### `rewrite_not`

```mojo
def rewrite_not(e: Expr) -> Expr
```

Push every `not` down to the leaves (De Morgan plus op negation).

### `project_inclusive`

```mojo
def project_inclusive(e: Expr, spec: PartitionSpec, schema: Schema) -> Expr
```

Project a bound row filter onto the partition tuple, inclusively.

The result is true for every partition that *could* hold a matching row. It
is deliberately loose: dropping a partition that might match would lose
data, keeping one that cannot only costs a wasted read.

### `project_strict`

```mojo
def project_strict(e: Expr, spec: PartitionSpec, schema: Schema) -> Expr
```

Project strictly: true only for partitions where *every* row matches.

A scan uses this to drop the filter from a partition's residual — if the
whole partition matches, there is nothing left to check per row.

## Aliases

### `OP_TRUE`

```mojo
comptime OP_TRUE = UInt8(0)
```

### `OP_FALSE`

```mojo
comptime OP_FALSE = UInt8(1)
```

### `OP_AND`

```mojo
comptime OP_AND = UInt8(2)
```

### `OP_OR`

```mojo
comptime OP_OR = UInt8(3)
```

### `OP_NOT`

```mojo
comptime OP_NOT = UInt8(4)
```

### `OP_IS_NULL`

```mojo
comptime OP_IS_NULL = UInt8(5)
```

### `OP_NOT_NULL`

```mojo
comptime OP_NOT_NULL = UInt8(6)
```

### `OP_IS_NAN`

```mojo
comptime OP_IS_NAN = UInt8(7)
```

### `OP_NOT_NAN`

```mojo
comptime OP_NOT_NAN = UInt8(8)
```

### `OP_LT`

```mojo
comptime OP_LT = UInt8(9)
```

### `OP_LT_EQ`

```mojo
comptime OP_LT_EQ = UInt8(10)
```

### `OP_GT`

```mojo
comptime OP_GT = UInt8(11)
```

### `OP_GT_EQ`

```mojo
comptime OP_GT_EQ = UInt8(12)
```

### `OP_EQ`

```mojo
comptime OP_EQ = UInt8(13)
```

### `OP_NOT_EQ`

```mojo
comptime OP_NOT_EQ = UInt8(14)
```

### `OP_IN`

```mojo
comptime OP_IN = UInt8(15)
```

### `OP_NOT_IN`

```mojo
comptime OP_NOT_IN = UInt8(16)
```

### `OP_STARTS_WITH`

```mojo
comptime OP_STARTS_WITH = UInt8(17)
```

### `OP_NOT_STARTS_WITH`

```mojo
comptime OP_NOT_STARTS_WITH = UInt8(18)
```

### `ROWS_MIGHT_MATCH`

```mojo
comptime ROWS_MIGHT_MATCH = True
```

### `ROWS_CANNOT_MATCH`

```mojo
comptime ROWS_CANNOT_MATCH = False
```
