Custom index expression in SELECT forces index selection

Patch by Sam Tunnicliffe; reviewed by Sylvain Lebresne for
CASSANDRA-10436
This commit is contained in:
Sam Tunnicliffe 2015-10-02 13:27:45 +01:00
parent 2d472370b4
commit 5ffae4f45a
5 changed files with 91 additions and 7 deletions

View File

@ -111,7 +111,9 @@ Upgrading
- SizeTieredCompactionStrategy parameter cold_reads_to_omit has been removed.
- The secondary index API has been comprehensively reworked. This will be a breaking
change for any custom index implementations, which should now look to implement
the new org.apache.cassandra.index.Index interface.
the new org.apache.cassandra.index.Index interface. New syntax has been added to create
and query row-based indexes, which are not explicitly linked to a single column in the
base table.
2.2.2

View File

@ -21,7 +21,7 @@ package org.apache.cassandra.cql3.restrictions;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.AbstractType;
public class CustomIndexExpression
{
@ -38,9 +38,9 @@ public class CustomIndexExpression
this.valueRaw = value;
}
public void prepareValue(CFMetaData cfm, VariableSpecifications boundNames)
public void prepareValue(CFMetaData cfm, AbstractType<?> expressionType, VariableSpecifications boundNames)
{
ColumnSpecification spec = new ColumnSpecification(cfm.ksName, cfm.ksName, valueColId, UTF8Type.instance);
ColumnSpecification spec = new ColumnSpecification(cfm.ksName, cfm.ksName, valueColId, expressionType);
value = valueRaw.prepare(cfm.ksName, spec);
value.collectMarkerSpecification(boundNames);
}

View File

@ -31,6 +31,7 @@ import org.apache.cassandra.cql3.statements.Bound;
import org.apache.cassandra.cql3.statements.StatementType;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.dht.*;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.index.Index;
@ -503,7 +504,6 @@ public final class StatementRestrictions
throw new InvalidRequestException(IndexRestrictions.MULTIPLE_EXPRESSIONS);
CustomIndexExpression expression = expressions.get(0);
expression.prepareValue(cfm, boundNames);
CFName cfName = expression.targetIndex.getCfName();
if (cfName.hasKeyspace()
@ -521,9 +521,12 @@ public final class StatementRestrictions
if (!index.getIndexMetadata().isCustom())
throw IndexRestrictions.nonCustomIndexInExpression(expression.targetIndex);
if (index.customExpressionValueType() == null)
AbstractType<?> expressionType = index.customExpressionValueType();
if (expressionType == null)
throw IndexRestrictions.customExpressionNotSupported(expression.targetIndex);
expression.prepareValue(cfm, expressionType, boundNames);
indexRestrictions.add(expression);
}

View File

@ -585,8 +585,12 @@ public class SecondaryIndexManager implements IndexRegistry
{
if (expression.isCustom())
{
// Only a single custom expression is allowed per query and, if present,
// we want to always favour the index specified in such an expression
RowFilter.CustomExpression customExpression = (RowFilter.CustomExpression)expression;
searchableIndexes.add(indexes.get(customExpression.getTargetIndex().name));
logger.trace("Command contains a custom index expression, using target index {}", customExpression.getTargetIndex().name);
Tracing.trace("Command contains a custom index expression, using target index {}", customExpression.getTargetIndex().name);
return indexes.get(customExpression.getTargetIndex().name);
}
else
{

View File

@ -17,6 +17,8 @@ import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.schema.Indexes;
@ -403,6 +405,53 @@ public class CustomIndexTest extends CQLTester
assertEquals(1, lessSelective.searchersProvided);
}
@Test
public void customExpressionForcesIndexSelection() throws Throwable
{
createTable("CREATE TABLE %s(a int, b int, c int, PRIMARY KEY (a))");
createIndex(String.format("CREATE CUSTOM INDEX more_selective ON %%s(b) USING '%s'",
SettableSelectivityIndex.class.getName()));
createIndex(String.format("CREATE CUSTOM INDEX less_selective ON %%s(c) USING '%s'",
SettableSelectivityIndex.class.getName()));
SettableSelectivityIndex moreSelective =
(SettableSelectivityIndex)getCurrentColumnFamilyStore().indexManager.getIndexByName("more_selective");
SettableSelectivityIndex lessSelective =
(SettableSelectivityIndex)getCurrentColumnFamilyStore().indexManager.getIndexByName("less_selective");
assertEquals(0, moreSelective.searchersProvided);
assertEquals(0, lessSelective.searchersProvided);
// without a custom expression, the more selective index should be chosen
moreSelective.setEstimatedResultRows(1);
lessSelective.setEstimatedResultRows(1000);
execute("SELECT * FROM %s WHERE b=0 AND c=0 ALLOW FILTERING");
assertEquals(1, moreSelective.searchersProvided);
assertEquals(0, lessSelective.searchersProvided);
// when a custom expression is present, its target index should be preferred
execute("SELECT * FROM %s WHERE b=0 AND expr(less_selective, 'expression') ALLOW FILTERING");
assertEquals(1, moreSelective.searchersProvided);
assertEquals(1, lessSelective.searchersProvided);
}
@Test
public void testCustomExpressionValueType() throws Throwable
{
// verify that the type of the expression value is determined by Index::customExpressionValueType
createTable("CREATE TABLE %s (k int, v1 uuid, v2 blob, PRIMARY KEY(k))");
createIndex(String.format("CREATE CUSTOM INDEX int_index ON %%s() USING '%s'",
Int32ExpressionIndex.class.getName()));
createIndex(String.format("CREATE CUSTOM INDEX text_index ON %%s() USING '%s'",
UTF8ExpressionIndex.class.getName()));
execute("SELECT * FROM %s WHERE expr(text_index, 'foo')");
assertInvalidMessage("Invalid INTEGER constant (99) for \"custom index expression\" of type text",
"SELECT * FROM %s WHERE expr(text_index, 99)");
execute("SELECT * FROM %s WHERE expr(int_index, 99)");
assertInvalidMessage("Invalid STRING constant (foo) for \"custom index expression\" of type int",
"SELECT * FROM %s WHERE expr(int_index, 'foo')");
}
private void testCreateIndex(String indexName, String... targetColumnNames) throws Throwable
{
createIndex(String.format("CREATE CUSTOM INDEX %s ON %%s(%s) USING '%s'",
@ -459,6 +508,32 @@ public class CustomIndexTest extends CQLTester
}
}
public static final class UTF8ExpressionIndex extends StubIndex
{
public UTF8ExpressionIndex(ColumnFamilyStore baseCfs, IndexMetadata metadata)
{
super(baseCfs, metadata);
}
public AbstractType<?> customExpressionValueType()
{
return UTF8Type.instance;
}
}
public static final class Int32ExpressionIndex extends StubIndex
{
public Int32ExpressionIndex(ColumnFamilyStore baseCfs, IndexMetadata metadata)
{
super(baseCfs, metadata);
}
public AbstractType<?> customExpressionValueType()
{
return Int32Type.instance;
}
}
public static final class SettableSelectivityIndex extends StubIndex
{
private int searchersProvided = 0;