blob: 0c10f13e91503a260789c73641fa3fa308704067 [file] [log] [blame]
/*
* 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.*;
import com.google.common.collect.Lists;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.statements.Bound;
import org.apache.cassandra.db.IndexExpression;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.composites.*;
import org.apache.cassandra.db.composites.Composite.EOC;
import org.apache.cassandra.db.index.SecondaryIndexManager;
import org.apache.cassandra.exceptions.InvalidRequestException;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
/**
* A set of single column restrictions on a primary key part (partition key or clustering key).
*/
final class PrimaryKeyRestrictionSet extends AbstractPrimaryKeyRestrictions
{
/**
* The restrictions.
*/
private final RestrictionSet restrictions;
/**
* <code>true</code> if the restrictions are corresponding to an EQ, <code>false</code> otherwise.
*/
private boolean eq;
/**
* <code>true</code> if the restrictions are corresponding to an IN, <code>false</code> otherwise.
*/
private boolean in;
/**
* <code>true</code> if the restrictions are corresponding to a Slice, <code>false</code> otherwise.
*/
private boolean slice;
/**
* <code>true</code> if the restrictions are corresponding to a Contains, <code>false</code> otherwise.
*/
private boolean contains;
/**
* If restrictions apply to clustering columns, we need to check whether they can be satisfied by an index lookup
* as this affects which other restrictions can legally be specified (if an index is present, we are more lenient
* about what additional filtering can be performed on the results of a lookup - see CASSANDRA-11510).
*
* We don't hold a reference to the SecondaryIndexManager itself as this is not strictly a singleton (although
* we often treat is as one), the field would also require annotation with @Unmetered to avoid blowing up the
* object size (used when calculating the size of prepared statements for caching). Instead, we refer to the
* CFMetaData and retrieve the index manager when necessary.
*
* There are a couple of scenarios where the CFM can be null (and we make sure and test for null when we use it):
* * where an empty set of restrictions are created for use in processing query results - see
* SelectStatement.forSelection
* * where the restrictions apply to partition keys and not clustering columns e.g.
* StatementRestrictions.partitionKeyRestrictions
* * in unit tests (in particular PrimaryKeyRestrictionSetTest which is primarily concerned with the correct
* generation of bounds when secondary indexes are not used).
*/
private final CFMetaData cfm;
public PrimaryKeyRestrictionSet(CType ctype)
{
this(ctype, null);
}
public PrimaryKeyRestrictionSet(CType ctype, CFMetaData cfm)
{
super(ctype);
this.cfm = cfm;
this.restrictions = new RestrictionSet();
this.eq = true;
}
private PrimaryKeyRestrictionSet(PrimaryKeyRestrictionSet primaryKeyRestrictions,
Restriction restriction) throws InvalidRequestException
{
super(primaryKeyRestrictions.ctype);
this.restrictions = primaryKeyRestrictions.restrictions.addRestriction(restriction);
this.cfm = primaryKeyRestrictions.cfm;
if (!primaryKeyRestrictions.isEmpty() && !hasSupportingIndex(restriction))
{
ColumnDefinition lastRestrictionStart = primaryKeyRestrictions.restrictions.lastRestriction().getFirstColumn();
ColumnDefinition newRestrictionStart = restriction.getFirstColumn();
checkFalse(primaryKeyRestrictions.isSlice() && newRestrictionStart.position() > lastRestrictionStart.position(),
"Clustering column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)",
newRestrictionStart.name,
lastRestrictionStart.name);
if (newRestrictionStart.position() < lastRestrictionStart.position() && restriction.isSlice())
throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)",
restrictions.nextColumn(newRestrictionStart).name,
newRestrictionStart.name);
}
if (restriction.isSlice() || primaryKeyRestrictions.isSlice())
this.slice = true;
else if (restriction.isContains() || primaryKeyRestrictions.isContains())
this.contains = true;
else if (restriction.isIN() || primaryKeyRestrictions.isIN())
this.in = true;
else
this.eq = true;
}
private boolean hasSupportingIndex(Restriction restriction)
{
return cfm != null
&& restriction.hasSupportingIndex(Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfId).indexManager);
}
@Override
public boolean isSlice()
{
return slice;
}
@Override
public boolean isEQ()
{
return eq;
}
@Override
public boolean isIN()
{
return in;
}
@Override
public boolean isOnToken()
{
return false;
}
@Override
public boolean isContains()
{
return contains;
}
@Override
public boolean isMultiColumn()
{
return false;
}
@Override
public Iterable<Function> getFunctions()
{
return restrictions.getFunctions();
}
@Override
public PrimaryKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException
{
if (restriction.isOnToken())
{
if (isEmpty())
return (PrimaryKeyRestrictions) restriction;
return new TokenFilter(this, (TokenRestriction) restriction);
}
return new PrimaryKeyRestrictionSet(this, restriction);
}
@Override
public List<Composite> valuesAsComposites(QueryOptions options) throws InvalidRequestException
{
return filterAndSort(appendTo(new CompositesBuilder(ctype), options).build());
}
@Override
public CompositesBuilder appendTo(CompositesBuilder builder, QueryOptions options)
{
for (Restriction r : restrictions)
{
r.appendTo(builder, options);
if (builder.hasMissingElements())
break;
}
return builder;
}
@Override
public CompositesBuilder appendBoundTo(CompositesBuilder builder, Bound bound, QueryOptions options)
{
throw new UnsupportedOperationException();
}
@Override
public List<Composite> boundsAsComposites(Bound bound, QueryOptions options) throws InvalidRequestException
{
CompositesBuilder builder = new CompositesBuilder(ctype);
// The end-of-component of composite doesn't depend on whether the
// component type is reversed or not (i.e. the ReversedType is applied
// to the component comparator but not to the end-of-component itself),
// it only depends on whether the slice is reversed
int keyPosition = 0;
for (Restriction r : restrictions)
{
ColumnDefinition def = r.getFirstColumn();
if (keyPosition != def.position() || r.isContains())
break;
if (r.isSlice())
{
r.appendBoundTo(builder, bound, options);
// Since CASSANDRA-7281, the composites might not end with the same components and it is possible
// that one of the composites is an empty one. Unfortunatly, AbstractCType will always sort
// Composites.EMPTY before all the other components due to its EOC, even if it is not the desired
// behaviour in some cases. To avoid that problem the code will use normal composites for the empty
// ones until the composites are properly sorted. They will then be replaced by Composites.EMPTY as
// it is what is expected by the intra-node serialization.
// It is clearly a hack but it does not make a lot of sense to refactor 2.2 for that as the problem is
// already solved in 3.0.
List<Composite> composites = filterAndSort(setEocs(r, bound, builder.build()));
return Lists.transform(composites, new com.google.common.base.Function<Composite, Composite>()
{
@Override
public Composite apply(Composite composite)
{
return composite.isEmpty() ? Composites.EMPTY: composite;
}
});
}
r.appendBoundTo(builder, bound, options);
if (builder.hasMissingElements())
return Collections.emptyList();
keyPosition = r.getLastColumn().position() + 1;
}
// Means no relation at all or everything was an equal
// Note: if the builder is "full", there is no need to use the end-of-component bit. For columns selection,
// it would be harmless to do it. However, we use this method got the partition key too. And when a query
// with 2ndary index is done, and with the the partition provided with an EQ, we'll end up here, and in that
// case using the eoc would be bad, since for the random partitioner we have no guarantee that
// prefix.end() will sort after prefix (see #5240).
EOC eoc = !builder.hasRemaining() ? EOC.NONE : (bound.isEnd() ? EOC.END : EOC.START);
return filterAndSort(builder.buildWithEOC(eoc));
}
/**
* Removes duplicates and sort the specified composites.
*
* @param composites the composites to filter and sort
* @return the composites sorted and without duplicates
*/
private List<Composite> filterAndSort(List<Composite> composites)
{
if (composites.size() <= 1)
return composites;
TreeSet<Composite> set = new TreeSet<Composite>(ctype);
set.addAll(composites);
return new ArrayList<>(set);
}
/**
* Sets EOCs for the composites returned by the specified slice restriction for the given bound.
*
* @param r the slice restriction
* @param bound the bound
* @param composites the composites
* @return the composites with their EOCs properly set
*/
private List<Composite> setEocs(Restriction r, Bound bound, List<Composite> composites)
{
List<Composite> list = new ArrayList<>(composites.size());
// The first column of the slice might not be the first clustering column (e.g. clustering_0 = ? AND (clustering_1, clustering_2) >= (?, ?)
int offset = r.getFirstColumn().position();
for (int i = 0, m = composites.size(); i < m; i++)
{
Composite composite = composites.get(i);
// Handle the no bound case
if (composite.size() == offset)
{
list.add(composite.withEOC(bound.isEnd() ? EOC.END : EOC.START));
continue;
}
// In the case of mixed order columns, we will have some extra slices where the columns change directions.
// For example: if we have clustering_0 DESC and clustering_1 ASC a slice like (clustering_0, clustering_1) > (1, 2)
// will produce 2 slices: [EMPTY, 1.START] and [1.2.END, 1.END]
// So, the END bound will return 2 composite with the same values 1
if (composite.size() <= r.getLastColumn().position() && i < m - 1 && composite.equals(composites.get(i + 1)))
{
list.add(composite.withEOC(EOC.START));
list.add(composites.get(i++).withEOC(EOC.END));
continue;
}
// Handle the normal bounds
ColumnDefinition column = r.getColumnDefs().get(composite.size() - 1 - offset);
Bound b = reverseBoundIfNeeded(column, bound);
Composite.EOC eoc = eocFor(r, bound, b);
list.add(composite.withEOC(eoc));
}
return list;
}
@Override
public List<ByteBuffer> values(QueryOptions options) throws InvalidRequestException
{
return Composites.toByteBuffers(valuesAsComposites(options));
}
@Override
public List<ByteBuffer> bounds(Bound b, QueryOptions options) throws InvalidRequestException
{
return Composites.toByteBuffers(boundsAsComposites(b, options));
}
private static Composite.EOC eocFor(Restriction r, Bound eocBound, Bound inclusiveBound)
{
if (eocBound.isStart())
return r.isInclusive(inclusiveBound) ? Composite.EOC.NONE : Composite.EOC.END;
return r.isInclusive(inclusiveBound) ? Composite.EOC.END : Composite.EOC.START;
}
@Override
public boolean hasBound(Bound b)
{
if (isEmpty())
return false;
return restrictions.lastRestriction().hasBound(b);
}
@Override
public boolean isInclusive(Bound b)
{
if (isEmpty())
return false;
return restrictions.lastRestriction().isInclusive(b);
}
@Override
public boolean hasSupportingIndex(SecondaryIndexManager indexManager)
{
return restrictions.hasSupportingIndex(indexManager);
}
@Override
public void addIndexExpressionTo(List<IndexExpression> expressions,
SecondaryIndexManager indexManager,
QueryOptions options) throws InvalidRequestException
{
Boolean clusteringColumns = null;
int position = 0;
for (Restriction restriction : restrictions)
{
ColumnDefinition columnDef = restriction.getFirstColumn();
// PrimaryKeyRestrictionSet contains only one kind of column, either partition key or clustering columns.
// Therefore we only need to check the column kind once. All the other columns will be of the same kind.
if (clusteringColumns == null)
clusteringColumns = columnDef.isClusteringColumn() ? Boolean.TRUE : Boolean.FALSE;
// We ignore all the clustering columns that can be handled by slices.
if (clusteringColumns && !restriction.isContains()&& position == columnDef.position())
{
position = restriction.getLastColumn().position() + 1;
if (!restriction.hasSupportingIndex(indexManager))
continue;
}
restriction.addIndexExpressionTo(expressions, indexManager, options);
}
}
@Override
public List<ColumnDefinition> getColumnDefs()
{
return restrictions.getColumnDefs();
}
@Override
public ColumnDefinition getFirstColumn()
{
return restrictions.firstColumn();
}
@Override
public ColumnDefinition getLastColumn()
{
return restrictions.lastColumn();
}
public final boolean needsFiltering()
{
// Backported from ClusteringColumnRestrictions from CASSANDRA-11310 for 3.6
// As that suggests, this should only be called on clustering column
// and not partition key restrictions.
int position = 0;
Restriction slice = null;
for (Restriction restriction : restrictions)
{
if (handleInFilter(restriction, position))
return true;
if (slice != null && !slice.getFirstColumn().equals(restriction.getFirstColumn()))
return true;
if (slice == null && restriction.isSlice())
slice = restriction;
else
position = restriction.getLastColumn().position() + 1;
}
return false;
}
private boolean handleInFilter(Restriction restriction, int index)
{
return restriction.isContains() || index != restriction.getFirstColumn().position();
}
}