<author email="javajedi@users.sf.net">Tim McCune</author>
</properties>
<body>
+ <release version="2.1.7" date="TBD">
+ <action dev="jahlborn" type="update">
+ Implement support for partial index lookups. Efficient IndexCursor
+ lookups can now be done with multi-column indexes using only some of
+ the columns in the index.
+ </action>
+ </release>
<release version="2.1.6" date="2016-11-29">
<action dev="jahlborn" type="update" system="SourceForge2Features"
issue="34">
import java.util.List;
import java.util.Map;
-import com.healthmarketscience.jackcess.impl.TableImpl;
-import com.healthmarketscience.jackcess.impl.IndexImpl;
import com.healthmarketscience.jackcess.impl.CursorImpl;
import com.healthmarketscience.jackcess.impl.IndexCursorImpl;
+import com.healthmarketscience.jackcess.impl.IndexData;
+import com.healthmarketscience.jackcess.impl.IndexImpl;
+import com.healthmarketscience.jackcess.impl.TableImpl;
import com.healthmarketscience.jackcess.util.ColumnMatcher;
* Searches for an index with the given column names.
*/
private CursorBuilder setIndexByColumns(List<String> searchColumns) {
- IndexImpl index = _table.findIndexForColumns(searchColumns, false);
+ IndexImpl index = _table.findIndexForColumns(
+ searchColumns, TableImpl.IndexFeature.ANY_MATCH);
if(index == null) {
throw new IllegalArgumentException("Index with columns " +
searchColumns +
*/
public CursorBuilder setStartEntry(Object... startEntry) {
if(startEntry != null) {
- setStartRow(_index.constructIndexRowFromEntry(startEntry));
+ setStartRow(_index.constructPartialIndexRowFromEntry(
+ IndexData.MIN_VALUE, startEntry));
}
return this;
}
*/
public CursorBuilder setEndEntry(Object... endEntry) {
if(endEntry != null) {
- setEndRow(_index.constructIndexRowFromEntry(endEntry));
+ setEndRow(_index.constructPartialIndexRowFromEntry(
+ IndexData.MAX_VALUE, endEntry));
}
return this;
}
public boolean isForeignKey();
+ /**
+ * @usage _general_method_
+ */
+ public int getColumnCount();
+
/**
* @return the Columns for this index (unmodifiable)
*/
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
return false;
}
- // either we found a row with the given value, or none exist in the
- // table
+ // either we found a row with the given value, or none exist in the table
return currentRowMatchesImpl(columnPattern, valuePattern, columnMatcher);
}
return false;
}
- // find actual matching row
- IndexData indexData = _entryCursor.getIndexData();
- Map<String,?> indexRowPattern = null;
- if(rowPattern.size() == indexData.getColumns().size()) {
- // the rowPattern matches our index columns exactly, so we can
- // streamline our testing below
- indexRowPattern = rowPattern;
- } else {
- // the rowPattern has more columns than just the index, so we need to
- // do more work when testing below
- Map<String,Object> tmpRowPattern = new LinkedHashMap<String,Object>();
- indexRowPattern = tmpRowPattern;
- for(IndexData.ColumnDescriptor idxCol : indexData.getColumns()) {
- tmpRowPattern.put(idxCol.getName(), rowValues[idxCol.getColumnIndex()]);
- }
- }
-
- // there may be multiple columns which fit the pattern subset used by
+ // determine if the pattern columns exactly match the index columns
+ boolean exactColumnMatch = rowPattern.keySet().equals(
+ getIndexEntryPattern());
+
+ // there may be multiple rows which fit the pattern subset used by
// the index, so we need to keep checking until our index values no
// longer match
do {
- if(!currentRowMatchesImpl(indexRowPattern, columnMatcher)) {
+ if(!currentRowMatchesEntryImpl(rowValues, columnMatcher)) {
// there are no more rows which could possibly match
break;
}
- // note, if rowPattern == indexRowPattern, no need to do an extra
- // comparison with the current row
- if((rowPattern == indexRowPattern) ||
- currentRowMatchesImpl(rowPattern, columnMatcher)) {
+ // note, if exactColumnMatch, no need to do an extra comparison with the
+ // current row (since the entry match check above is equivalent to this
+ // check)
+ if(exactColumnMatch || currentRowMatchesImpl(rowPattern, columnMatcher)) {
// found it!
return true;
}
Row row = getCurrentRow(getIndexEntryPattern());
for(IndexData.ColumnDescriptor col : getIndex().getColumns()) {
- String columnName = col.getName();
+
Object patValue = rowValues[col.getColumnIndex()];
+
+ if((patValue == IndexData.MIN_VALUE) ||
+ (patValue == IndexData.MAX_VALUE)) {
+ // all remaining entry values are "special" (used for partial lookups)
+ return true;
+ }
+
+ String columnName = col.getName();
Object rowValue = row.get(columnName);
if(!columnMatcher.matches(getTable(), columnName, patValue, rowValue)) {
return false;
protected Object prepareSearchInfo(ColumnImpl columnPattern, Object valuePattern)
{
// attempt to generate a lookup row for this index
- return _entryCursor.getIndexData().constructIndexRow(
- columnPattern.getName(), valuePattern);
+ return _entryCursor.getIndexData().constructPartialIndexRow(
+ IndexData.MIN_VALUE, columnPattern.getName(), valuePattern);
}
@Override
protected Object prepareSearchInfo(Map<String,?> rowPattern)
{
// attempt to generate a lookup row for this index
- return _entryCursor.getIndexData().constructIndexRow(rowPattern);
+ return _entryCursor.getIndexData().constructPartialIndexRow(
+ IndexData.MIN_VALUE, rowPattern);
}
@Override
private Object[] toRowValues(Object[] entryValues)
{
- return _entryCursor.getIndexData().constructIndexRowFromEntry(entryValues);
+ return _entryCursor.getIndexData().constructPartialIndexRowFromEntry(
+ IndexData.MIN_VALUE, entryValues);
}
@Override
}
}
-
}
return Collections.unmodifiableList(_columns);
}
+ public int getColumnCount() {
+ return _columns.size();
+ }
+
/**
* Whether or not the complete index state has been read.
*/
if(idx < 0) {
// the caller may have only read some of the row data, if this is the
// case, just search for the page/row numbers
- // FIXME, we could force caller to get relevant values?
+ // TODO, we could force caller to get relevant values?
EntryCursor cursor = cursor();
Position tmpPos = null;
Position endPos = cursor._lastPos;
}
return idxRow;
}
+
+ /**
+ * Constructs an array of values appropriate for this index from the given
+ * column values, possibly only providing a prefix subset of the index
+ * columns (at least one value must be provided). If a prefix entry is
+ * provided, any missing, trailing index entry values will use the given
+ * filler value.
+ * @return the appropriate sparse array of data
+ * @throws IllegalArgumentException if at least one value is not provided
+ */
+ public Object[] constructPartialIndexRowFromEntry(
+ Object filler, Object... values)
+ {
+ if(values.length == 0) {
+ throw new IllegalArgumentException(withErrorContext(
+ "At least one column value must be provided"));
+ }
+ if(values.length > _columns.size()) {
+ throw new IllegalArgumentException(withErrorContext(
+ "Too many column values given " + values.length +
+ ", expected at most " + _columns.size()));
+ }
+ int valIdx = 0;
+ Object[] idxRow = new Object[getTable().getColumnCount()];
+ for(ColumnDescriptor col : _columns) {
+ idxRow[col.getColumnIndex()] =
+ ((valIdx < values.length) ? values[valIdx] : filler);
+ ++valIdx;
+ }
+ return idxRow;
+ }
/**
* Constructs an array of values appropriate for this index from the given
return constructIndexRow(Collections.singletonMap(colName, value));
}
+ /**
+ * Constructs an array of values appropriate for this index from the given
+ * column value, which must be the first column of the index. Any missing,
+ * trailing index entry values will use the given filler value.
+ * @return the appropriate sparse array of data or {@code null} if no prefix
+ * list of columns for this index were provided
+ */
+ public Object[] constructPartialIndexRow(Object filler, String colName, Object value)
+ {
+ return constructPartialIndexRow(filler, Collections.singletonMap(colName, value));
+ }
+
/**
* Constructs an array of values appropriate for this index from the given
* column values.
return idxRow;
}
+ /**
+ * Constructs an array of values appropriate for this index from the given
+ * column values, possibly only using a subset of the given values. A
+ * partial row can be created if one or more prefix column values are
+ * provided. If a prefix can be found, any missing, trailing index entry
+ * values will use the given filler value.
+ * @return the appropriate sparse array of data or {@code null} if no prefix
+ * list of columns for this index were provided
+ */
+ public Object[] constructPartialIndexRow(Object filler, Map<String,?> row)
+ {
+ // see if we have at least one prefix column
+ int numCols = 0;
+ for(ColumnDescriptor col : _columns) {
+ if(!row.containsKey(col.getName())) {
+ if(numCols == 0) {
+ // can't do it, need at least first column
+ return null;
+ }
+ break;
+ }
+ ++numCols;
+ }
+
+ // fill in the row with either the prefix values or the filler value, as
+ // appropriate
+ Object[] idxRow = new Object[getTable().getColumnCount()];
+ int valIdx = 0;
+ for(ColumnDescriptor col : _columns) {
+ idxRow[col.getColumnIndex()] =
+ ((valIdx < numCols) ? row.get(col.getName()) : filler);
+ ++valIdx;
+ }
+ return idxRow;
+ }
+
@Override
public String toString() {
ToStringBuilder sb = CustomToStringStyle.builder(this)
_entryBuffer.reset();
for(ColumnDescriptor col : _columns) {
+
Object value = values[col.getColumnIndex()];
if(ColumnImpl.isRawData(value)) {
// ignore it, we could not parse it
}
if(value == MIN_VALUE) {
- // null is the "least" value
- _entryBuffer.write(getNullEntryFlag(col.isAscending()));
+ // null is the "least" value (note the column "ascending" flag is
+ // irrelevant here because the entry bytes are _always_ interpreted
+ // least to greatest)
+ _entryBuffer.write(getNullEntryFlag(true));
continue;
}
if(value == MAX_VALUE) {
- // the opposite null is the "greatest" value
- _entryBuffer.write(getNullEntryFlag(!col.isAscending()));
+ // the opposite null is the "greatest" value (note the column
+ // "ascending" flag is irrelevant here because the entry bytes are
+ // _always_ interpreted least to greatest)
+ _entryBuffer.write(getNullEntryFlag(false));
continue;
}
return getIndexData().getColumns();
}
+ public int getColumnCount() {
+ return getIndexData().getColumnCount();
+ }
+
public CursorBuilder newCursor() {
return getTable().newCursor().setIndex(this);
}
return getIndexData().constructIndexRowFromEntry(values);
}
+ /**
+ * Constructs an array of values appropriate for this index from the given
+ * column values, possibly only providing a prefix subset of the index
+ * columns (at least one value must be provided). If a prefix entry is
+ * provided, any missing, trailing index entry values will use the given
+ * filler value.
+ * @return the appropriate sparse array of data
+ * @throws IllegalArgumentException if at least one value is not provided
+ */
+ public Object[] constructPartialIndexRowFromEntry(
+ Object filler, Object... values)
+ {
+ return getIndexData().constructPartialIndexRowFromEntry(filler, values);
+ }
+
/**
* Constructs an array of values appropriate for this index from the given
* column value.
return constructIndexRow(Collections.singletonMap(colName, value));
}
+ /**
+ * Constructs an array of values appropriate for this index from the given
+ * column value, which must be the first column of the index. Any missing,
+ * trailing index entry values will use the given filler value.
+ * @return the appropriate sparse array of data or {@code null} if no prefix
+ * list of columns for this index were provided
+ */
+ public Object[] constructPartialIndexRow(Object filler, String colName, Object value)
+ {
+ return constructPartialIndexRow(filler, Collections.singletonMap(colName, value));
+ }
+
/**
* Constructs an array of values appropriate for this index from the given
* column values.
return getIndexData().constructIndexRow(row);
}
+ /**
+ * Constructs an array of values appropriate for this index from the given
+ * column values, possibly only using a subset of the given values. A
+ * partial row can be created if one or more prefix column values are
+ * provided. If a prefix can be found, any missing, trailing index entry
+ * values will use the given filler value.
+ * @return the appropriate sparse array of data or {@code null} if no prefix
+ * list of columns for this index were provided
+ */
+ public Object[] constructPartialIndexRow(Object filler, Map<String,?> row)
+ {
+ return getIndexData().constructPartialIndexRow(filler, row);
+ }
+
@Override
public String toString() {
ToStringBuilder sb = CustomToStringStyle.builder(this)
// for now, we will require the unique index on the primary table (just
// like access does). we could just create it auto-magically...
- IndexImpl primaryIdx = getPrimaryUniqueIndex();
+ IndexImpl primaryIdx = getUniqueIndex(_primaryTable, _primaryCols);
if(primaryIdx == null) {
throw new IllegalArgumentException(withErrorContext(
"Missing unique index on primary table required to enforce integrity"));
private boolean isOneToOne() {
// a relationship is one to one if the two sides of the relationship have
// unique indexes on the relevant columns
- if(getPrimaryUniqueIndex() == null) {
+ if(getUniqueIndex(_primaryTable, _primaryCols) == null) {
return false;
}
- IndexImpl idx = _secondaryTable.findIndexForColumns(
- getColumnNames(_secondaryCols), true);
+ IndexImpl idx = getUniqueIndex(_secondaryTable, _secondaryCols);
return (idx != null);
}
- private IndexImpl getPrimaryUniqueIndex() {
- return _primaryTable.findIndexForColumns(getColumnNames(_primaryCols), true);
+ private static IndexImpl getUniqueIndex(
+ TableImpl table, List<ColumnImpl> cols) {
+ return table.findIndexForColumns(getColumnNames(cols),
+ TableImpl.IndexFeature.EXACT_UNIQUE_ONLY);
}
private static String getTableErrorContext(
*/
public static final byte TYPE_USER = 0x4e;
+ public enum IndexFeature {
+ EXACT_MATCH, EXACT_UNIQUE_ONLY, ANY_MATCH;
+ }
+
/** comparator which sorts variable length columns based on their index into
the variable length offset table */
private static final Comparator<ColumnImpl> VAR_LEN_COLUMN_COMPARATOR =
}
public IndexImpl findIndexForColumns(Collection<String> searchColumns,
- boolean uniqueOnly) {
+ IndexFeature feature) {
+
+ IndexImpl partialIndex = null;
for(IndexImpl index : _indexes) {
Collection<? extends Index.Column> indexColumns = index.getColumns();
- if(indexColumns.size() != searchColumns.size()) {
+ if(indexColumns.size() < searchColumns.size()) {
continue;
}
+ boolean exactMatch = (indexColumns.size() == searchColumns.size());
+
Iterator<String> sIter = searchColumns.iterator();
Iterator<? extends Index.Column> iIter = indexColumns.iterator();
- boolean matches = true;
+ boolean searchMatches = true;
while(sIter.hasNext()) {
String sColName = sIter.next();
String iColName = iIter.next().getName();
if((sColName != iColName) &&
((sColName == null) || !sColName.equalsIgnoreCase(iColName))) {
- matches = false;
+ searchMatches = false;
break;
}
}
- if(matches && (!uniqueOnly || index.isUnique())) {
- return index;
+ if(searchMatches) {
+
+ if(exactMatch && ((feature != IndexFeature.EXACT_UNIQUE_ONLY) ||
+ index.isUnique())) {
+ return index;
+ }
+
+ if(!exactMatch && (feature == IndexFeature.ANY_MATCH) &&
+ ((partialIndex == null) ||
+ (indexColumns.size() < partialIndex.getColumnCount()))) {
+ // this is a better partial index match
+ partialIndex = index;
+ }
}
}
- return null;
+ return partialIndex;
}
List<ColumnImpl> getAutoNumberColumns() {
return false;
}
- if(idx1.getColumns().size() != idx2.getColumns().size()) {
+ if(idx1.getColumns().size() != idx2.getColumnCount()) {
return false;
}
import com.healthmarketscience.jackcess.impl.JetFormatTest;
import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
import com.healthmarketscience.jackcess.impl.RowIdImpl;
+import com.healthmarketscience.jackcess.impl.TableImpl;
import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher;
import com.healthmarketscience.jackcess.util.ColumnMatcher;
import com.healthmarketscience.jackcess.util.RowFilterTest;
}
}
+ public void testPartialIndexFind() throws Exception
+ {
+ for (final FileFormat fileFormat : JetFormatTest.SUPPORTED_FILEFORMATS) {
+
+ Database db = createMem(fileFormat);
+
+ TableImpl t = (TableImpl)new TableBuilder("Test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG))
+ .addColumn(new ColumnBuilder("data1", DataType.TEXT))
+ .addColumn(new ColumnBuilder("num2", DataType.LONG))
+ .addColumn(new ColumnBuilder("key3", DataType.TEXT))
+ .addColumn(new ColumnBuilder("value", DataType.TEXT))
+ .addIndex(new IndexBuilder("idx3").addColumns("data1", "num2", "key3"))
+ .toTable(db);
+
+ Index idx = t.findIndexForColumns(Arrays.asList("data1"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx3", idx.getName());
+
+ idx = t.findIndexForColumns(Arrays.asList("data1", "num2"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx3", idx.getName());
+
+ idx = t.findIndexForColumns(Arrays.asList("data1", "num2", "key3"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx3", idx.getName());
+
+ assertNull(t.findIndexForColumns(Arrays.asList("num2"),
+ TableImpl.IndexFeature.ANY_MATCH));
+ assertNull(t.findIndexForColumns(Arrays.asList("data1", "key3"),
+ TableImpl.IndexFeature.ANY_MATCH));
+ assertNull(t.findIndexForColumns(Arrays.asList("data1"),
+ TableImpl.IndexFeature.EXACT_MATCH));
+
+
+ new IndexBuilder("idx2")
+ .addColumns("data1", "num2")
+ .addToTable(t);
+
+ idx = t.findIndexForColumns(Arrays.asList("data1"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx2", idx.getName());
+
+ idx = t.findIndexForColumns(Arrays.asList("data1", "num2"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx2", idx.getName());
+
+ idx = t.findIndexForColumns(Arrays.asList("data1", "num2", "key3"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx3", idx.getName());
+
+ assertNull(t.findIndexForColumns(Arrays.asList("num2"),
+ TableImpl.IndexFeature.ANY_MATCH));
+ assertNull(t.findIndexForColumns(Arrays.asList("data1", "key3"),
+ TableImpl.IndexFeature.ANY_MATCH));
+ assertNull(t.findIndexForColumns(Arrays.asList("data1"),
+ TableImpl.IndexFeature.EXACT_MATCH));
+
+
+ new IndexBuilder("idx1")
+ .addColumns("data1")
+ .addToTable(t);
+
+ idx = t.findIndexForColumns(Arrays.asList("data1"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx1", idx.getName());
+
+ idx = t.findIndexForColumns(Arrays.asList("data1", "num2"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx2", idx.getName());
+
+ idx = t.findIndexForColumns(Arrays.asList("data1", "num2", "key3"),
+ TableImpl.IndexFeature.ANY_MATCH);
+ assertEquals("idx3", idx.getName());
+
+ assertNull(t.findIndexForColumns(Arrays.asList("num2"),
+ TableImpl.IndexFeature.ANY_MATCH));
+ assertNull(t.findIndexForColumns(Arrays.asList("data1", "key3"),
+ TableImpl.IndexFeature.ANY_MATCH));
+
+ db.close();
+ }
+ }
+
+ public void testPartialIndexLookup() throws Exception
+ {
+ for (final FileFormat fileFormat : JetFormatTest.SUPPORTED_FILEFORMATS) {
+
+ Database db = createMem(fileFormat);
+
+ TableImpl t = (TableImpl)new TableBuilder("Test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG))
+ .addColumn(new ColumnBuilder("data1", DataType.TEXT))
+ .addColumn(new ColumnBuilder("num2", DataType.LONG))
+ .addColumn(new ColumnBuilder("key3", DataType.TEXT))
+ .addColumn(new ColumnBuilder("value", DataType.TEXT))
+ .addIndex(new IndexBuilder("idx3")
+ .addColumns(true, "data1")
+ .addColumns(false, "num2")
+ .addColumns(true, "key3")
+ )
+ .toTable(db);
+
+ int id = 1;
+ for(String str : Arrays.asList("A", "B", "C", "D")) {
+ for(int i = 4; i >= 0; --i) {
+ // for(int i = 0; i < 5; ++i) {
+ for(int j = 1; j < 3; ++j) {
+ t.addRow(id, str, i, "K" + j, "value" + id);
+ ++id;
+ }
+ }
+ }
+
+ Index idx = t.getIndex("idx3");
+ doPartialIndexLookup(idx);
+
+ idx = new IndexBuilder("idx2")
+ .addColumns(true, "data1")
+ .addColumns(false, "num2")
+ .addToTable(t);
+ doPartialIndexLookup(idx);
+
+ idx = new IndexBuilder("idx1")
+ .addColumns(true, "data1")
+ .addToTable(t);
+ doPartialIndexLookup(idx);
+
+ db.close();
+ }
+ }
+
+ private static void doPartialIndexLookup(Index idx) throws Exception
+ {
+ int colCount = idx.getColumnCount();
+ IndexCursor c = new CursorBuilder(idx.getTable()).setIndex(idx).toIndexCursor();
+
+ doFindFirstByEntry(c, 21, "C");
+ doFindFirstByEntry(c, null, "Z");
+
+ if(colCount > 1) {
+ doFindFirstByEntry(c, 23, "C", 3);
+ doFindFirstByEntry(c, null, "C", 20);
+ }
+
+ if(colCount > 2) {
+ doFindFirstByEntry(c, 27, "C", 1, "K1");
+ doFindFirstByEntry(c, null, "C", 4, "K3");
+ }
+
+ try {
+ if(colCount > 2) {
+ c.findFirstRowByEntry("C", 4, "K1", 14);
+ } else if(colCount > 1) {
+ c.findFirstRowByEntry("C", 4, "K1");
+ } else {
+ c.findFirstRowByEntry("C", 4);
+ }
+ fail("IllegalArgumentException should have been thrown");
+ } catch(IllegalArgumentException expected) {
+ // scucess
+ }
+
+ doFindByEntryRange(c, 11, 20, "B");
+ doFindByEntry(c, new int[]{}, "Z");
+
+ if(colCount > 1) {
+ doFindByEntryRange(c, 13, 14, "B", 3);
+ doFindByEntry(c, new int[]{}, "B", 20);
+ }
+
+ if(colCount > 2) {
+ doFindByEntryRange(c, 14, 14, "B", 3, "K2");
+ doFindByEntry(c, new int[]{}, "B", 3, "K3");
+ }
+
+ doFindByRow(idx, 13,
+ "data1", "B", "value", "value13");
+ doFindByRow(idx, 13,
+ "data1", "B", "key3", "K1", "value", "value13");
+ doFindByRow(idx, 13,
+ "data1", "B", "num2", 3, "key3", "K1", "value", "value13");
+ doFindByRow(idx, 13,
+ "num2", 3, "value", "value13");
+ doFindByRow(idx, 13,
+ "value", "value13");
+ doFindByRow(idx, null,
+ "data1", "B", "num2", 5, "key3", "K1", "value", "value13");
+ doFindByRow(idx, null,
+ "data1", "B", "value", "value4");
+
+ Column col = idx.getTable().getColumn("data1");
+ doFindValue(idx, 21, col, "C");
+ doFindValue(idx, null, col, "Z");
+ col = idx.getTable().getColumn("value");
+ doFindValue(idx, 21, col, "value21");
+ doFindValue(idx, null, col, "valueZ");
+ }
+
+ private static void doFindFirstByEntry(IndexCursor c, Integer expectedId,
+ Object... entry)
+ throws Exception
+ {
+ if(expectedId != null) {
+ assertTrue(c.findFirstRowByEntry(entry));
+ assertEquals(expectedId, c.getCurrentRow().get("id"));
+ } else {
+ assertFalse(c.findFirstRowByEntry(entry));
+ }
+ }
+
+ private static void doFindByEntryRange(IndexCursor c, int start, int end,
+ Object... entry)
+ {
+ List<Integer> expectedIds = new ArrayList<Integer>();
+ for(int i = start; i <= end; ++i) {
+ expectedIds.add(i);
+ }
+ doFindByEntry(c, expectedIds, entry);
+ }
+
+ private static void doFindByEntry(IndexCursor c, int[] ids,
+ Object... entry)
+ {
+ List<Integer> expectedIds = new ArrayList<Integer>();
+ for(int id : ids) {
+ expectedIds.add(id);
+ }
+ doFindByEntry(c, expectedIds, entry);
+ }
+
+ private static void doFindByEntry(IndexCursor c, List<Integer> expectedIds,
+ Object... entry)
+ {
+ List<Integer> foundIds = new ArrayList<Integer>();
+ for(Row row : c.newEntryIterable(entry)) {
+ foundIds.add((Integer)row.get("id"));
+ }
+ assertEquals(expectedIds, foundIds);
+ }
+
+ private static void doFindByRow(Index idx, Integer id, Object... rowPairs)
+ throws Exception
+ {
+ Map<String,Object> map = createExpectedRow(
+ rowPairs);
+ Row r = CursorBuilder.findRow(idx, map);
+ if(id != null) {
+ assertEquals(id, r.get("id"));
+ } else {
+ assertNull(r);
+ }
+ }
+
+ private static void doFindValue(Index idx, Integer id,
+ Column columnPattern, Object valuePattern)
+ throws Exception
+ {
+ Object value = CursorBuilder.findValue(
+ idx, idx.getTable().getColumn("id"), columnPattern, valuePattern);
+ if(id != null) {
+ assertEquals(id, value);
+ } else {
+ assertNull(value);
+ }
+ }
}