Allow to aggregate by time intervals

Patch by Benjamin Lerer; review by Andres De la Pena and Yifan Cai for CASSANDRA-11871

The patch allow to use pure monotonic functions on the last attribute of the GROUP BY clause and introduce some floor functions that can be use to group by time range.

A function is pure if:
    1. The function return values are identical for identical arguments
    2. The function application has no side effects

A function is monotonic if it is either entirely nonincreasing or nondecreasing.
This commit is contained in:
Benjamin Lerer 2021-11-04 16:47:16 +01:00
parent 7db3285e7b
commit 1ad8bf67a9
41 changed files with 3797 additions and 288 deletions

View File

@ -56,6 +56,8 @@ using the provided 'sstableupgrade' tool.
New features
------------
- Add support for the use of pure monotonic functions on the last attribute of the GROUP BY clause.
- Add floor functions that can be use to group by time range.
- Support for native transport rate limiting via native_transport_rate_limiting_enabled and
native_transport_max_requests_per_second in cassandra.yaml.
- Support for pre hashing passwords on CQL DCL commands

View File

@ -1100,7 +1100,7 @@ bc(syntax)..
| TOKEN '(' <identifier> ( ',' <identifer>)* ')' <op> <term>
<op> ::= '=' | '<' | '>' | '<=' | '>=' | CONTAINS | CONTAINS KEY
<group-by> ::= <identifier> (',' <identifier>)*
<group-by> ::= (<identifier>,)* (<identifier> | <function>)
<order-by> ::= <ordering> ( ',' <odering> )*
<ordering> ::= <identifer> ( ASC | DESC )?
<term-tuple> ::= '(' <term> (',' <term>)* ')'
@ -1201,7 +1201,7 @@ h4(#selectGroupBy). @<group-by>@
The @GROUP BY@ option allows to condense into a single row all selected rows that share the same values for a set of columns.
Using the @GROUP BY@ option, it is only possible to group rows at the partition key level or at a clustering column level. By consequence, the @GROUP BY@ option only accept as arguments primary key column names in the primary key order. If a primary key column is restricted by an equality restriction it is not required to be present in the @GROUP BY@ clause.
Using the @GROUP BY@ option, it is only possible to group rows at the partition key level or at a clustering column level. By consequence, the @GROUP BY@ option only accept as arguments primary key column names in the primary key order. If a primary key column is restricted by an equality restriction it is not required to be present in the @GROUP BY@ clause. The last argument can be a monotonic function on the primary key column.
Aggregate functions will produce a separate value for each group. If no @GROUP BY@ clause is specified, aggregates functions will produce a single value for all the rows.
@ -2458,9 +2458,6 @@ h3. 3.4.3
h3. 3.4.2
* Support for selecting elements and slices of a collection ("CASSANDRA-7396":https://issues.apache.org/jira/browse/CASSANDRA-7396).
h3. 3.4.2
* "@INSERT/UPDATE options@":#updateOptions for tables having a default_time_to_live specifying a TTL of 0 will remove the TTL from the inserted or updated values
* "@ALTER TABLE@":#alterTableStmt @ADD@ and @DROP@ now allow mutiple columns to be added/removed
* New "@PER PARTITION LIMIT@":#selectLimit option (see "CASSANDRA-7017":https://issues.apache.org/jira/browse/CASSANDRA-7017).

View File

@ -742,7 +742,13 @@ syntax_rules += r'''
<orderByClause> ::= [ordercol]=<cident> ( "ASC" | "DESC" )?
;
<groupByClause> ::= [groupcol]=<cident>
| <functionName><groupByFunctionArguments>
;
<groupByFunctionArguments> ::= "(" ( <groupByFunctionArgument> ( "," <groupByFunctionArgument> )* )? ")"
;
<groupByFunctionArgument> ::= [groupcol]=<cident>
| <term>
;
'''

View File

@ -267,7 +267,7 @@ selectStatement returns [SelectStatement.RawStatement expr]
Term.Raw limit = null;
Term.Raw perPartitionLimit = null;
Map<ColumnIdentifier, Boolean> orderings = new LinkedHashMap<>();
List<ColumnIdentifier> groups = new ArrayList<>();
List<Selectable.Raw> groups = new ArrayList<>();
boolean allowFiltering = false;
boolean isJson = false;
}
@ -463,8 +463,8 @@ orderByClause[Map<ColumnIdentifier, Boolean> orderings]
: c=cident (K_ASC | K_DESC { reversed = true; })? { orderings.put(c, reversed); }
;
groupByClause[List<ColumnIdentifier> groups]
: c=cident { groups.add(c); }
groupByClause[List<Selectable.Raw> groups]
: s=unaliasedSelector { groups.add(s); }
;
/**

View File

@ -27,10 +27,13 @@ import com.google.common.base.Objects;
import org.apache.cassandra.serializers.MarshalException;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
import static org.apache.commons.lang3.time.DateUtils.MILLIS_PER_DAY;
import io.netty.util.concurrent.FastThreadLocal;
/**
* Represents a duration. A durations store separately months, days, and seconds due to the fact that
* the number of days in a month varies, and a day can have 23 or 25 hours if a daylight saving is involved.
@ -45,6 +48,17 @@ public final class Duration
public static final int DAYS_PER_WEEK = 7;
public static final int MONTHS_PER_YEAR = 12;
// For some operations, like floor, a Calendar is needed if months or years are involved. Unfortunatly, creating a
// Calendar is a costly operation so instead of creating one with every call we reuse them.
private static final FastThreadLocal<Calendar> CALENDAR_PROVIDER = new FastThreadLocal<Calendar>()
{
@Override
public Calendar initialValue()
{
return Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.US);
}
};
/**
* The Regexp used to parse the duration provided as String.
*/
@ -339,7 +353,7 @@ public final class Duration
{
StringBuilder builder = new StringBuilder();
if (months < 0 || days < 0 || nanoseconds < 0)
if (isNegative())
builder.append('-');
long remainder = append(builder, Math.abs(months), MONTHS_PER_YEAR, "y");
@ -395,6 +409,124 @@ public final class Duration
return dividend % divisor;
}
/**
* Rounds a timestamp down to the closest multiple of a duration.
*
* @param timeInMillis the time to round in millisecond
* @param duration the duration
* @param startingTimeInMillis the time offset in milliseconds
* @return the timestamp rounded down to the closest multiple of the duration
*/
public static long floorTimestamp(long timeInMillis, Duration duration, long startingTimeInMillis)
{
checkFalse(startingTimeInMillis > timeInMillis, "The floor function starting time is greater than the provided time");
checkFalse(duration.isNegative(), "Negative durations are not supported by the floor function");
// If the duration does not contain any months we can ignore daylight saving,
// as time zones are not supported, and simply look at the milliseconds
if (duration.months == 0)
{
long durationInMillis = getDurationMilliseconds(duration);
// If the duration is smaller than millisecond
if (durationInMillis == 0)
return timeInMillis;
long delta = (timeInMillis - startingTimeInMillis) % durationInMillis;
return timeInMillis - delta;
}
/*
* Otherwise, we resort to Calendar for the computation.
* What we're trying to compute is the largest integer 'multiplier' value such that
* startingTimeMillis + (multiplier * duration) <= timeInMillis
* at which point we want to return 'startingTimeMillis + (multiplier * duration)'.
*
* One option would be to add 'duration' to 'statingTimeMillis' in a loop until we
* cross 'timeInMillis' and return how many iterator we did. But this might be slow if there is very many
* steps.
*
* So instead we first estimate 'multiplier' using the number of months between 'startingTimeMillis'
* and 'timeInMillis' ('durationInMonths' below) and the duration months. As the real computation
* should also take the 'days' and 'nanoseconds' parts of the duration, this multiplier may overshoot,
* so we detect it and work back from that, decreasing the multiplier until we find the proper one.
*/
Calendar calendar = CALENDAR_PROVIDER.get();
calendar.setTimeInMillis(timeInMillis);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
calendar.setTimeInMillis(startingTimeInMillis);
int startingYear = calendar.get(Calendar.YEAR);
int startingMonth = calendar.get(Calendar.MONTH);
int durationInMonths = (year - startingYear) * MONTHS_PER_YEAR + (month - startingMonth);
int multiplier = durationInMonths / duration.months;
calendar.add(Calendar.MONTH, multiplier * duration.months);
// If the duration was only containing months, we are done.
if (duration.days == 0 && duration.nanoseconds == 0)
return calendar.getTimeInMillis();
long durationInMillis = getDurationMilliseconds(duration);
long floor = calendar.getTimeInMillis() + (multiplier * durationInMillis);
// Once the milliseconds have been added we might have gone too far. If it is the case we will reduce the
// multiplier until the floor value is smaller than time in millis.
while (floor > timeInMillis)
{
multiplier--;
calendar.add(Calendar.MONTH, -duration.months);
floor = calendar.getTimeInMillis() + (multiplier * durationInMillis);
}
return Math.max(startingTimeInMillis, floor);
}
/**
* Returns the milliseconds part of the duration ignoring the month part
*
* @param duration the duration
* @return the milliseconds corresponding to the duration days and nanoseconds
*/
private static long getDurationMilliseconds(Duration duration)
{
// We can ignore daylight saving as time zones are not supported
return (duration.days * MILLIS_PER_DAY) + (duration.nanoseconds / NANOS_PER_MILLI);
}
/**
* Rounds a time down to the closest multiple of a duration.
*
* @param timeInNanos the time of day in nanoseconds
* @param duration the duration
* @return the time rounded down to the closest multiple of the duration
*/
public static long floorTime(long timeInNanos, Duration duration)
{
checkFalse(duration.isNegative(), "Negative durations are not supported by the floor function");
checkFalse(duration.getMonths() != 0 || duration.getDays() != 0 || duration.getNanoseconds() > (NANOS_PER_HOUR * 24),
"For time values, the floor can only be computed for durations smaller that a day");
if (duration.nanoseconds == 0)
return timeInNanos;
long delta = timeInNanos % duration.nanoseconds;
return timeInNanos - delta;
}
/**
* Checks if the duration is negative.
* @return {@code true} if the duration is negative, {@code false} otherwise
*/
public boolean isNegative()
{
return nanoseconds < 0 || days < 0 || months < 0;
}
private static class Builder
{
/**

View File

@ -29,6 +29,12 @@ import org.github.jamm.Unmetered;
@Unmetered
public interface Function extends AssignmentTestable
{
/**
* A marker buffer used to represent function parameters that cannot be resolved at some stage of CQL processing.
* This is used for partial function application in particular.
*/
public static final ByteBuffer UNRESOLVED = ByteBuffer.allocate(0);
public FunctionName name();
public List<AbstractType<?>> argTypes();
public AbstractType<?> returnType();
@ -36,14 +42,21 @@ public interface Function extends AssignmentTestable
/**
* Checks whether the function is a native/hard coded one or not.
*
* @return <code>true</code> if the function is a native/hard coded one, <code>false</code> otherwise.
* @return {@code true} if the function is a native/hard coded one, {@code false} otherwise.
*/
public boolean isNative();
/**
* Checks whether the function is a pure function (as in doesn't depend on, nor produces side effects) or not.
*
* @return {@code true} if the function is a pure function, {@code false} otherwise.
*/
public boolean isPure();
/**
* Checks whether the function is an aggregate function or not.
*
* @return <code>true</code> if the function is an aggregate function, <code>false</code> otherwise.
* @return {@code true} if the function is an aggregate function, {@code false} otherwise.
*/
public boolean isAggregate();

View File

@ -31,8 +31,16 @@ public abstract class NativeFunction extends AbstractFunction
super(FunctionName.nativeFunction(name), Arrays.asList(argTypes), returnType);
}
@Override
public boolean isNative()
{
return true;
}
@Override
public boolean isPure()
{
// Most of our functions are pure, the other ones should override this
return true;
}
}

View File

@ -17,6 +17,9 @@
*/
package org.apache.cassandra.cql3.functions;
import java.nio.ByteBuffer;
import java.util.List;
import org.apache.cassandra.db.marshal.AbstractType;
/**
@ -38,4 +41,17 @@ public abstract class NativeScalarFunction extends NativeFunction implements Sca
{
return false;
}
/**
* Checks if a partial application of the function is monotonic.
*
* <p>A function is monotonic if it is either entirely nonincreasing or nondecreasing.</p>
*
* @param partialParameters the input parameters used to create the partial application of the function
* @return {@code true} if the partial application of the function is monotonic {@code false} otherwise.
*/
protected boolean isPartialApplicationMonotonic(List<ByteBuffer> partialParameters)
{
return isMonotonic();
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.functions;
import java.nio.ByteBuffer;
import java.util.List;
import org.apache.cassandra.transport.ProtocolVersion;
/**
* A partial application of a function.
*
* @see ScalarFunction#partialApplication(ProtocolVersion, List)
*/
public interface PartialScalarFunction extends ScalarFunction
{
/**
* Returns the original function.
*
* @return the original function
*/
public Function getFunction();
/**
* Returns the list of input parameters for the function where some parameters can be {@link #UNRESOLVED}.
*
* @return the list of input parameters for the function
*/
public List<ByteBuffer> getPartialParameters();
}

View File

@ -0,0 +1,113 @@
/*
* 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.functions;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.apache.cassandra.cql3.CqlBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.transport.ProtocolVersion;
/**
* An internal function used to hold the partial application of another function to only some of its parameters.
*
* @see ScalarFunction#partialApplication(ProtocolVersion, List)
*/
final class PartiallyAppliedScalarFunction extends NativeScalarFunction implements PartialScalarFunction
{
private final ScalarFunction function;
private final List<ByteBuffer> partialParameters;
PartiallyAppliedScalarFunction(ScalarFunction function, List<ByteBuffer> partialParameters, int unresolvedCount)
{
// Note that we never register those function, there are just used internally, so the name doesn't matter much
super("__partial_application__", function.returnType(), computeArgTypes(function, partialParameters, unresolvedCount));
this.function = function;
this.partialParameters = partialParameters;
}
@Override
public boolean isMonotonic()
{
return function.isNative() ? ((NativeScalarFunction) function).isPartialApplicationMonotonic(partialParameters)
: function.isMonotonic();
}
@Override
public boolean isPure()
{
return function.isPure();
}
@Override
public Function getFunction()
{
return function;
}
@Override
public List<ByteBuffer> getPartialParameters()
{
return partialParameters;
}
private static AbstractType<?>[] computeArgTypes(ScalarFunction function, List<ByteBuffer> partialParameters, int unresolvedCount)
{
AbstractType<?>[] argTypes = new AbstractType<?>[unresolvedCount];
int arg = 0;
for (int i = 0; i < partialParameters.size(); i++)
{
if (partialParameters.get(i) == UNRESOLVED)
argTypes[arg++] = function.argTypes().get(i);
}
return argTypes;
}
public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException
{
List<ByteBuffer> fullParameters = new ArrayList<>(partialParameters);
int arg = 0;
for (int i = 0; i < fullParameters.size(); i++)
{
if (fullParameters.get(i) == UNRESOLVED)
fullParameters.set(i, parameters.get(arg++));
}
return function.execute(protocolVersion, fullParameters);
}
@Override
public String toString()
{
CqlBuilder b = new CqlBuilder().append(function.name()).append(" : (");
List<AbstractType<?>> types = function.argTypes();
for (int i = 0, m = types.size(); i < m; i++)
{
if (i > 0)
b.append(", ");
b.append(toCqlString(types.get(i)));
if (partialParameters.get(i) != Function.UNRESOLVED)
b.append("(constant)");
}
b.append(") -> ").append(returnType);
return b.toString();
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.functions;
import java.nio.ByteBuffer;
import java.util.List;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.transport.ProtocolVersion;
/**
* Function used internally to hold the pre-computed result of another function.
* <p>
* See {@link ScalarFunction#partialApplication(ProtocolVersion, List)} for why this is used.
* <p>
* Note : the function is cautious in keeping the protocol version used for the pre-computed value and to
* fallback to recomputation if the version we get when {@link #execute} is called. I don't think it's truly necessary
* though as I don't think we actually depend on the protocol version for values anymore (it's remnant of previous
* transitions). It's not a lot of code to be on safe side though until this is cleaned (assuming we do clean it).
*/
class PreComputedScalarFunction extends NativeScalarFunction implements PartialScalarFunction
{
private final ByteBuffer value;
private final ProtocolVersion valueVersion;
private final ScalarFunction function;
private final List<ByteBuffer> parameters;
PreComputedScalarFunction(AbstractType<?> returnType,
ByteBuffer value,
ProtocolVersion valueVersion,
ScalarFunction function,
List<ByteBuffer> parameters)
{
// Note that we never register those function, there are just used internally, so the name doesn't matter much
super("__constant__", returnType);
this.value = value;
this.valueVersion = valueVersion;
this.function = function;
this.parameters = parameters;
}
@Override
public Function getFunction()
{
return function;
}
@Override
public List<ByteBuffer> getPartialParameters()
{
return parameters;
}
public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> nothing) throws InvalidRequestException
{
if (protocolVersion == valueVersion)
return value;
return function.execute(protocolVersion, parameters);
}
public ScalarFunction partialApplication(ProtocolVersion protocolVersion, List<ByteBuffer> nothing) throws InvalidRequestException
{
return this;
}
}

View File

@ -24,12 +24,24 @@ import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.transport.ProtocolVersion;
/**
* Determines a single output value based on a single input value.
* Determines a single output value based on any number of input values.
*/
public interface ScalarFunction extends Function
{
public boolean isCalledOnNullInput();
/**
* Checks if the function is monotonic.
*
*<p>A function is monotonic if it is either entirely nonincreasing or nondecreasing given an ordered set of inputs.</p>
*
* @return {@code true} if the function is monotonic {@code false} otherwise.
*/
public default boolean isMonotonic()
{
return false;
}
/**
* Applies this function to the specified parameter.
*
@ -39,4 +51,49 @@ public interface ScalarFunction extends Function
* @throws InvalidRequestException if this function cannot not be applied to the parameter
*/
public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters) throws InvalidRequestException;
/**
* Does a partial application of the function. That is, given only some of the parameters of the function provided,
* return a new function that only expect the parameters not provided.
* <p>
* To take an example, if you consider the function
* <pre>
* text foo(int a, text b, text c, int d)
* </pre>
* then {@code foo.partialApplication([3, <ommitted>, 'bar', <omitted>])} will return a function {@code bar} of signature:
* <pre>
* text bar(text b, int d)
* </pre>
* and such that for any value of {@code b} and {@code d}, {@code bar(b, d) == foo(3, b, 'bar', d)}.
*
* @param protocolVersion protocol version used for parameters
* @param partialParameters a list of input parameters for the function where some parameters can be {@link #UNRESOLVED}.
* The input <b>must</b> be of size {@code this.argsType().size()}. For convenience, it is
* allowed both to pass a list with all parameters being {@link #UNRESOLVED} (the function is
* then returned directly) and with none of them unresolved (in which case, if the function is pure,
* it is computed and a dummy no-arg function returning the result is returned).
* @return a function corresponding to the partial application of this function to the parameters of
* {@code partialParameters} that are not {@link #UNRESOLVED}.
*/
public default ScalarFunction partialApplication(ProtocolVersion protocolVersion, List<ByteBuffer> partialParameters)
{
int unresolvedCount = 0;
for (ByteBuffer parameter : partialParameters)
{
if (parameter == UNRESOLVED)
++unresolvedCount;
}
if (unresolvedCount == argTypes().size())
return this;
if (isPure() && unresolvedCount == 0)
return new PreComputedScalarFunction(returnType(),
execute(protocolVersion, partialParameters),
protocolVersion,
this,
partialParameters);
return new PartiallyAppliedScalarFunction(this, partialParameters, unresolvedCount);
}
}

View File

@ -25,13 +25,17 @@ import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.Duration;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.UUIDGen;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
public abstract class TimeFcts
{
public static Logger logger = LoggerFactory.getLogger(TimeFcts.class);
@ -53,7 +57,14 @@ public abstract class TimeFcts
toUnixTimestamp(TimestampType.instance),
toDate(TimestampType.instance),
toUnixTimestamp(SimpleDateType.instance),
toTimestamp(SimpleDateType.instance));
toTimestamp(SimpleDateType.instance),
FloorTimestampFunction.newInstance(),
FloorTimestampFunction.newInstanceWithStartTimeArgument(),
FloorTimeUuidFunction.newInstance(),
FloorTimeUuidFunction.newInstanceWithStartTimeArgument(),
FloorDateFunction.newInstance(),
FloorDateFunction.newInstanceWithStartTimeArgument(),
floorTime);
}
public static final Function now(final String name, final TemporalType<?> type)
@ -65,6 +76,12 @@ public abstract class TimeFcts
{
return type.now();
}
@Override
public boolean isPure()
{
return false; // as it returns non-identical results for identical arguments
}
};
};
@ -161,6 +178,12 @@ public abstract class TimeFcts
long millis = type.toTimeInMillis(bb);
return SimpleDateType.instance.fromTimeInMillis(millis);
}
@Override
public boolean isMonotonic()
{
return true;
}
};
}
@ -182,6 +205,12 @@ public abstract class TimeFcts
long millis = type.toTimeInMillis(bb);
return TimestampType.instance.fromTimeInMillis(millis);
}
@Override
public boolean isMonotonic()
{
return true;
}
};
}
@ -202,7 +231,282 @@ public abstract class TimeFcts
return ByteBufferUtil.bytes(type.toTimeInMillis(bb));
}
@Override
public boolean isMonotonic()
{
return true;
}
};
}
}
/**
* Function that rounds a timestamp down to the closest multiple of a duration.
*/
private static abstract class FloorFunction extends NativeScalarFunction
{
private static final Long ZERO = Long.valueOf(0);
protected FloorFunction(AbstractType<?> returnType,
AbstractType<?>... argsType)
{
super("floor", returnType, argsType);
// The function can accept either 2 parameters (time and duration) or 3 parameters (time, duration and startTime)r
assert argsType.length == 2 || argsType.length == 3;
}
@Override
protected boolean isPartialApplicationMonotonic(List<ByteBuffer> partialParameters)
{
return partialParameters.get(0) == UNRESOLVED
&& partialParameters.get(1) != UNRESOLVED
&& (partialParameters.size() == 2 || partialParameters.get(2) != UNRESOLVED);
}
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
{
ByteBuffer timeBuffer = parameters.get(0);
ByteBuffer durationBuffer = parameters.get(1);
Long startingTime = getStartingTime(parameters);
if (timeBuffer == null || durationBuffer == null || startingTime == null)
return null;
Long time = toTimeInMillis(timeBuffer);
Duration duration = DurationType.instance.compose(durationBuffer);
if (time == null || duration == null)
return null;
validateDuration(duration);
long floor = Duration.floorTimestamp(time, duration, startingTime);
return fromTimeInMillis(floor);
}
/**
* Returns the time to use as the starting time.
*
* @param parameters the function parameters
* @return the time to use as the starting time
*/
private Long getStartingTime(List<ByteBuffer> parameters)
{
if (parameters.size() == 3)
{
ByteBuffer startingTimeBuffer = parameters.get(2);
if (startingTimeBuffer == null)
return null;
return toStartingTimeInMillis(startingTimeBuffer);
}
return ZERO;
}
/**
* Validates that the duration has the correct precision.
* @param duration the duration to validate.
*/
protected void validateDuration(Duration duration)
{
if (!duration.hasMillisecondPrecision())
throw invalidRequest("The floor cannot be computed for the %s duration as precision is below 1 millisecond", duration);
}
/**
* Serializes the specified time.
*
* @param timeInMillis the time in milliseconds
* @return the serialized time
*/
protected abstract ByteBuffer fromTimeInMillis(long timeInMillis);
/**
* Deserializes the specified input time.
*
* @param bytes the serialized time
* @return the time in milliseconds
*/
protected abstract Long toTimeInMillis(ByteBuffer bytes);
/**
* Deserializes the specified starting time.
*
* @param bytes the serialized starting time
* @return the starting time in milliseconds
*/
protected abstract Long toStartingTimeInMillis(ByteBuffer bytes);
}
/**
* Function that rounds a timestamp down to the closest multiple of a duration.
*/
public static final class FloorTimestampFunction extends FloorFunction
{
public static FloorTimestampFunction newInstance()
{
return new FloorTimestampFunction(TimestampType.instance,
TimestampType.instance,
DurationType.instance);
}
public static FloorTimestampFunction newInstanceWithStartTimeArgument()
{
return new FloorTimestampFunction(TimestampType.instance,
TimestampType.instance,
DurationType.instance,
TimestampType.instance);
}
private FloorTimestampFunction(AbstractType<?> returnType,
AbstractType<?>... argTypes)
{
super(returnType, argTypes);
}
protected ByteBuffer fromTimeInMillis(long timeInMillis)
{
return TimestampType.instance.fromTimeInMillis(timeInMillis);
}
protected Long toStartingTimeInMillis(ByteBuffer bytes)
{
return TimestampType.instance.toTimeInMillis(bytes);
}
protected Long toTimeInMillis(ByteBuffer bytes)
{
return TimestampType.instance.toTimeInMillis(bytes);
}
}
/**
* Function that rounds a timeUUID down to the closest multiple of a duration.
*/
public static final class FloorTimeUuidFunction extends FloorFunction
{
public static FloorTimeUuidFunction newInstance()
{
return new FloorTimeUuidFunction(TimestampType.instance,
TimeUUIDType.instance,
DurationType.instance);
}
public static FloorTimeUuidFunction newInstanceWithStartTimeArgument()
{
return new FloorTimeUuidFunction(TimestampType.instance,
TimeUUIDType.instance,
DurationType.instance,
TimestampType.instance);
}
private FloorTimeUuidFunction(AbstractType<?> returnType,
AbstractType<?>... argTypes)
{
super(returnType, argTypes);
}
protected ByteBuffer fromTimeInMillis(long timeInMillis)
{
return TimestampType.instance.fromTimeInMillis(timeInMillis);
}
protected Long toStartingTimeInMillis(ByteBuffer bytes)
{
return TimestampType.instance.toTimeInMillis(bytes);
}
protected Long toTimeInMillis(ByteBuffer bytes)
{
return UUIDGen.getAdjustedTimestamp(UUIDGen.getUUID(bytes));
}
}
/**
* Function that rounds a date down to the closest multiple of a duration.
*/
public static final class FloorDateFunction extends FloorFunction
{
public static FloorDateFunction newInstance()
{
return new FloorDateFunction(SimpleDateType.instance,
SimpleDateType.instance,
DurationType.instance);
}
public static FloorDateFunction newInstanceWithStartTimeArgument()
{
return new FloorDateFunction(SimpleDateType.instance,
SimpleDateType.instance,
DurationType.instance,
SimpleDateType.instance);
}
private FloorDateFunction(AbstractType<?> returnType,
AbstractType<?>... argTypes)
{
super(returnType, argTypes);
}
protected ByteBuffer fromTimeInMillis(long timeInMillis)
{
return SimpleDateType.instance.fromTimeInMillis(timeInMillis);
}
protected Long toStartingTimeInMillis(ByteBuffer bytes)
{
return SimpleDateType.instance.toTimeInMillis(bytes);
}
protected Long toTimeInMillis(ByteBuffer bytes)
{
return SimpleDateType.instance.toTimeInMillis(bytes);
}
@Override
protected void validateDuration(Duration duration)
{
// Checks that the duration has no data below days.
if (duration.getNanoseconds() != 0)
throw invalidRequest("The floor on %s values cannot be computed for the %s duration as precision is below 1 day",
SimpleDateType.instance.asCQL3Type(), duration);
}
}
/**
* Function that rounds a time down to the closest multiple of a duration.
*/
public static final NativeScalarFunction floorTime = new NativeScalarFunction("floor", TimeType.instance, TimeType.instance, DurationType.instance)
{
@Override
protected boolean isPartialApplicationMonotonic(List<ByteBuffer> partialParameters)
{
return partialParameters.get(0) == UNRESOLVED && partialParameters.get(1) != UNRESOLVED;
}
public ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
{
ByteBuffer timeBuffer = parameters.get(0);
ByteBuffer durationBuffer = parameters.get(1);
if (timeBuffer == null || durationBuffer == null)
return null;
Long time = TimeType.instance.compose(timeBuffer);
Duration duration = DurationType.instance.compose(durationBuffer);
if (time == null || duration == null)
return null;
long floor = Duration.floorTime(time, duration);
return TimeType.instance.decompose(floor);
}
};
}

View File

@ -100,6 +100,12 @@ public class UDAggregate extends AbstractFunction implements AggregateFunction,
.orElseThrow(() -> new ConfigurationException(String.format("Unable to find function %s referenced by UDA %s", name, udaName)));
}
public boolean isPure()
{
// Right now, we have no way to check if an UDA is pure. Due to that we consider them as non pure to avoid any risk.
return false;
}
public boolean hasReferenceTo(Function function)
{
return stateFunction == function || finalFunction == function;

View File

@ -368,6 +368,12 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct
return builder.toString();
}
public boolean isPure()
{
// Right now, we have no way to check if an UDF is pure. Due to that we consider them as non pure to avoid any risk.
return false;
}
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
{
assertUdfsEnabled(language);

View File

@ -17,24 +17,93 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import com.google.common.base.Objects;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.text.StrBuilder;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.FunctionName;
import org.apache.cassandra.cql3.functions.PartialScalarFunction;
import org.apache.cassandra.cql3.functions.ScalarFunction;
import org.apache.cassandra.cql3.statements.RequestValidations;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import static java.util.stream.Collectors.joining;
abstract class AbstractFunctionSelector<T extends Function> extends Selector
{
protected static abstract class AbstractFunctionSelectorDeserializer extends SelectorDeserializer
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
FunctionName name = new FunctionName(in.readUTF(), in.readUTF());
int numberOfArguments = (int) in.readUnsignedVInt();
List<AbstractType<?>> argTypes = new ArrayList<>(numberOfArguments);
for (int i = 0; i < numberOfArguments; i++)
{
argTypes.add(readType(metadata, in));
}
Optional<Function> optional = Schema.instance.findFunction(name, argTypes);
if (!optional.isPresent())
throw new IOException(String.format("Unknown serialized function %s(%s)",
name,
argTypes.stream()
.map(p -> p.asCQL3Type().toString())
.collect(joining(", "))));
Function function = optional.get();
boolean isPartial = in.readBoolean();
if (isPartial)
{
int bitset = (int) in.readUnsignedVInt();
List<ByteBuffer> partialParameters = new ArrayList<>(numberOfArguments);
for (int i = 0; i < numberOfArguments; i++)
{
ByteBuffer parameter = ((bitset & 1) == 1) ? ByteBufferUtil.readWithVIntLength(in)
: Function.UNRESOLVED;
partialParameters.add(parameter);
bitset >>= 1;
}
function = ((ScalarFunction) function).partialApplication(ProtocolVersion.CURRENT, partialParameters);
}
int numberOfRemainingArguments = (int) in.readUnsignedVInt();
List<Selector> argSelectors = new ArrayList<>(numberOfRemainingArguments);
for (int i = 0; i < numberOfRemainingArguments; i++)
{
argSelectors.add(Selector.serializer.deserialize(in, version, metadata));
}
return newFunctionSelector(function, argSelectors);
}
protected abstract Selector newFunctionSelector(Function function, List<Selector> argSelectors);
};
protected final T fun;
/**
@ -88,7 +157,45 @@ abstract class AbstractFunctionSelector<T extends Function> extends Selector
public Selector newInstance(QueryOptions options) throws InvalidRequestException
{
return fun.isAggregate() ? new AggregateFunctionSelector(fun, factories.newInstances(options))
: new ScalarFunctionSelector(fun, factories.newInstances(options));
: createScalarSelector(options, (ScalarFunction) fun, factories.newInstances(options));
}
private Selector createScalarSelector(QueryOptions options, ScalarFunction function, List<Selector> argSelectors)
{
ProtocolVersion version = options.getProtocolVersion();
int terminalCount = 0;
List<ByteBuffer> terminalArgs = new ArrayList<>(argSelectors.size());
for (Selector selector : argSelectors)
{
if (selector.isTerminal())
{
++terminalCount;
ByteBuffer output = selector.getOutput(version);
RequestValidations.checkBindValueSet(output, "Invalid unset value for argument in call to function %s", fun.name().name);
terminalArgs.add(output);
}
else
{
terminalArgs.add(Function.UNRESOLVED);
}
}
if (terminalCount == 0)
return new ScalarFunctionSelector(fun, argSelectors);
// All terminal, reduce to a simple value if the function is pure
if (terminalCount == argSelectors.size() && function.isPure())
return new TermSelector(function.execute(version, terminalArgs), function.returnType());
// We have some terminal arguments but not all, do a partial application
ScalarFunction partialFunction = function.partialApplication(version, terminalArgs);
List<Selector> remainingSelectors = new ArrayList<>(argSelectors.size() - terminalCount);
for (Selector selector : argSelectors)
{
if (!selector.isTerminal())
remainingSelectors.add(selector);
}
return new ScalarFunctionSelector(partialFunction, remainingSelectors);
}
public boolean isWritetimeSelectorFactory()
@ -121,8 +228,9 @@ abstract class AbstractFunctionSelector<T extends Function> extends Selector
};
}
protected AbstractFunctionSelector(T fun, List<Selector> argSelectors)
protected AbstractFunctionSelector(Kind kind, T fun, List<Selector> argSelectors)
{
super(kind);
this.fun = fun;
this.argSelectors = argSelectors;
this.args = Arrays.asList(new ByteBuffer[argSelectors.size()]);
@ -153,6 +261,28 @@ abstract class AbstractFunctionSelector<T extends Function> extends Selector
return fun.returnType();
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof AbstractFunctionSelector))
return false;
AbstractFunctionSelector<?> s = (AbstractFunctionSelector<?>) o;
return Objects.equal(fun.name(), s.fun.name())
&& Objects.equal(fun.argTypes(), s.fun.argTypes())
&& Objects.equal(argSelectors, s.argSelectors);
}
@Override
public int hashCode()
{
return Objects.hashCode(fun.name(), fun.argTypes(), argSelectors);
}
@Override
public String toString()
{
@ -162,4 +292,97 @@ abstract class AbstractFunctionSelector<T extends Function> extends Selector
.append(")")
.toString();
}
@Override
protected int serializedSize(int version)
{
boolean isPartial = fun instanceof PartialScalarFunction;
Function function = isPartial ? ((PartialScalarFunction) fun).getFunction() : fun;
FunctionName name = function.name();
int size = TypeSizes.sizeof(name.keyspace) + TypeSizes.sizeof(name.name);
List<AbstractType<?>> argTypes = function.argTypes();
size += TypeSizes.sizeofUnsignedVInt(argTypes.size());
for (int i = 0, m = argTypes.size(); i < m; i++)
{
size += sizeOf(argTypes.get(i));
}
size += TypeSizes.sizeof(isPartial);
if (isPartial)
{
List<ByteBuffer> partialParameters = ((PartialScalarFunction) fun).getPartialParameters();
// We use a bitset to track the position of the unresolved arguments
size += TypeSizes.sizeofUnsignedVInt(computeBitSet(partialParameters));
for (int i = 0, m = partialParameters.size(); i < m; i++)
{
ByteBuffer buffer = partialParameters.get(i);
if (buffer != Function.UNRESOLVED)
size += ByteBufferUtil.serializedSizeWithVIntLength(buffer);
}
}
int numberOfRemainingArguments = argSelectors.size();
size += TypeSizes.sizeofUnsignedVInt(numberOfRemainingArguments);
for (int i = 0; i < numberOfRemainingArguments; i++)
size += serializer.serializedSize(argSelectors.get(i), version);
return size;
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
boolean isPartial = fun instanceof PartialScalarFunction;
Function function = isPartial ? ((PartialScalarFunction) fun).getFunction() : fun;
FunctionName name = function.name();
out.writeUTF(name.keyspace);
out.writeUTF(name.name);
List<AbstractType<?>> argTypes = function.argTypes();
int numberOfArguments = argTypes.size();
out.writeUnsignedVInt(numberOfArguments);
for (int i = 0; i < numberOfArguments; i++)
writeType(out, argTypes.get(i));
out.writeBoolean(isPartial);
if (isPartial)
{
List<ByteBuffer> partialParameters = ((PartialScalarFunction) fun).getPartialParameters();
// We use a bitset to track the position of the unresolved arguments
out.writeUnsignedVInt(computeBitSet(partialParameters));
for (int i = 0, m = partialParameters.size(); i < m; i++)
{
ByteBuffer buffer = partialParameters.get(i);
if (buffer != Function.UNRESOLVED)
ByteBufferUtil.writeWithVIntLength(buffer, out);
}
}
int numberOfRemainingArguments = argSelectors.size();
out.writeUnsignedVInt(numberOfRemainingArguments);
for (int i = 0; i < numberOfRemainingArguments; i++)
serializer.serialize(argSelectors.get(i), out, version);
}
private int computeBitSet(List<ByteBuffer> partialParameters)
{
assert partialParameters.size() <= 32 : "cannot serialize partial function with more than 32 parameters";
int bitset = 0;
for (int i = 0, m = partialParameters.size(); i < m; i++)
{
if (partialParameters.get(i) != Function.UNRESOLVED)
bitset |= 1 << i;
}
return bitset;
}
}

View File

@ -27,6 +27,15 @@ import org.apache.cassandra.transport.ProtocolVersion;
final class AggregateFunctionSelector extends AbstractFunctionSelector<AggregateFunction>
{
protected static final SelectorDeserializer deserializer = new AbstractFunctionSelectorDeserializer()
{
@Override
protected Selector newFunctionSelector(Function function, List<Selector> argSelectors)
{
return new AggregateFunctionSelector(function, argSelectors);
}
};
private final AggregateFunction.Aggregate aggregate;
public boolean isAggregate()
@ -34,13 +43,13 @@ final class AggregateFunctionSelector extends AbstractFunctionSelector<Aggregate
return true;
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
// Aggregation of aggregation is not supported
for (int i = 0, m = argSelectors.size(); i < m; i++)
{
Selector s = argSelectors.get(i);
s.addInput(protocolVersion, rs);
s.addInput(protocolVersion, input);
setArg(i, s.getOutput(protocolVersion));
s.reset();
}
@ -59,7 +68,7 @@ final class AggregateFunctionSelector extends AbstractFunctionSelector<Aggregate
AggregateFunctionSelector(Function fun, List<Selector> argSelectors) throws InvalidRequestException
{
super((AggregateFunction) fun, argSelectors);
super(Kind.AGGREGATE_FUNCTION_SELECTOR, (AggregateFunction) fun, argSelectors);
this.aggregate = this.fun.newAggregate();
}

View File

@ -17,17 +17,24 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.base.Objects;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.Term;
import org.apache.cassandra.cql3.selection.SimpleSelector.SimpleSelectorFactory;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
@ -38,8 +45,9 @@ abstract class ElementsSelector extends Selector
{
protected final Selector selected;
protected ElementsSelector(Selector selected)
protected ElementsSelector(Kind kind,Selector selected)
{
super(kind);
this.selected = selected;
}
@ -210,7 +218,7 @@ abstract class ElementsSelector extends Selector
};
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
ByteBuffer value = selected.getOutput(protocolVersion);
return value == null ? null : extractSelection(value);
@ -218,9 +226,9 @@ abstract class ElementsSelector extends Selector
protected abstract ByteBuffer extractSelection(ByteBuffer collection);
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
selected.addInput(protocolVersion, rs);
selected.addInput(protocolVersion, input);
}
public void reset()
@ -228,14 +236,31 @@ abstract class ElementsSelector extends Selector
selected.reset();
}
private static class ElementSelector extends ElementsSelector
@Override
public boolean isTerminal()
{
return selected.isTerminal();
}
static class ElementSelector extends ElementsSelector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
Selector selected = Selector.serializer.deserialize(in, version, metadata);
ByteBuffer key = ByteBufferUtil.readWithVIntLength(in);
return new ElementSelector(selected, key);
}
};
private final CollectionType<?> type;
private final ByteBuffer key;
private ElementSelector(Selector selected, ByteBuffer key)
{
super(selected);
super(Kind.ELEMENT_SELECTOR, selected);
assert selected.getType() instanceof MapType || selected.getType() instanceof SetType : "this shouldn't have passed validation in Selectable";
this.type = (CollectionType<?>) selected.getType();
this.key = key;
@ -269,10 +294,60 @@ abstract class ElementsSelector extends Selector
{
return String.format("%s[%s]", selected, keyType(type).getString(key));
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof ElementSelector))
return false;
ElementSelector s = (ElementSelector) o;
return Objects.equal(selected, s.selected)
&& Objects.equal(key, s.key);
}
@Override
public int hashCode()
{
return Objects.hashCode(selected, key);
}
@Override
protected int serializedSize(int version)
{
return TypeSizes.sizeofWithVIntLength(key) + serializer.serializedSize(selected, version);
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
serializer.serialize(selected, out, version);
ByteBufferUtil.serializedSizeWithVIntLength(key);
}
}
private static class SliceSelector extends ElementsSelector
static class SliceSelector extends ElementsSelector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
Selector selected = Selector.serializer.deserialize(in, version, metadata);
boolean isFromUnset = in.readBoolean();
ByteBuffer from = isFromUnset ? ByteBufferUtil.UNSET_BYTE_BUFFER : ByteBufferUtil.readWithVIntLength(in);
boolean isToUnset = in.readBoolean();
ByteBuffer to = isToUnset ? ByteBufferUtil.UNSET_BYTE_BUFFER : ByteBufferUtil.readWithVIntLength(in);
return new SliceSelector(selected, from, to);
}
};
private final CollectionType<?> type;
// Note that neither from nor to can be null, but they can both be ByteBufferUtil.UNSET_BYTE_BUFFER to represent no particular bound
@ -281,7 +356,7 @@ abstract class ElementsSelector extends Selector
private SliceSelector(Selector selected, ByteBuffer from, ByteBuffer to)
{
super(selected);
super(Kind.SLICE_SELECTOR, selected);
assert selected.getType() instanceof MapType || selected.getType() instanceof SetType : "this shouldn't have passed validation in Selectable";
assert from != null && to != null : "We can have unset buffers, but not nulls";
this.type = (CollectionType<?>) selected.getType();
@ -321,5 +396,57 @@ abstract class ElementsSelector extends Selector
? selected.toString()
: String.format("%s[%s..%s]", selected, fromUnset ? "" : keyType(type).getString(from), toUnset ? "" : keyType(type).getString(to));
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof SliceSelector))
return false;
SliceSelector s = (SliceSelector) o;
return Objects.equal(selected, s.selected)
&& Objects.equal(from, s.from)
&& Objects.equal(to, s.to);
}
@Override
public int hashCode()
{
return Objects.hashCode(selected, from, to);
}
@Override
protected int serializedSize(int version)
{
int size = serializer.serializedSize(selected, version) + 2;
if (!isUnset(from))
size += TypeSizes.sizeofWithVIntLength(from);
if (!isUnset(to))
size += TypeSizes.sizeofWithVIntLength(to);
return size;
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
serializer.serialize(selected, out, version);
boolean isFromUnset = isUnset(from);
out.writeBoolean(isFromUnset);
if (!isFromUnset)
ByteBufferUtil.serializedSizeWithVIntLength(from);
boolean isToUnset = isUnset(to);
out.writeBoolean(isToUnset);
if (!isToUnset)
ByteBufferUtil.serializedSizeWithVIntLength(to);
}
}
}

View File

@ -17,18 +17,37 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.base.Objects;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.UserType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.transport.ProtocolVersion;
final class FieldSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
UserType type = (UserType) readType(metadata, in);
int field = (int) in.readUnsignedVInt();
Selector selected = Selector.serializer.deserialize(in, version, metadata);
return new FieldSelector(type, field, selected);
}
};
private final UserType type;
private final int field;
private final Selector selected;
@ -79,12 +98,12 @@ final class FieldSelector extends Selector
selected.addFetchedColumns(builder);
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
selected.addInput(protocolVersion, rs);
selected.addInput(protocolVersion, input);
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
ByteBuffer value = selected.getOutput(protocolVersion);
if (value == null)
@ -103,6 +122,12 @@ final class FieldSelector extends Selector
selected.reset();
}
@Override
public boolean isTerminal()
{
return selected.isTerminal();
}
@Override
public String toString()
{
@ -111,8 +136,45 @@ final class FieldSelector extends Selector
private FieldSelector(UserType type, int field, Selector selected)
{
super(Kind.FIELD_SELECTOR);
this.type = type;
this.field = field;
this.selected = selected;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof FieldSelector))
return false;
FieldSelector s = (FieldSelector) o;
return Objects.equal(type, s.type)
&& Objects.equal(field, s.field)
&& Objects.equal(selected, s.selected);
}
@Override
public int hashCode()
{
return Objects.hashCode(type, field, selected);
}
@Override
protected int serializedSize(int version)
{
return sizeOf(type) + TypeSizes.sizeofUnsignedVInt(field) + serializer.serializedSize(selected, version);
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
writeType(out, type);
out.writeUnsignedVInt(field);
serializer.serialize(selected, out, version);
}
}

View File

@ -17,15 +17,22 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import com.google.common.base.Objects;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.Lists;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter.Builder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.serializers.CollectionSerializer;
import org.apache.cassandra.transport.ProtocolVersion;
@ -35,6 +42,20 @@ import org.apache.cassandra.transport.ProtocolVersion;
*/
final class ListSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
ListType<?> type = (ListType<?>) readType(metadata, in);
int size = (int) in.readUnsignedVInt();
List<Selector> elements = new ArrayList<>(size);
for (int i = 0; i < size; i++)
elements.add(serializer.deserialize(in, version, metadata));
return new ListSelector(type, elements);
}
};
/**
* The list type.
*/
@ -68,13 +89,13 @@ final class ListSelector extends Selector
elements.get(i).addFetchedColumns(builder);
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
for (int i = 0, m = elements.size(); i < m; i++)
elements.get(i).addInput(protocolVersion, rs);
elements.get(i).addInput(protocolVersion, input);
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
List<ByteBuffer> buffers = new ArrayList<>(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
@ -90,6 +111,17 @@ final class ListSelector extends Selector
elements.get(i).reset();
}
@Override
public boolean isTerminal()
{
for (int i = 0, m = elements.size(); i < m; i++)
{
if (!elements.get(i).isTerminal())
return false;
}
return true;
}
public AbstractType<?> getType()
{
return type;
@ -103,7 +135,48 @@ final class ListSelector extends Selector
private ListSelector(AbstractType<?> type, List<Selector> elements)
{
super(Kind.LIST_SELECTOR);
this.type = type;
this.elements = elements;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof ListSelector))
return false;
ListSelector s = (ListSelector) o;
return Objects.equal(type, s.type)
&& Objects.equal(elements, s.elements);
}
@Override
public int hashCode()
{
return Objects.hashCode(type, elements);
}
@Override
protected int serializedSize(int version)
{
int size = sizeOf(type) + TypeSizes.sizeofUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
size += serializer.serializedSize(elements.get(i), version);
return size;
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
writeType(out, type);
out.writeUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
serializer.serialize(elements.get(i), out, version);
}
}

View File

@ -17,6 +17,7 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@ -24,15 +25,21 @@ import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import com.google.common.base.Objects;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.Maps;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter.Builder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.serializers.CollectionSerializer;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.Pair;
@ -43,6 +50,23 @@ import org.apache.cassandra.utils.Pair;
*/
final class MapSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
MapType<?, ?> type = (MapType<?, ?>) readType(metadata, in);
int size = (int) in.readUnsignedVInt();
List<Pair<Selector, Selector>> entries = new ArrayList<>(size);
for (int i = 0; i < size; i++)
{
Pair<Selector, Selector> entry = Pair.create(serializer.deserialize(in, version, metadata),
serializer.deserialize(in, version, metadata));
entries.add(entry);
}
return new MapSelector(type, entries);
}
};
/**
* The map type.
*/
@ -170,17 +194,17 @@ final class MapSelector extends Selector
}
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
for (int i = 0, m = elements.size(); i < m; i++)
{
Pair<Selector, Selector> pair = elements.get(i);
pair.left.addInput(protocolVersion, rs);
pair.right.addInput(protocolVersion, rs);
pair.left.addInput(protocolVersion, input);
pair.right.addInput(protocolVersion, input);
}
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
Map<ByteBuffer, ByteBuffer> map = new TreeMap<>(type.getKeysType());
for (int i = 0, m = elements.size(); i < m; i++)
@ -208,6 +232,18 @@ final class MapSelector extends Selector
}
}
@Override
public boolean isTerminal()
{
for (int i = 0, m = elements.size(); i < m; i++)
{
Pair<Selector, Selector> pair = elements.get(i);
if (!pair.left.isTerminal() || !pair.right.isTerminal())
return false;
}
return true;
}
public AbstractType<?> getType()
{
return type;
@ -221,7 +257,58 @@ final class MapSelector extends Selector
private MapSelector(AbstractType<?> type, List<Pair<Selector, Selector>> elements)
{
super(Kind.MAP_SELECTOR);
this.type = (MapType<?, ?>) type;
this.elements = elements;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof MapSelector))
return false;
MapSelector s = (MapSelector) o;
return Objects.equal(type, s.type)
&& Objects.equal(elements, s.elements);
}
@Override
public int hashCode()
{
return Objects.hashCode(type, elements);
}
@Override
protected int serializedSize(int version)
{
int size = sizeOf(type) + TypeSizes.sizeofUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
{
Pair<Selector, Selector> entry = elements.get(i);
size += serializer.serializedSize(entry.left, version) + serializer.serializedSize(entry.right, version);
}
return size;
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
writeType(out, type);
out.writeUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
{
Pair<Selector, Selector> entry = elements.get(i);
serializer.serialize(entry.left, out, version);
serializer.serialize(entry.right, out, version);
}
}
}

View File

@ -19,7 +19,6 @@ package org.apache.cassandra.cql3.selection;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.cassandra.config.Config;
@ -29,9 +28,7 @@ import org.apache.cassandra.cql3.selection.Selection.Selectors;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.aggregation.GroupMaker;
import org.apache.cassandra.db.context.CounterContext;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.utils.ByteBufferUtil;
public final class ResultSetBuilder
{
@ -50,15 +47,8 @@ public final class ResultSetBuilder
/*
* We'll build CQL3 row one by one.
* The currentRow is the values for the (CQL3) columns we've fetched.
* We also collect timestamps and ttls for the case where the writetime and
* ttl functions are used. Note that we might collect timestamp and/or ttls
* we don't care about, but since the array below are allocated just once,
* it doesn't matter performance wise.
*/
List<ByteBuffer> current;
final long[] timestamps;
final int[] ttls;
private Selector.InputRow inputRow;
private long size = 0;
private boolean sizeWarningEmitted = false;
@ -73,14 +63,6 @@ public final class ResultSetBuilder
this.resultSet = new ResultSet(metadata.copy(), new ArrayList<List<ByteBuffer>>());
this.selectors = selectors;
this.groupMaker = groupMaker;
this.timestamps = selectors.collectTimestamps() ? new long[selectors.numberOfFetchedColumns()] : null;
this.ttls = selectors.collectTTLs() ? new int[selectors.numberOfFetchedColumns()] : null;
// We use MIN_VALUE to indicate no timestamp and -1 for no ttl
if (timestamps != null)
Arrays.fill(timestamps, Long.MIN_VALUE);
if (ttls != null)
Arrays.fill(ttls, -1);
}
private void addSize(List<ByteBuffer> row)
@ -114,40 +96,12 @@ public final class ResultSetBuilder
public void add(ByteBuffer v)
{
current.add(v);
inputRow.add(v);
}
public void add(Cell<?> c, int nowInSec)
{
if (c == null)
{
current.add(null);
return;
}
current.add(value(c));
if (timestamps != null)
timestamps[current.size() - 1] = c.timestamp();
if (ttls != null)
ttls[current.size() - 1] = remainingTTL(c, nowInSec);
}
private int remainingTTL(Cell<?> c, int nowInSec)
{
if (!c.isExpiring())
return -1;
int remaining = c.localDeletionTime() - nowInSec;
return remaining >= 0 ? remaining : -1;
}
private <V> ByteBuffer value(Cell<V> c)
{
return c.isCounterCell()
? ByteBufferUtil.bytes(CounterContext.instance().total(c.value(), c.accessor()))
: c.buffer();
inputRow.add(c, nowInSec);
}
/**
@ -160,22 +114,24 @@ public final class ResultSetBuilder
{
// The groupMaker needs to be called for each row
boolean isNewAggregate = groupMaker == null || groupMaker.isNewGroup(partitionKey, clustering);
if (current != null)
if (inputRow != null)
{
selectors.addInputRow(this);
selectors.addInputRow(inputRow);
if (isNewAggregate)
{
resultSet.addRow(getOutputRow());
inputRow.reset(!selectors.hasProcessing());
selectors.reset();
}
else
{
inputRow.reset(!selectors.hasProcessing());
}
}
else
{
inputRow = new Selector.InputRow(selectors.numberOfFetchedColumns(), selectors.collectTimestamps(), selectors.collectTTLs());
}
current = new ArrayList<>(selectors.numberOfFetchedColumns());
// Timestamps and TTLs are arrays per row, we must null them out between rows
if (timestamps != null)
Arrays.fill(timestamps, Long.MIN_VALUE);
if (ttls != null)
Arrays.fill(ttls, -1);
}
/**
@ -183,12 +139,12 @@ public final class ResultSetBuilder
*/
public ResultSet build()
{
if (current != null)
if (inputRow != null)
{
selectors.addInputRow(this);
selectors.addInputRow(inputRow);
resultSet.addRow(getOutputRow());
inputRow.reset(!selectors.hasProcessing());
selectors.reset();
current = null;
}
// For aggregates we need to return a row even it no records have been found

View File

@ -22,17 +22,27 @@ import java.util.List;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.ScalarFunction;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.transport.ProtocolVersion;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
final class ScalarFunctionSelector extends AbstractFunctionSelector<ScalarFunction>
{
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
protected static final SelectorDeserializer deserializer = new AbstractFunctionSelectorDeserializer()
{
@Override
protected Selector newFunctionSelector(Function function, List<Selector> argSelectors)
{
return new ScalarFunctionSelector(function, argSelectors);
}
};
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
for (int i = 0, m = argSelectors.size(); i < m; i++)
{
Selector s = argSelectors.get(i);
s.addInput(protocolVersion, rs);
s.addInput(protocolVersion, input);
}
}
@ -40,7 +50,7 @@ final class ScalarFunctionSelector extends AbstractFunctionSelector<ScalarFuncti
{
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
for (int i = 0, m = argSelectors.size(); i < m; i++)
{
@ -51,8 +61,16 @@ final class ScalarFunctionSelector extends AbstractFunctionSelector<ScalarFuncti
return fun.execute(protocolVersion, args());
}
@Override
public void validateForGroupBy()
{
checkTrue(fun.isMonotonic(), "Only monotonic functions are supported in the GROUP BY clause. Got: %s ", fun);
for (int i = 0, m = argSelectors.size(); i < m; i++)
argSelectors.get(i).validateForGroupBy();
}
ScalarFunctionSelector(Function fun, List<Selector> argSelectors)
{
super((ScalarFunction) fun, argSelectors);
super(Kind.SCALAR_FUNCTION_SELECTOR, (ScalarFunction) fun, argSelectors);
}
}

View File

@ -28,6 +28,7 @@ import com.google.common.collect.Lists;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.selection.Selector.InputRow;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.InvalidRequestException;
@ -345,6 +346,12 @@ public abstract class Selection
*/
public ColumnFilter getColumnFilter();
/**
* Checks if this Selectors perform some processing
* @return {@code true} if this Selectors perform some processing, {@code false} otherwise.
*/
public boolean hasProcessing();
/**
* Checks if one of the selectors perform some aggregations.
* @return {@code true} if one of the selectors perform some aggregations, {@code false} otherwise.
@ -372,10 +379,9 @@ public abstract class Selection
/**
* Adds the current row of the specified <code>ResultSetBuilder</code>.
*
* @param rs the <code>ResultSetBuilder</code>
* @throws InvalidRequestException
* @param input the input row
*/
public void addInputRow(ResultSetBuilder rs);
public void addInputRow(InputRow input);
public List<ByteBuffer> getOutputRow();
@ -467,9 +473,9 @@ public abstract class Selection
return current;
}
public void addInputRow(ResultSetBuilder rs) throws InvalidRequestException
public void addInputRow(InputRow input)
{
current = rs.current;
current = input.getValues();
}
public boolean isAggregate()
@ -477,6 +483,11 @@ public abstract class Selection
return false;
}
public boolean hasProcessing()
{
return false;
}
@Override
public int numberOfFetchedColumns()
{
@ -572,6 +583,11 @@ public abstract class Selection
return factories.doesAggregation();
}
public boolean hasProcessing()
{
return true;
}
public List<ByteBuffer> getOutputRow()
{
List<ByteBuffer> outputRow = new ArrayList<>(selectors.size());
@ -582,10 +598,10 @@ public abstract class Selection
return isJson ? rowToJson(outputRow, options.getProtocolVersion(), metadata, orderingColumns) : outputRow;
}
public void addInputRow(ResultSetBuilder rs) throws InvalidRequestException
public void addInputRow(InputRow input)
{
for (Selector selector : selectors)
selector.addInput(options.getProtocolVersion(), rs);
selector.addInput(options.getProtocolVersion(), input);
}
@Override

View File

@ -17,18 +17,31 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.apache.cassandra.schema.CQLTypeParser;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.context.CounterContext;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
/**
* A <code>Selector</code> is used to convert the data returned by the storage engine into the data requested by the
@ -38,6 +51,50 @@ import org.apache.cassandra.transport.ProtocolVersion;
*/
public abstract class Selector
{
protected static abstract class SelectorDeserializer
{
protected abstract Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException;
protected final AbstractType<?> readType(TableMetadata metadata, DataInputPlus in) throws IOException
{
KeyspaceMetadata keyspace = Schema.instance.getKeyspaceMetadata(metadata.keyspace);
return readType(keyspace, in);
}
protected final AbstractType<?> readType(KeyspaceMetadata keyspace, DataInputPlus in) throws IOException
{
String cqlType = in.readUTF();
return CQLTypeParser.parse(keyspace.name, cqlType, keyspace.types);
}
}
/**
* The <code>Selector</code> kinds.
*/
public static enum Kind
{
SIMPLE_SELECTOR(SimpleSelector.deserializer),
TERM_SELECTOR(TermSelector.deserializer),
WRITETIME_OR_TTL_SELECTOR(WritetimeOrTTLSelector.deserializer),
LIST_SELECTOR(ListSelector.deserializer),
SET_SELECTOR(SetSelector.deserializer),
MAP_SELECTOR(MapSelector.deserializer),
TUPLE_SELECTOR(TupleSelector.deserializer),
USER_TYPE_SELECTOR(UserTypeSelector.deserializer),
FIELD_SELECTOR(FieldSelector.deserializer),
SCALAR_FUNCTION_SELECTOR(ScalarFunctionSelector.deserializer),
AGGREGATE_FUNCTION_SELECTOR(AggregateFunctionSelector.deserializer),
ELEMENT_SELECTOR(ElementsSelector.ElementSelector.deserializer),
SLICE_SELECTOR(ElementsSelector.SliceSelector.deserializer);
private final SelectorDeserializer deserializer;
Kind(SelectorDeserializer deserializer)
{
this.deserializer = deserializer;
}
}
/**
* A factory for <code>Selector</code> instances.
*/
@ -70,7 +127,7 @@ public abstract class Selector
* depends on the bound values in particular).
* @return a new <code>Selector</code> instance
*/
public abstract Selector newInstance(QueryOptions options) throws InvalidRequestException;
public abstract Selector newInstance(QueryOptions options);
/**
* Checks if this factory creates selectors instances that creates aggregates.
@ -174,6 +231,50 @@ public abstract class Selector
abstract void addFetchedColumns(ColumnFilter.Builder builder);
}
public static class Serializer
{
public void serialize(Selector selector, DataOutputPlus out, int version) throws IOException
{
out.writeByte(selector.kind().ordinal());
selector.serialize(out, version);
}
public Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
Kind kind = Kind.values()[in.readUnsignedByte()];
return kind.deserializer.deserialize(in, version, metadata);
}
public int serializedSize(Selector selector, int version)
{
return TypeSizes.sizeof((byte) selector.kind().ordinal()) + selector.serializedSize(version);
}
}
/**
* The {@code Selector} serializer.
*/
public static final Serializer serializer = new Serializer();
/**
* The {@code Selector} kind.
*/
private final Kind kind;
/**
* Returns the {@code Selector} kind.
* @return the {@code Selector} kind
*/
public final Kind kind()
{
return kind;
}
protected Selector(Kind kind)
{
this.kind = kind;
}
/**
* Add to the provided builder the column (and potential subselections) to fetch for this
* selection.
@ -182,14 +283,158 @@ public abstract class Selector
*/
public abstract void addFetchedColumns(ColumnFilter.Builder builder);
/**
* A row of data that need to be processed by a {@code Selector}
*/
public static final class InputRow
{
private ByteBuffer[] values;
private final long[] timestamps;
private final int[] ttls;
private int index;
public InputRow(int size, boolean collectTimestamps, boolean collectTTLs)
{
this.values = new ByteBuffer[size];
if (collectTimestamps)
{
this.timestamps = new long[size];
// We use MIN_VALUE to indicate no timestamp
Arrays.fill(timestamps, Long.MIN_VALUE);
}
else
{
timestamps = null;
}
if (collectTTLs)
{
this.ttls = new int[size];
// We use -1 to indicate no ttl
Arrays.fill(ttls, -1);
}
else
{
ttls = null;
}
}
public void add(ByteBuffer v)
{
values[index] = v;
if (timestamps != null)
timestamps[index] = Long.MIN_VALUE;
if (ttls != null)
ttls[index] = -1;
index++;
}
public void add(Cell<?> c, int nowInSec)
{
if (c == null)
{
add(null);
return;
}
values[index] = value(c);
if (timestamps != null)
timestamps[index] = c.timestamp();
if (ttls != null)
ttls[index] = remainingTTL(c, nowInSec);
index++;
}
private int remainingTTL(Cell<?> c, int nowInSec)
{
if (!c.isExpiring())
return -1;
int remaining = c.localDeletionTime() - nowInSec;
return remaining >= 0 ? remaining : -1;
}
private <V> ByteBuffer value(Cell<V> c)
{
return c.isCounterCell()
? ByteBufferUtil.bytes(CounterContext.instance().total(c.value(), c.accessor()))
: c.buffer();
}
/**
* Return the value of the column with the specified index.
*
* @param index the column index
* @return the value of the column with the specified index
*/
public ByteBuffer getValue(int index)
{
return values[index];
}
/**
* Reset the row internal state.
* <p>If the reset is not a deep one only the index will be reset. If the reset is a deep one a new
* array will be created to store the column values. This allow to reduce object creation when it is not
* necessary.</p>
*
* @param deep {@code true} if the reset must be a deep one.
*/
public void reset(boolean deep)
{
index = 0;
if (deep)
values = new ByteBuffer[values.length];
}
/**
* Return the timestamp of the column with the specified index.
*
* @param index the column index
* @return the timestamp of the column with the specified index
*/
public long getTimestamp(int index)
{
return timestamps[index];
}
/**
* Return the ttl of the column with the specified index.
*
* @param index the column index
* @return the ttl of the column with the specified index
*/
public int getTtl(int index)
{
return ttls[index];
}
/**
* Returns the column values as list.
* <p>This content of the list will be shared with the {@code InputRow} unless a deep reset has been done.</p>
* @return the column values as list.
*/
public List<ByteBuffer> getValues()
{
return Arrays.asList(values);
}
}
/**
* Add the current value from the specified <code>ResultSetBuilder</code>.
*
* @param protocolVersion protocol version used for serialization
* @param rs the <code>ResultSetBuilder</code>
* @throws InvalidRequestException if a problem occurs while add the input value
* @param input the input row
* @throws InvalidRequestException if a problem occurs while adding the input row
*/
public abstract void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException;
public abstract void addInput(ProtocolVersion protocolVersion, InputRow input);
/**
* Returns the selector output.
@ -211,4 +456,36 @@ public abstract class Selector
* Reset the internal state of this <code>Selector</code>.
*/
public abstract void reset();
/**
* A selector is terminal if it doesn't require any input for it's output to be computed, i.e. if {@link #getOutput}
* result doesn't depend of {@link #addInput}. This is typically the case of a constant value or functions on constant
* values.
*/
public boolean isTerminal()
{
return false;
}
/**
* Checks that this selector is valid for GROUP BY clause.
*/
public void validateForGroupBy()
{
throw invalidRequest("Only column names and monotonic scalar functions are supported in the GROUP BY clause.");
}
protected abstract int serializedSize(int version);
protected abstract void serialize(DataOutputPlus out, int version) throws IOException;
protected static void writeType(DataOutputPlus out, AbstractType<?> type) throws IOException
{
out.writeUTF(type.asCQL3Type().toString());
}
protected static int sizeOf(AbstractType<?> type)
{
return TypeSizes.sizeof(type.asCQL3Type().toString());
}
}

View File

@ -17,17 +17,24 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import com.google.common.base.Objects;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.Sets;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter.Builder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.SetType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.serializers.CollectionSerializer;
import org.apache.cassandra.transport.ProtocolVersion;
@ -37,6 +44,20 @@ import org.apache.cassandra.transport.ProtocolVersion;
*/
final class SetSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
SetType<?> type = (SetType<?>) readType(metadata, in);
int size = (int) in.readUnsignedVInt();
List<Selector> elements = new ArrayList<>(size);
for (int i = 0; i < size; i++)
elements.add(serializer.deserialize(in, version, metadata));
return new SetSelector(type, elements);
}
};
/**
* The set type.
*/
@ -70,13 +91,13 @@ final class SetSelector extends Selector
elements.get(i).addFetchedColumns(builder);
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
for (int i = 0, m = elements.size(); i < m; i++)
elements.get(i).addInput(protocolVersion, rs);
elements.get(i).addInput(protocolVersion, input);
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
Set<ByteBuffer> buffers = new TreeSet<>(type.getElementsType());
for (int i = 0, m = elements.size(); i < m; i++)
@ -92,6 +113,17 @@ final class SetSelector extends Selector
elements.get(i).reset();
}
@Override
public boolean isTerminal()
{
for (int i = 0, m = elements.size(); i < m; i++)
{
if (!elements.get(i).isTerminal())
return false;
}
return true;
}
public AbstractType<?> getType()
{
return type;
@ -105,7 +137,49 @@ final class SetSelector extends Selector
private SetSelector(AbstractType<?> type, List<Selector> elements)
{
super(Kind.SET_SELECTOR);
this.type = (SetType<?>) type;
this.elements = elements;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof SetSelector))
return false;
SetSelector s = (SetSelector) o;
return Objects.equal(type, s.type)
&& Objects.equal(elements, s.elements);
}
@Override
public int hashCode()
{
return Objects.hashCode(type, elements);
}
@Override
protected int serializedSize(int version)
{
int size = sizeOf(type) + TypeSizes.sizeofUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
size += serializer.serializedSize(elements.get(i), version);
return size;
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
writeType(out, type);
out.writeUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
serializer.serialize(elements.get(i), out, version);
}
}

View File

@ -17,19 +17,38 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.base.Objects;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.filter.ColumnFilter.Builder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
public final class SimpleSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
ByteBuffer columnName = ByteBufferUtil.readWithVIntLength(in);
ColumnMetadata column = metadata.getColumn(columnName);
int idx = in.readInt();
return new SimpleSelector(column, idx);
}
};
/**
* The Factory for {@code SimpleSelector}.
*/
@ -113,17 +132,17 @@ public final class SimpleSelector extends Selector
}
@Override
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input) throws InvalidRequestException
{
if (!isSet)
{
isSet = true;
current = rs.current.get(idx);
current = input.getValue(idx);
}
}
@Override
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
return current;
}
@ -149,7 +168,48 @@ public final class SimpleSelector extends Selector
private SimpleSelector(ColumnMetadata column, int idx)
{
super(Kind.SIMPLE_SELECTOR);
this.column = column;
this.idx = idx;
}
@Override
public void validateForGroupBy()
{
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof SimpleSelector))
return false;
SimpleSelector s = (SimpleSelector) o;
return Objects.equal(column, s.column)
&& Objects.equal(idx, s.idx);
}
@Override
public int hashCode()
{
return Objects.hashCode(column, idx);
}
@Override
protected int serializedSize(int version)
{
return ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes)
+ TypeSizes.sizeof(idx);
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
ByteBufferUtil.writeWithVIntLength(column.name.bytes, out);
out.writeInt(idx);;
}
}

View File

@ -17,16 +17,22 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.base.Objects;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.Term;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
/**
* Selector representing a simple term (literals or bound variables).
@ -36,6 +42,16 @@ import org.apache.cassandra.transport.ProtocolVersion;
*/
public class TermSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
AbstractType<?> type = readType(metadata, in);
ByteBuffer value = ByteBufferUtil.readWithVIntLength(in);
return new TermSelector(value, type);
}
};
private final ByteBuffer value;
private final AbstractType<?> type;
@ -74,8 +90,9 @@ public class TermSelector extends Selector
};
}
private TermSelector(ByteBuffer value, AbstractType<?> type)
TermSelector(ByteBuffer value, AbstractType<?> type)
{
super(Kind.TERM_SELECTOR);
this.value = value;
this.type = type;
}
@ -84,11 +101,11 @@ public class TermSelector extends Selector
{
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
return value;
}
@ -101,4 +118,44 @@ public class TermSelector extends Selector
public void reset()
{
}
@Override
public boolean isTerminal()
{
return true;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof TermSelector))
return false;
TermSelector s = (TermSelector) o;
return Objects.equal(value, s.value)
&& Objects.equal(type, s.type);
}
@Override
public int hashCode()
{
return Objects.hashCode(value, type);
}
@Override
protected int serializedSize(int version)
{
return sizeOf(type) + ByteBufferUtil.serializedSizeWithVIntLength(value);
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
writeType(out, type);
ByteBufferUtil.writeWithVIntLength(value, out);
}
}

View File

@ -17,15 +17,23 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import com.google.common.base.Objects;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.Tuples;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter.Builder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.TupleType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.transport.ProtocolVersion;
/**
@ -34,6 +42,20 @@ import org.apache.cassandra.transport.ProtocolVersion;
*/
final class TupleSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
AbstractType<?> type = readType(metadata, in);
int size = (int) in.readUnsignedVInt();
List<Selector> elements = new ArrayList<>(size);
for (int i = 0; i < size; i++)
elements.add(serializer.deserialize(in, version, metadata));
return new TupleSelector(type, elements);
}
};
/**
* The tuple type.
*/
@ -67,10 +89,10 @@ final class TupleSelector extends Selector
elements.get(i).addFetchedColumns(builder);
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
for (int i = 0, m = elements.size(); i < m; i++)
elements.get(i).addInput(protocolVersion, rs);
elements.get(i).addInput(protocolVersion, input);
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
@ -89,6 +111,17 @@ final class TupleSelector extends Selector
elements.get(i).reset();
}
@Override
public boolean isTerminal()
{
for (int i = 0, m = elements.size(); i < m; i++)
{
if (!elements.get(i).isTerminal())
return false;
}
return true;
}
public AbstractType<?> getType()
{
return type;
@ -102,7 +135,50 @@ final class TupleSelector extends Selector
private TupleSelector(AbstractType<?> type, List<Selector> elements)
{
super(Kind.TUPLE_SELECTOR);
this.type = type;
this.elements = elements;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof TupleSelector))
return false;
TupleSelector s = (TupleSelector) o;
return Objects.equal(type, s.type)
&& Objects.equal(elements, s.elements);
}
@Override
public int hashCode()
{
return Objects.hashCode(type, elements);
}
@Override
protected int serializedSize(int version)
{
int size = sizeOf(type) + TypeSizes.sizeofUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
size += serializer.serializedSize(elements.get(i), version);
return size;
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
writeType(out, type);
out.writeUnsignedVInt(elements.size());
for (int i = 0, m = elements.size(); i < m; i++)
serializer.serialize(elements.get(i), out, version);
}
}

View File

@ -17,25 +17,32 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.google.common.base.Objects;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.FieldIdentifier;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.UserTypes;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.filter.ColumnFilter.Builder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.TupleType;
import org.apache.cassandra.db.marshal.UserType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
/**
* <code>Selector</code> for literal map (e.g. {'min' : min(value), 'max' : max(value), 'count' : count(value)}).
@ -43,6 +50,23 @@ import org.apache.cassandra.transport.ProtocolVersion;
*/
final class UserTypeSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
UserType type = (UserType) readType(metadata, in);
int size = (int) in.readUnsignedVInt();
Map<FieldIdentifier, Selector> fields = new HashMap<>(size);
for (int i = 0; i < size; i++)
{
FieldIdentifier identifier = new FieldIdentifier(ByteBufferUtil.readWithVIntLength(in));
Selector selector = serializer.deserialize(in, version, metadata);
fields.put(identifier, selector);
}
return new UserTypeSelector(type, fields);
}
};
/**
* The map type.
*/
@ -158,13 +182,13 @@ final class UserTypeSelector extends Selector
field.addFetchedColumns(builder);
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs) throws InvalidRequestException
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
for (Selector field : fields.values())
field.addInput(protocolVersion, rs);
field.addInput(protocolVersion, input);
}
public ByteBuffer getOutput(ProtocolVersion protocolVersion) throws InvalidRequestException
public ByteBuffer getOutput(ProtocolVersion protocolVersion)
{
UserType userType = (UserType) type;
ByteBuffer[] buffers = new ByteBuffer[userType.size()];
@ -183,6 +207,17 @@ final class UserTypeSelector extends Selector
field.reset();
}
@Override
public boolean isTerminal()
{
for (Selector field : fields.values())
{
if(!field.isTerminal())
return false;
}
return true;
}
public AbstractType<?> getType()
{
return type;
@ -196,7 +231,53 @@ final class UserTypeSelector extends Selector
private UserTypeSelector(AbstractType<?> type, Map<FieldIdentifier, Selector> fields)
{
super(Kind.USER_TYPE_SELECTOR);
this.type = type;
this.fields = fields;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof UserTypeSelector))
return false;
UserTypeSelector s = (UserTypeSelector) o;
return Objects.equal(type, s.type)
&& Objects.equal(fields, s.fields);
}
@Override
public int hashCode()
{
return Objects.hashCode(type, fields);
}
@Override
protected int serializedSize(int version)
{
int size = sizeOf(type) + TypeSizes.sizeofUnsignedVInt(fields.size());
for (Map.Entry<FieldIdentifier, Selector> field : fields.entrySet())
size += ByteBufferUtil.serializedSizeWithVIntLength(field.getKey().bytes) + serializer.serializedSize(field.getValue(), version);
return size;
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
writeType(out, type);
out.writeUnsignedVInt(fields.size());
for (Map.Entry<FieldIdentifier, Selector> field : fields.entrySet())
{
ByteBufferUtil.writeWithVIntLength(field.getKey().bytes, out);
serializer.serialize(field.getValue(), out, version);
}
}
}

View File

@ -17,20 +17,39 @@
*/
package org.apache.cassandra.cql3.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.base.Objects;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.LongType;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
final class WritetimeOrTTLSelector extends Selector
{
protected static final SelectorDeserializer deserializer = new SelectorDeserializer()
{
protected Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
ByteBuffer columnName = ByteBufferUtil.readWithVIntLength(in);
ColumnMetadata column = metadata.getColumn(columnName);
int idx = in.readInt();
boolean isWritetime = in.readBoolean();
return new WritetimeOrTTLSelector(column, idx, isWritetime);
}
};
private final ColumnMetadata column;
private final int idx;
private final boolean isWritetime;
@ -88,7 +107,7 @@ final class WritetimeOrTTLSelector extends Selector
builder.add(column);
}
public void addInput(ProtocolVersion protocolVersion, ResultSetBuilder rs)
public void addInput(ProtocolVersion protocolVersion, InputRow input)
{
if (isSet)
return;
@ -97,12 +116,12 @@ final class WritetimeOrTTLSelector extends Selector
if (isWritetime)
{
long ts = rs.timestamps[idx];
long ts = input.getTimestamp(idx);
current = ts != Long.MIN_VALUE ? ByteBufferUtil.bytes(ts) : null;
}
else
{
int ttl = rs.ttls[idx];
int ttl = input.getTtl(idx);
current = ttl > 0 ? ByteBufferUtil.bytes(ttl) : null;
}
}
@ -131,8 +150,47 @@ final class WritetimeOrTTLSelector extends Selector
private WritetimeOrTTLSelector(ColumnMetadata column, int idx, boolean isWritetime)
{
super(Kind.WRITETIME_OR_TTL_SELECTOR);
this.column = column;
this.idx = idx;
this.isWritetime = isWritetime;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof WritetimeOrTTLSelector))
return false;
WritetimeOrTTLSelector s = (WritetimeOrTTLSelector) o;
return Objects.equal(column, s.column)
&& Objects.equal(idx, s.idx)
&& Objects.equal(isWritetime, s.isWritetime);
}
@Override
public int hashCode()
{
return Objects.hashCode(column, idx, isWritetime);
}
@Override
protected int serializedSize(int version)
{
return ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes)
+ TypeSizes.sizeof(idx)
+ TypeSizes.sizeof(isWritetime);
}
@Override
protected void serialize(DataOutputPlus out, int version) throws IOException
{
ByteBufferUtil.writeWithVIntLength(column.name.bytes, out);
out.writeInt(idx);
out.writeBoolean(isWritetime);
}
}

View File

@ -19,6 +19,7 @@ package org.apache.cassandra.cql3.statements;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.stream.Collectors;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
@ -42,8 +43,10 @@ import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
import org.apache.cassandra.cql3.selection.RawSelector;
import org.apache.cassandra.cql3.selection.ResultSetBuilder;
import org.apache.cassandra.cql3.selection.Selectable;
import org.apache.cassandra.cql3.selection.Selectable.WithFunction;
import org.apache.cassandra.cql3.selection.Selection;
import org.apache.cassandra.cql3.selection.Selection.Selectors;
import org.apache.cassandra.cql3.selection.Selector;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.aggregation.AggregationSpecification;
import org.apache.cassandra.db.aggregation.GroupMaker;
@ -80,6 +83,7 @@ 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.checkNull;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
import static org.apache.cassandra.utils.ByteBufferUtil.UNSET_BYTE_BUFFER;
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
@ -110,9 +114,9 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
private final boolean isReversed;
/**
* The <code>AggregationSpecification</code> used to make the aggregates.
* The {@code Factory} used to create the {@code AggregationSpecification}.
*/
private final AggregationSpecification aggregationSpec;
private final AggregationSpecification.Factory aggregationSpecFactory;
/**
* The comparator used to orders results when multiple keys are selected (using IN).
@ -132,7 +136,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
Selection selection,
StatementRestrictions restrictions,
boolean isReversed,
AggregationSpecification aggregationSpec,
AggregationSpecification.Factory aggregationSpecFactory,
Comparator<List<ByteBuffer>> orderingComparator,
Term limit,
Term perPartitionLimit)
@ -142,7 +146,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
this.selection = selection;
this.restrictions = restrictions;
this.isReversed = isReversed;
this.aggregationSpec = aggregationSpec;
this.aggregationSpecFactory = aggregationSpecFactory;
this.orderingComparator = orderingComparator;
this.parameters = parameters;
this.limit = limit;
@ -174,6 +178,9 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
selection.addFunctionsTo(functions);
restrictions.addFunctionsTo(functions);
if (aggregationSpecFactory != null)
aggregationSpecFactory.addFunctionsTo(functions);
if (limit != null)
limit.addFunctionsTo(functions);
@ -248,14 +255,15 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
int pageSize = options.getPageSize();
Selectors selectors = selection.newSelectors(options);
AggregationSpecification aggregationSpec = getAggregationSpec(options);
ReadQuery query = getQuery(options, state.getClientState(), selectors.getColumnFilter(),
nowInSec, userLimit, userPerPartitionLimit, pageSize);
nowInSec, userLimit, userPerPartitionLimit, pageSize, aggregationSpec);
if (options.isReadThresholdsEnabled())
query.trackWarnings();
if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize)))
return execute(query, options, state.getClientState(), selectors, nowInSec, userLimit, queryStartNanoTime);
return execute(query, options, state.getClientState(), selectors, nowInSec, userLimit, null, queryStartNanoTime);
QueryPager pager = getPager(query, options);
@ -266,9 +274,16 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
pageSize,
nowInSec,
userLimit,
aggregationSpec,
queryStartNanoTime);
}
public AggregationSpecification getAggregationSpec(QueryOptions options)
{
return aggregationSpecFactory == null ? null : aggregationSpecFactory.newInstance(options);
}
public ReadQuery getQuery(QueryOptions options, int nowInSec) throws RequestValidationException
{
Selectors selectors = selection.newSelectors(options);
@ -278,7 +293,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
nowInSec,
getLimit(options),
getPerPartitionLimit(options),
options.getPageSize());
options.getPageSize(),
getAggregationSpec(options));
}
public ReadQuery getQuery(QueryOptions options,
@ -287,11 +303,12 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
int nowInSec,
int userLimit,
int perPartitionLimit,
int pageSize)
int pageSize,
AggregationSpecification aggregationSpec)
{
boolean isPartitionRangeQuery = restrictions.isKeyRange() || restrictions.usesSecondaryIndexing();
DataLimits limit = getDataLimits(userLimit, perPartitionLimit, pageSize);
DataLimits limit = getDataLimits(userLimit, perPartitionLimit, pageSize, aggregationSpec);
if (isPartitionRangeQuery)
return getRangeCommand(options, state, columnFilter, limit, nowInSec);
@ -304,11 +321,13 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
ClientState state,
Selectors selectors,
int nowInSec,
int userLimit, long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
int userLimit,
AggregationSpecification aggregationSpec,
long queryStartNanoTime)
{
try (PartitionIterator data = query.execute(options.getConsistency(), state, queryStartNanoTime))
{
return processResults(data, options, selectors, nowInSec, userLimit);
return processResults(data, options, selectors, nowInSec, userLimit, aggregationSpec);
}
}
@ -392,11 +411,12 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
int pageSize,
int nowInSec,
int userLimit,
long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
AggregationSpecification aggregationSpec,
long queryStartNanoTime)
{
Guardrails.pageSize.guard(pageSize, table(), false, state.getClientState());
if (aggregationSpec != null)
if (aggregationSpecFactory != null)
{
if (!restrictions.hasPartitionKeyRestrictions())
{
@ -417,7 +437,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
ResultMessage.Rows msg;
try (PartitionIterator page = pager.fetchPage(pageSize, queryStartNanoTime))
{
msg = processResults(page, options, selectors, nowInSec, userLimit);
msg = processResults(page, options, selectors, nowInSec, userLimit, aggregationSpec);
}
// Please note that the isExhausted state of the pager only gets updated when we've closed the page, so this
@ -438,9 +458,10 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
QueryOptions options,
Selectors selectors,
int nowInSec,
int userLimit) throws RequestValidationException
int userLimit,
AggregationSpecification aggregationSpec) throws RequestValidationException
{
ResultSet rset = process(partitions, options, selectors, nowInSec, userLimit);
ResultSet rset = process(partitions, options, selectors, nowInSec, userLimit, aggregationSpec);
return new ResultMessage.Rows(rset);
}
@ -449,15 +470,25 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
return executeInternal(state, options, options.getNowInSeconds(state), nanoTime());
}
public ResultMessage.Rows executeInternal(QueryState state, QueryOptions options, int nowInSec, long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
public ResultMessage.Rows executeInternal(QueryState state,
QueryOptions options,
int nowInSec,
long queryStartNanoTime)
{
int userLimit = getLimit(options);
int userPerPartitionLimit = getPerPartitionLimit(options);
int pageSize = options.getPageSize();
Selectors selectors = selection.newSelectors(options);
ReadQuery query = getQuery(options, state.getClientState(), selectors.getColumnFilter(), nowInSec, userLimit,
userPerPartitionLimit, pageSize);
AggregationSpecification aggregationSpec = getAggregationSpec(options);
ReadQuery query = getQuery(options,
state.getClientState(),
selectors.getColumnFilter(),
nowInSec,
userLimit,
userPerPartitionLimit,
pageSize,
aggregationSpec);
try (ReadExecutionController executionController = query.executionController())
{
@ -465,7 +496,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
{
try (PartitionIterator data = query.executeInternal(executionController))
{
return processResults(data, options, selectors, nowInSec, userLimit);
return processResults(data, options, selectors, nowInSec, userLimit, null);
}
}
@ -478,6 +509,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
pageSize,
nowInSec,
userLimit,
aggregationSpec,
queryStartNanoTime);
}
}
@ -486,7 +518,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
{
QueryPager pager = query.getPager(options.getPagingState(), options.getProtocolVersion());
if (aggregationSpec == null || query.isEmpty())
if (aggregationSpecFactory == null || query.isEmpty())
return pager;
return new AggregationQueryPager(pager, query.limits());
@ -498,11 +530,11 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
int userPerPartitionLimit = getPerPartitionLimit(options);
if (options.getPageSize() > 0)
throw new IllegalStateException();
if (aggregationSpec != null)
if (aggregationSpecFactory != null)
throw new IllegalStateException();
Selectors selectors = selection.newSelectors(options);
ReadQuery query = getQuery(options, state, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, Integer.MAX_VALUE);
ReadQuery query = getQuery(options, state, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, Integer.MAX_VALUE, null);
Map<DecoratedKey, List<Row>> result = Collections.emptyMap();
try (ReadExecutionController executionController = query.executionController())
@ -540,7 +572,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
{
QueryOptions options = QueryOptions.DEFAULT;
Selectors selectors = selection.newSelectors(options);
return process(partitions, options, selectors, nowInSec, getLimit(options));
return process(partitions, options, selectors, nowInSec, getLimit(options), getAggregationSpec(options));
}
@Override
@ -735,7 +767,10 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
return builder.build();
}
private DataLimits getDataLimits(int userLimit, int perPartitionLimit, int pageSize)
private DataLimits getDataLimits(int userLimit,
int perPartitionLimit,
int pageSize,
AggregationSpecification aggregationSpec)
{
int cqlRowLimit = DataLimits.NO_LIMIT;
int cqlPerPartitionLimit = DataLimits.NO_LIMIT;
@ -842,7 +877,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
QueryOptions options,
Selectors selectors,
int nowInSec,
int userLimit) throws InvalidRequestException
int userLimit,
AggregationSpecification aggregationSpec) throws InvalidRequestException
{
GroupMaker groupMaker = aggregationSpec == null ? null : aggregationSpec.newGroupMaker();
ResultSetBuilder result = new ResultSetBuilder(getResultMetadata(), selectors, groupMaker);
@ -1089,12 +1125,14 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
validateDistinctSelection(table, selection, restrictions);
}
AggregationSpecification aggregationSpec = getAggregationSpecification(table,
selection,
restrictions,
parameters.isDistinct);
AggregationSpecification.Factory aggregationSpecFactory = getAggregationSpecFactory(table,
bindVariables,
selection,
restrictions,
parameters.isDistinct);
checkFalse(aggregationSpec == AggregationSpecification.AGGREGATE_EVERYTHING && perPartitionLimit != null,
checkFalse(aggregationSpecFactory == AggregationSpecification.AGGREGATE_EVERYTHING_FACTORY
&& perPartitionLimit != null,
"PER PARTITION LIMIT is not allowed with aggregate queries.");
Comparator<List<ByteBuffer>> orderingComparator = null;
@ -1118,7 +1156,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
selection,
restrictions,
isReversed,
aggregationSpec,
aggregationSpecFactory,
orderingComparator,
prepareLimit(bindVariables, limit, keyspace(), limitReceiver()),
prepareLimit(bindVariables, perPartitionLimit, keyspace(), perPartitionLimitReceiver()));
@ -1255,32 +1293,56 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
}
/**
* Creates the <code>AggregationSpecification</code>s used to make the aggregates.
* Creates the {@code AggregationSpecification.Factory} used to make the aggregates.
*
* @param metadata the table metadata
* @param selection the selection
* @param restrictions the restrictions
* @param isDistinct <code>true</code> if the query is a DISTINCT one.
* @return the <code>AggregationSpecification</code>s used to make the aggregates
* @param isDistinct <code>true</code> if the query is a DISTINCT one.
* @return the {@code AggregationSpecification.Factory} used to make the aggregates
*/
private AggregationSpecification getAggregationSpecification(TableMetadata metadata,
Selection selection,
StatementRestrictions restrictions,
boolean isDistinct)
private AggregationSpecification.Factory getAggregationSpecFactory(TableMetadata metadata,
VariableSpecifications boundNames,
Selection selection,
StatementRestrictions restrictions,
boolean isDistinct)
{
if (parameters.groups.isEmpty())
return selection.isAggregate() ? AggregationSpecification.AGGREGATE_EVERYTHING
return selection.isAggregate() ? AggregationSpecification.AGGREGATE_EVERYTHING_FACTORY
: null;
int clusteringPrefixSize = 0;
Iterator<ColumnMetadata> pkColumns = metadata.primaryKeyColumns().iterator();
for (ColumnIdentifier id : parameters.groups)
Selector.Factory selectorFactory = null;
for (Selectable.Raw raw : parameters.groups)
{
ColumnMetadata def = metadata.getExistingColumn(id);
Selectable selectable = raw.prepare(metadata);
ColumnMetadata def = null;
checkTrue(def.isPartitionKey() || def.isClusteringColumn(),
"Group by is currently only supported on the columns of the PRIMARY KEY, got %s", def.name);
// For GROUP BY we only allow column names or functions at the higher level.
if (selectable instanceof WithFunction)
{
WithFunction withFunction = (WithFunction) selectable;
validateGroupByFunction(withFunction);
List<ColumnMetadata> columns = new ArrayList<ColumnMetadata>();
selectorFactory = selectable.newSelectorFactory(metadata, null, columns, boundNames);
checkFalse(columns.isEmpty(), "GROUP BY functions must have one clustering column name as parameter");
if (columns.size() > 1)
throw invalidRequest("GROUP BY functions accept only one clustering column as parameter, got: %s",
columns.stream().map(c -> c.name.toCQLString()).collect(Collectors.joining(",")));
def = columns.get(0);
checkTrue(def.isClusteringColumn(),
"Group by functions are only supported on clustering columns, got %s", def.name);
}
else
{
def = (ColumnMetadata) selectable;
checkTrue(def.isPartitionKey() || def.isClusteringColumn(),
"Group by is currently only supported on the columns of the PRIMARY KEY, got %s", def.name);
checkNull(selectorFactory, "Functions are only supported on the last element of the GROUP BY clause");
}
while (true)
{
@ -1308,7 +1370,22 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
checkFalse(clusteringPrefixSize > 0 && isDistinct,
"Grouping on clustering columns is not allowed for SELECT DISTINCT queries");
return AggregationSpecification.aggregatePkPrefix(metadata.comparator, clusteringPrefixSize);
return selectorFactory == null ? AggregationSpecification.aggregatePkPrefixFactory(metadata.comparator, clusteringPrefixSize)
: AggregationSpecification.aggregatePkPrefixFactoryWithSelector(metadata.comparator,
clusteringPrefixSize,
selectorFactory);
}
/**
* Checks that the function used is a valid one for the GROUP BY clause.
*
* @param withFunction the {@code Selectable} from which the function must be retrieved.
* @return the monotonic scalar function that must be used for determining the groups.
*/
private void validateGroupByFunction(WithFunction withFunction)
{
Function f = withFunction.function;
checkFalse(f.isAggregate(), "Aggregate functions are not supported within the GROUP BY clause, got: %s", f.name());
}
private Comparator<List<ByteBuffer>> getOrderingComparator(Selection selection,
@ -1410,13 +1487,13 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
{
// Public because CASSANDRA-9858
public final Map<ColumnIdentifier, Boolean> orderings;
public final List<ColumnIdentifier> groups;
public final List<Selectable.Raw> groups;
public final boolean isDistinct;
public final boolean allowFiltering;
public final boolean isJson;
public Parameters(Map<ColumnIdentifier, Boolean> orderings,
List<ColumnIdentifier> groups,
List<Selectable.Raw> groups,
boolean isDistinct,
boolean allowFiltering,
boolean isJson)
@ -1618,7 +1695,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
sb.append(" AND ").append(filterString);
}
DataLimits limits = getDataLimits(getLimit(options), getPerPartitionLimit(options), options.getPageSize());
DataLimits limits = getDataLimits(getLimit(options), getPerPartitionLimit(options), options.getPageSize(), getAggregationSpec(options));
if (limits != DataLimits.NONE)
sb.append(' ').append(limits);
return sb.toString();

View File

@ -1054,7 +1054,7 @@ public abstract class ReadCommand extends AbstractReadQuery
int nowInSec = in.readInt();
ColumnFilter columnFilter = ColumnFilter.serializer.deserialize(in, version, metadata);
RowFilter rowFilter = RowFilter.serializer.deserialize(in, version, metadata);
DataLimits limits = DataLimits.serializer.deserialize(in, version, metadata.comparator);
DataLimits limits = DataLimits.serializer.deserialize(in, version, metadata);
IndexMetadata index = hasIndex ? deserializeIndexMetadata(in, version, metadata) : null;
return kind.selectionDeserializer.deserialize(in, version, isDigest, digestVersion, acceptsTransient, metadata, nowInSec, columnFilter, rowFilter, limits, index);

View File

@ -18,11 +18,16 @@
package org.apache.cassandra.db.aggregation;
import java.io.IOException;
import java.util.List;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.selection.Selector;
import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
/**
* Defines how rows should be grouped for creating aggregates.
@ -43,6 +48,11 @@ public abstract class AggregationSpecification
}
};
/**
* Factory for <code>AggregationSpecification</code> that group all the row together.
*/
public static final AggregationSpecification.Factory AGGREGATE_EVERYTHING_FACTORY = options -> AGGREGATE_EVERYTHING;
/**
* The <code>AggregationSpecification</code> kind.
*/
@ -53,7 +63,7 @@ public abstract class AggregationSpecification
*/
public static enum Kind
{
AGGREGATE_EVERYTHING, AGGREGATE_BY_PK_PREFIX
AGGREGATE_EVERYTHING, AGGREGATE_BY_PK_PREFIX, AGGREGATE_BY_PK_PREFIX_WITH_SELECTOR
}
/**
@ -89,37 +99,86 @@ public abstract class AggregationSpecification
public abstract GroupMaker newGroupMaker(GroupingState state);
/**
* Creates a new <code>AggregationSpecification</code> instance that will build aggregates based on primary key
* columns.
* Creates a new {@code Factory} instance to create {@code AggregationSpecification} that will build aggregates
* based on primary key columns.
*
* @param comparator the comparator used to compare the clustering prefixes
* @param clusteringPrefixSize the number of clustering columns used to create the aggregates
* @return a new <code>AggregationSpecification</code> instance that will build aggregates based on primary key
* columns
* @return a new {@code Factory} instance to create {@code AggregationSpecification} that will build aggregates
* based on primary key columns.
*/
public static AggregationSpecification aggregatePkPrefix(ClusteringComparator comparator, int clusteringPrefixSize)
public static AggregationSpecification.Factory aggregatePkPrefixFactory(ClusteringComparator comparator,
int clusteringPrefixSize)
{
return new AggregateByPkPrefix(comparator, clusteringPrefixSize);
return options -> new AggregateByPkPrefix(comparator, clusteringPrefixSize);
}
public static AggregationSpecification.Factory aggregatePkPrefixFactoryWithSelector(final ClusteringComparator comparator,
final int clusteringPrefixSize,
final Selector.Factory factory)
{
return new Factory()
{
@Override
public void addFunctionsTo(List<Function> functions)
{
factory.addFunctionsTo(functions);
}
@Override
public AggregationSpecification newInstance(QueryOptions options)
{
Selector selector = factory.newInstance(options);
selector.validateForGroupBy();
return new AggregateByPkPrefixWithSelector(comparator,
clusteringPrefixSize,
selector);
}
};
}
/**
* Factory for {@code AggregationSpecification}.
*
*/
public static interface Factory
{
/**
* Creates a new {@code AggregationSpecification} instance after having binded the parameters.
*
* @param options the query options
* @return a new {@code AggregationSpecification} instance.
*/
public AggregationSpecification newInstance(QueryOptions options);
public default void addFunctionsTo(List<Function> functions)
{
}
}
/**
* <code>AggregationSpecification</code> that build aggregates based on primary key columns
*/
private static final class AggregateByPkPrefix extends AggregationSpecification
private static class AggregateByPkPrefix extends AggregationSpecification
{
/**
* The number of clustering component to compare.
*/
private final int clusteringPrefixSize;
protected final int clusteringPrefixSize;
/**
* The comparator used to compare the clustering prefixes.
*/
private final ClusteringComparator comparator;
protected final ClusteringComparator comparator;
public AggregateByPkPrefix(ClusteringComparator comparator, int clusteringPrefixSize)
{
super(Kind.AGGREGATE_BY_PK_PREFIX);
this(Kind.AGGREGATE_BY_PK_PREFIX, comparator, clusteringPrefixSize);
}
protected AggregateByPkPrefix(Kind kind, ClusteringComparator comparator, int clusteringPrefixSize)
{
super(kind);
this.comparator = comparator;
this.clusteringPrefixSize = clusteringPrefixSize;
}
@ -127,7 +186,32 @@ public abstract class AggregationSpecification
@Override
public GroupMaker newGroupMaker(GroupingState state)
{
return GroupMaker.newInstance(comparator, clusteringPrefixSize, state);
return GroupMaker.newPkPrefixGroupMaker(comparator, clusteringPrefixSize, state);
}
}
/**
* <code>AggregationSpecification</code> that build aggregates based on primary key columns using a selector.
*/
private static final class AggregateByPkPrefixWithSelector extends AggregateByPkPrefix
{
/**
* The selector.
*/
private final Selector selector;
public AggregateByPkPrefixWithSelector(ClusteringComparator comparator,
int clusteringPrefixSize,
Selector selector)
{
super(Kind.AGGREGATE_BY_PK_PREFIX_WITH_SELECTOR, comparator, clusteringPrefixSize);
this.selector = selector;
}
@Override
public GroupMaker newGroupMaker(GroupingState state)
{
return GroupMaker.newSelectorGroupMaker(comparator, clusteringPrefixSize, selector, state);
}
}
@ -143,12 +227,17 @@ public abstract class AggregationSpecification
case AGGREGATE_BY_PK_PREFIX:
out.writeUnsignedVInt(((AggregateByPkPrefix) aggregationSpec).clusteringPrefixSize);
break;
case AGGREGATE_BY_PK_PREFIX_WITH_SELECTOR:
AggregateByPkPrefixWithSelector spec = (AggregateByPkPrefixWithSelector) aggregationSpec;
out.writeUnsignedVInt(spec.clusteringPrefixSize);
Selector.serializer.serialize(spec.selector, out, version);
break;
default:
throw new AssertionError();
throw new AssertionError("Unknow aggregation kind: " + aggregationSpec.kind());
}
}
public AggregationSpecification deserialize(DataInputPlus in, int version, ClusteringComparator comparator) throws IOException
public AggregationSpecification deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
Kind kind = Kind.values()[in.readUnsignedByte()];
switch (kind)
@ -156,10 +245,15 @@ public abstract class AggregationSpecification
case AGGREGATE_EVERYTHING:
return AggregationSpecification.AGGREGATE_EVERYTHING;
case AGGREGATE_BY_PK_PREFIX:
return new AggregateByPkPrefix(metadata.comparator, (int) in.readUnsignedVInt());
case AGGREGATE_BY_PK_PREFIX_WITH_SELECTOR:
int clusteringPrefixSize = (int) in.readUnsignedVInt();
return AggregationSpecification.aggregatePkPrefix(comparator, clusteringPrefixSize);
Selector selector = Selector.serializer.deserialize(in, version, metadata);
return new AggregateByPkPrefixWithSelector(metadata.comparator,
clusteringPrefixSize,
selector);
default:
throw new AssertionError();
throw new AssertionError("Unknow aggregation kind: " + kind);
}
}
@ -173,8 +267,15 @@ public abstract class AggregationSpecification
case AGGREGATE_BY_PK_PREFIX:
size += TypeSizes.sizeofUnsignedVInt(((AggregateByPkPrefix) aggregationSpec).clusteringPrefixSize);
break;
case AGGREGATE_BY_PK_PREFIX_WITH_SELECTOR:
AggregateByPkPrefixWithSelector spec = (AggregateByPkPrefixWithSelector) aggregationSpec;
size += TypeSizes.sizeofUnsignedVInt(spec.clusteringPrefixSize);
size += Selector.serializer.serializedSize(spec.selector, version
);
break;
default:
throw new AssertionError();
throw new AssertionError("Unknow aggregation kind: " + aggregationSpec.kind());
}
return size;
}

View File

@ -19,9 +19,11 @@ package org.apache.cassandra.db.aggregation;
import java.nio.ByteBuffer;
import org.apache.cassandra.cql3.selection.Selector;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.transport.ProtocolVersion;
/**
* A <code>GroupMaker</code> can be used to determine if some sorted rows belongs to the same group or not.
@ -44,16 +46,33 @@ public abstract class GroupMaker
}
};
public static GroupMaker newInstance(ClusteringComparator comparator, int clusteringPrefixSize, GroupingState state)
public static GroupMaker newPkPrefixGroupMaker(ClusteringComparator comparator,
int clusteringPrefixSize,
GroupingState state)
{
return new PkPrefixGroupMaker(comparator, clusteringPrefixSize, state);
}
public static GroupMaker newInstance(ClusteringComparator comparator, int clusteringPrefixSize)
public static GroupMaker newPkPrefixGroupMaker(ClusteringComparator comparator, int clusteringPrefixSize)
{
return new PkPrefixGroupMaker(comparator, clusteringPrefixSize);
}
public static GroupMaker newSelectorGroupMaker(ClusteringComparator comparator,
int clusteringPrefixSize,
Selector selector,
GroupingState state)
{
return new SelectorGroupMaker(comparator, clusteringPrefixSize, selector, state);
}
public static GroupMaker newSelectorGroupMaker(ClusteringComparator comparator,
int clusteringPrefixSize,
Selector selector)
{
return new SelectorGroupMaker(comparator, clusteringPrefixSize, selector);
}
/**
* Checks if a given row belongs to the same group that the previous row or not.
*
@ -75,27 +94,27 @@ public abstract class GroupMaker
return false;
}
private static final class PkPrefixGroupMaker extends GroupMaker
private static class PkPrefixGroupMaker extends GroupMaker
{
/**
* The size of the clustering prefix used to make the groups
*/
private final int clusteringPrefixSize;
protected final int clusteringPrefixSize;
/**
* The comparator used to compare the clustering prefixes.
*/
private final ClusteringComparator comparator;
protected final ClusteringComparator comparator;
/**
* The last partition key seen
*/
private ByteBuffer lastPartitionKey;
protected ByteBuffer lastPartitionKey;
/**
* The last clustering seen
*/
private Clustering<?> lastClustering;
protected Clustering<?> lastClustering;
public PkPrefixGroupMaker(ClusteringComparator comparator, int clusteringPrefixSize, GroupingState state)
{
@ -113,28 +132,97 @@ public abstract class GroupMaker
@Override
public boolean isNewGroup(DecoratedKey partitionKey, Clustering<?> clustering)
{
boolean isNew = false;
ByteBuffer key = partitionKey.getKey();
// We are entering a new group if:
// - the partition key is a new one
// - the last clustering was not null and does not have the same prefix as the new clustering one
boolean isNew = !key.equals(lastPartitionKey)
|| lastClustering == null
|| comparator.compare(lastClustering, clustering, clusteringPrefixSize) != 0;
lastPartitionKey = key;
lastClustering = Clustering.STATIC_CLUSTERING == clustering ? null : clustering;
return isNew;
}
}
private static class SelectorGroupMaker extends PkPrefixGroupMaker
{
/**
* The selector used to build the groups.
*/
private final Selector selector;
/**
* The output of the selector call on the last clustering
*/
private ByteBuffer lastOutput;
private final Selector.InputRow input = new Selector.InputRow(1, false, false);
public SelectorGroupMaker(ClusteringComparator comparator,
int clusteringPrefixSize,
Selector selector,
GroupingState state)
{
super(comparator, clusteringPrefixSize, state);
this.selector = selector;
this.lastOutput = lastClustering == null ? null :
executeSelector(lastClustering.bufferAt(clusteringPrefixSize - 1));
}
public SelectorGroupMaker(ClusteringComparator comparator,
int clusteringPrefixSize,
Selector selector)
{
super(comparator, clusteringPrefixSize);
this.selector = selector;
}
@Override
public boolean isNewGroup(DecoratedKey partitionKey, Clustering<?> clustering)
{
ByteBuffer output =
Clustering.STATIC_CLUSTERING == clustering ? null
: executeSelector(clustering.bufferAt(clusteringPrefixSize - 1));
ByteBuffer key = partitionKey.getKey();
// We are entering a new group if:
// - the partition key is a new one
// - the last clustering was not null and does not have the same prefix as the new clustering one
if (!partitionKey.getKey().equals(lastPartitionKey))
{
lastPartitionKey = partitionKey.getKey();
isNew = true;
if (Clustering.STATIC_CLUSTERING == clustering)
{
lastClustering = null;
return true;
}
}
else if (lastClustering != null && comparator.compare(lastClustering, clustering, clusteringPrefixSize) != 0)
{
isNew = true;
}
boolean isNew = !key.equals(lastPartitionKey)
|| lastClustering == null
|| comparator.compare(lastClustering, clustering, clusteringPrefixSize - 1) != 0
|| compareOutput(output) != 0;
lastClustering = clustering;
lastPartitionKey = key;
lastClustering = Clustering.STATIC_CLUSTERING == clustering ? null : clustering;
lastOutput = output;
return isNew;
}
private int compareOutput(ByteBuffer output)
{
if (output == null)
return lastOutput == null ? 0 : -1;
if (lastOutput == null)
return 1;
return selector.getType().compare(output, lastOutput);
}
private ByteBuffer executeSelector(ByteBuffer argument)
{
input.add(argument);
// For computing groups we do not need to use the client protocol version.
selector.addInput(ProtocolVersion.CURRENT, input);
ByteBuffer output = selector.getOutput(ProtocolVersion.CURRENT);
selector.reset();
input.reset(false);
return output;
}
}
}

View File

@ -32,6 +32,7 @@ import org.apache.cassandra.db.transform.StoppingTransformation;
import org.apache.cassandra.db.transform.Transformation;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
/**
@ -1175,7 +1176,7 @@ public abstract class DataLimits
}
}
public DataLimits deserialize(DataInputPlus in, int version, ClusteringComparator comparator) throws IOException
public DataLimits deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
Kind kind = Kind.values()[in.readUnsignedByte()];
switch (kind)
@ -1199,9 +1200,9 @@ public abstract class DataLimits
int groupPerPartitionLimit = (int) in.readUnsignedVInt();
int rowLimit = (int) in.readUnsignedVInt();
AggregationSpecification groupBySpec = AggregationSpecification.serializer.deserialize(in, version, comparator);
AggregationSpecification groupBySpec = AggregationSpecification.serializer.deserialize(in, version, metadata);
GroupingState state = GroupingState.serializer.deserialize(in, version, comparator);
GroupingState state = GroupingState.serializer.deserialize(in, version, metadata.comparator);
if (kind == Kind.CQL_GROUP_BY_LIMIT)
return new CQLGroupByLimits(groupLimit,

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.distributed.test;
import java.util.Date;
import java.util.Iterator;
import com.google.common.collect.Iterators;
@ -31,7 +32,11 @@ import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.ICoordinator;
import org.apache.cassandra.serializers.SimpleDateSerializer;
import org.apache.cassandra.serializers.TimeSerializer;
import org.apache.cassandra.serializers.TimestampSerializer;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
@ -149,6 +154,183 @@ public class GroupByTest extends TestBaseImpl
}
}
@Test
public void testGroupByTimeRangesWithTimestampType() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(3).start()))
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithTimestamp (pk int, time timestamp, v int, primary key (pk, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:10:00 UTC', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:12:00 UTC', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:14:00 UTC', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:15:00 UTC', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:21:00 UTC', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:22:00 UTC', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:26:00 UTC', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:26:20 UTC', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (2, '2016-09-27 16:26:20 UTC', 10)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (2, '2016-09-27 16:30:00 UTC', 11)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
{
for (String startingTime : new String[] {"", ", '2016-09-27 UTC'"} )
{
String stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ")";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ") LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ") PER PARTITION LIMIT 1";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L));
}
}
}
}
@Test
public void testGroupByTimeRangesWithDateType() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(3).start()))
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithDate (pk int, time date, v int, primary key (pk, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-27', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-28', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-29', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-30', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-01', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-04', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-20', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-11-27', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (2, '2016-11-01', 10)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (2, '2016-11-02', 11)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
{
for (String startingTime : new String[] {"", ", '2016-06-01'"} )
{
String stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ")";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ") LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ") PER PARTITION LIMIT 1";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-09-01"), 1, 4, 4L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
}
}
}
}
@Test
public void testGroupByTimeRangesWithTimeType() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(3).start()))
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithTime (pk int, date date, time time, v int, primary key (pk, date, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:10:00', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:12:00', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:14:00', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:15:00', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:21:00', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:22:00', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:00', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:20', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:20', 9)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:30', 10)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
{
String stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime GROUP BY pk, date, floor(time, 5m)";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L));
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime GROUP BY pk, date, floor(time, 5m) LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L));
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L));
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L));
}
}
}
private static void initFunctions(Cluster cluster)
{
cluster.schemaChange(withKeyspace("CREATE FUNCTION %s.concat_strings_fn(a text, b text) " +
@ -162,4 +344,20 @@ public class GroupByTest extends TestBaseImpl
" STYPE text" +
" INITCOND '_'"));
}
private static Date toTimestamp(String timestampAsString)
{
return new Date(TimestampSerializer.dateStringToTimestamp(timestampAsString));
}
private static int toLocalDate(String dateAsString)
{
return SimpleDateSerializer.dateStringToDays(dateAsString) ;
}
private static long toTime(String timeAsString)
{
return TimeSerializer.timeStringToLong(timeAsString) ;
}
}

View File

@ -18,20 +18,19 @@
*/
package org.apache.cassandra.cql3;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import org.apache.commons.lang3.time.DateUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.serializers.TimeSerializer;
import static org.junit.Assert.assertEquals;
import static org.apache.cassandra.cql3.Duration.*;
@ -109,72 +108,287 @@ public class DurationTest
@Test
public void testAddTo()
{
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("0m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("10us").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T00:10:00"), Duration.from("10m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T01:30:00"), Duration.from("90m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T02:10:00"), Duration.from("2h10m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-23T00:10:00"), Duration.from("2d10m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-24T01:00:00"), Duration.from("2d25h").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-10-21T00:00:00"), Duration.from("1mo").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2017-11-21T00:00:00"), Duration.from("14mo").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2017-02-28T00:00:00"), Duration.from("12mo").addTo(toMillis("2016-02-29T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("0m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("10us").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T00:10:00.000", Duration.from("10m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T01:30:00.000", Duration.from("90m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T02:10:00.000", Duration.from("2h10m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-23T00:10:00.000", Duration.from("2d10m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-24T01:00:00.000", Duration.from("2d25h").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-10-21T00:00:00.000", Duration.from("1mo").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2017-11-21T00:00:00.000", Duration.from("14mo").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2017-02-28T00:00:00.000", Duration.from("12mo").addTo(toMillis("2016-02-29T00:00:00")));
}
@Test
public void testAddToWithNegativeDurations()
{
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("-0m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("-10us").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-20T23:50:00"), Duration.from("-10m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-20T22:30:00"), Duration.from("-90m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-20T21:50:00"), Duration.from("-2h10m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-18T23:50:00"), Duration.from("-2d10m").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-17T23:00:00"), Duration.from("-2d25h").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-08-21T00:00:00"), Duration.from("-1mo").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2015-07-21T00:00:00"), Duration.from("-14mo").addTo(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2015-02-28T00:00:00"), Duration.from("-12mo").addTo(toMillis("2016-02-29T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("-0m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("-10us").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-20T23:50:00.000", Duration.from("-10m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-20T22:30:00.000", Duration.from("-90m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-20T21:50:00.000", Duration.from("-2h10m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-18T23:50:00.000", Duration.from("-2d10m").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-17T23:00:00.000", Duration.from("-2d25h").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-08-21T00:00:00.000", Duration.from("-1mo").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2015-07-21T00:00:00.000", Duration.from("-14mo").addTo(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2015-02-28T00:00:00.000", Duration.from("-12mo").addTo(toMillis("2016-02-29T00:00:00")));
}
@Test
public void testSubstractFrom()
{
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("0m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("10us").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-20T23:50:00"), Duration.from("10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-20T22:30:00"), Duration.from("90m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-20T21:50:00"), Duration.from("2h10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-18T23:50:00"), Duration.from("2d10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-17T23:00:00"), Duration.from("2d25h").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-08-21T00:00:00"), Duration.from("1mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2015-07-21T00:00:00"), Duration.from("14mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2015-02-28T00:00:00"), Duration.from("12mo").substractFrom(toMillis("2016-02-29T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("0m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("10us").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-20T23:50:00.000", Duration.from("10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-20T22:30:00.000", Duration.from("90m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-20T21:50:00.000", Duration.from("2h10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-18T23:50:00.000", Duration.from("2d10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-17T23:00:00.000", Duration.from("2d25h").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-08-21T00:00:00.000", Duration.from("1mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2015-07-21T00:00:00.000", Duration.from("14mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2015-02-28T00:00:00.000", Duration.from("12mo").substractFrom(toMillis("2016-02-29T00:00:00")));
}
@Test
public void testSubstractWithNegativeDurations()
{
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("-0m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T00:00:00"), Duration.from("-10us").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T00:10:00"), Duration.from("-10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T01:30:00"), Duration.from("-90m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-21T02:10:00"), Duration.from("-2h10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-23T00:10:00"), Duration.from("-2d10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-09-24T01:00:00"), Duration.from("-2d25h").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2016-10-21T00:00:00"), Duration.from("-1mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2017-11-21T00:00:00"), Duration.from("-14mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertEquals(toMillis("2017-02-28T00:00:00"), Duration.from("-12mo").substractFrom(toMillis("2016-02-29T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("-0m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T00:00:00.000", Duration.from("-10us").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T00:10:00.000", Duration.from("-10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T01:30:00.000", Duration.from("-90m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-21T02:10:00.000", Duration.from("-2h10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-23T00:10:00.000", Duration.from("-2d10m").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-09-24T01:00:00.000", Duration.from("-2d25h").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2016-10-21T00:00:00.000", Duration.from("-1mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2017-11-21T00:00:00.000", Duration.from("-14mo").substractFrom(toMillis("2016-09-21T00:00:00")));
assertTimeEquals("2017-02-28T00:00:00.000", Duration.from("-12mo").substractFrom(toMillis("2016-02-29T00:00:00")));
}
private long toMillis(String timeAsString)
@Test
public void testFloorTime()
{
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
parser.setTimeZone(TimeZone.getTimeZone("UTC"));
Date date = parser.parse(timeAsString, new ParsePosition(0));
return DateUtils.truncate(date, Calendar.SECOND).getTime();
long time = floorTime("16:12:00", "2h");
Duration result = Duration.newInstance(0, 0, time);
Duration expected = Duration.from("16h");
assertEquals(expected, result);
}
public void assertInvalidDuration(String duration, String expectedErrorMessage)
@Test
public void testInvalidFloorTimestamp()
{
try
{
floorTimestamp("2016-09-27T16:12:00", "2h", "2017-09-01T00:00:00");
Assert.fail();
}
catch (InvalidRequestException e)
{
assertEquals("The floor function starting time is greater than the provided time", e.getMessage());
}
try
{
floorTimestamp("2016-09-27T16:12:00", "-2h", "2016-09-27T00:00:00");
Assert.fail();
}
catch (InvalidRequestException e)
{
assertEquals("Negative durations are not supported by the floor function", e.getMessage());
}
}
@Test
public void testFloorTimestampWithDurationInHours()
{
// Test floor with a timestamp equals to the start time
long result = floorTimestamp("2016-09-27T16:12:00", "2h", "2016-09-27T16:12:00");
assertTimeEquals("2016-09-27T16:12:00.000", result);
// Test floor with a duration equals to zero
result = floorTimestamp("2016-09-27T18:12:00", "0h", "2016-09-27T16:12:00");
assertTimeEquals("2016-09-27T18:12:00.000", result);
// Test floor with a timestamp exactly equals to the start time + (1 x duration)
result = floorTimestamp("2016-09-27T18:12:00", "2h", "2016-09-27T16:12:00");
assertTimeEquals("2016-09-27T18:12:00.000", result);
// Test floor with a timestamp in the first bucket
result = floorTimestamp("2016-09-27T16:13:00", "2h", "2016-09-27T16:12:00");
assertTimeEquals("2016-09-27T16:12:00.000", result);
// Test floor with a timestamp in another bucket
result = floorTimestamp("2016-09-27T16:12:00", "2h", "2016-09-27T00:00:00");
assertTimeEquals("2016-09-27T16:00:00.000", result);
}
@Test
public void testFloorTimestampWithDurationInDays()
{
// Test floor with a start time at the beginning of the month
long result = floorTimestamp("2016-09-27T16:12:00", "2d", "2016-09-01T00:00:00");
assertTimeEquals("2016-09-27T00:00:00.000", result);
// Test floor with a start time in the previous month
result = floorTimestamp("2016-09-27T16:12:00", "2d", "2016-08-01T00:00:00");
assertTimeEquals("2016-09-26T00:00:00.000", result);
}
@Test
public void testFloorTimestampWithDurationInDaysAndHours()
{
long result = floorTimestamp("2016-09-27T16:12:00", "2d12h", "2016-09-01T00:00:00");
assertTimeEquals("2016-09-26T00:00:00.000", result);
}
@Test
public void testFloorTimestampWithDurationInMonths()
{
// Test floor with a timestamp equals to the start time
long result = floorTimestamp("2016-09-01T00:00:00", "2mo", "2016-09-01T00:00:00");
assertTimeEquals("2016-09-01T00:00:00.000", result);
// Test floor with a timestamp in the first bucket
result = floorTimestamp("2016-09-27T16:12:00", "2mo", "2016-09-01T00:00:00");
assertTimeEquals("2016-09-01T00:00:00.000", result);
// Test floor with a start time at the beginning of the year (LEAP YEAR)
result = floorTimestamp("2016-09-27T16:12:00", "1mo", "2016-01-01T00:00:00");
assertTimeEquals("2016-09-01T00:00:00.000", result);
// Test floor with a start time at the beginning of the previous year
result = floorTimestamp("2016-09-27T16:12:00", "2mo", "2015-01-01T00:00:00");
assertTimeEquals("2016-09-01T00:00:00.000", result);
// Test floor with a start time in the previous year
result = floorTimestamp("2016-09-27T16:12:00", "2mo", "2015-02-02T00:00:00");
assertTimeEquals("2016-08-02T00:00:00.000", result);
}
@Test
public void testFloorTimestampWithDurationInMonthsAndDays()
{
long result = floorTimestamp("2016-09-27T16:12:00", "2mo2d", "2016-01-01T00:00:00");
assertTimeEquals("2016-09-09T00:00:00.000", result);
result = floorTimestamp("2016-09-27T16:12:00", "2mo5d", "2016-01-01T00:00:00");
assertTimeEquals("2016-09-21T00:00:00.000", result);
// Test floor with a timestamp in the first bucket
result = floorTimestamp("2016-09-04T16:12:00", "2mo5d", "2016-07-01T00:00:00");
assertTimeEquals("2016-07-01T00:00:00.000", result);
// Test floor with a timestamp in a bucket starting on the last day of the month
result = floorTimestamp("2016-09-27T16:12:00", "2mo10d", "2016-01-01T00:00:00");
assertTimeEquals("2016-07-31T00:00:00.000", result);
// Test floor with a timestamp in a bucket starting on the first day of the month
result = floorTimestamp("2016-09-27T16:12:00", "2mo12d", "2016-01-01T00:00:00");
assertTimeEquals("2016-08-06T00:00:00.000", result);
// Test leap years
result = floorTimestamp("2016-04-27T16:12:00", "1mo30d", "2016-01-01T00:00:00");
assertTimeEquals("2016-03-02T00:00:00.000", result);
result = floorTimestamp("2015-04-27T16:12:00", "1mo30d", "2015-01-01T00:00:00");
assertTimeEquals("2015-03-03T00:00:00.000", result);
}
@Test
public void testFloorTimestampWithDurationSmallerThanPrecision()
{
long result = floorTimestamp("2016-09-27T18:14:00", "5us", "2016-09-27T16:12:00");
assertTimeEquals("2016-09-27T18:14:00.000", result);
result = floorTimestamp("2016-09-27T18:14:00", "1h5us", "2016-09-27T16:12:00");
assertTimeEquals("2016-09-27T18:12:00.000", result);
}
@Test
public void testFloorTimestampWithLeapSecond()
{
long result = floorTimestamp("2016-07-02T00:00:00", "2m", "2016-06-30T23:58:00");
assertTimeEquals("2016-07-02T00:00:00.000", result);
}
@Test
public void testFloorTimestampWithComplexDuration()
{
long result = floorTimestamp("2016-07-02T00:00:00", "2mo2d8h", "2016-01-01T00:00:00");
assertTimeEquals("2016-05-05T16:00:00.000", result);
}
@Test
public void testInvalidFloorTime()
{
try
{
floorTime("16:12:00", "2d");
Assert.fail();
}
catch (InvalidRequestException e)
{
assertEquals("For time values, the floor can only be computed for durations smaller that a day", e.getMessage());
}
try
{
floorTime("16:12:00", "25h");
Assert.fail();
}
catch (InvalidRequestException e)
{
assertEquals("For time values, the floor can only be computed for durations smaller that a day", e.getMessage());
}
try
{
floorTime("16:12:00", "-2h");
Assert.fail();
}
catch (InvalidRequestException e)
{
assertEquals("Negative durations are not supported by the floor function", e.getMessage());
}
}
private static long toMillis(String timeAsString)
{
OffsetDateTime dateTime = LocalDateTime.parse(timeAsString, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
.atOffset(ZoneOffset.UTC);
return Instant.from(dateTime).toEpochMilli();
}
private static String fromMillis(long timeInMillis)
{
return Instant.ofEpochMilli(timeInMillis)
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"));
}
private static void assertTimeEquals(String expected, long actualTimeInMillis)
{
assertEquals(expected, fromMillis(actualTimeInMillis));
}
private static long floorTimestamp(String time, String duration, String startingTime)
{
return Duration.floorTimestamp(toMillis(time), Duration.from(duration), toMillis(startingTime));
}
private static long floorTime(String time, String duration)
{
return Duration.floorTime(timeInNanos(time), Duration.from(duration));
}
private static long timeInNanos(String timeAsString)
{
return TimeSerializer.timeStringToLong(timeAsString);
}
private static void assertInvalidDuration(String duration, String expectedErrorMessage)
{
try
{

View File

@ -0,0 +1,140 @@
/*
* 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.selection;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.Constants.Literal;
import org.apache.cassandra.cql3.functions.AggregateFcts;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.functions.TimeFcts;
import org.apache.cassandra.cql3.selection.Selectable.RawIdentifier;
import org.apache.cassandra.cql3.selection.Selector.Serializer;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.Pair;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
public class SelectorSerializationTest extends CQLTester
{
@Test
public void testSerDes() throws IOException
{
createTable("CREATE TABLE %s (pk int, c1 int, c2 timestamp, v int, PRIMARY KEY(pk, c1, c2))");
KeyspaceMetadata keyspace = Schema.instance.getKeyspaceMetadata(KEYSPACE);
TableMetadata table = keyspace.getTableOrViewNullable(currentTable());
// Test SimpleSelector serialization
checkSerialization(table.getColumn(new ColumnIdentifier("c1", false)), table);
// Test WritetimeOrTTLSelector serialization
checkSerialization(new Selectable.WritetimeOrTTL(table.getColumn(new ColumnIdentifier("v", false)), true), table);
checkSerialization(new Selectable.WritetimeOrTTL(table.getColumn(new ColumnIdentifier("v", false)), false), table);
// Test ListSelector serialization
checkSerialization(new Selectable.WithList(asList(table.getColumn(new ColumnIdentifier("v", false)),
table.getColumn(new ColumnIdentifier("c1", false)))), table);
// Test SetSelector serialization
checkSerialization(new Selectable.WithSet(asList(table.getColumn(new ColumnIdentifier("v", false)),
table.getColumn(new ColumnIdentifier("c1", false)))), table);
// Test MapSelector serialization
Pair<Selectable.Raw, Selectable.Raw> pair = Pair.create(RawIdentifier.forUnquoted("v"),
RawIdentifier.forUnquoted("c1"));
checkSerialization(new Selectable.WithMapOrUdt(table, asList(pair)), table, MapType.getInstance(Int32Type.instance, Int32Type.instance, false));
// Test TupleSelector serialization
checkSerialization(new Selectable.BetweenParenthesesOrWithTuple(asList(table.getColumn(new ColumnIdentifier("c2", false)),
table.getColumn(new ColumnIdentifier("c1", false)))), table);
// Test TermSelector serialization
checkSerialization(new Selectable.WithTerm(Literal.duration("5m")), table, DurationType.instance);
// Test UserTypeSelector serialization
String typeName = createType("CREATE TYPE %s (f1 int, f2 int)");
UserType type = new UserType(KEYSPACE, ByteBufferUtil.bytes(typeName),
asList(FieldIdentifier.forUnquoted("f1"),
FieldIdentifier.forUnquoted("f2")),
asList(Int32Type.instance,
Int32Type.instance),
false);
List<Pair<Selectable.Raw, Selectable.Raw>> list = asList(Pair.create(RawIdentifier.forUnquoted("f1"),
RawIdentifier.forUnquoted("c1")),
Pair.create(RawIdentifier.forUnquoted("f2"),
RawIdentifier.forUnquoted("pk")));
checkSerialization(new Selectable.WithMapOrUdt(table, list), table, type);
// Test FieldSelector serialization
checkSerialization(new Selectable.WithFieldSelection(new Selectable.WithTypeHint(typeName, type, new Selectable.WithMapOrUdt(table, list)), FieldIdentifier.forUnquoted("f1")), table, type);
// Test AggregateFunctionSelector serialization
Function max = AggregateFcts.makeMaxFunction(Int32Type.instance);
checkSerialization(new Selectable.WithFunction(max, asList(table.getColumn(new ColumnIdentifier("v", false)))), table);
// Test SCalarFunctionSelector serialization
Function toDate = TimeFcts.toDate(TimestampType.instance);
checkSerialization(new Selectable.WithFunction(toDate, asList(table.getColumn(new ColumnIdentifier("c2", false)))), table);
Function floor = TimeFcts.FloorTimestampFunction.newInstanceWithStartTimeArgument();
checkSerialization(new Selectable.WithFunction(floor, asList(table.getColumn(new ColumnIdentifier("c2", false)),
new Selectable.WithTerm(Literal.duration("5m")),
new Selectable.WithTerm(Literal.string("2016-09-27 16:00:00 UTC")))), table);
}
private static void checkSerialization(Selectable selectable, TableMetadata table) throws IOException
{
checkSerialization(selectable, table, null);
}
private static void checkSerialization(Selectable selectable, TableMetadata table, AbstractType<?> expectedType) throws IOException
{
int version = MessagingService.current_version;
Serializer serializer = Selector.serializer;
Selector.Factory factory = selectable.newSelectorFactory(table, expectedType, new ArrayList<>(), VariableSpecifications.empty());
Selector selector = factory.newInstance(QueryOptions.DEFAULT);
int size = serializer.serializedSize(selector, version);
DataOutputBuffer out = new DataOutputBuffer(size);
serializer.serialize(selector, out, version);
ByteBuffer buffer = out.asNewBuffer();
DataInputBuffer in = new DataInputBuffer(buffer, false);
Selector deserialized = serializer.deserialize(in, version, table);
assertEquals(selector, deserialized);
}
}

View File

@ -19,7 +19,19 @@ package org.apache.cassandra.cql3.validation.operations;
import org.junit.Test;
import com.datastax.driver.core.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.UUID;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.serializers.SimpleDateSerializer;
import org.apache.cassandra.serializers.TimeSerializer;
import org.apache.cassandra.serializers.TimestampSerializer;
import org.apache.cassandra.utils.TimeUUID;
public class SelectGroupByTest extends CQLTester
{
@ -2132,4 +2144,493 @@ public class SelectGroupByTest extends CQLTester
row(1, 1, 4L, 3L));
}
}
@Test
public void testGroupByTimeRangesWithTimestamTypeAndWithoutPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, time timestamp, v int, primary key (pk, time))" + compactOption);
assertInvalidMessage("Group by currently only support groups of columns following their declared order in the PRIMARY KEY",
"SELECT pk, floor(time, 2h, '2016-09-01'), v FROM %s GROUP BY floor(time, 2h, '2016-09-01')");
assertInvalidMessage("Only monotonic functions are supported in the GROUP BY clause. Got: system.floor : (timestamp, duration(constant), timestamp) -> timestamp",
"SELECT pk, floor(time, 2h, time), v FROM %s GROUP BY pk, floor(time, 2h, time)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:10:00 UTC', 1)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:12:00 UTC', 2)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:14:00 UTC', 3)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:15:00 UTC', 4)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:21:00 UTC', 5)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:22:00 UTC', 6)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:26:00 UTC', 7)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:26:20 UTC', 8)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-09-27 16:26:20 UTC', 10)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-09-27 16:30:00 UTC', 11)");
// Test prepared statement
assertRows(execute("SELECT pk, floor(time, 5m, ?), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m, ?)",
toTimestamp("2016-09-27 00:00:00 UTC"),
toTimestamp("2016-09-27 00:00:00 UTC")),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
for (String startingTime : new String[]{"", ", '2016-09-27 UTC'"})
{
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ")"),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
// Checks with duration lower than precisions
assertInvalidMessage("The floor cannot be computed for the 10us duration as precision is below 1 millisecond",
"SELECT pk, floor(time, 10us" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 10us" + startingTime + ")");
// Checks with a negative duration
assertInvalidMessage("Negative durations are not supported by the floor function",
"SELECT pk, floor(time, 10us" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, -5m" + startingTime + ")");
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") LIMIT 2"),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L));
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") PER PARTITION LIMIT 1"),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L));
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC"),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L));
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC LIMIT 2"),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L));
}
// Checks with start time is greater than the timestamp
assertInvalidMessage("The floor function starting time is greater than the provided time",
"SELECT pk, floor(time, 5m, '2016-10-27'), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m, '2016-10-27')");
}
}
@Test
public void testGroupByTimeRangesWithTimeUUIDAndWithoutPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, time timeuuid, v int, primary key (pk, time))" + compactOption);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:10:00"), 1);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:12:00"), 2);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:14:00"), 3);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:15:00"), 4);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:21:00"), 5);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:22:00"), 6);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:26:00"), 7);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:26:20"), 8);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 2, toTimeUUID("2016-09-27 16:26:00"), 10);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 2, toTimeUUID("2016-09-27 16:30:00"), 11);
for (String startingTime : new String[]{"", ", '2016-09-27 UTC'"})
{
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ")"),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
assertRows(execute("SELECT pk, floor(toTimestamp(time), 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(toTimestamp(time), 5m" + startingTime + ")"),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
// Checks with duration lower than precisions
assertInvalidMessage("The floor cannot be computed for the 10us duration as precision is below 1 millisecond",
"SELECT pk, floor(time, 10us" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 10us" + startingTime + ")");
// Checks with a negative duration
assertInvalidMessage("Negative durations are not supported by the floor function",
"SELECT pk, floor(time, 10us" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, -5m" + startingTime + ")");
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") LIMIT 2"),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L));
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") PER PARTITION LIMIT 1"),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L));
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC"),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L));
assertRows(execute("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC LIMIT 2"),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L));
}
// Checks with start time is greater than the timestamp
assertInvalidMessage("The floor function starting time is greater than the provided time",
"SELECT pk, floor(time, 5m, '2016-10-27'), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m, '2016-10-27')");
}
}
@Test
public void testGroupByTimeRangesWithDateTypeAndWithoutPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, time date, v int, primary key (pk, time))" + compactOption);
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27', 1)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-28', 2)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-29', 3)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-30', 4)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-10-01', 5)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-10-04', 6)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-10-20', 7)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-11-27', 8)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-11-01', 10)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-11-02', 11)");
for (String startingTime : new String[]{"", ", '2016-06-01'"})
{
assertRows(execute("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1mo" + startingTime + ")"),
row(1, toDate("2016-09-01"), 1, 4, 4L),
row(1, toDate("2016-10-01"), 5, 7, 3L),
row(1, toDate("2016-11-01"), 8, 8, 1L),
row(2, toDate("2016-11-01"), 10, 11, 2L));
// Checks with duration lower than precisions
assertInvalidMessage("The floor on date values cannot be computed for the 1h duration as precision is below 1 day",
"SELECT pk, floor(time, 1h" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1h" + startingTime + ")");
// Checks with a negative duration
assertInvalidMessage("Negative durations are not supported by the floor function",
"SELECT pk, floor(time, -1mo" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, -1mo" + startingTime + ")");
assertRows(execute("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1mo" + startingTime + ") LIMIT 2"),
row(1, toDate("2016-09-01"), 1, 4, 4L),
row(1, toDate("2016-10-01"), 5, 7, 3L));
assertRows(execute("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1mo" + startingTime + ") PER PARTITION LIMIT 1"),
row(1, toDate("2016-09-01"), 1, 4, 4L),
row(2, toDate("2016-11-01"), 10, 11, 2L));
assertRows(execute("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC"),
row(1, toDate("2016-11-01"), 8, 8, 1L),
row(1, toDate("2016-10-01"), 5, 7, 3L),
row(1, toDate("2016-09-01"), 1, 4, 4L));
assertRows(execute("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC LIMIT 2"),
row(1, toDate("2016-11-01"), 8, 8, 1L),
row(1, toDate("2016-10-01"), 5, 7, 3L));
}
// Checks with start time is greater than the timestamp
assertInvalidMessage("The floor function starting time is greater than the provided time",
"SELECT pk, floor(time, 1mo, '2017-01-01'), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1mo, '2017-01-01')");
}
}
@Test
public void testGroupByTimeRangesWithTimeTypeAndWithoutPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, date date, time time, v int, primary key (pk, date, time))" + compactOption);
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:10:00', 1)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:12:00', 2)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:14:00', 3)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:15:00', 4)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:21:00', 5)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:22:00', 6)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:00', 7)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:20', 8)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:20', 9)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:30', 10)");
assertInvalidMessage("Functions are only supported on the last element of the GROUP BY clause",
"SELECT pk, floor(date, 1w), time, min(v), max(v), count(v) FROM %s GROUP BY pk, floor(date, 1w), time");
assertRows(execute("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s GROUP BY pk, date, floor(time, 5m)"),
row(1, toDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L));
// Checks with duration greater than dayprecisions
assertInvalidMessage("For time values, the floor can only be computed for durations smaller that a day",
"SELECT pk, date, floor(time, 10d), min(v), max(v), count(v) FROM %s GROUP BY pk, date, floor(time, 10d)");
// Checks with a negative duration
assertInvalidMessage("Negative durations are not supported by the floor function",
"SELECT pk, date, floor(time, -10m), min(v), max(v), count(v) FROM %s GROUP BY pk, date, floor(time, -10m)");
assertRows(execute("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s GROUP BY pk, date, floor(time, 5m) LIMIT 2"),
row(1, toDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L));
assertRows(execute("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC"),
row(1, toDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L));
assertRows(execute("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC LIMIT 2"),
row(1, toDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L));
}
}
@Test
public void testGroupByTimeRangesWithTimestampTypeAndPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, time timestamp, v int, primary key (pk, time))"
+ compactOption);
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:10:00 UTC', 1)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:12:00 UTC', 2)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:14:00 UTC', 3)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:15:00 UTC', 4)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:21:00 UTC', 5)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:22:00 UTC', 6)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:26:00 UTC', 7)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27 16:26:20 UTC', 8)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-09-27 16:26:20 UTC', 10)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-09-27 16:30:00 UTC', 11)");
for (int pageSize = 1; pageSize < 10; pageSize++)
{
for (String startingTime : new String[]{"", ", '2016-09-27 UTC'"})
{
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ")", pageSize),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") LIMIT 2", pageSize),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") PER PARTITION LIMIT 1", pageSize),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC", pageSize),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC LIMIT 2", pageSize),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L));
}
}
}
}
@Test
public void testGroupByTimeRangesWithTimeUUIDAndPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, time timeuuid, v int, primary key (pk, time))" + compactOption);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:10:00"), 1);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:12:00"), 2);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:14:00"), 3);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:15:00"), 4);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:21:00"), 5);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:22:00"), 6);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:26:00"), 7);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 1, toTimeUUID("2016-09-27 16:26:20"), 8);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 2, toTimeUUID("2016-09-27 16:26:00"), 10);
execute("INSERT INTO %s (pk, time, v) VALUES (?, ?, ?)", 2, toTimeUUID("2016-09-27 16:30:00"), 11);
for (int pageSize = 1; pageSize < 10; pageSize++)
{
for (String startingTime : new String[]{"", ", '2016-09-27 UTC'"})
{
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ")", pageSize),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") LIMIT 2", pageSize),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 5m" + startingTime + ") PER PARTITION LIMIT 1", pageSize),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC", pageSize),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC LIMIT 2", pageSize),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L));
}
}
}
}
@Test
public void testGroupByTimeRangesWithDateTypeAndPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, time date, v int, primary key (pk, time))" + compactOption);
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-27', 1)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-28', 2)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-29', 3)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-09-30', 4)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-10-01', 5)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-10-04', 6)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-10-20', 7)");
execute("INSERT INTO %s (pk, time, v) VALUES (1, '2016-11-27', 8)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-11-01', 10)");
execute("INSERT INTO %s (pk, time, v) VALUES (2, '2016-11-02', 11)");
for (int pageSize = 1; pageSize < 10; pageSize++)
{
for (String startingTime : new String[]{"", ", '2016-06-01'"})
{
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1mo" + startingTime + ")", pageSize),
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1mo" + startingTime + ") LIMIT 2", pageSize),
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s GROUP BY pk, floor(time, 1mo" + startingTime + ") PER PARTITION LIMIT 1", pageSize),
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC", pageSize),
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-09-01"), 1, 4, 4L));
assertRowsNet(executeNetWithPaging("SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC LIMIT 2", pageSize),
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
}
}
}
}
@Test
public void testGroupByTimeRangesWithTimeTypeAndPaging() throws Throwable
{
for (String compactOption : new String[] { "", " WITH COMPACT STORAGE" })
{
createTable("CREATE TABLE %s (pk int, date date, time time, v int, primary key (pk, date, time))" + compactOption);
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:10:00', 1)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:12:00', 2)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:14:00', 3)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:15:00', 4)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:21:00', 5)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:22:00', 6)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:00', 7)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:20', 8)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:20', 9)");
execute("INSERT INTO %s (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:30', 10)");
for (int pageSize = 1; pageSize < 10; pageSize++)
{
assertRowsNet(executeNetWithPaging("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s GROUP BY pk, date, floor(time, 5m)", pageSize),
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L));
assertRowsNet(executeNetWithPaging("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s GROUP BY pk, date, floor(time, 5m) LIMIT 2", pageSize),
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L));
assertRowsNet(executeNetWithPaging("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC", pageSize),
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L));
assertRowsNet(executeNetWithPaging("SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC LIMIT 2", pageSize),
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L));
}
}
}
private static UUID toTimeUUID(String string)
{
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(string, formatter);
long timeInMillis = dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
return TimeUUID.Generator.atUnixMillis(timeInMillis).asUUID();
}
private static Date toTimestamp(String timestampAsString)
{
return new Date(TimestampSerializer.dateStringToTimestamp(timestampAsString));
}
private static int toDate(String dateAsString)
{
return SimpleDateSerializer.dateStringToDays(dateAsString);
}
private static LocalDate toLocalDate(String dateAsString)
{
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDateTime dateTime = java.time.LocalDate.parse(dateAsString, formatter).atStartOfDay();
long timeInMillis = dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
return LocalDate.fromMillisSinceEpoch(timeInMillis);
}
private static long toTime(String timeAsString)
{
return TimeSerializer.timeStringToLong(timeAsString);
}
}

View File

@ -18,17 +18,30 @@
package org.apache.cassandra.db.aggregation;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Constants.Literal;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.VariableSpecifications;
import org.apache.cassandra.cql3.functions.ScalarFunction;
import org.apache.cassandra.cql3.functions.TimeFcts;
import org.apache.cassandra.cql3.selection.Selectable;
import org.apache.cassandra.cql3.selection.Selector;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.ReversedType;
import org.apache.cassandra.db.marshal.TimestampType;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -45,7 +58,7 @@ public class GroupMakerTest
public void testIsNewGroupWithClusteringColumns()
{
ClusteringComparator comparator = newComparator(false, false, false);
GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
GroupMaker groupMaker = GroupMaker.newPkPrefixGroupMaker(comparator, 2);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
@ -63,7 +76,7 @@ public class GroupMakerTest
public void testIsNewGroupWithOneClusteringColumnsPrefix()
{
ClusteringComparator comparator = newComparator(false, false, false);
GroupMaker groupMaker = GroupMaker.newInstance(comparator, 1);
GroupMaker groupMaker = GroupMaker.newPkPrefixGroupMaker(comparator, 1);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
@ -82,7 +95,7 @@ public class GroupMakerTest
{
ClusteringComparator comparator = newComparator(true, true, true);
GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
GroupMaker groupMaker = GroupMaker.newPkPrefixGroupMaker(comparator, 2);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 2)));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 1)));
@ -102,7 +115,7 @@ public class GroupMakerTest
{
ClusteringComparator comparator = newComparator(true, false, false);
GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
GroupMaker groupMaker = GroupMaker.newPkPrefixGroupMaker(comparator, 2);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 1)));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 3, 2)));
@ -121,7 +134,7 @@ public class GroupMakerTest
public void testIsNewGroupWithStaticClusteringColumns()
{
ClusteringComparator comparator = newComparator(false, false, false);
GroupMaker groupMaker = GroupMaker.newInstance(comparator, 2);
GroupMaker groupMaker = GroupMaker.newPkPrefixGroupMaker(comparator, 2);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 1)));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, 1, 2)));
@ -135,7 +148,7 @@ public class GroupMakerTest
public void testIsNewGroupWithOnlyPartitionKeyComponents()
{
ClusteringComparator comparator = newComparator(false, false, false);
GroupMaker goupMaker = GroupMaker.newInstance(comparator, 2);
GroupMaker goupMaker = GroupMaker.newPkPrefixGroupMaker(comparator, 2);
assertTrue(goupMaker.isNewGroup(partitionKey(1, 1), clustering(1, 1, 1)));
assertFalse(goupMaker.isNewGroup(partitionKey(1, 1), clustering(1, 1, 2)));
@ -146,6 +159,100 @@ public class GroupMakerTest
assertTrue(goupMaker.isNewGroup(partitionKey(2, 2), clustering(1, 1, 2)));
}
@Test
public void testIsNewGroupWithFunction()
{
GroupMaker groupMaker = newSelectorGroupMaker(false);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:10:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:12:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:14:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:15:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:21:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:22:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:20 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering("2016-09-27 16:26:20 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering("2016-09-27 16:30:00 UTC")));
}
@Test
public void testIsNewGroupWithFunctionAndReversedOrder()
{
GroupMaker groupMaker = newSelectorGroupMaker(true);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:20 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:22:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:21:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:15:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:14:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:12:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:10:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering("2016-09-27 16:30:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering("2016-09-27 16:26:20 UTC")));
}
@Test
public void testIsNewGroupWithFunctionWithStaticColumn()
{
GroupMaker groupMaker = newSelectorGroupMaker(false);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:10:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:12:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:14:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:15:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:21:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:22:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:20 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), Clustering.STATIC_CLUSTERING));
assertTrue(groupMaker.isNewGroup(partitionKey(3), Clustering.STATIC_CLUSTERING));
assertTrue(groupMaker.isNewGroup(partitionKey(4), clustering("2016-09-27 16:26:20 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(4), clustering("2016-09-27 16:30:00 UTC")));
}
@Test
public void testIsNewGroupWithFunctionAndReversedOrderWithStaticColumns()
{
GroupMaker groupMaker = newSelectorGroupMaker(true);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:20 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:26:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:22:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:21:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:15:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:14:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:12:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering("2016-09-27 16:10:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), Clustering.STATIC_CLUSTERING));
assertTrue(groupMaker.isNewGroup(partitionKey(3), Clustering.STATIC_CLUSTERING));
assertTrue(groupMaker.isNewGroup(partitionKey(4), clustering("2016-09-27 16:30:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(4), clustering("2016-09-27 16:26:20 UTC")));
}
@Test
public void testIsNewGroupWithPrefixAndFunction()
{
GroupMaker groupMaker = newSelectorGroupMaker(false, false);
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, "2016-09-27 16:10:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, "2016-09-27 16:12:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(1, "2016-09-27 16:14:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(1, "2016-09-27 16:15:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(2, "2016-09-27 16:16:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(2, "2016-09-27 16:22:00 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(1), clustering(2, "2016-09-27 16:26:00 UTC")));
assertFalse(groupMaker.isNewGroup(partitionKey(1), clustering(2, "2016-09-27 16:26:20 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering(1, "2016-09-27 16:26:20 UTC")));
assertTrue(groupMaker.isNewGroup(partitionKey(2), clustering(1, "2016-09-27 16:30:00 UTC")));
}
private static DecoratedKey partitionKey(int... components)
{
ByteBuffer buffer = ByteBuffer.allocate(components.length * 4);
@ -162,6 +269,19 @@ public class GroupMakerTest
return Clustering.make(toByteBufferArray(components));
}
private static Clustering<?> clustering(String timeComponent)
{
ByteBuffer buffer = TimestampType.instance.fromString(timeComponent);
return Clustering.make(buffer);
}
private static Clustering<?> clustering(int component, String timeComponent)
{
ByteBuffer first = Int32Type.instance.decompose(component);
ByteBuffer second = TimestampType.instance.fromString(timeComponent);
return Clustering.make(first, second);
}
private static ByteBuffer[] toByteBufferArray(int[] values)
{
ByteBuffer[] buffers = new ByteBuffer[values.length];
@ -182,4 +302,31 @@ public class GroupMakerTest
return new ClusteringComparator(types);
}
private GroupMaker newSelectorGroupMaker(boolean... reversed)
{
TableMetadata.Builder builder = TableMetadata.builder("keyspace", "test")
.addPartitionKeyColumn("partition_key", Int32Type.instance);
int last = reversed.length - 1;
for (int i = 0; i < reversed.length; i++)
{
AbstractType<?> type = i == last ? TimestampType.instance : Int32Type.instance;
builder.addClusteringColumn("clustering" + i, reversed[i] ? ReversedType.getInstance(type) : type);
}
TableMetadata table = builder.build();
ColumnMetadata column = table.getColumn(new ColumnIdentifier("clustering" + last, false));
Selectable.WithTerm duration = new Selectable.WithTerm(Literal.duration("5m"));
Selectable.WithTerm startTime = new Selectable.WithTerm(Literal.string("2016-09-27 16:00:00 UTC"));
ScalarFunction function = TimeFcts.FloorTimestampFunction.newInstanceWithStartTimeArgument();
Selectable.WithFunction selectable = new Selectable.WithFunction(function, Arrays.asList(column, duration, startTime));
Selector.Factory factory = selectable.newSelectorFactory(table, null, new ArrayList<>(), VariableSpecifications.empty());
Selector selector = factory.newInstance(QueryOptions.DEFAULT);
return GroupMaker.newSelectorGroupMaker(table.comparator, reversed.length, selector);
}
}