git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1087 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-2.1.7
@@ -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"> |
@@ -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; | |||
} |
@@ -39,6 +39,11 @@ public interface Index | |||
public boolean isForeignKey(); | |||
/** | |||
* @usage _general_method_ | |||
*/ | |||
public int getColumnCount(); | |||
/** | |||
* @return the Columns for this index (unmodifiable) | |||
*/ |
@@ -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 | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
@@ -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) |
@@ -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( |
@@ -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() { |
@@ -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; | |||
} | |||
@@ -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); | |||
} | |||
} | |||
} | |||