String into a duration.
+ * The accepted formats are: + *
String 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);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java
index 4dbb7daf7a..719ef6885f 100644
--- a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java
+++ b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java
@@ -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);
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java
index 90f0cdbf3e..7f8eebc244 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java
@@ -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);
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java
index 5f2ba712e5..3781a6ede4 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateViewStatement.java
@@ -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
diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
index 2b5503b918..8cd40cbfd6 100644
--- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
@@ -446,6 +446,11 @@ public abstract class AbstractTypeInternally he duration is stored as months (unsigned integer), days (unsigned integer), and nanoseconds.
+ */ +public class DurationType extends AbstractType