From 26dd119679605bf61ad3caa24a70509e5be5aac9 Mon Sep 17 00:00:00 2001 From: Yifan Cai Date: Wed, 18 May 2022 10:27:16 -0700 Subject: [PATCH] Add new CQL function maxWritetime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit patch by Yifan Cai; reviewed by Andres de la Peña, Francisco Guerrero for CASSANDRA-17425 --- CHANGES.txt | 1 + NEWS.txt | 3 + doc/cql3/CQL.textile | 6 +- .../cassandra/pages/cql/appendices.adoc | 1 + .../cassandra/pages/cql/cql_singlefile.adoc | 9 +- doc/modules/cassandra/pages/cql/dml.adoc | 13 +-- pylib/cqlshlib/cql3handling.py | 1 + src/antlr/Lexer.g | 1 + src/antlr/Parser.g | 7 +- .../cql3/selection/ResultSetBuilder.java | 25 ++++++ .../cassandra/cql3/selection/Selectable.java | 59 +++++++++---- .../cassandra/cql3/selection/Selection.java | 22 ++++- .../cassandra/cql3/selection/Selector.java | 24 +++++- .../cql3/selection/SelectorFactories.java | 25 +++++- .../selection/WritetimeOrTTLSelector.java | 55 ++++++------ .../cql3/statements/SelectStatement.java | 16 ++-- .../selection/SelectorSerializationTest.java | 5 +- .../validation/entities/TimestampTest.java | 83 +++++++++++++++++++ 18 files changed, 286 insertions(+), 70 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0fc3cd431c..76bd58e977 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.2 + * Add new CQL function maxWritetime (CASSANDRA-17425) * Add guardrail for ALTER TABLE ADD / DROP / REMOVE column operations (CASSANDRA-17495) * Rename DisableFlag class to EnableFlag on guardrails (CASSANDRA-17544) Merged from 4.1: diff --git a/NEWS.txt b/NEWS.txt index d42d03afe6..4d9b9f70b3 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -57,6 +57,9 @@ using the provided 'sstableupgrade' tool. New features ------------ + - Added a new CQL function, maxwritetime. It shows the largest unix timestamp that the data was written, similar to + its sibling CQL function, writetime. Unlike writetime, maxwritetime can be applied to multi-cell data types, e.g. + non-frozen collections and UDT, and returns the largest timestamp. One should not to use it when upgrading to 4.2. - New Guardrails added: - Whether ALTER TABLE commands are allowed to mutate columns diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile index 8bedf19a78..5fef1a9a27 100644 --- a/doc/cql3/CQL.textile +++ b/doc/cql3/CQL.textile @@ -1083,6 +1083,7 @@ bc(syntax).. ::= | | WRITETIME '(' ')' + | MAXWRITETIME '(' ')' | COUNT '(' '*' ')' | TTL '(' ')' | CAST '(' AS ')' @@ -1131,7 +1132,7 @@ h4(#selectSelection). @@ The @@ determines which columns needs to be queried and returned in the result-set. It consists of either the comma-separated list of or the wildcard character (@*@) to select all the columns defined for the table. Please note that for wildcard @SELECT@ queries the order of columns returned is not specified and is not guaranteed to be stable between Cassandra versions. -A @@ is either a column name to retrieve or a @@ of one or more @@s. The function allowed are the same as for @@ and are described in the "function section":#functions. In addition to these generic functions, the @WRITETIME@ (resp. @TTL@) function allows to select the timestamp of when the column was inserted (resp. the time to live (in seconds) for the column (or null if the column has no expiration set)) and the "@CAST@":#castFun function can be used to convert one data type to another. +A @@ is either a column name to retrieve or a @@ of one or more @@s. The function allowed are the same as for @@ and are described in the "function section":#functions. In addition to these generic functions, the @WRITETIME@ and @MAXWRITETIME@ (resp. @TTL@) function allows to select the timestamp of when the column was inserted (resp. the time to live (in seconds) for the column (or null if the column has no expiration set)) and the "@CAST@":#castFun function can be used to convert one data type to another. Additionally, individual values of maps and sets can be selected using @[ ]@. For maps, this will return the value corresponding to the key, if such entry exists. For sets, this will return the key that is selected if it exists and is thus mainly a way to check element existence. It is also possible to select a slice of a set or map with @[ ... @], where both bound can be omitted. @@ -2052,7 +2053,7 @@ A number of functions are provided to "convert" the native types into binary dat h2(#aggregates). Aggregates Aggregate functions work on a set of rows. They receive values for each row and returns one value for the whole set. -If @normal@ columns, @scalar functions@, @UDT@ fields, @writetime@ or @ttl@ are selected together with aggregate functions, the values returned for them will be the ones of the first row matching the query. +If @normal@ columns, @scalar functions@, @UDT@ fields, @writetime@, @maxwritetime@ or @ttl@ are selected together with aggregate functions, the values returned for them will be the ones of the first row matching the query. CQL3 distinguishes between built-in aggregates (so called 'native aggregates') and "user-defined aggregates":#udas. CQL3 includes several native aggregates, described below: @@ -2433,6 +2434,7 @@ CQL distinguishes between _reserved_ and _non-reserved_ keywords. Reserved keywo | @WHERE@ | yes | | @WITH@ | yes | | @WRITETIME@ | no | +| @MAXWRITETIME@ | no | h2(#appendixB). Appendix B: CQL Reserved Types diff --git a/doc/modules/cassandra/pages/cql/appendices.adoc b/doc/modules/cassandra/pages/cql/appendices.adoc index 7e17266a3f..544afc009f 100644 --- a/doc/modules/cassandra/pages/cql/appendices.adoc +++ b/doc/modules/cassandra/pages/cql/appendices.adoc @@ -139,6 +139,7 @@ or not. |`WHERE` |yes |`WITH` |yes |`WRITETIME` |no +|`MAXWRITETIME` |no |=== == Appendix B: CQL Reserved Types diff --git a/doc/modules/cassandra/pages/cql/cql_singlefile.adoc b/doc/modules/cassandra/pages/cql/cql_singlefile.adoc index 4fe8c10a65..207856a6d6 100644 --- a/doc/modules/cassandra/pages/cql/cql_singlefile.adoc +++ b/doc/modules/cassandra/pages/cql/cql_singlefile.adoc @@ -1645,6 +1645,7 @@ FROM + ::= + | + | WRITETIME `(' `)' + +| MAXWRITETIME `(' `)' + | COUNT `(' `*' `)' + | TTL `(' `)' + | CAST `(' AS `)' + @@ -1706,8 +1707,8 @@ be stable between Cassandra versions. A `` is either a column name to retrieve or a `` of one or more ``s. The function allowed are the same as for `` and are described in the link:#functions[function section]. In addition -to these generic functions, the `WRITETIME` (resp. `TTL`) function -allows to select the timestamp of when the column was inserted (resp. +to these generic functions, the `WRITETIME` and `MAXWRITETIME` (resp. `TTL`) +function allows to select the timestamp of when the column was inserted (resp. the time to live (in seconds) for the column (or null if the column has no expiration set)) and the link:#castFun[`CAST`] function can be used to convert one data type to another. @@ -3148,8 +3149,8 @@ is `0x0000000000000003` and `blobAsBigint(0x0000000000000003)` is `3`. Aggregate functions work on a set of rows. They receive values for each row and returns one value for the whole set. + -If `normal` columns, `scalar functions`, `UDT` fields, `writetime` or -`ttl` are selected together with aggregate functions, the values +If `normal` columns, `scalar functions`, `UDT` fields, `writetime`, `maxwritetime` +or `ttl` are selected together with aggregate functions, the values returned for them will be the ones of the first row matching the query. CQL3 distinguishes between built-in aggregates (so called `native diff --git a/doc/modules/cassandra/pages/cql/dml.adoc b/doc/modules/cassandra/pages/cql/dml.adoc index 8a4df2fecb..7989d4447a 100644 --- a/doc/modules/cassandra/pages/cql/dml.adoc +++ b/doc/modules/cassandra/pages/cql/dml.adoc @@ -75,14 +75,17 @@ You must use the orignal column name instead. ==== [[writetime-and-ttl-function]] -==== `WRITETIME` and `TTL` function +==== `WRITETIME`, `MAXWRITETIME` and `TTL` function -Selection supports two special functions that aren't allowed anywhere -else: `WRITETIME` and `TTL`. -Both functions take only one argument, a column name. +Selection supports three special functions that aren't allowed anywhere +else: `WRITETIME`, `MAXWRITETIME` and `TTL`. +All functions take only one argument, a column name. These functions retrieve meta-information that is stored internally for each column: -* `WRITETIME` stores the timestamp of the value of the column +* `WRITETIME` stores the timestamp of the value of the column. Note that this function cannot be applied to non-frozen collection +and UDT. +* `MAXWRITETIME` stores the largest timestamp of the value of the column. For non-collection and non-UDT columns, `MAXWRITETIME` +is equivalent to `WRITETIME`. In the other cases, it returns the largest timestamp of the values in the column. * `TTL` stores the remaining time to live (in seconds) for the value of the column if it is set to expire; otherwise the value is `null`. [[where-clause]] diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index 7de95cf24c..7e123bd67a 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -731,6 +731,7 @@ syntax_rules += r''' ::= [colname]= ( "[" ( ( ".." "]" )? | ".." ) )? | | "WRITETIME" "(" [colname]= ")" + | "MAXWRITETIME" "(" [colname]= ")" | "TTL" "(" [colname]= ")" | "COUNT" "(" star=( "*" | "1" ) ")" | "CAST" "(" "AS" ")" diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g index 34c7e2ed2f..84dd0361f1 100644 --- a/src/antlr/Lexer.g +++ b/src/antlr/Lexer.g @@ -178,6 +178,7 @@ K_VARINT: V A R I N T; K_TIMEUUID: T I M E U U I D; K_TOKEN: T O K E N; K_WRITETIME: W R I T E T I M E; +K_MAXWRITETIME:M A X W R I T E T I M E; K_DATE: D A T E; K_TIME: T I M E; diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index d061ee4df3..2643e0a6b5 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -415,8 +415,9 @@ simpleUnaliasedSelector returns [Selectable.Raw s] selectionFunction returns [Selectable.Raw s] : K_COUNT '(' '\*' ')' { $s = Selectable.WithFunction.Raw.newCountRowsFunction(); } - | K_WRITETIME '(' c=sident ')' { $s = new Selectable.WritetimeOrTTL.Raw(c, true); } - | K_TTL '(' c=sident ')' { $s = new Selectable.WritetimeOrTTL.Raw(c, false); } + | K_MAXWRITETIME '(' c=sident ')' { $s = new Selectable.WritetimeOrTTL.Raw(c, Selectable.WritetimeOrTTL.Kind.MAX_WRITE_TIME); } + | K_WRITETIME '(' c=sident ')' { $s = new Selectable.WritetimeOrTTL.Raw(c, Selectable.WritetimeOrTTL.Kind.WRITE_TIME); } + | K_TTL '(' c=sident ')' { $s = new Selectable.WritetimeOrTTL.Raw(c, Selectable.WritetimeOrTTL.Kind.TTL); } | K_CAST '(' sn=unaliasedSelector K_AS t=native_type ')' {$s = new Selectable.WithCast.Raw(sn, t);} | f=functionName args=selectionFunctionArgs { $s = new Selectable.WithFunction.Raw(f, args); } ; @@ -1870,7 +1871,7 @@ non_type_ident returns [ColumnIdentifier id] unreserved_keyword returns [String str] : u=unreserved_function_keyword { $str = u; } - | k=(K_TTL | K_COUNT | K_WRITETIME | K_KEY | K_CAST | K_JSON | K_DISTINCT) { $str = $k.text; } + | k=(K_TTL | K_COUNT | K_WRITETIME | K_MAXWRITETIME | K_KEY | K_CAST | K_JSON | K_DISTINCT) { $str = $k.text; } ; unreserved_function_keyword returns [String str] diff --git a/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java b/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java index 22566b26d7..3e652dfeb4 100644 --- a/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java +++ b/src/java/org/apache/cassandra/cql3/selection/ResultSetBuilder.java @@ -19,7 +19,10 @@ package org.apache.cassandra.cql3.selection; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; import org.apache.cassandra.cql3.ResultSet; import org.apache.cassandra.cql3.ResultSet.ResultMetadata; @@ -28,6 +31,7 @@ import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.aggregation.GroupMaker; import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ComplexColumnData; public final class ResultSetBuilder { @@ -98,6 +102,27 @@ public final class ResultSetBuilder inputRow.add(v); } + public void add(ComplexColumnData complexColumnData, Function>, ByteBuffer> serializer) + { + if (complexColumnData == null) + { + inputRow.add(null); + return; + } + + long timestamp = -1L; + if (selectors.collectMaxTimestamps()) + { + Iterator> cells = complexColumnData.iterator(); + while (cells.hasNext()) + { + timestamp = Math.max(timestamp, cells.next().timestamp()); + } + } + + inputRow.add(serializer.apply(complexColumnData.iterator()), timestamp, -1); + } + public void add(Cell c, int nowInSec) { inputRow.add(c, nowInSec); diff --git a/src/java/org/apache/cassandra/cql3/selection/Selectable.java b/src/java/org/apache/cassandra/cql3/selection/Selectable.java index de5360f525..afa86b12a0 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selectable.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selectable.java @@ -222,19 +222,46 @@ public interface Selectable extends AssignmentTestable public static class WritetimeOrTTL implements Selectable { - public final ColumnMetadata column; - public final boolean isWritetime; + // The order of the variants in the Kind enum matters as they are used in ser/deser + public enum Kind + { + TTL("ttl", Int32Type.instance), + WRITE_TIME("writetime", LongType.instance), + MAX_WRITE_TIME("maxwritetime", LongType.instance); // maxwritetime is available after Cassandra 4.1 (exclusive) - public WritetimeOrTTL(ColumnMetadata column, boolean isWritetime) + public final String name; + public final AbstractType returnType; + + public static Kind fromOrdinal(int ordinal) + { + return values()[ordinal]; + } + + Kind(String name, AbstractType returnType) + { + this.name = name; + this.returnType = returnType; + } + + public boolean allowedForMultiCell() + { + return this == MAX_WRITE_TIME; + } + } + + public final ColumnMetadata column; + public final Kind kind; + + public WritetimeOrTTL(ColumnMetadata column, Kind kind) { this.column = column; - this.isWritetime = isWritetime; + this.kind = kind; } @Override public String toString() { - return (isWritetime ? "writetime" : "ttl") + "(" + column.name + ")"; + return kind.name + "(" + column.name + ")"; } public Selector.Factory newSelectorFactory(TableMetadata table, @@ -245,18 +272,20 @@ public interface Selectable extends AssignmentTestable if (column.isPrimaryKeyColumn()) throw new InvalidRequestException( String.format("Cannot use selection function %s on PRIMARY KEY part %s", - isWritetime ? "writeTime" : "ttl", + kind.name, column.name)); - if (column.type.isCollection()) - throw new InvalidRequestException(String.format("Cannot use selection function %s on collections", - isWritetime ? "writeTime" : "ttl")); - return WritetimeOrTTLSelector.newFactory(column, addAndGetIndex(column, defs), isWritetime); + // only maxwritetime is allowed for collection + if (column.type.isCollection() && !kind.allowedForMultiCell()) + throw new InvalidRequestException(String.format("Cannot use selection function %s on collections", + kind.name)); + + return WritetimeOrTTLSelector.newFactory(column, addAndGetIndex(column, defs), kind); } public AbstractType getExactTypeIfKnown(String keyspace) { - return isWritetime ? LongType.instance : Int32Type.instance; + return kind.returnType; } @Override @@ -268,18 +297,18 @@ public interface Selectable extends AssignmentTestable public static class Raw implements Selectable.Raw { private final Selectable.RawIdentifier id; - private final boolean isWritetime; + private final Kind kind; - public Raw(Selectable.RawIdentifier id, boolean isWritetime) + public Raw(Selectable.RawIdentifier id, Kind kind) { this.id = id; - this.isWritetime = isWritetime; + this.kind = kind; } @Override public WritetimeOrTTL prepare(TableMetadata table) { - return new WritetimeOrTTL(id.prepare(table), isWritetime); + return new WritetimeOrTTL(id.prepare(table), kind); } } } diff --git a/src/java/org/apache/cassandra/cql3/selection/Selection.java b/src/java/org/apache/cassandra/cql3/selection/Selection.java index f07184a599..2f41192c37 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selection.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selection.java @@ -376,6 +376,12 @@ public abstract class Selection */ public boolean collectTimestamps(); + /** + * Checks if one of the selectors collects maxTimestamps. + * @return {@code true} if one of the selectors collect maxTimestamps, {@code false} otherwise. + */ + public boolean collectMaxTimestamps(); + /** * Adds the current row of the specified ResultSetBuilder. * @@ -506,6 +512,11 @@ public abstract class Selection return false; } + @Override + public boolean collectMaxTimestamps() { + return false; + } + @Override public ColumnFilter getColumnFilter() { @@ -521,6 +532,7 @@ public abstract class Selection { private final SelectorFactories factories; private final boolean collectTimestamps; + private final boolean collectMaxTimestamps; private final boolean collectTTLs; public SelectionWithProcessing(TableMetadata table, @@ -541,7 +553,8 @@ public abstract class Selection this.factories = factories; this.collectTimestamps = factories.containsWritetimeSelectorFactory(); - this.collectTTLs = factories.containsTTLSelectorFactory();; + this.collectMaxTimestamps = factories.containsMaxWritetimeSelectorFactory(); + this.collectTTLs = factories.containsTTLSelectorFactory(); for (ColumnMetadata orderingColumn : orderingColumns) { @@ -619,7 +632,12 @@ public abstract class Selection @Override public boolean collectTimestamps() { - return collectTimestamps; + return collectTimestamps || collectMaxTimestamps; + } + + @Override + public boolean collectMaxTimestamps() { + return collectMaxTimestamps; } @Override diff --git a/src/java/org/apache/cassandra/cql3/selection/Selector.java b/src/java/org/apache/cassandra/cql3/selection/Selector.java index 463382d584..8226c2d5d2 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selector.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selector.java @@ -20,8 +20,10 @@ package org.apache.cassandra.cql3.selection; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.Iterator; import java.util.List; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.schema.CQLTypeParser; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.Schema; @@ -71,7 +73,7 @@ public abstract class Selector /** * The Selector kinds. */ - public static enum Kind + public enum Kind { SIMPLE_SELECTOR(SimpleSelector.deserializer), TERM_SELECTOR(TermSelector.deserializer), @@ -151,6 +153,17 @@ public abstract class Selector return false; } + /** + * Checks if this factory creates maxwritetime selector instances. + * + * @return true if this factory creates maxwritetime selectors instances, + * false otherwise + */ + public boolean isMaxWritetimeSelectorFactory() + { + return false; + } + /** * Checks if this factory creates TTL selectors instances. * @@ -321,14 +334,19 @@ public abstract class Selector } public void add(ByteBuffer v) + { + add(v, Long.MIN_VALUE, -1); + } + + public void add(ByteBuffer v, long timestamp, int ttl) { values[index] = v; if (timestamps != null) - timestamps[index] = Long.MIN_VALUE; + timestamps[index] = timestamp; if (ttls != null) - ttls[index] = -1; + ttls[index] = ttl; index++; } diff --git a/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java b/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java index 7f4bcb3012..1b275a8c90 100644 --- a/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java +++ b/src/java/org/apache/cassandra/cql3/selection/SelectorFactories.java @@ -32,22 +32,27 @@ import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.exceptions.InvalidRequestException; /** - * A set of Selector factories. + * A set of {@code Selector} factories. */ final class SelectorFactories implements Iterable { /** - * The Selector factories. + * The {@code Selector} factories. */ private final List factories; /** - * true if one of the factory creates writetime selectors. + * {@code true} if one of the factories creates writetime selectors. */ private boolean containsWritetimeFactory; /** - * true if one of the factory creates TTL selectors. + * {@code true} if one of the factories creates maxWritetime selectors. + */ + private boolean containsMaxWritetimeFactory; + + /** + * {@code true} if one of the factories creates TTL selectors. */ private boolean containsTTLFactory; @@ -96,6 +101,7 @@ final class SelectorFactories implements Iterable Factory factory = selectable.newSelectorFactory(table, expectedType, defs, boundNames); containsWritetimeFactory |= factory.isWritetimeSelectorFactory(); containsTTLFactory |= factory.isTTLSelectorFactory(); + containsMaxWritetimeFactory |= factory.isMaxWritetimeSelectorFactory(); if (factory.isAggregateSelectorFactory()) ++numberOfAggregateFactories; factories.add(factory); @@ -165,6 +171,17 @@ final class SelectorFactories implements Iterable return containsWritetimeFactory; } + /** + * Checks if this {@code SelectorFactories} contains at least one factory for maxWritetime selectors. + * + * @return {@link true} if this {@link SelectorFactories} contains at least one factory for maxWritetime + * selectors, {@link false} otherwise. + */ + public boolean containsMaxWritetimeSelectorFactory() + { + return containsMaxWritetimeFactory; + } + /** * Checks if this SelectorFactories contains at least one factory for TTL selectors. * diff --git a/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java b/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java index 2c56f5ced2..29ebfbbdf6 100644 --- a/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java +++ b/src/java/org/apache/cassandra/cql3/selection/WritetimeOrTTLSelector.java @@ -29,8 +29,6 @@ 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; @@ -45,29 +43,30 @@ final class WritetimeOrTTLSelector extends Selector ByteBuffer columnName = ByteBufferUtil.readWithVIntLength(in); ColumnMetadata column = metadata.getColumn(columnName); int idx = in.readInt(); - boolean isWritetime = in.readBoolean(); - return new WritetimeOrTTLSelector(column, idx, isWritetime); + int ordinal = in.readByte(); + Selectable.WritetimeOrTTL.Kind k = Selectable.WritetimeOrTTL.Kind.fromOrdinal(ordinal); + return new WritetimeOrTTLSelector(column, idx, k); } }; private final ColumnMetadata column; private final int idx; - private final boolean isWritetime; + private final Selectable.WritetimeOrTTL.Kind kind; private ByteBuffer current; private boolean isSet; - public static Factory newFactory(final ColumnMetadata def, final int idx, final boolean isWritetime) + public static Factory newFactory(final ColumnMetadata def, final int idx, final Selectable.WritetimeOrTTL.Kind kind) { return new Factory() { protected String getColumnName() { - return String.format("%s(%s)", isWritetime ? "writetime" : "ttl", def.name.toString()); + return String.format("%s(%s)", kind.name, def.name.toString()); } protected AbstractType getReturnType() { - return isWritetime ? LongType.instance : Int32Type.instance; + return kind.returnType; } protected void addColumnMapping(SelectionColumnMapping mapping, ColumnSpecification resultsColumn) @@ -77,17 +76,25 @@ final class WritetimeOrTTLSelector extends Selector public Selector newInstance(QueryOptions options) { - return new WritetimeOrTTLSelector(def, idx, isWritetime); + return new WritetimeOrTTLSelector(def, idx, kind); } + @Override public boolean isWritetimeSelectorFactory() { - return isWritetime; + return kind != Selectable.WritetimeOrTTL.Kind.TTL; } + @Override public boolean isTTLSelectorFactory() { - return !isWritetime; + return kind == Selectable.WritetimeOrTTL.Kind.TTL; + } + + @Override + public boolean isMaxWritetimeSelectorFactory() + { + return kind == Selectable.WritetimeOrTTL.Kind.MAX_WRITE_TIME; } public boolean areAllFetchedColumnsKnown() @@ -114,16 +121,16 @@ final class WritetimeOrTTLSelector extends Selector isSet = true; - if (isWritetime) - { - long ts = input.getTimestamp(idx); - current = ts != Long.MIN_VALUE ? ByteBufferUtil.bytes(ts) : null; - } - else + if (kind == Selectable.WritetimeOrTTL.Kind.TTL) { int ttl = input.getTtl(idx); current = ttl > 0 ? ByteBufferUtil.bytes(ttl) : null; } + else + { + long ts = input.getTimestamp(idx); + current = ts != Long.MIN_VALUE ? ByteBufferUtil.bytes(ts) : null; + } } public ByteBuffer getOutput(ProtocolVersion protocolVersion) @@ -139,7 +146,7 @@ final class WritetimeOrTTLSelector extends Selector public AbstractType getType() { - return isWritetime ? LongType.instance : Int32Type.instance; + return kind.returnType; } @Override @@ -148,12 +155,12 @@ final class WritetimeOrTTLSelector extends Selector return column.name.toString(); } - private WritetimeOrTTLSelector(ColumnMetadata column, int idx, boolean isWritetime) + private WritetimeOrTTLSelector(ColumnMetadata column, int idx, Selectable.WritetimeOrTTL.Kind kind) { super(Kind.WRITETIME_OR_TTL_SELECTOR); this.column = column; this.idx = idx; - this.isWritetime = isWritetime; + this.kind = kind; } @Override @@ -169,13 +176,13 @@ final class WritetimeOrTTLSelector extends Selector return Objects.equal(column, s.column) && Objects.equal(idx, s.idx) - && Objects.equal(isWritetime, s.isWritetime); + && kind == s.kind; } @Override public int hashCode() { - return Objects.hashCode(column, idx, isWritetime); + return Objects.hashCode(column, idx, kind); } @Override @@ -183,7 +190,7 @@ final class WritetimeOrTTLSelector extends Selector { return ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes) + TypeSizes.sizeof(idx) - + TypeSizes.sizeof(isWritetime); + + TypeSizes.sizeofUnsignedVInt(kind.ordinal()); } @Override @@ -191,6 +198,6 @@ final class WritetimeOrTTLSelector extends Selector { ByteBufferUtil.writeWithVIntLength(column.name.bytes, out); out.writeInt(idx); - out.writeBoolean(isWritetime); + out.writeByte(kind.ordinal()); } } diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index 030b4cd785..0d43313e53 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -1039,12 +1039,16 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement { assert def.type.isMultiCell(); ComplexColumnData complexData = row.getComplexColumnData(def); - if (complexData == null) - result.add(null); - else if (def.type.isCollection()) - result.add(((CollectionType) def.type).serializeForNativeProtocol(complexData.iterator(), protocolVersion)); - else - result.add(((UserType) def.type).serializeForNativeProtocol(complexData.iterator(), protocolVersion)); + result.add(complexData, iterator -> { + if (def.type.isCollection()) + { + return ((CollectionType) def.type).serializeForNativeProtocol(iterator, protocolVersion); + } + else + { + return ((UserType) def.type).serializeForNativeProtocol(iterator, protocolVersion); + } + }); } else { diff --git a/test/unit/org/apache/cassandra/cql3/selection/SelectorSerializationTest.java b/test/unit/org/apache/cassandra/cql3/selection/SelectorSerializationTest.java index ee4dd356e1..4eadb95d6d 100644 --- a/test/unit/org/apache/cassandra/cql3/selection/SelectorSerializationTest.java +++ b/test/unit/org/apache/cassandra/cql3/selection/SelectorSerializationTest.java @@ -60,8 +60,9 @@ public class SelectorSerializationTest extends CQLTester 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); + checkSerialization(new Selectable.WritetimeOrTTL(table.getColumn(new ColumnIdentifier("v", false)), Selectable.WritetimeOrTTL.Kind.WRITE_TIME), table); + checkSerialization(new Selectable.WritetimeOrTTL(table.getColumn(new ColumnIdentifier("v", false)), Selectable.WritetimeOrTTL.Kind.TTL), table); + checkSerialization(new Selectable.WritetimeOrTTL(table.getColumn(new ColumnIdentifier("v", false)), Selectable.WritetimeOrTTL.Kind.MAX_WRITE_TIME), table); // Test ListSelector serialization checkSerialization(new Selectable.WithList(asList(table.getColumn(new ColumnIdentifier("v", false)), diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/TimestampTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/TimestampTest.java index 13090a6b62..7c6cd8f9ea 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/TimestampTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/TimestampTest.java @@ -17,11 +17,16 @@ */ package org.apache.cassandra.cql3.validation.entities; +import java.util.Arrays; +import java.util.List; + import org.junit.Test; import org.junit.Assert; import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.utils.Pair; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -97,6 +102,84 @@ public class TimestampTest extends CQLTester row(1, null, null)); } + private void setupSchemaForMaxTimestamp() + { + String myType = createType("CREATE TYPE %s (a int, b int)"); + createTable("CREATE TABLE %s (k int PRIMARY KEY, a text, " + + "l list, fl frozen>," + + "s set, fs frozen>," + + "m map, fm frozen>," + + "t " + myType + ", ft frozen<" + myType + ">)"); + } + + @Test + public void testCallMaxTimestampOnEmptyCollectionReturnsNull() throws Throwable + { + setupSchemaForMaxTimestamp(); + + execute("INSERT INTO %s (k) VALUES (1)"); + Object[][] res = getRows(execute("SELECT maxwritetime(a), maxwritetime(l), maxwritetime(fl)," + + "maxwritetime(s), maxwritetime(fs), maxwritetime(m), maxwritetime(fm)," + + "maxwritetime(t), maxwritetime(ft) FROM %s WHERE k=1")); + + assertEquals(1, res.length); + for (Object v : res[0]) + { + assertNull("All the multi-cell data are empty (we did not insert), calling maxwritetime should return null", + v); + } + } + + @Test + public void testMaxTimestamp() throws Throwable + { + setupSchemaForMaxTimestamp(); + + execute("INSERT INTO %s (k, a, l, fl, s, fs, m, fm, t, ft) VALUES " + + "(1, 'test', [1], [2], {1}, {2}, {1 : 'a'}, {2 : 'b'}, {a : 1, b : 1 }, {a : 2, b : 2}) USING TIMESTAMP 1"); + + // enumerate through all multi-cell types and make sure maxwritetime reflects the expected result + testMaxTimestampWithColumnUpdate(Arrays.asList( + Pair.create(1, "UPDATE %s USING TIMESTAMP 10 SET l = l + [10] WHERE k = 1"), + Pair.create(3, "UPDATE %s USING TIMESTAMP 11 SET s = s + {10} WHERE k = 1"), + Pair.create(5, "UPDATE %s USING TIMESTAMP 12 SET m = m + {10 : 'c'} WHERE k = 1"), + Pair.create(7, "UPDATE %s USING TIMESTAMP 13 SET t.a = 10 WHERE k = 1") + )); + } + + private void testMaxTimestampWithColumnUpdate(List> updateStatements) throws Throwable + { + for (Pair update : updateStatements) + { + int fieldPos = update.left(); + String statement = update.right(); + + // run the update statement and update the timestamp of the column + execute(statement); + + Object[][] res = getRows(execute("SELECT maxwritetime(a), maxwritetime(l), maxwritetime(fl)," + + "maxwritetime(s), maxwritetime(fs), maxwritetime(m), maxwritetime(fm)," + + "maxwritetime(t), maxwritetime(ft) FROM %s WHERE k=1")); + Assert.assertEquals(1, res.length); + Assert.assertEquals("maxwritetime should work on both single cell and complex columns", + 9, res[0].length); + for (Object ts : res[0]) + { + assertTrue(ts instanceof Long); // all the result fields are timestamps + } + + long updatedTs = (long) res[0][fieldPos]; // maxwritetime the updated column + + for (int i = 0; i < res[0].length; i++) + { + long ts = (long) res[0][i]; + if (i != fieldPos) + assertTrue("The updated column should have a large maxwritetime since it is updated later", + ts < updatedTs); + } + } + } + /** * Migrated from cql_tests.py:TestCQL.invalid_custom_timestamp_test() */