Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.InetAddressType;
import org.apache.cassandra.db.marshal.StringType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.distributed.Cluster;
Expand Down Expand Up @@ -429,6 +430,39 @@ else if (rs.nextBoolean())
return state.command(rs, select, annotation);
}

public Property.Command<State, Void, ?> likeQuery(RandomSource rs, State state)
{
Symbol symbol = rs.pick(state.searchableTextColumns);
TreeMap<ByteBuffer, List<BytesPartitionState.PrimaryKey>> universe = state.model.index(symbol);

NavigableSet<ByteBuffer> allowed = Sets.filter(universe.navigableKeySet(),
b -> !ByteBufferUtil.EMPTY_BYTE_BUFFER.equals(b));

if (allowed.isEmpty())
return Property.ignoreCommand();

ByteBuffer value = rs.pickOrderedSet(allowed);
String valueStr = symbol.type().getString(value);
String pattern;

if (rs.nextBoolean())
{
int prefixLen = Math.min(valueStr.length(), rs.nextInt(1, Math.max(2, valueStr.length())));
pattern = valueStr.substring(0, prefixLen) + "%";
}
else
{
int suffixLen = Math.min(valueStr.length(), rs.nextInt(1, Math.max(2, valueStr.length())));
pattern = "%" + valueStr.substring(valueStr.length() - suffixLen);
}

Select.Builder builder = Select.builder().table(state.metadata);
builder.allowFiltering();
builder.like(symbol, new Bind(pattern, symbol.type()));
Select select = builder.build();
return state.command(rs, select, "LIKE query on " + symbol.detailedName());
}

protected State createState(RandomSource rs, Cluster cluster)
{
return new State(rs, cluster);
Expand Down Expand Up @@ -464,6 +498,7 @@ public void test() throws IOException
.addIf(State::allowNonPartitionMultiColumnQuery, this::multiColumnQuery)
.addIf(State::allowPartitionQuery, this::partitionRestrictedQuery)
.addIf(State::allowClusteringBetweenQuery, this::clusteringBetweenQuery)
.addIf(State::allowLikeQuery, this::likeQuery)
.destroyState(State::close)
.commandsTransformer(LoggingCommand.factory())
.onSuccess(onSuccess(logger))
Expand Down Expand Up @@ -518,6 +553,7 @@ public class State extends CommonState
private final List<Symbol> searchableNonPartitionColumns;
private final List<Symbol> searchableColumns;
private final List<Symbol> nonPkIndexedColumns;
private final List<Symbol> searchableTextColumns;

public State(RandomSource rs, Cluster cluster)
{
Expand Down Expand Up @@ -585,6 +621,11 @@ public State(RandomSource rs, Cluster cluster)
.stream()
.filter(this::isSearchable)
.collect(Collectors.toList());

searchableTextColumns = nonPartitionColumns
.stream()
.filter(this::supportsLike)
.collect(Collectors.toList());
}

@Override
Expand All @@ -606,6 +647,12 @@ private boolean isSearchable(Symbol symbol)
return !(symbol.type().isMultiCell() && (symbol.type().isCollection() || symbol.type().isUDT()));
}

private boolean supportsLike(Symbol symbol)
{
AbstractType<?> type = symbol.type().unwrap();
return type instanceof StringType;
}

@Override
protected Gen<Mutation> mutationGen()
{
Expand Down Expand Up @@ -654,6 +701,11 @@ private LinkedHashMap<Symbol, IndexedColumn> createIndexes(RandomSource rs, Tabl
return indexed;
}

public boolean allowLikeQuery()
{
return !model.isEmpty() && !searchableTextColumns.isEmpty();
}

public boolean supportTokens()
{
return hasPartitions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2104,6 +2104,7 @@ private class LookupContext
{
private final Map<ReferenceExpression, List<? extends Expression>> eq = new HashMap<>();
private final Map<ReferenceExpression, List<ColumnCondition>> ltOrGt = new HashMap<>();
private final Map<ReferenceExpression, LikeCondition> like = new HashMap<>();
@Nullable
private Token token = null;
@Nullable
Expand Down Expand Up @@ -2323,6 +2324,20 @@ else if (conditional instanceof Conditional.And)
addConditional(and.left);
addConditional(and.right);
}
else if (conditional instanceof Conditional.Like)
{
Conditional.Like likeCondition = (Conditional.Like) conditional;
if (likeCondition.ref instanceof Symbol)
{
Symbol col = (Symbol) likeCondition.ref;
ByteBuffer pattern = eval(likeCondition.pattern);
var override = like.put(col, new LikeCondition(pattern));
if (override != null)
throw new IllegalStateException("Column " + col.detailedName() + " had 2 LIKE statements...");
}
else
throw new UnsupportedOperationException(likeCondition.ref.getClass().getCanonicalName());
}
else
{
//TODO (coverage): IS
Expand Down Expand Up @@ -2375,20 +2390,30 @@ private boolean include(ImmutableUniqueList<Symbol> columns, IntFunction<ByteBuf
if (!matches(col.type(), actual, ltOrGt.get(col)))
return false;
}
if (like.containsKey(col))
{
ByteBuffer actual = accessor.apply(columns.indexOf(col));
if (actual == null)
return false;
if (!like.get(col).matches(col.type(), actual))
return false;
}
}
return true;
}

private boolean testsClustering()
{
return factory.clusteringColumns.stream().anyMatch(eq::containsKey)
|| factory.clusteringColumns.stream().anyMatch(ltOrGt::containsKey);
|| factory.clusteringColumns.stream().anyMatch(ltOrGt::containsKey)
|| factory.clusteringColumns.stream().anyMatch(like::containsKey);
}

private boolean testsRegular()
{
return factory.regularColumns.stream().anyMatch(eq::containsKey)
|| factory.regularColumns.stream().anyMatch(ltOrGt::containsKey);
|| factory.regularColumns.stream().anyMatch(ltOrGt::containsKey)
|| factory.regularColumns.stream().anyMatch(like::containsKey);
}

private boolean testsRow()
Expand Down Expand Up @@ -2421,6 +2446,46 @@ private TokenCondition(Inequality inequality, Token token)
}
}

private static class LikeCondition
{
private final ByteBuffer pattern;

private LikeCondition(ByteBuffer pattern)
{
this.pattern = pattern;
}

private boolean matches(AbstractType<?> type, ByteBuffer value)
{
String valueStr = type.getString(value);
String patternStr = type.getString(pattern);

if (patternStr.startsWith("%") && patternStr.endsWith("%"))
{
// Contains
String substring = patternStr.substring(1, patternStr.length() - 1);
return valueStr.contains(substring);
}
else if (patternStr.startsWith("%"))
{
// Suffix
String suffix = patternStr.substring(1);
return valueStr.endsWith(suffix);
}
else if (patternStr.endsWith("%"))
{
// Prefix
String prefix = patternStr.substring(0, patternStr.length() - 1);
return valueStr.startsWith(prefix);
}
else
{
// Exact
return valueStr.equals(patternStr);
}
}
}

private interface ColumnUpdate
{
void update(long nowTs, Map<Symbol, ByteBuffer> write);
Expand Down
47 changes: 47 additions & 0 deletions test/unit/org/apache/cassandra/cql3/ast/Conditional.java
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,45 @@ public void toCQL(StringBuilder sb, CQLFormatter formatter)
}
}

class Like implements Conditional
{
public final ReferenceExpression ref;
public final Expression pattern;

public Like(ReferenceExpression ref, Expression pattern)
{
this.ref = ref;
this.pattern = pattern;
}

@Override
public void toCQL(StringBuilder sb, CQLFormatter formatter)
{
ref.toCQL(sb, formatter);
sb.append(" LIKE ");
pattern.toCQL(sb, formatter);
}

@Override
public Stream<? extends Element> stream()
{
return Stream.of(ref, pattern);
}

@Override
public Conditional visit(Visitor v)
{
var u = v.visit(this);
if (u != this) return u;
var ref = this.ref.visit(v);
var pattern = this.pattern.visit(v);
if (ref == this.ref && pattern == this.pattern)
return this;

return new Like(ref, pattern);
}
}

class And implements Conditional
{
public final Conditional left, right;
Expand Down Expand Up @@ -412,6 +451,8 @@ default <Type> T in(String name, AbstractType<Type> type, List<Type> values)

T is(ReferenceExpression ref, Is.Kind kind);

T like(ReferenceExpression ref, Expression pattern);

@Override
default T value(ReferenceExpression symbol, Expression e)
{
Expand Down Expand Up @@ -488,6 +529,12 @@ public Builder is(ReferenceExpression ref, Is.Kind kind)
return add(new Is(ref, kind));
}

@Override
public Builder like(ReferenceExpression ref, Expression pattern)
{
return add(new Like(ref, pattern));
}

public Builder is(String ref, Is.Kind kind)
{
return is(Symbol.unknownType(ref), kind);
Expand Down
14 changes: 14 additions & 0 deletions test/unit/org/apache/cassandra/cql3/ast/Mutation.java
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,13 @@ public B is(ReferenceExpression ref, Conditional.Is.Kind kind)
return (B) this;
}

@Override
public B like(ReferenceExpression ref, Expression pattern)
{
where.like(ref, pattern);
return (B) this;
}

@Override
public Update build()
{
Expand Down Expand Up @@ -1137,6 +1144,13 @@ public DeleteBuilder is(ReferenceExpression ref, Conditional.Is.Kind kind)
return this;
}

@Override
public DeleteBuilder like(ReferenceExpression ref, Expression pattern)
{
where.like(ref, pattern);
return this;
}

@Override
public Delete build()
{
Expand Down
7 changes: 7 additions & 0 deletions test/unit/org/apache/cassandra/cql3/ast/Select.java
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,13 @@ public T is(ReferenceExpression ref, Conditional.Is.Kind kind)
return (T) this;
}

@Override
public T like(ReferenceExpression ref, Expression pattern)
{
where.like(ref, pattern);
return (T) this;
}

public T orderByColumn(String name, AbstractType<?> type, OrderBy.Ordering ordering)
{
return orderByColumn(new Symbol(name, type), ordering);
Expand Down