You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by yc...@apache.org on 2022/05/18 18:57:31 UTC

[cassandra] branch trunk updated: Add new CQL function maxWritetime

This is an automated email from the ASF dual-hosted git repository.

ycai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 26dd119679 Add new CQL function maxWritetime
26dd119679 is described below

commit 26dd119679605bf61ad3caa24a70509e5be5aac9
Author: Yifan Cai <yc...@apache.org>
AuthorDate: Wed May 18 10:27:16 2022 -0700

    Add new CQL function maxWritetime
    
    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 +-
 doc/modules/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 +-
 .../cassandra/cql3/selection/ResultSetBuilder.java | 25 +++++++
 .../cassandra/cql3/selection/Selectable.java       | 55 ++++++++++----
 .../apache/cassandra/cql3/selection/Selection.java | 22 +++++-
 .../apache/cassandra/cql3/selection/Selector.java  | 24 ++++++-
 .../cql3/selection/SelectorFactories.java          | 25 +++++--
 .../cql3/selection/WritetimeOrTTLSelector.java     | 53 ++++++++------
 .../cassandra/cql3/statements/SelectStatement.java | 16 +++--
 .../cql3/selection/SelectorSerializationTest.java  |  5 +-
 .../cql3/validation/entities/TimestampTest.java    | 83 ++++++++++++++++++++++
 18 files changed, 283 insertions(+), 67 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)..
 <selector> ::= <identifier>
              | <term>
              | WRITETIME '(' <identifier> ')'
+             | MAXWRITETIME '(' <identifier> ')'
              | COUNT '(' '*' ')'
              | TTL '(' <identifier> ')'
              | CAST '(' <selector> AS <type> ')'
@@ -1131,7 +1132,7 @@ h4(#selectSelection). @<select-clause>@
 
 The @<select-clause>@ determines which columns needs to be queried and returned in the result-set. It consists of either the comma-separated list of <selector> 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 @<selector>@ is either a column name to retrieve or a @<function>@ of one or more @<term>@s. The function allowed are the same as for @<term>@ 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  [...]
+A @<selector>@ is either a column name to retrieve or a @<function>@ of one or more @<term>@s. The function allowed are the same as for @<term>@ 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 funct [...]
 
 Additionally, individual values of maps and sets can be selected using @[ <term> ]@. 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 @[ <term> ... <term> @], 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 `<selector>` is either a column name to retrieve or a `<function>` of
 one or more `<term>`s. The function allowed are the same as for `<term>`
 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'''
 <selector> ::= [colname]=<cident> ( "[" ( <term> ( ".." <term> "]" )? | <term> ".." ) )?
              | <udtSubfieldSelection>
              | "WRITETIME" "(" [colname]=<cident> ")"
+             | "MAXWRITETIME" "(" [colname]=<cident> ")"
              | "TTL" "(" [colname]=<cident> ")"
              | "COUNT" "(" star=( "*" | "1" ) ")"
              | "CAST" "(" <selector> "AS" <storageType> ")"
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<Iterator<Cell<?>>, ByteBuffer> serializer)
+    {
+        if (complexColumnData == null)
+        {
+            inputRow.add(null);
+            return;
+        }
+
+        long timestamp = -1L;
+        if (selectors.collectMaxTimestamps())
+        {
+            Iterator<Cell<?>> 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
     {
+        // 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 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 boolean isWritetime;
+        public final Kind kind;
 
-        public WritetimeOrTTL(ColumnMetadata column, boolean isWritetime)
+        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())
+
+            // only maxwritetime is allowed for collection
+            if (column.type.isCollection() && !kind.allowedForMultiCell())
                 throw new InvalidRequestException(String.format("Cannot use selection function %s on collections",
-                                                                isWritetime ? "writeTime" : "ttl"));
+                                                                kind.name));
 
-            return WritetimeOrTTLSelector.newFactory(column, addAndGetIndex(column, defs), isWritetime);
+            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 <code>ResultSetBuilder</code>.
          *
@@ -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 <code>Selector</code> 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 <code>maxwritetime</code> selector instances.
+         *
+         * @return <code>true</code> if this factory creates <code>maxwritetime</code> selectors instances,
+         * <code>false</code> otherwise
+         */
+        public boolean isMaxWritetimeSelectorFactory()
+        {
+            return false;
+        }
+
         /**
          * Checks if this factory creates <code>TTL</code> 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 <code>Selector</code> factories.
+ * A set of {@code Selector} factories.
  */
 final class SelectorFactories implements Iterable<Selector.Factory>
 {
     /**
-     * The <code>Selector</code> factories.
+     * The {@code Selector} factories.
      */
     private final List<Selector.Factory> factories;
 
     /**
-     * <code>true</code> if one of the factory creates writetime selectors.
+     * {@code true} if one of the factories creates writetime selectors.
      */
     private boolean containsWritetimeFactory;
 
     /**
-     * <code>true</code> 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<Selector.Factory>
             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<Selector.Factory>
         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 <code>SelectorFactories</code> 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,15 +121,15 @@ final class WritetimeOrTTLSelector extends Selector
 
         isSet = true;
 
-        if (isWritetime)
+        if (kind == Selectable.WritetimeOrTTL.Kind.TTL)
         {
-            long ts = input.getTimestamp(idx);
-            current = ts != Long.MIN_VALUE ? ByteBufferUtil.bytes(ts) : null;
+            int ttl = input.getTtl(idx);
+            current = ttl > 0 ? ByteBufferUtil.bytes(ttl) : null;
         }
         else
         {
-            int ttl = input.getTtl(idx);
-            current = ttl > 0 ? ByteBufferUtil.bytes(ttl) : null;
+            long ts = input.getTimestamp(idx);
+            current = ts != Long.MIN_VALUE ? ByteBufferUtil.bytes(ts) : null;
         }
     }
 
@@ -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<int>, fl frozen<list<int>>," +
+                    "s set<int>, fs frozen<set<int>>," +
+                    "m map<int, text>, fm frozen<map<int, text>>," +
+                    "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<Pair<Integer, String>> updateStatements) throws Throwable
+    {
+        for (Pair<Integer, String> 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()
      */


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org