@Override
    public PlanNode visitIndexJoin(IndexJoinNode node, RewriteContext<Context> context) {
      // Lookup symbols can only be passed through the probe side of an index join
      Set<Symbol> probeLookupSymbols =
          context
              .get()
              .getLookupSymbols()
              .stream()
              .filter(node.getProbeSource().getOutputSymbols()::contains)
              .collect(toImmutableSet());

      if (probeLookupSymbols.isEmpty()) {
        return node;
      }

      PlanNode rewrittenProbeSource =
          context.rewrite(
              node.getProbeSource(), new Context(probeLookupSymbols, context.get().getSuccess()));

      PlanNode source = node;
      if (rewrittenProbeSource != node.getProbeSource()) {
        source =
            new IndexJoinNode(
                node.getId(),
                node.getType(),
                rewrittenProbeSource,
                node.getIndexSource(),
                node.getCriteria(),
                node.getProbeHashSymbol(),
                node.getIndexHashSymbol());
      }

      return source;
    }
    @Override
    public PlanNode visitIndexJoin(IndexJoinNode node, RewriteContext<Set<Symbol>> context) {
      ImmutableSet.Builder<Symbol> probeInputsBuilder = ImmutableSet.builder();
      probeInputsBuilder
          .addAll(context.get())
          .addAll(Iterables.transform(node.getCriteria(), IndexJoinNode.EquiJoinClause::getProbe));
      if (node.getProbeHashSymbol().isPresent()) {
        probeInputsBuilder.add(node.getProbeHashSymbol().get());
      }
      Set<Symbol> probeInputs = probeInputsBuilder.build();

      ImmutableSet.Builder<Symbol> indexInputBuilder = ImmutableSet.builder();
      indexInputBuilder
          .addAll(context.get())
          .addAll(Iterables.transform(node.getCriteria(), IndexJoinNode.EquiJoinClause::getIndex));
      if (node.getIndexHashSymbol().isPresent()) {
        indexInputBuilder.add(node.getIndexHashSymbol().get());
      }
      Set<Symbol> indexInputs = indexInputBuilder.build();

      PlanNode probeSource = context.rewrite(node.getProbeSource(), probeInputs);
      PlanNode indexSource = context.rewrite(node.getIndexSource(), indexInputs);

      return new IndexJoinNode(
          node.getId(),
          node.getType(),
          probeSource,
          indexSource,
          node.getCriteria(),
          node.getProbeHashSymbol(),
          node.getIndexHashSymbol());
    }
    @Override
    public ActualProperties visitIndexJoin(
        IndexJoinNode node, List<ActualProperties> inputProperties) {
      // TODO: include all equivalent columns in partitioning properties
      ActualProperties probeProperties = inputProperties.get(0);
      ActualProperties indexProperties = inputProperties.get(1);

      switch (node.getType()) {
        case INNER:
          return ActualProperties.builderFrom(probeProperties)
              .constants(
                  ImmutableMap.<Symbol, NullableValue>builder()
                      .putAll(probeProperties.getConstants())
                      .putAll(indexProperties.getConstants())
                      .build())
              .build();
        case SOURCE_OUTER:
          return ActualProperties.builderFrom(probeProperties)
              .constants(probeProperties.getConstants())
              .build();
        default:
          throw new UnsupportedOperationException("Unsupported join type: " + node.getType());
      }
    }
    @Override
    public Void visitIndexJoin(IndexJoinNode node, Void context) {
      List<Expression> joinExpressions = new ArrayList<>();
      for (IndexJoinNode.EquiJoinClause clause : node.getCriteria()) {
        joinExpressions.add(
            new ComparisonExpression(
                ComparisonExpression.Type.EQUAL,
                new QualifiedNameReference(clause.getProbe().toQualifiedName()),
                new QualifiedNameReference(clause.getIndex().toQualifiedName())));
      }

      String criteria = Joiner.on(" AND ").join(joinExpressions);
      String joinLabel = format("%sIndexJoin", node.getType().getJoinLabel());
      printNode(node, joinLabel, criteria, NODE_COLORS.get(NodeType.JOIN));

      node.getProbeSource().accept(this, context);
      node.getIndexSource().accept(this, context);

      return null;
    }