Implement distributed planning as an optimizer

Add a new optimizer (AddExchanges) that introduces Exchange node
at the right places in the logical plan and determines partitioning
and placement requirements.

This also adds basic partition awareness, which can result in better plans:

E.g.,

 SELECT custkey, sum(t)
 FROM (
   SELECT custkey, count(*) t
   FROM orders
   GROUP BY custkey
 )
 GROUP BY custkey

Before:

 Fragment 3 [SINGLE]
     Output layout: [custkey, sum]
     - Output[custkey, _col1] => [custkey:bigint, sum:bigint]
             _col1 := sum
         - RemoteSource[2] => [custkey:bigint, sum:bigint]

 Fragment 2 [FIXED]
     Output layout: [custkey, sum]
     - Aggregate(FINAL)[custkey] => [custkey:bigint, sum:bigint]
             sum := "sum"("sum_18")
         - RemoteSource[1] => [custkey:bigint, sum_18:bigint]

 Fragment 1 [FIXED]
     Output layout: [custkey, sum_18]
     Output partitioning: [custkey]
     - Aggregate(PARTIAL)[custkey] => [custkey:bigint, sum_18:bigint]
             sum_18 := "sum"("count")
         - Aggregate(FINAL)[custkey] => [custkey:bigint, count:bigint]
                 count := "count"("count_17")
             - RemoteSource[0] => [custkey:bigint, count_17:bigint]

 Fragment 0 [SOURCE]
     Output layout: [custkey, count_17]
     Output partitioning: [custkey]
     - Aggregate(PARTIAL)[custkey] => [custkey:bigint, count_17:bigint]
             count_17 := "count"(*)
         - TableScan[tpch:tpch:orders:sf1.0, original constraint=true] => [custkey:bigint]
                 custkey := tpch:tpch:custkey:1

After:

 Fragment 2 [SINGLE]
     Output layout: [custkey, sum]
     - Output[custkey, _col1] => [custkey:bigint, sum:bigint]
             _col1 := sum
         - RemoteSource[1] => [custkey:bigint, sum:bigint]

 Fragment 1 [FIXED]
     Output layout: [custkey, sum]
     - Aggregate[custkey] => [custkey:bigint, sum:bigint]
             sum := "sum"("count")
         - Aggregate(FINAL)[custkey] => [custkey:bigint, count:bigint]
                 count := "count"("count_17")
             - RemoteSource[0] => [custkey:bigint, count_17:bigint]

 Fragment 0 [SOURCE]
     Output layout: [custkey, count_17]
     Output partitioning: [custkey]
     - Aggregate(PARTIAL)[custkey] => [custkey:bigint, count_17:bigint]
             count_17 := "count"(*)
         - TableScan[tpch:tpch:orders:sf1.0, original constraint=true] => [custkey:bigint]
                 custkey := tpch:tpch:custkey:1
Add missing support for UnionNode to DistributedExecutionPlanner
Fix SymbolExtractor implementation for UnionNode
Add missing json serialization for UnionNode
Generate local union when inputs are unpartitioned

Before:

 Fragment 2 [SINGLE]
     Output layout: [expr_3]
     - Output[_col0] => [expr_3:bigint]
             _col0 := expr_3
         - RemoteSource[0,1] => [expr_3:bigint]

 Fragment 0 [SINGLE]
     Output layout: [expr]
     - Project => [expr:bigint]
             expr := 1
         - Values => []
                 ()

 Fragment 1 [SOURCE]
     Output layout: [orderkey]
     - TableScan[tpch:tpch:orders:sf1.0, original constraint=true] => [orderkey:bigint]
             orderkey := tpch:tpch:orderkey:0

After:

 Fragment 1 [SINGLE]
     Output layout: [expr_3]
     - Output[_col0] => [expr_3:bigint]
             _col0 := expr_3
         - Union => [expr_3:bigint]
             - Project => [expr:bigint]
                     expr := 1
                 - Values => []
                         ()
             - RemoteSource[0] => [expr_3:bigint]

 Fragment 0 [SOURCE]
     Output layout: [orderkey]
     - TableScan[tpch:tpch:orders:sf1.0, original constraint=true] => [orderkey:bigint]
             orderkey := tpch:tpch:orderkey:0