]> source.dussan.org Git - jackcess.git/commitdiff
Implement support for partial index lookups. Efficient IndexCursor lookups can now...
authorJames Ahlborn <jtahlborn@yahoo.com>
Mon, 3 Apr 2017 04:23:31 +0000 (04:23 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Mon, 3 Apr 2017 04:23:31 +0000 (04:23 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1087 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java
src/main/java/com/healthmarketscience/jackcess/Index.java
src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java
src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java
src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java
src/test/java/com/healthmarketscience/jackcess/CursorTest.java

index 2191cd40ea635ff0093485011d63bd54fb852933..a172b925f4e4881e11bcab7b5d24e81021f1f629 100644 (file)
@@ -4,6 +4,13 @@
     <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">
index 688eb9e38adb297f8ed16cb544dc079013746562..831c78fb34252486c91eab3572a3274aa8898ec2 100644 (file)
@@ -22,10 +22,11 @@ import java.util.Arrays;
 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;
 
 
@@ -145,7 +146,8 @@ public class CursorBuilder {
    * 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 +
@@ -198,7 +200,8 @@ public class CursorBuilder {
    */
   public CursorBuilder setStartEntry(Object... startEntry) {
     if(startEntry != null) {
-      setStartRow(_index.constructIndexRowFromEntry(startEntry));
+      setStartRow(_index.constructPartialIndexRowFromEntry(
+                      IndexData.MIN_VALUE, startEntry));
     }
     return this;
   }
@@ -230,7 +233,8 @@ public class CursorBuilder {
    */
   public CursorBuilder setEndEntry(Object... endEntry) {
     if(endEntry != null) {
-      setEndRow(_index.constructIndexRowFromEntry(endEntry));
+      setEndRow(_index.constructPartialIndexRowFromEntry(
+                    IndexData.MAX_VALUE, endEntry));
     }
     return this;
   }
index 3b35b70050b5320c03af483d8d566c2cc54c4a29..a9b9b832b8f20195b25d6a004c7805952d77bc5f 100644 (file)
@@ -39,6 +39,11 @@ public interface Index
 
   public boolean isForeignKey();
 
+  /**
+   * @usage _general_method_
+   */
+  public int getColumnCount();
+
   /**
    * @return the Columns for this index (unmodifiable)
    */
index 025f7358c66a8726fe96a731988818032143f07f..21024e2859afef5e3ac277922161d0fea612b1e9 100644 (file)
@@ -20,7 +20,6 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
@@ -260,8 +259,7 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor
       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);
   }
 
@@ -310,37 +308,24 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor
       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;
       }
@@ -359,8 +344,16 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor
     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;
@@ -388,15 +381,16 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor
   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
@@ -417,7 +411,8 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor
 
   private Object[] toRowValues(Object[] entryValues)
   {
-    return _entryCursor.getIndexData().constructIndexRowFromEntry(entryValues);
+    return _entryCursor.getIndexData().constructPartialIndexRowFromEntry(
+        IndexData.MIN_VALUE, entryValues);
   }
   
   @Override
@@ -545,5 +540,4 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor
     }    
   }
 
-  
 }
index ba8be579e67fcc6fbc31507e54e77c7a60667cc5..e7ae7d507188db82c23d2e79791b9ba8e7494b03 100644 (file)
@@ -330,6 +330,10 @@ public class IndexData {
     return Collections.unmodifiableList(_columns);
   }
 
+  public int getColumnCount() {
+    return _columns.size();
+  }
+
   /**
    * Whether or not the complete index state has been read.
    */
@@ -784,7 +788,7 @@ public class IndexData {
     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;
@@ -979,6 +983,37 @@ public class IndexData {
     }
     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
@@ -991,6 +1026,18 @@ public class IndexData {
     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.
@@ -1012,6 +1059,42 @@ public class IndexData {
     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)
@@ -1266,6 +1349,7 @@ public class IndexData {
     _entryBuffer.reset();
     
     for(ColumnDescriptor col : _columns) {
+
       Object value = values[col.getColumnIndex()];
       if(ColumnImpl.isRawData(value)) {
         // ignore it, we could not parse it
@@ -1273,13 +1357,17 @@ public class IndexData {
       }
 
       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;
       }
 
index 8cdb54c4323510bc7ef2b0002ff52c9d2b32fcff..0fbd2313acd16acb68029e8023262757debada1c 100644 (file)
@@ -220,6 +220,10 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
     return getIndexData().getColumns();
   }
 
+  public int getColumnCount() {
+    return getIndexData().getColumnCount();
+  }
+
   public CursorBuilder newCursor() {
     return getTable().newCursor().setIndex(this);
   }
@@ -286,6 +290,21 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
     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.
@@ -297,6 +316,18 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
     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.
@@ -308,6 +339,20 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
     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)
index 2b41fe766411453af044db010aaf494cfecdf956..d8fcbbdc10985c2f9aca131f764ff59de751e626 100644 (file)
@@ -201,7 +201,7 @@ public class RelationshipCreator extends DBMutator
 
     // 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"));
@@ -307,16 +307,17 @@ public class RelationshipCreator extends DBMutator
   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(
index 5155b167e5255431f9479069e4f38cdf8f7bfa17..aa824dff8cd7be0218eda551776318111001d87a 100644 (file)
@@ -86,6 +86,10 @@ public class TableImpl implements Table
    */
   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 =
@@ -496,32 +500,47 @@ public class TableImpl implements Table
   }
 
   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() {
index 8d8f348b5e523f8e6f66cbb3cc760312ad9682f5..89f935f9ff3dee52e703065626bc5d30a8fb6e77 100644 (file)
@@ -295,7 +295,7 @@ public class TableUpdater extends TableMutator
       return false;
     }
 
-    if(idx1.getColumns().size() != idx2.getColumns().size()) {
+    if(idx1.getColumns().size() != idx2.getColumnCount()) {
       return false;
     }
     
index fca26fc3a4a2f8142a6d176c051bd49ff9c7b413..16ed72a363410b21c0baff26bbfeb84cb35fac57 100644 (file)
@@ -30,6 +30,7 @@ import com.healthmarketscience.jackcess.impl.ColumnImpl;
 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;
@@ -1332,5 +1333,271 @@ public class CursorTest extends TestCase {
     }
   }
 
+  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);
+    }
+  } 
 }