docs: public static List<Column> findRequiredColumns( QueryContext context,
                                                      PlanNode planNode ) {
          List<Column> columns = null;
          PlanNode node = planNode;
          // First find the columns from the nearest PROJECT ancestor ...
          do {
              switch (node.getType()) {
                  case PROJECT:
                      columns = node.getPropertyAsList(Property.PROJECT_COLUMNS, Column.class);
                      node = null;
                      break;
                  default:
                      node = node.getParent();
                      break;
              }
          } while (node != null);
  
          // Find the names of the selectors ...
          Set<SelectorName> names = new HashSet<SelectorName>();
          for (PlanNode source : planNode.findAllAtOrBelow(Type.SOURCE)) {
              names.add(source.getProperty(Property.SOURCE_NAME, SelectorName.class));
              SelectorName alias = source.getProperty(Property.SOURCE_ALIAS, SelectorName.class);
              if (alias != null) names.add(alias);
          }
  
          // Add the PROJECT columns first ...
          RequiredColumnVisitor collectionVisitor = new RequiredColumnVisitor(names);
          if (columns != null) {
              for (Column projectedColumn : columns) {
                  collectionVisitor.visit(projectedColumn);
              }
          }
  
          // Now add the columns from the JOIN, SELECT, PROJECT and SORT ancestors ...
          node = planNode;
          do {
              switch (node.getType()) {
                  case JOIN:
                      List<Constraint> criteria = node.getPropertyAsList(Property.JOIN_CONSTRAINTS, Constraint.class);
                      JoinCondition joinCondition = node.getProperty(Property.JOIN_CONDITION, JoinCondition.class);
                      Visitors.visitAll(criteria, collectionVisitor);
                      Visitors.visitAll(joinCondition, collectionVisitor);
                      break;
                  case SELECT:
                      Constraint constraint = node.getProperty(Property.SELECT_CRITERIA, Constraint.class);
                      Visitors.visitAll(constraint, collectionVisitor);
                      break;
                  case SORT:
                      List<Object> orderBys = node.getPropertyAsList(Property.SORT_ORDER_BY, Object.class);
                      if (orderBys != null && !orderBys.isEmpty()) {
                          if (orderBys.get(0) instanceof Ordering) {
                              for (int i = 0; i != orderBys.size(); ++i) {
                                  Ordering ordering = (Ordering)orderBys.get(i);
                                  Visitors.visitAll(ordering, collectionVisitor);
                              }
                          }
                      }
                      break;
                  case PROJECT:
                      if (node != planNode) {
                          // Already handled above, but we can stop looking for columns ...
                          return collectionVisitor.getRequiredColumns();
                      }
                      break;
                  default:
                      break;
              }
              node = node.getParent();
          } while (node != null);
          return collectionVisitor.getRequiredColumns();
      }