docs: private IndexSortablePlanNode getSelectSubPlanForJoin(BranchNode joinNode,
                                                       AbstractPlanNode outerPlan,
                                                       AbstractPlanNode innerPlan)
      {
          // Filter (post-join) expressions
          ArrayList<AbstractExpression> whereClauses  = new ArrayList<>();
          whereClauses.addAll(joinNode.m_whereInnerList);
          whereClauses.addAll(joinNode.m_whereInnerOuterList);
          if (joinNode.getJoinType() == JoinType.FULL) {
              // For all other join types, the whereOuterList expressions were pushed down to the outer node
              whereClauses.addAll(joinNode.m_whereOuterList);
          }
  
          // The usual approach of calculating a local (partial) join result on each partition,
          // then sending and merging them with other partial results on the coordinator does not
          // ensure correct answers for some queries like:
          //     SELECT * FROM replicated LEFT JOIN partitioned ON ...
          // They require a "global view" of the partitioned working set in order to
          // properly identify which replicated rows need to be null-padded,
          // and to ensure that the same replicated row is not null-padded redundantly on multiple partitions.
          // Many queries using this pattern impractically require redistribution and caching of a considerable
          // subset of a partitioned table in preparation for a "coordinated" join.
          // Yet, there may be useful cases with sufficient constant-based partitioned table filtering
          // in the "ON clause" to keep the distributed working set size under control, like
          //     SELECT * FROM replicated R LEFT JOIN partitioned P
          //     ON R.key == P.non_partition_key AND P.non_partition_key BETWEEN ? and ?;
          //
          // Such queries need to be prohibited by the planner if it can not guarantee the
          // correct results that require a "send before join" plan.
          // This could be the case if the replicated-to-partition join in these examples
          // were subject to another join with a partitioned table, like
          //     SELECT * FROM replicated R LEFT JOIN partitioned P1 ON ...
          //                                LEFT JOIN also_partitioned P2 ON ...
          //
  
          assert(joinNode.getRightNode() != null);
          JoinNode innerJoinNode = joinNode.getRightNode();
          AccessPath innerAccessPath = innerJoinNode.m_currentAccessPath;
          // We may need to add a send/receive pair to the inner plan for the special case.
          // This trick only works once per plan, BUT once the partitioned data has been
          // received on the coordinator, it can be treated as replicated data in later
          // joins, which MAY help with later outer joins with replicated data.
  
          boolean needInnerSendReceive = m_partitioning.requiresTwoFragments() &&
                                         ! innerPlan.hasReplicatedResult() &&
                                         outerPlan.hasReplicatedResult() &&
                                         joinNode.getJoinType() != JoinType.INNER;
  
          // When the inner plan is an IndexScan, there MAY be a choice of whether to join using a
          // NestLoopJoin (NLJ) or a NestLoopIndexJoin (NLIJ). The NLJ will have an advantage over the
          // NLIJ in the cases where it applies, since it does a single access or iteration over the index
          // and caches the result, where the NLIJ does an index access or iteration for each outer row.
          // The NestLoopJoin applies when the inner IndexScan is driven only by parameter and constant
          // expressions determined at the start of the query. That requires that none of the IndexScan's
          // various expressions that drive the index access may reference columns from the outer row
          // -- they can only reference columns of the index's base table (the indexed expressions)
          // as well as constants and parameters. The IndexScan's "otherExprs" expressions that only
          // drive post-filtering are not an issue since the NestLoopJoin does feature per-outer-tuple
          // post-filtering on each pass over the cached index scan result.
  
          // The special case of an OUTER JOIN of replicated outer row data with a partitioned inner
          // table requires that the partitioned data be sent to the coordinator prior to the join.
          // This limits the join option to NLJ. The index scan must make a single index access on
          // each partition and cache the result at the coordinator for post-filtering.
          // This requires that the index access be based on parameters or constants only
          // -- the replicated outer row data will only be available later at the coordinator,
          // so it can not drive the per-partition index scan.
  
          // If the NLJ option is precluded for the usual reason (outer-row-based indexing) AND
          // the NLIJ is precluded by the special case (OUTER JOIN of replicated outer rows and
          // partitioned inner rows) this method returns null, effectively rejecting this indexed
          // access path for the inner node. Other access paths or join orders may prove more successful.
  
          boolean canHaveNLJ = true;
          boolean canHaveNLIJ = true;
          if (innerPlan instanceof IndexScanPlanNode) {
              if (hasInnerOuterIndexExpression(joinNode.getRightNode().getTableAlias(),
                                               innerAccessPath.indexExprs,
                                               innerAccessPath.initialExpr,
                                               innerAccessPath.endExprs)) {
                  canHaveNLJ = false;
              }
          }
          else {
              canHaveNLIJ = false;
          }
          if (needInnerSendReceive) {
              canHaveNLIJ = false;
          }
  
          // Prohibit FULL join plans with distributed outer and replicated inner branches -
          // The join must happen on either a coordinator node or both tables must be joined on
          // partition columns
          if (joinNode.getJoinType() == JoinType.FULL &&
                  m_partitioning.requiresTwoFragments() &&
                  !outerPlan.hasReplicatedResult() &&
                  innerPlan.hasReplicatedResult()) {
              canHaveNLIJ = false;
              canHaveNLJ = false;
          }
  
          AbstractJoinPlanNode ajNode = null;
          if (canHaveNLJ) {
              NestLoopPlanNode nljNode = new NestLoopPlanNode();
              // get all the clauses that join the applicable two tables
              // Copy innerAccessPath.joinExprs to leave it unchanged,
              // avoiding accumulation of redundant expressions when
              // joinClauses gets built up for various alternative plans.
              ArrayList<AbstractExpression> joinClauses = new ArrayList<>(innerAccessPath.joinExprs);
              if ((innerPlan instanceof IndexScanPlanNode) ||
                  (innerPlan instanceof NestLoopIndexPlanNode
                      && innerPlan.getChild(0) instanceof MaterializedScanPlanNode)) {
                  // InnerPlan is an IndexScan OR an NLIJ of a MaterializedScan
                  // (IN LIST) and an IndexScan. In this case, the inner and
                  // inner-outer non-index join expressions (if any) are in the
                  // indexScan's otherExpr. The former should stay as IndexScanPlan
                  // predicates but the latter need to be pulled up into NLJ
                  // predicates because the IndexScan is executed once, not once
                  // per outer tuple.
                  ArrayList<AbstractExpression> otherExprs = new ArrayList<>();
                  // PLEASE do not update the "innerAccessPath.otherExprs", it may be reused
                  // for other path evaluation on the other outer side join.
                  List<AbstractExpression> innerExpr = filterSingleTVEExpressions(innerAccessPath.otherExprs, otherExprs);
                  joinClauses.addAll(otherExprs);
                  IndexScanPlanNode scanNode = null;
                  if (innerPlan instanceof IndexScanPlanNode) {
                      scanNode = (IndexScanPlanNode)innerPlan;
                  }
                  else {
                      assert(innerPlan instanceof NestLoopIndexPlanNode);
                      scanNode = ((NestLoopIndexPlanNode) innerPlan).getInlineIndexScan();
                  }
                  scanNode.setPredicate(innerExpr);
              }
              else if (innerJoinNode instanceof BranchNode && joinNode.getJoinType() != JoinType.INNER) {
                  // If the innerJoinNode is a LEAF node OR if the join type is an INNER join,
                  // the conditions that apply to the inner side
                  // have been applied as predicates to the inner scan node already.
  
                  // otherExpr of innerAccessPath comes from its parentNode's joinInnerList.
                  // For Outer join (LEFT or FULL), it could mean a join predicate on the table of
                  // the inner node ONLY, that can not be pushed down.
                  joinClauses.addAll(innerAccessPath.otherExprs);
              }
              nljNode.setJoinPredicate(ExpressionUtil.combinePredicates(joinClauses));
  
              // combine the tails plan graph with the new head node
              nljNode.addAndLinkChild(outerPlan);
  
              // If successful in the special case, the NLJ plan must be modified to cause the
              // partitioned inner data to be sent to the coordinator prior to the join.
              // This is done by adding send and receive plan nodes between the NLJ and its
              // right child node.
              if (needInnerSendReceive) {
                  // This trick only works once per plan.
                  if (outerPlan.hasAnyNodeOfClass(AbstractReceivePlanNode.class) || innerPlan.hasAnyNodeOfClass(AbstractReceivePlanNode.class)) {
                      return null;
                  }
                  innerPlan = addSendReceivePair(innerPlan);
              }
  
              nljNode.addAndLinkChild(innerPlan);
              ajNode = nljNode;
          }
          else if (canHaveNLIJ) {
              NestLoopIndexPlanNode nlijNode = new NestLoopIndexPlanNode();
  
              IndexScanPlanNode innerNode = (IndexScanPlanNode) innerPlan;
              // Set IndexScan predicate. The INNER join expressions for a FULL join come from
              // the innerAccessPath.joinExprs and need to be combined with the other join expressions
              innerNode.setPredicate(innerAccessPath.joinExprs, innerAccessPath.otherExprs);
  
              nlijNode.addInlinePlanNode(innerPlan);
  
              // combine the tails plan graph with the new head node
              nlijNode.addAndLinkChild(outerPlan);
  
              ajNode = nlijNode;
          }
          else {
              m_recentErrorMsg =
                  "Unsupported special case of complex OUTER JOIN between replicated outer table and partitioned inner table.";
              return null;
          }
          ajNode.setJoinType(joinNode.getJoinType());
          ajNode.setPreJoinPredicate(ExpressionUtil.combinePredicates(joinNode.m_joinOuterList));
          ajNode.setWherePredicate(ExpressionUtil.combinePredicates(whereClauses));
          ajNode.resolveSortDirection();
          return ajNode;
      }