@Override
    public PlanNode visitSemiJoin(SemiJoinNode node, RewriteContext<Set<Symbol>> context) {
      ImmutableSet.Builder<Symbol> sourceInputsBuilder = ImmutableSet.builder();
      sourceInputsBuilder.addAll(context.get()).add(node.getSourceJoinSymbol());
      if (node.getSourceHashSymbol().isPresent()) {
        sourceInputsBuilder.add(node.getSourceHashSymbol().get());
      }
      Set<Symbol> sourceInputs = sourceInputsBuilder.build();

      ImmutableSet.Builder<Symbol> filteringSourceInputBuilder = ImmutableSet.builder();
      filteringSourceInputBuilder.add(node.getFilteringSourceJoinSymbol());
      if (node.getFilteringSourceHashSymbol().isPresent()) {
        filteringSourceInputBuilder.add(node.getFilteringSourceHashSymbol().get());
      }
      Set<Symbol> filteringSourceInputs = filteringSourceInputBuilder.build();

      PlanNode source = context.rewrite(node.getSource(), sourceInputs);
      PlanNode filteringSource = context.rewrite(node.getFilteringSource(), filteringSourceInputs);

      return new SemiJoinNode(
          node.getId(),
          source,
          filteringSource,
          node.getSourceJoinSymbol(),
          node.getFilteringSourceJoinSymbol(),
          node.getSemiJoinOutput(),
          node.getSourceHashSymbol(),
          node.getFilteringSourceHashSymbol());
    }
    @Override
    public Void visitSemiJoin(SemiJoinNode node, Void context) {
      printNode(
          node,
          "SemiJoin",
          format("%s = %s", node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol()),
          NODE_COLORS.get(NodeType.JOIN));

      node.getSource().accept(this, context);
      node.getFilteringSource().accept(this, context);

      return null;
    }
    @Override
    public SubPlanBuilder visitSemiJoin(SemiJoinNode node, Void context) {
      SubPlanBuilder source = node.getSource().accept(this, context);
      SubPlanBuilder filteringSource = node.getFilteringSource().accept(this, context);

      if (source.isDistributed() || filteringSource.isDistributed()) {
        filteringSource.setRoot(
            new SinkNode(
                idAllocator.getNextId(),
                filteringSource.getRoot(),
                filteringSource.getRoot().getOutputSymbols()));
        source.setRoot(
            new SemiJoinNode(
                node.getId(),
                source.getRoot(),
                new ExchangeNode(
                    idAllocator.getNextId(),
                    filteringSource.getId(),
                    filteringSource.getRoot().getOutputSymbols()),
                node.getSourceJoinSymbol(),
                node.getFilteringSourceJoinSymbol(),
                node.getSemiJoinOutput()));
        source.addChild(filteringSource.build());

        return source;
      } else {
        SemiJoinNode semiJoinNode =
            new SemiJoinNode(
                node.getId(),
                source.getRoot(),
                filteringSource.getRoot(),
                node.getSourceJoinSymbol(),
                node.getFilteringSourceJoinSymbol(),
                node.getSemiJoinOutput());
        return createSingleNodePlan(semiJoinNode)
            .setChildren(Iterables.concat(source.getChildren(), filteringSource.getChildren()));
      }
    }
 @Override
 public Expression visitSemiJoin(SemiJoinNode node, Void context) {
   // Filtering source does not change the effective predicate over the output symbols
   return node.getSource().accept(this, context);
 }
    @Override
    public PlanNode rewriteSemiJoin(
        SemiJoinNode node, Expression inheritedPredicate, PlanRewriter<Expression> planRewriter) {
      Expression sourceEffectivePredicate = EffectivePredicateExtractor.extract(node.getSource());

      List<Expression> sourceConjuncts = new ArrayList<>();
      List<Expression> filteringSourceConjuncts = new ArrayList<>();
      List<Expression> postJoinConjuncts = new ArrayList<>();

      // TODO: see if there are predicates that can be inferred from the semi join output

      // Push inherited and source predicates to filtering source via a contrived join predicate
      // (but needs to avoid touching NULL values in the filtering source)
      Expression joinPredicate =
          equalsExpression(node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol());
      EqualityInference joinInference =
          createEqualityInference(inheritedPredicate, sourceEffectivePredicate, joinPredicate);
      for (Expression conjunct :
          Iterables.concat(
              EqualityInference.nonInferrableConjuncts(inheritedPredicate),
              EqualityInference.nonInferrableConjuncts(sourceEffectivePredicate))) {
        Expression rewrittenConjunct =
            joinInference.rewriteExpression(conjunct, equalTo(node.getFilteringSourceJoinSymbol()));
        if (rewrittenConjunct != null && DeterminismEvaluator.isDeterministic(rewrittenConjunct)) {
          // Alter conjunct to include an OR filteringSourceJoinSymbol IS NULL disjunct
          Expression rewrittenConjunctOrNull =
              expressionOrNullSymbols(equalTo(node.getFilteringSourceJoinSymbol()))
                  .apply(rewrittenConjunct);
          filteringSourceConjuncts.add(rewrittenConjunctOrNull);
        }
      }
      EqualityInference.EqualityPartition joinInferenceEqualityPartition =
          joinInference.generateEqualitiesPartitionedBy(
              equalTo(node.getFilteringSourceJoinSymbol()));
      filteringSourceConjuncts.addAll(
          ImmutableList.copyOf(
              transform(
                  joinInferenceEqualityPartition.getScopeEqualities(),
                  expressionOrNullSymbols(equalTo(node.getFilteringSourceJoinSymbol())))));

      // Push inheritedPredicates down to the source if they don't involve the semi join output
      EqualityInference inheritedInference = createEqualityInference(inheritedPredicate);
      for (Expression conjunct : EqualityInference.nonInferrableConjuncts(inheritedPredicate)) {
        Expression rewrittenConjunct =
            inheritedInference.rewriteExpression(conjunct, in(node.getSource().getOutputSymbols()));
        // Since each source row is reflected exactly once in the output, ok to push
        // non-deterministic predicates down
        if (rewrittenConjunct != null) {
          sourceConjuncts.add(rewrittenConjunct);
        } else {
          postJoinConjuncts.add(conjunct);
        }
      }

      // Add the inherited equality predicates back in
      EqualityInference.EqualityPartition equalityPartition =
          inheritedInference.generateEqualitiesPartitionedBy(
              in(node.getSource().getOutputSymbols()));
      sourceConjuncts.addAll(equalityPartition.getScopeEqualities());
      postJoinConjuncts.addAll(equalityPartition.getScopeComplementEqualities());
      postJoinConjuncts.addAll(equalityPartition.getScopeStraddlingEqualities());

      PlanNode rewrittenSource =
          planRewriter.rewrite(node.getSource(), combineConjuncts(sourceConjuncts));
      PlanNode rewrittenFilteringSource =
          planRewriter.rewrite(
              node.getFilteringSource(), combineConjuncts(filteringSourceConjuncts));

      PlanNode output = node;
      if (rewrittenSource != node.getSource()
          || rewrittenFilteringSource != node.getFilteringSource()) {
        output =
            new SemiJoinNode(
                node.getId(),
                rewrittenSource,
                rewrittenFilteringSource,
                node.getSourceJoinSymbol(),
                node.getFilteringSourceJoinSymbol(),
                node.getSemiJoinOutput());
      }
      if (!postJoinConjuncts.isEmpty()) {
        output =
            new FilterNode(idAllocator.getNextId(), output, combineConjuncts(postJoinConjuncts));
      }
      return output;
    }