### Description

As we have more rules doing analysis across plans, a number of methods are useful to add to the `QueryPlan` class itself, mainly around expressions:

```
- expressions() - return the expressions declared by this plan
- references() - return the references from the expressions above
```
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java
@Override
        protected LogicalPlan rule(UnaryPlan plan) {
            return plan.output().isEmpty() ? skipPlan(plan) : plan;
        }
    }
    static class PropagateEmptyRelation extends OptimizerRules.OptimizerRule<UnaryPlan> {
        @Override
        protected LogicalPlan rule(UnaryPlan plan) {
            LogicalPlan p = plan;
            if (plan.child() instanceof LocalRelation local && local.supplier() == LocalSupplier.EMPTY) {
                
                if (plan instanceof Aggregate agg && agg.groupings().isEmpty()) {
                    p = skipPlan(plan, aggsFromEmpty(agg.aggregates()));
                } else {
                    p = skipPlan(plan);
                }
            }
            return p;
        }
        private static LocalSupplier aggsFromEmpty(List<? extends NamedExpression> aggs) {
            var result = new ArrayList<Object>(aggs.size());
            for (var agg : aggs) {
                
                if (agg instanceof Alias a && a.child() instanceof AggregateFunction aggFunc) {
                    result.add(aggFunc instanceof Count ? 0L : null);