/**
 *
 *
 * <h1>Overview</h1>
 *
 * GroupScan_Default scans a group in hkey order.
 *
 * <h1>Arguments</h1>
 *
 * <ul>
 *   <li><b>GroupTable groupTable:</b> The group table to be scanned.
 *   <li><b>Limit limit (DEPRECATED):</b> A limit on the number of rows to be returned. The limit is
 *       specific to one Table. Deprecated because the result is not well-defined. In the case of a
 *       branching group, a limit on one sibling has impliciations on the return of rows of other
 *       siblings.
 *   <li>IndexKeyRange indexKeyRange (DEPRECATED):</b> Specifies an index restriction for
 *       hkey-equivalent indexes. Deprecated because hkey-equivalent indexes were used
 *       automatically, sometimes reducing performance. Need to revisit the concept.
 *       <ul>
 *         <h1>Behavior</h1>
 *         The rows of a group table are returned in hkey order.
 *         <h1>Output</h1>
 *         Nothing else to say.
 *         <h1>Assumptions</h1>
 *         None.
 *         <h1>Performance</h1>
 *         GroupScan_Default does a complete scan of a group table, relying on nothing but
 *         sequential access.
 *         <h1>Memory Requirements</h1>
 *         None.
 */
class GroupScan_Default extends Operator {
  // Object interface

  @Override
  public String toString() {
    return getClass().getSimpleName() + '(' + cursorCreator + ')';
  }

  // Operator interface

  @Override
  protected Cursor cursor(QueryContext context, QueryBindingsCursor bindingsCursor) {
    return new Execution(context, bindingsCursor, cursorCreator);
  }

  // GroupScan_Default interface

  public GroupScan_Default(GroupCursorCreator cursorCreator) {
    ArgumentValidation.notNull("groupTable", cursorCreator);
    this.cursorCreator = cursorCreator;
  }

  // Class state

  private static final InOutTap TAP_OPEN =
      OPERATOR_TAP.createSubsidiaryTap("operator: GroupScan_Default open");
  private static final InOutTap TAP_NEXT =
      OPERATOR_TAP.createSubsidiaryTap("operator: GroupScan_Default next");
  private static final Logger LOG = LoggerFactory.getLogger(GroupScan_Default.class);

  // Object state

  private final GroupCursorCreator cursorCreator;

  @Override
  public CompoundExplainer getExplainer(ExplainContext context) {
    Attributes att = new Attributes();

    att.put(Label.NAME, PrimitiveExplainer.getInstance(getName()));
    att.put(Label.SCAN_OPTION, PrimitiveExplainer.getInstance(cursorCreator.describeRange()));
    TableName rootName = cursorCreator.group().getRoot().getName();
    att.put(Label.TABLE_SCHEMA, PrimitiveExplainer.getInstance(rootName.getSchemaName()));
    att.put(Label.TABLE_NAME, PrimitiveExplainer.getInstance(rootName.getTableName()));
    return new CompoundExplainer(Type.SCAN_OPERATOR, att);
  }

  // Inner classes

  private static class Execution extends LeafCursor implements Rebindable {

    // Cursor interface

    @Override
    public void open() {
      TAP_OPEN.in();
      try {
        super.open();
        cursor.open();
      } finally {
        TAP_OPEN.out();
      }
    }

    @Override
    public Row next() {
      if (TAP_NEXT_ENABLED) {
        TAP_NEXT.in();
      }
      try {
        checkQueryCancelation();
        Row row;
        if ((row = cursor.next()) == null) {
          setIdle();
        }
        if (LOG_EXECUTION) {
          LOG.debug("GroupScan_Default: yield {}", row);
        }
        return row;
      } finally {
        if (TAP_NEXT_ENABLED) {
          TAP_NEXT.out();
        }
      }
    }

    @Override
    public void close() {
      try {
        cursor.close();
      } finally {
        super.close();
      }
    }

    @Override
    public boolean isIdle() {
      return cursor.isIdle();
    }

    @Override
    public boolean isActive() {
      return cursor.isActive();
    }

    @Override
    public QueryBindings nextBindings() {
      QueryBindings bindings = super.nextBindings();
      if (cursor instanceof BindingsAwareCursor) ((BindingsAwareCursor) cursor).rebind(bindings);
      return bindings;
    }

    @Override
    public void rebind(HKey hKey, boolean deep) {
      if (!canRebind) {
        throw new IllegalStateException("rebind not allowed for");
      }
      cursor.rebind(hKey, deep);
    }

    // Execution interface

    Execution(
        QueryContext context,
        QueryBindingsCursor bindingsCursor,
        GroupCursorCreator cursorCreator) {
      super(context, bindingsCursor);
      this.cursor = cursorCreator.cursor(context);
      this.canRebind = (cursorCreator instanceof FullGroupCursorCreator);
    }

    // Object state

    private final GroupCursor cursor;
    private final boolean canRebind;
  }

  static interface GroupCursorCreator {
    GroupCursor cursor(QueryContext context);

    Group group();

    String describeRange();
  }

  private abstract static class AbstractGroupCursorCreator implements GroupCursorCreator {

    // GroupCursorCreator interface

    @Override
    public final Group group() {
      return targetGroup;
    }

    // for use by subclasses

    protected AbstractGroupCursorCreator(Group group) {
      this.targetGroup = group;
    }

    @Override
    public final String toString() {
      return describeRange() + " on " + targetGroup.getRoot().getName();
    }

    // for overriding in subclasses

    private final Group targetGroup;
  }

  static class FullGroupCursorCreator extends AbstractGroupCursorCreator {

    // GroupCursorCreator interface

    @Override
    public GroupCursor cursor(QueryContext context) {
      return context.getStore(group().getRoot()).newGroupCursor(group());
    }

    // FullGroupCursorCreator interface

    public FullGroupCursorCreator(Group group) {
      super(group);
    }

    // AbstractGroupCursorCreator interface

    @Override
    public String describeRange() {
      return "full scan";
    }
  }

  static class PositionalGroupCursorCreator extends AbstractGroupCursorCreator {

    // GroupCursorCreator interface

    @Override
    public GroupCursor cursor(QueryContext context) {
      return new HKeyBoundCursor(
          context,
          context.getStore(group().getRoot()).newGroupCursor(group()),
          hKeyBindingPosition,
          deep,
          hKeyType,
          shortenUntil);
    }

    // PositionalGroupCursorCreator interface

    PositionalGroupCursorCreator(
        Group group, int hKeyBindingPosition, boolean deep, Table hKeyType, Table shortenUntil) {
      super(group);
      this.hKeyBindingPosition = hKeyBindingPosition;
      this.deep = deep;
      if ((shortenUntil == hKeyType) || shortenUntil.isDescendantOf(hKeyType)) {
        shortenUntil = null;
        hKeyType = null;
      }
      this.shortenUntil = shortenUntil;
      this.hKeyType = hKeyType;
      assert (hKeyType == null) == (shortenUntil == null) : hKeyType + " ~ " + shortenUntil;
      assert hKeyType == null || hKeyType.isDescendantOf(shortenUntil)
          : hKeyType + " is not a descendant of " + shortenUntil;
    }

    // AbstractGroupCursorCreator interface

    @Override
    public String describeRange() {
      return deep ? "deep hkey-bound scan" : "shallow hkey-bound scan";
    }

    // object state

    private final int hKeyBindingPosition;
    private final boolean deep;
    private final Table shortenUntil;
    private final Table hKeyType;
  }

  private static class HKeyBoundCursor extends RowCursorImpl
      implements BindingsAwareCursor, GroupCursor {

    @Override
    public void open() {
      super.open();
      HKey hKey = getHKeyFromBindings();
      input.rebind(hKey, deep);
      input.open();
    }

    @Override
    public Row next() {
      // If we've ever seen a row, just defer to input
      if (sawOne) {
        return input.next();
      }
      Row result = input.next();
      // If we saw a row, mark it as such and defer to input
      if (result != null) {
        sawOne = true;
        return result;
      }
      // Our search is at an end; return our answer
      if (atTable == null || atTable == stopSearchTable) {
        return null;
      }
      // Close the input, shorten our hkey, re-open and try again
      input.close();
      assert atTable.getParentTable() != null : atTable;
      atTable = atTable.getParentTable();
      HKey hkey = getHKeyFromBindings();
      hkey.useSegments(atTable.getDepth() + 1);
      input.rebind(hkey, deep);
      input.open();
      return next();
    }

    @Override
    public void jump(Row row, ColumnSelector columnSelector) {
      input.jump(row, columnSelector);
      state = CursorLifecycle.CursorState.ACTIVE;
    }

    @Override
    public void close() {
      try {
        // input is closed before hopefully reopening in next()
        if (!input.isClosed()) {
          input.close();
        }
      } finally {
        super.close();
      }
    }

    @Override
    public void rebind(QueryBindings bindings) {
      this.bindings = bindings;
    }

    @Override
    public void rebind(HKey hKey, boolean deep) {
      throw new UnsupportedOperationException();
    }

    HKeyBoundCursor(
        QueryContext context,
        GroupCursor input,
        int hKeyBindingPosition,
        boolean deep,
        Table hKeyType,
        Table shortenUntil) {
      this.context = context;
      this.input = input;
      this.hKeyBindingPosition = hKeyBindingPosition;
      this.deep = deep;
      this.atTable = hKeyType;
      this.stopSearchTable = shortenUntil;
    }

    private HKey getHKeyFromBindings() {
      return bindings.getHKey(hKeyBindingPosition);
    }

    private final QueryContext context;
    private final GroupCursor input;
    private final int hKeyBindingPosition;
    private final boolean deep;
    private Table atTable;
    private final Table stopSearchTable;
    private boolean sawOne = false;
    private QueryBindings bindings;
  }
}
Пример #2
0
/**
 *
 *
 * <h1>Overview</h1>
 *
 * Provides a very simple, count-based limit.
 *
 * <h1>Arguments</h1>
 *
 * <ul>
 *   <li><b>PhysicalOperator inputOperator:</b> The input operator, whose cursor will be limited
 *   <li><b>int limit:</b>&nbsp;the number of rows to limit to; cannot be negative
 * </ul>
 *
 * <h1>Behavior</h1>
 *
 * Limit_Default's cursor returns at most <i>limit</i> rows from the input operator's cursor. When
 * the limit is reached, the input cursor is automatically closed, and all subsequent calls to
 * <i>next</i> will return <i>false</i>.
 *
 * <h1>Output</h1>
 *
 * Nothing else to say.
 *
 * <h1>Assumptions</h1>
 *
 * None.
 *
 * <h1>Performance</h1>
 *
 * Essentially free.
 *
 * <h1>Memory Requirements</h1>
 *
 * None.
 */
final class Limit_Default extends Operator {

  // Operator interface

  @Override
  protected Cursor cursor(QueryContext context, QueryBindingsCursor bindingsCursor) {
    return new Execution(context, inputOperator.cursor(context, bindingsCursor));
  }

  // Plannable interface

  @Override
  public void findDerivedTypes(Set<RowType> derivedTypes) {
    inputOperator.findDerivedTypes(derivedTypes);
  }

  @Override
  public List<Operator> getInputOperators() {
    return Collections.singletonList(inputOperator);
  }

  @Override
  public String describePlan() {
    return super.describePlan();
  }

  // Object interface

  @Override
  public String toString() {
    StringBuilder str = new StringBuilder(getClass().getSimpleName());
    str.append("(");
    if (skip > 0) {
      str.append(String.format("skip=%d", skip));
    }
    if ((limit >= 0) && (limit < Integer.MAX_VALUE)) {
      if (skip > 0) str.append(", ");
      str.append(String.format("limit=%d", limit));
    }
    str.append(": ");
    str.append(inputOperator);
    str.append(")");
    return str.toString();
  }

  // Limit_Default interface

  Limit_Default(Operator inputOperator, int limit) {
    this(inputOperator, 0, false, limit, false);
  }

  Limit_Default(
      Operator inputOperator, int skip, boolean skipIsBinding, int limit, boolean limitIsBinding) {
    ArgumentValidation.isGTE("skip", skip, 0);
    ArgumentValidation.isGTE("limit", limit, 0);
    this.skip = skip;
    this.skipIsBinding = skipIsBinding;
    this.limit = limit;
    this.limitIsBinding = limitIsBinding;
    this.inputOperator = inputOperator;
  }

  public int skip() {
    return skip;
  }

  public boolean isSkipBinding() {
    return skipIsBinding;
  }

  public int limit() {
    return limit;
  }

  public boolean isLimitBinding() {
    return limitIsBinding;
  }

  // Class state

  private static final InOutTap TAP_OPEN =
      OPERATOR_TAP.createSubsidiaryTap("operator: Limit_Default open");
  private static final InOutTap TAP_NEXT =
      OPERATOR_TAP.createSubsidiaryTap("operator: Limit_Default next");
  private static final Logger LOG = LoggerFactory.getLogger(Limit_Default.class);

  // Object state

  private final int skip, limit;
  private final boolean skipIsBinding, limitIsBinding;
  private final Operator inputOperator;

  @Override
  public CompoundExplainer getExplainer(ExplainContext context) {
    Attributes atts = new Attributes();
    atts.put(Label.NAME, PrimitiveExplainer.getInstance(getName()));
    atts.put(Label.LIMIT, PrimitiveExplainer.getInstance(limit));
    atts.put(Label.INPUT_OPERATOR, inputOperator.getExplainer(context));
    return new CompoundExplainer(Type.LIMIT_OPERATOR, atts);
  }

  // internal classes

  private class Execution extends ChainedCursor {

    // Cursor interface

    @Override
    public void open() {
      TAP_OPEN.in();
      try {
        if (isSkipBinding()) {
          ValueSource value = bindings.getValue(skip());
          if (!value.isNull()) this.skipLeft = value.getInt32();
        } else {
          this.skipLeft = skip();
        }
        if (skipLeft < 0) throw new NegativeLimitException("OFFSET", skipLeft);
        if (isLimitBinding()) {
          ValueSource value = bindings.getValue(limit());
          if (value.isNull()) this.limitLeft = Integer.MAX_VALUE;
          else {
            TInstance type = MNumeric.INT.instance(true);
            TExecutionContext executionContext = new TExecutionContext(null, type, context);
            Value ivalue = new Value(MNumeric.INT.instance(true));
            MNumeric.INT.fromObject(executionContext, value, ivalue);
            this.limitLeft = ivalue.getInt32();
          }
        } else {
          this.limitLeft = limit();
        }
        if (limitLeft < 0) throw new NegativeLimitException("LIMIT", limitLeft);
        super.open();
      } finally {
        TAP_OPEN.out();
      }
    }

    @Override
    public void close() {
      // Because the checks in open() may
      // prevent the cursor from being opened.
      if (!isClosed()) {
        super.close();
      }
    }

    @Override
    public Row next() {
      if (TAP_NEXT_ENABLED) {
        TAP_NEXT.in();
      }
      try {
        if (CURSOR_LIFECYCLE_ENABLED) {
          CursorLifecycle.checkIdleOrActive(this);
        }
        checkQueryCancelation();
        Row row;
        while (skipLeft > 0) {
          if ((row = input.next()) == null) {
            skipLeft = 0;
            limitLeft = -1;
            setIdle();
            if (LOG_EXECUTION) {
              LOG.debug("Limit_Default: skipLeft until complete yield null");
            }
            return null;
          }
          skipLeft--;
        }
        if (limitLeft < 0) {
          setIdle();
          if (LOG_EXECUTION) {
            LOG.debug("Limit_Default: limitLeft < 0, yield null");
          }
          return null;
        }
        if (limitLeft == 0) {
          setIdle();
          if (LOG_EXECUTION) {
            LOG.debug("Limit_Default: limitLeft == 0, yield null");
          }
          return null;
        }
        if ((row = input.next()) == null) {
          limitLeft = -1;
          setIdle();
          if (LOG_EXECUTION) {
            LOG.debug("Limit_Default: yield null");
          }
          return null;
        }
        --limitLeft;
        if (LOG_EXECUTION) {
          LOG.debug("Limit_Default: yield {}", row);
        }
        return row;
      } finally {
        if (TAP_NEXT_ENABLED) {
          TAP_NEXT.out();
        }
      }
    }

    // Execution interface
    Execution(QueryContext context, Cursor input) {
      super(context, input);
    }

    // object state

    private int skipLeft, limitLeft;
  }
}
Пример #3
0
/**
 *
 *
 * <h1>Overview</h1>
 *
 * <p>Select_BloomFilter checks whether an input row, projected to a given set of expressions, is
 * present in a "filtering" set of rows. The implementation is optimized to avoid IO operations on
 * the filtering set of rows through the use of a bloom filter.
 *
 * <p>
 *
 * <h1>Arguments</h1>
 *
 * <p>
 * <li><b>Operator input:</b> Operator providing the input stream
 * <li><b>Operator onPositive:</b> Operator used to provide output rows
 * <li><b>List<BoundFieldExpression> fields:</b> expressions applied to the input row to obtain a
 *     bloom filter hash key
 * <li><b>int bindingPosition:</b> Location in the query context of the bloom filter, which has been
 *     loaded by the Using_BloomFilter operator. This bindingPosition is also used to hold a row
 *     from the input stream that passes the filter and needs to be tested for existence using the
 *     onPositive operator.
 * <li><b>boolean pipeline:</b> Whether to use bracketing cursors instead of rebinding.
 * <li><b>int depth:</b> Number of nested Maps, including this one.
 *
 *     <p>
 *
 *     <h1>Behavior</h1>
 *
 *     <p>Each call of next operates as follows. A row is obtained from the input operator. The
 *     expressions from fields are evaluated and the resulting values are used to probe the bloom
 *     filter. If the filter returns false, this indicates that there is no matching row in the
 *     filtering set of rows and null is returned. If the filter returns true, then the onPositive
 *     operator is used to locate the matching row. If a row is located then the input row (not the
 *     row from onPositive) is returned, otherwise null is returned.
 *
 *     <p>
 *
 *     <h1>Output</h1>
 *
 *     <p>A subset of rows from the input stream.
 *
 *     <p>
 *
 *     <h1>Assumptions</h1>
 *
 *     <p>None.
 *
 *     <p>
 *
 *     <h1>Performance</h1>
 *
 *     <p>This operator should generate very little IO activity, although bloom filters are
 *     probabilistic.
 *
 *     <p>
 *
 *     <h1>Memory Requirements</h1>
 *
 *     <p>This operator relies on the bloom filter created by Using_BloomFilter.
 */
class Select_BloomFilter extends Operator {
  // Object interface

  @Override
  public String toString() {
    return getClass().getSimpleName();
  }

  // Operator interface

  @Override
  public void findDerivedTypes(Set<RowType> derivedTypes) {
    input.findDerivedTypes(derivedTypes);
    onPositive.findDerivedTypes(derivedTypes);
  }

  @Override
  protected Cursor cursor(QueryContext context, QueryBindingsCursor bindingsCursor) {
    if (!pipeline) {
      return new Execution<>(context, bindingsCursor, tFields, newExpressionsAdapter);
    } else {
      assert (tFields != null);
      Cursor inputCursor = input.cursor(context, bindingsCursor);
      QueryBindingsCursor toBindings =
          new FilterBindingsCursor(
              context, inputCursor, bindingPosition, depth, tFields, newExpressionsAdapter);
      Cursor checkCursor = onPositive.cursor(context, toBindings);
      return new RecoverRowsCursor(context, checkCursor, bindingPosition, depth);
    }
  }

  @Override
  public List<Operator> getInputOperators() {
    return Arrays.asList(input, onPositive);
  }

  @Override
  public String describePlan() {
    return String.format("%s\n%s", describePlan(input), describePlan(onPositive));
  }

  // Select_BloomFilter interface

  public Select_BloomFilter(
      Operator input,
      Operator onPositive,
      List<? extends TPreparedExpression> tFields,
      List<AkCollator> collators,
      int bindingPosition,
      boolean pipeline,
      int depth) {
    ArgumentValidation.notNull("input", input);
    ArgumentValidation.notNull("onPositive", onPositive);
    ArgumentValidation.notNull("Fields", tFields);
    int size = tFields.size();
    ArgumentValidation.isGT("fields.size()", size, 0);
    ArgumentValidation.isGTE("bindingPosition", bindingPosition, 0);
    ArgumentValidation.isGT("depth", depth, 0);
    this.input = input;
    this.onPositive = onPositive;
    this.bindingPosition = bindingPosition;
    this.pipeline = pipeline;
    this.depth = depth;
    this.tFields = tFields;
    this.collators = collators;
  }

  // For use by this class

  private AkCollator collator(int f) {
    return collators == null ? null : collators.get(f);
  }

  // Class state

  private static final InOutTap TAP_OPEN =
      OPERATOR_TAP.createSubsidiaryTap("operator: Select_BloomFilter open");
  private static final InOutTap TAP_NEXT =
      OPERATOR_TAP.createSubsidiaryTap("operator: Select_BloomFilter next");
  private static final InOutTap TAP_CHECK =
      OPERATOR_TAP.createSubsidiaryTap("operator: Select_BloomFilter check");
  private static final Logger LOG = LoggerFactory.getLogger(Select_BloomFilter.class);

  // Object state

  private final Operator input;
  private final Operator onPositive;
  private final int bindingPosition, depth;
  private final boolean pipeline;
  private final List<? extends TPreparedExpression> tFields;
  private final List<AkCollator> collators;

  @Override
  public CompoundExplainer getExplainer(ExplainContext context) {
    Attributes atts = new Attributes();
    atts.put(Label.NAME, PrimitiveExplainer.getInstance(getName()));
    atts.put(Label.BINDING_POSITION, PrimitiveExplainer.getInstance(bindingPosition));
    atts.put(Label.INPUT_OPERATOR, input.getExplainer(context));
    atts.put(Label.INPUT_OPERATOR, onPositive.getExplainer(context));
    for (TPreparedExpression field : tFields) {
      atts.put(Label.EXPRESSIONS, field.getExplainer(context));
    }
    atts.put(Label.PIPELINE, PrimitiveExplainer.getInstance(pipeline));
    return new CompoundExplainer(Type.BLOOM_FILTER, atts);
  }

  // Inner classes

  private interface ExpressionAdapter<EXPR, EVAL> {
    EVAL evaluate(EXPR expression, QueryContext contex);

    int hash(StoreAdapter adapter, EVAL evaluation, Row row, AkCollator collator);
  }

  private static ExpressionAdapter<TPreparedExpression, TEvaluatableExpression>
      newExpressionsAdapter =
          new ExpressionAdapter<TPreparedExpression, TEvaluatableExpression>() {
            @Override
            public TEvaluatableExpression evaluate(
                TPreparedExpression expression, QueryContext contex) {
              TEvaluatableExpression eval = expression.build();
              eval.with(contex);
              return eval;
            }

            @Override
            public int hash(
                StoreAdapter adapter,
                TEvaluatableExpression evaluation,
                Row row,
                AkCollator collator) {
              evaluation.with(row);
              evaluation.evaluate();
              return ValueSources.hash(evaluation.resultValue(), collator);
            }
          };

  private class Execution<E> extends OperatorCursor {
    // Cursor interface

    @Override
    public void open() {
      TAP_OPEN.in();
      try {
        CursorLifecycle.checkIdle(this);
        filter = bindings.getBloomFilter(bindingPosition);
        bindings.setBloomFilter(bindingPosition, null);
        inputCursor.open();
        idle = false;
      } finally {
        TAP_OPEN.out();
      }
    }

    @Override
    public Row next() {
      if (TAP_NEXT_ENABLED) {
        TAP_NEXT.in();
      }
      try {
        if (CURSOR_LIFECYCLE_ENABLED) {
          CursorLifecycle.checkIdleOrActive(this);
        }
        Row row;
        do {
          row = inputCursor.next();
          if (row == null) {
            close();
          } else if (!filter.maybePresent(hashProjectedRow(row)) || !rowReallyHasMatch(row)) {
            row = null;
          }
        } while (!idle && row == null);
        if (LOG_EXECUTION) {
          LOG.debug("Select_BloomFilter: yield {}", row);
        }
        return row;
      } finally {
        if (TAP_NEXT_ENABLED) {
          TAP_NEXT.out();
        }
      }
    }

    @Override
    public void close() {
      CursorLifecycle.checkIdleOrActive(this);
      if (!idle) {
        inputCursor.close();
        idle = true;
      }
    }

    @Override
    public void destroy() {
      if (!destroyed) {
        close();
        inputCursor.destroy();
        onPositiveCursor.destroy();
        filter = null;
        destroyed = true;
      }
    }

    @Override
    public boolean isIdle() {
      return !destroyed && idle;
    }

    @Override
    public boolean isActive() {
      return !destroyed && !idle;
    }

    @Override
    public boolean isDestroyed() {
      return destroyed;
    }

    @Override
    public void openBindings() {
      inputCursor.openBindings();
    }

    @Override
    public QueryBindings nextBindings() {
      bindings = inputCursor.nextBindings();
      return bindings;
    }

    @Override
    public void closeBindings() {
      inputCursor.closeBindings();
    }

    @Override
    public void cancelBindings(QueryBindings bindings) {
      inputCursor.cancelBindings(bindings);
      idle = true;
    }

    // Execution interface

    <EXPR> Execution(
        QueryContext context,
        QueryBindingsCursor bindingsCursor,
        List<? extends EXPR> expressions,
        ExpressionAdapter<EXPR, E> adapter) {
      super(context);
      this.inputCursor = input.cursor(context, bindingsCursor);
      this.onPositiveBindingsCursor = new SingletonQueryBindingsCursor(null);
      this.onPositiveCursor = onPositive.cursor(context, onPositiveBindingsCursor);
      this.adapter = adapter;
      for (EXPR field : expressions) {
        E eval = adapter.evaluate(field, context);
        fieldEvals.add(eval);
      }
    }

    // For use by this class

    private int hashProjectedRow(Row row) {
      int hash = 0;
      for (int f = 0; f < fieldEvals.size(); f++) {
        E fieldEval = fieldEvals.get(f);
        hash = hash ^ adapter.hash(adapter(), fieldEval, row, collator(f));
      }
      return hash;
    }

    private boolean rowReallyHasMatch(Row row) {
      // bindingPosition is used to hold onto a row for use during the evaluation of expressions
      // during onPositiveCursor.open(). This is somewhat sleazy, but the alternative is to
      // complicate the Select_BloomFilter API, requiring the caller to specify another binding
      // position.
      // It is safe to reuse the binding position in this way because the filter is extracted and
      // stored
      // in a field during open(), while the use of the binding position for use in the onPositive
      // lookup
      // occurs during next().
      if (LOG_EXECUTION) {
        LOG.debug("Select_BloomFilter: candidate {}", row);
      }
      TAP_CHECK.in();
      try {
        bindings.setRow(bindingPosition, row);
        onPositiveBindingsCursor.reset(bindings);
        onPositiveCursor.openTopLevel();
        try {
          return onPositiveCursor.next() != null;
        } finally {
          onPositiveCursor.closeTopLevel();
          bindings.setRow(bindingPosition, null);
        }
      } finally {
        TAP_CHECK.out();
      }
    }

    // Object state

    private final Cursor inputCursor;
    private final Cursor onPositiveCursor;
    private final SingletonQueryBindingsCursor onPositiveBindingsCursor;
    private QueryBindings bindings;
    private BloomFilter filter;
    private final List<E> fieldEvals = new ArrayList<>();
    private final ExpressionAdapter<?, E> adapter;
    private boolean idle = true;
    private boolean destroyed = false;
  }

  // Turn input rows that match the filter into bindings for the onPositive plan.
  private class FilterBindingsCursor extends Map_NestedLoops.RowToBindingsCursor {
    private final StoreAdapter storeAdapter;
    private final List<TEvaluatableExpression> fieldEvals = new ArrayList<>();
    private final ExpressionAdapter<TPreparedExpression, TEvaluatableExpression> expressionAdapter;

    public FilterBindingsCursor(
        QueryContext context,
        Cursor input,
        int bindingPosition,
        int depth,
        List<? extends TPreparedExpression> expressions,
        ExpressionAdapter<TPreparedExpression, TEvaluatableExpression> expressionAdapter) {
      super(input, bindingPosition, depth);
      this.storeAdapter = context.getStore();
      this.expressionAdapter = expressionAdapter;
      for (TPreparedExpression field : expressions) {
        TEvaluatableExpression eval = expressionAdapter.evaluate(field, context);
        fieldEvals.add(eval);
      }
    }

    @Override
    protected Row nextInputRow() {
      BloomFilter filter = baseBindings.getBloomFilter(bindingPosition);
      while (true) {
        Row row = input.next();
        if (row == null) {
          return row;
        }
        if (filter.maybePresent(hashProjectedRow(row))) {
          if (ExecutionBase.LOG_EXECUTION) {
            LOG.debug("Select_BloomFilter: candidate {}", row);
          }
          return row;
        }
      }
    }

    private int hashProjectedRow(Row row) {
      int hash = 0;
      for (int f = 0; f < fieldEvals.size(); f++) {
        TEvaluatableExpression fieldEval = fieldEvals.get(f);
        hash = hash ^ expressionAdapter.hash(storeAdapter, fieldEval, row, collator(f));
      }
      return hash;
    }
  }

  // If any context at our depth has a non-empty rowset from
  // onPositive, it passed, so let it through.
  private static class RecoverRowsCursor extends Map_NestedLoops.CollapseBindingsCursor {
    private final int bindingPosition;

    public RecoverRowsCursor(QueryContext context, Cursor input, int bindingPosition, int depth) {
      super(context, input, depth);
      this.bindingPosition = bindingPosition;
    }

    @Override
    public Row next() {
      if (TAP_NEXT_ENABLED) {
        TAP_NEXT.in();
      }
      try {
        if (CURSOR_LIFECYCLE_ENABLED) {
          CursorLifecycle.checkIdleOrActive(this);
        }
        checkQueryCancelation();
        Row row = null;
        while (true) {
          QueryBindings bindings = input.nextBindings();
          if (bindings == null) {
            openBindings = null;
            break;
          }
          if (bindings.getDepth() < depth) {
            pendingBindings = bindings;
            openBindings = null;
            break;
          }
          assert (bindings.getDepth() == depth);
          input.open();
          inputOpenBindings = bindings;
          row = input.next();
          input.close();
          inputOpenBindings = null;
          if (row != null) {
            row = bindings.getRow(bindingPosition);
            break;
          }
        }
        if (LOG_EXECUTION) {
          LOG.debug("Select_BloomFilter: yield {}", row);
        }
        return row;
      } finally {
        if (TAP_NEXT_ENABLED) {
          TAP_NEXT.out();
        }
      }
    }
  }
}