/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.store; import com.foundationdb.ais.model.AkibanInformationSchema; import com.foundationdb.ais.model.CacheValueGenerator; import com.foundationdb.ais.model.Column; import com.foundationdb.ais.model.ForeignKey; import com.foundationdb.ais.model.Group; import com.foundationdb.ais.model.Index; import com.foundationdb.ais.model.JoinColumn; import com.foundationdb.ais.model.Table; import com.foundationdb.ais.model.TableName; import com.foundationdb.qp.expression.IndexBound; import com.foundationdb.qp.expression.IndexKeyRange; import com.foundationdb.qp.expression.RowBasedUnboundExpressions; import com.foundationdb.qp.expression.UnboundExpressions; import com.foundationdb.qp.operator.API; import com.foundationdb.qp.operator.Cursor; import com.foundationdb.qp.operator.Operator; import com.foundationdb.qp.operator.QueryBindings; import com.foundationdb.qp.operator.QueryContext; import com.foundationdb.qp.operator.SimpleQueryContext; import com.foundationdb.qp.operator.UpdateFunction; import com.foundationdb.qp.row.OverlayingRow; import com.foundationdb.qp.row.Row; import com.foundationdb.qp.rowtype.IndexRowType; import com.foundationdb.qp.rowtype.Schema; import com.foundationdb.qp.rowtype.TableRowType; import com.foundationdb.qp.util.SchemaCache; import com.foundationdb.server.PersistitKeyValueTarget; import com.foundationdb.server.api.dml.ColumnSelector; import com.foundationdb.server.error.ForeignKeyReferencedViolationException; import com.foundationdb.server.error.ForeignKeyReferencingViolationException; import com.foundationdb.server.error.NotNullViolationException; import com.foundationdb.server.explain.Attributes; import com.foundationdb.server.explain.CompoundExplainer; import com.foundationdb.server.explain.ExplainContext; import com.foundationdb.server.explain.Explainer; import com.foundationdb.server.explain.Label; import com.foundationdb.server.explain.PrimitiveExplainer; import com.foundationdb.server.explain.Type; import com.foundationdb.server.explain.format.DefaultFormatter; import com.foundationdb.server.types.TKeyComparable; import com.foundationdb.server.types.service.TypesRegistryService; import com.foundationdb.server.service.ServiceManager; import com.foundationdb.server.service.config.ConfigurationService; import com.foundationdb.server.service.config.PropertyNotDefinedException; import com.foundationdb.server.service.session.Session; import com.foundationdb.server.types.TClass; import com.foundationdb.server.types.TInstance; import com.foundationdb.server.types.TPreptimeValue; import com.foundationdb.server.types.aksql.aktypes.AkBool; import com.foundationdb.server.types.texpressions.TPreparedExpression; import com.foundationdb.server.types.texpressions.TPreparedField; import com.foundationdb.server.types.texpressions.TPreparedFunction; import com.foundationdb.server.types.texpressions.TPreparedParameter; import com.foundationdb.server.types.texpressions.TValidatedScalar; import com.foundationdb.server.types.value.ValueSource; import com.foundationdb.server.types.value.ValueSources; import com.foundationdb.util.AkibanAppender; import com.foundationdb.util.Strings; import com.persistit.Key; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Handle constraint (foreign key only at present) implications of * basic <code>Store</code> operations. */ public abstract class ConstraintHandler<SType extends AbstractStore,SDType,SSDType> implements CacheValueGenerator<Map<Table,ConstraintHandler.Handler>> { private static final Logger LOG = LoggerFactory.getLogger(ConstraintHandler.class); protected final SType store; protected final int groupLookupPipelineQuantum; protected final TypesRegistryService typesRegistryService; protected final ServiceManager serviceManager; protected ConstraintHandler(SType store, ConfigurationService config, TypesRegistryService typesRegistryService, ServiceManager serviceManager) { this.store = store; int quantum; try { quantum = Integer.parseInt(config.getProperty("fdbsql.pipeline.groupLookup.lookaheadQuantum")); } catch (PropertyNotDefinedException ex) { quantum = 1; } this.groupLookupPipelineQuantum = quantum; this.typesRegistryService = typesRegistryService; this.serviceManager = serviceManager; } public void handleInsert(Session session, Table table, Row row) { Handler th = getTableHandler(table); if (th != null) { th.handleInsert(session, row); } } public void handleUpdatePre(Session session, Table table, Row oldRow, Row newRow) { Handler th = getTableHandler(table); if (th != null) { th.handleUpdatePre(session, oldRow, newRow); } } public void handleUpdatePost(Session session, Table table, Row oldRow, Row newRow) { Handler th = getTableHandler(table); if (th != null) { th.handleUpdatePost(session, oldRow, newRow); } } public void handleDelete(Session session, Table table, Row row) { Handler th = getTableHandler(table); if (th != null) { th.handleDelete(session, row); } } public void handleTruncate(Session session, Table table) { Handler th = getTableHandler(table); if (th != null) { th.handleTruncate(session); } } protected Handler getTableHandler(Table table) { Collection<ForeignKey> fkeys = table.getForeignKeys(); Map<Table,Handler> handlers = table.getAIS().getCachedValue(this, this); synchronized (handlers) { Handler handler = handlers.get(table); if (handler == null) { handler = createHandler(table, fkeys); handlers.put(table, handler); } return handler; } } @Override public Map<Table,Handler> valueFor(AkibanInformationSchema ais) { return new HashMap<>(); } protected Handler createHandler(Table table, Collection<ForeignKey> fkeys) { ArrayList<Handler> handlers = new ArrayList<>(fkeys.size() + 1); if (!table.notNull().isEmpty()) { handlers.add(new NotNullHandler(table)); } for (ForeignKey fkey : fkeys) { handlers.add(new ForeignKeyHandler(fkey, table)); } switch (handlers.size()) { case 0: return null; case 1: return handlers.get(0); default: handlers.trimToSize(); return new CompoundHandler(handlers); } } protected interface Handler { public void handleInsert(Session session, Row row); public void handleUpdatePre(Session session, Row oldRow, Row newRow); public void handleUpdatePost(Session session, Row oldRow, Row newRow); public void handleDelete(Session session, Row row); public void handleTruncate(Session session); } protected class CompoundHandler implements Handler { protected final Collection<Handler> handlers; public CompoundHandler(Collection<Handler> handlers) { this.handlers = handlers; } @Override public void handleInsert (Session session, Row row) { for (Handler handler : handlers) { handler.handleInsert(session, row); } } @Override public void handleUpdatePre(Session session, Row oldRow, Row newRow) { for (Handler handler : handlers) { handler.handleUpdatePre(session, oldRow, newRow); } } @Override public void handleUpdatePost(Session session, Row oldRow, Row newRow) { for (Handler handler : handlers) { handler.handleUpdatePost(session, oldRow, newRow); } } @Override public void handleDelete(Session session, Row row){ for (Handler handler : handlers) { handler.handleDelete(session, row); } } @Override public void handleTruncate(Session session) { for (Handler handler : handlers) { handler.handleTruncate(session); } } } protected static List<Column> crossReferenceColumns(ForeignKey fkey, boolean referencing) { List <Column> rowColumns, indexColumns; Index index; if (referencing) { rowColumns = fkey.getReferencingColumns(); indexColumns = fkey.getReferencedColumns(); index = fkey.getReferencedIndex(); } else { rowColumns = fkey.getReferencedColumns(); indexColumns = fkey.getReferencingColumns(); index = fkey.getReferencingIndex(); } int ncols = rowColumns.size(); if (ncols <= 1) { return rowColumns; } List<Column> result = new ArrayList<>(ncols); for (int i = 0; i < ncols; i++) { Column keyColumn = index.getKeyColumns().get(i).getColumn(); result.add(rowColumns.get(indexColumns.indexOf(keyColumn))); } return result; } protected static class NotNullHandler implements Handler { private final Table table; private final BitSet notNull; public NotNullHandler(Table table) { this.table = table; this.notNull = table.notNull(); } @Override public void handleInsert(Session session, Row row) { checkNotNull(row); } @Override public void handleUpdatePre(Session session, Row oldRow, Row newRow) { checkNotNull(newRow); } @Override public void handleUpdatePost(Session session, Row oldRow, Row newRow) { // Checked in pre } @Override public void handleDelete(Session session, Row row) { //None } @Override public void handleTruncate(Session session) { // None } private void checkNotNull(Row row) { for (int f = notNull.nextSetBit(0); f >= 0; f = notNull.nextSetBit(f+1)) { if (row.value(f).isNull()) { throw new NotNullViolationException(table.getName().getSchemaName(), table.getName().getTableName(), table.getColumn(f).getName()); } } } } protected class ForeignKeyHandler implements Handler { protected final ForeignKey foreignKey; protected final boolean referencing, referenced; // referencingColumns in order of referencedIndex. protected final List<Column> crossReferencingColumns; // referencedColumns in order of referencingIndex. protected final List<Column> crossReferencedColumns; protected Plan updatePlan, deletePlan, truncatePlan; public ForeignKeyHandler(ForeignKey foreignKey, Table forTable) { this.foreignKey = foreignKey; this.referencing = (foreignKey.getReferencingTable() == forTable); this.referenced = (foreignKey.getReferencedTable() == forTable); this.crossReferencingColumns = (referencing) ? crossReferenceColumns(foreignKey, true) : null; this.crossReferencedColumns = (referenced) ? crossReferenceColumns(foreignKey, false) : null; } @Override public void handleInsert(Session session, Row row) { if (referencing) { checkReferencing(session, row, foreignKey, crossReferencingColumns, "insert into"); } } @Override public void handleUpdatePre(Session session, Row oldRow, Row newRow){ if (referencing && anyColumnChanged(session, oldRow, newRow, foreignKey.getReferencingColumns())) { checkReferencing(session, newRow, foreignKey, crossReferencingColumns, "update"); } if (referenced && anyColumnChanged(session, oldRow, newRow, foreignKey.getReferencedColumns())) { switch (foreignKey.getUpdateAction()) { case NO_ACTION: case RESTRICT: checkNotReferenced(session, oldRow, foreignKey, crossReferencedColumns, foreignKey.getUpdateAction(), "update"); break; case CASCADE: // This needs to refer to the after image of the row, so it needs // to be done in handleUpdatePost break; default: runOperatorPlan(getUpdatePlan(), session, oldRow, newRow); } } } @Override public void handleUpdatePost (Session session, Row oldRow, Row newRow) { if(referenced && (foreignKey.getUpdateAction() == ForeignKey.Action.CASCADE) && anyColumnChanged(session, oldRow, newRow, foreignKey.getReferencedColumns())) { runOperatorPlan(getUpdatePlan(), session, oldRow, newRow); } } @Override public void handleDelete(Session session, Row row) { if (referenced) { switch (foreignKey.getDeleteAction()) { case NO_ACTION: case RESTRICT: checkNotReferenced(session, row, foreignKey, crossReferencedColumns, foreignKey.getDeleteAction(), "delete from"); break; default: runOperatorPlan(getDeletePlan(), session, row, null); } } } @Override public void handleTruncate(Session session) { if (referenced) { if (referencing) { // Self-join no problem when whole table truncated. return; } switch (foreignKey.getDeleteAction()) { case NO_ACTION: case RESTRICT: checkNotReferenced(session, (Row)null, foreignKey, crossReferencedColumns, foreignKey.getDeleteAction(), "truncate"); break; default: runOperatorPlan(getTruncatePlan(), session, (Row)null, (Row)null); } } } protected synchronized Plan getUpdatePlan() { if (updatePlan == null) { updatePlan = buildPlan(foreignKey, crossReferencedColumns, true, true); } return updatePlan; } protected synchronized Plan getDeletePlan() { if (deletePlan == null) { deletePlan = buildPlan(foreignKey, crossReferencedColumns, true, false); } return deletePlan; } protected synchronized Plan getTruncatePlan() { if (truncatePlan == null) { truncatePlan = buildPlan(foreignKey, crossReferencedColumns, false, false); } return truncatePlan; } } protected boolean anyColumnChanged(Session session, Row oldRow, Row newRow, List<Column> columns) { for (Column column: columns) { int i = column.getPosition().intValue(); if (!TClass.areEqual(oldRow.value(i), newRow.value(i))) { return true; } } return false; } @SuppressWarnings("unchecked") protected void checkReferencing (Session session, Row row, ForeignKey foreignKey, List<Column> columns, String operation) { if (!compareSelfReference(row, foreignKey)) { Index index = foreignKey.getReferencedIndex(); SDType storeData = (SDType)store.createStoreData(session, index); Key key = store.getKey(session, storeData); try { boolean anyNull = crossReferenceKey(session, key, row, columns); if (!anyNull) { assert index.isUnique(); checkReferencing(session, index, storeData, row, foreignKey, operation); } } finally { store.releaseStoreData(session, storeData); } } } protected boolean compareSelfReference (Row row, ForeignKey foreignKey) { boolean selfReference = false; if (row == null) { return selfReference; } if (foreignKey.getReferencedTable() == foreignKey.getReferencingTable()) { selfReference = true; for (JoinColumn join : foreignKey.getJoinColumns()) { ValueSource parent = row.value(join.getParent().getColumn().getPosition().intValue()); ValueSource child = row.value(join.getChild().getColumn().getPosition().intValue()); TInstance pInst = parent.getType(); TInstance cInst = child.getType(); TKeyComparable comparable = typesRegistryService.getKeyComparable(pInst.typeClass(), cInst.typeClass()); int c = (comparable != null) ? comparable.getComparison().compare(pInst, parent, cInst, child) : TClass.compare(pInst, parent, cInst, child); selfReference &= (c == 0); } } return selfReference; } protected abstract void checkReferencing(Session session, Index index, SDType storeData, Row row, ForeignKey foreignKey, String operation); protected void notReferencing(Session session, Index index, SDType storeData, Row row, ForeignKey foreignKey, String operation) { String key = formatKey(session, row, foreignKey.getReferencingColumns()); throw new ForeignKeyReferencingViolationException(operation, foreignKey.getReferencingTable().getName(), key, foreignKey.getConstraintName().getTableName(), foreignKey.getReferencedTable().getName()); } @SuppressWarnings("unchecked") protected void checkNotReferenced(Session session, Row row, ForeignKey foreignKey, List<Column> columns, ForeignKey.Action action, String operation) { Index index = foreignKey.getReferencingIndex(); SDType storeData = (SDType)store.createStoreData(session, index); Key key = store.getKey(session, storeData); try { boolean anyNull = crossReferenceKey(session, key, row, columns); if (!anyNull) { checkNotReferenced(session, index, storeData, row, foreignKey, compareSelfReference(row, foreignKey), action, operation); } } finally { store.releaseStoreData(session, storeData); } } protected abstract void checkNotReferenced(Session session, Index index, SDType storeData, Row row, ForeignKey foreignKey, boolean selfReference, ForeignKey.Action action, String operation); @SuppressWarnings("unchecked") protected void stillReferenced(Session session, Index index, SDType storeData, Row row, ForeignKey foreignKey, String operation) { String key; if (row == null) { Key foundKey = store.getKey(session, storeData); key = formatKey(session, index, foundKey, foreignKey.getReferencedColumns(), foreignKey.getReferencingColumns()); } else { key = formatKey(session, row, foreignKey.getReferencedColumns()); } throw new ForeignKeyReferencedViolationException(operation, foreignKey.getReferencedTable().getName(), key, foreignKey.getConstraintName().getTableName(), foreignKey.getReferencingTable().getName()); } protected static boolean crossReferenceKey(Session session, Key key, Row row, List<Column> columns) { key.clear(); if (row == null) { // This is the truncate case, find all non-null referencing index entries. key.append(null); return false; } boolean anyNull = false; PersistitKeyValueTarget target = new PersistitKeyValueTarget(ConstraintHandler.class.getSimpleName()); target.attach(key); for (Column column : columns) { ValueSource source = row.value(column.getPosition().intValue()); if (source.isNull()) { target.putNull(); anyNull = true; } else { source.getType().writeCanonical(source, target); } } return anyNull; } public static boolean keyHasNullSegments(Key key, Index index) { key.reset(); for (int i = 0; i < index.getKeyColumns().size(); i++) { if (key.isNull()) { return true; } else { key.decode(); } } return false; } public static String formatKey(Session session, Row row, List<Column> columns) { StringBuilder str = new StringBuilder(); AkibanAppender appender = AkibanAppender.of(str); for (int i = 0; i < columns.size(); i++) { if (i > 0) { str.append(" and "); } Column column = columns.get(i); str.append(column.getName()).append(" = "); ValueSource source = row.value(column.getPosition()); source.getType().format(source, appender); } return str.toString(); } public static String formatKey(Session session, Index index, Key key, List<Column> reportColumns, List<Column> indexColumns) { key.reset(); Object[] reportColumnValues = new Object[reportColumns.size()]; for (int i = 0; i < index.getKeyColumns().size(); i++) { int idx = indexColumns.indexOf(index.getKeyColumns().get(i).getColumn()); reportColumnValues[idx] = key.decode(); } StringBuilder str = new StringBuilder(); for(int i = 0; i < reportColumns.size(); ++i) { if (str.length() > 0) { str.append(" and "); } str.append(reportColumns.get(i).getName()); str.append(" = "); str.append(reportColumnValues[i]); } return str.toString(); } protected static class Plan implements ColumnSelector, UpdateFunction { Schema schema; Operator operator; int ncols; int[] referencedColumns; boolean bindOldRow; boolean bindNewRow; ValueSource[] bindValues; int[] updatePositions; /* ColumnSelector */ @Override public boolean includesColumn(int columnPosition) { return (columnPosition < ncols); } /* UpdateFunction */ @Override public Row evaluate(Row original, QueryContext context, QueryBindings bindings) { OverlayingRow overlay = new OverlayingRow(original); for (int i = 0; i < ncols; i++) { overlay.overlay(updatePositions[i], // new values come after old keys. bindings.getValue(ncols + i)); } return overlay; } } protected Plan buildPlan(ForeignKey foreignKey, List<Column> crossReferencedColumns, boolean hasOldRow, boolean hasNewRow) { Plan plan = new Plan(); plan.ncols = crossReferencedColumns.size(); Group group = foreignKey.getReferencingTable().getGroup(); plan.schema = SchemaCache.globalSchema(group.getAIS()); TableRowType tableRowType = plan.schema.tableRowType(foreignKey.getReferencingTable()); Operator input; if (hasOldRow) { // referencing WHERE fk = $1 AND... plan.bindOldRow = true; List<Column> referencedColumns = foreignKey.getReferencedColumns(); plan.referencedColumns = new int[plan.ncols]; for (int i = 0; i < plan.ncols; i++) { plan.referencedColumns[i] = referencedColumns.get(i).getPosition().intValue(); } Index index = foreignKey.getReferencingIndex(); IndexRowType indexRowType = plan.schema.indexRowType(index); List<TPreparedExpression> vars = new ArrayList<>(plan.ncols); for (int i = 0; i < plan.ncols; i++) { // Convert from index column position to parameter number. Column indexedColumn = crossReferencedColumns.get(i); int fkpos = referencedColumns.indexOf(indexedColumn); vars.add(new TPreparedParameter(fkpos, indexedColumn.getType())); } UnboundExpressions indexExprs = new RowBasedUnboundExpressions(indexRowType, vars); IndexBound indexBound = new IndexBound(indexExprs, plan); IndexKeyRange indexKeyRange = IndexKeyRange.bounded(indexRowType, indexBound, true, indexBound, true); input = API.indexScan_Default(indexRowType, indexKeyRange, 1); input = API.groupLookup_Default(input, group, indexRowType, Collections.singletonList(tableRowType), API.InputPreservationOption.DISCARD_INPUT, groupLookupPipelineQuantum); } else { // referencing WHERE fk IS NOT NULL AND... List<Column> referencingColumns = foreignKey.getReferencingColumns(); TPreptimeValue emptyTPV = new TPreptimeValue(); TValidatedScalar isNull = typesRegistryService.getScalarsResolver().get("IsNull", Collections.nCopies(1, emptyTPV)).getOverload(); TValidatedScalar not = typesRegistryService.getScalarsResolver().get("NOT", Collections.nCopies(1, emptyTPV)).getOverload(); TValidatedScalar and = typesRegistryService.getScalarsResolver().get("AND", Collections.nCopies(2, emptyTPV)).getOverload(); TInstance boolType = AkBool.INSTANCE.instance(false); TPreparedExpression predicate = null; for (int i = 0; i < plan.ncols; i++) { Column referencingColumn = referencingColumns.get(i); TPreparedField field = new TPreparedField(referencingColumn.getType(), referencingColumn.getPosition()); TPreparedExpression clause = new TPreparedFunction(isNull, boolType, Arrays.asList(field)); clause = new TPreparedFunction(not, boolType, Arrays.asList(clause)); if (predicate == null) { predicate = clause; } else { predicate = new TPreparedFunction(and, boolType, Arrays.asList(predicate, clause)); } } input = API.groupScan_Default(group); input = API.filter_Default(input, Collections.singletonList(tableRowType)); input = API.select_HKeyOrdered(input, tableRowType, predicate); } ForeignKey.Action action; takeAction: { if (hasNewRow) { action = foreignKey.getUpdateAction(); } else { action = foreignKey.getDeleteAction(); if (action == ForeignKey.Action.CASCADE) { // DELETE FROM referencing ... plan.operator = API.delete_Returning(input, false); break takeAction; } } // UPDATE referencing SET fk = $2 ... switch (action) { case SET_NULL: case SET_DEFAULT: plan.bindValues = new ValueSource[plan.ncols]; for (int i = 0; i < plan.ncols; i++) { Column column = foreignKey.getReferencingColumns().get(i); plan.bindValues[i] = ValueSources.fromObject((action == ForeignKey.Action.SET_NULL) ? null : column.getDefaultValue(), column.getType()).value(); } break; case CASCADE: plan.bindNewRow = true; break; default: assert false : action; } if (hasOldRow) { // Halloween vulnerability input = API.buffer_Default(input, tableRowType); } plan.updatePositions = new int[plan.ncols]; for (int i = 0; i < plan.ncols; i++) { plan.updatePositions[i] = foreignKey.getReferencingColumns().get(i).getPosition(); } plan.operator = API.update_Returning(input, plan); } if (LOG.isDebugEnabled()) { ExplainContext context = new ExplainContext(); Attributes atts = new Attributes(); TableName tableName = foreignKey.getReferencingTable().getName(); atts.put(Label.TABLE_SCHEMA, PrimitiveExplainer.getInstance(tableName.getSchemaName())); atts.put(Label.TABLE_NAME, PrimitiveExplainer.getInstance(tableName.getTableName())); for (int i = 0; i < plan.ncols; i++) { atts.put(Label.COLUMN_NAME, PrimitiveExplainer.getInstance(foreignKey.getReferencingColumns().get(i).getName())); CompoundExplainer var = new CompoundExplainer(Type.VARIABLE); var.addAttribute(Label.BINDING_POSITION, PrimitiveExplainer.getInstance(plan.ncols + i)); atts.put(Label.EXPRESSIONS, var); } context.putExtraInfo(plan.operator, new CompoundExplainer(Type.EXTRA_INFO, atts)); Explainer explainer = plan.operator.getExplainer(context); LOG.debug("Plan for " + foreignKey.getConstraintName().getTableName() + ":\n" + Strings.join(new DefaultFormatter(tableName.getSchemaName()).format(explainer))); } return plan; } protected void runOperatorPlan (Plan plan, Session session, Row oldRow, Row newRow) { QueryContext context = new SimpleQueryContext(store.createAdapter(session), serviceManager); QueryBindings bindings = context.createBindings(); if (plan.bindOldRow) { for (int i = 0; i < plan.ncols; i++) { bindings.setValue(i, oldRow.value(plan.referencedColumns[i])); } } if (plan.bindNewRow) { for (int i = 0; i < plan.ncols; i++) { bindings.setValue(plan.referencedColumns.length + i, newRow.value(plan.referencedColumns[i])); } } else if (plan.bindValues != null) { for (int i = 0; i < plan.ncols; i++) { bindings.setValue(plan.bindValues.length + i, plan.bindValues[i]); } } Cursor cursor = API.cursor(plan.operator, context, bindings); cursor.openTopLevel(); try { Row row; do { row = cursor.next(); } while(row != null); } finally { cursor.closeTopLevel(); } } }