From 4feb8fbfd1f90589f6e7cc54bee67c1a200a7840 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Mon, 3 Apr 2017 04:23:31 +0000 Subject: [PATCH] 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. git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1087 f203690c-595d-4dc9-a70b-905162fa7fd2 --- src/changes/changes.xml | 7 + .../jackcess/CursorBuilder.java | 14 +- .../healthmarketscience/jackcess/Index.java | 5 + .../jackcess/impl/IndexCursorImpl.java | 58 ++-- .../jackcess/impl/IndexData.java | 98 ++++++- .../jackcess/impl/IndexImpl.java | 45 +++ .../jackcess/impl/RelationshipCreator.java | 13 +- .../jackcess/impl/TableImpl.java | 33 ++- .../jackcess/impl/TableUpdater.java | 2 +- .../jackcess/CursorTest.java | 267 ++++++++++++++++++ 10 files changed, 486 insertions(+), 56 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 2191cd4..a172b92 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,6 +4,13 @@ Tim McCune + + + 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. + + diff --git a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java index 688eb9e..831c78f 100644 --- a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -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 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; } diff --git a/src/main/java/com/healthmarketscience/jackcess/Index.java b/src/main/java/com/healthmarketscience/jackcess/Index.java index 3b35b70..a9b9b83 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Index.java +++ b/src/main/java/com/healthmarketscience/jackcess/Index.java @@ -39,6 +39,11 @@ public interface Index public boolean isForeignKey(); + /** + * @usage _general_method_ + */ + public int getColumnCount(); + /** * @return the Columns for this index (unmodifiable) */ diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java index 025f735..21024e2 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java @@ -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 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 tmpRowPattern = new LinkedHashMap(); - 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 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 } } - } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java index ba8be57..e7ae7d5 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -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 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; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java index 8cdb54c..0fbd231 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java @@ -220,6 +220,10 @@ public class IndexImpl implements Index, Comparable 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 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 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 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 row) + { + return getIndexData().constructPartialIndexRow(filler, row); + } + @Override public String toString() { ToStringBuilder sb = CustomToStringStyle.builder(this) diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java index 2b41fe7..d8fcbbd 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java @@ -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 cols) { + return table.findIndexForColumns(getColumnNames(cols), + TableImpl.IndexFeature.EXACT_UNIQUE_ONLY); } private static String getTableErrorContext( diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index 5155b16..aa824df 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -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 VAR_LEN_COLUMN_COMPARATOR = @@ -496,32 +500,47 @@ public class TableImpl implements Table } public IndexImpl findIndexForColumns(Collection searchColumns, - boolean uniqueOnly) { + IndexFeature feature) { + + IndexImpl partialIndex = null; for(IndexImpl index : _indexes) { Collection indexColumns = index.getColumns(); - if(indexColumns.size() != searchColumns.size()) { + if(indexColumns.size() < searchColumns.size()) { continue; } + boolean exactMatch = (indexColumns.size() == searchColumns.size()); + Iterator sIter = searchColumns.iterator(); Iterator 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 getAutoNumberColumns() { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java index 8d8f348..89f935f 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java @@ -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; } diff --git a/src/test/java/com/healthmarketscience/jackcess/CursorTest.java b/src/test/java/com/healthmarketscience/jackcess/CursorTest.java index fca26fc..16ed72a 100644 --- a/src/test/java/com/healthmarketscience/jackcess/CursorTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/CursorTest.java @@ -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 expectedIds = new ArrayList(); + 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 expectedIds = new ArrayList(); + for(int id : ids) { + expectedIds.add(id); + } + doFindByEntry(c, expectedIds, entry); + } + + private static void doFindByEntry(IndexCursor c, List expectedIds, + Object... entry) + { + List foundIds = new ArrayList(); + 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 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); + } + } } -- 2.39.5