docs: protected NodeSequence createNodeSequence( QueryCommand originalQuery,
                                                 ScanQueryContext context,
                                                 PlanNode plan,
                                                 Columns columns,
                                                 QuerySources sources ) {
          NodeSequence rows = null;
          final String workspaceName = sources.getWorkspaceName();
          final NodeCache cache = context.getNodeCache(workspaceName);
          final TypeSystem types = context.getTypeSystem();
          final BufferManager bufferManager = context.getBufferManager();
  
          switch (plan.getType()) {
              case ACCESS:
                  // If the ACCESS node is known to never have results ...
                  if (plan.hasProperty(Property.ACCESS_NO_RESULTS)) {
                      rows = NodeSequence.emptySequence(columns.getColumns().size());
                  } else {
                      // Create the sequence for the plan node under the the ACCESS node ...
                      assert plan.getChildCount() == 1;
                      rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources);
                  }
                  break;
              case DEPENDENT_QUERY:
                  assert plan.getChildCount() == 2;
                  // Create the independent query from the left ...
                  PlanNode indepPlan = plan.getFirstChild();
                  Columns indepColumns = context.columnsFor(indepPlan);
                  String variableName = indepPlan.getProperty(Property.VARIABLE_NAME, String.class);
                  NodeSequence independent = createNodeSequence(originalQuery, context, indepPlan, indepColumns, sources);
  
                  // Create an extractor to get the value specified in the columns ...
                  Column column = indepColumns.getColumns().get(0);
                  boolean allowMultiValued = false;
                  String typeName = indepColumns.getColumnTypeForProperty(column.getSelectorName(), column.getPropertyName());
                  TypeFactory<?> type = context.getTypeSystem().getTypeFactory(typeName);
                  ExtractFromRow indepExtractor = createExtractFromRow(column.getSelectorName(), column.getPropertyName(), context,
                                                                       indepColumns, sources, type, allowMultiValued);
                  // Create the sequence for the dependent query ...
                  PlanNode depPlan = plan.getLastChild();
                  Columns depColumns = context.columnsFor(depPlan);
                  NodeSequence dependent = createNodeSequence(originalQuery, context, depPlan, depColumns, sources);
  
                  // now create the dependent query ...
                  rows = new DependentQuery(independent, indepExtractor, type, dependent, variableName, context.getVariables());
                  break;
              case DUP_REMOVE:
                  assert plan.getChildCount() == 1;
                  if (plan.getFirstChild().getType() == Type.SORT) {
                      // There is a SORT below this DUP_REMOVE, and we can do that in one fell swoop with the sort ...
                      rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources);
                  } else {
                      // Create the sequence for the plan node under the DUP_REMOVE ...
                      rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources);
                      if (!rows.isEmpty() && !(rows instanceof DistinctSequence)) {
                          // Wrap that with a sequence that removes duplicates ...
                          boolean useHeap = false;
                          rows = new DistinctSequence(rows, context.getTypeSystem(), context.getBufferManager(), useHeap);
                      }
                  }
                  break;
              case GROUP:
                  throw new UnsupportedOperationException();
              case JOIN:
                  // Create the components under the JOIN ...
                  assert plan.getChildCount() == 2;
                  PlanNode leftPlan = plan.getFirstChild();
                  PlanNode rightPlan = plan.getLastChild();
  
                  // Define the columns for each side, taken from the supplied columns ...
                  Columns leftColumns = context.columnsFor(leftPlan);
                  Columns rightColumns = context.columnsFor(rightPlan);
  
                  // Query context for the join (must remove isExists condition).
                  ScanQueryContext joinQueryContext = context;
                  if (context.getHints().isExistsQuery) {
                      // must not push down a LIMIT 1 condition to joins.
                      PlanHints joinPlanHints = context.getHints().clone();
                      joinPlanHints.isExistsQuery = false;
                      joinQueryContext = context.with(joinPlanHints);
                  }
  
                  NodeSequence left = createNodeSequence(originalQuery, joinQueryContext, leftPlan, leftColumns, sources);
                  NodeSequence right = createNodeSequence(originalQuery, joinQueryContext, rightPlan, rightColumns, sources);
  
                  // Figure out the join algorithm ...
                  JoinAlgorithm algorithm = plan.getProperty(Property.JOIN_ALGORITHM, JoinAlgorithm.class);
                  JoinType joinType = plan.getProperty(Property.JOIN_TYPE, JoinType.class);
                  JoinCondition joinCondition = plan.getProperty(Property.JOIN_CONDITION, JoinCondition.class);
                  boolean pack = false;
                  boolean useHeap = false;
                  if (0 >= right.getRowCount() && right.getRowCount() < 100) useHeap = true;
                  ExtractFromRow leftExtractor = null;
                  ExtractFromRow rightExtractor = null;
                  RangeProducer<?> rangeProducer = null;
                  switch (algorithm) {
                      case NESTED_LOOP:
                          // rows = new NestedLoopJoinComponent(context, left, right, joinCondition, joinType);
                          // break;
                      case MERGE:
                          if (joinCondition instanceof SameNodeJoinCondition) {
                              SameNodeJoinCondition condition = (SameNodeJoinCondition)joinCondition;
                              // check if the JOIN was not reversed by an optimization
                              boolean joinReversed = !leftColumns.getSelectorNames().contains(condition.getSelector1Name());
                              int leftIndex;
                              int rightIndex;
                              if (joinReversed) {
                                  // figure out the row indexes for the different selectors ...
                                  leftIndex = leftColumns.getSelectorIndex(condition.getSelector2Name());
                                  rightIndex = rightColumns.getSelectorIndex(condition.getSelector1Name());
                              } else {
                                  leftIndex = leftColumns.getSelectorIndex(condition.getSelector1Name());
                                  rightIndex = rightColumns.getSelectorIndex(condition.getSelector2Name());
                              }
                              String relativePath = condition.getSelector2Path();
                              if (relativePath != null) {
                                  // Get extractors that will get the path of the nodes ...
                                  PathFactory pathFactory = context.getExecutionContext().getValueFactories().getPathFactory();
                                  Path relPath = pathFactory.create(relativePath);
                                  if (joinReversed) {
                                      leftExtractor = RowExtractors.extractRelativePath(leftIndex, relPath, cache, types);
                                      rightExtractor = RowExtractors.extractPath(rightIndex, cache, types);
                                  } else {
                                      leftExtractor = RowExtractors.extractPath(leftIndex, cache, types);
                                      rightExtractor = RowExtractors.extractRelativePath(rightIndex, relPath, cache, types);
                                  }
                              } else {
                                  // The nodes must be the same node ...
                                  leftExtractor = RowExtractors.extractNodeKey(leftIndex, cache, types);
                                  rightExtractor = RowExtractors.extractNodeKey(rightIndex, cache, types);
                              }
                          } else if (joinCondition instanceof ChildNodeJoinCondition) {
                              ChildNodeJoinCondition condition = (ChildNodeJoinCondition)joinCondition;
  
                              // check if the JOIN was not reversed by an optimization
                              boolean joinReversed = !leftColumns.getSelectorNames().contains(condition.getParentSelectorName());
  
                              if (joinReversed) {
                                  int leftIndex = leftColumns.getSelectorIndex(condition.getChildSelectorName());
                                  int rightIndex = rightColumns.getSelectorIndex(condition.getParentSelectorName());
  
                                  leftExtractor = RowExtractors.extractParentNodeKey(leftIndex, cache, types);
                                  rightExtractor = RowExtractors.extractNodeKey(rightIndex, cache, types);
                              } else {
                                  int leftIndex = leftColumns.getSelectorIndex(condition.getParentSelectorName());
                                  int rightIndex = rightColumns.getSelectorIndex(condition.getChildSelectorName());
  
                                  leftExtractor = RowExtractors.extractNodeKey(leftIndex, cache, types);
                                  rightExtractor = RowExtractors.extractParentNodeKey(rightIndex, cache, types);
                              }
                          } else if (joinCondition instanceof EquiJoinCondition) {
                              EquiJoinCondition condition = (EquiJoinCondition)joinCondition;
                              // check if the JOIN was not reversed by an optimization
                              boolean joinReversed = !leftColumns.getSelectorNames().contains(condition.getSelector1Name());
  
                              String sel1 = condition.getSelector1Name();
                              String sel2 = condition.getSelector2Name();
                              String prop1 = condition.getProperty1Name();
                              String prop2 = condition.getProperty2Name();
                              if (joinReversed) {
                                  leftExtractor = createExtractFromRow(sel2, prop2, joinQueryContext, leftColumns, sources, null,
                                                                       true);
                                  rightExtractor = createExtractFromRow(sel1, prop1, joinQueryContext, rightColumns, sources, null,
                                                                        true);
                              } else {
                                  leftExtractor = createExtractFromRow(sel1, prop1, joinQueryContext, leftColumns, sources, null,
                                                                       true);
                                  rightExtractor = createExtractFromRow(sel2, prop2, joinQueryContext, rightColumns, sources, null,
                                                                        true);
                              }
  
                          } else if (joinCondition instanceof DescendantNodeJoinCondition) {
                              DescendantNodeJoinCondition condition = (DescendantNodeJoinCondition)joinCondition;
                              // For this to work, we want the ancestors to be on the left, so that the descendants can quickly
                              // be found given a path of each ancestor ...
                              assert leftColumns.getSelectorNames().contains(condition.getAncestorSelectorName());
                              String ancestorSelector = condition.getAncestorSelectorName();
                              String descendantSelector = condition.getDescendantSelectorName();
                              int ancestorSelectorIndex = leftColumns.getSelectorIndex(ancestorSelector);
                              int descendantSelectorIndex = rightColumns.getSelectorIndex(descendantSelector);
                              leftExtractor = RowExtractors.extractPath(ancestorSelectorIndex, cache, types);
                              rightExtractor = RowExtractors.extractPath(descendantSelectorIndex, cache, types);
                              // This is the only time we need a RangeProducer ...
                              final PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory();
                              rangeProducer = new RangeProducer<Path>() {
                                  @Override
                                  public Range<Path> getRange( Path leftPath ) {
                                      if (leftPath.isRoot()) {
                                          // All paths are descendants of the root
                                          return new Range<>(leftPath, false, null, true);
                                      }
                                      // Given the path of the node on the left side of the join, find the range of all paths
                                      // that might be considered descendants of the left path....
                                      boolean includeLower = false; // we don't want to include the left node; only descendants
                                      // The upper bound path is the same as the left path, just with an incremented SNS ...
                                      Path.Segment lastSegment = leftPath.getLastSegment();
                                      Path.Segment upperSegment = paths.createSegment(lastSegment.getName(),
                                                                                      lastSegment.getIndex() + 1);
                                      Path upperBoundPath = paths.create(leftPath.getParent(), upperSegment);
                                      return new Range<>(leftPath, includeLower, upperBoundPath, false);
                                  }
                              };
                          } else {
                              assert false : "Unable to use merge algorithm with join conditions: " + joinCondition;
                              throw new UnsupportedOperationException();
                          }
                          break;
                  }
  
                  // Perform conversion if required ...
                  assert leftExtractor != null;
                  assert rightExtractor != null;
                  TypeFactory<?> leftType = leftExtractor.getType();
                  TypeFactory<?> rightType = rightExtractor.getType();
                  if (!leftType.equals(rightType)) {
                      // wrap the right extractor with a converting extractor ...
                      final TypeFactory<?> commonType = context.getTypeSystem().getCompatibleType(leftType, rightType);
                      if (!leftType.equals(commonType)) leftExtractor = RowExtractors.convert(leftExtractor, commonType);
                      if (!rightType.equals(commonType)) rightExtractor = RowExtractors.convert(rightExtractor, commonType);
                  }
  
                  rows = new HashJoinSequence(workspaceName, left, right, leftExtractor, rightExtractor, joinType,
                                              context.getBufferManager(), cache, rangeProducer, pack, useHeap);
                  // For each Constraint object applied to the JOIN, simply create a SelectComponent on top ...
                  RowFilter filter = null;
                  List<Constraint> constraints = plan.getPropertyAsList(Property.JOIN_CONSTRAINTS, Constraint.class);
                  if (constraints != null) {
                      for (Constraint constraint : constraints) {
                          RowFilter constraintFilter = createRowFilter(constraint, context, columns, sources);
                          filter = NodeSequence.requireBoth(filter, constraintFilter);
                      }
                  }
                  rows = NodeSequence.filter(rows, filter); // even if filter is null
                  break;
              case LIMIT:
                  // Create the sequence for the plan node under the LIMIT ...
                  assert plan.getChildCount() == 1;
                  rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources);
                  // Calculate the limit ...
                  Integer rowLimit = plan.getProperty(Property.LIMIT_COUNT, Integer.class);
                  Integer offset = plan.getProperty(Property.LIMIT_OFFSET, Integer.class);
                  Limit limit = Limit.NONE;
                  if (rowLimit != null) limit = limit.withRowLimit(rowLimit.intValue());
                  if (offset != null) limit = limit.withOffset(offset.intValue());
                  // Then create the limited sequence ...
                  if (!limit.isUnlimited()) {
                      rows = NodeSequence.limit(rows, limit);
                  }
                  break;
              case NULL:
                  // No results ...
                  rows = NodeSequence.emptySequence(columns.getColumns().size());
                  break;
              case PROJECT:
                  // Nothing to do, since the projected columns will be accessed as needed when the results are processed. Instead,
                  // just process the PROJECT node's only child ...
                  PlanNode child = plan.getFirstChild();
                  columns = context.columnsFor(child);
                  rows = createNodeSequence(originalQuery, context, child, columns, sources);
                  break;
              case SELECT:
                  // Create the sequence for the plan node under the SELECT ...
                  assert plan.getChildCount() == 1;
                  rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources);
                  Constraint constraint = plan.getProperty(Property.SELECT_CRITERIA, Constraint.class);
                  filter = createRowFilter(constraint, context, columns, sources);
                  rows = NodeSequence.filter(rows, filter);
                  break;
              case SET_OPERATION:
                  Operation operation = plan.getProperty(Property.SET_OPERATION, Operation.class);
                  boolean all = plan.getProperty(Property.SET_USE_ALL, Boolean.class);
                  PlanNode firstPlan = plan.getFirstChild();
                  PlanNode secondPlan = plan.getLastChild();
                  Columns firstColumns = context.columnsFor(firstPlan);
                  Columns secondColumns = context.columnsFor(secondPlan);
                  NodeSequence first = createNodeSequence(originalQuery, context, firstPlan, firstColumns, sources);
                  NodeSequence second = createNodeSequence(originalQuery, context, secondPlan, secondColumns, sources);
                  useHeap = 0 >= second.getRowCount() && second.getRowCount() < 100;
                  if (first.width() != second.width()) {
                      // A set operation requires that the 'first' and 'second' sequences have the same width, but this is
                      // not necessarily the case (e.g., when one side involves a JOIN but the other does not). The columns
                      // will dictate which subset of selector indexes in the sequences should be used.
                      first = NodeSequence.slice(first, firstColumns);
                      second = NodeSequence.slice(second, secondColumns);
                      assert first.width() == second.width();
                  }
                  pack = false;
                  switch (operation) {
                      case UNION: {
                          // If one of them is empty, return the other ...
                          if (first.isEmpty()) return second;
                          if (second.isEmpty()) return first;
                          // This is really just a sequence with the two parts ...
                          rows = NodeSequence.append(first, second);
                          break;
                      }
                      case INTERSECT: {
                          // If one of them is empty, there are no results ...
                          if (first.isEmpty()) return first;
                          if (second.isEmpty()) return second;
                          rows = new IntersectSequence(workspaceName, first, second, types, bufferManager, cache, pack, useHeap);
                          break;
                      }
                      case EXCEPT: {
                          // If the second is empty, there's nothing to exclude ...
                          if (second.isEmpty()) return first;
                          rows = new ExceptSequence(workspaceName, first, second, types, bufferManager, cache, pack, useHeap);
                          break;
                      }
                  }
                  if (!all) {
                      useHeap = false;
                      rows = new DistinctSequence(rows, context.getTypeSystem(), context.getBufferManager(), useHeap);
                  }
                  break;
              case SORT:
                  assert plan.getChildCount() == 1;
                  PlanNode delegate = plan.getFirstChild();
                  boolean allowDuplicates = true;
                  if (delegate.getType() == Type.DUP_REMOVE) {
                      // This SORT already removes duplicates, so we can skip the first child ...
                      delegate = delegate.getFirstChild();
                      allowDuplicates = false;
                  }
                  PlanNode parent = plan.getParent();
                  if (parent != null && parent.getType() == Type.DUP_REMOVE) {
                      // The parent is a DUP_REMOVE (shouldn't really happen in an optimized plan), we should disallow duplicates
                      // ...
                      allowDuplicates = false;
                  }
                  // Create the sequence for the delegate plan node ...
                  rows = createNodeSequence(originalQuery, context, delegate, columns, sources);
                  if (!rows.isEmpty()) {
                      // Prepare to wrap this delegate sequence based upon the SORT_ORDER_BY ...
                      List<Object> orderBys = plan.getPropertyAsList(Property.SORT_ORDER_BY, Object.class);
                      if (!orderBys.isEmpty()) {
                          // Create an extractor from the orderings that we'll use for the sorting ...
                          ExtractFromRow sortExtractor = null;
                          pack = false;
                          useHeap = false;
                          NullOrder nullOrder = null;
                          if (orderBys.get(0) instanceof Ordering) {
                              List<Ordering> orderings = new ArrayList<Ordering>(orderBys.size());
                              for (Object orderBy : orderBys) {
                                  orderings.add((Ordering)orderBy);
                              }
                              // Determine the alias-to-name mappings for the selectors in the orderings ...
                              Map<SelectorName, SelectorName> sourceNamesByAlias = new HashMap<SelectorName, SelectorName>();
                              for (PlanNode source : plan.findAllAtOrBelow(Type.SOURCE)) {
                                  SelectorName name = source.getProperty(Property.SOURCE_NAME, SelectorName.class);
                                  SelectorName alias = source.getProperty(Property.SOURCE_ALIAS, SelectorName.class);
                                  if (alias != null) sourceNamesByAlias.put(alias, name);
                              }
                              // If there are multiple orderings, then we'll never have nulls. But if there is just one ordering,
                              // we have to handle nulls ...
                              if (orderings.size() == 1) {
                                  nullOrder = orderings.get(0).nullOrder();
                              }
                              // Now create the single sorting extractor ...
                              sortExtractor = createSortingExtractor(orderings, sourceNamesByAlias, context, columns, sources);
                          } else {
                              // Order by the location(s) because it's before a merge-join ...
                              final TypeFactory<?> keyType = context.getTypeSystem().getReferenceFactory();
                              List<ExtractFromRow> extractors = new ArrayList<>();
                              for (Object ordering : orderBys) {
                                  SelectorName selectorName = (SelectorName)ordering;
                                  final int index = columns.getSelectorIndex(selectorName.name());
                                  extractors.add(new ExtractFromRow() {
                                      @Override
                                      public TypeFactory<?> getType() {
                                          return keyType;
                                      }
  
                                      @Override
                                      public Object getValueInRow( RowAccessor row ) {
                                          CachedNode node = row.getNode(index);
                                          return node != null ? node.getKey() : null;
                                      }
                                  });
                              }
                              // This is jsut for a merge join, so use standard null ordering ...
                              nullOrder = NullOrder.NULLS_LAST;
                              // Now create the single sorting extractor ...
                              sortExtractor = RowExtractors.extractorWith(extractors);
                          }
  
                          // Now create the sorting sequence ...
                          if (sortExtractor != null) {
                              rows = new SortingSequence(workspaceName, rows, sortExtractor, bufferManager, cache, pack, useHeap,
                                                         allowDuplicates, nullOrder);
                          }
                      }
                  }
                  break;
              case SOURCE:
                  // Otherwise, just grab all of the nodes ...
                  rows = createNodeSequenceForSource(originalQuery, context, plan, columns, sources);
                  break;
              default:
                  break;
          }
          return rows;
      }