You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ma...@apache.org on 2025/01/14 18:17:43 UTC
(cassandra) branch trunk updated: AST library for CQL-based fuzz tests
This is an automated email from the ASF dual-hosted git repository.
maedhroz 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 3c1e10b74e AST library for CQL-based fuzz tests
3c1e10b74e is described below
commit 3c1e10b74e64b4fd9ca8bb203d503120c56f0e77
Author: Caleb Rackliffe <ca...@gmail.com>
AuthorDate: Mon Jan 13 18:05:10 2025 -0600
AST library for CQL-based fuzz tests
patch by Caleb Rackliffe; reviewed by David Capwell for CASSANDRA-20198
Co-authored-by: Caleb Rackliffe <ca...@gmail.com>
Co-authored-by: David Capwell <dc...@apache.org>
---
CHANGES.txt | 1 +
.../cassandra/cql3/ast/AssignmentOperator.java | 122 +++
test/unit/org/apache/cassandra/cql3/ast/Bind.java | 71 ++
.../apache/cassandra/cql3/ast/CQLFormatter.java | 128 ++++
.../cql3/ast/CQLFormatterPrettyPrintTest.java | 172 +++++
.../apache/cassandra/cql3/ast/CasCondition.java | 66 ++
test/unit/org/apache/cassandra/cql3/ast/Cast.java | 67 ++
.../cassandra/cql3/ast/CollectionAccess.java | 58 ++
.../org/apache/cassandra/cql3/ast/Conditional.java | 437 +++++++++++
.../apache/cassandra/cql3/ast/CreateIndexDDL.java | 271 +++++++
test/unit/org/apache/cassandra/cql3/ast/DDL.java | 23 +
.../org/apache/cassandra/cql3/ast/Element.java | 63 ++
.../org/apache/cassandra/cql3/ast/Elements.java | 32 +
.../org/apache/cassandra/cql3/ast/Expression.java | 35 +
.../cassandra/cql3/ast/ExpressionEvaluator.java | 146 ++++
.../apache/cassandra/cql3/ast/ExpressionTest.java | 125 ++++
.../apache/cassandra/cql3/ast/FunctionCall.java | 133 ++++
.../org/apache/cassandra/cql3/ast/Literal.java | 71 ++
.../org/apache/cassandra/cql3/ast/Mutation.java | 817 +++++++++++++++++++++
.../org/apache/cassandra/cql3/ast/Operator.java | 106 +++
.../org/apache/cassandra/cql3/ast/Reference.java | 182 +++++
.../cassandra/cql3/ast/ReferenceExpression.java | 31 +
.../unit/org/apache/cassandra/cql3/ast/Select.java | 417 +++++++++++
.../cassandra/cql3/ast/StandardVisitors.java | 50 ++
.../org/apache/cassandra/cql3/ast/Statement.java | 110 +++
.../unit/org/apache/cassandra/cql3/ast/Symbol.java | 182 +++++
.../apache/cassandra/cql3/ast/TableReference.java | 57 ++
test/unit/org/apache/cassandra/cql3/ast/Txn.java | 399 ++++++++++
.../org/apache/cassandra/cql3/ast/TypeHint.java | 84 +++
test/unit/org/apache/cassandra/cql3/ast/Value.java | 36 +
.../org/apache/cassandra/cql3/ast/Visitor.java | 159 ++++
31 files changed, 4651 insertions(+)
diff --git a/CHANGES.txt b/CHANGES.txt
index dddb461b53..f4f3d72b6f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.1
+ * AST library for CQL-based fuzz tests (CASSANDRA-20198)
* Support audit logging for JMX operations (CASSANDRA-20128)
* Enable sorting of nodetool status output (CASSANDRA-20104)
* Support downgrading after CMS is initialized (CASSANDRA-20145)
diff --git a/test/unit/org/apache/cassandra/cql3/ast/AssignmentOperator.java b/test/unit/org/apache/cassandra/cql3/ast/AssignmentOperator.java
new file mode 100644
index 0000000000..f72fceb817
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/AssignmentOperator.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.EnumSet;
+import java.util.stream.Stream;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.ListType;
+import org.apache.cassandra.db.marshal.MapType;
+import org.apache.cassandra.db.marshal.SetType;
+
+//TODO this is hacky as Mutation takes Expression then figures out where to put it...
+// this can only be applied to UPDATE ... SET
+public class AssignmentOperator implements Expression
+{
+ public enum Kind
+ {
+ ADD('+'),
+ SUBTRACT('-');
+
+ private final char value;
+
+ Kind(char value)
+ {
+ this.value = value;
+ }
+ }
+
+ public final Kind kind;
+ public final Expression right;
+
+ public AssignmentOperator(Kind kind, Expression right)
+ {
+ this.kind = kind;
+ this.right = right;
+ }
+
+ public static EnumSet<Kind> supportsOperators(AbstractType<?> type)
+ {
+ type = type.unwrap();
+ EnumSet<Kind> result = EnumSet.noneOf(Kind.class);
+ for (Operator.Kind supported : Operator.supportsOperators(type))
+ {
+ Kind kind = toKind(supported);
+ if (kind != null)
+ result.add(kind);
+ }
+ if (result.isEmpty())
+ {
+ if (type instanceof CollectionType && type.isMultiCell())
+ {
+ if (type instanceof SetType || type instanceof ListType)
+ return EnumSet.of(Kind.ADD, Kind.SUBTRACT);
+ if (type instanceof MapType)
+ {
+ // map supports subtract, but not map - map; only map - set!
+ // since this is annoying to support, for now dropping -
+ return EnumSet.of(Kind.ADD);
+ }
+ throw new AssertionError("Unexpected collection type: " + type);
+ }
+ }
+ return result;
+ }
+
+ private static Kind toKind(Operator.Kind kind)
+ {
+ switch (kind)
+ {
+ case ADD: return Kind.ADD;
+ case SUBTRACT: return Kind.SUBTRACT;
+ default: return null;
+ }
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append(' ').append(kind.value).append("= ");
+ right.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(right);
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return right.type();
+ }
+
+ @Override
+ public Expression visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ var r = right.visit(v);
+ if (r == right) return this;
+ return new AssignmentOperator(kind, r);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Bind.java b/test/unit/org/apache/cassandra/cql3/ast/Bind.java
new file mode 100644
index 0000000000..ac58dfc7df
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Bind.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.Int32Type;
+
+public class Bind implements Value
+{
+ private final Object value;
+ private final AbstractType<?> type;
+
+ public Bind(Object value, AbstractType<?> type)
+ {
+ this.value = value;
+ this.type = type;
+ }
+
+ public static Bind of(int value)
+ {
+ return new Bind(value, Int32Type.instance);
+ }
+
+ @Override
+ public Object value()
+ {
+ return value;
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return type;
+ }
+
+ @Override
+ public ByteBuffer valueEncoded()
+ {
+ return value instanceof ByteBuffer ? (ByteBuffer) value : ((AbstractType) type).decompose(value);
+ }
+
+ @Override
+ public Bind with(Object value, AbstractType<?> type)
+ {
+ return new Bind(value, type);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append('?');
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/CQLFormatter.java b/test/unit/org/apache/cassandra/cql3/ast/CQLFormatter.java
new file mode 100644
index 0000000000..a163e7ceec
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/CQLFormatter.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import static org.apache.cassandra.cql3.ast.Elements.newLine;
+
+public interface CQLFormatter
+{
+ void group(StringBuilder sb);
+ void endgroup(StringBuilder sb);
+
+ void subgroup(StringBuilder sb);
+ void endsubgroup(StringBuilder sb);
+
+ void section(StringBuilder sb);
+ void element(StringBuilder sb);
+
+ class PrettyPrint implements CQLFormatter
+ {
+ private static final int SPACE_PER_GROUP = 2;
+ private int indent;
+ private int subgroup = -1;
+
+ @Override
+ public void group(StringBuilder sb)
+ {
+ indent += SPACE_PER_GROUP;
+ }
+
+ @Override
+ public void endgroup(StringBuilder sb)
+ {
+ if (indent == 0)
+ throw new IllegalStateException("Unable to end group; more endgroup calls than group calls");
+ indent -= SPACE_PER_GROUP;
+ }
+
+ @Override
+ public void subgroup(StringBuilder sb)
+ {
+ if (subgroup != -1)
+ throw new IllegalStateException("Can not have nested subgroups");
+ int offset = sb.lastIndexOf("\n");
+ if (offset == -1)
+ throw new IllegalStateException("subgroup called without a previous section");
+ // offset is before the current indent, so this group will already account for indent
+ subgroup = indent;
+ indent = sb.length() - offset - 1;
+ }
+
+ @Override
+ public void endsubgroup(StringBuilder sb)
+ {
+ // this is for Txn.LET
+ indent = subgroup;
+ subgroup = -1;
+ }
+
+ @Override
+ public void section(StringBuilder sb)
+ {
+ newLine(sb, indent);
+ }
+
+ @Override
+ public void element(StringBuilder sb)
+ {
+ newLine(sb, indent);
+ }
+ }
+
+ enum None implements CQLFormatter
+ {
+ instance;
+
+ @Override
+ public void group(StringBuilder sb)
+ {
+
+ }
+
+ @Override
+ public void endgroup(StringBuilder sb)
+ {
+
+ }
+
+ @Override
+ public void subgroup(StringBuilder sb)
+ {
+
+ }
+
+ @Override
+ public void endsubgroup(StringBuilder sb)
+ {
+
+ }
+
+ @Override
+ public void section(StringBuilder sb)
+ {
+ sb.append(' ');
+ }
+
+ @Override
+ public void element(StringBuilder sb)
+ {
+ sb.append(' ');
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/CQLFormatterPrettyPrintTest.java b/test/unit/org/apache/cassandra/cql3/ast/CQLFormatterPrettyPrintTest.java
new file mode 100644
index 0000000000..f14336d779
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/CQLFormatterPrettyPrintTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import org.junit.Test;
+
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.schema.TableMetadata;
+import org.assertj.core.api.Assertions;
+
+public class CQLFormatterPrettyPrintTest
+{
+ private static final TableMetadata TBL1 = TableMetadata.builder("ks", "tbl")
+ .addPartitionKeyColumn("pk", Int32Type.instance)
+ .addClusteringColumn("ck", Int32Type.instance)
+ .addRegularColumn("v0", Int32Type.instance)
+ .addRegularColumn("v1", Int32Type.instance)
+ .build();
+ private static final Symbol pk = new Symbol("pk", Int32Type.instance);
+ private static final Symbol ck = new Symbol("ck", Int32Type.instance);
+ private static final Symbol v0 = new Symbol("v0", Int32Type.instance);
+ private static final Symbol v1 = new Symbol("v1", Int32Type.instance);
+
+ @Test
+ public void selectTest()
+ {
+ Select select = select();
+ Assertions.assertThat(select.toCQL(format())).isEqualTo("SELECT *\n" +
+ "FROM ks.tbl\n" +
+ "WHERE foo = ?\n" +
+ "LIMIT ?");
+ }
+
+ @Test
+ public void insertTest()
+ {
+ Mutation.InsertBuilder builder = Mutation.insert(TBL1);
+ builder.value("pk", 0);
+ builder.value("v1", 0);
+ builder.ttl(Literal.of(42));
+ Assertions.assertThat(builder.build().toCQL(format())).isEqualTo("INSERT INTO ks.tbl (pk, v1)\n" +
+ "VALUES (?, ?)\n" +
+ "USING TTL 42");
+ }
+
+ @Test
+ public void updatTest()
+ {
+ Mutation.UpdateBuilder builder = Mutation.update(TBL1);
+ builder.value("pk", 0);
+ builder.set("v0", 0);
+ builder.set("v1", 0);
+ builder.ttl(Literal.of(42));
+ Assertions.assertThat(builder.build().toCQL(format())).isEqualTo("UPDATE ks.tbl\n" +
+ "USING TTL 42\n" +
+ "SET\n" +
+ " v0=?,\n" +
+ " v1=?\n" +
+ "WHERE \n" +
+ " pk = ?");
+
+ }
+
+ @Test
+ public void updateWithInClause()
+ {
+ Mutation.UpdateBuilder builder = Mutation.update(TBL1);
+ builder.set("v0", 0);
+ builder.in("pk", Bind.of(42), Literal.of(78));
+ Assertions.assertThat(builder.build().toCQL(format())).isEqualTo("UPDATE ks.tbl\n" +
+ "SET\n" +
+ " v0=?\n" +
+ "WHERE \n" +
+ " pk IN (?, 78)");
+ }
+
+ @Test
+ public void updateWithBetweenClause()
+ {
+ Mutation.UpdateBuilder builder = Mutation.update(TBL1);
+ builder.set("v0", 0);
+ builder.value("pk", 0);
+ builder.between("ck", Bind.of(42), Literal.of(78));
+ Assertions.assertThat(builder.build().toCQL(format())).isEqualTo("UPDATE ks.tbl\n" +
+ "SET\n" +
+ " v0=?\n" +
+ "WHERE \n" +
+ " pk = ? AND \n" +
+ " ck BETWEEN ? AND 78");
+ }
+
+ @Test
+ public void updateWithIsClause()
+ {
+ Mutation.UpdateBuilder builder = Mutation.update(TBL1);
+ builder.set("v0", 0);
+ builder.value("pk", 0);
+ builder.is(new Symbol("ck", Int32Type.instance),
+ Conditional.Is.Kind.Null);
+ Assertions.assertThat(builder.build().toCQL(format())).isEqualTo("UPDATE ks.tbl\n" +
+ "SET\n" +
+ " v0=?\n" +
+ "WHERE \n" +
+ " pk = ? AND \n" +
+ " ck IS NULL");
+ }
+
+ @Test
+ public void deleteTest()
+ {
+ Mutation.DeleteBuilder builder = Mutation.delete(TBL1);
+ builder.column(v1, v0);
+ builder.value("pk", 0);
+ Assertions.assertThat(builder.build().toCQL(format())).isEqualTo("DELETE v1, v0\n" +
+ "FROM ks.tbl\n" +
+ "WHERE \n" +
+ " pk = ?");
+ }
+
+ @Test
+ public void txnTest()
+ {
+ Txn.Builder builder = Txn.builder();
+ builder.addLet("a", select());
+ builder.addLet("b", select());
+ builder.addReturn(select());
+ Assertions.assertThat(builder.build().toCQL(format())).isEqualTo("BEGIN TRANSACTION\n" +
+ " LET a = (SELECT *\n" +
+ " FROM ks.tbl\n" +
+ " WHERE foo = ?\n" +
+ " LIMIT ?);\n" +
+ " LET b = (SELECT *\n" +
+ " FROM ks.tbl\n" +
+ " WHERE foo = ?\n" +
+ " LIMIT ?);\n" +
+ " SELECT *\n" +
+ " FROM ks.tbl\n" +
+ " WHERE foo = ?\n" +
+ " LIMIT ?;\n" +
+ "COMMIT TRANSACTION");
+ }
+
+ private static CQLFormatter.PrettyPrint format()
+ {
+ return new CQLFormatter.PrettyPrint();
+ }
+
+ private static Select select()
+ {
+ return Select.builder()
+ .table("ks", "tbl")
+ .value("foo",42)
+ .limit(1)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/cql3/ast/CasCondition.java b/test/unit/org/apache/cassandra/cql3/ast/CasCondition.java
new file mode 100644
index 0000000000..9f8d5867e0
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/CasCondition.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.stream.Stream;
+
+public interface CasCondition extends Element
+{
+ enum Simple implements CasCondition
+ {
+ NotExists("IF NOT EXISTS"),
+ Exists("IF EXISTS");
+
+ private final String cql;
+
+ Simple(String s)
+ {
+ this.cql = s;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append(cql);
+ }
+ }
+
+ class IfCondition implements CasCondition
+ {
+ private final Conditional conditional;
+
+ public IfCondition(Conditional conditional)
+ {
+ this.conditional = conditional;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("IF ");
+ conditional.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(conditional);
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Cast.java b/test/unit/org/apache/cassandra/cql3/ast/Cast.java
new file mode 100644
index 0000000000..433ffd13c2
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Cast.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.stream.Stream;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public class Cast implements Expression
+{
+ private final Expression e;
+ private final AbstractType<?> type;
+
+ public Cast(Expression e, AbstractType<?> type)
+ {
+ this.e = e;
+ this.type = type;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("CAST (");
+ e.toCQL(sb, formatter);
+ sb.append(" AS ");
+ sb.append(type.asCQL3Type());
+ sb.append(")");
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(e);
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return type;
+ }
+
+ @Override
+ public Expression visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ Expression e = this.e.visit(v);
+ if (e == this.e) return this;
+ return new Cast(e, type);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/CollectionAccess.java b/test/unit/org/apache/cassandra/cql3/ast/CollectionAccess.java
new file mode 100644
index 0000000000..19c8176f6e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/CollectionAccess.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.stream.Stream;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public class CollectionAccess implements ReferenceExpression
+{
+ private final ReferenceExpression column;
+ private final ReferenceExpression element;
+ private final AbstractType<?> type;
+
+ public CollectionAccess(ReferenceExpression column, ReferenceExpression element, AbstractType<?> type)
+ {
+ this.column = column;
+ this.element = element;
+ this.type = type;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ column.toCQL(sb, formatter);
+ sb.append('[');
+ element.toCQL(sb, formatter);
+ sb.append(']');
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return type;
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(column, element);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Conditional.java b/test/unit/org/apache/cassandra/cql3/ast/Conditional.java
new file mode 100644
index 0000000000..61241adc51
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Conditional.java
@@ -0,0 +1,437 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import accord.utils.Invariants;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BooleanType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.Int32Type;
+
+public interface Conditional extends Expression
+{
+ @Override
+ default AbstractType<?> type()
+ {
+ return BooleanType.instance;
+ }
+
+ @Override
+ default Conditional visit(Visitor v)
+ {
+ return v.visit(this);
+ }
+
+ default List<Conditional> simplify()
+ {
+ return Collections.singletonList(this);
+ }
+
+ class Where implements Conditional
+ {
+ public enum Inequality
+ {
+ EQUAL("="),
+ NOT_EQUAL("!="),
+ GREATER_THAN(">"),
+ GREATER_THAN_EQ(">="),
+ LESS_THAN("<"),
+ LESS_THAN_EQ("<=");
+
+ public final String value;
+
+ Inequality(String value)
+ {
+ this.value = value;
+ }
+ }
+
+ public final Inequality kind;
+ public final Expression lhs;
+ public final Expression rhs;
+
+ private Where(Inequality kind, Expression lhs, Expression rhs)
+ {
+ this.kind = kind;
+ this.lhs = lhs;
+ this.rhs = rhs;
+ }
+
+ public static Where create(Inequality kind, Expression ref, Expression expression)
+ {
+ return new Where(kind, ref, expression);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ lhs.toCQL(sb, formatter);
+ sb.append(' ').append(kind.value).append(' ');
+ rhs.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(lhs, rhs);
+ }
+
+ @Override
+ public Conditional visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ var lhs = this.lhs.visit(v);
+ var rhs = this.rhs.visit(v);
+ if (lhs == this.lhs && rhs == this.rhs) return this;
+ return new Where(kind, lhs, rhs);
+ }
+ }
+
+ class Between implements Conditional
+ {
+ public final Expression ref, start, end;
+
+ public Between(Expression ref, Expression start, Expression end)
+ {
+ this.ref = ref;
+ this.start = start;
+ this.end = end;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ ref.toCQL(sb, formatter);
+ sb.append(" BETWEEN ");
+ start.toCQL(sb, formatter);
+ sb.append(" AND ");
+ end.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(ref, start, end);
+ }
+
+ @Override
+ public Conditional visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ Expression ref = this.ref.visit(v);
+ Expression start = this.start.visit(v);
+ Expression end = this.end.visit(v);
+ if (ref == this.ref && start == this.start && end == this.end) return this;
+ return new Between(ref, start, end);
+ }
+ }
+
+ class In implements Conditional
+ {
+ public final ReferenceExpression ref;
+ public final List<? extends Expression> expressions;
+
+ public In(ReferenceExpression ref, List<? extends Expression> expressions)
+ {
+ Invariants.checkArgument(!expressions.isEmpty());
+ this.ref = ref;
+ this.expressions = expressions;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ ref.toCQL(sb, formatter);
+ sb.append(" IN ");
+ sb.append('(');
+ for (Expression e : expressions)
+ {
+ e.toCQL(sb, formatter);
+ sb.append(", ");
+ }
+ sb.setLength(sb.length() - 2); // ", "
+ sb.append(')');
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ List<Element> es = new ArrayList<>(expressions.size() + 1);
+ es.add(ref);
+ es.addAll(expressions);
+ return es.stream();
+ }
+
+ @Override
+ public Conditional visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ var symbol = this.ref.visit(v);
+ boolean updated = symbol == this.ref;
+ List<Expression> expressions = new ArrayList<>(this.expressions.size());
+ for (Expression e : this.expressions)
+ {
+ var e2 = e.visit(v);
+ updated |= e == e2;
+ expressions.add(e2);
+ }
+ if (!updated) return this;
+ return new In(symbol, expressions);
+ }
+ }
+
+ class Is implements Conditional
+ {
+ public enum Kind
+ {
+ Null("NULL"),
+ NotNull("NOT NULL");
+
+ private final String cql;
+
+ Kind(String s)
+ {
+ this.cql = s;
+ }
+ }
+
+ public final ReferenceExpression reference;
+ public final Kind kind;
+
+ public Is(String symbol, Kind kind)
+ {
+ this(Symbol.unknownType(symbol), kind);
+ }
+
+ public Is(ReferenceExpression reference, Kind kind)
+ {
+ this.reference = reference;
+ this.kind = kind;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ reference.toCQL(sb, formatter);
+ sb.append(" IS ").append(kind.cql);
+ }
+ }
+
+ class And implements Conditional
+ {
+ public final Conditional left, right;
+
+ public And(Conditional left, Conditional right)
+ {
+ this.left = left;
+ this.right = right;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ left.toCQL(sb, formatter);
+ sb.append(" AND ");
+ right.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(left, right);
+ }
+
+ @Override
+ public Conditional visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ Conditional left = this.left.visit(v);
+ Conditional right = this.right.visit(v);
+ if (this.left == left && this.right == right) return this;
+ return new And(left, right);
+ }
+
+ @Override
+ public List<Conditional> simplify()
+ {
+ List<Conditional> result = new ArrayList<>();
+ result.addAll(left.simplify());
+ result.addAll(right.simplify());
+ return result;
+ }
+ }
+
+ interface EqBuilder<T extends EqBuilder<T>>
+ {
+ T value(Symbol symbol, Expression e);
+ default T value(Symbol symbol, Object value)
+ {
+ return value(symbol, new Bind(value, symbol.type()));
+ }
+
+ default T value(String symbol, int e)
+ {
+ return value(new Symbol(symbol, Int32Type.instance), Bind.of(e));
+ }
+ }
+
+ interface ConditionalBuilder<T extends ConditionalBuilder<T>> extends EqBuilder<T>
+ {
+
+ T where(Expression ref, Where.Inequality kind, Expression expression);
+ default T where(Expression ref, Where.Inequality kind, Object value)
+ {
+ return where(ref, kind, new Bind(value, ref.type()));
+ }
+
+ default <Type> T where(String name, Where.Inequality kind, Type value, AbstractType<Type> type)
+ {
+ return where(new Symbol(name, type), kind, new Bind(value, type));
+ }
+
+ default T where(String name, Where.Inequality kind, int value)
+ {
+ return where(name, kind, value, Int32Type.instance);
+ }
+
+ T between(Expression ref, Expression start, Expression end);
+
+ default T between(String name, Expression start, Expression end)
+ {
+ return between(new Symbol(name, start.type()), start, end);
+ }
+
+ T in(ReferenceExpression ref, List<? extends Expression> expressions);
+
+ default T in(ReferenceExpression ref, Expression... expressions)
+ {
+ return in(ref, Arrays.asList(expressions));
+ }
+
+ default T in(String name, Expression... expressions)
+ {
+ if (expressions == null || expressions.length == 0)
+ throw new IllegalArgumentException("expressions may not be empty");
+ return in(new Symbol(name, expressions[0].type()), expressions);
+ }
+
+ default T in(String name, int... values)
+ {
+ return in(name, Int32Type.instance, IntStream.of(values).boxed().collect(Collectors.toList()));
+ }
+
+ default <Type> T in(String name, AbstractType<Type> type, Type... values)
+ {
+ return in(name, type, Arrays.asList(values));
+ }
+
+ default <Type> T in(String name, AbstractType<Type> type, List<Type> values)
+ {
+ return in(new Symbol(name, type), values.stream().map(v -> new Bind(v, type)).collect(Collectors.toList()));
+ }
+
+ T is(Symbol ref, Is.Kind kind);
+
+ @Override
+ default T value(Symbol symbol, Expression e)
+ {
+ return where(symbol, Where.Inequality.EQUAL, e);
+ }
+
+ default T value(String symbol, int e)
+ {
+ return value(symbol, e, Int32Type.instance);
+ }
+
+ default T value(String symbol, ByteBuffer e)
+ {
+ return value(symbol, e, BytesType.instance);
+ }
+
+ default <Type> T value(String symbol, Type value, AbstractType<Type> type)
+ {
+ return value(new Symbol(symbol, type), new Bind(value, type));
+ }
+ }
+
+ class Builder implements ConditionalBuilder<Builder>
+ {
+ private final List<Conditional> sub = new ArrayList<>();
+
+ public boolean isEmpty()
+ {
+ return sub.isEmpty();
+ }
+
+ private Builder add(Conditional conditional)
+ {
+ sub.add(conditional);
+ return this;
+ }
+
+ @Override
+ public Builder where(Expression ref, Where.Inequality kind, Expression expression)
+ {
+ return add(Where.create(kind, ref, expression));
+ }
+
+ @Override
+ public Builder between(Expression ref, Expression start, Expression end)
+ {
+ return add(new Between(ref, start, end));
+ }
+
+ @Override
+ public Builder in(ReferenceExpression symbol, List<? extends Expression> expressions)
+ {
+ return add(new In(symbol, expressions));
+ }
+
+ @Override
+ public Builder is(Symbol ref, Is.Kind kind)
+ {
+ return add(new Is(ref, kind));
+ }
+
+ public Conditional build()
+ {
+ if (sub.isEmpty())
+ throw new IllegalStateException("Unable to build an empty Conditional");
+ if (sub.size() == 1)
+ return sub.get(0);
+ Conditional accum = sub.get(0);
+ for (int i = 1; i < sub.size(); i++)
+ accum = new And(accum, sub.get(i));
+ return accum;
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/CreateIndexDDL.java b/test/unit/org/apache/cassandra/cql3/ast/CreateIndexDDL.java
new file mode 100644
index 0000000000..252dd99bc9
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/CreateIndexDDL.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.cassandra.cql3.ast.Symbol.UnquotedSymbol;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.AsciiType;
+import org.apache.cassandra.db.marshal.BooleanType;
+import org.apache.cassandra.db.marshal.CompositeType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UUIDType;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+
+//TODO (now): replace with IndexMetadata? Rather than create a custom DDL type can just leverage the existing metadata like Table/Keyspace
+public class CreateIndexDDL implements Element
+{
+ public enum Version { V1, V2 }
+ public enum QueryType { Eq, Range }
+
+ public interface Indexer
+ {
+ enum Kind { legacy, sai }
+ Kind kind();
+ default boolean isCustom()
+ {
+ return kind() != Kind.legacy;
+ }
+ UnquotedSymbol name();
+ default UnquotedSymbol longName()
+ {
+ return name();
+ }
+ boolean supported(TableMetadata table, ColumnMetadata column);
+ default EnumSet<QueryType> supportedQueries(AbstractType<?> type)
+ {
+ return EnumSet.of(QueryType.Eq);
+ }
+ }
+
+ public static List<Indexer> supportedIndexers()
+ {
+ return Arrays.asList(LEGACY, SAI);
+ }
+
+ private static boolean standardSupported(TableMetadata metadata, ColumnMetadata col)
+ {
+ if (metadata.partitionKeyColumns().size() == 1 && col.isPartitionKey()) return false;
+ AbstractType<?> type = col.type.unwrap();
+ if (type.isUDT() && type.isMultiCell()) return false; // non-frozen UDTs are not supported
+ if (type.referencesDuration()) return false; // Duration is not allowed! See org.apache.cassandra.cql3.statements.schema.CreateIndexStatement.validateIndexTarget
+ return true;
+ }
+
+ private static boolean isFrozen(AbstractType<?> type)
+ {
+ return !type.subTypes().isEmpty() && !type.isMultiCell();
+ }
+
+ public static final Indexer LEGACY = new Indexer()
+ {
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.legacy;
+ }
+
+ @Override
+ public UnquotedSymbol name()
+ {
+ return new UnquotedSymbol("legacy_local_table", UTF8Type.instance);
+ }
+
+ @Override
+ public boolean supported(TableMetadata table, ColumnMetadata column)
+ {
+ return standardSupported(table, column);
+ }
+ };
+
+ private static final Set<AbstractType<?>> SAI_EQ_ONLY = ImmutableSet.of(UTF8Type.instance, AsciiType.instance,
+ BooleanType.instance,
+ UUIDType.instance);
+
+ public static final Indexer SAI = new Indexer()
+ {
+ @Override
+ public Kind kind()
+ {
+ return Kind.sai;
+ }
+
+ @Override
+ public UnquotedSymbol name()
+ {
+ return new UnquotedSymbol("SAI", UTF8Type.instance);
+ }
+
+ @Override
+ public UnquotedSymbol longName()
+ {
+ return new UnquotedSymbol("StorageAttachedIndex", UTF8Type.instance);
+ }
+
+ @Override
+ public boolean supported(TableMetadata table, ColumnMetadata col)
+ {
+ if (!standardSupported(table, col)) return false;
+ AbstractType<?> type = col.type.unwrap();
+ if (type instanceof CompositeType)
+ {
+ // each element must be SUPPORTED_TYPES only...
+ if (type.subTypes().stream().allMatch(StorageAttachedIndex.SUPPORTED_TYPES::contains))
+ return true;
+ }
+ else if (((isFrozen(type) && !type.isVector()) || StorageAttachedIndex.SUPPORTED_TYPES.contains(type.asCQL3Type())))
+ return true;
+ return false;
+ }
+
+ @Override
+ public EnumSet<QueryType> supportedQueries(AbstractType<?> type)
+ {
+ type = type.unwrap();
+ if (SAI_EQ_ONLY.contains(type))
+ return EnumSet.of(QueryType.Eq);
+ return EnumSet.allOf(QueryType.class);
+ }
+ };
+
+ public final Version version;
+ public final Indexer indexer;
+ public final Optional<Symbol> name;
+ public final TableReference on;
+ public final List<ReferenceExpression> references;
+ public final Map<String, String> options;
+
+ public CreateIndexDDL(Version version, Indexer indexer, Optional<Symbol> name, TableReference on, List<ReferenceExpression> references, Map<String, String> options)
+ {
+ this.version = version;
+ this.indexer = indexer;
+ this.name = name;
+ this.on = on;
+ this.references = references;
+ this.options = options;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ switch (version)
+ {
+ case V1:
+ if (indexer.isCustom()) sb.append("CREATE CUSTOM INDEX");
+ else sb.append("CREATE INDEX");
+ break;
+ case V2:
+ sb.append("CREATE INDEX");
+ break;
+ default:
+ throw new UnsupportedOperationException(version.name());
+ }
+ if (name.isPresent())
+ {
+ sb.append(' ');
+ name.get().toCQL(sb, formatter);
+ }
+ formatter.section(sb);
+ sb.append("ON ");
+ on.toCQL(sb, formatter);
+ sb.append('(');
+ for (ReferenceExpression ref : references)
+ {
+ ref.toCQL(sb, formatter);
+ sb.append(", ");
+ }
+ sb.setLength(sb.length() - 2); // remove last ", "
+ sb.append(')');
+ UnquotedSymbol indexerName = null;
+ switch (version)
+ {
+ case V1:
+ if (indexer.isCustom())
+ indexerName = indexer.longName();
+ break;
+ case V2:
+ indexerName = indexer.name();
+ break;
+ default:
+ throw new UnsupportedOperationException(version.name());
+ }
+ if (indexerName != null)
+ {
+ formatter.section(sb);
+ sb.append("USING '");
+ indexerName.toCQL(sb, formatter);
+ sb.append("'");
+ }
+ if (!options.isEmpty())
+ {
+ formatter.section(sb);
+ sb.append("WITH OPTIONS = {");
+ for (Map.Entry<String, String> e : options.entrySet())
+ sb.append("'").append(e.getKey()).append("': '").append(e.getValue()).append("', ");
+ sb.setLength(sb.length() - 2); // remove ", "
+ sb.append('}');
+ }
+ }
+
+ public static class CollectionReference implements ReferenceExpression
+ {
+ public enum Kind { FULL, KEYS, ENTRIES }
+
+ public final Kind kind;
+ public final ReferenceExpression column;
+
+ public CollectionReference(Kind kind, ReferenceExpression column)
+ {
+ this.kind = kind;
+ this.column = column;
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return column.type();
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append(kind.name()).append('(');
+ column.toCQL(sb, formatter);
+ sb.append(')');
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(column);
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/DDL.java b/test/unit/org/apache/cassandra/cql3/ast/DDL.java
new file mode 100644
index 0000000000..7575bec7a4
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/DDL.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+public interface DDL extends Statement
+{
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Element.java b/test/unit/org/apache/cassandra/cql3/ast/Element.java
new file mode 100644
index 0000000000..05bcd92ce5
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Element.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.stream.Stream;
+
+public interface Element
+{
+ void toCQL(StringBuilder sb, CQLFormatter formatter);
+
+ default void toCQL(StringBuilder sb)
+ {
+ toCQL(sb, CQLFormatter.None.instance);
+ }
+
+ default String toCQL()
+ {
+ StringBuilder sb = new StringBuilder();
+ toCQL(sb, CQLFormatter.None.instance);
+ return sb.toString();
+ }
+
+ default String toCQL(CQLFormatter formatter)
+ {
+ StringBuilder sb = new StringBuilder();
+ toCQL(sb, formatter);
+ return sb.toString();
+ }
+
+ default Stream<? extends Element> stream()
+ {
+ return Stream.empty();
+ }
+
+ default Stream<? extends Element> streamRecursive()
+ {
+ return streamRecursive(false);
+ }
+
+ default Stream<? extends Element> streamRecursive(boolean includeSelf)
+ {
+ Stream<Element> stream = stream().flatMap(e -> Stream.concat(Stream.of(e), e.streamRecursive()));
+ if (includeSelf)
+ stream = Stream.concat(Stream.of(this), stream);
+ return stream;
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Elements.java b/test/unit/org/apache/cassandra/cql3/ast/Elements.java
new file mode 100644
index 0000000000..f750c4efc1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Elements.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+public final class Elements
+{
+ private Elements()
+ {}
+
+ public static void newLine(StringBuilder sb, int indent)
+ {
+ sb.append('\n');
+ for (int i = 0; i < indent; i++)
+ sb.append(' ');
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Expression.java b/test/unit/org/apache/cassandra/cql3/ast/Expression.java
new file mode 100644
index 0000000000..96bc41a78c
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Expression.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public interface Expression extends Element
+{
+ AbstractType<?> type();
+ default String name()
+ {
+ return toCQL();
+ }
+
+ default Expression visit(Visitor v)
+ {
+ return v.visit(this);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/ExpressionEvaluator.java b/test/unit/org/apache/cassandra/cql3/ast/ExpressionEvaluator.java
new file mode 100644
index 0000000000..8270b438f4
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/ExpressionEvaluator.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.Optional;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+
+import static java.util.Optional.of;
+
+public class ExpressionEvaluator
+{
+ public static Optional<Object> tryEval(Expression e)
+ {
+ if (e instanceof Value)
+ return of(((Value) e).value());
+ if (e instanceof TypeHint)
+ return tryEval(((TypeHint) e).e);
+ if (e instanceof Operator)
+ return tryEval((Operator) e);
+ return Optional.empty();
+ }
+
+ public static Optional<Object> tryEval(Operator e)
+ {
+ switch (e.kind)
+ {
+ case ADD:
+ {
+ var lhsOpt = tryEval(e.left);
+ var rhsOpt = tryEval(e.right);
+ if (lhsOpt.isEmpty() || rhsOpt.isEmpty())
+ return Optional.empty();
+ Object lhs = lhsOpt.get();
+ Object rhs = rhsOpt.get();
+ if (lhs instanceof Byte)
+ return of((byte) (((Byte) lhs) + ((Byte) rhs)));
+ if (lhs instanceof Short)
+ return of((short) (((Short) lhs) + ((Short) rhs)));
+ if (lhs instanceof Integer)
+ return of((int) (((Integer) lhs) + ((Integer) rhs)));
+ if (lhs instanceof Long)
+ return of((long) (((Long) lhs) + ((Long) rhs)));
+ if (lhs instanceof Float)
+ return of((float) (((Float) lhs) + ((Float) rhs)));
+ if (lhs instanceof Double)
+ return of((double) (((Double) lhs) + ((Double) rhs)));
+ if (lhs instanceof BigInteger)
+ return of(((BigInteger) lhs).add((BigInteger) rhs));
+ if (lhs instanceof BigDecimal)
+ return of(((BigDecimal) lhs).add((BigDecimal) rhs));
+ if (lhs instanceof String)
+ return of(lhs.toString() + rhs.toString());
+ throw new UnsupportedOperationException("Unexpected type: " + lhs.getClass());
+ }
+ case SUBTRACT:
+ {
+ var lhsOpt = tryEval(e.left);
+ var rhsOpt = tryEval(e.right);
+ if (lhsOpt.isEmpty() || rhsOpt.isEmpty())
+ return Optional.empty();
+ Object lhs = lhsOpt.get();
+ Object rhs = rhsOpt.get();
+ if (lhs instanceof Byte)
+ return of((byte) (((Byte) lhs) - ((Byte) rhs)));
+ if (lhs instanceof Short)
+ return of((short) (((Short) lhs) - ((Short) rhs)));
+ if (lhs instanceof Integer)
+ return of((int) (((Integer) lhs) - ((Integer) rhs)));
+ if (lhs instanceof Long)
+ return of((long) (((Long) lhs) - ((Long) rhs)));
+ if (lhs instanceof Float)
+ return of((float) (((Float) lhs) - ((Float) rhs)));
+ if (lhs instanceof Double)
+ return of((double) (((Double) lhs) - ((Double) rhs)));
+ if (lhs instanceof BigInteger)
+ return of(((BigInteger) lhs).subtract((BigInteger) rhs));
+ if (lhs instanceof BigDecimal)
+ return of(((BigDecimal) lhs).subtract((BigDecimal) rhs));
+ throw new UnsupportedOperationException("Unexpected type: " + lhs.getClass());
+ }
+ case MULTIPLY:
+ {
+ var lhsOpt = tryEval(e.left);
+ var rhsOpt = tryEval(e.right);
+ if (lhsOpt.isEmpty() || rhsOpt.isEmpty())
+ return Optional.empty();
+ Object lhs = lhsOpt.get();
+ Object rhs = rhsOpt.get();
+ if (lhs instanceof Byte)
+ return of((byte) (((Byte) lhs) * ((Byte) rhs)));
+ if (lhs instanceof Short)
+ return of((short) (((Short) lhs) * ((Short) rhs)));
+ if (lhs instanceof Integer)
+ return of((int) (((Integer) lhs) * ((Integer) rhs)));
+ if (lhs instanceof Long)
+ return of((long) (((Long) lhs) * ((Long) rhs)));
+ if (lhs instanceof Float)
+ return of((float) (((Float) lhs) * ((Float) rhs)));
+ if (lhs instanceof Double)
+ return of((double) ((Double) lhs) * ((Double) rhs));
+ if (lhs instanceof BigInteger)
+ return of(((BigInteger) lhs).multiply((BigInteger) rhs));
+ if (lhs instanceof BigDecimal)
+ return of(((BigDecimal) lhs).multiply((BigDecimal) rhs));
+ throw new UnsupportedOperationException("Unexpected type: " + lhs.getClass());
+ }
+ default:
+ throw new UnsupportedOperationException(e.kind.name());
+ }
+ }
+
+ public static Optional<ByteBuffer> tryEvalEncoded(Expression e)
+ {
+ return tryEval(e).map(v -> {
+ if (v instanceof ByteBuffer) return (ByteBuffer) v;
+ try
+ {
+ return ((AbstractType) e.type()).decompose(v);
+ }
+ catch (Throwable t)
+ {
+ throw t;
+ }
+ });
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/ExpressionTest.java b/test/unit/org/apache/cassandra/cql3/ast/ExpressionTest.java
new file mode 100644
index 0000000000..333f3dec8e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/ExpressionTest.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import org.junit.Test;
+
+import accord.utils.Gen;
+import accord.utils.Gens;
+import org.apache.cassandra.cql3.ast.Conditional.And;
+import org.apache.cassandra.cql3.ast.Conditional.Where;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.assertj.core.api.Assertions;
+
+import static accord.utils.Property.qt;
+
+public class ExpressionTest
+{
+ private static final Visitor.CompositeVisitor COMPOSITE_VISITOR = Visitor.CompositeVisitor.of(StandardVisitors.BIND_TO_LITERAL);
+
+ @Test
+ public void simple()
+ {
+ qt().forAll(Gens.random(), expressions()).check((rs, ex) -> {
+ Visitor v = rs.nextBoolean() ? StandardVisitors.BIND_TO_LITERAL : COMPOSITE_VISITOR;
+ Expression update = ex.visit(v);
+ assertNoBind(update);
+ });
+ }
+
+ @Test
+ public void select()
+ {
+ assertNoBind(selectWithBind().visit(StandardVisitors.BIND_TO_LITERAL));
+ }
+
+ @Test
+ public void txn()
+ {
+ Txn txn = Txn.builder()
+ .addLet("a", selectWithBind())
+ .addReturn(selectWithBind())
+ .build();
+ assertNoBind(txn.visit(StandardVisitors.BIND_TO_LITERAL));
+ }
+
+ private static Select selectWithBind()
+ {
+ return Select.builder()
+ .table("ks", "tbl")
+ .value("foo", 42)
+ .limit(1)
+ .build();
+ }
+
+ private static void assertNoBind(Element update)
+ {
+ Assertions.assertThat(update
+ .streamRecursive(true)
+ .filter(e -> e instanceof Bind)
+ .count()).isEqualTo(0);
+ }
+
+ private static Gen<Conditional> conditionals()
+ {
+ return rs -> {
+ // in / where / and
+ switch (rs.nextInt(0, 2))
+ {
+ case 0:
+ return new Conditional.In(new Symbol("col", Int32Type.instance), Gens.lists(expressions()).ofSizeBetween(1, 3).next(rs));
+ case 1:
+ return Where.create(Where.Inequality.EQUAL, new Symbol("col", Int32Type.instance), expressions().next(rs));
+ case 2:
+ {
+ var gen = conditionals();
+ return new And(gen.next(rs), gen.next(rs));
+ }
+ default:
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ private static Gen<Expression> expressions()
+ {
+ return rs -> {
+ var subExpression = expressions().filter(e -> e.type() == Int32Type.instance);
+ if (rs.decide(.1))
+ {
+ // operator
+ return new Operator(Operator.Kind.ADD, subExpression.next(rs), subExpression.next(rs));
+ }
+ if (rs.decide(.1))
+ {
+ return new AssignmentOperator(AssignmentOperator.Kind.ADD, subExpression.next(rs));
+ }
+ if (rs.decide(.1))
+ return new Cast(subExpression.next(rs), Int32Type.instance);
+ if (rs.decide(.1))
+ return new FunctionCall("fn", Gens.lists(subExpression).ofSizeBetween(1, 10).next(rs), Int32Type.instance);
+ if (rs.decide(.1))
+ return new TypeHint(subExpression.next(rs));
+ if (rs.decide(.1))
+ return conditionals().next(rs);
+ int value = rs.nextInt();
+ return rs.nextBoolean() ? Literal.of(value) : Bind.of(value);
+ };
+ }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/cql3/ast/FunctionCall.java b/test/unit/org/apache/cassandra/cql3/ast/FunctionCall.java
new file mode 100644
index 0000000000..afd8bac2a3
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/FunctionCall.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.LongType;
+
+public class FunctionCall implements Expression
+{
+ public final String name;
+ public final List<? extends Expression> arguments;
+ public final AbstractType<?> returnType;
+
+ public FunctionCall(String name, List<? extends Expression> arguments, AbstractType<?> returnType)
+ {
+ this.name = name;
+ this.arguments = arguments;
+ this.returnType = returnType;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append(name).append('(');
+ for (Expression e : arguments)
+ {
+ e.toCQL(sb, formatter);
+ sb.append(", ");
+ }
+ if (!arguments.isEmpty())
+ sb.setLength(sb.length() - 2);
+ sb.append(')');
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return arguments.stream();
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return returnType;
+ }
+
+ @Override
+ public String name()
+ {
+ return name;
+ }
+
+ @Override
+ public Expression visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ boolean updated = false;
+ List<Expression> as = new ArrayList<>(arguments.size());
+ for (Expression e : arguments)
+ {
+ u = e.visit(v);
+ if (u != e)
+ updated = true;
+ as.add(u);
+ }
+ if (!updated) return this;
+ return new FunctionCall(name, as, returnType);
+ }
+
+ public static FunctionCall writetime(String column, AbstractType<?> type)
+ {
+ return new FunctionCall("writetime", Collections.singletonList(new Symbol(column, type)), LongType.instance);
+ }
+
+ public static FunctionCall countStar()
+ {
+ return new FunctionCall("count(*)", Collections.emptyList(), LongType.instance);
+ }
+
+ public static FunctionCall count(String symbol)
+ {
+ return count(Symbol.unknownType(symbol));
+ }
+
+ public static FunctionCall count(Symbol symbol)
+ {
+ return new FunctionCall("count", Collections.singletonList(symbol), LongType.instance);
+ }
+
+ public static FunctionCall tokenByColumns(Symbol... columns)
+ {
+ return tokenByColumns(Arrays.asList(columns));
+ }
+
+ public static FunctionCall tokenByColumns(List<Symbol> columns)
+ {
+ return new FunctionCall("token", columns, BytesType.instance);
+ }
+
+ public static FunctionCall tokenByValue(Value... values)
+ {
+ return tokenByValue(Arrays.asList(values));
+ }
+
+ public static FunctionCall tokenByValue(List<? extends Value> values)
+ {
+ return new FunctionCall("token", values, BytesType.instance);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Literal.java b/test/unit/org/apache/cassandra/cql3/ast/Literal.java
new file mode 100644
index 0000000000..ad8e3ddc74
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Literal.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.Int32Type;
+
+public class Literal implements Value
+{
+ private final Object value;
+ private final AbstractType<?> type;
+
+ public Literal(Object value, AbstractType<?> type)
+ {
+ this.value = value;
+ this.type = type;
+ }
+
+ public static Literal of(int value)
+ {
+ return new Literal(value, Int32Type.instance);
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return type;
+ }
+
+ @Override
+ public Object value()
+ {
+ return value;
+ }
+
+ @Override
+ public ByteBuffer valueEncoded()
+ {
+ return value instanceof ByteBuffer ? (ByteBuffer) value : ((AbstractType) type).decompose(value);
+ }
+
+ @Override
+ public Literal with(Object value, AbstractType<?> type)
+ {
+ return new Literal(value, type);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append(type.asCQL3Type().toCQLLiteral(valueEncoded()));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Mutation.java b/test/unit/org/apache/cassandra/cql3/ast/Mutation.java
new file mode 100644
index 0000000000..729bb8127a
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Mutation.java
@@ -0,0 +1,817 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+
+public abstract class Mutation implements Statement
+{
+ public enum Kind
+ {INSERT, UPDATE, DELETE}
+
+ public final Kind kind;
+ public final TableReference table;
+
+ protected Mutation(Kind kind, TableReference table)
+ {
+ this.kind = kind;
+ this.table = table;
+ }
+
+ public abstract boolean isCas();
+
+ public final Kind mutationKind()
+ {
+ return kind;
+ }
+
+ @Override
+ public final Statement.Kind kind()
+ {
+ return Statement.Kind.MUTATION;
+ }
+
+ @Override
+ public String toString()
+ {
+ return detailedToString();
+ }
+
+ public static InsertBuilder insert(TableMetadata metadata)
+ {
+ return new InsertBuilder(metadata);
+ }
+
+ public static UpdateBuilder update(TableMetadata metadata)
+ {
+ return new UpdateBuilder(metadata);
+ }
+
+ public static DeleteBuilder delete(TableMetadata metadata)
+ {
+ return new DeleteBuilder(metadata);
+ }
+
+ public static LinkedHashSet<Symbol> toSet(Iterable<ColumnMetadata> columns)
+ {
+ LinkedHashSet<Symbol> set = new LinkedHashSet<>();
+ for (var col : columns)
+ set.add(Symbol.from(col));
+ return set;
+ }
+
+ private static void toCQLWhere(Conditional conditional, StringBuilder sb, CQLFormatter formatter)
+ {
+ formatter.section(sb);
+ sb.append("WHERE ");
+ List<Conditional> where = conditional.simplify();
+ formatter.group(sb);
+ for (var c : where)
+ {
+ formatter.element(sb);
+ c.toCQL(sb, formatter);
+ sb.append(" AND ");
+ }
+ sb.setLength(sb.length() - 5); // 5 = " AND "
+ formatter.endgroup(sb);
+ }
+
+ public static class TTL implements Element
+ {
+ public final Value value;
+
+ public TTL(Value value)
+ {
+ this.value = value;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("TTL ");
+ value.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(value);
+ }
+ }
+
+ public static class Timestamp implements Element
+ {
+ public final Value value;
+
+ public Timestamp(Value value)
+ {
+ this.value = value;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("TIMESTAMP ");
+ value.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(value);
+ }
+ }
+
+ public static class Using implements Element
+ {
+ public final Optional<TTL> ttl;
+ public final Optional<Timestamp> timestamp;
+
+ public Using(Optional<TTL> ttl, Optional<Timestamp> timestamp)
+ {
+ this.ttl = ttl;
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ if (ttl.isEmpty() && timestamp.isEmpty())
+ return;
+ sb.append("USING ");
+ if (ttl.isPresent())
+ ttl.get().toCQL(sb, formatter);
+ if (timestamp.isPresent())
+ {
+ if (ttl.isPresent())
+ sb.append(" AND ");
+ timestamp.get().toCQL(sb, formatter);
+ }
+ }
+ }
+
+ public static class Insert extends Mutation
+ {
+ public final LinkedHashMap<Symbol, Expression> values;
+ public final boolean ifNotExists;
+ public final Optional<Using> using;
+
+ public Insert(TableReference table, LinkedHashMap<Symbol, Expression> values, boolean ifNotExists, Optional<Using> using)
+ {
+ super(Mutation.Kind.INSERT, table);
+ this.values = values;
+ this.ifNotExists = ifNotExists;
+ this.using = using;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ // INSERT INTO [keyspace_name.] table_name (column_list)
+ sb.append("INSERT INTO ");
+ table.toCQL(sb, formatter);
+ sb.append(" (");
+ for (Symbol col : values.keySet())
+ {
+ col.toCQL(sb, formatter);
+ sb.append(", ");
+ }
+ sb.setLength(sb.length() - 2); // 2 = ", "
+ sb.append(")");
+ // VALUES (column_values)
+ formatter.section(sb);
+ sb.append("VALUES (");
+ for (Expression e : values.values())
+ {
+ e.toCQL(sb, formatter);
+ sb.append(", ");
+ }
+ sb.setLength(sb.length() - 2); // 2 = ", "
+ sb.append(")");
+ // [IF NOT EXISTS]
+ if (ifNotExists)
+ {
+ formatter.section(sb);
+ sb.append("IF NOT EXISTS");
+ }
+ // [USING TTL seconds | TIMESTAMP epoch_in_microseconds]
+ if (using.isPresent())
+ {
+ formatter.section(sb);
+ using.get().toCQL(sb, formatter);
+ }
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ List<Element> elements = new ArrayList<>(1 + (using.isPresent() ? 1 : 0) + (values.size() * 2));
+ elements.add(table);
+ elements.addAll(values.keySet());
+ elements.addAll(values.values());
+ if (using.isPresent())
+ elements.add(using.get());
+ return elements.stream();
+ }
+
+ @Override
+ public Statement visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ boolean updated = false;
+ LinkedHashMap<Symbol, Expression> copied = new LinkedHashMap<>(values.size());
+ for (var e : values.entrySet())
+ {
+ Symbol s = e.getKey();
+ Symbol s2 = s.visit("INSERT", v);
+ if (s != s2)
+ updated = true;
+ Expression ex = e.getValue();
+ Expression ex2 = ex.visit(v);
+ if (ex != ex2)
+ updated = true;
+ copied.put(s2, ex2);
+ }
+ if (!updated) return this;
+ return new Insert(table, copied, ifNotExists, using);
+ }
+
+ @Override
+ public boolean isCas()
+ {
+ return ifNotExists;
+ }
+ }
+
+ public static class Update extends Mutation
+ {
+ public final Optional<Using> using;
+ public final LinkedHashMap<Symbol, Expression> set;
+ public final Conditional where;
+ public final Optional<? extends CasCondition> casCondition;
+
+ public Update(TableReference table, Optional<Using> using, LinkedHashMap<Symbol, Expression> set, Conditional where, Optional<? extends CasCondition> casCondition)
+ {
+ super(Mutation.Kind.UPDATE, table);
+ this.using = using;
+ this.set = set;
+ this.where = where;
+ this.casCondition = casCondition;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ // UPDATE [keyspace_name.] table_name
+ sb.append("UPDATE ");
+ table.toCQL(sb, formatter);
+ // [USING TTL time_value | USING TIMESTAMP timestamp_value]
+ if (using.isPresent())
+ {
+ formatter.section(sb);
+ using.get().toCQL(sb, formatter);
+ }
+ // SET assignment [, assignment] . . .
+ formatter.section(sb);
+ sb.append("SET");
+ formatter.group(sb);
+ for (var e : set.entrySet())
+ {
+ formatter.element(sb);
+ e.getKey().toCQL(sb, formatter);
+ // when a AssignmentOperator the `=` is added there so don't add
+ //TODO this is super hacky...
+ if (!(e.getValue() instanceof AssignmentOperator))
+ sb.append('=');
+ e.getValue().toCQL(sb, formatter);
+ sb.append(",");
+ }
+ sb.setLength(sb.length() - 1); // 1 = ","
+ formatter.endgroup(sb);
+ // WHERE row_specification
+ toCQLWhere(this.where, sb, formatter);
+ // [IF EXISTS | IF condition [AND condition] . . .] ;
+ if (casCondition.isPresent())
+ {
+ formatter.section(sb);
+ casCondition.get().toCQL(sb, formatter);
+ }
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ List<Element> elements = new ArrayList<>(2 +
+ (using.isPresent() ? 1 : 0) +
+ (casCondition.isPresent() ? 1 : 0) +
+ (set.size() * 2));
+ elements.add(table);
+ if (using.isPresent())
+ elements.add(using.get());
+ for (var e : set.entrySet())
+ {
+ elements.add(e.getKey());
+ elements.add(e.getValue());
+ }
+ elements.add(where);
+ if (casCondition.isPresent())
+ elements.add(casCondition.get());
+ return elements.stream();
+ }
+
+ @Override
+ public Statement visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ boolean updated = false;
+ LinkedHashMap<Symbol, Expression> copied = new LinkedHashMap<>(set.size());
+ for (var e : set.entrySet())
+ {
+ Symbol s = e.getKey().visit("UPDATE", v);
+ if (s != e.getKey())
+ updated = true;
+ Expression ex = e.getValue().visit(v);
+ if (e.getValue() != ex)
+ updated = true;
+ copied.put(s, ex);
+ }
+ Conditional copiedWhere = where.visit(v);
+ if (where != copiedWhere)
+ updated = true;
+
+ if (!updated) return this;
+ return new Update(table, using, copied, copiedWhere, casCondition);
+ }
+
+ @Override
+ public boolean isCas()
+ {
+ return casCondition.isPresent();
+ }
+ }
+
+ public static class Delete extends Mutation
+ {
+ public final List<Symbol> columns;
+ public final Optional<Timestamp> using;
+ public final Conditional where;
+ public final Optional<? extends CasCondition> casCondition;
+
+ public Delete(List<Symbol> columns,
+ TableReference table,
+ Optional<Timestamp> using,
+ Conditional where,
+ Optional<? extends CasCondition> casCondition)
+ {
+ super(Mutation.Kind.DELETE, table);
+ this.columns = columns;
+ this.using = using;
+ this.where = where;
+ this.casCondition = casCondition;
+ }
+
+ /*
+DELETE [column_name (term)][, ...]
+FROM [keyspace_name.] table_name
+[USING TIMESTAMP timestamp_value]
+WHERE PK_column_conditions
+[IF EXISTS | IF static_column_conditions]
+ */
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ // DELETE [column_name (term)][, ...]
+ sb.append("DELETE");
+ if (!columns.isEmpty())
+ {
+ sb.append(" ");
+ for (var c : columns)
+ {
+ c.toCQL(sb, formatter);
+ sb.append(", ");
+ }
+ sb.setLength(sb.length() - 2); // 2 = ", "
+ }
+ // FROM [keyspace_name.] table_name
+ formatter.section(sb);
+ sb.append("FROM ");
+ table.toCQL(sb, formatter);
+ // [USING TIMESTAMP timestamp_value]
+ if (using.isPresent())
+ {
+ formatter.section(sb);
+ using.get().toCQL(sb, formatter);
+ }
+ // WHERE PK_column_conditions
+ toCQLWhere(this.where, sb, formatter);
+ // [IF EXISTS | IF static_column_conditions]
+ if (casCondition.isPresent())
+ {
+ formatter.section(sb);
+ casCondition.get().toCQL(sb, formatter);
+ }
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ List<Element> elements = new ArrayList<>(columns.size() + 4);
+ elements.addAll(columns);
+ elements.add(table);
+ if (using.isPresent())
+ elements.add(using.get());
+ elements.add(where);
+ if (casCondition.isPresent())
+ elements.add(casCondition.get());
+ return elements.stream();
+ }
+
+ @Override
+ public Statement visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this)
+ return u;
+ boolean updated = false;
+ List<Symbol> copiedColumns = new ArrayList<>(columns.size());
+ for (var s : columns)
+ {
+ var s2 = s.visit("DELETE", v);
+ if (s != s2)
+ updated = true;
+ copiedColumns.add(s2);
+ }
+ var copiedWhere = where.visit(v);
+ if (copiedWhere != where)
+ updated = true;
+
+ if (!updated) return this;
+ return new Delete(copiedColumns, table, using, copiedWhere, casCondition);
+ }
+
+ @Override
+ public boolean isCas()
+ {
+ return casCondition.isPresent();
+ }
+ }
+
+ public static abstract class BaseBuilder<T, B extends BaseBuilder<T, B>>
+ {
+ private final Kind kind;
+ private final TableMetadata metadata;
+ protected final LinkedHashSet<Symbol> partitionColumns, clusteringColumns, primaryColumns, regularAndStatic, allColumns;
+ private boolean includeKeyspace = true;
+ private final Set<Symbol> neededPks = new HashSet<>();
+
+ protected BaseBuilder(Kind kind, TableMetadata table)
+ {
+ this.kind = kind;
+ this.metadata = table;
+
+ // partition key is always required, so validate
+ this.partitionColumns = toSet(table.partitionKeyColumns());
+ this.clusteringColumns = toSet(table.clusteringColumns());
+ this.primaryColumns = new LinkedHashSet<>();
+ this.primaryColumns.addAll(partitionColumns);
+ this.primaryColumns.addAll(clusteringColumns);
+ this.regularAndStatic = new LinkedHashSet<>();
+ this.regularAndStatic.addAll(toSet(table.regularAndStaticColumns()));
+ this.allColumns = toSet(safeColumns(table));
+ neededPks.addAll(partitionColumns);
+ }
+
+ public static List<ColumnMetadata> safeColumns(TableMetadata metadata)
+ {
+ List<ColumnMetadata> columns = new ArrayList<>(metadata.columns().size());
+ metadata.allColumnsInSelectOrder().forEachRemaining(columns::add);
+ return columns;
+ }
+
+ public abstract T build();
+
+ protected void assertAllPksHaveEq()
+ {
+ if (neededPks.isEmpty())
+ return;
+ throw new IllegalStateException("Attempted to create a " + kind + " but not all partition columns have an equality condition; missing " + neededPks);
+ }
+
+ protected void maybePkEq(Expression symbol)
+ {
+ if (symbol instanceof Symbol)
+ pkEq((Symbol) symbol);
+ }
+
+ private void pkEq(Symbol symbol)
+ {
+ neededPks.remove(symbol);
+ }
+
+ public B includeKeyspace(boolean value)
+ {
+ this.includeKeyspace = value;
+ return (B) this;
+ }
+
+ public B includeKeyspace()
+ {
+ return includeKeyspace(true);
+ }
+
+ public B excludeKeyspace()
+ {
+ return includeKeyspace(false);
+ }
+
+ protected TableReference tableRef()
+ {
+ return includeKeyspace ? TableReference.from(metadata) : new TableReference(metadata.name);
+ }
+ }
+
+ public static class InsertBuilder extends BaseBuilder<Insert, InsertBuilder> implements Conditional.EqBuilder<InsertBuilder>
+ {
+ private final LinkedHashMap<Symbol, Expression> values = new LinkedHashMap<>();
+ private boolean ifNotExists = false;
+ private @Nullable TTL ttl;
+ private @Nullable Timestamp timestamp;
+
+ protected InsertBuilder(TableMetadata table)
+ {
+ super(Kind.INSERT, table);
+ }
+
+ public InsertBuilder ifNotExists()
+ {
+ ifNotExists = true;
+ return this;
+ }
+
+ public InsertBuilder timestamp(Value value)
+ {
+ this.timestamp = new Timestamp(value);
+ return this;
+ }
+
+ public InsertBuilder ttl(Value value)
+ {
+ this.ttl = new TTL(value);
+ return this;
+ }
+
+ public InsertBuilder ttl(int value)
+ {
+ return ttl(Bind.of(value));
+ }
+
+ @Override
+ public InsertBuilder value(Symbol ref, Expression e)
+ {
+ maybePkEq(ref);
+ values.put(ref, e);
+ return this;
+ }
+
+ @Override
+ public Insert build()
+ {
+ assertAllPksHaveEq();
+ return new Insert(tableRef(),
+ new LinkedHashMap<>(values),
+ ifNotExists,
+ (ttl == null && timestamp == null) ? Optional.empty() : Optional.of(new Using(Optional.ofNullable(ttl), Optional.ofNullable(timestamp))));
+ }
+ }
+
+ public static class UpdateBuilder extends BaseBuilder<Update, UpdateBuilder> implements Conditional.ConditionalBuilder<UpdateBuilder>
+ {
+ private @Nullable TTL ttl;
+ private @Nullable Timestamp timestamp;
+ private final LinkedHashMap<Symbol, Expression> set = new LinkedHashMap<>();
+ private final Conditional.Builder where = new Conditional.Builder();
+ private @Nullable CasCondition casCondition;
+
+ protected UpdateBuilder(TableMetadata table)
+ {
+ super(Kind.UPDATE, table);
+ }
+
+ public UpdateBuilder timestamp(Value value)
+ {
+ this.timestamp = new Timestamp(value);
+ return this;
+ }
+
+ public UpdateBuilder ttl(Value value)
+ {
+ this.ttl = new TTL(value);
+ return this;
+ }
+
+ public UpdateBuilder ttl(int value)
+ {
+ return ttl(Bind.of(value));
+ }
+
+ public UpdateBuilder ifExists()
+ {
+ casCondition = CasCondition.Simple.Exists;
+ return this;
+ }
+
+ public UpdateBuilder ifCondition(CasCondition condition)
+ {
+ casCondition = condition;
+ return this;
+ }
+
+ public UpdateBuilder set(Symbol column, Expression value)
+ {
+ if (!regularAndStatic.contains(column))
+ throw new IllegalArgumentException("Attempted to set a non regular or static column " + column + "; expected " + regularAndStatic);
+ set.put(column, value);
+ return this;
+ }
+
+ public UpdateBuilder set(String column, int value)
+ {
+ return set(new Symbol(column, Int32Type.instance), Bind.of(value));
+ }
+
+ @Override
+ public UpdateBuilder where(Expression ref, Conditional.Where.Inequality kind, Expression expression)
+ {
+ if (kind == Conditional.Where.Inequality.EQUAL)
+ maybePkEq(ref);
+ where.where(ref, kind, expression);
+ return this;
+ }
+
+ @Override
+ public UpdateBuilder between(Expression ref, Expression start, Expression end)
+ {
+ where.between(ref, start, end);
+ return this;
+ }
+
+ @Override
+ public UpdateBuilder in(ReferenceExpression ref, List<? extends Expression> expressions)
+ {
+ maybePkEq(ref);
+ where.in(ref, expressions);
+ return this;
+ }
+
+ @Override
+ public UpdateBuilder is(Symbol ref, Conditional.Is.Kind kind)
+ {
+ where.is(ref, kind);
+ return this;
+ }
+
+ @Override
+ public Update build()
+ {
+ assertAllPksHaveEq();
+ if (set.isEmpty())
+ throw new IllegalStateException("Unable to create an Update without a SET section; set function was never called");
+
+ return new Update(tableRef(),
+ (ttl == null && timestamp == null) ? Optional.empty() : Optional.of(new Using(Optional.ofNullable(ttl), Optional.ofNullable(timestamp))),
+ new LinkedHashMap<>(set),
+ where.build(),
+ Optional.ofNullable(casCondition));
+ }
+ }
+
+ public static class DeleteBuilder extends BaseBuilder<Delete, DeleteBuilder> implements Conditional.ConditionalBuilder<DeleteBuilder>
+ {
+ private final List<Symbol> columns = new ArrayList<>();
+ private @Nullable Timestamp timestamp = null;
+ private final Conditional.Builder where = new Conditional.Builder();
+ //TODO (now): casCondition
+ private @Nullable CasCondition casCondition;
+
+ public DeleteBuilder(TableMetadata table)
+ {
+ super(Kind.DELETE, table);
+ }
+
+ public DeleteBuilder ifExists()
+ {
+ casCondition = CasCondition.Simple.Exists;
+ return this;
+ }
+
+ public DeleteBuilder ifCondition(CasCondition condition)
+ {
+ casCondition = condition;
+ return this;
+ }
+
+ public List<Symbol> columns()
+ {
+ return Collections.unmodifiableList(columns);
+ }
+
+ public DeleteBuilder column(Symbol symbol)
+ {
+ if (!regularAndStatic.contains(symbol))
+ throw new IllegalArgumentException("Can not delete column " + symbol + "; only regular/static columns can be deleted, expected " + regularAndStatic);
+ columns.add(symbol);
+ return this;
+ }
+
+ public DeleteBuilder column(Symbol... symbols)
+ {
+ return column(Arrays.asList(symbols));
+ }
+
+ public DeleteBuilder column(List<Symbol> symbols)
+ {
+ symbols.forEach(this::column);
+ return this;
+ }
+
+ public DeleteBuilder timestamp(Value value)
+ {
+ this.timestamp = new Timestamp(value);
+ return this;
+ }
+
+ @Override
+ public DeleteBuilder where(Expression ref, Conditional.Where.Inequality kind, Expression expression)
+ {
+ if (kind == Conditional.Where.Inequality.EQUAL)
+ maybePkEq(ref);
+ where.where(ref, kind, expression);
+ return this;
+ }
+
+ @Override
+ public DeleteBuilder between(Expression ref, Expression start, Expression end)
+ {
+ where.between(ref, start, end);
+ return this;
+ }
+
+ @Override
+ public DeleteBuilder in(ReferenceExpression ref, List<? extends Expression> expressions)
+ {
+ maybePkEq(ref);
+ where.in(ref, expressions);
+ return this;
+ }
+
+ @Override
+ public DeleteBuilder is(Symbol ref, Conditional.Is.Kind kind)
+ {
+ where.is(ref, kind);
+ return this;
+ }
+
+ @Override
+ public Delete build()
+ {
+ assertAllPksHaveEq();
+ return new Delete(columns.isEmpty() ? Collections.emptyList() : new ArrayList<>(columns),
+ tableRef(),
+ Optional.ofNullable(timestamp),
+ where.build(),
+ Optional.ofNullable(casCondition));
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Operator.java b/test/unit/org/apache/cassandra/cql3/ast/Operator.java
new file mode 100644
index 0000000000..d0baa10ec0
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Operator.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.EnumSet;
+import java.util.stream.Stream;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.NumberType;
+import org.apache.cassandra.db.marshal.StringType;
+import org.apache.cassandra.db.marshal.TemporalType;
+
+public class Operator implements Expression
+{
+ public enum Kind
+ {
+ ADD('+'),
+ SUBTRACT('-'),
+ MULTIPLY('*');
+ //TODO support avoiding 42 / 0 and 42 % 0 as these will fail
+// DIVIDE('/'),
+// MOD('%');
+
+ private final char value;
+
+ Kind(char value)
+ {
+ this.value = value;
+ }
+ }
+
+ public final Kind kind;
+ public final Expression left;
+ public final Expression right;
+
+ public Operator(Kind kind, Expression left, Expression right)
+ {
+ this.kind = kind;
+ this.left = left;
+ this.right = right;
+ if (!left.type().equals(right.type()))
+ throw new IllegalArgumentException("Types do not match: left=" + left.type() + ", right=" + right.type());
+ }
+
+ public static EnumSet<Kind> supportsOperators(AbstractType<?> type)
+ {
+ //TODO (coverage): some operators exist but only for different inputs than (type, type)
+ // date and time both support +- duration; aka date + duration = date, time - duration = time (looks like all temporal types are this way)
+ type = type.unwrap();
+ // see org.apache.cassandra.cql3.functions.OperationFcts.OPERATION
+ if (type instanceof NumberType)
+ return EnumSet.allOf(Kind.class);
+ if (type instanceof TemporalType)
+ return EnumSet.noneOf(Kind.class); //TODO (coverage): not true, but requires the RHS to be duration type only
+ if (type instanceof StringType)
+ return EnumSet.of(Kind.ADD);
+ return EnumSet.noneOf(Kind.class);
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return left.type();
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ left.toCQL(sb, formatter);
+ sb.append(' ').append(kind.value).append(' ');
+ right.toCQL(sb, formatter);
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(left, right);
+ }
+
+ @Override
+ public Expression visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ var left = this.left.visit(v);
+ var right = this.right.visit(v);
+ if (left == this.left && right == this.right) return this;
+ return new Operator(kind, left, right);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Reference.java b/test/unit/org/apache/cassandra/cql3/ast/Reference.java
new file mode 100644
index 0000000000..fe836a05dd
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Reference.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import com.google.common.collect.ImmutableList;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BytesType;
+
+public class Reference implements ReferenceExpression
+{
+ public final List<ReferenceExpression> path;
+
+ private Reference(List<ReferenceExpression> path)
+ {
+ if (path.isEmpty())
+ throw new IllegalArgumentException("Reference may not be empty");
+ this.path = path;
+ if (path.stream().anyMatch(e -> e.toCQL().contains("\"\"")))
+ new Throwable().printStackTrace();
+ }
+
+ public static Reference of(ReferenceExpression top)
+ {
+ return new Reference(Collections.singletonList(Objects.requireNonNull(top)));
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return path.get(path.size() - 1).type();
+ }
+
+ public Builder unbuild()
+ {
+ return new Builder(path);
+ }
+
+ public Reference add(String symbol, AbstractType<?> subType)
+ {
+ return unbuild().add(symbol, subType).build();
+ }
+
+ public Reference add(Expression expression)
+ {
+ if (expression instanceof Reference)
+ {
+ Reference other = (Reference) expression;
+ List<ReferenceExpression> path = new ArrayList<>(this.path.size() + other.path.size());
+ path.addAll(this.path);
+ path.addAll(other.path);
+ return new Reference(path);
+ }
+ else if (expression instanceof Symbol)
+ {
+ return unbuild().add((Symbol) expression).build();
+ }
+ else
+ {
+ return unbuild().add(expression.name(), expression.type()).build();
+ }
+ }
+
+ public Reference lastAsCollection(Function<ReferenceExpression, CollectionAccess> fn)
+ {
+ List<ReferenceExpression> path = new ArrayList<>(this.path.size());
+ for (int i = 0; i < this.path.size() - 1; i++)
+ path.add(this.path.get(i));
+ ReferenceExpression last = this.path.get(this.path.size() - 1);
+ path.add(Objects.requireNonNull(fn.apply(last)));
+ return new Reference(path);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ path.forEach(p -> {
+ p.toCQL(sb, formatter);
+ sb.append('.');
+ });
+ sb.setLength(sb.length() - 1); // last .
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return path.stream();
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Reference elements = (Reference) o;
+ return Objects.equals(path, elements.path);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(path);
+ }
+
+ @Override
+ public String toString()
+ {
+ return toCQL();
+ }
+
+ public static class Builder
+ {
+ private final List<ReferenceExpression> path;
+
+ public Builder()
+ {
+ path = new ArrayList<>();
+ }
+
+ private Builder(List<ReferenceExpression> path)
+ {
+ this.path = new ArrayList<>(path.size() + 1);
+ this.path.addAll(path);
+ }
+
+ public Builder add(String name)
+ {
+ return add(new Symbol(name, BytesType.instance));
+ }
+
+ public Builder add(String symbol, AbstractType<?> subType)
+ {
+ return add(new Symbol(symbol, subType));
+ }
+
+ public Builder add(Symbol symbol)
+ {
+ path.add(Objects.requireNonNull(symbol));
+ return this;
+ }
+
+ public Builder add(String[] paths)
+ {
+ for (String name : paths)
+ path.add(new Symbol(name, BytesType.instance));
+ return this;
+ }
+
+ public Reference build()
+ {
+ switch (path.size())
+ {
+ case 0: throw new IllegalStateException("Unable to make a Reference from an empty path");
+ case 1: return Reference.of(path.get(0));
+ default: return new Reference(ImmutableList.copyOf(path));
+ }
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/ReferenceExpression.java b/test/unit/org/apache/cassandra/cql3/ast/ReferenceExpression.java
new file mode 100644
index 0000000000..7a99e030a4
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/ReferenceExpression.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+/**
+ * Marker interface of what is allowed in a reference
+ */
+public interface ReferenceExpression extends Expression
+{
+ @Override
+ default ReferenceExpression visit(Visitor v)
+ {
+ return this;
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Select.java b/test/unit/org/apache/cassandra/cql3/ast/Select.java
new file mode 100644
index 0000000000..224c8f6d56
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Select.java
@@ -0,0 +1,417 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+
+import com.google.common.collect.ImmutableList;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.schema.TableMetadata;
+
+public class Select implements Statement
+{
+ /*
+SELECT * | select_expression | DISTINCT partition //TODO DISTINCT
+FROM [keyspace_name.] table_name
+[WHERE partition_value
+ [AND clustering_filters
+ [AND static_filters]]]
+[ORDER BY PK_column_name ASC|DESC]
+[LIMIT N]
+[ALLOW FILTERING]
+ */
+ // select
+ public final List<Expression> selections;
+ // from
+ public final Optional<TableReference> source;
+ // where
+ public final Optional<Conditional> where;
+ public final Optional<OrderBy> orderBy;
+ public final Optional<Value> limit;
+ public final boolean allowFiltering;
+
+ public Select(List<Expression> selections)
+ {
+ this(selections, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
+ }
+
+ public Select(List<Expression> selections, Optional<TableReference> source, Optional<Conditional> where, Optional<OrderBy> orderBy, Optional<Value> limit)
+ {
+ this(selections, source, where, orderBy, limit, false);
+ }
+
+ public Select(List<Expression> selections, Optional<TableReference> source, Optional<Conditional> where, Optional<OrderBy> orderBy, Optional<Value> limit, boolean allowFiltering)
+ {
+ this.selections = selections;
+ this.source = source;
+ this.where = where;
+ this.orderBy = orderBy;
+ this.limit = limit;
+ this.allowFiltering = allowFiltering;
+
+ if (!source.isPresent())
+ {
+ if (where.isPresent())
+ throw new IllegalArgumentException("Can not have a WHERE clause when there isn't a FROM");
+ if (orderBy.isPresent())
+ throw new IllegalArgumentException("Can not have a ORDER BY clause when there isn't a FROM");
+ if (limit.isPresent())
+ throw new IllegalArgumentException("Can not have a LIMIT clause when there isn't a FROM");
+ if (allowFiltering)
+ throw new IllegalArgumentException("Can not have a ALLOW FILTERING clause when there isn't a FROM");
+ }
+ }
+
+ public static Builder builder()
+ {
+ return new Builder();
+ }
+
+ public Select withAllowFiltering()
+ {
+ return new Select(selections, source, where, orderBy, limit, true);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("SELECT ");
+ if (selections.isEmpty())
+ {
+ sb.append('*');
+ }
+ else
+ {
+ selections.forEach(s -> {
+ s.toCQL(sb, formatter);
+ sb.append(", ");
+ });
+ sb.setLength(sb.length() - 2); // last ', '
+ }
+ if (source.isPresent())
+ {
+ formatter.section(sb);
+ sb.append("FROM ");
+ source.get().toCQL(sb, formatter);
+ if (where.isPresent())
+ {
+ formatter.section(sb);
+ sb.append("WHERE ");
+ where.get().toCQL(sb, formatter);
+ }
+ if (orderBy.isPresent())
+ {
+ formatter.section(sb);
+ sb.append("ORDER BY ");
+ orderBy.get().toCQL(sb, formatter);
+ }
+ if (limit.isPresent())
+ {
+ formatter.section(sb);
+ sb.append("LIMIT ");
+ limit.get().toCQL(sb, formatter);
+ }
+
+ if (allowFiltering)
+ {
+ formatter.section(sb);
+ sb.append("ALLOW FILTERING");
+ }
+ }
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ List<Element> es = new ArrayList<>(selections.size()
+ + (source.isPresent() ? 1 : 0)
+ + (where.isPresent() ? 1 : 0)
+ + (orderBy.isPresent() ? 1 : 0)
+ + (limit.isPresent() ? 1 : 0));
+ es.addAll(selections);
+ if (source.isPresent())
+ es.add(source.get());
+ if (where.isPresent())
+ es.add(where.get());
+ if (orderBy.isPresent())
+ es.add(orderBy.get());
+ if (limit.isPresent())
+ es.add(limit.get());
+ return es.stream();
+ }
+
+ @Override
+ public String toString()
+ {
+ return detailedToString();
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.SELECT;
+ }
+
+ @Override
+ public Statement visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ boolean updated = false;
+ List<Expression> selections = new ArrayList<>(this.selections.size());
+ for (Expression e : this.selections)
+ {
+ Expression update = e.visit(v);
+ updated |= e != update;
+ selections.add(update);
+ }
+ Optional<Conditional> where;
+ if (this.where.isPresent())
+ {
+ var c = this.where.get();
+ var update = c.visit(v);
+ updated |= c != update;
+ where = Optional.ofNullable(update);
+ }
+ else
+ {
+ where = this.where;
+ }
+ Optional<Value> limit;
+ if (this.limit.isPresent())
+ {
+ var l = this.limit.get();
+ var update = l.visit(v);
+ updated |= l != update;
+ limit = Optional.ofNullable(update);
+ }
+ else
+ {
+ limit = this.limit;
+ }
+ if (!updated) return this;
+ return new Select(selections, source, where, orderBy, limit, allowFiltering);
+ }
+
+ public static class OrderBy implements Element
+ {
+ public enum Ordering
+ {ASC, DESC}
+
+ public final List<Ordered> ordered;
+
+ public OrderBy(List<Ordered> ordered)
+ {
+ if (ordered.isEmpty())
+ throw new IllegalArgumentException("Can not ORDER BY an empty list");
+ this.ordered = ordered;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ if (ordered.size() == 1)
+ {
+ ordered.get(0).toCQL(sb, formatter);
+ return;
+ }
+
+ String postfix = ", ";
+ for (Ordered o : ordered)
+ {
+ o.toCQL(sb, formatter);
+ sb.append(postfix);
+ }
+ sb.setLength(sb.length() - postfix.length());
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return ordered.stream();
+ }
+
+ public static class Ordered implements Element
+ {
+ public final ReferenceExpression expression;
+ public final Ordering ordering;
+
+ public Ordered(ReferenceExpression expression, Ordering ordering)
+ {
+ this.expression = expression;
+ this.ordering = ordering;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ expression.toCQL(sb, formatter);
+ sb.append(' ');
+ sb.append(ordering.name());
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(expression);
+ }
+ }
+
+ public static class Builder
+ {
+ private final List<Ordered> ordered = new ArrayList<>();
+
+ public boolean isEmpty()
+ {
+ return ordered.isEmpty();
+ }
+
+ public Builder add(ReferenceExpression expression, Ordering ordering)
+ {
+ ordered.add(new Ordered(expression, ordering));
+ return this;
+ }
+
+ public OrderBy build()
+ {
+ return new OrderBy(ImmutableList.copyOf(ordered));
+ }
+ }
+ }
+
+ public static class Builder implements Conditional.ConditionalBuilder<Builder>
+ {
+ @Nullable // null means wildcard
+ private List<Expression> selections = new ArrayList<>();
+ private Optional<TableReference> source = Optional.empty();
+ private Conditional.Builder where = new Conditional.Builder();
+ private OrderBy.Builder orderBy = new OrderBy.Builder();
+ private Optional<Value> limit = Optional.empty();
+ private boolean allowFiltering = false;
+
+ public Builder wildcard()
+ {
+ if (selections != null && !selections.isEmpty())
+ throw new IllegalStateException("Attempted to use * for selection but existing selections exist: " + selections);
+ selections = null;
+ return this;
+ }
+
+ public Builder columnSelection(String name, AbstractType<?> type)
+ {
+ return selection(new Symbol(name, type));
+ }
+
+ public Builder allowFiltering()
+ {
+ allowFiltering = true;
+ return this;
+ }
+
+ public Builder selection(Expression e)
+ {
+ if (selections == null)
+ throw new IllegalStateException("Unable to add '" + e.name() + "' as a selection as * was already requested");
+ selections.add(e);
+ return this;
+ }
+
+ public Builder table(TableReference ref)
+ {
+ source = Optional.of(ref);
+ return this;
+ }
+
+ public Builder table(String ks, String name)
+ {
+ return table(new TableReference(Optional.of(ks), name));
+ }
+
+ public Builder table(String name)
+ {
+ return table(new TableReference(name));
+ }
+
+ public Builder table(TableMetadata table)
+ {
+ return table(TableReference.from(table));
+ }
+
+ @Override
+ public Builder where(Expression ref, Conditional.Where.Inequality kind, Expression expression)
+ {
+ where.where(ref, kind, expression);
+ return this;
+ }
+
+ @Override
+ public Builder between(Expression ref, Expression start, Expression end)
+ {
+ where.between(ref, start, end);
+ return this;
+ }
+
+ @Override
+ public Builder in(ReferenceExpression ref, List<? extends Expression> expressions)
+ {
+ where.in(ref, expressions);
+ return this;
+ }
+
+ @Override
+ public Builder is(Symbol ref, Conditional.Is.Kind kind)
+ {
+ where.is(ref, kind);
+ return this;
+ }
+
+ public Builder orderByColumn(String name, AbstractType<?> type, OrderBy.Ordering ordering)
+ {
+ orderBy.add(new Symbol(name, type), ordering);
+ return this;
+ }
+
+ public Builder limit(Value limit)
+ {
+ this.limit = Optional.of(limit);
+ return this;
+ }
+
+ public Builder limit(int limit)
+ {
+ return limit(Bind.of(limit));
+ }
+
+ public Select build()
+ {
+ return new Select((selections == null || selections.isEmpty()) ? Collections.emptyList() : ImmutableList.copyOf(selections),
+ source,
+ where.isEmpty() ? Optional.empty() : Optional.of(where.build()),
+ orderBy.isEmpty() ? Optional.empty() : Optional.of(orderBy.build()),
+ limit,
+ allowFiltering);
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/StandardVisitors.java b/test/unit/org/apache/cassandra/cql3/ast/StandardVisitors.java
new file mode 100644
index 0000000000..2a76cd8097
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/StandardVisitors.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+public class StandardVisitors
+{
+ public static final Visitor BIND_TO_LITERAL = new Visitor()
+ {
+ @Override
+ public Value visit(Value v)
+ {
+ if (!(v instanceof Bind)) return v;
+ Bind b = (Bind) v;
+ return new Literal(b.value(), b.type());
+ }
+ };
+
+ public static final Visitor UNWRAP_TYPE_HINT = new Visitor()
+ {
+ @Override
+ public Expression visit(Expression e)
+ {
+ if (!(e instanceof TypeHint)) return e;
+ TypeHint hint = (TypeHint) e;
+ if (hint.type.equals(hint.e.type()))
+ return hint.e;
+ return e;
+ }
+ };
+
+ public static final Visitor.CompositeVisitor DEBUG = Visitor.CompositeVisitor.of(UNWRAP_TYPE_HINT, BIND_TO_LITERAL);
+
+ private StandardVisitors() {}
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Statement.java b/test/unit/org/apache/cassandra/cql3/ast/Statement.java
new file mode 100644
index 0000000000..ffcba03465
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Statement.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+public interface Statement extends Element
+{
+ enum Kind { SELECT, MUTATION, TXN }
+ Kind kind();
+
+ default Object[] binds()
+ {
+ return streamRecursive()
+ .filter(e -> e instanceof Bind)
+ .map(e -> ((Bind) e).value())
+ .toArray(Object[]::new);
+ }
+
+ default ByteBuffer[] bindsEncoded()
+ {
+ return streamRecursive()
+ .filter(e -> e instanceof Bind)
+ .map(e -> ((Bind) e).valueEncoded())
+ .toArray(ByteBuffer[]::new);
+ }
+
+ default String detailedToString()
+ {
+ Object[] binds = binds();
+ return "CQL:\n" + toCQL() + "\nBinds:\n" + IntStream.range(0, binds.length)
+ .mapToObj(i -> i + " -> " + binds[i].getClass().getCanonicalName() + "(" + normalize(binds[i]) + ")")
+ .collect(Collectors.joining("\n"));
+ }
+
+ Statement visit(Visitor v);
+
+ static boolean hasByteBuffer(Object value)
+ {
+ if (value == null)
+ return false;
+ if (value instanceof ByteBuffer)
+ return true;
+ else if (value instanceof Collection)
+ {
+ Collection<Object> collection = (Collection<Object>) value;
+ return collection.stream().anyMatch(Statement::hasByteBuffer);
+ }
+ else if (value instanceof Map)
+ {
+ Map<Object, Object> map = (Map<Object, Object>) value;
+ return map.entrySet().stream().anyMatch(e -> hasByteBuffer(e.getKey()) || hasByteBuffer(e.getValue()));
+ }
+ return false;
+ }
+
+ static String normalize(Object value)
+ {
+ if (value == null)
+ return null;
+ if (value instanceof ByteBuffer)
+ {
+ ByteBuffer bb = (ByteBuffer) value;
+ if (bb.remaining() > 100)
+ {
+ bb = bb.duplicate();
+ bb.limit(bb.position() + 100);
+ return ByteBufferUtil.bytesToHex(bb) + "...";
+ }
+ return ByteBufferUtil.bytesToHex(bb);
+ }
+ else if (value instanceof Collection)
+ {
+ Collection<Object> collection = (Collection<Object>) value;
+ if (hasByteBuffer(collection))
+ return collection.stream().map(Statement::normalize).collect(Collectors.toList()).toString();
+ }
+ else if (value instanceof Map)
+ {
+ Map<Object, Object> map = (Map<Object, Object>) value;
+ if (hasByteBuffer(map))
+ return map.entrySet().stream()
+ .map(e -> normalize(e.getKey()) + " -> " + normalize(e.getValue()))
+ .collect(Collectors.joining(", ", "{", "}"));
+ }
+ return value.toString();
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Symbol.java b/test/unit/org/apache/cassandra/cql3/ast/Symbol.java
new file mode 100644
index 0000000000..f905218c45
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Symbol.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.ReservedKeywords;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.ReversedType;
+import org.apache.cassandra.schema.ColumnMetadata;
+
+public class Symbol implements ReferenceExpression, Comparable<Symbol>
+{
+ public final String symbol;
+ private final AbstractType<?> type;
+ public final boolean reversed;
+
+ public Symbol(ColumnMetadata column)
+ {
+ this(column.name.toString(), column.type);
+ }
+
+ public Symbol(String symbol, AbstractType<?> type)
+ {
+ this.symbol = Objects.requireNonNull(symbol);
+ this.type = Objects.requireNonNull(type).unwrap();
+ this.reversed = type.isReversed();
+ }
+
+ public static Symbol from(ColumnMetadata metadata)
+ {
+ return new Symbol(metadata.name.toString(), metadata.type);
+ }
+
+ public static Symbol unknownType(String name)
+ {
+ return new Symbol(name, BytesType.instance);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ maybeQuote(sb, symbol);
+ }
+
+ public static void maybeQuote(StringBuilder sb, String symbol)
+ {
+ sb.append(maybeQuote(symbol));
+ }
+
+ public static String maybeQuote(String symbol)
+ {
+ if (ReservedKeywords.isReserved(symbol))
+ {
+ return quote(symbol);
+ }
+ else
+ {
+ return ColumnIdentifier.maybeQuote(symbol);
+ }
+ }
+
+ private static final Pattern PATTERN_DOUBLE_QUOTE = Pattern.compile("\"", Pattern.LITERAL);
+ private static final String ESCAPED_DOUBLE_QUOTE = Matcher.quoteReplacement("\"\"");
+
+ //TODO refactor ColumnIdentifier to expose this
+ private static String quote(String text)
+ {
+ return '"' + PATTERN_DOUBLE_QUOTE.matcher(text).replaceAll(ESCAPED_DOUBLE_QUOTE) + '"';
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return type;
+ }
+
+ public AbstractType<?> rawType()
+ {
+ if (reversed) return ReversedType.getInstance(type);
+ return type;
+ }
+
+ @Override
+ public String name()
+ {
+ return symbol;
+ }
+
+ /**
+ * Same as {@link #visit(Visitor)} but has the restriction that the return type may not be changed away from {@link Symbol}.
+ */
+ public Symbol visit(String type, Visitor v)
+ {
+ var u = v.visit(this);
+ if (u == this) return this;
+ if (!(u instanceof Symbol))
+ throw new IllegalStateException("Visitor converted a Symbol to " + u.getClass().getSimpleName() + "; this is not supported with " + type);
+ return (Symbol) u;
+ }
+
+ public String detailedName()
+ {
+ return symbol + " " + type.asCQL3Type() + (reversed ? " (reversed)" : "");
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Symbol symbol1 = (Symbol) o;
+ return Objects.equals(symbol, symbol1.symbol);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(symbol);
+ }
+
+ @Override
+ public String toString()
+ {
+ return toCQL();
+ }
+
+ @Override
+ public int compareTo(Symbol o)
+ {
+ return toCQL().compareTo(o.toCQL());
+ }
+
+ public static class UnquotedSymbol extends Symbol
+ {
+ public UnquotedSymbol(String symbol, AbstractType<?> type)
+ {
+ super(symbol, type);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append(symbol);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UnquotedSymbol symbol1 = (UnquotedSymbol) o;
+ return Objects.equals(symbol, symbol1.symbol);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(symbol);
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/TableReference.java b/test/unit/org/apache/cassandra/cql3/ast/TableReference.java
new file mode 100644
index 0000000000..c2d01200b4
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/TableReference.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.cassandra.schema.TableMetadata;
+
+public class TableReference implements Element
+{
+ public final Optional<String> keyspace;
+ public final String name;
+
+ public TableReference(String name)
+ {
+ this(Optional.empty(), name);
+ }
+
+ public TableReference(Optional<String> keyspace, String name)
+ {
+ this.keyspace = Objects.requireNonNull(keyspace);
+ this.name = Objects.requireNonNull(name);
+ }
+
+ public static TableReference from(TableMetadata metadata)
+ {
+ return new TableReference(Optional.of(metadata.keyspace), metadata.name);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ if (keyspace.isPresent())
+ {
+ Symbol.maybeQuote(sb, keyspace.get());
+ sb.append('.');
+ }
+ Symbol.maybeQuote(sb, name);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Txn.java b/test/unit/org/apache/cassandra/cql3/ast/Txn.java
new file mode 100644
index 0000000000..2e60574eac
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Txn.java
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.cassandra.cql3.FieldIdentifier;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.UserType;
+
+public class Txn implements Statement
+{
+ // lets
+ public final List<Let> lets;
+ // return
+ public final Optional<Select> output;
+ public final Optional<If> ifBlock;
+ public final List<Mutation> mutations;
+
+ public Txn(List<Let> lets, Optional<Select> output, Optional<If> ifBlock, List<Mutation> mutations)
+ {
+ this.lets = lets;
+ this.output = output;
+ this.ifBlock = ifBlock;
+ if (ifBlock.isPresent() && !mutations.isEmpty())
+ throw new IllegalArgumentException("Unable to define both IF and non-conditional mutations");
+ this.mutations = mutations;
+ }
+
+ public static Txn wrap(Select select)
+ {
+ return new Txn(Collections.emptyList(), Optional.of(select), Optional.empty(), Collections.emptyList());
+ }
+
+ public static Txn wrap(Mutation mutation)
+ {
+ return new Txn(Collections.emptyList(), Optional.empty(), Optional.empty(), Collections.singletonList(mutation));
+ }
+
+ public static Builder builder()
+ {
+ return new Builder();
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("BEGIN TRANSACTION");
+
+ formatter.group(sb);
+ stream().forEach(e -> {
+ formatter.section(sb);
+ e.toCQL(sb, formatter);
+ if (!(e instanceof If))
+ sb.append(';');
+ });
+ formatter.endgroup(sb);
+ formatter.section(sb);
+ sb.append("COMMIT TRANSACTION");
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ Stream<? extends Element> ret = lets.stream();
+ if (output.isPresent())
+ ret = Stream.concat(ret, Stream.of(output.get()));
+ if (ifBlock.isPresent())
+ ret = Stream.concat(ret, Stream.of(ifBlock.get()));
+ ret = Stream.concat(ret, mutations.stream());
+ return ret;
+ }
+
+ @Override
+ public String toString()
+ {
+ return toCQL();
+ }
+
+ @Override
+ public Kind kind()
+ {
+ return Kind.TXN;
+ }
+
+ @Override
+ public Statement visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ boolean updated = false;
+ List<Let> lets = new ArrayList<>(this.lets.size());
+ for (Let l : this.lets)
+ {
+ Statement update = l.select.visit(v);
+ if (!(update instanceof Select))
+ throw new IllegalArgumentException("Unable to use type " + update.getClass() + " in Let");
+ boolean localUpdated = update != l.select;
+ updated |= localUpdated;
+ lets.add(!localUpdated ? l : new Let(l.symbol, (Select) update));
+ }
+ Optional<Select> output;
+ if (this.output.isPresent())
+ {
+ Select select = this.output.get();
+ Statement update = select.visit(v);
+ if (!(update instanceof Select))
+ throw new IllegalArgumentException("Unable to use type " + update.getClass() + " in output");
+ boolean localUpdated = select != update;
+ updated |= localUpdated;
+ output = !localUpdated ? this.output : Optional.ofNullable((Select) update);
+ }
+ else
+ {
+ output = this.output;
+ }
+ Optional<If> ifBlock;
+ if (this.ifBlock.isPresent())
+ {
+ If block = this.ifBlock.get();
+ boolean localUpdated = false;
+ Conditional c = block.conditional.visit(v);
+ localUpdated |= c != block.conditional;
+ List<Mutation> mutations = new ArrayList<>(this.mutations.size());
+ for (Mutation m : block.mutations)
+ {
+ Statement update = m.visit(v);
+ if (!(update instanceof Mutation))
+ throw new IllegalArgumentException("Unable to use type " + update.getClass() + " where Mutation is expected");
+ updated |= update != m;
+ mutations.add((Mutation) update);
+ }
+ ifBlock = !localUpdated ? this.ifBlock : Optional.of(new If(c, mutations));
+ }
+ else
+ {
+ ifBlock = this.ifBlock;
+ }
+ List<Mutation> mutations = new ArrayList<>(this.mutations.size());
+ for (Mutation m : this.mutations)
+ {
+ Statement update = m.visit(v);
+ if (!(update instanceof Mutation))
+ throw new IllegalArgumentException("Unable to use type " + update.getClass() + " where Mutation is expected");
+ updated |= update != m;
+ mutations.add((Mutation) update);
+ }
+ if (!updated) return this;
+ return new Txn(lets, output, ifBlock, mutations);
+ }
+
+ private static void recursiveReferences(Collection<Reference> accum, Reference ref)
+ {
+ //NOTE: this doesn't actually collect the references due to lack of support in CQL within a Txn, need to fix those cases
+ //Coverage: Map/Set/List are a bit annoying as they use real values and not something known in the schema, so may need to handle that different in validation
+ AbstractType<?> type = ref.type().unwrap();
+ if (type.isCollection())
+ {
+ //TODO Caleb to add support for [] like normal read/write supports
+// if (type instanceof SetType)
+// {
+// // [value] syntax
+// SetType set = (SetType) type;
+// AbstractType subType = set.getElementsType();
+
+// }
+// else if (type instanceof MapType)
+// {
+// // [key] syntax
+// MapType map = (MapType) type;
+// AbstractType keyType = map.getKeysType();
+// AbstractType valueType = map.getValuesType();
+// }
+ // see Selectable.specForElementOrSlice; ListType is not supported
+ }
+ else if (type.isUDT())
+ {
+ //TODO Caleb to support multiple nesting
+// UserType udt = (UserType) type;
+// for (int i = 0; i < udt.size(); i++)
+// {
+// Reference subRef = ref.add(udt.fieldName(i).toString(), udt.type(i));
+// accum.add(subRef);
+// recursiveReferences(accum, subRef);
+// }
+ }
+ }
+
+ public static class Builder
+ {
+ private final Map<String, Select> lets = new LinkedHashMap<>();
+ // no type system so don't need easy lookup to Expression; just existence check
+ private final Set<Reference> allowedReferences = new HashSet<>();
+ private Optional<Select> output = Optional.empty();
+ private Optional<If> ifBlock = Optional.empty();
+ private final List<Mutation> mutations = new ArrayList<>();
+
+ public Set<Reference> allowedReferences()
+ {
+ return Collections.unmodifiableSet(allowedReferences);
+ }
+
+ public Map<String, Select> lets()
+ {
+ return Collections.unmodifiableMap(lets);
+ }
+
+ public boolean isEmpty()
+ {
+ // don't include output as 'BEGIN TRANSACTION SELECT "000000000000000010000"; COMMIT TRANSACTION' isn't valid
+// return lets.isEmpty();
+ // TransactionStatement defines empty as no SELECT or updates
+ return !output.isPresent() && !ifBlock.isPresent() && mutations.isEmpty();
+ }
+
+ public Builder addLet(String name, Select.Builder select)
+ {
+ return addLet(name, select.build());
+ }
+
+ public Builder addLet(String name, Select select)
+ {
+ if (lets.containsKey(name))
+ throw new IllegalArgumentException("Let name " + name + " already exists");
+ lets.put(name, select);
+
+ Reference ref = Reference.of(new Symbol.UnquotedSymbol(name, toNamedTuple(select)));
+ for (Expression e : select.selections)
+ addAllowedReference(ref.add(e));
+ return this;
+ }
+
+ private AbstractType<?> toNamedTuple(Select select)
+ {
+ //TODO don't rely on UserType...
+ List<FieldIdentifier> fieldNames = new ArrayList<>(select.selections.size());
+ List<AbstractType<?>> fieldTypes = new ArrayList<>(select.selections.size());
+ for (Expression e : select.selections)
+ {
+ fieldNames.add(FieldIdentifier.forQuoted(e.name()));
+ fieldTypes.add(e.type());
+ }
+ return new UserType(null, null, fieldNames, fieldTypes, false);
+ }
+
+ private Builder addAllowedReference(Reference ref)
+ {
+ allowedReferences.add(ref);
+ recursiveReferences(allowedReferences, ref);
+ return this;
+ }
+
+ public Builder addReturn(Select select)
+ {
+ output = Optional.of(select);
+ return this;
+ }
+
+ public Builder addReturnReferences(String... names)
+ {
+ Select.Builder builder = new Select.Builder();
+ for (String name : names)
+ builder.selection(ref(name));
+ addReturn(builder.build());
+ return this;
+ }
+
+ private Reference ref(String name)
+ {
+ if (!name.contains("."))
+ return Reference.of(new Symbol(name, BytesType.instance));
+ String[] splits = name.split("\\.");
+ Reference.Builder builder = new Reference.Builder();
+ builder.add(splits);
+ return builder.build();
+ }
+
+ public Builder addIf(If block)
+ {
+ ifBlock = Optional.of(block);
+ return this;
+ }
+
+ public Builder addIf(Conditional conditional, Mutation... mutations)
+ {
+ return addIf(conditional, Arrays.asList(mutations));
+ }
+
+ public Builder addIf(Conditional conditional, List<Mutation> mutations)
+ {
+ return addIf(new If(conditional, mutations));
+ }
+
+ public Builder addUpdate(Mutation mutation)
+ {
+ this.mutations.add(Objects.requireNonNull(mutation));
+ return this;
+ }
+
+ public Txn build()
+ {
+ List<Let> lets = this.lets.entrySet().stream().map(e -> new Let(e.getKey(), e.getValue())).collect(Collectors.toList());
+ return new Txn(lets, output, ifBlock, new ArrayList<>(mutations));
+ }
+ }
+
+ public static class If implements Element
+ {
+ private final Conditional conditional;
+ private final List<Mutation> mutations;
+
+ public If(Conditional conditional, List<Mutation> mutations)
+ {
+ this.conditional = conditional;
+ this.mutations = mutations;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("IF ");
+ conditional.toCQL(sb, formatter);
+ sb.append(" THEN");
+ formatter.group(sb);
+ for (Mutation mutation : mutations)
+ {
+ formatter.element(sb);
+ mutation.toCQL(sb, formatter);
+ sb.append(';');
+ }
+ formatter.endgroup(sb);
+ formatter.section(sb);
+ sb.append("END IF");
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.concat(Stream.of(conditional), mutations.stream());
+ }
+ }
+
+ public static class Let implements Element
+ {
+ public final String symbol;
+ public final Select select;
+
+ public Let(String symbol, Select select)
+ {
+ this.symbol = symbol;
+ this.select = select;
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append("LET ").append(symbol).append(" = (");
+ formatter.subgroup(sb);
+ select.toCQL(sb, formatter);
+ formatter.endsubgroup(sb);
+ sb.append(")");
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(select);
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/TypeHint.java b/test/unit/org/apache/cassandra/cql3/ast/TypeHint.java
new file mode 100644
index 0000000000..d49f30c118
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/TypeHint.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.stream.Stream;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public class TypeHint implements Expression
+{
+ public final Expression e;
+ public final AbstractType<?> type;
+
+ public TypeHint(Expression e, AbstractType<?> type)
+ {
+ this.e = e;
+ this.type = type;
+ }
+
+ public TypeHint(Expression e)
+ {
+ this.e = e;
+ this.type = e.type();
+ }
+
+ /**
+ * {@code ? + ?} is not clear to parsing, so rather than assume the type (like we do for literals) we fail and ask
+ * the user to CAST... so need to {@link TypeHint} when a {@link Bind} is found.
+ *
+ * Wait, {@link TypeHint} and not {@link Cast}? See CASSANDRA-17915...
+ */
+ public static Expression maybeApplyTypeHint(Expression e)
+ {
+ if (!(e instanceof Bind))
+ return e;
+ // see https://the-asf.slack.com/archives/CK23JSY2K/p1663788235000449
+ return new TypeHint(e);
+ }
+
+ @Override
+ public void toCQL(StringBuilder sb, CQLFormatter formatter)
+ {
+ sb.append('(').append(type.asCQL3Type()).append(") ");
+ e.toCQL(sb, formatter);
+ }
+
+ @Override
+ public AbstractType<?> type()
+ {
+ return type;
+ }
+
+ @Override
+ public Stream<? extends Element> stream()
+ {
+ return Stream.of(e);
+ }
+
+ @Override
+ public Expression visit(Visitor v)
+ {
+ var u = v.visit(this);
+ if (u != this) return u;
+ var e = this.e.visit(v);
+ if (e == this.e) return this;
+ return new TypeHint(e, type);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Value.java b/test/unit/org/apache/cassandra/cql3/ast/Value.java
new file mode 100644
index 0000000000..f04b5fcd36
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Value.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+
+public interface Value extends Expression
+{
+ Object value();
+ ByteBuffer valueEncoded();
+ Value with(Object value, AbstractType<?> type);
+
+ @Override
+ default Value visit(Visitor v)
+ {
+ return v.visit(this);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/ast/Visitor.java b/test/unit/org/apache/cassandra/cql3/ast/Visitor.java
new file mode 100644
index 0000000000..71e4797c25
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/ast/Visitor.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cql3.ast;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import accord.utils.Invariants;
+
+/**
+ * Visits and conditionally replaces an element in the ast. Each "visit" method offers the ability to be notified when the
+ * desired type is found in the ast tree, and allows the visitor to replace the current element (return the element unchanged
+ * if no mutations are desired, return a different element if a change is desired).
+ *
+ * For elements that support visitors, the following pattern should be respected.
+ * <ol>
+ * <li>check the top level element first
+ * {code}
+ * var u = visitor.visit(this);
+ * if (u != this) return u;
+ * {code}</li>
+ * <li>check each sub element; replacing the top level element if any sub-elements are changed
+ * {code}
+ * boolean updated = false;
+ * var a = a.visit(visitor);
+ * updated |= (a != this.a);
+ * ...
+ * return !updated ? this : copy...
+ * {code}
+ * </li>
+ * </ol>
+ */
+public interface Visitor
+{
+ default Statement visit(Statement s)
+ {
+ return s;
+ }
+
+ default Expression visit(Expression e)
+ {
+ if (e instanceof Value) return visit((Value) e);
+ if (e instanceof ReferenceExpression) return visit((ReferenceExpression) e);
+ return e;
+ }
+
+ default Conditional visit(Conditional c) { return c; }
+
+ default ReferenceExpression visit(ReferenceExpression r)
+ {
+ return r;
+ }
+
+ default Value visit(Value v) { return v; }
+
+ class CompositeVisitor implements Visitor
+ {
+ private final List<Visitor> visitors;
+
+ private CompositeVisitor(List<Visitor> visitors)
+ {
+ this.visitors = visitors;
+ }
+
+ public static CompositeVisitor of(Visitor... visitors)
+ {
+ return of(Arrays.asList(visitors));
+ }
+
+ public static CompositeVisitor of(List<Visitor> visitors)
+ {
+ Invariants.checkArgument(!visitors.isEmpty(), "Visitors may not be empty");
+
+ if (Stream.of(visitors).noneMatch(v -> v instanceof CompositeVisitor))
+ return new CompositeVisitor(visitors);
+ List<Visitor> flatten = new ArrayList<>();
+ for (Visitor v : visitors)
+ {
+ if (!(v instanceof CompositeVisitor))
+ {
+ flatten.add(v);
+ continue;
+ }
+ CompositeVisitor cv = (CompositeVisitor) v;
+ flatten.addAll(cv.visitors);
+ }
+ return new CompositeVisitor(flatten);
+ }
+
+ public CompositeVisitor append(Visitor v)
+ {
+ if (v instanceof CompositeVisitor)
+ {
+ CompositeVisitor other = (CompositeVisitor) v;
+ List<Visitor> vs = new ArrayList<>(visitors.size() + other.visitors.size());
+ vs.addAll(visitors);
+ vs.addAll(other.visitors);
+ return new CompositeVisitor(vs);
+ }
+ else
+ {
+ List<Visitor> vs = new ArrayList<>(visitors.size() + 1);
+ vs.addAll(visitors);
+ vs.add(v);
+ return new CompositeVisitor(vs);
+ }
+ }
+
+ @Override
+ public Expression visit(Expression e)
+ {
+ for (var v : visitors)
+ e = v.visit(e);
+ return e;
+ }
+
+ @Override
+ public Conditional visit(Conditional c)
+ {
+ for (var v : visitors)
+ c = v.visit(c);
+ return c;
+ }
+
+ @Override
+ public Statement visit(Statement s)
+ {
+ for (var v : visitors)
+ s = v.visit(s);
+ return s;
+ }
+
+ @Override
+ public Value visit(Value c)
+ {
+ for (var v : visitors)
+ c = v.visit(c);
+ return c;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org