|
| 1 | +# Intermediate Representation Layer |
| 2 | + |
| 3 | +## Purpose |
| 4 | + |
| 5 | +Decouple the YAML frontend (parsing/serialization) from the execution backend (Spark, DataFusion, etc.) by introducing an explicit IR that serves as the canonical representation of a pipeline. |
| 6 | + |
| 7 | +Currently, the `Source` sealed trait in `teckel-model` acts as an implicit IR. This design document proposes making it explicit with optimization capabilities. |
| 8 | + |
| 9 | +## IR Node Design |
| 10 | + |
| 11 | +IR nodes map directly to the existing `Source` sealed trait hierarchy: |
| 12 | + |
| 13 | +``` |
| 14 | +IRNode |
| 15 | + |-- ReadNode(format, options, path) |
| 16 | + |-- WriteNode(ref, format, mode, options, path) |
| 17 | + |-- ProjectNode(ref, columns) // Select |
| 18 | + |-- FilterNode(ref, condition) // Where |
| 19 | + |-- AggregateNode(ref, keys, exprs) // GroupBy, Rollup, Cube |
| 20 | + |-- SortNode(ref, keys, order) // OrderBy |
| 21 | + |-- JoinNode(ref, others, type, on) // Join |
| 22 | + |-- SetOpNode(ref, others, op, all) // Union, Intersect, Except |
| 23 | + |-- WindowNode(ref, partition, order, funcs) |
| 24 | + |-- TransformNode(ref, kind, params) // AddColumns, DropColumns, Rename, Cast, etc. |
| 25 | + |-- SCD2Node(ref, keys, track, dates) // SCD Type 2 |
| 26 | + |-- EnrichNode(ref, url, method, ...) // API Enrichment |
| 27 | +``` |
| 28 | + |
| 29 | +Each node carries: |
| 30 | +- A unique `NodeId` (currently `AssetRef`) |
| 31 | +- Input dependency references |
| 32 | +- Output schema (inferred or declared) |
| 33 | + |
| 34 | +## Optimization Passes |
| 35 | + |
| 36 | +The IR enables query optimization passes applied before execution: |
| 37 | + |
| 38 | +### Pass 1: Predicate Pushdown |
| 39 | +Move `FilterNode` closer to `ReadNode` when the filter references only columns available at the source. |
| 40 | + |
| 41 | +``` |
| 42 | +Before: Read -> Project -> Filter |
| 43 | +After: Read -> Filter -> Project |
| 44 | +``` |
| 45 | + |
| 46 | +### Pass 2: Projection Pruning |
| 47 | +Remove columns from intermediate nodes that are never referenced downstream. |
| 48 | + |
| 49 | +``` |
| 50 | +Before: Read(a,b,c,d) -> Project(a,b) -> Filter(a > 1) |
| 51 | +After: Read(a,b) -> Filter(a > 1) -> Project(a,b) |
| 52 | +``` |
| 53 | + |
| 54 | +### Pass 3: Join Reordering |
| 55 | +Reorder joins based on estimated cardinality when statistics are available. |
| 56 | + |
| 57 | +### Pass 4: Common Subexpression Elimination |
| 58 | +Detect shared subtrees and materialize them once. |
| 59 | + |
| 60 | +## Implementation Plan |
| 61 | + |
| 62 | +1. Define `IRNode` sealed trait in a new `teckel-ir` module |
| 63 | +2. Add `Rewrite.toIR: Context[Asset] => IRPlan` conversion |
| 64 | +3. Add `Optimizer.optimize: IRPlan => IRPlan` pass pipeline |
| 65 | +4. Add `Backend.execute: IRPlan => F[Unit]` interface |
| 66 | +5. Migrate `SparkETL` to consume `IRPlan` instead of `Context[Asset]` |
| 67 | + |
| 68 | +## References |
| 69 | + |
| 70 | +- Apache Calcite: relational algebra + cost-based optimization |
| 71 | +- DataFusion: Rust query engine with logical/physical plan separation |
| 72 | +- Spark Catalyst: internal optimizer with rules-based and cost-based passes |
0 commit comments