org.apache.cassandra.cql3.restrictions.SingleColumnRestriction Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cassandra-all Show documentation
Show all versions of cassandra-all Show documentation
The Apache Cassandra Project develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model.
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.cql3.restrictions;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.serializers.ListSerializer;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.Term.Terminal;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.statements.Bound;
import org.apache.cassandra.db.MultiCBuilder;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.IndexRegistry;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.Pair;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkBindValueSet;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
public abstract class SingleColumnRestriction implements SingleRestriction
{
/**
* The definition of the column to which apply the restriction.
*/
protected final ColumnMetadata columnDef;
public SingleColumnRestriction(ColumnMetadata columnDef)
{
this.columnDef = columnDef;
}
@Override
public List getColumnDefs()
{
return Collections.singletonList(columnDef);
}
@Override
public ColumnMetadata getFirstColumn()
{
return columnDef;
}
@Override
public ColumnMetadata getLastColumn()
{
return columnDef;
}
@Override
public boolean hasSupportingIndex(IndexRegistry indexRegistry)
{
for (Index index : indexRegistry.listIndexes())
if (isSupportedBy(index))
return true;
return false;
}
@Override
public final SingleRestriction mergeWith(SingleRestriction otherRestriction)
{
// We want to allow query like: b > ? AND (b,c) < (?, ?)
if (otherRestriction.isMultiColumn() && canBeConvertedToMultiColumnRestriction())
{
return toMultiColumnRestriction().mergeWith(otherRestriction);
}
return doMergeWith(otherRestriction);
}
protected abstract SingleRestriction doMergeWith(SingleRestriction otherRestriction);
/**
* Converts this SingleColumnRestriction
into a {@link MultiColumnRestriction}
*
* @return the MultiColumnRestriction
corresponding to this
*/
abstract MultiColumnRestriction toMultiColumnRestriction();
/**
* Checks if this Restriction
can be converted into a {@link MultiColumnRestriction}
*
* @return true
if this Restriction
can be converted into a
* {@link MultiColumnRestriction}, false
otherwise.
*/
boolean canBeConvertedToMultiColumnRestriction()
{
return true;
}
/**
* Check if this type of restriction is supported by the specified index.
*
* @param index the secondary index
* @return true
this type of restriction is supported by the specified index,
* false
otherwise.
*/
protected abstract boolean isSupportedBy(Index index);
public static final class EQRestriction extends SingleColumnRestriction
{
private final Term value;
public EQRestriction(ColumnMetadata columnDef, Term value)
{
super(columnDef);
this.value = value;
}
@Override
public void addFunctionsTo(List functions)
{
value.addFunctionsTo(functions);
}
@Override
public boolean isEQ()
{
return true;
}
@Override
MultiColumnRestriction toMultiColumnRestriction()
{
return new MultiColumnRestriction.EQRestriction(Collections.singletonList(columnDef), value);
}
@Override
public void addRowFilterTo(RowFilter filter,
IndexRegistry indexRegistry,
QueryOptions options)
{
filter.add(columnDef, Operator.EQ, value.bindAndGet(options));
}
@Override
public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
{
builder.addElementToAll(value.bindAndGet(options));
checkFalse(builder.containsNull(), "Invalid null value in condition for column %s", columnDef.name);
checkFalse(builder.containsUnset(), "Invalid unset value for column %s", columnDef.name);
return builder;
}
@Override
public String toString()
{
return String.format("EQ(%s)", value);
}
@Override
public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
{
throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal", columnDef.name);
}
@Override
protected boolean isSupportedBy(Index index)
{
return index.supportsExpression(columnDef, Operator.EQ);
}
}
public static abstract class INRestriction extends SingleColumnRestriction
{
public INRestriction(ColumnMetadata columnDef)
{
super(columnDef);
}
@Override
public final boolean isIN()
{
return true;
}
@Override
public final SingleRestriction doMergeWith(SingleRestriction otherRestriction)
{
throw invalidRequest("%s cannot be restricted by more than one relation if it includes a IN", columnDef.name);
}
@Override
public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
{
builder.addEachElementToAll(getValues(options));
checkFalse(builder.containsNull(), "Invalid null value in condition for column %s", columnDef.name);
checkFalse(builder.containsUnset(), "Invalid unset value for column %s", columnDef.name);
return builder;
}
@Override
public void addRowFilterTo(RowFilter filter,
IndexRegistry indexRegistry,
QueryOptions options)
{
List values = getValues(options);
ByteBuffer buffer = ListSerializer.pack(values, values.size(), ProtocolVersion.V3);
filter.add(columnDef, Operator.IN, buffer);
}
@Override
protected final boolean isSupportedBy(Index index)
{
return index.supportsExpression(columnDef, Operator.IN);
}
protected abstract List getValues(QueryOptions options);
}
public static class InRestrictionWithValues extends INRestriction
{
protected final List values;
public InRestrictionWithValues(ColumnMetadata columnDef, List values)
{
super(columnDef);
this.values = values;
}
@Override
MultiColumnRestriction toMultiColumnRestriction()
{
return new MultiColumnRestriction.InRestrictionWithValues(Collections.singletonList(columnDef), values);
}
@Override
public void addFunctionsTo(List functions)
{
Terms.addFunctions(values, functions);
}
@Override
protected List getValues(QueryOptions options)
{
List buffers = new ArrayList<>(values.size());
for (Term value : values)
buffers.add(value.bindAndGet(options));
return buffers;
}
@Override
public String toString()
{
return String.format("IN(%s)", values);
}
}
public static class InRestrictionWithMarker extends INRestriction
{
protected final AbstractMarker marker;
public InRestrictionWithMarker(ColumnMetadata columnDef, AbstractMarker marker)
{
super(columnDef);
this.marker = marker;
}
@Override
public void addFunctionsTo(List functions)
{
}
@Override
MultiColumnRestriction toMultiColumnRestriction()
{
return new MultiColumnRestriction.InRestrictionWithMarker(Collections.singletonList(columnDef), marker);
}
@Override
protected List getValues(QueryOptions options)
{
Terminal term = marker.bind(options);
checkNotNull(term, "Invalid null value for column %s", columnDef.name);
checkFalse(term == Constants.UNSET_VALUE, "Invalid unset value for column %s", columnDef.name);
Term.MultiItemTerminal lval = (Term.MultiItemTerminal) term;
return lval.getElements();
}
@Override
public String toString()
{
return "IN ?";
}
}
public static class SliceRestriction extends SingleColumnRestriction
{
private final TermSlice slice;
public SliceRestriction(ColumnMetadata columnDef, Bound bound, boolean inclusive, Term term)
{
super(columnDef);
slice = TermSlice.newInstance(bound, inclusive, term);
}
@Override
public void addFunctionsTo(List functions)
{
slice.addFunctionsTo(functions);
}
@Override
MultiColumnRestriction toMultiColumnRestriction()
{
return new MultiColumnRestriction.SliceRestriction(Collections.singletonList(columnDef), slice);
}
@Override
public boolean isSlice()
{
return true;
}
@Override
public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
{
throw new UnsupportedOperationException();
}
@Override
public boolean hasBound(Bound b)
{
return slice.hasBound(b);
}
@Override
public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
{
Bound b = bound.reverseIfNeeded(getFirstColumn());
if (!hasBound(b))
return builder;
ByteBuffer value = slice.bound(b).bindAndGet(options);
checkBindValueSet(value, "Invalid unset value for column %s", columnDef.name);
return builder.addElementToAll(value);
}
@Override
public boolean isInclusive(Bound b)
{
return slice.isInclusive(b);
}
@Override
public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
{
checkTrue(otherRestriction.isSlice(),
"Column \"%s\" cannot be restricted by both an equality and an inequality relation",
columnDef.name);
SingleColumnRestriction.SliceRestriction otherSlice = (SingleColumnRestriction.SliceRestriction) otherRestriction;
checkFalse(hasBound(Bound.START) && otherSlice.hasBound(Bound.START),
"More than one restriction was found for the start bound on %s", columnDef.name);
checkFalse(hasBound(Bound.END) && otherSlice.hasBound(Bound.END),
"More than one restriction was found for the end bound on %s", columnDef.name);
return new SliceRestriction(columnDef, slice.merge(otherSlice.slice));
}
@Override
public void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
{
for (Bound b : Bound.values())
if (hasBound(b))
filter.add(columnDef, slice.getIndexOperator(b), slice.bound(b).bindAndGet(options));
}
@Override
protected boolean isSupportedBy(Index index)
{
return slice.isSupportedBy(columnDef, index);
}
@Override
public String toString()
{
return String.format("SLICE%s", slice);
}
private SliceRestriction(ColumnMetadata columnDef, TermSlice slice)
{
super(columnDef);
this.slice = slice;
}
}
// This holds CONTAINS, CONTAINS_KEY, and map[key] = value restrictions because we might want to have any combination of them.
public static final class ContainsRestriction extends SingleColumnRestriction
{
private List values = new ArrayList<>(); // for CONTAINS
private List keys = new ArrayList<>(); // for CONTAINS_KEY
private List entryKeys = new ArrayList<>(); // for map[key] = value
private List entryValues = new ArrayList<>(); // for map[key] = value
public ContainsRestriction(ColumnMetadata columnDef, Term t, boolean isKey)
{
super(columnDef);
if (isKey)
keys.add(t);
else
values.add(t);
}
public ContainsRestriction(ColumnMetadata columnDef, Term mapKey, Term mapValue)
{
super(columnDef);
entryKeys.add(mapKey);
entryValues.add(mapValue);
}
@Override
MultiColumnRestriction toMultiColumnRestriction()
{
throw new UnsupportedOperationException();
}
@Override
boolean canBeConvertedToMultiColumnRestriction()
{
return false;
}
@Override
public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
{
throw new UnsupportedOperationException();
}
@Override
public boolean isContains()
{
return true;
}
@Override
public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
{
checkTrue(otherRestriction.isContains(),
"Collection column %s can only be restricted by CONTAINS, CONTAINS KEY, or map-entry equality",
columnDef.name);
SingleColumnRestriction.ContainsRestriction newContains = new ContainsRestriction(columnDef);
copyKeysAndValues(this, newContains);
copyKeysAndValues((ContainsRestriction) otherRestriction, newContains);
return newContains;
}
@Override
public void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
{
for (ByteBuffer value : bindAndGet(values, options))
filter.add(columnDef, Operator.CONTAINS, value);
for (ByteBuffer key : bindAndGet(keys, options))
filter.add(columnDef, Operator.CONTAINS_KEY, key);
List eks = bindAndGet(entryKeys, options);
List evs = bindAndGet(entryValues, options);
assert eks.size() == evs.size();
for (int i = 0; i < eks.size(); i++)
filter.addMapEquality(columnDef, eks.get(i), Operator.EQ, evs.get(i));
}
@Override
protected boolean isSupportedBy(Index index)
{
boolean supported = false;
if (numberOfValues() > 0)
supported |= index.supportsExpression(columnDef, Operator.CONTAINS);
if (numberOfKeys() > 0)
supported |= index.supportsExpression(columnDef, Operator.CONTAINS_KEY);
if (numberOfEntries() > 0)
supported |= index.supportsExpression(columnDef, Operator.EQ);
return supported;
}
public int numberOfValues()
{
return values.size();
}
public int numberOfKeys()
{
return keys.size();
}
public int numberOfEntries()
{
return entryKeys.size();
}
@Override
public void addFunctionsTo(List functions)
{
Terms.addFunctions(values, functions);
Terms.addFunctions(keys, functions);
Terms.addFunctions(entryKeys, functions);
Terms.addFunctions(entryValues, functions);
}
@Override
public String toString()
{
return String.format("CONTAINS(values=%s, keys=%s, entryKeys=%s, entryValues=%s)", values, keys, entryKeys, entryValues);
}
@Override
public boolean hasBound(Bound b)
{
throw new UnsupportedOperationException();
}
@Override
public MultiCBuilder appendBoundTo(MultiCBuilder builder, Bound bound, QueryOptions options)
{
throw new UnsupportedOperationException();
}
@Override
public boolean isInclusive(Bound b)
{
throw new UnsupportedOperationException();
}
/**
* Binds the query options to the specified terms and returns the resulting values.
*
* @param terms the terms
* @param options the query options
* @return the value resulting from binding the query options to the specified terms
*/
private static List bindAndGet(List terms, QueryOptions options)
{
List buffers = new ArrayList<>(terms.size());
for (Term value : terms)
buffers.add(value.bindAndGet(options));
return buffers;
}
/**
* Copies the keys and value from the first Contains
to the second one.
*
* @param from the Contains
to copy from
* @param to the Contains
to copy to
*/
private static void copyKeysAndValues(ContainsRestriction from, ContainsRestriction to)
{
to.values.addAll(from.values);
to.keys.addAll(from.keys);
to.entryKeys.addAll(from.entryKeys);
to.entryValues.addAll(from.entryValues);
}
private ContainsRestriction(ColumnMetadata columnDef)
{
super(columnDef);
}
}
public static final class IsNotNullRestriction extends SingleColumnRestriction
{
public IsNotNullRestriction(ColumnMetadata columnDef)
{
super(columnDef);
}
@Override
public void addFunctionsTo(List functions)
{
}
@Override
public boolean isNotNull()
{
return true;
}
@Override
MultiColumnRestriction toMultiColumnRestriction()
{
return new MultiColumnRestriction.NotNullRestriction(Collections.singletonList(columnDef));
}
@Override
public void addRowFilterTo(RowFilter filter,
IndexRegistry indexRegistry,
QueryOptions options)
{
throw new UnsupportedOperationException("Secondary indexes do not support IS NOT NULL restrictions");
}
@Override
public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
{
throw new UnsupportedOperationException("Cannot use IS NOT NULL restriction for slicing");
}
@Override
public String toString()
{
return "IS NOT NULL";
}
@Override
public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
{
throw invalidRequest("%s cannot be restricted by a relation if it includes an IS NOT NULL", columnDef.name);
}
@Override
protected boolean isSupportedBy(Index index)
{
return index.supportsExpression(columnDef, Operator.IS_NOT);
}
}
public static final class LikeRestriction extends SingleColumnRestriction
{
private static final ByteBuffer LIKE_WILDCARD = ByteBufferUtil.bytes("%");
private final Operator operator;
private final Term value;
public LikeRestriction(ColumnMetadata columnDef, Operator operator, Term value)
{
super(columnDef);
this.operator = operator;
this.value = value;
}
@Override
public void addFunctionsTo(List functions)
{
value.addFunctionsTo(functions);
}
@Override
public boolean isEQ()
{
return false;
}
@Override
public boolean isLIKE()
{
return true;
}
@Override
public boolean canBeConvertedToMultiColumnRestriction()
{
return false;
}
@Override
MultiColumnRestriction toMultiColumnRestriction()
{
throw new UnsupportedOperationException();
}
@Override
public void addRowFilterTo(RowFilter filter,
IndexRegistry indexRegistry,
QueryOptions options)
{
Pair operation = makeSpecific(value.bindAndGet(options));
// there must be a suitable INDEX for LIKE_XXX expressions
RowFilter.SimpleExpression expression = filter.add(columnDef, operation.left, operation.right);
indexRegistry.getBestIndexFor(expression)
.orElseThrow(() -> invalidRequest("%s is only supported on properly indexed columns",
expression));
}
@Override
public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
{
// LIKE can be used with clustering columns, but as it doesn't
// represent an actual clustering value, it can't be used in a
// clustering filter.
throw new UnsupportedOperationException();
}
@Override
public String toString()
{
return operator.toString();
}
@Override
public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
{
throw invalidRequest("%s cannot be restricted by more than one relation if it includes a %s", columnDef.name, operator);
}
@Override
protected boolean isSupportedBy(Index index)
{
return index.supportsExpression(columnDef, operator);
}
/**
* As the specific subtype of LIKE (LIKE_PREFIX, LIKE_SUFFIX, LIKE_CONTAINS, LIKE_MATCHES) can only be
* determined by examining the value, which in turn can only be known after binding, all LIKE restrictions
* are initially created with the generic LIKE operator. This function takes the bound value, trims the
* wildcard '%' chars from it and returns a tuple of the inferred operator subtype and the final value
* @param value the bound value for the LIKE operation
* @return Pair containing the inferred LIKE subtype and the value with wildcards removed
*/
private static Pair makeSpecific(ByteBuffer value)
{
Operator operator;
int beginIndex = value.position();
int endIndex = value.limit() - 1;
if (ByteBufferUtil.endsWith(value, LIKE_WILDCARD))
{
if (ByteBufferUtil.startsWith(value, LIKE_WILDCARD))
{
operator = Operator.LIKE_CONTAINS;
beginIndex =+ 1;
}
else
{
operator = Operator.LIKE_PREFIX;
}
}
else if (ByteBufferUtil.startsWith(value, LIKE_WILDCARD))
{
operator = Operator.LIKE_SUFFIX;
beginIndex += 1;
endIndex += 1;
}
else
{
operator = Operator.LIKE_MATCHES;
endIndex += 1;
}
if (endIndex == 0 || beginIndex == endIndex)
throw invalidRequest("LIKE value can't be empty.");
ByteBuffer newValue = value.duplicate();
newValue.position(beginIndex);
newValue.limit(endIndex);
return Pair.create(operator, newValue);
}
}
}