@Override
    public PlanAndMappings visitJoin(JoinNode node, UnaliasContext context) {
      // it is assumed that symbols are distinct between left and right join source. Only symbols
      // from outer correlation might be the exception
      PlanAndMappings rewrittenLeft = node.getLeftChild().accept(this, context);
      PlanAndMappings rewrittenRight = node.getRightChild().accept(this, context);

      // unify mappings from left and right join source
      Map<Symbol, Symbol> unifiedMapping = new HashMap<>();
      unifiedMapping.putAll(rewrittenLeft.getMappings());
      unifiedMapping.putAll(rewrittenRight.getMappings());

      SymbolMapper mapper = symbolMapper(unifiedMapping);

      ImmutableList.Builder<JoinNode.EquiJoinClause> builder = ImmutableList.builder();
      for (JoinNode.EquiJoinClause clause : node.getCriteria()) {
        builder.add(
            new JoinNode.EquiJoinClause(
                mapper.map(clause.getLeft()), mapper.map(clause.getRight())));
      }
      List<JoinNode.EquiJoinClause> newCriteria = builder.build();

      Optional<Expression> newFilter = node.getFilter().map(mapper::map);

      // derive new mappings from inner join equi criteria
      Map<Symbol, Symbol> newMapping = new HashMap<>();
      if (node.getJoinType() == INNER) {
        newCriteria
            // Map right equi-condition symbol to left symbol. This helps to
            // reuse join node partitioning better as partitioning properties are
            // only derived from probe side symbols
            .forEach(clause -> newMapping.put(clause.getRight(), clause.getLeft()));
      }

      Map<Symbol, Symbol> outputMapping = new HashMap<>();
      outputMapping.putAll(unifiedMapping);
      outputMapping.putAll(newMapping);

      mapper = symbolMapper(outputMapping);
      List<Symbol> canonicalOutputs = mapper.mapAndDistinct(node.getOutputSymbols());
      List<Symbol> newLeftOutputSymbols =
          canonicalOutputs.stream()
              .filter(rewrittenLeft.getRoot().getOutputSymbols()::contains)
              .collect(toImmutableList());
      List<Symbol> newRightOutputSymbols =
          canonicalOutputs.stream()
              .filter(rewrittenRight.getRoot().getOutputSymbols()::contains)
              .collect(toImmutableList());

      return new PlanAndMappings(
          new JoinNode(
              node.getPlanNodeId(),
              node.getJoinType(),
              rewrittenLeft.getRoot(),
              rewrittenRight.getRoot(),
              newCriteria,
              node.getAsofCriteria(),
              newLeftOutputSymbols,
              newRightOutputSymbols,
              newFilter,
              node.isSpillable()),
          outputMapping);
    }
Replace all symbols in the plan with new symbols. The returned plan has different output than
the original plan. Also, the order of symbols might change during symbol replacement. Symbols
in the list `fields` are replaced maintaining the order so they might be used to match original
symbols with their replacements. Replacing symbols helps avoid collisions when symbols or parts
of the plan are reused.
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java