All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cassandra.cql3.restrictions.SingleColumnRestriction Maven / Gradle / Ivy

There is a newer version: 2.2.18-2.2.18-1.170.1-rc1
Show newest version
/*
 * 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.Iterables;

import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
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.IndexExpression;
import org.apache.cassandra.db.composites.CompositesBuilder;
import org.apache.cassandra.db.index.SecondaryIndex;
import org.apache.cassandra.db.index.SecondaryIndexManager;
import org.apache.cassandra.db.marshal.CompositeType;
import org.apache.cassandra.exceptions.InvalidRequestException;
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 extends AbstractRestriction
{
    /**
     * The definition of the column to which apply the restriction.
     */
    protected final ColumnDefinition columnDef;

    public SingleColumnRestriction(ColumnDefinition columnDef)
    {
        this.columnDef = columnDef;
    }

    @Override
    public List getColumnDefs()
    {
        return Collections.singletonList(columnDef);
    }

    @Override
    public ColumnDefinition getFirstColumn()
    {
        return columnDef;
    }

    @Override
    public ColumnDefinition getLastColumn()
    {
        return columnDef;
    }

    @Override
    public boolean hasSupportingIndex(SecondaryIndexManager indexManager)
    {
        SecondaryIndex index = indexManager.getIndexForColumn(columnDef.name.bytes);
        return index != null && isSupportedBy(index);
    }

    @Override
    public final Restriction mergeWith(Restriction otherRestriction) throws InvalidRequestException
    {
        // We want to allow query like: b > ? AND (b,c) < (?, ?)
        if (otherRestriction.isMultiColumn() && canBeConvertedToMultiColumnRestriction())
        {
            return toMultiColumnRestriction().mergeWith(otherRestriction);
        }

        return doMergeWith(otherRestriction);
    }

    protected abstract Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException;

    /**
     * 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(SecondaryIndex index);

    public static final class EQ extends SingleColumnRestriction
    {
        private final Term value;

        public EQ(ColumnDefinition columnDef, Term value)
        {
            super(columnDef);
            this.value = value;
        }

        @Override
        public Iterable getFunctions()
        {
            return value.getFunctions();
        }

        @Override
        public boolean isEQ()
        {
            return true;
        }

        @Override
        MultiColumnRestriction toMultiColumnRestriction()
        {
            return new MultiColumnRestriction.EQ(Collections.singletonList(columnDef), value);
        }

        @Override
        public void addIndexExpressionTo(List expressions,
                                         SecondaryIndexManager indexManager,
                                         QueryOptions options) throws InvalidRequestException
        {
            ByteBuffer buffer = validateIndexedValue(columnDef, value.bindAndGet(options));
            expressions.add(new IndexExpression(columnDef.name.bytes, Operator.EQ, buffer));
        }

        @Override
        public CompositesBuilder appendTo(CFMetaData cfm, CompositesBuilder 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 Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
        {
            throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal", columnDef.name);
        }

        @Override
        protected boolean isSupportedBy(SecondaryIndex index)
        {
            return index.supportsOperator(Operator.EQ);
        }

        @Override
        public boolean isNotReturningAnyRows(CFMetaData cfm, QueryOptions options)
        {
            assert columnDef.isClusteringColumn();

            // Dense non-compound tables do not accept empty ByteBuffers. By consequence, we know that
            // any query with an EQ restriction containing an empty value will not return any results.
            return !cfm.comparator.isCompound() && !value.bindAndGet(options).hasRemaining();
        }
    }

    public static abstract class IN extends SingleColumnRestriction
    {
        public IN(ColumnDefinition columnDef)
        {
            super(columnDef);
        }

        @Override
        public final boolean isIN()
        {
            return true;
        }

        @Override
        public final Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
        {
            throw invalidRequest("%s cannot be restricted by more than one relation if it includes a IN", columnDef.name);
        }

        @Override
        public CompositesBuilder appendTo(CFMetaData cfm, CompositesBuilder builder, QueryOptions options)
        {
            List values = filterValuesIfNeeded(cfm, getValues(options));

            builder.addEachElementToAll(values);
            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;
        }

        private List filterValuesIfNeeded(CFMetaData cfm, List values)
        {
            if (!columnDef.isClusteringColumn() || cfm.comparator.isCompound())
                return values;

            // Dense non-compound tables do not accept empty ByteBuffers. By consequence, we know that we can
            // ignore any IN value which is an empty byte buffer an which otherwise will trigger an error.

            // As some List implementations do not support remove, we copy the list to be on the safe side.
            List filteredValues = new ArrayList<>(values.size());
            for (ByteBuffer value : values)
            {
                if (value.hasRemaining())
                    filteredValues.add(value);
            }
            return filteredValues;
        }

        @Override
        public void addIndexExpressionTo(List expressions,
                                         SecondaryIndexManager indexManager,
                                         QueryOptions options) throws InvalidRequestException
        {
            List values = getValues(options);
            checkTrue(values.size() == 1, "IN restrictions are not supported on indexed columns");

            ByteBuffer value = validateIndexedValue(columnDef, values.get(0));
            expressions.add(new IndexExpression(columnDef.name.bytes, Operator.EQ, value));
        }

        @Override
        protected final boolean isSupportedBy(SecondaryIndex index)
        {
            return index.supportsOperator(Operator.IN);
        }

        protected abstract List getValues(QueryOptions options) throws InvalidRequestException;
    }

    public static class InWithValues extends IN
    {
        protected final List values;

        public InWithValues(ColumnDefinition columnDef, List values)
        {
            super(columnDef);
            this.values = values;
        }

        @Override
        MultiColumnRestriction toMultiColumnRestriction()
        {
            return new MultiColumnRestriction.InWithValues(Collections.singletonList(columnDef), values);
        }

        @Override
        public Iterable getFunctions()
        {
            return Terms.getFunctions(values);
        }

        @Override
        protected List getValues(QueryOptions options) throws InvalidRequestException
        {
            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 InWithMarker extends IN
    {
        protected final AbstractMarker marker;

        public InWithMarker(ColumnDefinition columnDef, AbstractMarker marker)
        {
            super(columnDef);
            this.marker = marker;
        }

        @Override
        public Iterable getFunctions()
        {
            return Collections.emptySet();
        }

        @Override
        MultiColumnRestriction toMultiColumnRestriction()
        {
            return new MultiColumnRestriction.InWithMarker(Collections.singletonList(columnDef), marker);
        }

        @Override
        protected List getValues(QueryOptions options) throws InvalidRequestException
        {
            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 final class Slice extends SingleColumnRestriction
    {
        private final TermSlice slice;

        public Slice(ColumnDefinition columnDef, Bound bound, boolean inclusive, Term term)
        {
            super(columnDef);
            slice = TermSlice.newInstance(bound, inclusive, term);
        }

        @Override
        public Iterable getFunctions()
        {
            return slice.getFunctions();
        }

        @Override
        MultiColumnRestriction toMultiColumnRestriction()
        {
            return new MultiColumnRestriction.Slice(Collections.singletonList(columnDef), slice);
        }

        @Override
        public boolean isSlice()
        {
            return true;
        }

        @Override
        public CompositesBuilder appendTo(CFMetaData cfm, CompositesBuilder builder, QueryOptions options)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean hasBound(Bound b)
        {
            return slice.hasBound(b);
        }

        @Override
        public CompositesBuilder appendBoundTo(CFMetaData cfm, CompositesBuilder builder, Bound bound, QueryOptions options)
        {
            Bound b = reverseBoundIfNeeded(getFirstColumn(), bound);

            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 Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
        {
            checkTrue(otherRestriction.isSlice(),
                      "Column \"%s\" cannot be restricted by both an equality and an inequality relation",
                      columnDef.name);

            SingleColumnRestriction.Slice otherSlice = (SingleColumnRestriction.Slice) 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 Slice(columnDef,  slice.merge(otherSlice.slice));
        }

        @Override
        public void addIndexExpressionTo(List expressions,
                                         SecondaryIndexManager indexManager,
                                         QueryOptions options) throws InvalidRequestException
        {
            for (Bound b : Bound.values())
            {
                if (hasBound(b))
                {
                    ByteBuffer value = validateIndexedValue(columnDef, slice.bound(b).bindAndGet(options));
                    Operator op = slice.getIndexOperator(b);
                    // If the underlying comparator for name is reversed, we need to reverse the IndexOperator: user operation
                    // always refer to the "forward" sorting even if the clustering order is reversed, but the 2ndary code does
                    // use the underlying comparator as is.
                    op = columnDef.isReversedType() ? op.reverse() : op;
                    expressions.add(new IndexExpression(columnDef.name.bytes, op, value));
                }
            }
        }

        @Override
        protected boolean isSupportedBy(SecondaryIndex index)
        {
            return slice.isSupportedBy(index);
        }

        @Override
        public String toString()
        {
            return String.format("SLICE%s", slice);
        }

        @Override
        public boolean isNotReturningAnyRows(CFMetaData cfm, QueryOptions options)
        {
            assert columnDef.isClusteringColumn();

            // Dense non-compound tables do not accept empty ByteBuffers. By consequence, we know that
            // any query with a slice restriction with an empty value for the END bound will not return any results.
            return !cfm.comparator.isCompound()
                    && hasBound(Bound.END)
                    && !slice.bound(Bound.END).bindAndGet(options).hasRemaining();
        }

        private Slice(ColumnDefinition 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 Contains 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 Contains(ColumnDefinition columnDef, Term t, boolean isKey)
        {
            super(columnDef);
            if (isKey)
                keys.add(t);
            else
                values.add(t);
        }

        public Contains(ColumnDefinition 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 CompositesBuilder appendTo(CFMetaData cfm, CompositesBuilder builder, QueryOptions options)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean isContains()
        {
            return true;
        }

        @Override
        public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException
        {
            checkTrue(otherRestriction.isContains(),
                      "Collection column %s can only be restricted by CONTAINS, CONTAINS KEY, or map-entry equality",
                      columnDef.name);

            SingleColumnRestriction.Contains newContains = new Contains(columnDef);

            copyKeysAndValues(this, newContains);
            copyKeysAndValues((Contains) otherRestriction, newContains);

            return newContains;
        }

        @Override
        public void addIndexExpressionTo(List expressions,
                                         SecondaryIndexManager indexManager,
                                         QueryOptions options)
                                         throws InvalidRequestException
        {
            addExpressionsFor(expressions, bindAndGet(values, options), Operator.CONTAINS);
            addExpressionsFor(expressions, bindAndGet(keys, options), Operator.CONTAINS_KEY);
            addExpressionsFor(expressions, entries(options), Operator.EQ);
        }

        private void addExpressionsFor(List target, List values,
                                       Operator op) throws InvalidRequestException
        {
            for (ByteBuffer value : values)
            {
                validateIndexedValue(columnDef, value);
                target.add(new IndexExpression(columnDef.name.bytes, op, value));
            }
        }

        @Override
        protected boolean isSupportedBy(SecondaryIndex index)
        {
            boolean supported = false;

            if (numberOfValues() > 0)
                supported |= index.supportsOperator(Operator.CONTAINS);

            if (numberOfKeys() > 0)
                supported |= index.supportsOperator(Operator.CONTAINS_KEY);

            if (numberOfEntries() > 0)
                supported |= index.supportsOperator(Operator.EQ);

            return supported;
        }

        public int numberOfValues()
        {
            return values.size();
        }

        public int numberOfKeys()
        {
            return keys.size();
        }

        public int numberOfEntries()
        {
            return entryKeys.size();
        }

        @Override
        public Iterable getFunctions()
        {
            return Iterables.concat(Terms.getFunctions(values),
                                    Terms.getFunctions(keys),
                                    Terms.getFunctions(entryKeys),
                                    Terms.getFunctions(entryValues));
        }

        @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 CompositesBuilder appendBoundTo(CFMetaData cfm, CompositesBuilder builder, Bound bound, QueryOptions options)
        {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean isInclusive(Bound b)
        {
            throw new UnsupportedOperationException();
        }

        private List entries(QueryOptions options) throws InvalidRequestException
        {
            List entryBuffers = new ArrayList<>(entryKeys.size());
            List keyBuffers = bindAndGet(entryKeys, options);
            List valueBuffers = bindAndGet(entryValues, options);
            for (int i = 0; i < entryKeys.size(); i++)
            {
                if (valueBuffers.get(i) == null)
                    throw new InvalidRequestException("Unsupported null value for map-entry equality");
                entryBuffers.add(CompositeType.build(keyBuffers.get(i), valueBuffers.get(i)));
            }
            return entryBuffers;
        }

        /**
         * 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
         * @throws InvalidRequestException if a problem occurs while binding the query options
         */
        private static List bindAndGet(List terms, QueryOptions options) throws InvalidRequestException
        {
            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(Contains from, Contains to)
        {
            to.values.addAll(from.values);
            to.keys.addAll(from.keys);
            to.entryKeys.addAll(from.entryKeys);
            to.entryValues.addAll(from.entryValues);
        }

        private Contains(ColumnDefinition columnDef)
        {
            super(columnDef);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy