Skip to content

Commit cf806ca

Browse files
pranavshenoymaedhroz
authored andcommitted
Support LIKE expressions in filtering queries
patch by Pranav Shenoy; reviewed by Caleb Rackliffe and David Capwell for CASSANDRA-17198
1 parent 06440e9 commit cf806ca

File tree

12 files changed

+156
-41
lines changed

12 files changed

+156
-41
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
5.1
2+
* Support LIKE expressions in filtering queries (CASSANDRA-17198)
23
* Make legacy index rebuilds safe on Gossip -> TCM upgrades (CASSANDRA-20887)
34
* Minor improvements and hardening for IndexHints (CASSANDRA-20888)
45
* Stop repair scheduler if two major versions are detected (CASSANDRA-20048)

src/java/org/apache/cassandra/cql3/Operator.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ public String toString()
611611

612612
public boolean isSatisfiedBy(AbstractType<?> type, ByteBuffer leftOperand, ByteBuffer rightOperand)
613613
{
614-
return leftOperand != null && ByteBufferUtil.contains(leftOperand, rightOperand);
614+
return leftOperand != null && leftOperand.equals(rightOperand);
615615
}
616616
},
617617
LIKE(14)
@@ -621,12 +621,6 @@ public boolean isSatisfiedBy(AbstractType<?> type, ByteBuffer leftOperand, ByteB
621621
{
622622
throw new UnsupportedOperationException();
623623
}
624-
625-
@Override
626-
public boolean requiresIndexing()
627-
{
628-
return true;
629-
}
630624
},
631625
ANN(15)
632626
{

src/java/org/apache/cassandra/cql3/Ordering.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ public SingleRestriction toRestriction()
105105
{
106106
return new SimpleRestriction(ColumnsExpression.singleColumn(columnMetadata, tableMetadata),
107107
Operator.ANN,
108-
Terms.of(vectorValue));
108+
Terms.of(vectorValue),
109+
false);
109110
}
110111
}
111112

src/java/org/apache/cassandra/cql3/Relation.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public boolean onToken()
187187
* @return the <code>Restriction</code> corresponding to this <code>Relation</code>
188188
* @throws InvalidRequestException if this <code>Relation</code> is not valid
189189
*/
190-
public SingleRestriction toRestriction(TableMetadata table, VariableSpecifications boundNames)
190+
public SingleRestriction toRestriction(TableMetadata table, VariableSpecifications boundNames, boolean allowFiltering)
191191
{
192192
ColumnsExpression columnsExpression = rawExpressions.prepare(table);
193193

@@ -217,9 +217,9 @@ public SingleRestriction toRestriction(TableMetadata table, VariableSpecificatio
217217

218218
// An IN restriction with only one element is the same as an EQ restriction
219219
if (operator.isIN() && terms.containsSingleTerm())
220-
return new SimpleRestriction(columnsExpression, Operator.EQ, terms);
220+
return new SimpleRestriction(columnsExpression, Operator.EQ, terms, allowFiltering);
221221

222-
return new SimpleRestriction(columnsExpression, operator, terms);
222+
return new SimpleRestriction(columnsExpression, operator, terms, allowFiltering);
223223
}
224224

225225
public ColumnIdentifier column()

src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
4545
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
46+
import java.util.Optional;
4647

4748
/**
4849
* A simple predicate on a columns expression (e.g. columnA = X).
@@ -64,11 +65,17 @@ public final class SimpleRestriction implements SingleRestriction
6465
*/
6566
private final Terms values;
6667

67-
public SimpleRestriction(ColumnsExpression columnsExpression, Operator operator, Terms values)
68+
/**
69+
* Indicates if the query has allow filtering
70+
*/
71+
private final boolean allowFiltering;
72+
73+
public SimpleRestriction(ColumnsExpression columnsExpression, Operator operator, Terms values, boolean allowFiltering)
6874
{
6975
this.columnsExpression = columnsExpression;
7076
this.operator = operator;
7177
this.values = values;
78+
this.allowFiltering = allowFiltering;
7279
}
7380

7481
@Override
@@ -344,11 +351,11 @@ public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryO
344351
else if (operator == Operator.LIKE)
345352
{
346353
LikePattern pattern = LikePattern.parse(buffers.get(0));
347-
// there must be a suitable INDEX for LIKE_XXX expressions
354+
348355
RowFilter.SimpleExpression expression = filter.add(column, pattern.kind().operator(), pattern.value());
349-
indexRegistry.getBestIndexFor(expression, indexHints)
350-
.orElseThrow(() -> invalidRequest("%s is only supported on properly indexed columns",
351-
expression));
356+
Optional<Index> index = indexRegistry.getBestIndexFor(expression, indexHints);
357+
if(!index.isPresent() && !allowFiltering)
358+
throw invalidRequest("%s is only supported on properly indexed columns or with ALLOW FILTERING", expression);
352359
}
353360
else
354361
{

src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,11 @@ public StatementRestrictions(ClientState state,
226226
if (!forView)
227227
throw new InvalidRequestException("Unsupported restriction: " + relation);
228228

229-
this.notNullColumns.addAll(relation.toRestriction(table, boundNames).columns());
229+
this.notNullColumns.addAll(relation.toRestriction(table, boundNames, allowFiltering).columns());
230230
}
231231
else if (operator.requiresIndexing())
232232
{
233-
Restriction restriction = relation.toRestriction(table, boundNames);
233+
Restriction restriction = relation.toRestriction(table, boundNames, allowFiltering);
234234

235235
if (!type.allowUseOfSecondaryIndices() || !restriction.hasSupportingIndex(indexRegistry, indexHints))
236236
throw invalidRequest("%s restriction is only supported on properly " +
@@ -240,7 +240,7 @@ else if (operator.requiresIndexing())
240240
}
241241
else
242242
{
243-
addRestriction(relation.toRestriction(table, boundNames), indexRegistry, indexHints);
243+
addRestriction(relation.toRestriction(table, boundNames, allowFiltering), indexRegistry, indexHints);
244244
}
245245
}
246246

src/java/org/apache/cassandra/index/sai/plan/Expression.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.cassandra.index.sai.StorageAttachedIndex;
3232
import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer;
3333
import org.apache.cassandra.index.sai.utils.IndexTermType;
34+
import org.apache.cassandra.utils.ByteBufferUtil;
3435

3536
/**
3637
* An {@link Expression} is an internal representation of an index query operation. They are built from
@@ -79,7 +80,7 @@ public static boolean supportsOperator(Operator operator)
7980

8081
public enum IndexOperator
8182
{
82-
EQ, RANGE, CONTAINS_KEY, CONTAINS_VALUE, ANN, IN;
83+
EQ, RANGE, CONTAINS_KEY, CONTAINS_VALUE, ANN, IN, LIKE_PREFIX, LIKE_SUFFIX, LIKE_MATCHES, LIKE_CONTAINS;
8384

8485
public static IndexOperator valueOf(Operator operator)
8586
{
@@ -106,6 +107,15 @@ public static IndexOperator valueOf(Operator operator)
106107

107108
case IN:
108109
return IN;
110+
case LIKE_PREFIX:
111+
return LIKE_PREFIX;
112+
case LIKE_SUFFIX:
113+
return LIKE_SUFFIX;
114+
case LIKE_CONTAINS:
115+
return LIKE_CONTAINS;
116+
case LIKE_MATCHES:
117+
return LIKE_MATCHES;
118+
109119

110120
default:
111121
return null;
@@ -114,13 +124,18 @@ public static IndexOperator valueOf(Operator operator)
114124

115125
public boolean isEquality()
116126
{
117-
return this == EQ || this == CONTAINS_KEY || this == CONTAINS_VALUE || this == IN;
127+
return this == EQ || this == CONTAINS_KEY || this == CONTAINS_VALUE || this == IN || isLikeVariant();
118128
}
119129

120130
public boolean isEqualityOrRange()
121131
{
122132
return isEquality() || this == RANGE;
123133
}
134+
135+
public boolean isLikeVariant()
136+
{
137+
return this == LIKE_SUFFIX || this == LIKE_PREFIX || this == LIKE_CONTAINS || this == LIKE_MATCHES;
138+
}
124139
}
125140

126141
public abstract boolean isNotIndexed();
@@ -172,6 +187,10 @@ public Expression add(Operator op, ByteBuffer value)
172187
case EQ:
173188
case CONTAINS:
174189
case CONTAINS_KEY:
190+
case LIKE_PREFIX:
191+
case LIKE_SUFFIX:
192+
case LIKE_MATCHES:
193+
case LIKE_CONTAINS:
175194
case IN:
176195
lower = new Bound(value, indexTermType, true);
177196
upper = lower;
@@ -354,6 +373,26 @@ private boolean termMatches(ByteBuffer term, ByteBuffer requestedValue)
354373
}
355374
}
356375
break;
376+
case LIKE_PREFIX:
377+
{
378+
isMatch = ByteBufferUtil.startsWith(term, requestedValue);
379+
break;
380+
}
381+
case LIKE_SUFFIX:
382+
{
383+
isMatch = ByteBufferUtil.endsWith(term, requestedValue);
384+
break;
385+
}
386+
case LIKE_CONTAINS:
387+
{
388+
isMatch = ByteBufferUtil.contains(term, requestedValue);
389+
break;
390+
}
391+
case LIKE_MATCHES:
392+
{
393+
isMatch = term.equals(requestedValue);
394+
break;
395+
}
357396
}
358397
return isMatch;
359398
}

test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,7 +1645,7 @@ private static TableMetadata newTableMetadata(Sort... sorts)
16451645
private static Restriction newSingleRestriction(TableMetadata tableMetadata, int index, Operator operator, ByteBuffer... values)
16461646
{
16471647
ColumnMetadata column = getClusteringColumnDefinition(tableMetadata, index);
1648-
return new SimpleRestriction(ColumnsExpression.singleColumn(column, tableMetadata), operator, toTerms(values));
1648+
return new SimpleRestriction(ColumnsExpression.singleColumn(column, tableMetadata), operator, toTerms(values), false);
16491649
}
16501650

16511651
/**
@@ -1669,7 +1669,8 @@ private static Restriction newMultiEq(TableMetadata tableMetadata, int firstInde
16691669
TupleType tupleType = new TupleType(types);
16701670
return new SimpleRestriction(ColumnsExpression.multiColumns(columns, tableMetadata),
16711671
Operator.EQ,
1672-
Terms.of(new MultiElements.Value(tupleType, asList(values))));
1672+
Terms.of(new MultiElements.Value(tupleType, asList(values))),
1673+
false);
16731674
}
16741675

16751676
/**
@@ -1700,7 +1701,7 @@ private static Restriction newMultiIN(TableMetadata tableMetadata, int firstInde
17001701
{
17011702
terms.add(new MultiElements.Value(tupleType, values[i]));
17021703
}
1703-
return new SimpleRestriction(ColumnsExpression.multiColumns(columns, tableMetadata), Operator.IN, Terms.of(terms));
1704+
return new SimpleRestriction(ColumnsExpression.multiColumns(columns, tableMetadata), Operator.IN, Terms.of(terms), false);
17041705
}
17051706

17061707
/**
@@ -1737,7 +1738,8 @@ private static Restriction newMultiSlice(TableMetadata tableMetadata, int firstI
17371738
TupleType type = new TupleType(types);
17381739
return new SimpleRestriction(ColumnsExpression.multiColumns(columns, tableMetadata),
17391740
operator,
1740-
Terms.of(new MultiElements.Value(type, asList(values))));
1741+
Terms.of(new MultiElements.Value(type, asList(values))),
1742+
false);
17411743
}
17421744

17431745
/**

test/unit/org/apache/cassandra/cql3/validation/entities/SecondaryIndexTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -843,16 +843,16 @@ public void prepareStatementsWithLIKEClauses() throws Throwable
843843

844844
// LIKE is not supported on indexes of non-literal values
845845
// this is rejected before binding, so the value isn't available in the error message
846-
assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
846+
assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
847847
"SELECT * FROM %s WHERE v3 LIKE ?",
848848
"%abc");
849-
assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
849+
assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
850850
"SELECT * FROM %s WHERE v3 LIKE ?",
851851
"%abc%");
852-
assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
852+
assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
853853
"SELECT * FROM %s WHERE v3 LIKE ?",
854854
"%abc%");
855-
assertInvalidMessage("LIKE restriction is only supported on properly indexed columns. v3 LIKE ? is not valid",
855+
assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE,
856856
"SELECT * FROM %s WHERE v3 LIKE ?",
857857
"abc");
858858
}

test/unit/org/apache/cassandra/index/sai/cql/AllowFilteringTest.java

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import static java.lang.String.format;
3434
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3535
import static org.junit.Assert.assertNotNull;
36-
36+
import org.apache.cassandra.exceptions.InvalidRequestException;
3737
/**
3838
* Tests that {@code ALLOW FILTERING} is required only if needed.
3939
*/
@@ -433,4 +433,83 @@ public void testAllowFilteringDuringIndexBuild() throws Throwable
433433
execute("SELECT * FROM %s WHERE v=0");
434434
execute("SELECT * FROM %s WHERE v=0 ALLOW FILTERING");
435435
}
436+
437+
@Test
438+
public void testAllowFilteringWithLikePrefixPostFiltering()
439+
{
440+
createTable("CREATE TABLE %S (k1 int, k2 text, k3 int, PRIMARY KEY (k1))");
441+
createIndex("CREATE INDEX ON %s(k3) USING 'sai'");
442+
443+
execute("insert into %s (k1, k2, k3) values (1, 'fo', 1)");
444+
execute("insert into %s (k1, k2, k3) values (2, 'foo', 2)");
445+
execute("insert into %s (k1, k2, k3) values (3, 'fo', 3)");
446+
execute("insert into %s (k1, k2, k3) values (4, 'ba', 4)");
447+
execute("insert into %s (k1, k2, k3) values (5, 'bar', 5)");
448+
449+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE 'f%%' ALLOW FILTERING"), 3);
450+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE 'ca%%' ALLOW FILTERING"), 0);
451+
assertThatThrownBy(() -> execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE 'f%%'"))
452+
.hasMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE)
453+
.isInstanceOf(InvalidRequestException.class);
454+
}
455+
456+
@Test
457+
public void testAllowFilteringWithLikeSuffixPostFiltering()
458+
{
459+
createTable("CREATE TABLE %S (k1 int, k2 text, k3 int, PRIMARY KEY (k1))");
460+
createIndex("CREATE INDEX ON %s(k3) USING 'sai'");
461+
462+
execute("insert into %s (k1, k2, k3) values (1, 'fo', 1)");
463+
execute("insert into %s (k1, k2, k3) values (2, 'foo', 2)");
464+
execute("insert into %s (k1, k2, k3) values (3, 'fo', 3)");
465+
execute("insert into %s (k1, k2, k3) values (4, 'ba', 4)");
466+
execute("insert into %s (k1, k2, k3) values (5, 'bar', 5)");
467+
468+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE '%%o' ALLOW FILTERING"), 3);
469+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE '%%c' ALLOW FILTERING"), 0);
470+
assertThatThrownBy(() -> execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE '%%c'"))
471+
.hasMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE)
472+
.isInstanceOf(InvalidRequestException.class);
473+
}
474+
475+
@Test
476+
public void testAllowFilteringWithLikeContainsPostFiltering()
477+
{
478+
createTable("CREATE TABLE %S (k1 int, k2 text, k3 int, PRIMARY KEY (k1))");
479+
createIndex("CREATE INDEX ON %s(k3) USING 'sai'");
480+
481+
execute("insert into %s (k1, k2, k3) values (1, 'fo', 1)");
482+
execute("insert into %s (k1, k2, k3) values (2, 'foo', 2)");
483+
execute("insert into %s (k1, k2, k3) values (3, 'fo', 3)");
484+
execute("insert into %s (k1, k2, k3) values (4, 'ba', 4)");
485+
execute("insert into %s (k1, k2, k3) values (5, 'bar', 5)");
486+
487+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE '%%ar%%' ALLOW FILTERING"), 1);
488+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE '%%ca%%' ALLOW FILTERING"), 0);
489+
assertThatThrownBy(() -> execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE '%%ca%%'"))
490+
.hasMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE)
491+
.isInstanceOf(InvalidRequestException.class);
492+
}
493+
494+
@Test
495+
public void testAllowFilteringWithLikeMatchesPostFiltering()
496+
{
497+
createTable("CREATE TABLE %S (k1 int, k2 text, k3 int, PRIMARY KEY (k1))");
498+
createIndex("CREATE INDEX ON %s(k3) USING 'sai'");
499+
500+
execute("insert into %s (k1, k2, k3) values (1, 'fo', 1)");
501+
execute("insert into %s (k1, k2, k3) values (2, 'foo', 2)");
502+
execute("insert into %s (k1, k2, k3) values (3, 'fo', 3)");
503+
execute("insert into %s (k1, k2, k3) values (4, 'ba', 4)");
504+
execute("insert into %s (k1, k2, k3) values (5, 'bar', 5)");
505+
506+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE 'foo' ALLOW FILTERING"), 1);
507+
assertRowCount(execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE 'baar' ALLOW FILTERING"), 0);
508+
assertThatThrownBy(() -> execute("SELECT * FROM %s WHERE k3 > 0 AND k2 LIKE 'baar'"))
509+
.hasMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE)
510+
.isInstanceOf(InvalidRequestException.class);
511+
}
512+
513+
514+
436515
}

0 commit comments

Comments
 (0)