diff --git a/CHANGES.txt b/CHANGES.txt index 67e85f68e5..4cd4cc5843 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -277,6 +277,8 @@ 3.11.4 Merged from 3.0: + * Fix static column order for SELECT * wildcard queries (CASSANDRA-14638) + * sstableloader should use discovered broadcast address to connect intra-cluster (CASSANDRA-14522) * Fix reading columns with non-UTF names from schema (CASSANDRA-14468) Merged from 2.2: * Returns null instead of NaN or Infinity in JSON strings (CASSANDRA-14377) diff --git a/NEWS.txt b/NEWS.txt index 3fab849881..9d0da9b3d3 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -184,6 +184,16 @@ Materialized Views to be NOT NULL, and no base primary key columns get automatically included in view definition. You have to specify them explicitly now. +3.11.4 +====== + +Upgrading +--------- + - The order of static columns in SELECT * has been fixed to match that of 2.0 and 2.1 - they are now sorted + alphabetically again, by their name, just like regular columns are. If you use prepared statements and + SELECT * queries, and have both simple and collection static columns in those tables, and are upgrading from an + earlier 3.0 version, then you might be affected by this change. Please see CASSANDRA-14638 for details. + 3.11.3 ===== diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile index 8d55baa831..85bacd8d57 100644 --- a/doc/cql3/CQL.textile +++ b/doc/cql3/CQL.textile @@ -1108,7 +1108,7 @@ The @SELECT@ statements reads one or more columns for one or more rows in a tabl 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. +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. diff --git a/src/java/org/apache/cassandra/db/Columns.java b/src/java/org/apache/cassandra/db/Columns.java index 00cdf4a66f..bced4b7a54 100644 --- a/src/java/org/apache/cassandra/db/Columns.java +++ b/src/java/org/apache/cassandra/db/Columns.java @@ -53,7 +53,16 @@ public class Columns extends AbstractCollection implements Colle { public static final Serializer serializer = new Serializer(); public static final Columns NONE = new Columns(BTree.empty(), 0); - private static final ColumnMetadata FIRST_COMPLEX = + + private static final ColumnMetadata FIRST_COMPLEX_STATIC = + new ColumnMetadata("", + "", + ColumnIdentifier.getInterned(ByteBufferUtil.EMPTY_BYTE_BUFFER, UTF8Type.instance), + SetType.getInstance(UTF8Type.instance, true), + ColumnMetadata.NO_POSITION, + ColumnMetadata.Kind.STATIC); + + private static final ColumnMetadata FIRST_COMPLEX_REGULAR = new ColumnMetadata("", "", ColumnIdentifier.getInterned(ByteBufferUtil.EMPTY_BYTE_BUFFER, UTF8Type.instance), @@ -102,11 +111,14 @@ public class Columns extends AbstractCollection implements Colle private static int findFirstComplexIdx(Object[] tree) { - // have fast path for common no-complex case + if (BTree.isEmpty(tree)) + return 0; + int size = BTree.size(tree); - if (!BTree.isEmpty(tree) && BTree.findByIndex(tree, size - 1).isSimple()) - return size; - return BTree.ceilIndex(tree, Comparator.naturalOrder(), FIRST_COMPLEX); + ColumnMetadata last = BTree.findByIndex(tree, size - 1); + return last.isSimple() + ? size + : BTree.ceilIndex(tree, Comparator.naturalOrder(), last.isStatic() ? FIRST_COMPLEX_STATIC : FIRST_COMPLEX_REGULAR); } /** diff --git a/test/unit/org/apache/cassandra/db/ColumnsTest.java b/test/unit/org/apache/cassandra/db/ColumnsTest.java index 6a3b86f374..dae0d0aa8b 100644 --- a/test/unit/org/apache/cassandra/db/ColumnsTest.java +++ b/test/unit/org/apache/cassandra/db/ColumnsTest.java @@ -23,6 +23,7 @@ import java.util.*; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Predicate; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; @@ -31,6 +32,7 @@ import org.junit.Test; import org.junit.Assert; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.SetType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.io.util.DataInputBuffer; @@ -134,21 +136,77 @@ public class ColumnsTest { List names = new ArrayList<>(); for (int i = 0; i < 50; i++) - names.add("clustering_" + i); + names.add("regular_" + i); List defs = new ArrayList<>(); - addClustering(names, defs); + addRegular(names, defs); Columns columns = Columns.from(new HashSet<>(defs)); defs = new ArrayList<>(); - addClustering(names.subList(0, 8), defs); + addRegular(names.subList(0, 8), defs); Columns subset = Columns.from(new HashSet<>(defs)); Assert.assertTrue(columns.containsAll(subset)); } + @Test + public void testStaticColumns() + { + testColumns(ColumnMetadata.Kind.STATIC); + } + + @Test + public void testRegularColumns() + { + testColumns(ColumnMetadata.Kind.REGULAR); + } + + private void testColumns(ColumnMetadata.Kind kind) + { + List definitions = ImmutableList.of( + def("a", UTF8Type.instance, kind), + def("b", SetType.getInstance(UTF8Type.instance, true), kind), + def("c", UTF8Type.instance, kind), + def("d", SetType.getInstance(UTF8Type.instance, true), kind), + def("e", UTF8Type.instance, kind), + def("f", SetType.getInstance(UTF8Type.instance, true), kind), + def("g", UTF8Type.instance, kind), + def("h", SetType.getInstance(UTF8Type.instance, true), kind) + ); + Columns columns = Columns.from(definitions); + + // test simpleColumnCount() + Assert.assertEquals(4, columns.simpleColumnCount()); + + // test simpleColumns() + List simpleColumnsExpected = + ImmutableList.of(definitions.get(0), definitions.get(2), definitions.get(4), definitions.get(6)); + List simpleColumnsActual = new ArrayList<>(); + Iterators.addAll(simpleColumnsActual, columns.simpleColumns()); + Assert.assertEquals(simpleColumnsExpected, simpleColumnsActual); + + // test complexColumnCount() + Assert.assertEquals(4, columns.complexColumnCount()); + + // test complexColumns() + List complexColumnsExpected = + ImmutableList.of(definitions.get(1), definitions.get(3), definitions.get(5), definitions.get(7)); + List complexColumnsActual = new ArrayList<>(); + Iterators.addAll(complexColumnsActual, columns.complexColumns()); + Assert.assertEquals(complexColumnsExpected, complexColumnsActual); + + // test size() + Assert.assertEquals(8, columns.size()); + + // test selectOrderIterator() + List columnsExpected = definitions; + List columnsActual = new ArrayList<>(); + Iterators.addAll(columnsActual, columns.selectOrderIterator()); + Assert.assertEquals(columnsExpected, columnsActual); + } + private void testSerializeSubset(ColumnsCheck input) throws IOException { testSerializeSubset(input.columns, input.columns, input.definitions); @@ -408,6 +466,11 @@ public class ColumnsTest results.add(ColumnMetadata.regularColumn(TABLE_METADATA, bytes(name), SetType.getInstance(UTF8Type.instance, true))); } + private static ColumnMetadata def(String name, AbstractType type, ColumnMetadata.Kind kind) + { + return new ColumnMetadata(TABLE_METADATA, bytes(name), type, ColumnMetadata.NO_POSITION, kind); + } + private static TableMetadata mock(Columns columns) { if (columns.isEmpty())