From 3b31bdb782c1a60a3aae59677fa9f1d7849846f4 Mon Sep 17 00:00:00 2001 From: Lukasz Antoniak Date: Wed, 2 Oct 2024 15:49:16 +0200 Subject: [PATCH 1/2] CASSANDRA-19931: Support OR operator and sub-conditions in query builder --- .../driver/api/querybuilder/QueryBuilder.java | 8 + .../relation/OngoingWhereClause.java | 15 ++ .../relation/DefaultSubConditionRelation.java | 161 ++++++++++++++++++ .../relation/LogicalRelation.java | 46 +++++ .../querybuilder/select/DefaultSelect.java | 4 +- .../api/querybuilder/BuildableQueryTest.java | 32 ++++ .../select/SelectOrderingTest.java | 98 +++++++++++ 7 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultSubConditionRelation.java create mode 100644 query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/LogicalRelation.java diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/QueryBuilder.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/QueryBuilder.java index 8df2b7efdd0..e70184b8cbc 100644 --- a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/QueryBuilder.java +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/QueryBuilder.java @@ -28,6 +28,7 @@ import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; import com.datastax.oss.driver.api.querybuilder.delete.DeleteSelection; import com.datastax.oss.driver.api.querybuilder.insert.InsertInto; +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; import com.datastax.oss.driver.api.querybuilder.relation.Relation; import com.datastax.oss.driver.api.querybuilder.select.SelectFrom; import com.datastax.oss.driver.api.querybuilder.select.Selector; @@ -43,6 +44,7 @@ import com.datastax.oss.driver.internal.querybuilder.DefaultRaw; import com.datastax.oss.driver.internal.querybuilder.delete.DefaultDelete; import com.datastax.oss.driver.internal.querybuilder.insert.DefaultInsert; +import com.datastax.oss.driver.internal.querybuilder.relation.DefaultSubConditionRelation; import com.datastax.oss.driver.internal.querybuilder.select.DefaultBindMarker; import com.datastax.oss.driver.internal.querybuilder.select.DefaultSelect; import com.datastax.oss.driver.internal.querybuilder.term.BinaryArithmeticTerm; @@ -538,4 +540,10 @@ public static Truncate truncate(@Nullable String keyspace, @NonNull String table return truncate( keyspace == null ? null : CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table)); } + + /** Creates new sub-condition in the WHERE clause, surrounded by parenthesis. */ + @NonNull + public static OngoingWhereClause subCondition() { + return new DefaultSubConditionRelation(true); + } } diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/relation/OngoingWhereClause.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/relation/OngoingWhereClause.java index 16b8072fdff..02d49649990 100644 --- a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/relation/OngoingWhereClause.java +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/relation/OngoingWhereClause.java @@ -27,6 +27,7 @@ import com.datastax.oss.driver.internal.querybuilder.relation.DefaultColumnRelationBuilder; import com.datastax.oss.driver.internal.querybuilder.relation.DefaultMultiColumnRelationBuilder; import com.datastax.oss.driver.internal.querybuilder.relation.DefaultTokenRelationBuilder; +import com.datastax.oss.driver.internal.querybuilder.relation.LogicalRelation; import edu.umd.cs.findbugs.annotations.CheckReturnValue; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Arrays; @@ -47,6 +48,20 @@ public interface OngoingWhereClause> { @CheckReturnValue SelfT where(@NonNull Relation relation); + /** Adds conjunction clause. Next relation is logically joined with AND. */ + @NonNull + @CheckReturnValue + default SelfT and() { + return where(LogicalRelation.AND); + } + + /** Adds alternative clause. Next relation is logically joined with OR. */ + @NonNull + @CheckReturnValue + default SelfT or() { + return where(LogicalRelation.OR); + } + /** * Adds multiple relations at once. All relations are logically joined with AND. * diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultSubConditionRelation.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultSubConditionRelation.java new file mode 100644 index 00000000000..c490850d340 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultSubConditionRelation.java @@ -0,0 +1,161 @@ +/* + * 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 com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder; +import com.datastax.oss.driver.api.querybuilder.BuildableQuery; +import com.datastax.oss.driver.api.querybuilder.CqlSnippet; +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DefaultSubConditionRelation + implements OngoingWhereClause, BuildableQuery, Relation { + + private final List relations; + private final boolean isSubCondition; + + /** Construct sub-condition relation with empty WHERE clause. */ + public DefaultSubConditionRelation(boolean isSubCondition) { + this.relations = new ArrayList<>(); + this.isSubCondition = isSubCondition; + } + + @NonNull + @Override + public DefaultSubConditionRelation where(@NonNull Relation relation) { + relations.add(relation); + return this; + } + + @NonNull + @Override + public DefaultSubConditionRelation where(@NonNull Iterable additionalRelations) { + for (Relation relation : additionalRelations) { + relations.add(relation); + } + return this; + } + + @NonNull + public DefaultSubConditionRelation withRelations(@NonNull List newRelations) { + relations.addAll(newRelations); + return this; + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder(); + + if (isSubCondition) { + builder.append("("); + } + appendWhereClause(builder, relations, isSubCondition); + if (isSubCondition) { + builder.append(")"); + } + + return builder.toString(); + } + + public static void appendWhereClause( + StringBuilder builder, List relations, boolean isSubCondition) { + boolean first = true; + for (int i = 0; i < relations.size(); ++i) { + CqlSnippet snippet = relations.get(i); + if (first && !isSubCondition) { + builder.append(" WHERE "); + } + first = false; + + snippet.appendTo(builder); + + boolean logicalOperatorAdded = false; + LogicalRelation logicalRelation = lookAheadNextRelation(relations, i, LogicalRelation.class); + if (logicalRelation != null) { + builder.append(" "); + logicalRelation.appendTo(builder); + builder.append(" "); + logicalOperatorAdded = true; + ++i; + } + if (!logicalOperatorAdded && i + 1 < relations.size()) { + builder.append(" AND "); + } + } + } + + private static T lookAheadNextRelation( + List relations, int position, Class clazz) { + if (position + 1 >= relations.size()) { + return null; + } + Relation relation = relations.get(position + 1); + if (relation.getClass().isAssignableFrom(clazz)) { + return (T) relation; + } + return null; + } + + @NonNull + @Override + public SimpleStatement build() { + return builder().build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Object... values) { + return builder().addPositionalValues(values).build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Map namedValues) { + SimpleStatementBuilder builder = builder(); + for (Map.Entry entry : namedValues.entrySet()) { + builder.addNamedValue(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + @Override + public String toString() { + return asCql(); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(asCql()); + } + + @Override + public boolean isIdempotent() { + for (Relation relation : relations) { + if (!relation.isIdempotent()) { + return false; + } + } + return true; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/LogicalRelation.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/LogicalRelation.java new file mode 100644 index 00000000000..a464035775b --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/LogicalRelation.java @@ -0,0 +1,46 @@ +/* + * 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 com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class LogicalRelation implements Relation { + public static final LogicalRelation AND = new LogicalRelation("AND"); + public static final LogicalRelation OR = new LogicalRelation("OR"); + + private final String operator; + + public LogicalRelation(@NonNull String operator) { + Preconditions.checkNotNull(operator); + this.operator = operator; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(operator); + } + + @Override + public boolean isIdempotent() { + return true; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultSelect.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultSelect.java index 86a2a07a3f2..687f38f5b88 100644 --- a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultSelect.java +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultSelect.java @@ -28,6 +28,7 @@ import com.datastax.oss.driver.api.querybuilder.select.Selector; import com.datastax.oss.driver.internal.querybuilder.CqlHelper; import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.internal.querybuilder.relation.DefaultSubConditionRelation; import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; @@ -388,7 +389,8 @@ public String asCql() { builder.append(" FROM "); CqlHelper.qualify(keyspace, table, builder); - CqlHelper.append(relations, builder, " WHERE ", " AND ", null); + DefaultSubConditionRelation.appendWhereClause(builder, relations, false); + CqlHelper.append(groupByClauses, builder, " GROUP BY ", ",", null); boolean first = true; diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryTest.java index 875f957b2fb..e360aeaf32a 100644 --- a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryTest.java +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryTest.java @@ -22,6 +22,7 @@ import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.function; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.subCondition; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.tuple; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update; import static org.assertj.core.api.Assertions.assertThat; @@ -49,6 +50,37 @@ public static Object[][] sampleQueries() { "SELECT * FROM foo WHERE k=:k", true }, + { + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker("k")) + .or() + .whereColumn("l") + .isEqualTo(bindMarker("l")), + ImmutableMap.of("k", 1, "l", 2), + "SELECT * FROM foo WHERE k=:k OR l=:l", + true + }, + { + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker("k")) + .and() + .where( + subCondition() + .whereColumn("l") + .isEqualTo(bindMarker("l")) + .or() + .whereColumn("m") + .isEqualTo(bindMarker("m"))) + .whereColumn("n") + .isEqualTo(bindMarker("n")), + ImmutableMap.of("k", 1, "l", 2, "m", 3, "n", 4), + "SELECT * FROM foo WHERE k=:k AND (l=:l OR m=:m) AND n=:n", + true + }, { deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker("k")), ImmutableMap.of("k", 1), diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java index ff27fde4f8f..d7e30bcd2ae 100644 --- a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java @@ -22,6 +22,7 @@ import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.subCondition; import com.datastax.oss.driver.api.querybuilder.relation.Relation; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; @@ -46,6 +47,103 @@ public void should_generate_ordering_clauses() { .hasCql("SELECT * FROM foo WHERE k=1 ORDER BY c1 ASC,c2 DESC"); } + @Test + public void should_support_nested_sub_conditions() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .and() + .where( + subCondition() + .where(Relation.column("l").isEqualTo(literal(2))) + .where( + subCondition() + .where(Relation.column("m").isEqualTo(literal(3))) + .or() + .where(Relation.column("x").isEqualTo(literal(4))))) + .orderBy("c1", ASC)) + .hasCql("SELECT * FROM foo WHERE k=1 AND (l=2 AND (m=3 OR x=4)) ORDER BY c1 ASC"); + } + + @Test + public void should_generate_criteria_alternative() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .or() + .where(Relation.column("l").isEqualTo(literal(2))) + .orderBy("c1", ASC) + .orderBy("c2", DESC)) + .hasCql("SELECT * FROM foo WHERE k=1 OR l=2 ORDER BY c1 ASC,c2 DESC"); + } + + @Test + public void should_support_sub_condition() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .and() + .where( + subCondition() + .where(Relation.column("l").isEqualTo(literal(2))) + .or() + .where(Relation.column("m").isEqualTo(literal(3)))) + .orderBy("c1", ASC) + .orderBy("c2", DESC)) + .hasCql("SELECT * FROM foo WHERE k=1 AND (l=2 OR m=3) ORDER BY c1 ASC,c2 DESC"); + assertThat( + selectFrom("foo") + .all() + .where( + subCondition() + .where(Relation.column("l").isEqualTo(literal(2))) + .where(Relation.column("m").isEqualTo(literal(3)))) + .and() + .where(Relation.column("k").isEqualTo(literal(1))) + .orderBy("c1", ASC)) + .hasCql("SELECT * FROM foo WHERE (l=2 AND m=3) AND k=1 ORDER BY c1 ASC"); + assertThat( + selectFrom("foo") + .all() + .where( + subCondition() + .where(Relation.column("l").isEqualTo(literal(2))) + .where(Relation.column("m").isEqualTo(literal(3))) + .where(Relation.column("x").isEqualTo(literal(4)))) + .orderBy("c1", ASC)) + .hasCql("SELECT * FROM foo WHERE (l=2 AND m=3 AND x=4) ORDER BY c1 ASC"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .where( + subCondition() + .where(Relation.column("l").isEqualTo(literal(2))) + .where(Relation.column("m").isEqualTo(literal(3))) + .or() + .where(Relation.column("x").isEqualTo(literal(4))))) + .hasCql("SELECT * FROM foo WHERE k=1 AND (l=2 AND m=3 OR x=4)"); + } + + @Test + public void should_use_conjunction_as_default_logical_operator() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .where( + subCondition() + .where(Relation.column("l").isEqualTo(literal(2))) + .or() + .where(Relation.column("m").isEqualTo(literal(3)))) + .orderBy("c1", ASC) + .orderBy("c2", DESC)) + .hasCql("SELECT * FROM foo WHERE k=1 AND (l=2 OR m=3) ORDER BY c1 ASC,c2 DESC"); + } + @Test(expected = IllegalArgumentException.class) public void should_fail_when_provided_names_resolve_to_the_same_id() { selectFrom("foo") From 65a015e60de5b44e2fb3fdb6d2a28a3c4e123b27 Mon Sep 17 00:00:00 2001 From: Lukasz Antoniak Date: Fri, 4 Oct 2024 10:39:39 +0200 Subject: [PATCH 2/2] Test more complex expression --- .../api/querybuilder/select/SelectOrderingTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java index d7e30bcd2ae..a664e980dad 100644 --- a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java @@ -122,10 +122,15 @@ public void should_support_sub_condition() { .where( subCondition() .where(Relation.column("l").isEqualTo(literal(2))) + .or() .where(Relation.column("m").isEqualTo(literal(3))) .or() - .where(Relation.column("x").isEqualTo(literal(4))))) - .hasCql("SELECT * FROM foo WHERE k=1 AND (l=2 AND m=3 OR x=4)"); + .where( + subCondition() + .where(Relation.column("q").isEqualTo(literal(4))) + .and() + .where(Relation.column("p").isEqualTo(literal(5)))))) + .hasCql("SELECT * FROM foo WHERE k=1 AND (l=2 OR m=3 OR (q=4 AND p=5))"); } @Test