Merge branch 'cassandra-3.X' of https://git-wip-us.apache.org/repos/asf/cassandra into cassandra-3.X

This commit is contained in:
Marcus Eriksson 2016-10-18 14:10:34 +02:00
commit 747a62f607
32 changed files with 1315 additions and 22 deletions

View File

@ -1,5 +1,6 @@
3.10
* Make the fanout size for LeveledCompactionStrategy to be configurable (CASSANDRA-11550)
* Add duration data type (CASSANDRA-11873)
* Fix timeout in ReplicationAwareTokenAllocatorTest (CASSANDRA-12784)
* Improve sum aggregate functions (CASSANDRA-12417)
* Make cassandra.yaml docs for batch_size_*_threshold_in_kb reflect changes in CASSANDRA-10876 (CASSANDRA-12761)

View File

@ -18,6 +18,7 @@ using the provided 'sstableupgrade' tool.
New features
------------
- New `DurationType` (cql duration). See CASSANDRA-11873
- Runtime modification of concurrent_compactors is now available via nodetool
- Support for the assignment operators +=/-= has been added for update queries.
- An Index implementation may now provide a task which runs prior to joining
@ -98,6 +99,8 @@ Upgrading
- Application layer keep-alives were added to the streaming protocol to prevent idle incoming connections from
timing out and failing the stream session (CASSANDRA-11839). This effectively deprecates the streaming_socket_timeout_in_ms
property in favor of streaming_keep_alive_period_in_secs. See cassandra.yaml for more details about this property.
- Duration litterals support the ISO 8601 format. By consequence, identifiers matching that format
(e.g P2Y or P1MT6H) will not be supported anymore (CASSANDRA-11873).
3.8
===

View File

@ -1,4 +1,3 @@
<link rel="StyleSheet" href="CQL.css" type="text/css" media="screen">
h1. Cassandra Query Language (CQL) v3.4.3

View File

@ -24,6 +24,7 @@ The following describes the changes in each version of CQL.
3.4.3
^^^^^
- Adds a new ``duration `` :ref:`data types <data-types>` (:jira:`11873`).
- Support for ``GROUP BY`` (:jira:`10707`).
- Adds a ``DEFAULT UNSET`` option for ``INSERT JSON`` to ignore omitted columns (:jira:`11424`).
- Allows ``null`` as a legal value for TTL on insert and update. It will be treated as equivalent to

View File

@ -47,6 +47,7 @@ The native types supported by CQL are:
: | DATE
: | DECIMAL
: | DOUBLE
: | DURATION
: | FLOAT
: | INET
: | INT
@ -77,6 +78,7 @@ The following table gives additional informations on the native data types, and
:token:`float`
``double`` :token:`integer` 64-bit IEEE-754 floating point
:token:`float`
``duration`` :token:`duration`, A duration with nanosecond precision. See :ref:`durations` below for details
``float`` :token:`integer`, 32-bit IEEE-754 floating point
:token:`float`
``inet`` :token:`string` An IP address, either IPv4 (4 bytes long) or IPv6 (16 bytes long). Note that
@ -179,6 +181,46 @@ time:
- ``'08:12:54.123456'``
- ``'08:12:54.123456789'``
.. _durations:
Working with durations
^^^^^^^^^^^^^^^^^^^^^^
Values of the ``duration`` type are encoded as 3 signed integer of variable lengths. The first integer represents the
number of months, the second the number of days and the third the number of nanoseconds. This is due to the fact that
the number of days in a month can change, and a day can have 23 or 25 hours depending on the daylight saving.
A duration can be input as:
#. ``(quantity unit)+`` like ``12h30m`` where the unit can be:
* ``y``: years (12 months)
* ``mo``: months (1 month)
* ``w``: weeks (7 days)
* ``d``: days (1 day)
* ``h``: hours (3,600,000,000,000 nanoseconds)
* ``m``: minutes (60,000,000,000 nanoseconds)
* ``s``: seconds (1,000,000,000 nanoseconds)
* ``ms``: milliseconds (1,000,000 nanoseconds)
* ``us`` or ``µs`` : microseconds (1000 nanoseconds)
* ``ns``: nanoseconds (1 nanosecond)
#. ISO 8601 format: ``P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W``
#. ISO 8601 alternative format: ``P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]``
For example::
INSERT INTO RiderResults (rider, race, result) VALUES ('Christopher Froome', 'Tour de France', 89h4m48s);
INSERT INTO RiderResults (rider, race, result) VALUES ('BARDET Romain', 'Tour de France', PT89H8M53S);
INSERT INTO RiderResults (rider, race, result) VALUES ('QUINTANA Nairo', 'Tour de France', P0000-00-00T89:09:09);
.. _duration-limitation:
Duration columns cannot be used in a table's ``PRIMARY KEY``. This limitation is due to the fact that
durations cannot be ordered. It is effectively not possible to know if ``1mo`` is greater than ``29d`` without a date
context.
A ``1d`` duration is not equals to a ``24h`` one as the duration type has been created to be able to support daylight
saving.
.. _collections:

View File

@ -18,8 +18,8 @@ from .cqlhandling import CqlParsingRuleSet, Hint
from cassandra.metadata import maybe_escape_name
simple_cql_types = set(('ascii', 'bigint', 'blob', 'boolean', 'counter', 'date', 'decimal', 'double', 'float', 'inet', 'int',
'smallint', 'text', 'time', 'timestamp', 'timeuuid', 'tinyint', 'uuid', 'varchar', 'varint'))
simple_cql_types = set(('ascii', 'bigint', 'blob', 'boolean', 'counter', 'date', 'decimal', 'double', 'duration', 'float',
'inet', 'int', 'smallint', 'text', 'time', 'timestamp', 'timeuuid', 'tinyint', 'uuid', 'varchar', 'varint'))
simple_cql_types.difference_update(('set', 'map', 'list'))
from . import helptopics

View File

@ -113,6 +113,7 @@ DEFAULT_VALUE_COLORS = dict(
inet=GREEN,
boolean=GREEN,
uuid=GREEN,
duration=GREEN,
collection=BLUE,
reset=ANSI_RESET,
)

View File

@ -21,6 +21,7 @@ import math
import os
import re
import sys
import six
import platform
import wcwidth
@ -328,6 +329,7 @@ formatter_for('long')(format_integer_type)
formatter_for('int')(format_integer_type)
formatter_for('bigint')(format_integer_type)
formatter_for('varint')(format_integer_type)
formatter_for('duration')(format_integer_type)
@formatter_for('datetime')
@ -384,6 +386,79 @@ def format_value_time(val, colormap, **_):
return format_python_formatted_type(val, colormap, 'time')
@formatter_for('Duration')
def format_value_duration(val, colormap, **_):
buf = six.iterbytes(val)
months = decode_vint(buf)
days = decode_vint(buf)
nanoseconds = decode_vint(buf)
return format_python_formatted_type(duration_as_str(months, days, nanoseconds), colormap, 'duration')
def duration_as_str(months, days, nanoseconds):
builder = list()
if months < 0 or days < 0 or nanoseconds < 0:
builder.append('-')
remainder = append(builder, abs(months), MONTHS_PER_YEAR, "y")
append(builder, remainder, 1, "mo")
append(builder, abs(days), 1, "d")
if nanoseconds != 0:
remainder = append(builder, abs(nanoseconds), NANOS_PER_HOUR, "h")
remainder = append(builder, remainder, NANOS_PER_MINUTE, "m")
remainder = append(builder, remainder, NANOS_PER_SECOND, "s")
remainder = append(builder, remainder, NANOS_PER_MILLI, "ms")
remainder = append(builder, remainder, NANOS_PER_MICRO, "us")
append(builder, remainder, 1, "ns")
return ''.join(builder)
def append(builder, dividend, divisor, unit):
if dividend == 0 or dividend < divisor:
return dividend
builder.append(str(dividend / divisor))
builder.append(unit)
return dividend % divisor
def decode_vint(buf):
return decode_zig_zag_64(decode_unsigned_vint(buf))
def decode_unsigned_vint(buf):
"""
Cassandra vints are encoded differently than the varints used in protocol buffer.
The Cassandra vints are encoded with the most significant group first. The most significant byte will contains
the information about how many extra bytes need to be read as well as the most significant bits of the integer.
The number extra bytes to read is encoded as 1 bits on the left side.
For example, if we need to read 3 more bytes the first byte will start with 1110.
"""
first_byte = buf.next()
if (first_byte >> 7) == 0:
return first_byte
size = number_of_extra_bytes_to_read(first_byte)
retval = first_byte & (0xff >> size)
for i in range(size):
b = buf.next()
retval <<= 8
retval |= b & 0xff
return retval
def number_of_extra_bytes_to_read(b):
return 8 - (~b & 0xff).bit_length()
def decode_zig_zag_64(n):
return (n >> 1) ^ -(n & 1)
@formatter_for('str')
def format_value_text(val, encoding, colormap, quote=False, **_):
escapedval = val.replace(u'\\', u'\\\\')
@ -501,3 +576,10 @@ def format_value_utype(val, cqltype, encoding, colormap, date_time_format, float
+ rb
displaywidth = 4 * len(subs) + sum(k.displaywidth + v.displaywidth for (k, v) in subs)
return FormattedValue(bval, coloredval, displaywidth)
NANOS_PER_MICRO = 1000
NANOS_PER_MILLI = 1000 * NANOS_PER_MICRO
NANOS_PER_SECOND = 1000 * NANOS_PER_MILLI
NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND
NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE
MONTHS_PER_YEAR = 12

View File

@ -154,6 +154,7 @@ K_BOOLEAN: B O O L E A N;
K_COUNTER: C O U N T E R;
K_DECIMAL: D E C I M A L;
K_DOUBLE: D O U B L E;
K_DURATION: D U R A T I O N;
K_FLOAT: F L O A T;
K_INET: I N E T;
K_INT: I N T;
@ -275,6 +276,20 @@ fragment EXPONENT
: E ('+' | '-')? DIGIT+
;
fragment DURATION_UNIT
: Y
| M O
| W
| D
| H
| M
| S
| M S
| U S
| '\u00B5' S
| N S
;
INTEGER
: '-'? DIGIT+
;
@ -299,6 +314,13 @@ BOOLEAN
: T R U E | F A L S E
;
DURATION
: '-'? DIGIT+ DURATION_UNIT (DIGIT+ DURATION_UNIT)*
| '-'? 'P' (DIGIT+ 'Y')? (DIGIT+ 'M')? (DIGIT+ 'D')? ('T' (DIGIT+ 'H')? (DIGIT+ 'M')? (DIGIT+ 'S')?)? // ISO 8601 "format with designators"
| '-'? 'P' DIGIT+ 'W'
| '-'? 'P' DIGIT DIGIT DIGIT DIGIT '-' DIGIT DIGIT '-' DIGIT DIGIT 'T' DIGIT DIGIT ':' DIGIT DIGIT ':' DIGIT DIGIT // ISO 8601 "alternative format"
;
IDENT
: LETTER (LETTER | DIGIT | '_')*
;

View File

@ -1255,6 +1255,7 @@ constant returns [Constants.Literal constant]
| t=INTEGER { $constant = Constants.Literal.integer($t.text); }
| t=FLOAT { $constant = Constants.Literal.floatingPoint($t.text); }
| t=BOOLEAN { $constant = Constants.Literal.bool($t.text); }
| t=DURATION { $constant = Constants.Literal.duration($t.text);}
| t=UUID { $constant = Constants.Literal.uuid($t.text); }
| t=HEXNUMBER { $constant = Constants.Literal.hex($t.text); }
| { String sign=""; } ('-' {sign = "-"; } )? t=(K_NAN | K_INFINITY) { $constant = Constants.Literal.floatingPoint(sign + $t.text); }
@ -1556,6 +1557,7 @@ native_type returns [CQL3Type t]
| K_COUNTER { $t = CQL3Type.Native.COUNTER; }
| K_DECIMAL { $t = CQL3Type.Native.DECIMAL; }
| K_DOUBLE { $t = CQL3Type.Native.DOUBLE; }
| K_DURATION { $t = CQL3Type.Native.DURATION; }
| K_FLOAT { $t = CQL3Type.Native.FLOAT; }
| K_INET { $t = CQL3Type.Native.INET;}
| K_INT { $t = CQL3Type.Native.INT; }

View File

@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.db.marshal.CollectionType.Kind;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
@ -70,6 +71,7 @@ public interface CQL3Type
DATE (SimpleDateType.instance),
DECIMAL (DecimalType.instance),
DOUBLE (DoubleType.instance),
DURATION (DurationType.instance),
EMPTY (EmptyType.instance),
FLOAT (FloatType.instance),
INET (InetAddressType.instance),
@ -494,6 +496,11 @@ public interface CQL3Type
return true;
}
public boolean isDuration()
{
return false;
}
public boolean isCounter()
{
return false;
@ -595,6 +602,11 @@ public interface CQL3Type
return type == Native.COUNTER;
}
public boolean isDuration()
{
return type == Native.DURATION;
}
@Override
public String toString()
{
@ -656,10 +668,15 @@ public interface CQL3Type
if (values.isCounter() && !isInternal)
throw new InvalidRequestException("Counters are not allowed inside collections: " + this);
if (values.isDuration() && kind == Kind.SET)
throw new InvalidRequestException("Durations are not allowed inside sets: " + this);
if (keys != null)
{
if (keys.isCounter())
throw new InvalidRequestException("Counters are not allowed inside collections: " + this);
if (keys.isDuration())
throw new InvalidRequestException("Durations are not allowed as map keys: " + this);
if (!frozen && keys.supportsFreezing() && !keys.frozen)
throwNestedNonFrozenError(keys);
}

View File

@ -37,7 +37,7 @@ public abstract class Constants
public enum Type
{
STRING, INTEGER, UUID, FLOAT, BOOLEAN, HEX;
STRING, INTEGER, UUID, FLOAT, BOOLEAN, HEX, DURATION;
}
private static class UnsetLiteral extends Term.Raw
@ -156,6 +156,11 @@ public abstract class Constants
return new Literal(Type.HEX, text);
}
public static Literal duration(String text)
{
return new Literal(Type.DURATION, text);
}
public Value prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
{
if (!testAssignment(keyspace, receiver).isAssignable())
@ -221,6 +226,7 @@ public abstract class Constants
case DATE:
case DECIMAL:
case DOUBLE:
case DURATION:
case FLOAT:
case INT:
case SMALLINT:
@ -262,6 +268,13 @@ public abstract class Constants
return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
}
break;
case DURATION:
switch (nt)
{
case DURATION:
return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE;
}
break;
}
return AssignmentTestable.TestResult.NOT_ASSIGNABLE;
}

View File

@ -0,0 +1,590 @@
/*
* 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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Objects;
import org.apache.cassandra.serializers.MarshalException;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
/**
* 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.
*/
public final class Duration
{
public static final long NANOS_PER_MICRO = 1000L;
public static final long NANOS_PER_MILLI = 1000 * NANOS_PER_MICRO;
public static final long NANOS_PER_SECOND = 1000 * NANOS_PER_MILLI;
public static final long NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND;
public static final long NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE;
public static final int DAYS_PER_WEEK = 7;
public static final int MONTHS_PER_YEAR = 12;
/**
* The Regexp used to parse the duration provided as String.
*/
private static final Pattern STANDARD_PATTERN =
Pattern.compile("\\G(\\d+)(y|Y|mo|MO|mO|Mo|w|W|d|D|h|H|s|S|ms|MS|mS|Ms|us|US|uS|Us|µs|µS|ns|NS|nS|Ns|m|M)");
/**
* The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
*/
private static final Pattern ISO8601_PATTERN =
Pattern.compile("P((\\d+)Y)?((\\d+)M)?((\\d+)D)?(T((\\d+)H)?((\\d+)M)?((\\d+)S)?)?");
/**
* The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
*/
private static final Pattern ISO8601_WEEK_PATTERN = Pattern.compile("P(\\d+)W");
/**
* The Regexp used to parse the duration when provided in the ISO 8601 alternative format.
*/
private static final Pattern ISO8601_ALTERNATIVE_PATTERN =
Pattern.compile("P(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})");
/**
* The number of months.
*/
private final int months;
/**
* The number of days.
*/
private final int days;
/**
* The number of nanoseconds.
*/
private final long nanoseconds;
/**
* Creates a duration. A duration can be negative.
* In this case all the non zero values must be negatives.
*
* @param months the number of months
* @param days the number of days
* @param nanoseconds the number of nanoseconds
*/
private Duration(int months, int days, long nanoseconds)
{
// Makes sure that all the values are negatives if one of them is
assert (months >= 0 && days >= 0 && nanoseconds >= 0)
|| ((months <= 0 && days <=0 && nanoseconds <=0));
this.months = months;
this.days = days;
this.nanoseconds = nanoseconds;
}
public static Duration newInstance(int months, int days, long nanoseconds)
{
return new Duration(months, days, nanoseconds);
}
/**
* Converts a <code>String</code> into a duration.
* <p>The accepted formats are:
* <ul>
* <li>multiple digits followed by a time unit like: 12h30m where the time unit can be:
* <ul>
* <li>{@code y}: years</li>
* <li>{@code m}: months</li>
* <li>{@code w}: weeks</li>
* <li>{@code d}: days</li>
* <li>{@code h}: hours</li>
* <li>{@code m}: minutes</li>
* <li>{@code s}: seconds</li>
* <li>{@code ms}: milliseconds</li>
* <li>{@code us} or {@code µs}: microseconds</li>
* <li>{@code ns}: nanoseconds</li>
* </ul>
* </li>
* <li>ISO 8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W</li>
* <li>ISO 8601 alternative format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]</li>
* </ul>
*
* @param input the <code>String</code> to convert
* @return a number of nanoseconds
*/
public static Duration from(String input)
{
boolean isNegative = input.startsWith("-");
String source = isNegative ? input.substring(1) : input;
if (source.startsWith("P"))
{
if (source.endsWith("W"))
return parseIso8601WeekFormat(isNegative, source);
if (source.contains("-"))
return parseIso8601AlternativeFormat(isNegative, source);
return parseIso8601Format(isNegative, source);
}
return parseStandardFormat(isNegative, source);
}
private static Duration parseIso8601Format(boolean isNegative, String source)
{
Matcher matcher = ISO8601_PATTERN.matcher(source);
if (!matcher.matches())
throw invalidRequest("Unable to convert '%s' to a duration", source);
Builder builder = new Builder(isNegative);
if (matcher.group(1) != null)
builder.addYears(groupAsLong(matcher, 2));
if (matcher.group(3) != null)
builder.addMonths(groupAsLong(matcher, 4));
if (matcher.group(5) != null)
builder.addDays(groupAsLong(matcher, 6));
// Checks if the String contains time information
if (matcher.group(7) != null)
{
if (matcher.group(8) != null)
builder.addHours(groupAsLong(matcher, 9));
if (matcher.group(10) != null)
builder.addMinutes(groupAsLong(matcher, 11));
if (matcher.group(12) != null)
builder.addSeconds(groupAsLong(matcher, 13));
}
return builder.build();
}
private static Duration parseIso8601AlternativeFormat(boolean isNegative, String source)
{
Matcher matcher = ISO8601_ALTERNATIVE_PATTERN.matcher(source);
if (!matcher.matches())
throw invalidRequest("Unable to convert '%s' to a duration", source);
return new Builder(isNegative).addYears(groupAsLong(matcher, 1))
.addMonths(groupAsLong(matcher, 2))
.addDays(groupAsLong(matcher, 3))
.addHours(groupAsLong(matcher, 4))
.addMinutes(groupAsLong(matcher, 5))
.addSeconds(groupAsLong(matcher, 6))
.build();
}
private static Duration parseIso8601WeekFormat(boolean isNegative, String source)
{
Matcher matcher = ISO8601_WEEK_PATTERN.matcher(source);
if (!matcher.matches())
throw invalidRequest("Unable to convert '%s' to a duration", source);
return new Builder(isNegative).addWeeks(groupAsLong(matcher, 1))
.build();
}
private static Duration parseStandardFormat(boolean isNegative, String source)
{
Matcher matcher = STANDARD_PATTERN.matcher(source);
if (!matcher.find())
throw invalidRequest("Unable to convert '%s' to a duration", source);
Builder builder = new Builder(isNegative);
boolean done = false;
do
{
long number = groupAsLong(matcher, 1);
String symbol = matcher.group(2);
add(builder, number, symbol);
done = matcher.end() == source.length();
}
while (matcher.find());
if (!done)
throw invalidRequest("Unable to convert '%s' to a duration", source);
return builder.build();
}
private static long groupAsLong(Matcher matcher, int group)
{
return Long.parseLong(matcher.group(group));
}
private static Builder add(Builder builder, long number, String symbol)
{
switch (symbol.toLowerCase())
{
case "y": return builder.addYears(number);
case "mo": return builder.addMonths(number);
case "w": return builder.addWeeks(number);
case "d": return builder.addDays(number);
case "h": return builder.addHours(number);
case "m": return builder.addMinutes(number);
case "s": return builder.addSeconds(number);
case "ms": return builder.addMillis(number);
case "us":
case "µs": return builder.addMicros(number);
case "ns": return builder.addNanos(number);
}
throw new MarshalException(String.format("Unknown duration symbol '%s'", symbol));
}
public int getMonths()
{
return months;
}
public int getDays()
{
return days;
}
public long getNanoseconds()
{
return nanoseconds;
}
@Override
public int hashCode()
{
return Objects.hashCode(days, months, nanoseconds);
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof Duration))
return false;
Duration other = (Duration) obj;
return days == other.days
&& months == other.months
&& nanoseconds == other.nanoseconds;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
if (months < 0 || days < 0 || nanoseconds < 0)
builder.append('-');
long remainder = append(builder, Math.abs(months), MONTHS_PER_YEAR, "y");
append(builder, remainder, 1, "mo");
append(builder, Math.abs(days), 1, "d");
if (nanoseconds != 0)
{
remainder = append(builder, Math.abs(nanoseconds), NANOS_PER_HOUR, "h");
remainder = append(builder, remainder, NANOS_PER_MINUTE, "m");
remainder = append(builder, remainder, NANOS_PER_SECOND, "s");
remainder = append(builder, remainder, NANOS_PER_MILLI, "ms");
remainder = append(builder, remainder, NANOS_PER_MICRO, "us");
append(builder, remainder, 1, "ns");
}
return builder.toString();
}
/**
* Appends the result of the division to the specified builder if the dividend is not zero.
*
* @param builder the builder to append to
* @param dividend the dividend
* @param divisor the divisor
* @param unit the time unit to append after the result of the division
* @return the remainder of the division
*/
private static long append(StringBuilder builder, long dividend, long divisor, String unit)
{
if (dividend == 0 || dividend < divisor)
return dividend;
builder.append(dividend / divisor).append(unit);
return dividend % divisor;
}
private static class Builder
{
/**
* {@code true} if the duration is a negative one, {@code false} otherwise.
*/
private final boolean isNegative;
/**
* The number of months.
*/
private int months;
/**
* The number of days.
*/
private int days;
/**
* The number of nanoseconds.
*/
private long nanoseconds;
/**
* We need to make sure that the values for each units are provided in order.
*/
private int currentUnitIndex;
public Builder(boolean isNegative)
{
this.isNegative = isNegative;
}
/**
* Adds the specified amount of years.
*
* @param numberOfYears the number of years to add.
* @return this {@code Builder}
*/
public Builder addYears(long numberOfYears)
{
validateOrder(1);
validateMonths(numberOfYears, MONTHS_PER_YEAR);
months += numberOfYears * MONTHS_PER_YEAR;
return this;
}
/**
* Adds the specified amount of months.
*
* @param numberOfMonths the number of months to add.
* @return this {@code Builder}
*/
public Builder addMonths(long numberOfMonths)
{
validateOrder(2);
validateMonths(numberOfMonths, 1);
months += numberOfMonths;
return this;
}
/**
* Adds the specified amount of weeks.
*
* @param numberOfWeeks the number of weeks to add.
* @return this {@code Builder}
*/
public Builder addWeeks(long numberOfWeeks)
{
validateOrder(3);
validateDays(numberOfWeeks, DAYS_PER_WEEK);
days += numberOfWeeks * DAYS_PER_WEEK;
return this;
}
/**
* Adds the specified amount of days.
*
* @param numberOfDays the number of days to add.
* @return this {@code Builder}
*/
public Builder addDays(long numberOfDays)
{
validateOrder(4);
validateDays(numberOfDays, 1);
days += numberOfDays;
return this;
}
/**
* Adds the specified amount of hours.
*
* @param numberOfHours the number of hours to add.
* @return this {@code Builder}
*/
public Builder addHours(long numberOfHours)
{
validateOrder(5);
validateNanos(numberOfHours, NANOS_PER_HOUR);
nanoseconds += numberOfHours * NANOS_PER_HOUR;
return this;
}
/**
* Adds the specified amount of minutes.
*
* @param numberOfMinutes the number of minutes to add.
* @return this {@code Builder}
*/
public Builder addMinutes(long numberOfMinutes)
{
validateOrder(6);
validateNanos(numberOfMinutes, NANOS_PER_MINUTE);
nanoseconds += numberOfMinutes * NANOS_PER_MINUTE;
return this;
}
/**
* Adds the specified amount of seconds.
*
* @param numberOfSeconds the number of seconds to add.
* @return this {@code Builder}
*/
public Builder addSeconds(long numberOfSeconds)
{
validateOrder(7);
validateNanos(numberOfSeconds, NANOS_PER_SECOND);
nanoseconds += numberOfSeconds * NANOS_PER_SECOND;
return this;
}
/**
* Adds the specified amount of milliseconds.
*
* @param numberOfMillis the number of milliseconds to add.
* @return this {@code Builder}
*/
public Builder addMillis(long numberOfMillis)
{
validateOrder(8);
validateNanos(numberOfMillis, NANOS_PER_MILLI);
nanoseconds += numberOfMillis * NANOS_PER_MILLI;
return this;
}
/**
* Adds the specified amount of microseconds.
*
* @param numberOfMicros the number of microseconds to add.
* @return this {@code Builder}
*/
public Builder addMicros(long numberOfMicros)
{
validateOrder(9);
validateNanos(numberOfMicros, NANOS_PER_MICRO);
nanoseconds += numberOfMicros * NANOS_PER_MICRO;
return this;
}
/**
* Adds the specified amount of nanoseconds.
*
* @param numberOfNanos the number of nanoseconds to add.
* @return this {@code Builder}
*/
public Builder addNanos(long numberOfNanos)
{
validateOrder(10);
validateNanos(numberOfNanos, 1);
nanoseconds += numberOfNanos;
return this;
}
/**
* Validates that the total number of months can be stored.
* @param units the number of units that need to be added
* @param monthsPerUnit the number of days per unit
*/
private void validateMonths(long units, int monthsPerUnit)
{
validate(units, (Integer.MAX_VALUE - months) / monthsPerUnit, "months");
}
/**
* Validates that the total number of days can be stored.
* @param units the number of units that need to be added
* @param daysPerUnit the number of days per unit
*/
private void validateDays(long units, int daysPerUnit)
{
validate(units, (Integer.MAX_VALUE - days) / daysPerUnit, "days");
}
/**
* Validates that the total number of nanoseconds can be stored.
* @param units the number of units that need to be added
* @param nanosPerUnit the number of nanoseconds per unit
*/
private void validateNanos(long units, long nanosPerUnit)
{
validate(units, (Long.MAX_VALUE - nanoseconds) / nanosPerUnit, "nanoseconds");
}
/**
* Validates that the specified amount is less than the limit.
* @param units the number of units to check
* @param limit the limit on the number of units
* @param unitName the unit name
*/
private void validate(long units, long limit, String unitName)
{
checkTrue(units <= limit,
"Invalid duration. The total number of %s must be less or equal to %s",
unitName,
Integer.MAX_VALUE);
}
/**
* Validates that the duration values are added in the proper order.
* @param unitIndex the unit index (e.g. years=1, months=2, ...)
*/
private void validateOrder(int unitIndex)
{
if (unitIndex == currentUnitIndex)
throw invalidRequest("Invalid duration. The %s are specified multiple times", getUnitName(unitIndex));
if (unitIndex <= currentUnitIndex)
throw invalidRequest("Invalid duration. The %s should be after %s",
getUnitName(currentUnitIndex),
getUnitName(unitIndex));
currentUnitIndex = unitIndex;
}
/**
* Returns the name of the unit corresponding to the specified index.
* @param unitIndex the unit index
* @return the name of the unit corresponding to the specified index.
*/
private String getUnitName(int unitIndex)
{
switch (unitIndex)
{
case 1: return "years";
case 2: return "months";
case 3: return "weeks";
case 4: return "days";
case 5: return "hours";
case 6: return "minutes";
case 7: return "seconds";
case 8: return "milliseconds";
case 9: return "microseconds";
case 10: return "nanoseconds";
default: throw new AssertionError("unknown unit index: " + unitIndex);
}
}
public Duration build()
{
return isNegative ? new Duration(-months, -days, -nanoseconds) : new Duration(months, days, nanoseconds);
}
}
}

View File

@ -28,6 +28,7 @@ import org.apache.cassandra.cql3.restrictions.Restriction;
import org.apache.cassandra.cql3.restrictions.SingleColumnRestriction;
import org.apache.cassandra.cql3.statements.Bound;
import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.DurationType;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.exceptions.InvalidRequestException;
@ -197,6 +198,8 @@ public final class SingleColumnRelation extends Relation
boolean inclusive) throws InvalidRequestException
{
ColumnDefinition columnDef = entity.prepare(cfm);
checkFalse(columnDef.type instanceof DurationType, "Slice restriction are not supported on duration columns");
Term term = toTerm(toReceivers(columnDef), value, cfm.ksName, boundNames);
return new SingleColumnRestriction.SliceRestriction(columnDef, bound, inclusive, term);
}

View File

@ -259,6 +259,8 @@ public class CreateTableStatement extends SchemaAlteringStatement
AbstractType<?> t = getTypeAndRemove(stmt.columns, alias);
if (t.asCQL3Type().getType() instanceof CounterColumnType)
throw new InvalidRequestException(String.format("counter type is not supported for PRIMARY KEY part %s", alias));
if (t.asCQL3Type().getType().referencesDuration())
throw new InvalidRequestException(String.format("duration type is not supported for PRIMARY KEY part %s", alias));
if (staticColumns.contains(alias))
throw new InvalidRequestException(String.format("Static column %s cannot be part of the PRIMARY KEY", alias));
stmt.keyTypes.add(t);
@ -273,6 +275,8 @@ public class CreateTableStatement extends SchemaAlteringStatement
AbstractType<?> type = getTypeAndRemove(stmt.columns, t);
if (type.asCQL3Type().getType() instanceof CounterColumnType)
throw new InvalidRequestException(String.format("counter type is not supported for PRIMARY KEY part %s", t));
if (type.asCQL3Type().getType().referencesDuration())
throw new InvalidRequestException(String.format("duration type is not supported for PRIMARY KEY part %s", t));
if (staticColumns.contains(t))
throw new InvalidRequestException(String.format("Static column %s cannot be part of the PRIMARY KEY", t));
stmt.clusteringTypes.add(type);

View File

@ -19,7 +19,6 @@
package org.apache.cassandra.cql3.statements;
import java.util.*;
import java.util.stream.Collectors;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
@ -34,6 +33,7 @@ import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
import org.apache.cassandra.cql3.selection.RawSelector;
import org.apache.cassandra.cql3.selection.Selectable;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.DurationType;
import org.apache.cassandra.db.marshal.ReversedType;
import org.apache.cassandra.db.view.View;
import org.apache.cassandra.exceptions.AlreadyExistsException;
@ -184,6 +184,9 @@ public class CreateViewStatement extends SchemaAlteringStatement
if (cdef.isStatic())
throw new InvalidRequestException(String.format("Cannot use Static column '%s' in PRIMARY KEY of materialized view", identifier));
if (cdef.type instanceof DurationType)
throw new InvalidRequestException(String.format("Cannot use Duration column '%s' in PRIMARY KEY of materialized view", identifier));
}
// build the select statement

View File

@ -446,6 +446,11 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>, Assignm
return false;
}
public boolean referencesDuration()
{
return false;
}
/**
* This must be overriden by subclasses if necessary so that for any
* AbstractType, this == TypeParser.parse(toString()).

View File

@ -0,0 +1,95 @@
/*
* 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.db.marshal;
import java.nio.ByteBuffer;
import org.apache.cassandra.cql3.CQL3Type;
import org.apache.cassandra.cql3.Constants;
import org.apache.cassandra.cql3.Duration;
import org.apache.cassandra.cql3.Term;
import org.apache.cassandra.serializers.DurationSerializer;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.serializers.TypeSerializer;
import org.apache.cassandra.utils.ByteBufferUtil;
/**
* Represents a duration. The duration is stored as months, days, and nanoseconds. This is done
* <p>Internally he duration is stored as months (unsigned integer), days (unsigned integer), and nanoseconds.</p>
*/
public class DurationType extends AbstractType<Duration>
{
public static final DurationType instance = new DurationType();
DurationType()
{
super(ComparisonType.NOT_COMPARABLE);
} // singleton
public ByteBuffer fromString(String source) throws MarshalException
{
// Return an empty ByteBuffer for an empty string.
if (source.isEmpty())
return ByteBufferUtil.EMPTY_BYTE_BUFFER;
return decompose(Duration.from(source));
}
@Override
public boolean isValueCompatibleWithInternal(AbstractType<?> otherType)
{
return this == otherType;
}
public Term fromJSONObject(Object parsed) throws MarshalException
{
try
{
return new Constants.Value(fromString((String) parsed));
}
catch (ClassCastException exc)
{
throw new MarshalException(String.format("Expected a string representation of a duration, but got a %s: %s",
parsed.getClass().getSimpleName(), parsed));
}
}
@Override
public String toJSONString(ByteBuffer buffer, int protocolVersion)
{
return getSerializer().deserialize(buffer).toString();
}
@Override
public TypeSerializer<Duration> getSerializer()
{
return DurationSerializer.instance;
}
@Override
public CQL3Type asCQL3Type()
{
return CQL3Type.Native.DURATION;
}
@Override
public boolean referencesDuration()
{
return true;
}
}

View File

@ -80,6 +80,12 @@ public class ListType<T> extends CollectionType<List<T>>
return getElementsType().referencesUserType(userTypeName);
}
@Override
public boolean referencesDuration()
{
return getElementsType().referencesDuration();
}
public AbstractType<T> getElementsType()
{
return elements;

View File

@ -81,6 +81,13 @@ public class MapType<K, V> extends CollectionType<Map<K, V>>
getValuesType().referencesUserType(userTypeName);
}
@Override
public boolean referencesDuration()
{
// Maps cannot be created with duration as keys
return getValuesType().referencesDuration();
}
public AbstractType<K> getKeysType()
{
return keys;

View File

@ -79,6 +79,12 @@ public class TupleType extends AbstractType<ByteBuffer>
return allTypes().stream().anyMatch(f -> f.referencesUserType(name));
}
@Override
public boolean referencesDuration()
{
return allTypes().stream().anyMatch(f -> f.referencesDuration());
}
public AbstractType<?> type(int i)
{
return types.get(i);

View File

@ -378,6 +378,12 @@ public class UserType extends TupleType
fieldTypes().stream().anyMatch(f -> f.referencesUserType(userTypeName));
}
@Override
public boolean referencesDuration()
{
return fieldTypes().stream().anyMatch(f -> f.referencesDuration());
}
@Override
public String toString()
{

View File

@ -0,0 +1,94 @@
/*
* 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.serializers;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.apache.cassandra.cql3.Duration;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataOutputBufferFixed;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.vint.VIntCoding;
public final class DurationSerializer implements TypeSerializer<Duration>
{
public static final DurationSerializer instance = new DurationSerializer();
public ByteBuffer serialize(Duration duration)
{
if (duration == null)
return ByteBufferUtil.EMPTY_BYTE_BUFFER;
long months = duration.getMonths();
long days = duration.getDays();
long nanoseconds = duration.getNanoseconds();
int size = VIntCoding.computeVIntSize(months)
+ VIntCoding.computeVIntSize(days)
+ VIntCoding.computeVIntSize(nanoseconds);
try (DataOutputBufferFixed output = new DataOutputBufferFixed(size))
{
output.writeVInt(months);
output.writeVInt(days);
output.writeVInt(nanoseconds);
return output.buffer();
}
catch (IOException e)
{
// this should never happen with a DataOutputBufferFixed
throw new AssertionError("Unexpected error", e);
}
}
public Duration deserialize(ByteBuffer bytes)
{
if (bytes.remaining() == 0)
return null;
try (DataInputBuffer in = new DataInputBuffer(bytes, true))
{
int months = (int) in.readVInt();
int days = (int) in.readVInt();
long nanoseconds = in.readVInt();
return Duration.newInstance(months, days, nanoseconds);
}
catch (IOException e)
{
// this should never happen with a DataInputBuffer
throw new AssertionError("Unexpected error", e);
}
}
public void validate(ByteBuffer bytes) throws MarshalException
{
if (bytes.remaining() < 3)
throw new MarshalException(String.format("Expected at least 3 bytes for a duration (%d)", bytes.remaining()));
}
public String toString(Duration duration)
{
return duration == null ? "" : duration.toString();
}
public Class<Duration> getType()
{
return Duration.class;
}
}

View File

@ -188,17 +188,17 @@ public class CFMetaDataTest
}
private static Set<String> primitiveTypes = new HashSet<String>(Arrays.asList(new String[] { "ascii", "bigint", "blob", "boolean", "date",
"decimal", "double", "float", "inet", "int",
"smallint", "text", "time", "timestamp",
"timeuuid", "tinyint", "uuid", "varchar",
"varint" }));
"duration", "decimal", "double", "float",
"inet", "int", "smallint", "text", "time",
"timestamp", "timeuuid", "tinyint", "uuid",
"varchar", "varint" }));
@Test
public void typeCompatibilityTest() throws Throwable
{
Map<String, Set<String>> compatibilityMap = new HashMap<>();
compatibilityMap.put("bigint", new HashSet<>(Arrays.asList(new String[] {"timestamp"})));
compatibilityMap.put("blob", new HashSet<>(Arrays.asList(new String[] {"ascii", "bigint", "boolean", "date", "decimal", "double",
compatibilityMap.put("blob", new HashSet<>(Arrays.asList(new String[] {"ascii", "bigint", "boolean", "date", "decimal", "double", "duration",
"float", "inet", "int", "smallint", "text", "time", "timestamp",
"timeuuid", "tinyint", "uuid", "varchar", "varint"})));
compatibilityMap.put("date", new HashSet<>(Arrays.asList(new String[] {"int"})));

View File

@ -207,6 +207,13 @@ public class CQL3TypeLiteralTest
}
addNativeValue("null", CQL3Type.Native.TIME, null);
for (int i = 0; i < 100; i++)
{
Duration duration = Duration.newInstance(Math.abs(randInt()), Math.abs(randInt()), Math.abs(randLong()));
addNativeValue(DurationSerializer.instance.toString(duration), CQL3Type.Native.DURATION, DurationSerializer.instance.serialize(duration));
}
addNativeValue("null", CQL3Type.Native.DURATION, null);
// (mostly generates timestamp values with surreal values like in year 14273)
for (int i = 0; i < 20; i++)
{

View File

@ -1517,6 +1517,9 @@ public abstract class CQLTester
if (value instanceof Float)
return FloatType.instance;
if (value instanceof Duration)
return DurationType.instance;
if (value instanceof Double)
return DoubleType.instance;

View File

@ -0,0 +1,114 @@
/**
* 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;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import org.apache.cassandra.exceptions.InvalidRequestException;
import static org.apache.cassandra.cql3.Duration.*;
import static org.apache.cassandra.cql3.Duration.NANOS_PER_HOUR;
public class DurationTest
{
@Test
public void testFromStringWithStandardPattern()
{
assertEquals(Duration.newInstance(14, 0, 0), Duration.from("1y2mo"));
assertEquals(Duration.newInstance(-14, 0, 0), Duration.from("-1y2mo"));
assertEquals(Duration.newInstance(14, 0, 0), Duration.from("1Y2MO"));
assertEquals(Duration.newInstance(0, 14, 0), Duration.from("2w"));
assertEquals(Duration.newInstance(0, 2, 10 * NANOS_PER_HOUR), Duration.from("2d10h"));
assertEquals(Duration.newInstance(0, 2, 0), Duration.from("2d"));
assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR), Duration.from("30h"));
assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE), Duration.from("30h20m"));
assertEquals(Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE), Duration.from("20m"));
assertEquals(Duration.newInstance(0, 0, 56 * NANOS_PER_SECOND), Duration.from("56s"));
assertEquals(Duration.newInstance(0, 0, 567 * NANOS_PER_MILLI), Duration.from("567ms"));
assertEquals(Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO), Duration.from("1950us"));
assertEquals(Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO), Duration.from("1950µs"));
assertEquals(Duration.newInstance(0, 0, 1950000), Duration.from("1950000ns"));
assertEquals(Duration.newInstance(0, 0, 1950000), Duration.from("1950000NS"));
assertEquals(Duration.newInstance(0, 0, -1950000), Duration.from("-1950000ns"));
assertEquals(Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE), Duration.from("1y3mo2h10m"));
}
@Test
public void testFromStringWithIso8601Pattern()
{
assertEquals(Duration.newInstance(12, 2, 0), Duration.from("P1Y2D"));
assertEquals(Duration.newInstance(14, 0, 0), Duration.from("P1Y2M"));
assertEquals(Duration.newInstance(0, 14, 0), Duration.from("P2W"));
assertEquals(Duration.newInstance(12, 0, 2 * NANOS_PER_HOUR), Duration.from("P1YT2H"));
assertEquals(Duration.newInstance(-14, 0, 0), Duration.from("-P1Y2M"));
assertEquals(Duration.newInstance(0, 2, 0), Duration.from("P2D"));
assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR), Duration.from("PT30H"));
assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE), Duration.from("PT30H20M"));
assertEquals(Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE), Duration.from("PT20M"));
assertEquals(Duration.newInstance(0, 0, 56 * NANOS_PER_SECOND), Duration.from("PT56S"));
assertEquals(Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE), Duration.from("P1Y3MT2H10M"));
}
@Test
public void testFromStringWithIso8601AlternativePattern()
{
assertEquals(Duration.newInstance(12, 2, 0), Duration.from("P0001-00-02T00:00:00"));
assertEquals(Duration.newInstance(14, 0, 0), Duration.from("P0001-02-00T00:00:00"));
assertEquals(Duration.newInstance(12, 0, 2 * NANOS_PER_HOUR), Duration.from("P0001-00-00T02:00:00"));
assertEquals(Duration.newInstance(-14, 0, 0), Duration.from("-P0001-02-00T00:00:00"));
assertEquals(Duration.newInstance(0, 2, 0), Duration.from("P0000-00-02T00:00:00"));
assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR), Duration.from("P0000-00-00T30:00:00"));
assertEquals(Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE), Duration.from("P0000-00-00T30:20:00"));
assertEquals(Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE), Duration.from("P0000-00-00T00:20:00"));
assertEquals(Duration.newInstance(0, 0, 56 * NANOS_PER_SECOND), Duration.from("P0000-00-00T00:00:56"));
assertEquals(Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE), Duration.from("P0001-03-00T02:10:00"));
}
@Test
public void testInvalidDurations()
{
assertInvalidDuration(Long.MAX_VALUE + "d", "Invalid duration. The total number of days must be less or equal to 2147483647");
assertInvalidDuration("", "Unable to convert '2µ' to a duration");
assertInvalidDuration("-2µ", "Unable to convert '2µ' to a duration");
assertInvalidDuration("12.5s", "Unable to convert '12.5s' to a duration");
assertInvalidDuration("2m12.5s", "Unable to convert '2m12.5s' to a duration");
assertInvalidDuration("2m-12s", "Unable to convert '2m-12s' to a duration");
assertInvalidDuration("12s3s", "Invalid duration. The seconds are specified multiple times");
assertInvalidDuration("12s3m", "Invalid duration. The seconds should be after minutes");
assertInvalidDuration("1Y3M4D", "Invalid duration. The minutes should be after days");
assertInvalidDuration("P2Y3W", "Unable to convert 'P2Y3W' to a duration");
assertInvalidDuration("P0002-00-20", "Unable to convert 'P0002-00-20' to a duration");
}
public void assertInvalidDuration(String duration, String expectedErrorMessage)
{
try
{
System.out.println(Duration.from(duration));
Assert.fail();
}
catch (InvalidRequestException e)
{
assertEquals(expectedErrorMessage, e.getMessage());
}
}
}

View File

@ -347,6 +347,27 @@ public class ViewTest extends CQLTester
}
}
@Test
public void testDurationsTable() throws Throwable
{
createTable("CREATE TABLE %s (" +
"k int PRIMARY KEY, " +
"result duration)");
execute("USE " + keyspace());
executeNet(protocolVersion, "USE " + keyspace());
try
{
createView("mv_duration", "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s WHERE result IS NOT NULL AND k IS NOT NULL PRIMARY KEY (result,k)");
Assert.fail("MV on duration should fail");
}
catch (InvalidQueryException e)
{
Assert.assertEquals("Cannot use Duration column 'result' in PRIMARY KEY of materialized view", e.getMessage());
}
}
@Test
public void complexTimestampUpdateTestWithFlush() throws Throwable
{

View File

@ -17,11 +17,7 @@
*/
package org.apache.cassandra.cql3.validation.entities;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Arrays;
import java.util.UUID;
import java.util.*;
import org.junit.Test;

View File

@ -20,6 +20,7 @@ package org.apache.cassandra.cql3.validation.entities;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.Json;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.Duration;
import org.apache.cassandra.dht.ByteOrderedPartitioner;
import org.apache.cassandra.serializers.SimpleDateSerializer;
import org.apache.cassandra.serializers.TimeSerializer;
@ -81,8 +82,8 @@ public class JsonTest extends CQLTester
"mapval map<ascii, int>," +
"frozenmapval frozen<map<ascii, int>>," +
"tupleval frozen<tuple<int, ascii, uuid>>," +
"udtval frozen<" + typeName + ">)");
"udtval frozen<" + typeName + ">," +
"durationval duration)");
// fromJson() can only be used when the receiver type is known
assertInvalidMessage("fromJson() cannot be used in the selection clause", "SELECT fromJson(asciival) FROM %s", 0, 0);
@ -508,6 +509,16 @@ public class JsonTest extends CQLTester
row(0, 1, UUID.fromString("6bddc89a-5644-11e4-97fc-56847afe9799"), set("bar", "foo"))
);
// ================ duration ================
execute("INSERT INTO %s (k, durationval) VALUES (?, fromJson(?))", 0, "\"53us\"");
assertRows(execute("SELECT k, durationval FROM %s WHERE k = ?", 0), row(0, Duration.newInstance(0, 0, 53000L)));
execute("INSERT INTO %s (k, durationval) VALUES (?, fromJson(?))", 0, "\"P2W\"");
assertRows(execute("SELECT k, durationval FROM %s WHERE k = ?", 0), row(0, Duration.newInstance(0, 14, 0)));
assertInvalidMessage("Unable to convert 'xyz' to a duration",
"INSERT INTO %s (k, durationval) VALUES (?, fromJson(?))", 0, "\"xyz\"");
// order of fields shouldn't matter
execute("INSERT INTO %s (k, udtval) VALUES (?, fromJson(?))", 0, "{\"b\": \"6bddc89a-5644-11e4-97fc-56847afe9799\", \"a\": 1, \"c\": [\"foo\", \"bar\"]}");
assertRows(execute("SELECT k, udtval.a, udtval.b, udtval.c FROM %s WHERE k = ?", 0),
@ -563,7 +574,8 @@ public class JsonTest extends CQLTester
"mapval map<ascii, int>, " +
"frozenmapval frozen<map<ascii, int>>, " +
"tupleval frozen<tuple<int, ascii, uuid>>," +
"udtval frozen<" + typeName + ">)");
"udtval frozen<" + typeName + ">," +
"durationval duration)");
// toJson() can only be used in selections
assertInvalidMessage("toJson() may only be used within the selection clause",
@ -761,6 +773,13 @@ public class JsonTest extends CQLTester
assertRows(execute("SELECT k, toJson(udtval) FROM %s WHERE k = ?", 0),
row(0, "{\"a\": 1, \"b\": \"6bddc89a-5644-11e4-97fc-56847afe9799\", \"c\": null}")
);
// ================ duration ================
execute("INSERT INTO %s (k, durationval) VALUES (?, 12µs)", 0);
assertRows(execute("SELECT k, toJson(durationval) FROM %s WHERE k = ?", 0), row(0, "12us"));
execute("INSERT INTO %s (k, durationval) VALUES (?, P1Y1M2DT10H5M)", 0);
assertRows(execute("SELECT k, toJson(durationval) FROM %s WHERE k = ?", 0), row(0, "1y1mo2d10h5m"));
}
@Test

View File

@ -153,10 +153,10 @@ public class BatchTest extends CQLTester
execute("INSERT INTO %s (partitionKey, clustering_1, value) VALUES (0, 6, 6)");
execute("BEGIN BATCH " +
"UPDATE %1$s SET value = 7 WHERE partitionKey = 0 AND clustering_1 = 1" +
"UPDATE %1$s SET value = 8 WHERE partitionKey = 0 AND (clustering_1) = (2)" +
"UPDATE %1$s SET value = 10 WHERE partitionKey = 0 AND clustering_1 IN (3, 4)" +
"UPDATE %1$s SET value = 20 WHERE partitionKey = 0 AND (clustering_1) IN ((5), (6))" +
"UPDATE %1$s SET value = 7 WHERE partitionKey = 0 AND clustering_1 = 1;" +
"UPDATE %1$s SET value = 8 WHERE partitionKey = 0 AND (clustering_1) = (2);" +
"UPDATE %1$s SET value = 10 WHERE partitionKey = 0 AND clustering_1 IN (3, 4);" +
"UPDATE %1$s SET value = 20 WHERE partitionKey = 0 AND (clustering_1) IN ((5), (6));" +
"APPLY BATCH;");
assertRows(execute("SELECT * FROM %s"),

View File

@ -27,6 +27,7 @@ import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.config.SchemaConstants;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.Duration;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.exceptions.ConfigurationException;
@ -40,6 +41,8 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.apache.cassandra.cql3.Duration.*;
import static org.junit.Assert.assertEquals;
public class CreateTest extends CQLTester
{
@ -85,6 +88,134 @@ public class CreateTest extends CQLTester
"INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", "3", (byte) 1, ByteBufferUtil.EMPTY_BYTE_BUFFER);
}
@Test
public void testCreateTableWithDurationColumns() throws Throwable
{
assertInvalidMessage("duration type is not supported for PRIMARY KEY part a",
"CREATE TABLE test (a duration PRIMARY KEY, b int);");
assertInvalidMessage("duration type is not supported for PRIMARY KEY part b",
"CREATE TABLE test (a text, b duration, c duration, primary key (a, b));");
assertInvalidMessage("duration type is not supported for PRIMARY KEY part b",
"CREATE TABLE test (a text, b duration, c duration, primary key (a, b)) with clustering order by (b DESC);");
createTable("CREATE TABLE %s (a int, b int, c duration, primary key (a, b));");
execute("INSERT INTO %s (a, b, c) VALUES (1, 1, 1y2mo)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 2, -1y2mo)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 3, 1Y2MO)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 4, 2w)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 5, 2d10h)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 6, 30h20m)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 7, 20m)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 8, 567ms)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 9, 1950us)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 10, 1950µs)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 11, 1950000NS)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 12, -1950000ns)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 13, 1y3mo2h10m)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 14, -P1Y2M)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 15, P2D)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 16, PT20M)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 17, P2W)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 18, P1Y3MT2H10M)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 19, P0000-00-00T30:20:00)");
execute("INSERT INTO %s (a, b, c) VALUES (1, 20, P0001-03-00T02:10:00)");
assertRows(execute("SELECT * FROM %s"),
row(1, 1, Duration.newInstance(14, 0, 0)),
row(1, 2, Duration.newInstance(-14, 0, 0)),
row(1, 3, Duration.newInstance(14, 0, 0)),
row(1, 4, Duration.newInstance(0, 14, 0)),
row(1, 5, Duration.newInstance(0, 2, 10 * NANOS_PER_HOUR)),
row(1, 6, Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE)),
row(1, 7, Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE)),
row(1, 8, Duration.newInstance(0, 0, 567 * NANOS_PER_MILLI)),
row(1, 9, Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO)),
row(1, 10, Duration.newInstance(0, 0, 1950 * NANOS_PER_MICRO)),
row(1, 11, Duration.newInstance(0, 0, 1950000)),
row(1, 12, Duration.newInstance(0, 0, -1950000)),
row(1, 13, Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE)),
row(1, 14, Duration.newInstance(-14, 0, 0)),
row(1, 15, Duration.newInstance(0, 2, 0)),
row(1, 16, Duration.newInstance(0, 0, 20 * NANOS_PER_MINUTE)),
row(1, 17, Duration.newInstance(0, 14, 0)),
row(1, 18, Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE)),
row(1, 19, Duration.newInstance(0, 0, 30 * NANOS_PER_HOUR + 20 * NANOS_PER_MINUTE)),
row(1, 20, Duration.newInstance(15, 0, 130 * NANOS_PER_MINUTE)));
assertInvalidMessage("Slice restriction are not supported on duration columns",
"SELECT * FROM %s WHERE c > 1y ALLOW FILTERING");
assertInvalidMessage("Slice restriction are not supported on duration columns",
"SELECT * FROM %s WHERE c <= 1y ALLOW FILTERING");
assertInvalidMessage("Expected at least 3 bytes for a duration (1)",
"INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, (byte) 1);
assertInvalidMessage("Expected at least 3 bytes for a duration (0)",
"INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 2, 1, ByteBufferUtil.EMPTY_BYTE_BUFFER);
assertInvalidMessage("Invalid duration. The total number of days must be less or equal to 2147483647",
"INSERT INTO %s (a, b, c) VALUES (1, 2, " + Long.MAX_VALUE + "d)");
// Test with duration column name
createTable("CREATE TABLE %s (a text PRIMARY KEY, duration duration);");
// Test duration within Map
assertInvalidMessage("Durations are not allowed as map keys: map<duration, text>",
"CREATE TABLE test(pk int PRIMARY KEY, m map<duration, text>)");
createTable("CREATE TABLE %s(pk int PRIMARY KEY, m map<text, duration>)");
execute("INSERT INTO %s (pk, m) VALUES (1, {'one month' : 1mo, '60 days' : 60d})");
assertRows(execute("SELECT * FROM %s"),
row(1, map("one month", Duration.from("1mo"), "60 days", Duration.from("60d"))));
assertInvalidMessage("duration type is not supported for PRIMARY KEY part m",
"CREATE TABLE %s(m frozen<map<text, duration>> PRIMARY KEY, v int)");
assertInvalidMessage("duration type is not supported for PRIMARY KEY part m",
"CREATE TABLE %s(pk int, m frozen<map<text, duration>>, v int, PRIMARY KEY (pk, m))");
// Test duration within Set
assertInvalidMessage("Durations are not allowed inside sets: set<duration>",
"CREATE TABLE %s(pk int PRIMARY KEY, s set<duration>)");
assertInvalidMessage("Durations are not allowed inside sets: frozen<set<duration>>",
"CREATE TABLE %s(s frozen<set<duration>> PRIMARY KEY, v int)");
// Test duration within List
createTable("CREATE TABLE %s(pk int PRIMARY KEY, l list<duration>)");
execute("INSERT INTO %s (pk, l) VALUES (1, [1mo, 60d])");
assertRows(execute("SELECT * FROM %s"),
row(1, list(Duration.from("1mo"), Duration.from("60d"))));
assertInvalidMessage("duration type is not supported for PRIMARY KEY part l",
"CREATE TABLE %s(l frozen<list<duration>> PRIMARY KEY, v int)");
// Test duration within Tuple
createTable("CREATE TABLE %s(pk int PRIMARY KEY, t tuple<int, duration>)");
execute("INSERT INTO %s (pk, t) VALUES (1, (1, 1mo))");
assertRows(execute("SELECT * FROM %s"),
row(1, tuple(1, Duration.from("1mo"))));
assertInvalidMessage("duration type is not supported for PRIMARY KEY part t",
"CREATE TABLE %s(t frozen<tuple<int, duration>> PRIMARY KEY, v int)");
// Test duration within UDT
String typename = createType("CREATE TYPE %s (a duration)");
String myType = KEYSPACE + '.' + typename;
createTable("CREATE TABLE %s(pk int PRIMARY KEY, u " + myType + ")");
execute("INSERT INTO %s (pk, u) VALUES (1, {a : 1mo})");
assertRows(execute("SELECT * FROM %s"),
row(1, userType("a", Duration.from("1mo"))));
assertInvalidMessage("duration type is not supported for PRIMARY KEY part u",
"CREATE TABLE %s(pk int, u frozen<" + myType + ">, v int, PRIMARY KEY(pk, u))");
// Test duration with several level of depth
assertInvalidMessage("duration type is not supported for PRIMARY KEY part m",
"CREATE TABLE %s(pk int, m frozen<map<text, list<tuple<int, duration>>>>, v int, PRIMARY KEY (pk, m))");
}
/**
* Creation and basic operations on a static table,
* migrated from cql_tests.py:TestCQL.static_cf_test()