Implement a small optimizer pass that pushes filters closer to scans when doing so preserves query semantics.
Question
Implement a small optimizer pass that pushes filters closer to scans when doing so preserves query semantics.
```python
def push_filters(plan: dict) -> dict: ...
```
## Assumptions
Use these immutable-style dictionary nodes:
- `{"op":"scan", "table":..., "columns":[...]}`
- `{"op":"filter", "column":..., "child":...}`
- `{"op":"project", "columns":[...], "child":...}`
- `{"op":"join", "how":"inner"|"left", "left":..., "right":...}`
## Requirements
- Push a filter through a projection only if the filtered column remains available.
- Push into the unique side exposing that column for an inner join.
- For a left join, a filter may push into the preserved left side.
- Do not push a post-join filter into the null-producing right side of a left join.
- If both join inputs expose the column, leave the filter above the join.
- Preserve the input tree rather than mutating shared nodes, and apply the rewrite recursively to a fixed point.
Column names are strings. Each filter denotes a deterministic, side-effect-free predicate using only its named column; filters commute. Canonicalize each adjacent filter chain in ascending column-name order, preserving duplicate filters, so repeated optimization returns the same structure. This task does not model arbitrary SQL expressions, predicate evaluation errors, or null-rejection analysis. The provided `run_case` dispatches fixtures; implement the algorithm functions rather than this adapter.
## Follow-ups
1. Add qualified column identities and expression predicates.
2. Split conjunctive filters by referenced relation.
3. Use null-rejection analysis to enable safe outer-join rewrites.
4. Add estimated cost and choose among equivalent plans.The public brief is identical for people and crawlers. Account access changes tools, not the indexed description.